tetratto/crates/core/src/database/invite_codes.rs

208 lines
6.9 KiB
Rust
Raw Normal View History

use oiseau::{cache::Cache, query_row, query_rows};
use tetratto_shared::unix_epoch_timestamp;
2025-06-22 13:03:02 -04:00
use crate::model::{
Error, Result,
auth::{User, InviteCode},
permissions::FinePermission,
};
use crate::{auto_method, DataManager};
use oiseau::{PostgresRow, execute, get, params};
impl DataManager {
/// Get a [`InviteCode`] from an SQL row.
pub(crate) fn get_invite_code_from_row(x: &PostgresRow) -> InviteCode {
InviteCode {
id: get!(x->0(i64)) as usize,
created: get!(x->1(i64)) as usize,
owner: get!(x->2(i64)) as usize,
code: get!(x->3(String)),
is_used: get!(x->4(i32)) as i8 == 1,
}
}
2025-07-03 21:56:21 -04:00
auto_method!(get_invite_code_by_id()@get_invite_code_from_row -> "SELECT * FROM invite_codes WHERE id = $1" --name="invite code" --returns=InviteCode --cache-key-tmpl="atto.invite_code:{}");
auto_method!(get_invite_code_by_code(&str)@get_invite_code_from_row -> "SELECT * FROM invite_codes WHERE code = $1" --name="invite code" --returns=InviteCode);
2025-06-22 13:03:02 -04:00
/// Get invite_codes by `owner`.
pub async fn get_invite_codes_by_owner(
&self,
owner: usize,
batch: usize,
page: usize,
) -> Result<Vec<InviteCode>> {
2025-06-22 13:03:02 -04:00
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT * FROM invite_codes WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
&[&(owner as i64), &(batch as i64), &((page * batch) as i64)],
2025-06-22 13:03:02 -04:00
|x| { Self::get_invite_code_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("invite_code".to_string()));
}
Ok(res.unwrap())
}
/// Get invite_codes by `owner`.
pub async fn get_invite_codes_by_owner_count(&self, owner: usize) -> Result<i32> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_row!(
&conn,
"SELECT COUNT(*)::int FROM invite_codes WHERE owner = $1",
&[&(owner as i64)],
|x| Ok(x.get::<usize, i32>(0))
);
if res.is_err() {
return Err(Error::GeneralNotFound("invite_code".to_string()));
}
Ok(res.unwrap())
}
2025-06-22 13:03:02 -04:00
/// Fill a vector of invite codes with the user that used them.
pub async fn fill_invite_codes(
&self,
codes: Vec<InviteCode>,
) -> Result<Vec<(Option<User>, InviteCode)>> {
let mut out = Vec::new();
for code in codes {
if code.is_used {
out.push((
match self.get_user_by_invite_code(code.id as i64).await {
Ok(u) => Some(u),
Err(_) => None,
},
code,
))
} else {
out.push((None, code))
}
}
Ok(out)
}
2025-06-22 15:06:21 -04:00
const MAXIMUM_FREE_INVITE_CODES: usize = 4;
const MAXIMUM_SUPPORTER_INVITE_CODES: usize = 48;
2025-07-03 21:56:21 -04:00
const MINIMUM_ACCOUNT_AGE_FOR_INVITE_CODES: usize = 2_629_800_000; // 1mo
2025-06-22 13:03:02 -04:00
/// Create a new invite_code in the database.
///
/// # Arguments
/// * `data` - a mock [`InviteCode`] object to insert
pub async fn create_invite_code(&self, data: InviteCode, user: &User) -> Result<InviteCode> {
2025-07-05 11:58:51 -04:00
// check account creation date (if we aren't a supporter OR this is a purchased account)
if !user.permissions.check(FinePermission::SUPPORTER) | user.was_purchased {
if unix_epoch_timestamp() - user.created < Self::MINIMUM_ACCOUNT_AGE_FOR_INVITE_CODES {
return Err(Error::MiscError(
"Your account is too young to do this".to_string(),
));
}
2025-07-03 21:56:21 -04:00
}
2025-07-03 21:56:21 -04:00
// ...
if !user.permissions.check(FinePermission::SUPPORTER) {
// 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_count(user.id).await? as usize)
>= Self::MAXIMUM_FREE_INVITE_CODES
{
return Err(Error::MiscError(
"You already have the maximum number of invite codes you can create"
.to_string(),
));
}
2025-06-22 15:15:39 -04:00
} else if !user.permissions.check(FinePermission::MANAGE_USERS) {
// check count since we're also not a moderator with MANAGE_USERS
if (self.get_invite_codes_by_owner_count(user.id).await? as usize)
2025-06-22 13:03:02 -04:00
>= Self::MAXIMUM_SUPPORTER_INVITE_CODES
{
return Err(Error::MiscError(
"You already have the maximum number of invite codes you can create"
.to_string(),
));
}
}
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(
&conn,
"INSERT INTO invite_codes VALUES ($1, $2, $3, $4, $5)",
params![
&(data.id as i64),
&(data.created as i64),
&(data.owner as i64),
&data.code,
&{ if data.is_used { 1 } else { 0 } }
]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(data)
}
pub async fn delete_invite_code(&self, id: usize, user: &User) -> Result<()> {
2025-06-22 15:15:39 -04:00
if !user.permissions.check(FinePermission::MANAGE_USERS) {
2025-06-22 13:03:02 -04:00
return Err(Error::NotAllowed);
}
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(
&conn,
"DELETE FROM invite_codes WHERE id = $1",
&[&(id as i64)]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
self.0.1.remove(format!("atto.invite_code:{}", id)).await;
Ok(())
}
pub async fn update_invite_code_is_used(&self, id: usize, new_is_used: bool) -> Result<()> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(
&conn,
"UPDATE invite_codes SET is_used = $1 WHERE id = $2",
params![&{ if new_is_used { 1 } else { 0 } }, &(id as i64)]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
self.0.1.remove(format!("atto.invite_code:{}", id)).await;
Ok(())
}
}