add: user last_policy_consent

This commit is contained in:
trisua 2025-08-13 02:22:00 -04:00
parent befd9096b1
commit 2edef9bd35
11 changed files with 107 additions and 9 deletions

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -112,6 +112,29 @@ pub async fn me_request(jar: CookieJar, Extension(data): Extension<State>) -> im
})
}
pub async fn policy_consent_request(
jar: CookieJar,
Extension(data): Extension<State>,
) -> 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,

View file

@ -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))

View file

@ -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,
}
}
}

View file

@ -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<String>)@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);

View file

@ -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
)

View file

@ -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;

View file

@ -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::<usize>().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,
}
}