From d722e8af717593f65fecab8221f6bbed87e32463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Tue, 28 Jul 2026 15:58:01 +0200 Subject: [PATCH] fixes --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/auth/mod.rs | 47 ++++++++++++++++++++++++++++------------ src/queries/mod.rs | 2 ++ src/queries/mysql.rs | 1 + src/queries/postgres.rs | 1 + src/queries/sqlite.rs | 1 + static/js/home.js | 16 +++++++------- static/js/note-editor.js | 12 +++++++--- 9 files changed, 57 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16b31cd..152b77a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2581,7 +2581,7 @@ dependencies = [ [[package]] name = "rustpad" -version = "0.1.17" +version = "0.1.18" dependencies = [ "argon2", "aws-config", diff --git a/Cargo.toml b/Cargo.toml index 96b8b48..6b8b4d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustpad" -version = "0.1.17" +version = "0.1.18" edition = "2024" rust-version = "1.94" description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL" diff --git a/src/auth/mod.rs b/src/auth/mod.rs index a9e37aa..582bfc8 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -154,7 +154,8 @@ pub struct PrivacyRequest { pub struct ShareUsersRequest { kind: String, slug: String, - emails: String, + #[serde(alias = "emails")] + recipients: String, permission: String, } #[derive(Deserialize)] @@ -1188,26 +1189,29 @@ pub async fn share_resource_users( "Share confirmation requires SMTP configuration.", )); } - let emails: Vec = req - .emails - .split(',') - .map(|v| normalize(v)) - .filter(|v| !v.is_empty()) + let recipients: Vec = req + .recipients + .split([',', ';', '\n']) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + .collect::>() + .into_iter() .collect(); - if emails.is_empty() || emails.len() > 100 { + if recipients.is_empty() || recipients.len() > 100 { return Err(AuthError::bad_request( - "Enter between 1 and 100 registered e-mail addresses.", + "Enter between 1 and 100 e-mail addresses or user names.", )); } let mut missing = Vec::new(); - for email in emails { - let user = find_user_by_email(&state, &email).await?; + for recipient in recipients { + let user = find_user_by_share_identifier(&state, &recipient).await?; let Some(user) = user else { - missing.push(email); + missing.push(recipient); continue; }; if user.confirmed_at.is_none() { - missing.push(format!("{} (account not activated)", email)); + missing.push(format!("{} (account not activated)", recipient)); continue; } if user.id == owner.id { @@ -1289,7 +1293,7 @@ pub async fn share_resource_users( } if !missing.is_empty() { return Err(AuthError::bad_request(&format!( - "No registered account for: {}", + "No active registered account for: {}", missing.join(", ") ))); } @@ -1426,7 +1430,7 @@ pub async fn resource_sharing( .fetch_all(state.db.pool()) .await .map_err(AuthError::database)?; - let pending: Vec<(String, String, String, String)> = sqlx::query_as(queries::get( + let pending: Vec<(String, String, String, Option)> = sqlx::query_as(queries::get( state.db.kind(), queries::RESOURCE_SHARING_PENDING, )) @@ -1870,6 +1874,21 @@ async fn find_user_by_external_id( .map_err(AuthError::database) } + +async fn find_user_by_share_identifier( + state: &SharedState, + identifier: &str, +) -> Result, AuthError> { + sqlx::query_as::<_, User>(queries::get( + state.db.kind(), + queries::AUTH_USER_BY_SHARE_IDENTIFIER, + )) + .bind(normalize(identifier)) + .fetch_optional(state.db.pool()) + .await + .map_err(AuthError::database) +} + async fn find_user_by_email(state: &SharedState, email: &str) -> Result, AuthError> { sqlx::query_as::<_, User>(queries::get(state.db.kind(), queries::AUTH_USER_BY_EMAIL)) .bind(normalize(email)) diff --git a/src/queries/mod.rs b/src/queries/mod.rs index 9c09ed3..7ac2929 100644 --- a/src/queries/mod.rs +++ b/src/queries/mod.rs @@ -54,6 +54,7 @@ pub enum Query { AUTH_INSERT_SESSION, AUTH_USER_BY_NICKNAME, AUTH_USER_BY_EMAIL, + AUTH_USER_BY_SHARE_IDENTIFIER, USER_ATTACH_WORKSPACE, USER_ATTACH_PAD, USER_LIST_WORKSPACES, @@ -188,6 +189,7 @@ pub const AUTH_USER_BY_SESSION: Query = Query::AUTH_USER_BY_SESSION; pub const AUTH_INSERT_SESSION: Query = Query::AUTH_INSERT_SESSION; pub const AUTH_USER_BY_NICKNAME: Query = Query::AUTH_USER_BY_NICKNAME; pub const AUTH_USER_BY_EMAIL: Query = Query::AUTH_USER_BY_EMAIL; +pub const AUTH_USER_BY_SHARE_IDENTIFIER: Query = Query::AUTH_USER_BY_SHARE_IDENTIFIER; pub const USER_ATTACH_WORKSPACE: Query = Query::USER_ATTACH_WORKSPACE; pub const USER_ATTACH_PAD: Query = Query::USER_ATTACH_PAD; pub const USER_LIST_WORKSPACES: Query = Query::USER_LIST_WORKSPACES; diff --git a/src/queries/mysql.rs b/src/queries/mysql.rs index 2840f90..c421fea 100644 --- a/src/queries/mysql.rs +++ b/src/queries/mysql.rs @@ -49,6 +49,7 @@ pub fn get(query: Query) -> &'static str { Query::AUTH_INSERT_SESSION => r#"INSERT INTO user_sessions (token, user_id, expires_at) VALUES (?, ?, ?)"#, Query::AUTH_USER_BY_NICKNAME => r#"SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS SIGNED) AS is_active FROM users WHERE nickname_key = ?"#, Query::AUTH_USER_BY_EMAIL => r#"SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS SIGNED) AS is_active FROM users WHERE email_key = ?"#, + Query::AUTH_USER_BY_SHARE_IDENTIFIER => r#"SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS SIGNED) AS is_active FROM users JOIN (SELECT ? AS identifier) lookup ON 1 = 1 WHERE is_active = 1 AND (email_key = lookup.identifier OR nickname_key = lookup.identifier OR LOWER(external_id) = lookup.identifier) LIMIT 1"#, Query::USER_ATTACH_WORKSPACE => r#"INSERT INTO user_workspaces (user_id, workspace_id) SELECT ?, id FROM workspaces WHERE slug = ?"#, Query::USER_ATTACH_PAD => r#"INSERT INTO user_pads (user_id, pad_id) SELECT ?, id FROM pads WHERE slug = ?"#, Query::USER_LIST_WORKSPACES => r#"SELECT w.slug, CAST(w.title AS CHAR CHARACTER SET utf8mb4) AS title, CASE WHEN w.password_hash IS NULL THEN 0 ELSE 1 END AS protected, w.updated_at, CASE WHEN w.is_private THEN 1 ELSE 0 END AS private, 1 AS owned, 'rw' AS permission, '' AS shared_by FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = ? UNION SELECT w.slug, CAST(w.title AS CHAR CHARACTER SET utf8mb4) AS title, CASE WHEN w.password_hash IS NULL THEN 0 ELSE 1 END, w.updated_at, CASE WHEN w.is_private THEN 1 ELSE 0 END, 0, rp.permission, COALESCE((SELECT u.nickname FROM user_workspaces owner_uw JOIN users u ON u.id = owner_uw.user_id WHERE owner_uw.workspace_id = w.id LIMIT 1), 'Unknown user') AS shared_by FROM resource_permissions rp JOIN workspaces w ON w.slug = rp.resource_slug WHERE rp.resource_kind = 'workspace' AND rp.user_id = ? ORDER BY updated_at DESC"#, diff --git a/src/queries/postgres.rs b/src/queries/postgres.rs index fe6e6c3..a1c5ddf 100644 --- a/src/queries/postgres.rs +++ b/src/queries/postgres.rs @@ -49,6 +49,7 @@ pub fn get(query: Query) -> &'static str { Query::AUTH_INSERT_SESSION => r#"INSERT INTO user_sessions (token, user_id, expires_at) VALUES ($1, $2, $3)"#, Query::AUTH_USER_BY_NICKNAME => r#"SELECT id, nickname, email, password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS BIGINT) AS is_active FROM users WHERE nickname_key = $1"#, Query::AUTH_USER_BY_EMAIL => r#"SELECT id, nickname, email, password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS BIGINT) AS is_active FROM users WHERE email_key = $1"#, + Query::AUTH_USER_BY_SHARE_IDENTIFIER => r#"SELECT id, nickname, email, password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS BIGINT) AS is_active FROM users WHERE is_active = TRUE AND (email_key = $1 OR nickname_key = $1 OR LOWER(external_id) = $1) LIMIT 1"#, Query::USER_ATTACH_WORKSPACE => r#"INSERT INTO user_workspaces (user_id, workspace_id) SELECT $1, id FROM workspaces WHERE slug = $2"#, Query::USER_ATTACH_PAD => r#"INSERT INTO user_pads (user_id, pad_id) SELECT $1, id FROM pads WHERE slug = $2"#, Query::USER_LIST_WORKSPACES => r#"SELECT w.slug, w.title, CAST(CASE WHEN w.password_hash IS NULL THEN 0 ELSE 1 END AS BIGINT) AS protected, w.updated_at, CAST(CASE WHEN w.is_private THEN 1 ELSE 0 END AS BIGINT) AS private, CAST(1 AS BIGINT) AS owned, 'rw' AS permission, '' AS shared_by FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = $1 UNION SELECT w.slug, w.title, CAST(CASE WHEN w.password_hash IS NULL THEN 0 ELSE 1 END AS BIGINT), w.updated_at, CAST(CASE WHEN w.is_private THEN 1 ELSE 0 END AS BIGINT), CAST(0 AS BIGINT), rp.permission, COALESCE((SELECT u.nickname FROM user_workspaces owner_uw JOIN users u ON u.id = owner_uw.user_id WHERE owner_uw.workspace_id = w.id LIMIT 1), 'Unknown user') AS shared_by FROM resource_permissions rp JOIN workspaces w ON w.slug = rp.resource_slug WHERE rp.resource_kind = 'workspace' AND rp.user_id = $2 ORDER BY updated_at DESC"#, diff --git a/src/queries/sqlite.rs b/src/queries/sqlite.rs index 8401939..dc4df48 100644 --- a/src/queries/sqlite.rs +++ b/src/queries/sqlite.rs @@ -49,6 +49,7 @@ pub fn get(query: Query) -> &'static str { Query::AUTH_INSERT_SESSION => r#"INSERT INTO user_sessions (token, user_id, expires_at) VALUES (?, ?, ?)"#, Query::AUTH_USER_BY_NICKNAME => r#"SELECT id, nickname, email, password_hash, confirmed_at, is_active FROM users WHERE nickname_key = ?"#, Query::AUTH_USER_BY_EMAIL => r#"SELECT id, nickname, email, password_hash, confirmed_at, is_active FROM users WHERE email_key = ?"#, + Query::AUTH_USER_BY_SHARE_IDENTIFIER => r#"SELECT id, nickname, email, password_hash, confirmed_at, CASE WHEN is_active THEN 1 ELSE 0 END AS is_active FROM users WHERE is_active = 1 AND (email_key = ?1 OR nickname_key = ?1 OR LOWER(external_id) = ?1) LIMIT 1"#, Query::USER_ATTACH_WORKSPACE => r#"INSERT INTO user_workspaces (user_id, workspace_id) SELECT ?, id FROM workspaces WHERE slug = ?"#, Query::USER_ATTACH_PAD => r#"INSERT INTO user_pads (user_id, pad_id) SELECT ?, id FROM pads WHERE slug = ?"#, Query::USER_LIST_WORKSPACES => r#"SELECT w.slug, w.title, CASE WHEN w.password_hash IS NULL THEN 0 ELSE 1 END AS protected, w.updated_at, CASE WHEN w.is_private THEN 1 ELSE 0 END AS private, 1 AS owned, 'rw' AS permission, '' AS shared_by FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = ? UNION SELECT w.slug, w.title, CASE WHEN w.password_hash IS NULL THEN 0 ELSE 1 END, w.updated_at, CASE WHEN w.is_private THEN 1 ELSE 0 END, 0, rp.permission, COALESCE((SELECT u.nickname FROM user_workspaces owner_uw JOIN users u ON u.id = owner_uw.user_id WHERE owner_uw.workspace_id = w.id LIMIT 1), 'Unknown user') AS shared_by FROM resource_permissions rp JOIN workspaces w ON w.slug = rp.resource_slug WHERE rp.resource_kind = 'workspace' AND rp.user_id = ? ORDER BY updated_at DESC"#, diff --git a/static/js/home.js b/static/js/home.js index c8e1894..35de305 100644 --- a/static/js/home.js +++ b/static/js/home.js @@ -136,8 +136,8 @@ async function loadResources() { dialog.innerHTML = `