From 2edef9bd35b14c183d6d728c895f1b4f7ce22295 Mon Sep 17 00:00:00 2001 From: trisua Date: Wed, 13 Aug 2025 02:22:00 -0400 Subject: [PATCH] add: user last_policy_consent --- crates/app/src/assets.rs | 5 ++ crates/app/src/public/html/root.lisp | 48 ++++++++++++++++++- .../routes/api/v1/auth/connections/stripe.rs | 2 +- crates/app/src/routes/api/v1/auth/mod.rs | 1 - crates/app/src/routes/api/v1/auth/profile.rs | 23 +++++++++ crates/app/src/routes/api/v1/mod.rs | 4 ++ crates/core/src/config.rs | 10 ++++ crates/core/src/database/auth.rs | 5 +- .../src/database/drivers/sql/create_users.sql | 3 +- .../drivers/sql/version_migrations.sql | 4 ++ crates/core/src/model/auth.rs | 11 +++-- 11 files changed, 107 insertions(+), 9 deletions(-) diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index fcc76f0..393fe54 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -469,10 +469,15 @@ pub(crate) async fn initial_context( .check(SecondaryPermission::DEVELOPER_PASS), ); ctx.insert("home", &ua.settings.default_timeline.relative_url()); + ctx.insert( + "renew_policy_consent", + &(ua.last_policy_consent < config.policies.last_updated), + ); } else { ctx.insert("is_helper", &false); ctx.insert("is_manager", &false); ctx.insert("home", &DefaultTimelineChoice::default().relative_url()); + ctx.insert("renew_policy_consent", &false); } ctx.insert("lang", &lang.data); diff --git a/crates/app/src/public/html/root.lisp b/crates/app/src/public/html/root.lisp index 9a253fe..4fc5b81 100644 --- a/crates/app/src/public/html/root.lisp +++ b/crates/app/src/public/html/root.lisp @@ -151,7 +151,53 @@ } }); }")))))) - (text "{% elif user.is_deactivated -%}") + (text "{% elif user and renew_policy_consent -%}") + ; renew policy consent + (article + (main + (div + ("class" "card_nest") + (div + ("class" "card small flex items_center gap_2") + (icon (text "scroll-text")) + (text "Our policies have been updated!")) + + (div + ("class" "card flex flex_col gap_2 no_p_margin") + (p (text "Your consent is needed for the updated versions of our Terms of Service and Privacy Policy. Please reread them and click \"Accept\" if you agree to these updated terms.")) + (ul + (li + (a + ("href" "{{ config.policies.terms_of_service }}") + (text "Terms of service"))) + (li + (a + ("href" "{{ config.policies.privacy }}") + (text "Privacy policy")))) + (hr ("class" "margin")) + (button + ("onclick" "update_policy_consent()") + (icon (text "check")) + (str (text "general:action.accept"))))))) + + (script + (text "globalThis.update_policy_consent = async () => { + fetch(\"/api/v1/auth/user/me/policy_consent\", { + method: \"POST\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + window.location.reload(); + } + }); + };")) + (text "{% elif user and user.is_deactivated -%}") ; account deactivated message (article (main diff --git a/crates/app/src/routes/api/v1/auth/connections/stripe.rs b/crates/app/src/routes/api/v1/auth/connections/stripe.rs index 14ca49a..7721f6c 100644 --- a/crates/app/src/routes/api/v1/auth/connections/stripe.rs +++ b/crates/app/src/routes/api/v1/auth/connections/stripe.rs @@ -190,7 +190,7 @@ pub async fn stripe_webhook( return Json(e.into()); } - if data.0.0.security.enable_invite_codes && user.awaiting_purchase { + if user.awaiting_purchase { if let Err(e) = data .update_user_awaiting_purchased_status(user.id, false, user.clone(), false) .await diff --git a/crates/app/src/routes/api/v1/auth/mod.rs b/crates/app/src/routes/api/v1/auth/mod.rs index dff259e..5ab25e4 100644 --- a/crates/app/src/routes/api/v1/auth/mod.rs +++ b/crates/app/src/routes/api/v1/auth/mod.rs @@ -84,7 +84,6 @@ pub async fn register_request( // ... let mut user = User::new(props.username.to_lowercase(), props.password); - user.settings.policy_consent = true; // check invite code if data.0.0.security.enable_invite_codes { diff --git a/crates/app/src/routes/api/v1/auth/profile.rs b/crates/app/src/routes/api/v1/auth/profile.rs index 5323b33..e378567 100644 --- a/crates/app/src/routes/api/v1/auth/profile.rs +++ b/crates/app/src/routes/api/v1/auth/profile.rs @@ -112,6 +112,29 @@ pub async fn me_request(jar: CookieJar, Extension(data): Extension) -> im }) } +pub async fn policy_consent_request( + jar: CookieJar, + Extension(data): Extension, +) -> 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_last_policy_consent(user.id, unix_epoch_timestamp() as i64) + .await + { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Consent given".to_string(), + payload: Some(user), + }), + Err(e) => Json(e.into()), + } +} + /// Update the settings of the given user. pub async fn update_user_settings_request( jar: CookieJar, diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 46c1bdb..294ebb9 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -302,6 +302,10 @@ pub fn routes() -> Router { ) // profile .route("/auth/user/me", get(auth::profile::me_request)) + .route( + "/auth/user/me/policy_consent", + post(auth::profile::policy_consent_request), + ) .route("/auth/user/{id}/avatar", get(auth::images::avatar_request)) .route("/auth/user/{id}/banner", get(auth::images::banner_request)) .route("/auth/user/{id}/follow", post(auth::social::follow_request)) diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index b07df61..3843f0a 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -121,6 +121,15 @@ pub struct PoliciesConfig { /// /// Same deal as terms of service page. pub privacy: String, + /// The time (in ms since unix epoch) in which the site's policies last updated. + /// + /// This is required to automatically ask users to re-consent to policies. + /// + /// In user whose consent time in LESS THAN this date will be shown a dialog to re-consent to the policies. + /// + /// You can get this easily by running `echo "console.log(new Date().getTime())" | node`. + #[serde(default)] + pub last_updated: usize, } impl Default for PoliciesConfig { @@ -128,6 +137,7 @@ impl Default for PoliciesConfig { Self { terms_of_service: "/public/tos.html".to_string(), privacy: "/public/privacy.html".to_string(), + last_updated: 0, } } } diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index 21bba30..2245e47 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -131,6 +131,7 @@ impl DataManager { coins: get!(x->31(i32)), checkouts: serde_json::from_str(&get!(x->32(String)).to_string()).unwrap(), applied_configurations: serde_json::from_str(&get!(x->33(String)).to_string()).unwrap(), + last_policy_consent: get!(x->34(i64)) as usize, } } @@ -287,7 +288,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34)", + "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35)", params![ &(data.id as i64), &(data.created as i64), @@ -323,6 +324,7 @@ impl DataManager { &(data.coins as i32), &serde_json::to_string(&data.checkouts).unwrap(), &serde_json::to_string(&data.applied_configurations).unwrap(), + &(data.last_policy_consent as i64) ] ); @@ -1161,6 +1163,7 @@ impl DataManager { auto_method!(update_user_coins(i32)@get_user_by_id -> "UPDATE users SET coins = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); auto_method!(update_user_checkouts(Vec)@get_user_by_id -> "UPDATE users SET checkouts = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); auto_method!(update_user_applied_configurations(Vec<(AppliedConfigType, usize)>)@get_user_by_id -> "UPDATE users SET applied_configurations = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); + auto_method!(update_user_last_policy_consent(i64)@get_user_by_id -> "UPDATE users SET last_policy_consent = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); auto_method!(get_user_by_stripe_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE stripe_id = $1" --name="user" --returns=User); auto_method!(update_user_stripe_id(&str)@get_user_by_id -> "UPDATE users SET stripe_id = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); diff --git a/crates/core/src/database/drivers/sql/create_users.sql b/crates/core/src/database/drivers/sql/create_users.sql index 8ec2c22..1726d10 100644 --- a/crates/core/src/database/drivers/sql/create_users.sql +++ b/crates/core/src/database/drivers/sql/create_users.sql @@ -32,5 +32,6 @@ CREATE TABLE IF NOT EXISTS users ( ban_expire BIGINT NOT NULL, coins INT NOT NULL, checkouts TEXT NOT NULL, - applied_configurations TEXT NOT NULL + applied_configurations TEXT NOT NULL, + last_policy_consent BIGINT NOT NULL ) diff --git a/crates/core/src/database/drivers/sql/version_migrations.sql b/crates/core/src/database/drivers/sql/version_migrations.sql index fb70e6b..4598560 100644 --- a/crates/core/src/database/drivers/sql/version_migrations.sql +++ b/crates/core/src/database/drivers/sql/version_migrations.sql @@ -65,3 +65,7 @@ ADD COLUMN IF NOT EXISTS applied_configurations TEXT DEFAULT '[]'; -- products uploads ALTER TABLE products ADD COLUMN IF NOT EXISTS uploads TEXT DEFAULT '{}'; + +-- users last_policy_consent +ALTER TABLE users +ADD COLUMN IF NOT EXISTS last_policy_consent BIGINT DEFAULT 0; diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 902e97f..3da4db8 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -104,6 +104,9 @@ pub struct User { /// The IDs of products to be applied to the user's profile. #[serde(default)] pub applied_configurations: Vec<(AppliedConfigType, usize)>, + /// The time in which the user last consented to the site's policies. + #[serde(default)] + pub last_policy_consent: usize, } pub type UserConnections = @@ -180,8 +183,6 @@ impl Default for DefaultProfileTabChoice { #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct UserSettings { - #[serde(default)] - pub policy_consent: bool, #[serde(default)] pub display_name: String, #[serde(default)] @@ -391,10 +392,11 @@ impl User { pub fn new(username: String, password: String) -> Self { let salt = salt(); let password = hash_salted(password, salt.clone()); + let created = unix_epoch_timestamp(); Self { id: Snowflake::new().to_string().parse::().unwrap(), - created: unix_epoch_timestamp(), + created, username, password, salt, @@ -405,7 +407,7 @@ impl User { notification_count: 0, follower_count: 0, following_count: 0, - last_seen: unix_epoch_timestamp(), + last_seen: created, totp: String::new(), recovery_codes: Vec::new(), post_count: 0, @@ -427,6 +429,7 @@ impl User { coins: 0, checkouts: Vec::new(), applied_configurations: Vec::new(), + last_policy_consent: created, } }