fixes and new options
This commit is contained in:
Generated
+1
-1
@@ -2581,7 +2581,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.1.14"
|
version = "0.1.15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"aws-config",
|
"aws-config",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.1.14"
|
version = "0.1.15"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.94"
|
rust-version = "1.94"
|
||||||
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN is_active BOOLEAN NOT NULL DEFAULT TRUE;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN is_active BOOLEAN NOT NULL DEFAULT TRUE;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1;
|
||||||
+15
-3
@@ -1,5 +1,5 @@
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
use crate::{queries, state::SharedState};
|
use crate::{queries, state::SharedState};
|
||||||
|
|
||||||
@@ -172,12 +172,20 @@ pub async fn authenticate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn directory_nickname(display_name: &str, email: &str, username: &str) -> String {
|
fn directory_nickname(display_name: &str, email: &str, username: &str) -> String {
|
||||||
let words: Vec<&str> = display_name.split_whitespace().filter(|v| !v.is_empty()).collect();
|
let words: Vec<&str> = display_name
|
||||||
|
.split_whitespace()
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.collect();
|
||||||
let candidate = if words.len() >= 2 {
|
let candidate = if words.len() >= 2 {
|
||||||
let first = words[0].chars().next().unwrap_or('u');
|
let first = words[0].chars().next().unwrap_or('u');
|
||||||
format!("{}.{}", first, words[words.len() - 1])
|
format!("{}.{}", first, words[words.len() - 1])
|
||||||
} else {
|
} else {
|
||||||
email.split('@').next().filter(|v| !v.is_empty()).unwrap_or(username).to_owned()
|
email
|
||||||
|
.split('@')
|
||||||
|
.next()
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.unwrap_or(username)
|
||||||
|
.to_owned()
|
||||||
};
|
};
|
||||||
candidate.to_lowercase()
|
candidate.to_lowercase()
|
||||||
}
|
}
|
||||||
@@ -241,6 +249,10 @@ pub(super) async fn login(
|
|||||||
})?
|
})?
|
||||||
.ok_or_else(|| AuthError::unauthorized("Invalid organization login or password."))?;
|
.ok_or_else(|| AuthError::unauthorized("Invalid organization login or password."))?;
|
||||||
let user = provision_ldap_user(state, identity).await?;
|
let user = provision_ldap_user(state, identity).await?;
|
||||||
|
if user.is_active == 0 {
|
||||||
|
warn!(user_id = user.id, "LDAP login rejected: inactive account");
|
||||||
|
return Err(AuthError::forbidden("This account is inactive."));
|
||||||
|
}
|
||||||
let session = create_session(state, &user).await?;
|
let session = create_session(state, &user).await?;
|
||||||
info!(user_id = user.id, nickname = %user.nickname, "LDAP login successful");
|
info!(user_id = user.id, nickname = %user.nickname, "LDAP login successful");
|
||||||
Ok(session)
|
Ok(session)
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ pub(super) async fn login(
|
|||||||
let user = find_user_by_email(state, &email)
|
let user = find_user_by_email(state, &email)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| AuthError::unauthorized("Invalid e-mail address or password."))?;
|
.ok_or_else(|| AuthError::unauthorized("Invalid e-mail address or password."))?;
|
||||||
|
if user.is_active == 0 {
|
||||||
|
warn!(user_id = user.id, "login rejected: inactive account");
|
||||||
|
return Err(AuthError::forbidden("This account is inactive."));
|
||||||
|
}
|
||||||
if !verify_password(&user.password_hash, password) {
|
if !verify_password(&user.password_hash, password) {
|
||||||
warn!(user_id = user.id, "login rejected: invalid password");
|
warn!(user_id = user.id, "login rejected: invalid password");
|
||||||
return Err(AuthError::unauthorized(
|
return Err(AuthError::unauthorized(
|
||||||
|
|||||||
+44
-3
@@ -38,6 +38,7 @@ pub struct User {
|
|||||||
pub email: String,
|
pub email: String,
|
||||||
pub password_hash: String,
|
pub password_hash: String,
|
||||||
pub confirmed_at: Option<String>,
|
pub confirmed_at: Option<String>,
|
||||||
|
pub is_active: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -123,6 +124,7 @@ impl<'r> sqlx::FromRow<'r, AnyRow> for User {
|
|||||||
email: crate::row_decode::text(row, "email")?,
|
email: crate::row_decode::text(row, "email")?,
|
||||||
password_hash: crate::row_decode::text(row, "password_hash")?,
|
password_hash: crate::row_decode::text(row, "password_hash")?,
|
||||||
confirmed_at: crate::row_decode::optional_text(row, "confirmed_at")?,
|
confirmed_at: crate::row_decode::optional_text(row, "confirmed_at")?,
|
||||||
|
is_active: row.try_get("is_active")?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -827,12 +829,51 @@ pub async fn confirm_account_action(
|
|||||||
.map_err(AuthError::database)?;
|
.map_err(AuthError::database)?;
|
||||||
"E-mail address changed."
|
"E-mail address changed."
|
||||||
} else if action == "delete" {
|
} else if action == "delete" {
|
||||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_DELETE_USER))
|
let original_nickname: Option<String> = sqlx::query_scalar(queries::get(
|
||||||
.bind(user_id)
|
state.db.kind(),
|
||||||
|
queries::AUTH_NICKNAME_BY_ID,
|
||||||
|
))
|
||||||
|
.bind(user_id)
|
||||||
|
.fetch_optional(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(AuthError::database)?;
|
||||||
|
let deleted_nickname = format!("Deleted user #{user_id}");
|
||||||
|
if let Some(original_nickname) = original_nickname {
|
||||||
|
sqlx::query(queries::get(
|
||||||
|
state.db.kind(),
|
||||||
|
queries::AUTH_ANONYMIZE_NOTE_CREATORS,
|
||||||
|
))
|
||||||
|
.bind(&deleted_nickname)
|
||||||
|
.bind(original_nickname)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(AuthError::database)?;
|
.map_err(AuthError::database)?;
|
||||||
"Account deleted."
|
}
|
||||||
|
let deleted_email = format!("deleted-user-{user_id}@invalid.local");
|
||||||
|
let deleted_password = hash_password(&random_token())?;
|
||||||
|
sqlx::query(queries::get(
|
||||||
|
state.db.kind(),
|
||||||
|
queries::AUTH_DELETE_SESSIONS_BY_USER,
|
||||||
|
))
|
||||||
|
.bind(user_id)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(AuthError::database)?;
|
||||||
|
sqlx::query(queries::get(
|
||||||
|
state.db.kind(),
|
||||||
|
queries::AUTH_ANONYMIZE_USER,
|
||||||
|
))
|
||||||
|
.bind(&deleted_nickname)
|
||||||
|
.bind(normalize(&deleted_nickname))
|
||||||
|
.bind(&deleted_email)
|
||||||
|
.bind(normalize(&deleted_email))
|
||||||
|
.bind(deleted_password)
|
||||||
|
.bind(now.to_rfc3339())
|
||||||
|
.bind(user_id)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(AuthError::database)?;
|
||||||
|
"Account deleted. Content has been preserved under an anonymized owner."
|
||||||
} else {
|
} else {
|
||||||
return Err(AuthError::bad_request("Unknown account action."));
|
return Err(AuthError::bad_request("Unknown account action."));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ pub enum Query {
|
|||||||
AUTH_DIRECTORY_PROFILE_BY_USER,
|
AUTH_DIRECTORY_PROFILE_BY_USER,
|
||||||
AUTH_USER_BY_EXTERNAL_ID,
|
AUTH_USER_BY_EXTERNAL_ID,
|
||||||
AUTH_DELETE_USER,
|
AUTH_DELETE_USER,
|
||||||
|
AUTH_ANONYMIZE_USER,
|
||||||
|
AUTH_ANONYMIZE_NOTE_CREATORS,
|
||||||
|
AUTH_NICKNAME_BY_ID,
|
||||||
AUTH_SESSION_EXPIRES_AT,
|
AUTH_SESSION_EXPIRES_AT,
|
||||||
AUTH_DELETE_SESSION_BY_TOKEN,
|
AUTH_DELETE_SESSION_BY_TOKEN,
|
||||||
AUTH_REFRESH_SESSION,
|
AUTH_REFRESH_SESSION,
|
||||||
@@ -164,6 +167,9 @@ pub const AUTH_UPDATE_DIRECTORY_USER: Query = Query::AUTH_UPDATE_DIRECTORY_USER;
|
|||||||
pub const AUTH_DIRECTORY_PROFILE_BY_USER: Query = Query::AUTH_DIRECTORY_PROFILE_BY_USER;
|
pub const AUTH_DIRECTORY_PROFILE_BY_USER: Query = Query::AUTH_DIRECTORY_PROFILE_BY_USER;
|
||||||
pub const AUTH_USER_BY_EXTERNAL_ID: Query = Query::AUTH_USER_BY_EXTERNAL_ID;
|
pub const AUTH_USER_BY_EXTERNAL_ID: Query = Query::AUTH_USER_BY_EXTERNAL_ID;
|
||||||
pub const AUTH_DELETE_USER: Query = Query::AUTH_DELETE_USER;
|
pub const AUTH_DELETE_USER: Query = Query::AUTH_DELETE_USER;
|
||||||
|
pub const AUTH_ANONYMIZE_USER: Query = Query::AUTH_ANONYMIZE_USER;
|
||||||
|
pub const AUTH_ANONYMIZE_NOTE_CREATORS: Query = Query::AUTH_ANONYMIZE_NOTE_CREATORS;
|
||||||
|
pub const AUTH_NICKNAME_BY_ID: Query = Query::AUTH_NICKNAME_BY_ID;
|
||||||
pub const AUTH_SESSION_EXPIRES_AT: Query = Query::AUTH_SESSION_EXPIRES_AT;
|
pub const AUTH_SESSION_EXPIRES_AT: Query = Query::AUTH_SESSION_EXPIRES_AT;
|
||||||
pub const AUTH_DELETE_SESSION_BY_TOKEN: Query = Query::AUTH_DELETE_SESSION_BY_TOKEN;
|
pub const AUTH_DELETE_SESSION_BY_TOKEN: Query = Query::AUTH_DELETE_SESSION_BY_TOKEN;
|
||||||
pub const AUTH_REFRESH_SESSION: Query = Query::AUTH_REFRESH_SESSION;
|
pub const AUTH_REFRESH_SESSION: Query = Query::AUTH_REFRESH_SESSION;
|
||||||
|
|||||||
@@ -26,8 +26,11 @@ pub fn get(query: Query) -> &'static str {
|
|||||||
Query::AUTH_INSERT_DIRECTORY_USER => r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at, auth_provider, external_id, external_dn, directory_display_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
|
Query::AUTH_INSERT_DIRECTORY_USER => r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at, auth_provider, external_id, external_dn, directory_display_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
|
||||||
Query::AUTH_UPDATE_DIRECTORY_USER => r#"UPDATE users SET nickname = ?, nickname_key = ?, email = ?, email_key = ?, auth_provider = ?, external_id = ?, external_dn = ?, directory_display_name = ?, updated_at = ? WHERE id = ?"#,
|
Query::AUTH_UPDATE_DIRECTORY_USER => r#"UPDATE users SET nickname = ?, nickname_key = ?, email = ?, email_key = ?, auth_provider = ?, external_id = ?, external_dn = ?, directory_display_name = ?, updated_at = ? WHERE id = ?"#,
|
||||||
Query::AUTH_DIRECTORY_PROFILE_BY_USER => r#"SELECT auth_provider, directory_display_name FROM users WHERE id = ?"#,
|
Query::AUTH_DIRECTORY_PROFILE_BY_USER => r#"SELECT auth_provider, directory_display_name FROM users WHERE id = ?"#,
|
||||||
Query::AUTH_USER_BY_EXTERNAL_ID => r#"SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at FROM users WHERE auth_provider = ? AND external_id = ?"#,
|
Query::AUTH_USER_BY_EXTERNAL_ID => 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 auth_provider = ? AND external_id = ?"#,
|
||||||
Query::AUTH_DELETE_USER => r#"DELETE FROM users WHERE id = ?"#,
|
Query::AUTH_DELETE_USER => r#"DELETE FROM users WHERE id = ?"#,
|
||||||
|
Query::AUTH_ANONYMIZE_USER => r#"UPDATE users SET nickname = ?, nickname_key = ?, email = ?, email_key = ?, password_hash = ?, is_active = 0, auth_provider = 'deleted', external_id = NULL, external_dn = NULL, directory_display_name = NULL, updated_at = ? WHERE id = ?"#,
|
||||||
|
Query::AUTH_NICKNAME_BY_ID => r#"SELECT nickname FROM users WHERE id = ?"#,
|
||||||
|
Query::AUTH_ANONYMIZE_NOTE_CREATORS => r#"UPDATE notes SET created_by = ? WHERE created_by = ?"#,
|
||||||
Query::AUTH_SESSION_EXPIRES_AT => r#"SELECT expires_at FROM user_sessions WHERE token = ?"#,
|
Query::AUTH_SESSION_EXPIRES_AT => r#"SELECT expires_at FROM user_sessions WHERE token = ?"#,
|
||||||
Query::AUTH_DELETE_SESSION_BY_TOKEN => r#"DELETE FROM user_sessions WHERE token = ?"#,
|
Query::AUTH_DELETE_SESSION_BY_TOKEN => r#"DELETE FROM user_sessions WHERE token = ?"#,
|
||||||
Query::AUTH_REFRESH_SESSION => r#"UPDATE user_sessions SET expires_at = ? WHERE token = ? AND expires_at > ?"#,
|
Query::AUTH_REFRESH_SESSION => r#"UPDATE user_sessions SET expires_at = ? WHERE token = ? AND expires_at > ?"#,
|
||||||
@@ -42,10 +45,10 @@ pub fn get(query: Query) -> &'static str {
|
|||||||
Query::AUTH_UPDATE_PASSWORD => r#"UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?"#,
|
Query::AUTH_UPDATE_PASSWORD => r#"UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?"#,
|
||||||
Query::AUTH_CONSUME_RESET_TOKEN => r#"UPDATE password_reset_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL AND expires_at > ?"#,
|
Query::AUTH_CONSUME_RESET_TOKEN => r#"UPDATE password_reset_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL AND expires_at > ?"#,
|
||||||
Query::AUTH_DELETE_SESSIONS_BY_USER => r#"DELETE FROM user_sessions WHERE user_id = ?"#,
|
Query::AUTH_DELETE_SESSIONS_BY_USER => r#"DELETE FROM user_sessions WHERE user_id = ?"#,
|
||||||
Query::AUTH_USER_BY_SESSION => r#"SELECT u.id, u.nickname, u.email, CAST(u.password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, u.confirmed_at FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ? AND s.expires_at > ?"#,
|
Query::AUTH_USER_BY_SESSION => r#"SELECT u.id, u.nickname, u.email, CAST(u.password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, u.confirmed_at, CAST(CASE WHEN u.is_active THEN 1 ELSE 0 END AS SIGNED) AS is_active FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ? AND s.expires_at > ? AND u.is_active = 1"#,
|
||||||
Query::AUTH_INSERT_SESSION => r#"INSERT INTO user_sessions (token, user_id, expires_at) VALUES (?, ?, ?)"#,
|
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 FROM users WHERE nickname_key = ?"#,
|
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 FROM users WHERE email_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::USER_ATTACH_WORKSPACE => r#"INSERT INTO user_workspaces (user_id, workspace_id) SELECT ?, id FROM workspaces WHERE slug = ?"#,
|
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_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"#,
|
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"#,
|
||||||
|
|||||||
@@ -26,8 +26,11 @@ pub fn get(query: Query) -> &'static str {
|
|||||||
Query::AUTH_INSERT_DIRECTORY_USER => r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at, auth_provider, external_id, external_dn, directory_display_name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"#,
|
Query::AUTH_INSERT_DIRECTORY_USER => r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at, auth_provider, external_id, external_dn, directory_display_name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"#,
|
||||||
Query::AUTH_UPDATE_DIRECTORY_USER => r#"UPDATE users SET nickname = $1, nickname_key = $2, email = $3, email_key = $4, auth_provider = $5, external_id = $6, external_dn = $7, directory_display_name = $8, updated_at = $9 WHERE id = $10"#,
|
Query::AUTH_UPDATE_DIRECTORY_USER => r#"UPDATE users SET nickname = $1, nickname_key = $2, email = $3, email_key = $4, auth_provider = $5, external_id = $6, external_dn = $7, directory_display_name = $8, updated_at = $9 WHERE id = $10"#,
|
||||||
Query::AUTH_DIRECTORY_PROFILE_BY_USER => r#"SELECT auth_provider, directory_display_name FROM users WHERE id = $1"#,
|
Query::AUTH_DIRECTORY_PROFILE_BY_USER => r#"SELECT auth_provider, directory_display_name FROM users WHERE id = $1"#,
|
||||||
Query::AUTH_USER_BY_EXTERNAL_ID => r#"SELECT id, nickname, email, password_hash, confirmed_at FROM users WHERE auth_provider = $1 AND external_id = $2"#,
|
Query::AUTH_USER_BY_EXTERNAL_ID => 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 auth_provider = $1 AND external_id = $2"#,
|
||||||
Query::AUTH_DELETE_USER => r#"DELETE FROM users WHERE id = $1"#,
|
Query::AUTH_DELETE_USER => r#"DELETE FROM users WHERE id = $1"#,
|
||||||
|
Query::AUTH_ANONYMIZE_USER => r#"UPDATE users SET nickname = $1, nickname_key = $2, email = $3, email_key = $4, password_hash = $5, is_active = FALSE, auth_provider = 'deleted', external_id = NULL, external_dn = NULL, directory_display_name = NULL, updated_at = $6 WHERE id = $7"#,
|
||||||
|
Query::AUTH_NICKNAME_BY_ID => r#"SELECT nickname FROM users WHERE id = $1"#,
|
||||||
|
Query::AUTH_ANONYMIZE_NOTE_CREATORS => r#"UPDATE notes SET created_by = $1 WHERE created_by = $2"#,
|
||||||
Query::AUTH_SESSION_EXPIRES_AT => r#"SELECT expires_at FROM user_sessions WHERE token = $1"#,
|
Query::AUTH_SESSION_EXPIRES_AT => r#"SELECT expires_at FROM user_sessions WHERE token = $1"#,
|
||||||
Query::AUTH_DELETE_SESSION_BY_TOKEN => r#"DELETE FROM user_sessions WHERE token = $1"#,
|
Query::AUTH_DELETE_SESSION_BY_TOKEN => r#"DELETE FROM user_sessions WHERE token = $1"#,
|
||||||
Query::AUTH_REFRESH_SESSION => r#"UPDATE user_sessions SET expires_at = $1 WHERE token = $2 AND expires_at > $3"#,
|
Query::AUTH_REFRESH_SESSION => r#"UPDATE user_sessions SET expires_at = $1 WHERE token = $2 AND expires_at > $3"#,
|
||||||
@@ -42,10 +45,10 @@ pub fn get(query: Query) -> &'static str {
|
|||||||
Query::AUTH_UPDATE_PASSWORD => r#"UPDATE users SET password_hash = $1, updated_at = $2 WHERE id = $3"#,
|
Query::AUTH_UPDATE_PASSWORD => r#"UPDATE users SET password_hash = $1, updated_at = $2 WHERE id = $3"#,
|
||||||
Query::AUTH_CONSUME_RESET_TOKEN => r#"UPDATE password_reset_tokens SET used_at = $1 WHERE token = $2 AND used_at IS NULL AND expires_at > $3"#,
|
Query::AUTH_CONSUME_RESET_TOKEN => r#"UPDATE password_reset_tokens SET used_at = $1 WHERE token = $2 AND used_at IS NULL AND expires_at > $3"#,
|
||||||
Query::AUTH_DELETE_SESSIONS_BY_USER => r#"DELETE FROM user_sessions WHERE user_id = $1"#,
|
Query::AUTH_DELETE_SESSIONS_BY_USER => r#"DELETE FROM user_sessions WHERE user_id = $1"#,
|
||||||
Query::AUTH_USER_BY_SESSION => r#"SELECT u.id, u.nickname, u.email, u.password_hash, u.confirmed_at FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.token = $1 AND s.expires_at > $2"#,
|
Query::AUTH_USER_BY_SESSION => r#"SELECT u.id, u.nickname, u.email, u.password_hash, u.confirmed_at, CAST(CASE WHEN u.is_active THEN 1 ELSE 0 END AS BIGINT) AS is_active FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.token = $1 AND s.expires_at > $2 AND u.is_active = TRUE"#,
|
||||||
Query::AUTH_INSERT_SESSION => r#"INSERT INTO user_sessions (token, user_id, expires_at) VALUES ($1, $2, $3)"#,
|
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 FROM users WHERE nickname_key = $1"#,
|
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 FROM users WHERE email_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::USER_ATTACH_WORKSPACE => r#"INSERT INTO user_workspaces (user_id, workspace_id) SELECT $1, id FROM workspaces WHERE slug = $2"#,
|
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_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"#,
|
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"#,
|
||||||
|
|||||||
@@ -26,8 +26,11 @@ pub fn get(query: Query) -> &'static str {
|
|||||||
Query::AUTH_INSERT_DIRECTORY_USER => r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at, auth_provider, external_id, external_dn, directory_display_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
|
Query::AUTH_INSERT_DIRECTORY_USER => r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at, auth_provider, external_id, external_dn, directory_display_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
|
||||||
Query::AUTH_UPDATE_DIRECTORY_USER => r#"UPDATE users SET nickname = ?, nickname_key = ?, email = ?, email_key = ?, auth_provider = ?, external_id = ?, external_dn = ?, directory_display_name = ?, updated_at = ? WHERE id = ?"#,
|
Query::AUTH_UPDATE_DIRECTORY_USER => r#"UPDATE users SET nickname = ?, nickname_key = ?, email = ?, email_key = ?, auth_provider = ?, external_id = ?, external_dn = ?, directory_display_name = ?, updated_at = ? WHERE id = ?"#,
|
||||||
Query::AUTH_DIRECTORY_PROFILE_BY_USER => r#"SELECT auth_provider, directory_display_name FROM users WHERE id = ?"#,
|
Query::AUTH_DIRECTORY_PROFILE_BY_USER => r#"SELECT auth_provider, directory_display_name FROM users WHERE id = ?"#,
|
||||||
Query::AUTH_USER_BY_EXTERNAL_ID => r#"SELECT id, nickname, email, password_hash, confirmed_at FROM users WHERE auth_provider = ? AND external_id = ?"#,
|
Query::AUTH_USER_BY_EXTERNAL_ID => r#"SELECT id, nickname, email, password_hash, confirmed_at, is_active FROM users WHERE auth_provider = ? AND external_id = ?"#,
|
||||||
Query::AUTH_DELETE_USER => r#"DELETE FROM users WHERE id = ?"#,
|
Query::AUTH_DELETE_USER => r#"DELETE FROM users WHERE id = ?"#,
|
||||||
|
Query::AUTH_ANONYMIZE_USER => r#"UPDATE users SET nickname = ?, nickname_key = ?, email = ?, email_key = ?, password_hash = ?, is_active = 0, auth_provider = 'deleted', external_id = NULL, external_dn = NULL, directory_display_name = NULL, updated_at = ? WHERE id = ?"#,
|
||||||
|
Query::AUTH_NICKNAME_BY_ID => r#"SELECT nickname FROM users WHERE id = ?"#,
|
||||||
|
Query::AUTH_ANONYMIZE_NOTE_CREATORS => r#"UPDATE notes SET created_by = ? WHERE created_by = ?"#,
|
||||||
Query::AUTH_SESSION_EXPIRES_AT => r#"SELECT expires_at FROM user_sessions WHERE token = ?"#,
|
Query::AUTH_SESSION_EXPIRES_AT => r#"SELECT expires_at FROM user_sessions WHERE token = ?"#,
|
||||||
Query::AUTH_DELETE_SESSION_BY_TOKEN => r#"DELETE FROM user_sessions WHERE token = ?"#,
|
Query::AUTH_DELETE_SESSION_BY_TOKEN => r#"DELETE FROM user_sessions WHERE token = ?"#,
|
||||||
Query::AUTH_REFRESH_SESSION => r#"UPDATE user_sessions SET expires_at = ? WHERE token = ? AND expires_at > ?"#,
|
Query::AUTH_REFRESH_SESSION => r#"UPDATE user_sessions SET expires_at = ? WHERE token = ? AND expires_at > ?"#,
|
||||||
@@ -42,10 +45,10 @@ pub fn get(query: Query) -> &'static str {
|
|||||||
Query::AUTH_UPDATE_PASSWORD => r#"UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?"#,
|
Query::AUTH_UPDATE_PASSWORD => r#"UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?"#,
|
||||||
Query::AUTH_CONSUME_RESET_TOKEN => r#"UPDATE password_reset_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL AND expires_at > ?"#,
|
Query::AUTH_CONSUME_RESET_TOKEN => r#"UPDATE password_reset_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL AND expires_at > ?"#,
|
||||||
Query::AUTH_DELETE_SESSIONS_BY_USER => r#"DELETE FROM user_sessions WHERE user_id = ?"#,
|
Query::AUTH_DELETE_SESSIONS_BY_USER => r#"DELETE FROM user_sessions WHERE user_id = ?"#,
|
||||||
Query::AUTH_USER_BY_SESSION => r#"SELECT u.id, u.nickname, u.email, u.password_hash, u.confirmed_at FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ? AND s.expires_at > ?"#,
|
Query::AUTH_USER_BY_SESSION => r#"SELECT u.id, u.nickname, u.email, u.password_hash, u.confirmed_at, u.is_active FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ? AND s.expires_at > ? AND u.is_active = 1"#,
|
||||||
Query::AUTH_INSERT_SESSION => r#"INSERT INTO user_sessions (token, user_id, expires_at) VALUES (?, ?, ?)"#,
|
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 FROM users WHERE nickname_key = ?"#,
|
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 FROM users WHERE email_key = ?"#,
|
Query::AUTH_USER_BY_EMAIL => r#"SELECT id, nickname, email, password_hash, confirmed_at, is_active FROM users WHERE email_key = ?"#,
|
||||||
Query::USER_ATTACH_WORKSPACE => r#"INSERT INTO user_workspaces (user_id, workspace_id) SELECT ?, id FROM workspaces WHERE slug = ?"#,
|
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_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"#,
|
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"#,
|
||||||
|
|||||||
+14
-1
@@ -41,6 +41,7 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, to
|
|||||||
<div class="file-actions">
|
<div class="file-actions">
|
||||||
<button class="action-button action-button--secondary compact-button" data-show-file-code="link" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Link</button>
|
<button class="action-button action-button--secondary compact-button" data-show-file-code="link" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Link</button>
|
||||||
<button class="action-button action-button--primary compact-button" data-show-file-code="markdown" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Markdown</button>
|
<button class="action-button action-button--primary compact-button" data-show-file-code="markdown" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Markdown</button>
|
||||||
|
<button class="action-button action-button--primary compact-button" data-add-file-to-note data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Add to note</button>
|
||||||
${canDelete() ? `<button class="action-button action-button--danger compact-button" data-delete-file="${file.id}" data-file-name="${escapeHtml(file.filename)}">Delete</button>` : ""}
|
${canDelete() ? `<button class="action-button action-button--danger compact-button" data-delete-file="${file.id}" data-file-name="${escapeHtml(file.filename)}">Delete</button>` : ""}
|
||||||
</div>
|
</div>
|
||||||
<div class="file-code" hidden><textarea readonly aria-label="Generated file code"></textarea><button class="action-button action-button--primary compact-button" data-copy-generated>Copy</button></div>
|
<div class="file-code" hidden><textarea readonly aria-label="Generated file code"></textarea><button class="action-button action-button--primary compact-button" data-copy-generated>Copy</button></div>
|
||||||
@@ -78,13 +79,25 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, to
|
|||||||
footer.addEventListener("click", () => loadFiles({ open: true }));
|
footer.addEventListener("click", () => loadFiles({ open: true }));
|
||||||
document.querySelector("#close-files").addEventListener("click", () => dialog.close());
|
document.querySelector("#close-files").addEventListener("click", () => dialog.close());
|
||||||
list.addEventListener("click", async event => {
|
list.addEventListener("click", async event => {
|
||||||
|
const addButton = event.target.closest("[data-add-file-to-note]");
|
||||||
|
if (addButton) {
|
||||||
|
const relative = safeAppUrl(addButton.dataset.url);
|
||||||
|
const text = addButton.dataset.mime?.startsWith("image/") ? `` : `[${addButton.dataset.name}](${relative})`;
|
||||||
|
editor.setRangeText(text, editor.selectionStart, editor.selectionEnd, "end");
|
||||||
|
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
toast("Added to note");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const showButton = event.target.closest("[data-show-file-code]");
|
const showButton = event.target.closest("[data-show-file-code]");
|
||||||
if (showButton) {
|
if (showButton) {
|
||||||
const panel = showButton.closest(".file-row").querySelector(".file-code");
|
const panel = showButton.closest(".file-row").querySelector(".file-code");
|
||||||
const output = panel.querySelector("textarea");
|
const output = panel.querySelector("textarea");
|
||||||
const absolute = new URL(safeAppUrl(showButton.dataset.url), location.origin).href;
|
const absolute = new URL(safeAppUrl(showButton.dataset.url), location.origin).href;
|
||||||
let text = absolute;
|
let text = absolute;
|
||||||
if (showButton.dataset.showFileCode === "markdown") text = showButton.dataset.mime?.startsWith("image/") ? `` : `[${showButton.dataset.name}](${absolute})`;
|
if (showButton.dataset.showFileCode === "markdown") {
|
||||||
|
const relative = safeAppUrl(showButton.dataset.url);
|
||||||
|
text = showButton.dataset.mime?.startsWith("image/") ? `` : `[${showButton.dataset.name}](${relative})`;
|
||||||
|
}
|
||||||
output.value = text; panel.hidden = false; output.focus(); output.select(); return;
|
output.value = text; panel.hidden = false; output.focus(); output.select(); return;
|
||||||
}
|
}
|
||||||
const copyButton = event.target.closest("[data-copy-generated]");
|
const copyButton = event.target.closest("[data-copy-generated]");
|
||||||
|
|||||||
Reference in New Issue
Block a user