add: purchased accounts

This commit is contained in:
trisua 2025-07-03 21:56:21 -04:00
parent 0aa2ea362f
commit 2ec8d86edf
22 changed files with 1279 additions and 124 deletions

View file

@ -138,6 +138,15 @@ pub async fn stripe_webhook(
return Json(e.into());
}
if data.0.0.security.enable_invite_codes && user.awaiting_purchase {
if let Err(e) = data
.update_user_awaiting_purchased_status(user.id, false, user.clone(), false)
.await
{
return Json(e.into());
}
}
if let Err(e) = data
.create_notification(Notification::new(
"Welcome new supporter!".to_string(),
@ -174,6 +183,18 @@ pub async fn stripe_webhook(
return Json(e.into());
}
if data.0.0.security.enable_invite_codes && user.was_purchased && user.invite_code == 0
{
// user doesn't come from an invite code, and is a purchased account
// this means their account must be locked if they stop paying
if let Err(e) = data
.update_user_awaiting_purchased_status(user.id, true, user.clone(), false)
.await
{
return Json(e.into());
}
}
if let Err(e) = data
.create_notification(Notification::new(
"Sorry to see you go... :(".to_string(),

View file

@ -88,41 +88,46 @@ pub async fn register_request(
// check invite code
if data.0.0.security.enable_invite_codes {
if props.invite_code.is_empty() {
return (
None,
Json(Error::MiscError("Missing invite code".to_string()).into()),
);
if !props.purchase {
if props.invite_code.is_empty() {
return (
None,
Json(Error::MiscError("Missing invite code".to_string()).into()),
);
}
let invite_code = match data.get_invite_code_by_code(&props.invite_code).await {
Ok(c) => c,
Err(e) => return (None, Json(e.into())),
};
if invite_code.is_used {
return (
None,
Json(Error::MiscError("This code has already been used".to_string()).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(),
// ),
// );
// }
user.invite_code = invite_code.id;
} else {
// this account is being purchased
user.awaiting_purchase = true;
}
let invite_code = match data.get_invite_code_by_code(&props.invite_code).await {
Ok(c) => c,
Err(e) => return (None, Json(e.into())),
};
if invite_code.is_used {
return (
None,
Json(Error::MiscError("This code has already been used".to_string()).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(),
// ),
// );
// }
user.invite_code = invite_code.id;
}
// push initial token
@ -133,7 +138,7 @@ pub async fn register_request(
match data.create_user(user).await {
Ok(_) => {
// mark invite as used
if data.0.0.security.enable_invite_codes {
if data.0.0.security.enable_invite_codes && !props.purchase {
let invite_code = match data.get_invite_code_by_code(&props.invite_code).await {
Ok(c) => c,
Err(e) => return (None, Json(e.into())),

View file

@ -4,8 +4,8 @@ use crate::{
model::{ApiReturn, Error},
routes::api::v1::{
AppendAssociations, AwardAchievement, DeleteUser, DisableTotp, RefreshGrantToken,
UpdateSecondaryUserRole, UpdateUserIsVerified, UpdateUserPassword, UpdateUserRole,
UpdateUserUsername,
UpdateSecondaryUserRole, UpdateUserAwaitingPurchase, UpdateUserInviteCode,
UpdateUserIsVerified, UpdateUserPassword, UpdateUserRole, UpdateUserUsername,
},
State,
};
@ -343,6 +343,34 @@ pub async fn update_user_is_verified_request(
}
}
/// Update the verification status of the given user.
///
/// Does not support third-party grants.
pub async fn update_user_awaiting_purchase_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
Json(req): Json<UpdateUserAwaitingPurchase>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data
.update_user_awaiting_purchased_status(id, req.awaiting_purchase, user, true)
.await
{
Ok(_) => Json(ApiReturn {
ok: true,
message: "Awaiting purchase status updated".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
/// Update the role of the given user.
///
/// Does not support third-party grants.
@ -949,3 +977,55 @@ pub async fn self_serve_achievement_request(
Err(e) => Json(e.into()),
}
}
/// Update the verification status of the given user.
///
/// Does not support third-party grants.
pub async fn update_user_invite_code_request(
jar: CookieJar,
Extension(data): Extension<State>,
Json(req): Json<UpdateUserInviteCode>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
if req.invite_code.is_empty() {
return Json(Error::MiscError("Missing invite code".to_string()).into());
}
let invite_code = match data.get_invite_code_by_code(&req.invite_code).await {
Ok(c) => c,
Err(e) => return Json(e.into()),
};
if invite_code.is_used {
return Json(Error::MiscError("This code has already been used".to_string()).into());
}
if let Err(e) = data.update_invite_code_is_used(invite_code.id, true).await {
return Json(e.into());
}
match data
.update_user_invite_code(user.id, invite_code.id as i64)
.await
{
Ok(_) => {
match data
.update_user_awaiting_purchased_status(user.id, false, user, false)
.await
{
Ok(_) => Json(ApiReturn {
ok: true,
message: "Invite code updated".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
Err(e) => Json(e.into()),
}
}

View file

@ -331,6 +331,10 @@ pub fn routes() -> Router {
"/auth/user/{id}/verified",
post(auth::profile::update_user_is_verified_request),
)
.route(
"/auth/user/{id}/awaiting_purchase",
post(auth::profile::update_user_awaiting_purchase_request),
)
.route(
"/auth/user/{id}/totp",
post(auth::profile::enable_totp_request),
@ -394,6 +398,10 @@ pub fn routes() -> Router {
"/auth/user/me/achievement",
post(auth::profile::self_serve_achievement_request),
)
.route(
"/auth/user/me/invite_code",
post(auth::profile::update_user_invite_code_request),
)
// apps
.route("/apps", post(apps::create_request))
.route("/apps/{id}/title", post(apps::update_title_request))
@ -643,6 +651,12 @@ pub struct RegisterProps {
pub captcha_response: String,
#[serde(default)]
pub invite_code: String,
/// If this is true, invite_code should be empty.
///
/// If invite codes are enabled, but purchase is false, the invite_code MUST
/// be checked and MUST be valid.
#[serde(default)]
pub purchase: bool,
}
#[derive(Deserialize)]
@ -750,6 +764,11 @@ pub struct UpdateUserIsVerified {
pub is_verified: bool,
}
#[derive(Deserialize)]
pub struct UpdateUserAwaitingPurchase {
pub awaiting_purchase: bool,
}
#[derive(Deserialize)]
pub struct UpdateNotificationRead {
pub read: bool,
@ -775,6 +794,11 @@ pub struct UpdateSecondaryUserRole {
pub role: SecondaryPermission,
}
#[derive(Deserialize)]
pub struct UpdateUserInviteCode {
pub invite_code: String,
}
#[derive(Deserialize)]
pub struct DeleteUser {
pub password: String,