From ee8fc77830de837f2c1afe0349ea6efb0ea1afc9 Mon Sep 17 00:00:00 2001 From: Syfaro Date: Thu, 30 May 2024 14:19:31 -0400 Subject: [PATCH] Better account verification. --- ...0240530173905_better_verification.down.sql | 2 + .../20240530173905_better_verification.up.sql | 22 ++ queries/linked_account/create.sql | 6 +- queries/linked_account/verify.sql | 7 + sqlx-data.json | 222 ++++++++++++------ src/jobs.rs | 10 +- src/models.rs | 33 ++- src/site/bsky.rs | 1 + src/site/deviantart.rs | 1 + src/site/patreon.rs | 1 + src/site/twitter.rs | 1 + src/user.rs | 43 +++- templates/user/account/view.html | 4 +- 13 files changed, 255 insertions(+), 98 deletions(-) create mode 100644 migrations/20240530173905_better_verification.down.sql create mode 100644 migrations/20240530173905_better_verification.up.sql create mode 100644 queries/linked_account/verify.sql diff --git a/migrations/20240530173905_better_verification.down.sql b/migrations/20240530173905_better_verification.down.sql new file mode 100644 index 0000000..d7acbf8 --- /dev/null +++ b/migrations/20240530173905_better_verification.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE + linked_account DROP COLUMN verification_key, DROP COLUMN verified_at; diff --git a/migrations/20240530173905_better_verification.up.sql b/migrations/20240530173905_better_verification.up.sql new file mode 100644 index 0000000..f400a3f --- /dev/null +++ b/migrations/20240530173905_better_verification.up.sql @@ -0,0 +1,22 @@ +ALTER TABLE + linked_account +ADD + COLUMN created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT current_timestamp, +ADD + COLUMN verification_key TEXT, +ADD + COLUMN verified_at TIMESTAMP WITH TIME ZONE; + +UPDATE + linked_account +SET + verified_at = current_timestamp +WHERE + linked_account.data->'verification_key' IS NULL; + +UPDATE + linked_account +SET + verification_key = linked_account.data->>'verification_key' +WHERE + linked_account.data->'verification_key' IS NOT NULL; diff --git a/queries/linked_account/create.sql b/queries/linked_account/create.sql index af9b844..dc16503 100644 --- a/queries/linked_account/create.sql +++ b/queries/linked_account/create.sql @@ -3,7 +3,9 @@ INSERT INTO owner_id, source_site, username, - data + data, + verification_key, + verified_at ) VALUES - ($1, $2, $3, $4) RETURNING *; + ($1, $2, $3, $4, $5, $6) RETURNING *; diff --git a/queries/linked_account/verify.sql b/queries/linked_account/verify.sql new file mode 100644 index 0000000..8366abd --- /dev/null +++ b/queries/linked_account/verify.sql @@ -0,0 +1,7 @@ +UPDATE + linked_account +SET + verification_key = NULL, + verified_at = current_timestamp +WHERE + id = $1; diff --git a/sqlx-data.json b/sqlx-data.json index 6710c8d..4da8e56 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -1003,6 +1003,21 @@ "name": "disabled", "ordinal": 7, "type_info": "Bool" + }, + { + "name": "created_at", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "verification_key", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "verified_at", + "ordinal": 10, + "type_info": "Timestamptz" } ], "nullable": [ @@ -1013,7 +1028,10 @@ true, true, true, - false + false, + false, + true, + true ], "parameters": { "Left": [ @@ -1045,6 +1063,91 @@ }, "query": "SELECT\n value\nFROM\n user_setting\nWHERE\n owner_id = $1\n AND setting = $2;\n" }, + "3c62545c92caafe3484068f197dbc3f6c50759b4d32371a5747f39fa35171634": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "owner_id", + "ordinal": 1, + "type_info": "Uuid" + }, + { + "name": "source_site", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "username", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "last_update", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "loading_state", + "ordinal": 5, + "type_info": "Jsonb" + }, + { + "name": "data", + "ordinal": 6, + "type_info": "Jsonb" + }, + { + "name": "disabled", + "ordinal": 7, + "type_info": "Bool" + }, + { + "name": "created_at", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "verification_key", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "verified_at", + "ordinal": 10, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + false, + true, + true, + true, + false, + false, + true, + true + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Jsonb", + "Text", + "Timestamptz" + ] + } + }, + "query": "INSERT INTO\n linked_account (\n owner_id,\n source_site,\n username,\n data,\n verification_key,\n verified_at\n )\nVALUES\n ($1, $2, $3, $4, $5, $6) RETURNING *;\n" + }, "3cfa31c0b605317c1fedadaf0399ed2012b2f6573b1421b53cfb14a80316a35b": { "describe": { "columns": [ @@ -1209,6 +1312,21 @@ "name": "disabled", "ordinal": 7, "type_info": "Bool" + }, + { + "name": "created_at", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "verification_key", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "verified_at", + "ordinal": 10, + "type_info": "Timestamptz" } ], "nullable": [ @@ -1219,7 +1337,10 @@ true, true, true, - false + false, + false, + true, + true ], "parameters": { "Left": [ @@ -2073,6 +2194,21 @@ "name": "disabled", "ordinal": 7, "type_info": "Bool" + }, + { + "name": "created_at", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "verification_key", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "verified_at", + "ordinal": 10, + "type_info": "Timestamptz" } ], "nullable": [ @@ -2083,7 +2219,10 @@ true, true, true, - false + false, + false, + true, + true ], "parameters": { "Left": [ @@ -2581,71 +2720,6 @@ }, "query": "UPDATE linked_account SET disabled = $2 WHERE source_site = $1" }, - "930ae63c1ef71a87799036a706f5ce4a5522872ba7c1aad2f2c8ad68b2d4f883": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Uuid" - }, - { - "name": "owner_id", - "ordinal": 1, - "type_info": "Uuid" - }, - { - "name": "source_site", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "username", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "last_update", - "ordinal": 4, - "type_info": "Timestamptz" - }, - { - "name": "loading_state", - "ordinal": 5, - "type_info": "Jsonb" - }, - { - "name": "data", - "ordinal": 6, - "type_info": "Jsonb" - }, - { - "name": "disabled", - "ordinal": 7, - "type_info": "Bool" - } - ], - "nullable": [ - false, - false, - false, - false, - true, - true, - true, - false - ], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Text", - "Jsonb" - ] - } - }, - "query": "INSERT INTO\n linked_account (\n owner_id,\n source_site,\n username,\n data\n )\nVALUES\n ($1, $2, $3, $4) RETURNING *;\n" - }, "9499afaf61b5a24ace5360d39e21234c56d9c8daaf4ae6e2bb1fd10f30718802": { "describe": { "columns": [ @@ -2730,6 +2804,18 @@ }, "query": "SELECT\n *\nFROM\n user_event\nWHERE\n owner_id = $1\n AND related_to_media_item_id = $2\nORDER BY\n created_at DESC\nLIMIT\n 50;\n" }, + "96d74507c6b542a7ba43a098a3545db48ccf5a35e1689f672c470e9d36c1d5e0": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "UPDATE\n linked_account\nSET\n verification_key = NULL,\n verified_at = current_timestamp\nWHERE\n id = $1;\n" + }, "993d20c56e9eb4083f21d653c3c113649c4c646dc077c46f464dd787d778c6fa": { "describe": { "columns": [ diff --git a/src/jobs.rs b/src/jobs.rs index 14860ac..d7da3fb 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -684,7 +684,7 @@ pub async fn start_job_processing(ctx: JobContext) -> Result<(), Error> { .await? .ok_or(Error::Missing)?; - let key = match account.verification_key() { + let key = match account.verification_key { Some(key) => key, None => return Err(Error::Missing), }; @@ -718,10 +718,10 @@ pub async fn start_job_processing(ctx: JobContext) -> Result<(), Error> { .map(|elem| elem.text().collect::()) .unwrap_or_default(); - let verifier_found = text.contains(key); + let verifier_found = text.contains(&key); if verifier_found { - models::LinkedAccount::update_data(&ctx.conn, account.id, None).await?; + models::LinkedAccount::verify(&ctx.conn, account.id).await?; } verifier_found @@ -745,10 +745,10 @@ pub async fn start_job_processing(ctx: JobContext) -> Result<(), Error> { .and_then(|profile_text| profile_text.as_str()) .ok_or_else(|| Error::unknown_message("Weasyl was missing profile text"))?; - let verifier_found = profile_text.contains(key); + let verifier_found = profile_text.contains(&key); if verifier_found { - models::LinkedAccount::update_data(&ctx.conn, account.id, None).await?; + models::LinkedAccount::verify(&ctx.conn, account.id).await?; } verifier_found diff --git a/src/models.rs b/src/models.rs index 922e278..60846ff 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1147,6 +1147,8 @@ pub struct LinkedAccount { pub last_update: Option>, pub loading_state: Option, pub data: Option, + pub verification_key: Option, + pub verified_at: Option>, } impl LinkedAccount { @@ -1156,13 +1158,18 @@ impl LinkedAccount { source_site: Site, username: &str, data: Option, + verification_key: Option, ) -> Result { + let verified_at = verification_key.is_none().then(chrono::Utc::now); + let id = sqlx::query_file!( "queries/linked_account/create.sql", user_id, source_site.to_string(), username, - data + data, + verification_key, + verified_at ) .map(|row| LinkedAccount { id: row.id, @@ -1174,6 +1181,8 @@ impl LinkedAccount { .loading_state .and_then(|loading_state| serde_json::from_value(loading_state).ok()), data: row.data, + verification_key: row.verification_key, + verified_at: row.verified_at, }) .fetch_one(conn) .await?; @@ -1193,6 +1202,8 @@ impl LinkedAccount { .loading_state .and_then(|loading_state| serde_json::from_value(loading_state).ok()), data: row.data, + verification_key: row.verification_key, + verified_at: row.verified_at, }) .fetch_optional(conn) .await?; @@ -1222,6 +1233,8 @@ impl LinkedAccount { .loading_state .and_then(|loading_state| serde_json::from_value(loading_state).ok()), data: row.data, + verification_key: row.verification_key, + verified_at: row.verified_at, }) .fetch_optional(conn) .await?; @@ -1249,6 +1262,8 @@ impl LinkedAccount { .loading_state .and_then(|loading_state| serde_json::from_value(loading_state).ok()), data: row.data, + verification_key: row.verification_key, + verified_at: row.verified_at, }) .fetch_all(conn) .await?; @@ -1302,6 +1317,14 @@ impl LinkedAccount { Ok(()) } + pub async fn verify(conn: &sqlx::PgPool, account_id: Uuid) -> Result<(), Error> { + sqlx::query_file!("queries/linked_account/verify.sql", account_id) + .execute(conn) + .await?; + + Ok(()) + } + pub async fn update_data( conn: &sqlx::PgPool, account_id: Uuid, @@ -1366,14 +1389,6 @@ impl LinkedAccount { Ok(accounts) } - pub fn verification_key(&self) -> Option<&str> { - self.data - .as_ref() - .and_then(|data| data.as_object()) - .and_then(|obj| obj.get("verification_key")) - .and_then(|key| key.as_str()) - } - pub fn show_twitter_archive_import(&self) -> bool { if self.source_site != Site::Twitter { return false; diff --git a/src/site/bsky.rs b/src/site/bsky.rs index 138d365..fc564c5 100644 --- a/src/site/bsky.rs +++ b/src/site/bsky.rs @@ -932,6 +932,7 @@ async fn auth_verify( server: form.server.clone(), session: Some(resp), })?), + None, ) .await?; diff --git a/src/site/deviantart.rs b/src/site/deviantart.rs index f1ca301..d2dca6d 100644 --- a/src/site/deviantart.rs +++ b/src/site/deviantart.rs @@ -514,6 +514,7 @@ async fn callback( Site::DeviantArt, &da_user.username, Some(saved_data), + None, ) .await?; diff --git a/src/site/patreon.rs b/src/site/patreon.rs index b6b19c7..95fb730 100644 --- a/src/site/patreon.rs +++ b/src/site/patreon.rs @@ -300,6 +300,7 @@ async fn callback( Site::Patreon, &full_name, Some(serde_json::to_value(saved_data)?), + None, ) .await?; diff --git a/src/site/twitter.rs b/src/site/twitter.rs index 086c973..18d641a 100644 --- a/src/site/twitter.rs +++ b/src/site/twitter.rs @@ -310,6 +310,7 @@ async fn callback( models::Site::Twitter, &username, Some(saved_data), + None, ) .await?; diff --git a/src/user.rs b/src/user.rs index 85ccc70..6698396 100644 --- a/src/user.rs +++ b/src/user.rs @@ -504,23 +504,42 @@ async fn account_link_post( } } - let username = form.username.as_deref().ok_or(Error::Missing)?; + let username = form.username.as_deref().ok_or(Error::Missing)?.trim(); - let token: String = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(12) - .map(char::from) - .collect(); + let had_error = if username.is_empty() { + session.add_flash(FlashStyle::Error, "Username must not be empty."); + true + } else if username.starts_with("http:") || username.starts_with("https:") { + session.add_flash( + FlashStyle::Error, + "Please only enter the username, not the URL.", + ); + true + } else { + false + }; - let data = match form.site { - Site::FurAffinity => Some(serde_json::json!({ "verification_key": token })), - Site::Weasyl => Some(serde_json::json!({ "verification_key": token })), - _ => None, + if had_error { + let body = AccountLink.wrap(&request, Some(&user)).await.render()?; + return Ok(HttpResponse::Ok().content_type("text/html").body(body)); + } + + let token = if matches!(form.site, Site::FurAffinity | Site::Weasyl) { + Some( + rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(12) + .map(char::from) + .collect(), + ) + } else { + None }; - let account = models::LinkedAccount::create(&conn, user.id, form.site, username, data).await?; + let account = + models::LinkedAccount::create(&conn, user.id, form.site, username, None, token).await?; - let (style, message) = if account.verification_key().is_some() { + let (style, message) = if account.verification_key.is_some() { ( FlashStyle::Warning, "Added account, please verify it to import submissions.", diff --git a/templates/user/account/view.html b/templates/user/account/view.html index 13dc4e3..d54d955 100644 --- a/templates/user/account/view.html +++ b/templates/user/account/view.html @@ -41,7 +41,7 @@

- {% match account.verification_key() %} + {% match account.verification_key %} {% when Some with (verification_key) %}
@@ -58,7 +58,7 @@

- {{ verification_key }} + {{ verification_key }}