diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp
index 3c76c1d..598b0ec 100644
--- a/crates/app/src/public/html/profile/settings.lisp
+++ b/crates/app/src/public/html/profile/settings.lisp
@@ -536,7 +536,7 @@
(icon (text "plus"))
(str (text "settings:label.generate_invite")))
- (text "{{ components::supporter_ad(body=\"Become a supporter to generate invite codes!\") }} {% for code in invites %}")
+ (text "{{ components::supporter_ad(body=\"Become a supporter to generate up to 24 invite codes! You can currently have 2 maximum.\") }} {% for code in invites %}")
(div
("class" "card flex flex-col gap-2")
(text "{% if code[1].is_used -%}")
@@ -669,7 +669,12 @@
(li
(text "Create infinite journals"))
(li
- (text "Create infinite notes in each journal")))
+ (text "Create infinite notes in each journal"))
+
+ (text "{% if config.security.enable_invite_codes -%}")
+ (li
+ (text "Create up to 24 invite codes"))
+ (text "{%- endif %}"))
(a
("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}")
("class" "button")
diff --git a/crates/app/src/routes/api/v1/auth/mod.rs b/crates/app/src/routes/api/v1/auth/mod.rs
index f5d17f1..cff7d20 100644
--- a/crates/app/src/routes/api/v1/auth/mod.rs
+++ b/crates/app/src/routes/api/v1/auth/mod.rs
@@ -18,7 +18,7 @@ use axum::{
};
use axum_extra::extract::CookieJar;
use serde::Deserialize;
-use tetratto_core::model::{addr::RemoteAddr, permissions::FinePermission};
+use tetratto_core::model::addr::RemoteAddr;
use tetratto_shared::hash::hash;
use cf_turnstile::{SiteVerifyRequest, TurnstileClient};
@@ -107,20 +107,20 @@ pub async fn register_request(
);
}
- let owner = match data.get_user_by_id(invite_code.owner).await {
- Ok(u) => u,
- Err(e) => return (None, Json(e.into())),
- };
+ // let owner = match data.get_user_by_id(invite_code.owner).await {
+ // Ok(u) => u,
+ // Err(e) => return (None, Json(e.into())),
+ // };
- if !owner.permissions.check(FinePermission::SUPPORTER) {
- return (
- None,
- Json(
- Error::MiscError("Invite code owner must be an active supporter".to_string())
- .into(),
- ),
- );
- }
+ // if !owner.permissions.check(FinePermission::SUPPORTER) {
+ // return (
+ // None,
+ // Json(
+ // Error::MiscError("Invite code owner must be an active supporter".to_string())
+ // .into(),
+ // ),
+ // );
+ // }
user.invite_code = invite_code.id;
diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs
index 927b7fb..49f2713 100644
--- a/crates/core/src/database/auth.rs
+++ b/crates/core/src/database/auth.rs
@@ -420,6 +420,17 @@ impl DataManager {
return Err(Error::DatabaseError(e.to_string()));
}
+ // delete invite codes
+ let res = execute!(
+ &conn,
+ "DELETE FROM invite_codes WHERE owner = $1",
+ &[&(id as i64)]
+ );
+
+ if let Err(e) = res {
+ return Err(Error::DatabaseError(e.to_string()));
+ }
+
// delete message reactions
let res = execute!(
&conn,
@@ -479,6 +490,16 @@ impl DataManager {
self.delete_poll(poll.id, &user).await?;
}
+ // free up invite code
+ if self.0.0.security.enable_invite_codes {
+ if user.invite_code != 0 && self.get_invite_code_by_id(user.invite_code).await.is_ok() {
+ // we're checking if the code is ok because the owner might've deleted their account,
+ // deleting all of their invite codes as well
+ self.update_invite_code_is_used(user.invite_code, false)
+ .await?;
+ }
+ }
+
// ...
Ok(())
}
diff --git a/crates/core/src/database/invite_codes.rs b/crates/core/src/database/invite_codes.rs
index 672e5a9..386149a 100644
--- a/crates/core/src/database/invite_codes.rs
+++ b/crates/core/src/database/invite_codes.rs
@@ -1,4 +1,5 @@
use oiseau::{cache::Cache, query_rows};
+use tetratto_shared::unix_epoch_timestamp;
use crate::model::{
Error, Result,
auth::{User, InviteCode},
@@ -67,7 +68,9 @@ impl DataManager {
Ok(out)
}
- const MAXIMUM_SUPPORTER_INVITE_CODES: usize = 15;
+ const MAXIMUM_FREE_INVITE_CODES: usize = 2;
+ const MAXIMUM_SUPPORTER_INVITE_CODES: usize = 24;
+ const MINIMUM_ACCOUNT_AGE_FOR_FREE_INVITE_CODES: usize = 2_629_800_000; // 1mo
/// Create a new invite_code in the database.
///
@@ -75,8 +78,26 @@ impl DataManager {
/// * `data` - a mock [`InviteCode`] object to insert
pub async fn create_invite_code(&self, data: InviteCode, user: &User) -> Result {
if !user.permissions.check(FinePermission::SUPPORTER) {
- return Err(Error::RequiresSupporter);
- } else {
+ // check account creation date
+ if unix_epoch_timestamp() - user.created
+ < Self::MINIMUM_ACCOUNT_AGE_FOR_FREE_INVITE_CODES
+ {
+ return Err(Error::MiscError(
+ "Your account is too young to do this".to_string(),
+ ));
+ }
+
+ // our account is old enough, but we need to make sure we don't already have
+ // 2 invite codes
+ if self.get_invite_codes_by_owner(user.id).await?.len()
+ >= Self::MAXIMUM_FREE_INVITE_CODES
+ {
+ return Err(Error::MiscError(
+ "You already have the maximum number of invite codes you can create"
+ .to_string(),
+ ));
+ }
+ } else if !user.permissions.check(FinePermission::MANAGE_INVITES) {
// check count
if self.get_invite_codes_by_owner(user.id).await?.len()
>= Self::MAXIMUM_SUPPORTER_INVITE_CODES