From e7a580a27821a6435019a06eb1f1289899647256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 27 Jul 2026 23:02:20 +0200 Subject: [PATCH] fix in smtp and split rs files --- src/api/mod.rs | 1 - src/app/mod.rs | 3 +- src/app/pages.rs | 5 +- src/auth/ldap.rs | 12 +- src/config/mod.rs | 71 +++++--- src/config/smtp.rs | 5 +- src/config/values.rs | 171 +++++++++++++++----- src/db/mod.rs | 3 +- src/main.rs | 46 ++++-- src/queries/mod.rs | 23 ++- src/queries/mysql.rs | 348 ++++++++++++++++++++++++++++++---------- src/queries/postgres.rs | 336 ++++++++++++++++++++++++++++---------- src/queries/sqlite.rs | 336 ++++++++++++++++++++++++++++---------- src/row_decode.rs | 2 +- src/websocket/mod.rs | 1 - 15 files changed, 1022 insertions(+), 341 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 99bf426..9f06f62 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -20,7 +20,6 @@ const MIN_PASSWORD_LENGTH: usize = 8; const MAX_PASSWORD_LENGTH: usize = 128; const MIN_WORKSPACE_SLUG_LENGTH: usize = 6; - include!("workspace_notes.rs"); include!("pads_public.rs"); include!("files.rs"); diff --git a/src/app/mod.rs b/src/app/mod.rs index 482466d..f5448be 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,6 +1,5 @@ mod pages; -use pages::*; use axum::{ Router, extract::{DefaultBodyLimit, Request}, @@ -9,6 +8,7 @@ use axum::{ response::Response, routing::{get, post}, }; +use pages::*; use tower::{ServiceBuilder, service_fn}; use tower_http::{services::ServeDir, set_header::SetResponseHeaderLayer, trace::TraceLayer}; @@ -220,4 +220,3 @@ async fn add_non_asset_security_headers(request: Request, next: Next) -> Respons response } - diff --git a/src/app/pages.rs b/src/app/pages.rs index 17c13eb..0582032 100644 --- a/src/app/pages.rs +++ b/src/app/pages.rs @@ -78,7 +78,10 @@ pub(super) async fn pad(State(state): State, Path(slug): Path, Path(token): Path) -> Response { +pub(super) async fn public_page( + State(state): State, + Path(token): Path, +) -> Response { match db::find_published_page(&state.db, &token).await { Ok(Some(_)) => assets::render_html( include_str!("../../static/public.html"), diff --git a/src/auth/ldap.rs b/src/auth/ldap.rs index 19cea69..39f58cb 100644 --- a/src/auth/ldap.rs +++ b/src/auth/ldap.rs @@ -172,12 +172,20 @@ pub async fn authenticate( } 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 first = words[0].chars().next().unwrap_or('u'); format!("{}.{}", first, words[words.len() - 1]) } 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() } diff --git a/src/config/mod.rs b/src/config/mod.rs index 688ee11..99e3d43 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,8 +1,8 @@ mod smtp; mod values; -use std::{net::IpAddr, path::Path}; use smtp::load_smtp; +use std::{net::IpAddr, path::Path}; use values::ConfigValues; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -14,7 +14,12 @@ pub enum AuthorizationType { impl AuthorizationType { fn from_values(values: &ConfigValues) -> Result> { - match values.get("AUTHORIZATION_TYPE", "local").trim().to_ascii_lowercase().as_str() { + match values + .get("AUTHORIZATION_TYPE", "local") + .trim() + .to_ascii_lowercase() + .as_str() + { "local" => Ok(Self::Local), "ldap" => Ok(Self::Ldap), "ad" => Ok(Self::Ad), @@ -63,13 +68,22 @@ impl Config { let port = values.get("APP_PORT", "3000").parse()?; let database_max_connections = values.get("DATABASE_MAX_CONNECTIONS", "8").parse()?; let upload_max_size_mb: usize = values.get("UPLOAD_MAX_SIZE_MB", "20").parse()?; - let anonymous_access_token_ttl_days = values.positive_i64("ANONYMOUS_ACCESS_TOKEN_TTL_DAYS", 7)?; + let anonymous_access_token_ttl_days = + values.positive_i64("ANONYMOUS_ACCESS_TOKEN_TTL_DAYS", 7)?; let user_session_ttl_days = values.positive_i64("USER_SESSION_TTL_DAYS", 3)?; - let unconfirmed_account_ttl_days = values.positive_i64("UNCONFIRMED_ACCOUNT_TTL_DAYS", 3)?; + let unconfirmed_account_ttl_days = + values.positive_i64("UNCONFIRMED_ACCOUNT_TTL_DAYS", 3)?; let files_dir = values.get("FILES_DIR", "data/files"); - let storage = match values.get("STORAGE_DRIVER", "local").trim().to_ascii_lowercase().as_str() { - "local" => crate::storage::StorageConfig::Local { root: files_dir.clone().into() }, + let storage = match values + .get("STORAGE_DRIVER", "local") + .trim() + .to_ascii_lowercase() + .as_str() + { + "local" => crate::storage::StorageConfig::Local { + root: files_dir.clone().into(), + }, "s3" => crate::storage::StorageConfig::S3 { endpoint: values.optional("S3_ENDPOINT"), region: values.get("S3_REGION", "us-east-1"), @@ -92,7 +106,10 @@ impl Config { let context = format!("AUTHORIZATION_TYPE={}", authorization_type.as_str()); let (default_filter, default_username_attribute) = match authorization_type { AuthorizationType::Ldap => ("(uid={username})", "uid"), - AuthorizationType::Ad => ("(|(sAMAccountName={username})(userPrincipalName={username}))", "sAMAccountName"), + AuthorizationType::Ad => ( + "(|(sAMAccountName={username})(userPrincipalName={username}))", + "sAMAccountName", + ), AuthorizationType::Local => unreachable!(), }; Some(crate::auth::ldap::LdapConfig { @@ -102,21 +119,28 @@ impl Config { bind_password: values.get("LDAP_BIND_PASSWORD", ""), base_dn: values.required("LDAP_BASE_DN", &context)?, user_filter: values.get("LDAP_USER_FILTER", default_filter), - username_attribute: values.get("LDAP_USERNAME_ATTRIBUTE", default_username_attribute), + username_attribute: values + .get("LDAP_USERNAME_ATTRIBUTE", default_username_attribute), email_attribute: values.get("LDAP_EMAIL_ATTRIBUTE", "mail"), - display_name_attribute: values.get("LDAP_DISPLAY_NAME_ATTRIBUTE", "displayName"), - external_id_attribute: values.get("LDAP_EXTERNAL_ID_ATTRIBUTE", match authorization_type { - AuthorizationType::Ldap => "entryUUID", - AuthorizationType::Ad => "objectGUID", - AuthorizationType::Local => unreachable!(), - }), + display_name_attribute: values + .get("LDAP_DISPLAY_NAME_ATTRIBUTE", "displayName"), + external_id_attribute: values.get( + "LDAP_EXTERNAL_ID_ATTRIBUTE", + match authorization_type { + AuthorizationType::Ldap => "entryUUID", + AuthorizationType::Ad => "objectGUID", + AuthorizationType::Local => unreachable!(), + }, + ), organization: values.get("LDAP_ORGANIZATION", "organization"), provider: authorization_type.as_str().to_owned(), email_required: values.bool("LDAP_EMAIL_REQUIRED", true)?, link_existing_by_email: values.bool("LDAP_LINK_EXISTING_BY_EMAIL", false)?, tls_verify: values.bool("LDAP_TLS_VERIFY", true)?, - connect_timeout_seconds: values.positive_u64("LDAP_CONNECT_TIMEOUT_SECONDS", 5)?, - operation_timeout_seconds: values.positive_u64("LDAP_OPERATION_TIMEOUT_SECONDS", 10)?, + connect_timeout_seconds: values + .positive_u64("LDAP_CONNECT_TIMEOUT_SECONDS", 5)?, + operation_timeout_seconds: values + .positive_u64("LDAP_OPERATION_TIMEOUT_SECONDS", 10)?, }) } }; @@ -131,10 +155,14 @@ impl Config { static_dir: values.get("STATIC_DIR", "static"), files_dir, storage, - upload_max_size_bytes: upload_max_size_mb.checked_mul(1024 * 1024).ok_or("UPLOAD_MAX_SIZE_MB is too large")?, + upload_max_size_bytes: upload_max_size_mb + .checked_mul(1024 * 1024) + .ok_or("UPLOAD_MAX_SIZE_MB is too large")?, asset_version: env!("CARGO_PKG_VERSION").to_owned(), - asset_cache_max_age_seconds: values.nonnegative_u64("ASSET_CACHE_MAX_AGE_SECONDS", 600)?, - file_cache_max_age_seconds: values.nonnegative_u64("FILE_CACHE_MAX_AGE_SECONDS", 600)?, + asset_cache_max_age_seconds: values + .nonnegative_u64("ASSET_CACHE_MAX_AGE_SECONDS", 600)?, + file_cache_max_age_seconds: values + .nonnegative_u64("FILE_CACHE_MAX_AGE_SECONDS", 600)?, smtp, registration_enabled: values.bool("REGISTRATION_ENABLED", false)?, account_confirmation_required: values.bool("ACCOUNT_CONFIRMATION_REQUIRED", false)?, @@ -165,12 +193,11 @@ impl Config { return Err("STATIC_DIR and FILES_DIR cannot be empty".into()); } if let Some(smtp) = &self.smtp { - if !(smtp.public_url.starts_with("http://") || smtp.public_url.starts_with("https://")) { + if !(smtp.public_url.starts_with("http://") || smtp.public_url.starts_with("https://")) + { return Err("PUBLIC_URL must start with http:// or https://".into()); } } Ok(()) } } - - diff --git a/src/config/smtp.rs b/src/config/smtp.rs index 39ebb4c..2193ccd 100644 --- a/src/config/smtp.rs +++ b/src/config/smtp.rs @@ -84,7 +84,10 @@ mod tests { #[test] fn accepts_supported_security_modes() { assert_eq!(parse_smtp_security("none").unwrap(), SmtpSecurity::None); - assert_eq!(parse_smtp_security("starttls").unwrap(), SmtpSecurity::StartTls); + assert_eq!( + parse_smtp_security("starttls").unwrap(), + SmtpSecurity::StartTls + ); assert_eq!(parse_smtp_security("tls").unwrap(), SmtpSecurity::Tls); assert!(parse_smtp_security("auto").is_err()); } diff --git a/src/config/values.rs b/src/config/values.rs index 83e8e43..e873644 100644 --- a/src/config/values.rs +++ b/src/config/values.rs @@ -1,19 +1,53 @@ use std::{collections::HashMap, env, path::Path}; const KNOWN_CONFIG_KEYS: &[&str] = &[ - "APP_HOST", "APP_PORT", "DATABASE_URL", "DATABASE_MAX_CONNECTIONS", - "STATIC_DIR", "FILES_DIR", "STORAGE_DRIVER", "UPLOAD_MAX_SIZE_MB", - "ASSET_CACHE_MAX_AGE_SECONDS", "FILE_CACHE_MAX_AGE_SECONDS", - "REGISTRATION_ENABLED", "ACCOUNT_CONFIRMATION_REQUIRED", "SHARE_CONFIRMATION_REQUIRED", - "FRONTEND_LOG_LEVEL", "ANONYMOUS_ACCESS_TOKEN_TTL_DAYS", "USER_SESSION_TTL_DAYS", - "UNCONFIRMED_ACCOUNT_TTL_DAYS", "AUTHORIZATION_TYPE", - "S3_ENDPOINT", "S3_REGION", "S3_BUCKET", "S3_ACCESS_KEY", "S3_SECRET_KEY", - "S3_FORCE_PATH_STYLE", "SMTP_HOST", "SMTP_PORT", "SMTP_SECURITY", "SMTP_USERNAME", "SMTP_PASSWORD", - "SMTP_FROM", "PUBLIC_URL", "LDAP_URL", "LDAP_STARTTLS", "LDAP_BIND_DN", - "LDAP_BIND_PASSWORD", "LDAP_BASE_DN", "LDAP_USER_FILTER", "LDAP_USERNAME_ATTRIBUTE", - "LDAP_EMAIL_ATTRIBUTE", "LDAP_DISPLAY_NAME_ATTRIBUTE", "LDAP_EXTERNAL_ID_ATTRIBUTE", - "LDAP_ORGANIZATION", "LDAP_EMAIL_REQUIRED", "LDAP_LINK_EXISTING_BY_EMAIL", - "LDAP_TLS_VERIFY", "LDAP_CONNECT_TIMEOUT_SECONDS", "LDAP_OPERATION_TIMEOUT_SECONDS", + "APP_HOST", + "APP_PORT", + "DATABASE_URL", + "DATABASE_MAX_CONNECTIONS", + "STATIC_DIR", + "FILES_DIR", + "STORAGE_DRIVER", + "UPLOAD_MAX_SIZE_MB", + "ASSET_CACHE_MAX_AGE_SECONDS", + "FILE_CACHE_MAX_AGE_SECONDS", + "REGISTRATION_ENABLED", + "ACCOUNT_CONFIRMATION_REQUIRED", + "SHARE_CONFIRMATION_REQUIRED", + "FRONTEND_LOG_LEVEL", + "ANONYMOUS_ACCESS_TOKEN_TTL_DAYS", + "USER_SESSION_TTL_DAYS", + "UNCONFIRMED_ACCOUNT_TTL_DAYS", + "AUTHORIZATION_TYPE", + "S3_ENDPOINT", + "S3_REGION", + "S3_BUCKET", + "S3_ACCESS_KEY", + "S3_SECRET_KEY", + "S3_FORCE_PATH_STYLE", + "SMTP_HOST", + "SMTP_PORT", + "SMTP_SECURITY", + "SMTP_USERNAME", + "SMTP_PASSWORD", + "SMTP_FROM", + "PUBLIC_URL", + "LDAP_URL", + "LDAP_STARTTLS", + "LDAP_BIND_DN", + "LDAP_BIND_PASSWORD", + "LDAP_BASE_DN", + "LDAP_USER_FILTER", + "LDAP_USERNAME_ATTRIBUTE", + "LDAP_EMAIL_ATTRIBUTE", + "LDAP_DISPLAY_NAME_ATTRIBUTE", + "LDAP_EXTERNAL_ID_ATTRIBUTE", + "LDAP_ORGANIZATION", + "LDAP_EMAIL_REQUIRED", + "LDAP_LINK_EXISTING_BY_EMAIL", + "LDAP_TLS_VERIFY", + "LDAP_CONNECT_TIMEOUT_SECONDS", + "LDAP_OPERATION_TIMEOUT_SECONDS", ]; #[derive(Default)] @@ -23,7 +57,9 @@ pub(super) struct ConfigValues { impl ConfigValues { pub(super) fn load(path: Option<&Path>) -> Result> { - let Some(path) = path else { return Ok(Self::default()); }; + let Some(path) = path else { + return Ok(Self::default()); + }; let content = std::fs::read_to_string(path) .map_err(|error| format!("cannot read config file {}: {error}", path.display()))?; let file = parse_yaml_config(&content) @@ -37,26 +73,50 @@ impl ConfigValues { } pub(super) fn get(&self, name: &str, default: &str) -> String { - env::var(name).ok().or_else(|| self.file.get(name).cloned()).unwrap_or_else(|| default.to_owned()) + env::var(name) + .ok() + .or_else(|| self.file.get(name).cloned()) + .unwrap_or_else(|| default.to_owned()) } pub(super) fn optional(&self, name: &str) -> Option { - env::var(name).ok().or_else(|| self.file.get(name).cloned()).filter(|value| !value.trim().is_empty()) + env::var(name) + .ok() + .or_else(|| self.file.get(name).cloned()) + .filter(|value| !value.trim().is_empty()) } - pub(super) fn required(&self, name: &str, context: &str) -> Result> { - self.optional(name).ok_or_else(|| format!("{name} is required when {context}").into()) + pub(super) fn required( + &self, + name: &str, + context: &str, + ) -> Result> { + self.optional(name) + .ok_or_else(|| format!("{name} is required when {context}").into()) } - pub(super) fn bool(&self, name: &str, default: bool) -> Result> { - match self.get(name, if default { "true" } else { "false" }).trim().to_ascii_lowercase().as_str() { + pub(super) fn bool( + &self, + name: &str, + default: bool, + ) -> Result> { + match self + .get(name, if default { "true" } else { "false" }) + .trim() + .to_ascii_lowercase() + .as_str() + { "1" | "true" | "yes" | "on" => Ok(true), "0" | "false" | "no" | "off" => Ok(false), _ => Err(format!("{name} must be true or false").into()), } } - pub(super) fn log_level(&self, name: &str, default: &str) -> Result> { + pub(super) fn log_level( + &self, + name: &str, + default: &str, + ) -> Result> { let value = self.get(name, default).trim().to_ascii_lowercase(); match value.as_str() { "off" | "error" | "warn" | "info" | "debug" => Ok(value), @@ -64,20 +124,44 @@ impl ConfigValues { } } - pub(super) fn positive_i64(&self, name: &str, default: i64) -> Result> { - let value: i64 = self.get(name, &default.to_string()).parse().map_err(|_| format!("{name} must be an integer"))?; - if value <= 0 { return Err(format!("{name} must be greater than 0").into()); } + pub(super) fn positive_i64( + &self, + name: &str, + default: i64, + ) -> Result> { + let value: i64 = self + .get(name, &default.to_string()) + .parse() + .map_err(|_| format!("{name} must be an integer"))?; + if value <= 0 { + return Err(format!("{name} must be greater than 0").into()); + } Ok(value) } - pub(super) fn positive_u64(&self, name: &str, default: u64) -> Result> { - let value: u64 = self.get(name, &default.to_string()).parse().map_err(|_| format!("{name} must be a non-negative integer"))?; - if value == 0 { return Err(format!("{name} must be greater than 0").into()); } + pub(super) fn positive_u64( + &self, + name: &str, + default: u64, + ) -> Result> { + let value: u64 = self + .get(name, &default.to_string()) + .parse() + .map_err(|_| format!("{name} must be a non-negative integer"))?; + if value == 0 { + return Err(format!("{name} must be greater than 0").into()); + } Ok(value) } - pub(super) fn nonnegative_u64(&self, name: &str, default: u64) -> Result> { - self.get(name, &default.to_string()).parse().map_err(|_| format!("{name} must be a non-negative integer").into()) + pub(super) fn nonnegative_u64( + &self, + name: &str, + default: u64, + ) -> Result> { + self.get(name, &default.to_string()) + .parse() + .map_err(|_| format!("{name} must be a non-negative integer").into()) } } @@ -88,7 +172,9 @@ fn parse_yaml_config(content: &str) -> Result, String> { for (index, original) in content.lines().enumerate() { let line_number = index + 1; if original.contains('\t') { - return Err(format!("line {line_number}: tabs are not allowed for indentation")); + return Err(format!( + "line {line_number}: tabs are not allowed for indentation" + )); } let without_comment = strip_yaml_comment(original); if without_comment.trim().is_empty() || without_comment.trim() == "---" { @@ -100,10 +186,17 @@ fn parse_yaml_config(content: &str) -> Result, String> { .split_once(':') .ok_or_else(|| format!("line {line_number}: expected key: value"))?; let key = raw_key.trim(); - if key.is_empty() || !key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') { + if key.is_empty() + || !key + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + { return Err(format!("line {line_number}: invalid key {key:?}")); } - while sections.last().is_some_and(|(section_indent, _)| *section_indent >= indent) { + while sections + .last() + .is_some_and(|(section_indent, _)| *section_indent >= indent) + { sections.pop(); } let normalized = key.to_ascii_uppercase().replace('-', "_"); @@ -112,14 +205,19 @@ fn parse_yaml_config(content: &str) -> Result, String> { sections.push((indent, normalized)); continue; } - if matches!(value.chars().next(), Some('[' | '{' | '|' | '>' | '&' | '*' | '!')) { - return Err(format!("line {line_number}: only scalar values and nested mappings are supported")); + if matches!( + value.chars().next(), + Some('[' | '{' | '|' | '>' | '&' | '*' | '!') + ) { + return Err(format!( + "line {line_number}: only scalar values and nested mappings are supported" + )); } let mut path: Vec<&str> = sections.iter().map(|(_, key)| key.as_str()).collect(); path.push(&normalized); let full_key = path.join("_"); - let parsed_value = parse_yaml_scalar(value) - .map_err(|error| format!("line {line_number}: {error}"))?; + let parsed_value = + parse_yaml_scalar(value).map_err(|error| format!("line {line_number}: {error}"))?; if output.insert(full_key.clone(), parsed_value).is_some() { return Err(format!("line {line_number}: duplicate key {full_key}")); } @@ -166,4 +264,3 @@ fn parse_yaml_scalar(value: &str) -> Result { } Ok(value.to_owned()) } - diff --git a/src/db/mod.rs b/src/db/mod.rs index 5503017..00ab77c 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -7,8 +7,7 @@ use chrono::{DateTime, NaiveDateTime, Utc}; use rand_core::{OsRng, RngCore}; use serde::Serialize; use sqlx::FromRow; -use sqlx::{any::AnyRow, Any, Row, Transaction}; - +use sqlx::{Any, Row, Transaction, any::AnyRow}; include!("workspace_notes.rs"); include!("pads_public.rs"); diff --git a/src/main.rs b/src/main.rs index 04a6c3a..c71efd8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -mod row_decode; mod api; mod app; mod assets; @@ -7,6 +6,7 @@ mod config; mod database; mod db; mod queries; +mod row_decode; mod state; mod storage; mod websocket; @@ -20,7 +20,6 @@ use tokio::net::TcpListener; use tracing::{info, warn}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - #[tokio::main] async fn main() -> Result<(), Box> { dotenvy::dotenv().ok(); @@ -29,7 +28,13 @@ async fn main() -> Result<(), Box> { let config = Config::load(cli.config.as_deref())?; if matches!(cli.command, Command::CheckConfig) { - println!("configuration is valid{}", cli.config.as_ref().map(|path| format!(" ({})", path.display())).unwrap_or_default()); + println!( + "configuration is valid{}", + cli.config + .as_ref() + .map(|path| format!(" ({})", path.display())) + .unwrap_or_default() + ); return Ok(()); } info!( @@ -99,12 +104,25 @@ async fn main() -> Result<(), Box> { let mut interval = tokio::time::interval(std::time::Duration::from_secs(24 * 60 * 60)); loop { interval.tick().await; - let cutoff = (chrono::Utc::now() - chrono::Duration::days(cleanup_state.unconfirmed_account_ttl_days)).to_rfc3339(); - match sqlx::query(crate::queries::get(cleanup_state.db.kind(), crate::queries::AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS)) - .bind(cutoff).execute(cleanup_state.db.pool()).await { - Ok(result) if result.rows_affected() > 0 => info!(deleted = result.rows_affected(), "removed expired unconfirmed accounts"), - Ok(_) => {}, - Err(error) => tracing::error!(%error, "failed to remove expired unconfirmed accounts"), + let cutoff = (chrono::Utc::now() + - chrono::Duration::days(cleanup_state.unconfirmed_account_ttl_days)) + .to_rfc3339(); + match sqlx::query(crate::queries::get( + cleanup_state.db.kind(), + crate::queries::AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS, + )) + .bind(cutoff) + .execute(cleanup_state.db.pool()) + .await + { + Ok(result) if result.rows_affected() > 0 => info!( + deleted = result.rows_affected(), + "removed expired unconfirmed accounts" + ), + Ok(_) => {} + Err(error) => { + tracing::error!(%error, "failed to remove expired unconfirmed accounts") + } } } }); @@ -126,7 +144,11 @@ async fn main() -> Result<(), Box> { } #[derive(Clone, Copy)] -enum Command { Run, CheckConfig, Migrate } +enum Command { + Run, + CheckConfig, + Migrate, +} struct Cli { command: Command, @@ -166,7 +188,9 @@ fn parse_command() -> Result> { } command = Command::Migrate; } - _ if arg.starts_with('-') => return Err(format!("unknown option: {arg}; use --help").into()), + _ if arg.starts_with('-') => { + return Err(format!("unknown option: {arg}; use --help").into()); + } _ => return Err(format!("unknown command: {arg}; use --help").into()), } } diff --git a/src/queries/mod.rs b/src/queries/mod.rs index 6f144e0..9045c35 100644 --- a/src/queries/mod.rs +++ b/src/queries/mod.rs @@ -143,7 +143,8 @@ pub const SQLITE_JOURNAL_WAL: Query = Query::SQLITE_JOURNAL_WAL; pub const SQLITE_BUSY_TIMEOUT: Query = Query::SQLITE_BUSY_TIMEOUT; pub const SQLITE_LAST_INSERT_ID: Query = Query::SQLITE_LAST_INSERT_ID; pub const MYSQL_LAST_INSERT_ID: Query = Query::MYSQL_LAST_INSERT_ID; -pub const POSTGRES_NOTE_REVISION_LAST_INSERT_ID: Query = Query::POSTGRES_NOTE_REVISION_LAST_INSERT_ID; +pub const POSTGRES_NOTE_REVISION_LAST_INSERT_ID: Query = + Query::POSTGRES_NOTE_REVISION_LAST_INSERT_ID; pub const POSTGRES_PAD_REVISION_LAST_INSERT_ID: Query = Query::POSTGRES_PAD_REVISION_LAST_INSERT_ID; pub const AUTH_LATEST_CONFIRMATION_CREATED_AT: Query = Query::AUTH_LATEST_CONFIRMATION_CREATED_AT; pub const AUTH_UPDATE_NICKNAME: Query = Query::AUTH_UPDATE_NICKNAME; @@ -157,7 +158,8 @@ pub const AUTH_CONSUME_ACCOUNT_ACTION: Query = Query::AUTH_CONSUME_ACCOUNT_ACTIO pub const AUTH_UPDATE_EMAIL: Query = Query::AUTH_UPDATE_EMAIL; pub const AUTH_DELETE_ACCOUNT_ACTIONS: Query = Query::AUTH_DELETE_ACCOUNT_ACTIONS; pub const AUTH_INSERT_ACCOUNT_ACTION: Query = Query::AUTH_INSERT_ACCOUNT_ACTION; -pub const AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS: Query = Query::AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS; +pub const AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS: Query = + Query::AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS; pub const AUTH_INSERT_USER: Query = Query::AUTH_INSERT_USER; pub const AUTH_INSERT_DIRECTORY_USER: Query = Query::AUTH_INSERT_DIRECTORY_USER; pub const AUTH_UPDATE_DIRECTORY_USER: Query = Query::AUTH_UPDATE_DIRECTORY_USER; @@ -167,7 +169,8 @@ pub const AUTH_DELETE_USER: Query = Query::AUTH_DELETE_USER; 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_REFRESH_SESSION: Query = Query::AUTH_REFRESH_SESSION; -pub const AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER: Query = Query::AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER; +pub const AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER: Query = + Query::AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER; pub const AUTH_INSERT_CONFIRMATION_TOKEN: Query = Query::AUTH_INSERT_CONFIRMATION_TOKEN; pub const AUTH_FIND_CONFIRMATION_TOKEN: Query = Query::AUTH_FIND_CONFIRMATION_TOKEN; pub const AUTH_CONFIRM_USER: Query = Query::AUTH_CONFIRM_USER; @@ -194,7 +197,8 @@ pub const USER_SET_WORKSPACE_PASSWORD: Query = Query::USER_SET_WORKSPACE_PASSWOR pub const USER_SET_PAD_PASSWORD: Query = Query::USER_SET_PAD_PASSWORD; pub const USER_SET_WORKSPACE_PRIVACY: Query = Query::USER_SET_WORKSPACE_PRIVACY; pub const USER_SET_PAD_PRIVACY: Query = Query::USER_SET_PAD_PRIVACY; -pub const RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE: Query = Query::RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE; +pub const RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE: Query = + Query::RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE; pub const RESOURCE_ACCESS_TOKENS_INSERT: Query = Query::RESOURCE_ACCESS_TOKENS_INSERT; pub const RESOURCE_ACCESS_TOKENS_VALID_COUNT: Query = Query::RESOURCE_ACCESS_TOKENS_VALID_COUNT; pub const RESOURCE_PERMISSION_DELETE_USER: Query = Query::RESOURCE_PERMISSION_DELETE_USER; @@ -265,7 +269,16 @@ mod tests { use super::*; #[test] fn every_backend_has_explicit_queries() { - for query in [Q001, Q003, Q004, Q011, Q021, Q033, USER_LIST_WORKSPACES, USER_LIST_PADS] { + for query in [ + Q001, + Q003, + Q004, + Q011, + Q021, + Q033, + USER_LIST_WORKSPACES, + USER_LIST_PADS, + ] { assert!(!get(DatabaseKind::Sqlite, query).is_empty()); assert!(!get(DatabaseKind::Postgres, query).is_empty()); assert!(!get(DatabaseKind::MySql, query).is_empty()); diff --git a/src/queries/mysql.rs b/src/queries/mysql.rs index 2d47d89..d9385ed 100644 --- a/src/queries/mysql.rs +++ b/src/queries/mysql.rs @@ -7,121 +7,295 @@ pub fn get(query: Query) -> &'static str { Query::SQLITE_BUSY_TIMEOUT => r#"PRAGMA busy_timeout = 5000"#, Query::SQLITE_LAST_INSERT_ID => r#"SELECT last_insert_rowid()"#, Query::MYSQL_LAST_INSERT_ID => r#"SELECT LAST_INSERT_ID()"#, - Query::POSTGRES_NOTE_REVISION_LAST_INSERT_ID => r#"SELECT currval(pg_get_serial_sequence('note_revisions', 'id'))"#, - Query::POSTGRES_PAD_REVISION_LAST_INSERT_ID => r#"SELECT currval(pg_get_serial_sequence('revisions', 'id'))"#, - Query::AUTH_LATEST_CONFIRMATION_CREATED_AT => r#"SELECT created_at FROM account_confirmation_tokens WHERE user_id = ? ORDER BY created_at DESC LIMIT 1"#, - Query::AUTH_UPDATE_NICKNAME => r#"UPDATE users SET nickname = ?, nickname_key = ?, updated_at = ? WHERE id = ?"#, - Query::AUTH_UPDATE_EDITOR_COLOR => r#"UPDATE users SET editor_color = ?, updated_at = ? WHERE id = ?"#, + Query::POSTGRES_NOTE_REVISION_LAST_INSERT_ID => { + r#"SELECT currval(pg_get_serial_sequence('note_revisions', 'id'))"# + } + Query::POSTGRES_PAD_REVISION_LAST_INSERT_ID => { + r#"SELECT currval(pg_get_serial_sequence('revisions', 'id'))"# + } + Query::AUTH_LATEST_CONFIRMATION_CREATED_AT => { + r#"SELECT created_at FROM account_confirmation_tokens WHERE user_id = ? ORDER BY created_at DESC LIMIT 1"# + } + Query::AUTH_UPDATE_NICKNAME => { + r#"UPDATE users SET nickname = ?, nickname_key = ?, updated_at = ? WHERE id = ?"# + } + Query::AUTH_UPDATE_EDITOR_COLOR => { + r#"UPDATE users SET editor_color = ?, updated_at = ? WHERE id = ?"# + } Query::AUTH_EDITOR_COLOR_BY_USER => r#"SELECT editor_color FROM users WHERE id = ?"#, - Query::RESOURCE_COLOR_BY_USER => r#"SELECT color FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#, - Query::RESOURCE_COLOR_DELETE => r#"DELETE FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#, - Query::RESOURCE_COLOR_INSERT => r#"INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES (?, ?, ?, ?)"#, - Query::AUTH_ACCOUNT_ACTION_BY_TOKEN => r#"SELECT user_id, action, CAST(payload AS CHAR CHARACTER SET utf8mb4) AS payload, expires_at, used_at FROM account_action_tokens WHERE token = ?"#, - Query::AUTH_CONSUME_ACCOUNT_ACTION => r#"UPDATE account_action_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL"#, - Query::AUTH_UPDATE_EMAIL => r#"UPDATE users SET email = ?, email_key = ?, updated_at = ? WHERE id = ?"#, - Query::AUTH_DELETE_ACCOUNT_ACTIONS => r#"DELETE FROM account_action_tokens WHERE user_id = ? AND action = ?"#, - Query::AUTH_INSERT_ACCOUNT_ACTION => r#"INSERT INTO account_action_tokens (token,user_id,action,payload,expires_at,created_at) VALUES (?, ?, ?, ?, ?, ?)"#, - Query::AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS => r#"DELETE FROM users WHERE confirmed_at IS NULL AND created_at < ?"#, - Query::AUTH_INSERT_USER => r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at) 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_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::RESOURCE_COLOR_BY_USER => { + r#"SELECT color FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"# + } + Query::RESOURCE_COLOR_DELETE => { + r#"DELETE FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"# + } + Query::RESOURCE_COLOR_INSERT => { + r#"INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES (?, ?, ?, ?)"# + } + Query::AUTH_ACCOUNT_ACTION_BY_TOKEN => { + r#"SELECT user_id, action, CAST(payload AS CHAR CHARACTER SET utf8mb4) AS payload, expires_at, used_at FROM account_action_tokens WHERE token = ?"# + } + Query::AUTH_CONSUME_ACCOUNT_ACTION => { + r#"UPDATE account_action_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL"# + } + Query::AUTH_UPDATE_EMAIL => { + r#"UPDATE users SET email = ?, email_key = ?, updated_at = ? WHERE id = ?"# + } + Query::AUTH_DELETE_ACCOUNT_ACTIONS => { + r#"DELETE FROM account_action_tokens WHERE user_id = ? AND action = ?"# + } + Query::AUTH_INSERT_ACCOUNT_ACTION => { + r#"INSERT INTO account_action_tokens (token,user_id,action,payload,expires_at,created_at) VALUES (?, ?, ?, ?, ?, ?)"# + } + Query::AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS => { + r#"DELETE FROM users WHERE confirmed_at IS NULL AND created_at < ?"# + } + Query::AUTH_INSERT_USER => { + r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at) 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_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_DELETE_USER => r#"DELETE FROM users WHERE id = ?"#, 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_REFRESH_SESSION => r#"UPDATE user_sessions SET expires_at = ? WHERE token = ? AND expires_at > ?"#, - Query::AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER => r#"DELETE FROM account_confirmation_tokens WHERE user_id = ?"#, - Query::AUTH_INSERT_CONFIRMATION_TOKEN => r#"INSERT INTO account_confirmation_tokens (token, user_id, expires_at) VALUES (?, ?, ?)"#, - Query::AUTH_FIND_CONFIRMATION_TOKEN => r#"SELECT user_id, expires_at, used_at FROM account_confirmation_tokens WHERE token = ?"#, - Query::AUTH_CONFIRM_USER => r#"UPDATE users SET confirmed_at = ?, updated_at = ? WHERE id = ?"#, - Query::AUTH_CONSUME_CONFIRMATION_TOKEN => r#"UPDATE account_confirmation_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL AND expires_at > ?"#, - Query::AUTH_DELETE_RESET_TOKENS_BY_USER => r#"DELETE FROM password_reset_tokens WHERE user_id = ?"#, - Query::AUTH_INSERT_RESET_TOKEN => r#"INSERT INTO password_reset_tokens (token, user_id, expires_at) VALUES (?, ?, ?)"#, - Query::AUTH_FIND_RESET_TOKEN => r#"SELECT user_id, expires_at, used_at FROM password_reset_tokens WHERE token = ?"#, - 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_REFRESH_SESSION => { + r#"UPDATE user_sessions SET expires_at = ? WHERE token = ? AND expires_at > ?"# + } + Query::AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER => { + r#"DELETE FROM account_confirmation_tokens WHERE user_id = ?"# + } + Query::AUTH_INSERT_CONFIRMATION_TOKEN => { + r#"INSERT INTO account_confirmation_tokens (token, user_id, expires_at) VALUES (?, ?, ?)"# + } + Query::AUTH_FIND_CONFIRMATION_TOKEN => { + r#"SELECT user_id, expires_at, used_at FROM account_confirmation_tokens WHERE token = ?"# + } + Query::AUTH_CONFIRM_USER => { + r#"UPDATE users SET confirmed_at = ?, updated_at = ? WHERE id = ?"# + } + Query::AUTH_CONSUME_CONFIRMATION_TOKEN => { + r#"UPDATE account_confirmation_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL AND expires_at > ?"# + } + Query::AUTH_DELETE_RESET_TOKENS_BY_USER => { + r#"DELETE FROM password_reset_tokens WHERE user_id = ?"# + } + Query::AUTH_INSERT_RESET_TOKEN => { + r#"INSERT INTO password_reset_tokens (token, user_id, expires_at) VALUES (?, ?, ?)"# + } + Query::AUTH_FIND_RESET_TOKEN => { + r#"SELECT user_id, expires_at, used_at FROM password_reset_tokens WHERE token = ?"# + } + 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_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_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_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::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"#, - Query::USER_LIST_PADS => r#"SELECT p.slug, CAST(p.title AS CHAR CHARACTER SET utf8mb4) AS title, CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END AS protected, p.updated_at, CASE WHEN p.is_private THEN 1 ELSE 0 END AS private, 1 AS owned, 'rw' AS permission, '' AS shared_by FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = ? UNION SELECT p.slug, CAST(p.title AS CHAR CHARACTER SET utf8mb4) AS title, CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END, p.updated_at, CASE WHEN p.is_private THEN 1 ELSE 0 END, 0, rp.permission, COALESCE((SELECT u.nickname FROM user_pads owner_up JOIN users u ON u.id = owner_up.user_id WHERE owner_up.pad_id = p.id LIMIT 1), 'Unknown user') AS shared_by FROM resource_permissions rp JOIN pads p ON p.slug = rp.resource_slug WHERE rp.resource_kind = 'pad' AND rp.user_id = ? ORDER BY updated_at DESC"#, - Query::USER_OWNS_WORKSPACE => r#"SELECT COUNT(*) FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = ? AND w.slug = ?"#, - Query::USER_OWNS_PAD => r#"SELECT COUNT(*) FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = ? AND p.slug = ?"#, + 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_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_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::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"# + } + Query::USER_LIST_PADS => { + r#"SELECT p.slug, CAST(p.title AS CHAR CHARACTER SET utf8mb4) AS title, CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END AS protected, p.updated_at, CASE WHEN p.is_private THEN 1 ELSE 0 END AS private, 1 AS owned, 'rw' AS permission, '' AS shared_by FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = ? UNION SELECT p.slug, CAST(p.title AS CHAR CHARACTER SET utf8mb4) AS title, CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END, p.updated_at, CASE WHEN p.is_private THEN 1 ELSE 0 END, 0, rp.permission, COALESCE((SELECT u.nickname FROM user_pads owner_up JOIN users u ON u.id = owner_up.user_id WHERE owner_up.pad_id = p.id LIMIT 1), 'Unknown user') AS shared_by FROM resource_permissions rp JOIN pads p ON p.slug = rp.resource_slug WHERE rp.resource_kind = 'pad' AND rp.user_id = ? ORDER BY updated_at DESC"# + } + Query::USER_OWNS_WORKSPACE => { + r#"SELECT COUNT(*) FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = ? AND w.slug = ?"# + } + Query::USER_OWNS_PAD => { + r#"SELECT COUNT(*) FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = ? AND p.slug = ?"# + } Query::USER_DELETE_WORKSPACE => r#"DELETE FROM workspaces WHERE slug = ?"#, Query::USER_DELETE_PAD => r#"DELETE FROM pads WHERE slug = ?"#, - Query::USER_SET_WORKSPACE_PASSWORD => r#"UPDATE workspaces SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, - Query::USER_SET_PAD_PASSWORD => r#"UPDATE pads SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, - Query::USER_SET_WORKSPACE_PRIVACY => r#"UPDATE workspaces SET is_private = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, - Query::USER_SET_PAD_PRIVACY => r#"UPDATE pads SET is_private = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, - Query::RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE => r#"DELETE FROM resource_access_tokens WHERE resource_kind = ? AND resource_slug = ?"#, - Query::RESOURCE_ACCESS_TOKENS_INSERT => r#"INSERT INTO resource_access_tokens (token_hash, resource_kind, resource_slug, expires_at) VALUES (?, ?, ?, ?)"#, - Query::RESOURCE_ACCESS_TOKENS_VALID_COUNT => r#"SELECT COUNT(*) FROM resource_access_tokens WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND expires_at > ?"#, - Query::RESOURCE_PERMISSION_DELETE_USER => r#"DELETE FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"#, - Query::RESOURCE_PERMISSION_INSERT => r#"INSERT INTO resource_permissions (resource_kind, resource_slug, user_id, permission) VALUES (?, ?, ?, ?)"#, - Query::SHARE_INVITATION_DELETE_USER => r#"DELETE FROM resource_share_invitations WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"#, - Query::SHARE_INVITATION_INSERT => r#"INSERT INTO resource_share_invitations (token_hash, resource_kind, resource_slug, user_id, permission, created_by, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)"#, - Query::SHARE_INVITATION_DELETE_TOKEN => r#"DELETE FROM resource_share_invitations WHERE token_hash = ?"#, - Query::SHARE_INVITATION_FIND_TOKEN => r#"SELECT resource_kind, resource_slug, user_id, permission, CAST(expires_at AS CHAR CHARACTER SET utf8mb4) AS expires_at, CAST(accepted_at AS CHAR CHARACTER SET utf8mb4) AS accepted_at FROM resource_share_invitations WHERE token_hash = ?"#, - Query::SHARE_INVITATION_ACCEPT => r#"UPDATE resource_share_invitations SET accepted_at = ? WHERE token_hash = ?"#, - Query::RESOURCE_SHARING_USERS => r#"SELECT u.email, u.nickname, rp.permission FROM resource_permissions rp JOIN users u ON u.id = rp.user_id WHERE rp.resource_kind = ? AND rp.resource_slug = ? ORDER BY u.email"#, - Query::RESOURCE_SHARING_LINKS => r#"SELECT token_hash, CAST(token AS CHAR CHARACTER SET utf8mb4) AS token, permission, CAST(expires_at AS CHAR CHARACTER SET utf8mb4) AS expires_at, CAST(created_at AS CHAR CHARACTER SET utf8mb4) AS created_at FROM resource_share_links WHERE resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL ORDER BY created_at DESC"#, - Query::RESOURCE_SHARING_PENDING => r#"SELECT u.email, u.nickname, i.permission, CAST(i.expires_at AS CHAR CHARACTER SET utf8mb4) AS expires_at FROM resource_share_invitations i JOIN users u ON u.id = i.user_id WHERE i.resource_kind = ? AND i.resource_slug = ? AND i.accepted_at IS NULL ORDER BY u.email"#, - Query::SHARE_LINK_INSERT => r#"INSERT INTO resource_share_links (token_hash, token, resource_kind, resource_slug, permission, expires_at, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)"#, - Query::SHARE_LINK_UPDATE => r#"UPDATE resource_share_links SET permission = ?, expires_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL"#, - Query::SHARE_LINK_REVOKE => r#"UPDATE resource_share_links SET revoked_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ?"#, - Query::RESOURCE_PERMISSION_BY_USER => r#"SELECT permission FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"#, - Query::SHARE_LINK_PERMISSION => r#"SELECT permission FROM resource_share_links WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)"#, - Query::Q001 => r#"SELECT id, slug, CAST(title AS CHAR CHARACTER SET utf8mb4) AS title, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, created_at, updated_at, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS SIGNED) AS is_private FROM workspaces WHERE slug = ?"#, + Query::USER_SET_WORKSPACE_PASSWORD => { + r#"UPDATE workspaces SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"# + } + Query::USER_SET_PAD_PASSWORD => { + r#"UPDATE pads SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"# + } + Query::USER_SET_WORKSPACE_PRIVACY => { + r#"UPDATE workspaces SET is_private = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"# + } + Query::USER_SET_PAD_PRIVACY => { + r#"UPDATE pads SET is_private = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"# + } + Query::RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE => { + r#"DELETE FROM resource_access_tokens WHERE resource_kind = ? AND resource_slug = ?"# + } + Query::RESOURCE_ACCESS_TOKENS_INSERT => { + r#"INSERT INTO resource_access_tokens (token_hash, resource_kind, resource_slug, expires_at) VALUES (?, ?, ?, ?)"# + } + Query::RESOURCE_ACCESS_TOKENS_VALID_COUNT => { + r#"SELECT COUNT(*) FROM resource_access_tokens WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND expires_at > ?"# + } + Query::RESOURCE_PERMISSION_DELETE_USER => { + r#"DELETE FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"# + } + Query::RESOURCE_PERMISSION_INSERT => { + r#"INSERT INTO resource_permissions (resource_kind, resource_slug, user_id, permission) VALUES (?, ?, ?, ?)"# + } + Query::SHARE_INVITATION_DELETE_USER => { + r#"DELETE FROM resource_share_invitations WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"# + } + Query::SHARE_INVITATION_INSERT => { + r#"INSERT INTO resource_share_invitations (token_hash, resource_kind, resource_slug, user_id, permission, created_by, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)"# + } + Query::SHARE_INVITATION_DELETE_TOKEN => { + r#"DELETE FROM resource_share_invitations WHERE token_hash = ?"# + } + Query::SHARE_INVITATION_FIND_TOKEN => { + r#"SELECT resource_kind, resource_slug, user_id, permission, CAST(expires_at AS CHAR CHARACTER SET utf8mb4) AS expires_at, CAST(accepted_at AS CHAR CHARACTER SET utf8mb4) AS accepted_at FROM resource_share_invitations WHERE token_hash = ?"# + } + Query::SHARE_INVITATION_ACCEPT => { + r#"UPDATE resource_share_invitations SET accepted_at = ? WHERE token_hash = ?"# + } + Query::RESOURCE_SHARING_USERS => { + r#"SELECT u.email, u.nickname, rp.permission FROM resource_permissions rp JOIN users u ON u.id = rp.user_id WHERE rp.resource_kind = ? AND rp.resource_slug = ? ORDER BY u.email"# + } + Query::RESOURCE_SHARING_LINKS => { + r#"SELECT token_hash, CAST(token AS CHAR CHARACTER SET utf8mb4) AS token, permission, CAST(expires_at AS CHAR CHARACTER SET utf8mb4) AS expires_at, CAST(created_at AS CHAR CHARACTER SET utf8mb4) AS created_at FROM resource_share_links WHERE resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL ORDER BY created_at DESC"# + } + Query::RESOURCE_SHARING_PENDING => { + r#"SELECT u.email, u.nickname, i.permission, CAST(i.expires_at AS CHAR CHARACTER SET utf8mb4) AS expires_at FROM resource_share_invitations i JOIN users u ON u.id = i.user_id WHERE i.resource_kind = ? AND i.resource_slug = ? AND i.accepted_at IS NULL ORDER BY u.email"# + } + Query::SHARE_LINK_INSERT => { + r#"INSERT INTO resource_share_links (token_hash, token, resource_kind, resource_slug, permission, expires_at, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)"# + } + Query::SHARE_LINK_UPDATE => { + r#"UPDATE resource_share_links SET permission = ?, expires_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL"# + } + Query::SHARE_LINK_REVOKE => { + r#"UPDATE resource_share_links SET revoked_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ?"# + } + Query::RESOURCE_PERMISSION_BY_USER => { + r#"SELECT permission FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"# + } + Query::SHARE_LINK_PERMISSION => { + r#"SELECT permission FROM resource_share_links WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)"# + } + Query::Q001 => { + r#"SELECT id, slug, CAST(title AS CHAR CHARACTER SET utf8mb4) AS title, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, created_at, updated_at, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS SIGNED) AS is_private FROM workspaces WHERE slug = ?"# + } Query::Q002 => r#"INSERT INTO workspaces (slug, title, password_hash) VALUES (?, ?, ?)"#, - Query::Q003 => r#"SELECT id, workspace_id, slug, CAST(title AS CHAR CHARACTER SET utf8mb4) AS title, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, created_at, updated_at, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map, CAST(CASE WHEN protected THEN 1 ELSE 0 END AS SIGNED) AS protected, CAST(created_by AS CHAR CHARACTER SET utf8mb4) AS created_by FROM notes WHERE workspace_id = ? ORDER BY updated_at DESC, id DESC"#, - Query::Q004 => r#"SELECT id, workspace_id, slug, CAST(title AS CHAR CHARACTER SET utf8mb4) AS title, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, created_at, updated_at, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map, CAST(CASE WHEN protected THEN 1 ELSE 0 END AS SIGNED) AS protected, CAST(created_by AS CHAR CHARACTER SET utf8mb4) AS created_by FROM notes WHERE workspace_id = ? AND slug = ?"#, - Query::Q005 => r#"INSERT INTO notes (workspace_id, slug, title, protected, created_by) VALUES (?, ?, ?, ?, ?)"#, - Query::Q006 => r#"UPDATE notes SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, + Query::Q003 => { + r#"SELECT id, workspace_id, slug, CAST(title AS CHAR CHARACTER SET utf8mb4) AS title, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, created_at, updated_at, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map, CAST(CASE WHEN protected THEN 1 ELSE 0 END AS SIGNED) AS protected, CAST(created_by AS CHAR CHARACTER SET utf8mb4) AS created_by FROM notes WHERE workspace_id = ? ORDER BY updated_at DESC, id DESC"# + } + Query::Q004 => { + r#"SELECT id, workspace_id, slug, CAST(title AS CHAR CHARACTER SET utf8mb4) AS title, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, created_at, updated_at, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map, CAST(CASE WHEN protected THEN 1 ELSE 0 END AS SIGNED) AS protected, CAST(created_by AS CHAR CHARACTER SET utf8mb4) AS created_by FROM notes WHERE workspace_id = ? AND slug = ?"# + } + Query::Q005 => { + r#"INSERT INTO notes (workspace_id, slug, title, protected, created_by) VALUES (?, ?, ?, ?, ?)"# + } + Query::Q006 => { + r#"UPDATE notes SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"# + } Query::Q007 => r#"UPDATE workspaces SET updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, - Query::Q008 => r#"INSERT INTO note_revisions (note_id, content, author, owner_map) VALUES (?, ?, ?, ?)"#, + Query::Q008 => { + r#"INSERT INTO note_revisions (note_id, content, author, owner_map) VALUES (?, ?, ?, ?)"# + } Query::Q009 => r#"SELECT updated_at FROM notes WHERE id = ?"#, - Query::Q010 => r#"SELECT id, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, created_at, CAST(author AS CHAR CHARACTER SET utf8mb4) AS author, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map FROM note_revisions WHERE note_id = ? ORDER BY id DESC LIMIT 100"#, - Query::Q011 => r#"SELECT id, slug, CAST(title AS CHAR CHARACTER SET utf8mb4) AS title, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, created_at, updated_at, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS SIGNED) AS is_private FROM pads WHERE slug = ?"#, + Query::Q010 => { + r#"SELECT id, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, created_at, CAST(author AS CHAR CHARACTER SET utf8mb4) AS author, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map FROM note_revisions WHERE note_id = ? ORDER BY id DESC LIMIT 100"# + } + Query::Q011 => { + r#"SELECT id, slug, CAST(title AS CHAR CHARACTER SET utf8mb4) AS title, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, created_at, updated_at, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS SIGNED) AS is_private FROM pads WHERE slug = ?"# + } Query::Q012 => r#"INSERT INTO pads (slug, title, password_hash) VALUES (?, ?, ?)"#, - Query::Q013 => r#"UPDATE pads SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, - Query::Q014 => r#"INSERT INTO revisions (pad_id, content, author, owner_map) VALUES (?, ?, ?, ?)"#, + Query::Q013 => { + r#"UPDATE pads SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"# + } + Query::Q014 => { + r#"INSERT INTO revisions (pad_id, content, author, owner_map) VALUES (?, ?, ?, ?)"# + } Query::Q015 => r#"SELECT updated_at FROM pads WHERE id = ?"#, - Query::Q016 => r#"SELECT id, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, created_at, CAST(author AS CHAR CHARACTER SET utf8mb4) AS author, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map FROM revisions WHERE pad_id = ? ORDER BY id DESC LIMIT 100"#, + Query::Q016 => { + r#"SELECT id, CAST(content AS CHAR CHARACTER SET utf8mb4) AS content, created_at, CAST(author AS CHAR CHARACTER SET utf8mb4) AS author, CAST(owner_map AS CHAR CHARACTER SET utf8mb4) AS owner_map FROM revisions WHERE pad_id = ? ORDER BY id DESC LIMIT 100"# + } Query::Q017 => r#"SELECT token FROM published_pages WHERE pad_id = ?"#, Query::Q018 => r#"INSERT INTO published_pages (token, pad_id) VALUES (?, ?)"#, Query::Q019 => r#"SELECT token FROM published_pages WHERE note_id = ?"#, Query::Q020 => r#"INSERT INTO published_pages (token, note_id) VALUES (?, ?)"#, - Query::Q021 => r#"SELECT pp.token, pp.pad_id, pp.note_id, CAST(CASE WHEN pp.allow_task_updates THEN 1 ELSE 0 END AS SIGNED) AS allow_task_updates, CAST(COALESCE(p.title, n.title) AS CHAR CHARACTER SET utf8mb4) AS title, CAST(COALESCE(p.content, n.content) AS CHAR CHARACTER SET utf8mb4) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = ?"#, + Query::Q021 => { + r#"SELECT pp.token, pp.pad_id, pp.note_id, CAST(CASE WHEN pp.allow_task_updates THEN 1 ELSE 0 END AS SIGNED) AS allow_task_updates, CAST(COALESCE(p.title, n.title) AS CHAR CHARACTER SET utf8mb4) AS title, CAST(COALESCE(p.content, n.content) AS CHAR CHARACTER SET utf8mb4) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = ?"# + } Query::Q022 => r#"SELECT file_token FROM pads WHERE id = ?"#, Query::Q023 => r#"UPDATE pads SET file_token = ? WHERE id = ? AND file_token IS NULL"#, Query::Q024 => r#"SELECT file_token FROM notes WHERE id = ?"#, Query::Q025 => r#"UPDATE notes SET file_token = ? WHERE id = ? AND file_token IS NULL"#, Query::Q026 => r#"SELECT id FROM pads WHERE file_token = ?"#, Query::Q027 => r#"SELECT id FROM notes WHERE file_token = ?"#, - Query::Q028 => r#"SELECT CAST(content AS CHAR CHARACTER SET utf8mb4) FROM note_revisions WHERE id = ? AND note_id = ?"#, - Query::Q029 => r#"SELECT CAST(content AS CHAR CHARACTER SET utf8mb4) FROM revisions WHERE id = ? AND pad_id = ?"#, - Query::Q030 => r#"SELECT CAST(owner_map AS CHAR CHARACTER SET utf8mb4) FROM revisions WHERE id = ? AND pad_id = ?"#, + Query::Q028 => { + r#"SELECT CAST(content AS CHAR CHARACTER SET utf8mb4) FROM note_revisions WHERE id = ? AND note_id = ?"# + } + Query::Q029 => { + r#"SELECT CAST(content AS CHAR CHARACTER SET utf8mb4) FROM revisions WHERE id = ? AND pad_id = ?"# + } + Query::Q030 => { + r#"SELECT CAST(owner_map AS CHAR CHARACTER SET utf8mb4) FROM revisions WHERE id = ? AND pad_id = ?"# + } Query::Q031 => r#"DELETE FROM notes WHERE id = ?"#, - Query::Q032 => r#"INSERT INTO note_files (note_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)"#, - Query::Q033 => r#"SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, CAST(CASE WHEN is_attached THEN 1 ELSE 0 END AS SIGNED) AS is_attached, detached_at FROM note_files WHERE note_id = ? ORDER BY id DESC"#, + Query::Q032 => { + r#"INSERT INTO note_files (note_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)"# + } + Query::Q033 => { + r#"SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, CAST(CASE WHEN is_attached THEN 1 ELSE 0 END AS SIGNED) AS is_attached, detached_at FROM note_files WHERE note_id = ? ORDER BY id DESC"# + } Query::Q034 => r#"UPDATE note_files SET is_attached = ?, detached_at = ? WHERE id = ?"#, - Query::Q035 => r#"INSERT INTO pad_files (pad_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)"#, - Query::Q036 => r#"SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, CAST(CASE WHEN is_attached THEN 1 ELSE 0 END AS SIGNED) AS is_attached, detached_at FROM pad_files WHERE pad_id = ? ORDER BY id DESC"#, + Query::Q035 => { + r#"INSERT INTO pad_files (pad_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)"# + } + Query::Q036 => { + r#"SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, CAST(CASE WHEN is_attached THEN 1 ELSE 0 END AS SIGNED) AS is_attached, detached_at FROM pad_files WHERE pad_id = ? ORDER BY id DESC"# + } Query::Q037 => r#"UPDATE pad_files SET is_attached = ?, detached_at = ? WHERE id = ?"#, - Query::Q038 => r#"SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, CAST(CASE WHEN is_attached THEN 1 ELSE 0 END AS SIGNED) AS is_attached, detached_at FROM note_files WHERE id = ? AND note_id = ?"#, + Query::Q038 => { + r#"SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, CAST(CASE WHEN is_attached THEN 1 ELSE 0 END AS SIGNED) AS is_attached, detached_at FROM note_files WHERE id = ? AND note_id = ?"# + } Query::Q039 => r#"DELETE FROM note_files WHERE id = ? AND note_id = ?"#, - Query::Q046 => r#"SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, CAST(CASE WHEN is_attached THEN 1 ELSE 0 END AS SIGNED) AS is_attached, detached_at FROM pad_files WHERE id = ? AND pad_id = ?"#, + Query::Q046 => { + r#"SELECT id, CAST(filename AS CHAR CHARACTER SET utf8mb4) AS filename, url, mime_type, size_bytes, created_at, CAST(CASE WHEN is_attached THEN 1 ELSE 0 END AS SIGNED) AS is_attached, detached_at FROM pad_files WHERE id = ? AND pad_id = ?"# + } Query::Q047 => r#"DELETE FROM pad_files WHERE id = ? AND pad_id = ?"#, Query::Q040 => r#"UPDATE published_pages SET allow_task_updates = ? WHERE pad_id = ?"#, Query::Q041 => r#"UPDATE published_pages SET allow_task_updates = ? WHERE note_id = ?"#, - Query::Q042 => r#"UPDATE pads SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, - Query::Q043 => r#"UPDATE notes SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, - Query::Q044 => r#"SELECT CAST(CASE WHEN allow_task_updates THEN 1 ELSE 0 END AS SIGNED) FROM published_pages WHERE pad_id = ?"#, - Query::Q045 => r#"SELECT CAST(CASE WHEN allow_task_updates THEN 1 ELSE 0 END AS SIGNED) FROM published_pages WHERE note_id = ?"#, + Query::Q042 => { + r#"UPDATE pads SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"# + } + Query::Q043 => { + r#"UPDATE notes SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"# + } + Query::Q044 => { + r#"SELECT CAST(CASE WHEN allow_task_updates THEN 1 ELSE 0 END AS SIGNED) FROM published_pages WHERE pad_id = ?"# + } + Query::Q045 => { + r#"SELECT CAST(CASE WHEN allow_task_updates THEN 1 ELSE 0 END AS SIGNED) FROM published_pages WHERE note_id = ?"# + } } } diff --git a/src/queries/postgres.rs b/src/queries/postgres.rs index 2a974cb..8d633ef 100644 --- a/src/queries/postgres.rs +++ b/src/queries/postgres.rs @@ -7,96 +7,248 @@ pub fn get(query: Query) -> &'static str { Query::SQLITE_BUSY_TIMEOUT => r#"PRAGMA busy_timeout = 5000"#, Query::SQLITE_LAST_INSERT_ID => r#"SELECT last_insert_rowid()"#, Query::MYSQL_LAST_INSERT_ID => r#"SELECT LAST_INSERT_ID()"#, - Query::POSTGRES_NOTE_REVISION_LAST_INSERT_ID => r#"SELECT currval(pg_get_serial_sequence('note_revisions', 'id'))"#, - Query::POSTGRES_PAD_REVISION_LAST_INSERT_ID => r#"SELECT currval(pg_get_serial_sequence('revisions', 'id'))"#, - Query::AUTH_LATEST_CONFIRMATION_CREATED_AT => r#"SELECT created_at FROM account_confirmation_tokens WHERE user_id = $1 ORDER BY created_at DESC LIMIT 1"#, - Query::AUTH_UPDATE_NICKNAME => r#"UPDATE users SET nickname = $1, nickname_key = $2, updated_at = $3 WHERE id = $4"#, - Query::AUTH_UPDATE_EDITOR_COLOR => r#"UPDATE users SET editor_color = $1, updated_at = $2 WHERE id = $3"#, + Query::POSTGRES_NOTE_REVISION_LAST_INSERT_ID => { + r#"SELECT currval(pg_get_serial_sequence('note_revisions', 'id'))"# + } + Query::POSTGRES_PAD_REVISION_LAST_INSERT_ID => { + r#"SELECT currval(pg_get_serial_sequence('revisions', 'id'))"# + } + Query::AUTH_LATEST_CONFIRMATION_CREATED_AT => { + r#"SELECT created_at FROM account_confirmation_tokens WHERE user_id = $1 ORDER BY created_at DESC LIMIT 1"# + } + Query::AUTH_UPDATE_NICKNAME => { + r#"UPDATE users SET nickname = $1, nickname_key = $2, updated_at = $3 WHERE id = $4"# + } + Query::AUTH_UPDATE_EDITOR_COLOR => { + r#"UPDATE users SET editor_color = $1, updated_at = $2 WHERE id = $3"# + } Query::AUTH_EDITOR_COLOR_BY_USER => r#"SELECT editor_color FROM users WHERE id = $1"#, - Query::RESOURCE_COLOR_BY_USER => r#"SELECT color FROM user_resource_colors WHERE user_id = $1 AND resource_kind = $2 AND resource_slug = $3"#, - Query::RESOURCE_COLOR_DELETE => r#"DELETE FROM user_resource_colors WHERE user_id = $1 AND resource_kind = $2 AND resource_slug = $3"#, - Query::RESOURCE_COLOR_INSERT => r#"INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES ($1, $2, $3, $4)"#, - Query::AUTH_ACCOUNT_ACTION_BY_TOKEN => r#"SELECT user_id, action, payload, expires_at, used_at FROM account_action_tokens WHERE token = $1"#, - Query::AUTH_CONSUME_ACCOUNT_ACTION => r#"UPDATE account_action_tokens SET used_at = $1 WHERE token = $2 AND used_at IS NULL"#, - Query::AUTH_UPDATE_EMAIL => r#"UPDATE users SET email = $1, email_key = $2, updated_at = $3 WHERE id = $4"#, - Query::AUTH_DELETE_ACCOUNT_ACTIONS => r#"DELETE FROM account_action_tokens WHERE user_id = $1 AND action = $2"#, - Query::AUTH_INSERT_ACCOUNT_ACTION => r#"INSERT INTO account_action_tokens (token,user_id,action,payload,expires_at,created_at) VALUES ($1, $2, $3, $4, $5, $6)"#, - Query::AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS => r#"DELETE FROM users WHERE confirmed_at IS NULL AND created_at < $1"#, - Query::AUTH_INSERT_USER => r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at) VALUES ($1, $2, $3, $4, $5, $6)"#, - 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_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::RESOURCE_COLOR_BY_USER => { + r#"SELECT color FROM user_resource_colors WHERE user_id = $1 AND resource_kind = $2 AND resource_slug = $3"# + } + Query::RESOURCE_COLOR_DELETE => { + r#"DELETE FROM user_resource_colors WHERE user_id = $1 AND resource_kind = $2 AND resource_slug = $3"# + } + Query::RESOURCE_COLOR_INSERT => { + r#"INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES ($1, $2, $3, $4)"# + } + Query::AUTH_ACCOUNT_ACTION_BY_TOKEN => { + r#"SELECT user_id, action, payload, expires_at, used_at FROM account_action_tokens WHERE token = $1"# + } + Query::AUTH_CONSUME_ACCOUNT_ACTION => { + r#"UPDATE account_action_tokens SET used_at = $1 WHERE token = $2 AND used_at IS NULL"# + } + Query::AUTH_UPDATE_EMAIL => { + r#"UPDATE users SET email = $1, email_key = $2, updated_at = $3 WHERE id = $4"# + } + Query::AUTH_DELETE_ACCOUNT_ACTIONS => { + r#"DELETE FROM account_action_tokens WHERE user_id = $1 AND action = $2"# + } + Query::AUTH_INSERT_ACCOUNT_ACTION => { + r#"INSERT INTO account_action_tokens (token,user_id,action,payload,expires_at,created_at) VALUES ($1, $2, $3, $4, $5, $6)"# + } + Query::AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS => { + r#"DELETE FROM users WHERE confirmed_at IS NULL AND created_at < $1"# + } + Query::AUTH_INSERT_USER => { + r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at) VALUES ($1, $2, $3, $4, $5, $6)"# + } + 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_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_DELETE_USER => r#"DELETE FROM users WHERE id = $1"#, - 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_REFRESH_SESSION => r#"UPDATE user_sessions SET expires_at = $1 WHERE token = $2 AND expires_at > $3"#, - Query::AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER => r#"DELETE FROM account_confirmation_tokens WHERE user_id = $1"#, - Query::AUTH_INSERT_CONFIRMATION_TOKEN => r#"INSERT INTO account_confirmation_tokens (token, user_id, expires_at) VALUES ($1, $2, $3)"#, - Query::AUTH_FIND_CONFIRMATION_TOKEN => r#"SELECT user_id, expires_at, used_at FROM account_confirmation_tokens WHERE token = $1"#, - Query::AUTH_CONFIRM_USER => r#"UPDATE users SET confirmed_at = $1, updated_at = $2 WHERE id = $3"#, - Query::AUTH_CONSUME_CONFIRMATION_TOKEN => r#"UPDATE account_confirmation_tokens SET used_at = $1 WHERE token = $2 AND used_at IS NULL AND expires_at > $3"#, - Query::AUTH_DELETE_RESET_TOKENS_BY_USER => r#"DELETE FROM password_reset_tokens WHERE user_id = $1"#, - Query::AUTH_INSERT_RESET_TOKEN => r#"INSERT INTO password_reset_tokens (token, user_id, expires_at) VALUES ($1, $2, $3)"#, - Query::AUTH_FIND_RESET_TOKEN => r#"SELECT user_id, expires_at, used_at FROM password_reset_tokens WHERE token = $1"#, - 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_REFRESH_SESSION => { + r#"UPDATE user_sessions SET expires_at = $1 WHERE token = $2 AND expires_at > $3"# + } + Query::AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER => { + r#"DELETE FROM account_confirmation_tokens WHERE user_id = $1"# + } + Query::AUTH_INSERT_CONFIRMATION_TOKEN => { + r#"INSERT INTO account_confirmation_tokens (token, user_id, expires_at) VALUES ($1, $2, $3)"# + } + Query::AUTH_FIND_CONFIRMATION_TOKEN => { + r#"SELECT user_id, expires_at, used_at FROM account_confirmation_tokens WHERE token = $1"# + } + Query::AUTH_CONFIRM_USER => { + r#"UPDATE users SET confirmed_at = $1, updated_at = $2 WHERE id = $3"# + } + Query::AUTH_CONSUME_CONFIRMATION_TOKEN => { + r#"UPDATE account_confirmation_tokens SET used_at = $1 WHERE token = $2 AND used_at IS NULL AND expires_at > $3"# + } + Query::AUTH_DELETE_RESET_TOKENS_BY_USER => { + r#"DELETE FROM password_reset_tokens WHERE user_id = $1"# + } + Query::AUTH_INSERT_RESET_TOKEN => { + r#"INSERT INTO password_reset_tokens (token, user_id, expires_at) VALUES ($1, $2, $3)"# + } + Query::AUTH_FIND_RESET_TOKEN => { + r#"SELECT user_id, expires_at, used_at FROM password_reset_tokens WHERE token = $1"# + } + 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_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_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_EMAIL => r#"SELECT id, nickname, email, password_hash, confirmed_at 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_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_PADS => r#"SELECT p.slug, p.title, CAST(CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END AS BIGINT) AS protected, p.updated_at, CAST(CASE WHEN p.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_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = $1 UNION SELECT p.slug, p.title, CAST(CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END AS BIGINT), p.updated_at, CAST(CASE WHEN p.is_private THEN 1 ELSE 0 END AS BIGINT), CAST(0 AS BIGINT), rp.permission, COALESCE((SELECT u.nickname FROM user_pads owner_up JOIN users u ON u.id = owner_up.user_id WHERE owner_up.pad_id = p.id LIMIT 1), 'Unknown user') AS shared_by FROM resource_permissions rp JOIN pads p ON p.slug = rp.resource_slug WHERE rp.resource_kind = 'pad' AND rp.user_id = $2 ORDER BY updated_at DESC"#, - Query::USER_OWNS_WORKSPACE => r#"SELECT COUNT(*) FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = $1 AND w.slug = $2"#, - Query::USER_OWNS_PAD => r#"SELECT COUNT(*) FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = $1 AND p.slug = $2"#, + 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_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_EMAIL => { + r#"SELECT id, nickname, email, password_hash, confirmed_at 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_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_PADS => { + r#"SELECT p.slug, p.title, CAST(CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END AS BIGINT) AS protected, p.updated_at, CAST(CASE WHEN p.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_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = $1 UNION SELECT p.slug, p.title, CAST(CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END AS BIGINT), p.updated_at, CAST(CASE WHEN p.is_private THEN 1 ELSE 0 END AS BIGINT), CAST(0 AS BIGINT), rp.permission, COALESCE((SELECT u.nickname FROM user_pads owner_up JOIN users u ON u.id = owner_up.user_id WHERE owner_up.pad_id = p.id LIMIT 1), 'Unknown user') AS shared_by FROM resource_permissions rp JOIN pads p ON p.slug = rp.resource_slug WHERE rp.resource_kind = 'pad' AND rp.user_id = $2 ORDER BY updated_at DESC"# + } + Query::USER_OWNS_WORKSPACE => { + r#"SELECT COUNT(*) FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = $1 AND w.slug = $2"# + } + Query::USER_OWNS_PAD => { + r#"SELECT COUNT(*) FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = $1 AND p.slug = $2"# + } Query::USER_DELETE_WORKSPACE => r#"DELETE FROM workspaces WHERE slug = $1"#, Query::USER_DELETE_PAD => r#"DELETE FROM pads WHERE slug = $1"#, - Query::USER_SET_WORKSPACE_PASSWORD => r#"UPDATE workspaces SET password_hash = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE slug = $2"#, - Query::USER_SET_PAD_PASSWORD => r#"UPDATE pads SET password_hash = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE slug = $2"#, - Query::USER_SET_WORKSPACE_PRIVACY => r#"UPDATE workspaces SET is_private = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE slug = $2"#, - Query::USER_SET_PAD_PRIVACY => r#"UPDATE pads SET is_private = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE slug = $2"#, - Query::RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE => r#"DELETE FROM resource_access_tokens WHERE resource_kind = $1 AND resource_slug = $2"#, - Query::RESOURCE_ACCESS_TOKENS_INSERT => r#"INSERT INTO resource_access_tokens (token_hash, resource_kind, resource_slug, expires_at) VALUES ($1, $2, $3, $4)"#, - Query::RESOURCE_ACCESS_TOKENS_VALID_COUNT => r#"SELECT COUNT(*) FROM resource_access_tokens WHERE token_hash = $1 AND resource_kind = $2 AND resource_slug = $3 AND expires_at > $4"#, - Query::RESOURCE_PERMISSION_DELETE_USER => r#"DELETE FROM resource_permissions WHERE resource_kind = $1 AND resource_slug = $2 AND user_id = $3"#, - Query::RESOURCE_PERMISSION_INSERT => r#"INSERT INTO resource_permissions (resource_kind, resource_slug, user_id, permission) VALUES ($1, $2, $3, $4)"#, - Query::SHARE_INVITATION_DELETE_USER => r#"DELETE FROM resource_share_invitations WHERE resource_kind = $1 AND resource_slug = $2 AND user_id = $3"#, - Query::SHARE_INVITATION_INSERT => r#"INSERT INTO resource_share_invitations (token_hash, resource_kind, resource_slug, user_id, permission, created_by, expires_at) VALUES ($1, $2, $3, $4, $5, $6, $7)"#, - Query::SHARE_INVITATION_DELETE_TOKEN => r#"DELETE FROM resource_share_invitations WHERE token_hash = $1"#, - Query::SHARE_INVITATION_FIND_TOKEN => r#"SELECT resource_kind, resource_slug, user_id, permission, expires_at, accepted_at FROM resource_share_invitations WHERE token_hash = $1"#, - Query::SHARE_INVITATION_ACCEPT => r#"UPDATE resource_share_invitations SET accepted_at = $1 WHERE token_hash = $2"#, - Query::RESOURCE_SHARING_USERS => r#"SELECT u.email, u.nickname, rp.permission FROM resource_permissions rp JOIN users u ON u.id = rp.user_id WHERE rp.resource_kind = $1 AND rp.resource_slug = $2 ORDER BY u.email"#, - Query::RESOURCE_SHARING_LINKS => r#"SELECT token_hash, token, permission, expires_at, created_at FROM resource_share_links WHERE resource_kind = $1 AND resource_slug = $2 AND revoked_at IS NULL ORDER BY created_at DESC"#, - Query::RESOURCE_SHARING_PENDING => r#"SELECT u.email, u.nickname, i.permission, i.expires_at FROM resource_share_invitations i JOIN users u ON u.id = i.user_id WHERE i.resource_kind = $1 AND i.resource_slug = $2 AND i.accepted_at IS NULL ORDER BY u.email"#, - Query::SHARE_LINK_INSERT => r#"INSERT INTO resource_share_links (token_hash, token, resource_kind, resource_slug, permission, expires_at, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7)"#, - Query::SHARE_LINK_UPDATE => r#"UPDATE resource_share_links SET permission = $1, expires_at = $2 WHERE token_hash = $3 AND resource_kind = $4 AND resource_slug = $5 AND revoked_at IS NULL"#, - Query::SHARE_LINK_REVOKE => r#"UPDATE resource_share_links SET revoked_at = $1 WHERE token_hash = $2 AND resource_kind = $3 AND resource_slug = $4"#, - Query::RESOURCE_PERMISSION_BY_USER => r#"SELECT permission FROM resource_permissions WHERE resource_kind = $1 AND resource_slug = $2 AND user_id = $3"#, - Query::SHARE_LINK_PERMISSION => r#"SELECT permission FROM resource_share_links WHERE token_hash = $1 AND resource_kind = $2 AND resource_slug = $3 AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > $4)"#, - Query::Q001 => r#"SELECT id, slug, title, password_hash, created_at, updated_at, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS BIGINT) AS is_private FROM workspaces WHERE slug = $1"#, + Query::USER_SET_WORKSPACE_PASSWORD => { + r#"UPDATE workspaces SET password_hash = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE slug = $2"# + } + Query::USER_SET_PAD_PASSWORD => { + r#"UPDATE pads SET password_hash = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE slug = $2"# + } + Query::USER_SET_WORKSPACE_PRIVACY => { + r#"UPDATE workspaces SET is_private = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE slug = $2"# + } + Query::USER_SET_PAD_PRIVACY => { + r#"UPDATE pads SET is_private = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE slug = $2"# + } + Query::RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE => { + r#"DELETE FROM resource_access_tokens WHERE resource_kind = $1 AND resource_slug = $2"# + } + Query::RESOURCE_ACCESS_TOKENS_INSERT => { + r#"INSERT INTO resource_access_tokens (token_hash, resource_kind, resource_slug, expires_at) VALUES ($1, $2, $3, $4)"# + } + Query::RESOURCE_ACCESS_TOKENS_VALID_COUNT => { + r#"SELECT COUNT(*) FROM resource_access_tokens WHERE token_hash = $1 AND resource_kind = $2 AND resource_slug = $3 AND expires_at > $4"# + } + Query::RESOURCE_PERMISSION_DELETE_USER => { + r#"DELETE FROM resource_permissions WHERE resource_kind = $1 AND resource_slug = $2 AND user_id = $3"# + } + Query::RESOURCE_PERMISSION_INSERT => { + r#"INSERT INTO resource_permissions (resource_kind, resource_slug, user_id, permission) VALUES ($1, $2, $3, $4)"# + } + Query::SHARE_INVITATION_DELETE_USER => { + r#"DELETE FROM resource_share_invitations WHERE resource_kind = $1 AND resource_slug = $2 AND user_id = $3"# + } + Query::SHARE_INVITATION_INSERT => { + r#"INSERT INTO resource_share_invitations (token_hash, resource_kind, resource_slug, user_id, permission, created_by, expires_at) VALUES ($1, $2, $3, $4, $5, $6, $7)"# + } + Query::SHARE_INVITATION_DELETE_TOKEN => { + r#"DELETE FROM resource_share_invitations WHERE token_hash = $1"# + } + Query::SHARE_INVITATION_FIND_TOKEN => { + r#"SELECT resource_kind, resource_slug, user_id, permission, expires_at, accepted_at FROM resource_share_invitations WHERE token_hash = $1"# + } + Query::SHARE_INVITATION_ACCEPT => { + r#"UPDATE resource_share_invitations SET accepted_at = $1 WHERE token_hash = $2"# + } + Query::RESOURCE_SHARING_USERS => { + r#"SELECT u.email, u.nickname, rp.permission FROM resource_permissions rp JOIN users u ON u.id = rp.user_id WHERE rp.resource_kind = $1 AND rp.resource_slug = $2 ORDER BY u.email"# + } + Query::RESOURCE_SHARING_LINKS => { + r#"SELECT token_hash, token, permission, expires_at, created_at FROM resource_share_links WHERE resource_kind = $1 AND resource_slug = $2 AND revoked_at IS NULL ORDER BY created_at DESC"# + } + Query::RESOURCE_SHARING_PENDING => { + r#"SELECT u.email, u.nickname, i.permission, i.expires_at FROM resource_share_invitations i JOIN users u ON u.id = i.user_id WHERE i.resource_kind = $1 AND i.resource_slug = $2 AND i.accepted_at IS NULL ORDER BY u.email"# + } + Query::SHARE_LINK_INSERT => { + r#"INSERT INTO resource_share_links (token_hash, token, resource_kind, resource_slug, permission, expires_at, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7)"# + } + Query::SHARE_LINK_UPDATE => { + r#"UPDATE resource_share_links SET permission = $1, expires_at = $2 WHERE token_hash = $3 AND resource_kind = $4 AND resource_slug = $5 AND revoked_at IS NULL"# + } + Query::SHARE_LINK_REVOKE => { + r#"UPDATE resource_share_links SET revoked_at = $1 WHERE token_hash = $2 AND resource_kind = $3 AND resource_slug = $4"# + } + Query::RESOURCE_PERMISSION_BY_USER => { + r#"SELECT permission FROM resource_permissions WHERE resource_kind = $1 AND resource_slug = $2 AND user_id = $3"# + } + Query::SHARE_LINK_PERMISSION => { + r#"SELECT permission FROM resource_share_links WHERE token_hash = $1 AND resource_kind = $2 AND resource_slug = $3 AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > $4)"# + } + Query::Q001 => { + r#"SELECT id, slug, title, password_hash, created_at, updated_at, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS BIGINT) AS is_private FROM workspaces WHERE slug = $1"# + } Query::Q002 => r#"INSERT INTO workspaces (slug, title, password_hash) VALUES ($1, $2, $3)"#, - Query::Q003 => r#"SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map, CAST(CASE WHEN protected THEN 1 ELSE 0 END AS BIGINT) AS protected, created_by FROM notes WHERE workspace_id = $1 ORDER BY updated_at DESC, id DESC"#, - Query::Q004 => r#"SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map, CAST(CASE WHEN protected THEN 1 ELSE 0 END AS BIGINT) AS protected, created_by FROM notes WHERE workspace_id = $1 AND slug = $2"#, - Query::Q005 => r#"INSERT INTO notes (workspace_id, slug, title, protected, created_by) VALUES ($1, $2, $3, $4, $5)"#, - Query::Q006 => r#"UPDATE notes SET content = $1, owner_map = $2, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $3"#, - Query::Q007 => r#"UPDATE workspaces SET updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $1"#, - Query::Q008 => r#"INSERT INTO note_revisions (note_id, content, author, owner_map) VALUES ($1, $2, $3, $4)"#, + Query::Q003 => { + r#"SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map, CAST(CASE WHEN protected THEN 1 ELSE 0 END AS BIGINT) AS protected, created_by FROM notes WHERE workspace_id = $1 ORDER BY updated_at DESC, id DESC"# + } + Query::Q004 => { + r#"SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map, CAST(CASE WHEN protected THEN 1 ELSE 0 END AS BIGINT) AS protected, created_by FROM notes WHERE workspace_id = $1 AND slug = $2"# + } + Query::Q005 => { + r#"INSERT INTO notes (workspace_id, slug, title, protected, created_by) VALUES ($1, $2, $3, $4, $5)"# + } + Query::Q006 => { + r#"UPDATE notes SET content = $1, owner_map = $2, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $3"# + } + Query::Q007 => { + r#"UPDATE workspaces SET updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $1"# + } + Query::Q008 => { + r#"INSERT INTO note_revisions (note_id, content, author, owner_map) VALUES ($1, $2, $3, $4)"# + } Query::Q009 => r#"SELECT updated_at FROM notes WHERE id = $1"#, - Query::Q010 => r#"SELECT id, content, created_at, author, owner_map FROM note_revisions WHERE note_id = $1 ORDER BY id DESC LIMIT 100"#, - Query::Q011 => r#"SELECT id, slug, title, content, password_hash, created_at, updated_at, owner_map, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS BIGINT) AS is_private FROM pads WHERE slug = $1"#, + Query::Q010 => { + r#"SELECT id, content, created_at, author, owner_map FROM note_revisions WHERE note_id = $1 ORDER BY id DESC LIMIT 100"# + } + Query::Q011 => { + r#"SELECT id, slug, title, content, password_hash, created_at, updated_at, owner_map, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS BIGINT) AS is_private FROM pads WHERE slug = $1"# + } Query::Q012 => r#"INSERT INTO pads (slug, title, password_hash) VALUES ($1, $2, $3)"#, - Query::Q013 => r#"UPDATE pads SET content = $1, owner_map = $2, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $3"#, - Query::Q014 => r#"INSERT INTO revisions (pad_id, content, author, owner_map) VALUES ($1, $2, $3, $4)"#, + Query::Q013 => { + r#"UPDATE pads SET content = $1, owner_map = $2, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $3"# + } + Query::Q014 => { + r#"INSERT INTO revisions (pad_id, content, author, owner_map) VALUES ($1, $2, $3, $4)"# + } Query::Q015 => r#"SELECT updated_at FROM pads WHERE id = $1"#, - Query::Q016 => r#"SELECT id, content, created_at, author, owner_map FROM revisions WHERE pad_id = $1 ORDER BY id DESC LIMIT 100"#, + Query::Q016 => { + r#"SELECT id, content, created_at, author, owner_map FROM revisions WHERE pad_id = $1 ORDER BY id DESC LIMIT 100"# + } Query::Q017 => r#"SELECT token FROM published_pages WHERE pad_id = $1"#, Query::Q018 => r#"INSERT INTO published_pages (token, pad_id) VALUES ($1, $2)"#, Query::Q019 => r#"SELECT token FROM published_pages WHERE note_id = $1"#, Query::Q020 => r#"INSERT INTO published_pages (token, note_id) VALUES ($1, $2)"#, - Query::Q021 => r#"SELECT pp.token, pp.pad_id, pp.note_id, pp.allow_task_updates, COALESCE(p.title, n.title) AS title, COALESCE(p.content, n.content) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = $1"#, + Query::Q021 => { + r#"SELECT pp.token, pp.pad_id, pp.note_id, pp.allow_task_updates, COALESCE(p.title, n.title) AS title, COALESCE(p.content, n.content) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = $1"# + } Query::Q022 => r#"SELECT file_token FROM pads WHERE id = $1"#, Query::Q023 => r#"UPDATE pads SET file_token = $1 WHERE id = $2 AND file_token IS NULL"#, Query::Q024 => r#"SELECT file_token FROM notes WHERE id = $1"#, @@ -107,20 +259,36 @@ pub fn get(query: Query) -> &'static str { Query::Q029 => r#"SELECT content FROM revisions WHERE id = $1 AND pad_id = $2"#, Query::Q030 => r#"SELECT owner_map FROM revisions WHERE id = $1 AND pad_id = $2"#, Query::Q031 => r#"DELETE FROM notes WHERE id = $1"#, - Query::Q032 => r#"INSERT INTO note_files (note_id, filename, url, mime_type, size_bytes) VALUES ($1, $2, $3, $4, $5)"#, - Query::Q033 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE note_id = $1 ORDER BY id DESC"#, + Query::Q032 => { + r#"INSERT INTO note_files (note_id, filename, url, mime_type, size_bytes) VALUES ($1, $2, $3, $4, $5)"# + } + Query::Q033 => { + r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE note_id = $1 ORDER BY id DESC"# + } Query::Q034 => r#"UPDATE note_files SET is_attached = $1, detached_at = $2 WHERE id = $3"#, - Query::Q035 => r#"INSERT INTO pad_files (pad_id, filename, url, mime_type, size_bytes) VALUES ($1, $2, $3, $4, $5)"#, - Query::Q036 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE pad_id = $1 ORDER BY id DESC"#, + Query::Q035 => { + r#"INSERT INTO pad_files (pad_id, filename, url, mime_type, size_bytes) VALUES ($1, $2, $3, $4, $5)"# + } + Query::Q036 => { + r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE pad_id = $1 ORDER BY id DESC"# + } Query::Q037 => r#"UPDATE pad_files SET is_attached = $1, detached_at = $2 WHERE id = $3"#, - Query::Q038 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE id = $1 AND note_id = $2"#, + Query::Q038 => { + r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE id = $1 AND note_id = $2"# + } Query::Q039 => r#"DELETE FROM note_files WHERE id = $1 AND note_id = $2"#, - Query::Q046 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE id = $1 AND pad_id = $2"#, + Query::Q046 => { + r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE id = $1 AND pad_id = $2"# + } Query::Q047 => r#"DELETE FROM pad_files WHERE id = $1 AND pad_id = $2"#, Query::Q040 => r#"UPDATE published_pages SET allow_task_updates = $1 WHERE pad_id = $2"#, Query::Q041 => r#"UPDATE published_pages SET allow_task_updates = $1 WHERE note_id = $2"#, - Query::Q042 => r#"UPDATE pads SET content = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $2"#, - Query::Q043 => r#"UPDATE notes SET content = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $2"#, + Query::Q042 => { + r#"UPDATE pads SET content = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $2"# + } + Query::Q043 => { + r#"UPDATE notes SET content = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $2"# + } Query::Q044 => r#"SELECT allow_task_updates FROM published_pages WHERE pad_id = $1"#, Query::Q045 => r#"SELECT allow_task_updates FROM published_pages WHERE note_id = $1"#, } diff --git a/src/queries/sqlite.rs b/src/queries/sqlite.rs index 1c0b78a..8379fa8 100644 --- a/src/queries/sqlite.rs +++ b/src/queries/sqlite.rs @@ -7,96 +7,244 @@ pub fn get(query: Query) -> &'static str { Query::SQLITE_BUSY_TIMEOUT => r#"PRAGMA busy_timeout = 5000"#, Query::SQLITE_LAST_INSERT_ID => r#"SELECT last_insert_rowid()"#, Query::MYSQL_LAST_INSERT_ID => r#"SELECT LAST_INSERT_ID()"#, - Query::POSTGRES_NOTE_REVISION_LAST_INSERT_ID => r#"SELECT currval(pg_get_serial_sequence('note_revisions', 'id'))"#, - Query::POSTGRES_PAD_REVISION_LAST_INSERT_ID => r#"SELECT currval(pg_get_serial_sequence('revisions', 'id'))"#, - Query::AUTH_LATEST_CONFIRMATION_CREATED_AT => r#"SELECT created_at FROM account_confirmation_tokens WHERE user_id = ? ORDER BY created_at DESC LIMIT 1"#, - Query::AUTH_UPDATE_NICKNAME => r#"UPDATE users SET nickname = ?, nickname_key = ?, updated_at = ? WHERE id = ?"#, - Query::AUTH_UPDATE_EDITOR_COLOR => r#"UPDATE users SET editor_color = ?, updated_at = ? WHERE id = ?"#, + Query::POSTGRES_NOTE_REVISION_LAST_INSERT_ID => { + r#"SELECT currval(pg_get_serial_sequence('note_revisions', 'id'))"# + } + Query::POSTGRES_PAD_REVISION_LAST_INSERT_ID => { + r#"SELECT currval(pg_get_serial_sequence('revisions', 'id'))"# + } + Query::AUTH_LATEST_CONFIRMATION_CREATED_AT => { + r#"SELECT created_at FROM account_confirmation_tokens WHERE user_id = ? ORDER BY created_at DESC LIMIT 1"# + } + Query::AUTH_UPDATE_NICKNAME => { + r#"UPDATE users SET nickname = ?, nickname_key = ?, updated_at = ? WHERE id = ?"# + } + Query::AUTH_UPDATE_EDITOR_COLOR => { + r#"UPDATE users SET editor_color = ?, updated_at = ? WHERE id = ?"# + } Query::AUTH_EDITOR_COLOR_BY_USER => r#"SELECT editor_color FROM users WHERE id = ?"#, - Query::RESOURCE_COLOR_BY_USER => r#"SELECT color FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#, - Query::RESOURCE_COLOR_DELETE => r#"DELETE FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#, - Query::RESOURCE_COLOR_INSERT => r#"INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES (?, ?, ?, ?)"#, - Query::AUTH_ACCOUNT_ACTION_BY_TOKEN => r#"SELECT user_id, action, payload, expires_at, used_at FROM account_action_tokens WHERE token = ?"#, - Query::AUTH_CONSUME_ACCOUNT_ACTION => r#"UPDATE account_action_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL"#, - Query::AUTH_UPDATE_EMAIL => r#"UPDATE users SET email = ?, email_key = ?, updated_at = ? WHERE id = ?"#, - Query::AUTH_DELETE_ACCOUNT_ACTIONS => r#"DELETE FROM account_action_tokens WHERE user_id = ? AND action = ?"#, - Query::AUTH_INSERT_ACCOUNT_ACTION => r#"INSERT INTO account_action_tokens (token,user_id,action,payload,expires_at,created_at) VALUES (?, ?, ?, ?, ?, ?)"#, - Query::AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS => r#"DELETE FROM users WHERE confirmed_at IS NULL AND created_at < ?"#, - Query::AUTH_INSERT_USER => r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at) 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_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::RESOURCE_COLOR_BY_USER => { + r#"SELECT color FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"# + } + Query::RESOURCE_COLOR_DELETE => { + r#"DELETE FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"# + } + Query::RESOURCE_COLOR_INSERT => { + r#"INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES (?, ?, ?, ?)"# + } + Query::AUTH_ACCOUNT_ACTION_BY_TOKEN => { + r#"SELECT user_id, action, payload, expires_at, used_at FROM account_action_tokens WHERE token = ?"# + } + Query::AUTH_CONSUME_ACCOUNT_ACTION => { + r#"UPDATE account_action_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL"# + } + Query::AUTH_UPDATE_EMAIL => { + r#"UPDATE users SET email = ?, email_key = ?, updated_at = ? WHERE id = ?"# + } + Query::AUTH_DELETE_ACCOUNT_ACTIONS => { + r#"DELETE FROM account_action_tokens WHERE user_id = ? AND action = ?"# + } + Query::AUTH_INSERT_ACCOUNT_ACTION => { + r#"INSERT INTO account_action_tokens (token,user_id,action,payload,expires_at,created_at) VALUES (?, ?, ?, ?, ?, ?)"# + } + Query::AUTH_DELETE_EXPIRED_UNCONFIRMED_USERS => { + r#"DELETE FROM users WHERE confirmed_at IS NULL AND created_at < ?"# + } + Query::AUTH_INSERT_USER => { + r#"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash, confirmed_at) 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_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_DELETE_USER => r#"DELETE FROM users WHERE id = ?"#, 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_REFRESH_SESSION => r#"UPDATE user_sessions SET expires_at = ? WHERE token = ? AND expires_at > ?"#, - Query::AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER => r#"DELETE FROM account_confirmation_tokens WHERE user_id = ?"#, - Query::AUTH_INSERT_CONFIRMATION_TOKEN => r#"INSERT INTO account_confirmation_tokens (token, user_id, expires_at) VALUES (?, ?, ?)"#, - Query::AUTH_FIND_CONFIRMATION_TOKEN => r#"SELECT user_id, expires_at, used_at FROM account_confirmation_tokens WHERE token = ?"#, - Query::AUTH_CONFIRM_USER => r#"UPDATE users SET confirmed_at = ?, updated_at = ? WHERE id = ?"#, - Query::AUTH_CONSUME_CONFIRMATION_TOKEN => r#"UPDATE account_confirmation_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL AND expires_at > ?"#, - Query::AUTH_DELETE_RESET_TOKENS_BY_USER => r#"DELETE FROM password_reset_tokens WHERE user_id = ?"#, - Query::AUTH_INSERT_RESET_TOKEN => r#"INSERT INTO password_reset_tokens (token, user_id, expires_at) VALUES (?, ?, ?)"#, - Query::AUTH_FIND_RESET_TOKEN => r#"SELECT user_id, expires_at, used_at FROM password_reset_tokens WHERE token = ?"#, - 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_REFRESH_SESSION => { + r#"UPDATE user_sessions SET expires_at = ? WHERE token = ? AND expires_at > ?"# + } + Query::AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER => { + r#"DELETE FROM account_confirmation_tokens WHERE user_id = ?"# + } + Query::AUTH_INSERT_CONFIRMATION_TOKEN => { + r#"INSERT INTO account_confirmation_tokens (token, user_id, expires_at) VALUES (?, ?, ?)"# + } + Query::AUTH_FIND_CONFIRMATION_TOKEN => { + r#"SELECT user_id, expires_at, used_at FROM account_confirmation_tokens WHERE token = ?"# + } + Query::AUTH_CONFIRM_USER => { + r#"UPDATE users SET confirmed_at = ?, updated_at = ? WHERE id = ?"# + } + Query::AUTH_CONSUME_CONFIRMATION_TOKEN => { + r#"UPDATE account_confirmation_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL AND expires_at > ?"# + } + Query::AUTH_DELETE_RESET_TOKENS_BY_USER => { + r#"DELETE FROM password_reset_tokens WHERE user_id = ?"# + } + Query::AUTH_INSERT_RESET_TOKEN => { + r#"INSERT INTO password_reset_tokens (token, user_id, expires_at) VALUES (?, ?, ?)"# + } + Query::AUTH_FIND_RESET_TOKEN => { + r#"SELECT user_id, expires_at, used_at FROM password_reset_tokens WHERE token = ?"# + } + 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_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_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_EMAIL => r#"SELECT id, nickname, email, password_hash, confirmed_at 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_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_PADS => r#"SELECT p.slug, p.title, CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END AS protected, p.updated_at, CASE WHEN p.is_private THEN 1 ELSE 0 END AS private, 1 AS owned, 'rw' AS permission, '' AS shared_by FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = ? UNION SELECT p.slug, p.title, CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END, p.updated_at, CASE WHEN p.is_private THEN 1 ELSE 0 END, 0, rp.permission, COALESCE((SELECT u.nickname FROM user_pads owner_up JOIN users u ON u.id = owner_up.user_id WHERE owner_up.pad_id = p.id LIMIT 1), 'Unknown user') AS shared_by FROM resource_permissions rp JOIN pads p ON p.slug = rp.resource_slug WHERE rp.resource_kind = 'pad' AND rp.user_id = ? ORDER BY updated_at DESC"#, - Query::USER_OWNS_WORKSPACE => r#"SELECT COUNT(*) FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = ? AND w.slug = ?"#, - Query::USER_OWNS_PAD => r#"SELECT COUNT(*) FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = ? AND p.slug = ?"#, + 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_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_EMAIL => { + r#"SELECT id, nickname, email, password_hash, confirmed_at 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_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_PADS => { + r#"SELECT p.slug, p.title, CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END AS protected, p.updated_at, CASE WHEN p.is_private THEN 1 ELSE 0 END AS private, 1 AS owned, 'rw' AS permission, '' AS shared_by FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = ? UNION SELECT p.slug, p.title, CASE WHEN p.password_hash IS NULL THEN 0 ELSE 1 END, p.updated_at, CASE WHEN p.is_private THEN 1 ELSE 0 END, 0, rp.permission, COALESCE((SELECT u.nickname FROM user_pads owner_up JOIN users u ON u.id = owner_up.user_id WHERE owner_up.pad_id = p.id LIMIT 1), 'Unknown user') AS shared_by FROM resource_permissions rp JOIN pads p ON p.slug = rp.resource_slug WHERE rp.resource_kind = 'pad' AND rp.user_id = ? ORDER BY updated_at DESC"# + } + Query::USER_OWNS_WORKSPACE => { + r#"SELECT COUNT(*) FROM user_workspaces uw JOIN workspaces w ON w.id = uw.workspace_id WHERE uw.user_id = ? AND w.slug = ?"# + } + Query::USER_OWNS_PAD => { + r#"SELECT COUNT(*) FROM user_pads up JOIN pads p ON p.id = up.pad_id WHERE up.user_id = ? AND p.slug = ?"# + } Query::USER_DELETE_WORKSPACE => r#"DELETE FROM workspaces WHERE slug = ?"#, Query::USER_DELETE_PAD => r#"DELETE FROM pads WHERE slug = ?"#, - Query::USER_SET_WORKSPACE_PASSWORD => r#"UPDATE workspaces SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, - Query::USER_SET_PAD_PASSWORD => r#"UPDATE pads SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, - Query::USER_SET_WORKSPACE_PRIVACY => r#"UPDATE workspaces SET is_private = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, - Query::USER_SET_PAD_PRIVACY => r#"UPDATE pads SET is_private = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"#, - Query::RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE => r#"DELETE FROM resource_access_tokens WHERE resource_kind = ? AND resource_slug = ?"#, - Query::RESOURCE_ACCESS_TOKENS_INSERT => r#"INSERT INTO resource_access_tokens (token_hash, resource_kind, resource_slug, expires_at) VALUES (?, ?, ?, ?)"#, - Query::RESOURCE_ACCESS_TOKENS_VALID_COUNT => r#"SELECT COUNT(*) FROM resource_access_tokens WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND expires_at > ?"#, - Query::RESOURCE_PERMISSION_DELETE_USER => r#"DELETE FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"#, - Query::RESOURCE_PERMISSION_INSERT => r#"INSERT INTO resource_permissions (resource_kind, resource_slug, user_id, permission) VALUES (?, ?, ?, ?)"#, - Query::SHARE_INVITATION_DELETE_USER => r#"DELETE FROM resource_share_invitations WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"#, - Query::SHARE_INVITATION_INSERT => r#"INSERT INTO resource_share_invitations (token_hash, resource_kind, resource_slug, user_id, permission, created_by, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)"#, - Query::SHARE_INVITATION_DELETE_TOKEN => r#"DELETE FROM resource_share_invitations WHERE token_hash = ?"#, - Query::SHARE_INVITATION_FIND_TOKEN => r#"SELECT resource_kind, resource_slug, user_id, permission, expires_at, accepted_at FROM resource_share_invitations WHERE token_hash = ?"#, - Query::SHARE_INVITATION_ACCEPT => r#"UPDATE resource_share_invitations SET accepted_at = ? WHERE token_hash = ?"#, - Query::RESOURCE_SHARING_USERS => r#"SELECT u.email, u.nickname, rp.permission FROM resource_permissions rp JOIN users u ON u.id = rp.user_id WHERE rp.resource_kind = ? AND rp.resource_slug = ? ORDER BY u.email"#, - Query::RESOURCE_SHARING_LINKS => r#"SELECT token_hash, token, permission, expires_at, created_at FROM resource_share_links WHERE resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL ORDER BY created_at DESC"#, - Query::RESOURCE_SHARING_PENDING => r#"SELECT u.email, u.nickname, i.permission, i.expires_at FROM resource_share_invitations i JOIN users u ON u.id = i.user_id WHERE i.resource_kind = ? AND i.resource_slug = ? AND i.accepted_at IS NULL ORDER BY u.email"#, - Query::SHARE_LINK_INSERT => r#"INSERT INTO resource_share_links (token_hash, token, resource_kind, resource_slug, permission, expires_at, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)"#, - Query::SHARE_LINK_UPDATE => r#"UPDATE resource_share_links SET permission = ?, expires_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL"#, - Query::SHARE_LINK_REVOKE => r#"UPDATE resource_share_links SET revoked_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ?"#, - Query::RESOURCE_PERMISSION_BY_USER => r#"SELECT permission FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"#, - Query::SHARE_LINK_PERMISSION => r#"SELECT permission FROM resource_share_links WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)"#, - Query::Q001 => r#"SELECT id, slug, title, password_hash, created_at, updated_at, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS INTEGER) AS is_private FROM workspaces WHERE slug = ?"#, + Query::USER_SET_WORKSPACE_PASSWORD => { + r#"UPDATE workspaces SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"# + } + Query::USER_SET_PAD_PASSWORD => { + r#"UPDATE pads SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"# + } + Query::USER_SET_WORKSPACE_PRIVACY => { + r#"UPDATE workspaces SET is_private = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"# + } + Query::USER_SET_PAD_PRIVACY => { + r#"UPDATE pads SET is_private = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?"# + } + Query::RESOURCE_ACCESS_TOKENS_DELETE_BY_RESOURCE => { + r#"DELETE FROM resource_access_tokens WHERE resource_kind = ? AND resource_slug = ?"# + } + Query::RESOURCE_ACCESS_TOKENS_INSERT => { + r#"INSERT INTO resource_access_tokens (token_hash, resource_kind, resource_slug, expires_at) VALUES (?, ?, ?, ?)"# + } + Query::RESOURCE_ACCESS_TOKENS_VALID_COUNT => { + r#"SELECT COUNT(*) FROM resource_access_tokens WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND expires_at > ?"# + } + Query::RESOURCE_PERMISSION_DELETE_USER => { + r#"DELETE FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"# + } + Query::RESOURCE_PERMISSION_INSERT => { + r#"INSERT INTO resource_permissions (resource_kind, resource_slug, user_id, permission) VALUES (?, ?, ?, ?)"# + } + Query::SHARE_INVITATION_DELETE_USER => { + r#"DELETE FROM resource_share_invitations WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"# + } + Query::SHARE_INVITATION_INSERT => { + r#"INSERT INTO resource_share_invitations (token_hash, resource_kind, resource_slug, user_id, permission, created_by, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)"# + } + Query::SHARE_INVITATION_DELETE_TOKEN => { + r#"DELETE FROM resource_share_invitations WHERE token_hash = ?"# + } + Query::SHARE_INVITATION_FIND_TOKEN => { + r#"SELECT resource_kind, resource_slug, user_id, permission, expires_at, accepted_at FROM resource_share_invitations WHERE token_hash = ?"# + } + Query::SHARE_INVITATION_ACCEPT => { + r#"UPDATE resource_share_invitations SET accepted_at = ? WHERE token_hash = ?"# + } + Query::RESOURCE_SHARING_USERS => { + r#"SELECT u.email, u.nickname, rp.permission FROM resource_permissions rp JOIN users u ON u.id = rp.user_id WHERE rp.resource_kind = ? AND rp.resource_slug = ? ORDER BY u.email"# + } + Query::RESOURCE_SHARING_LINKS => { + r#"SELECT token_hash, token, permission, expires_at, created_at FROM resource_share_links WHERE resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL ORDER BY created_at DESC"# + } + Query::RESOURCE_SHARING_PENDING => { + r#"SELECT u.email, u.nickname, i.permission, i.expires_at FROM resource_share_invitations i JOIN users u ON u.id = i.user_id WHERE i.resource_kind = ? AND i.resource_slug = ? AND i.accepted_at IS NULL ORDER BY u.email"# + } + Query::SHARE_LINK_INSERT => { + r#"INSERT INTO resource_share_links (token_hash, token, resource_kind, resource_slug, permission, expires_at, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)"# + } + Query::SHARE_LINK_UPDATE => { + r#"UPDATE resource_share_links SET permission = ?, expires_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL"# + } + Query::SHARE_LINK_REVOKE => { + r#"UPDATE resource_share_links SET revoked_at = ? WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ?"# + } + Query::RESOURCE_PERMISSION_BY_USER => { + r#"SELECT permission FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"# + } + Query::SHARE_LINK_PERMISSION => { + r#"SELECT permission FROM resource_share_links WHERE token_hash = ? AND resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)"# + } + Query::Q001 => { + r#"SELECT id, slug, title, password_hash, created_at, updated_at, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS INTEGER) AS is_private FROM workspaces WHERE slug = ?"# + } Query::Q002 => r#"INSERT INTO workspaces (slug, title, password_hash) VALUES (?, ?, ?)"#, - Query::Q003 => r#"SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map, protected, created_by FROM notes WHERE workspace_id = ? ORDER BY updated_at DESC, id DESC"#, - Query::Q004 => r#"SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map, protected, created_by FROM notes WHERE workspace_id = ? AND slug = ?"#, - Query::Q005 => r#"INSERT INTO notes (workspace_id, slug, title, protected, created_by) VALUES (?, ?, ?, ?, ?)"#, - Query::Q006 => r#"UPDATE notes SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, + Query::Q003 => { + r#"SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map, protected, created_by FROM notes WHERE workspace_id = ? ORDER BY updated_at DESC, id DESC"# + } + Query::Q004 => { + r#"SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map, protected, created_by FROM notes WHERE workspace_id = ? AND slug = ?"# + } + Query::Q005 => { + r#"INSERT INTO notes (workspace_id, slug, title, protected, created_by) VALUES (?, ?, ?, ?, ?)"# + } + Query::Q006 => { + r#"UPDATE notes SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"# + } Query::Q007 => r#"UPDATE workspaces SET updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, - Query::Q008 => r#"INSERT INTO note_revisions (note_id, content, author, owner_map) VALUES (?, ?, ?, ?)"#, + Query::Q008 => { + r#"INSERT INTO note_revisions (note_id, content, author, owner_map) VALUES (?, ?, ?, ?)"# + } Query::Q009 => r#"SELECT updated_at FROM notes WHERE id = ?"#, - Query::Q010 => r#"SELECT id, content, created_at, author, owner_map FROM note_revisions WHERE note_id = ? ORDER BY id DESC LIMIT 100"#, - Query::Q011 => r#"SELECT id, slug, title, content, password_hash, created_at, updated_at, owner_map, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS INTEGER) AS is_private FROM pads WHERE slug = ?"#, + Query::Q010 => { + r#"SELECT id, content, created_at, author, owner_map FROM note_revisions WHERE note_id = ? ORDER BY id DESC LIMIT 100"# + } + Query::Q011 => { + r#"SELECT id, slug, title, content, password_hash, created_at, updated_at, owner_map, CAST(CASE WHEN is_private THEN 1 ELSE 0 END AS INTEGER) AS is_private FROM pads WHERE slug = ?"# + } Query::Q012 => r#"INSERT INTO pads (slug, title, password_hash) VALUES (?, ?, ?)"#, - Query::Q013 => r#"UPDATE pads SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, - Query::Q014 => r#"INSERT INTO revisions (pad_id, content, author, owner_map) VALUES (?, ?, ?, ?)"#, + Query::Q013 => { + r#"UPDATE pads SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"# + } + Query::Q014 => { + r#"INSERT INTO revisions (pad_id, content, author, owner_map) VALUES (?, ?, ?, ?)"# + } Query::Q015 => r#"SELECT updated_at FROM pads WHERE id = ?"#, - Query::Q016 => r#"SELECT id, content, created_at, author, owner_map FROM revisions WHERE pad_id = ? ORDER BY id DESC LIMIT 100"#, + Query::Q016 => { + r#"SELECT id, content, created_at, author, owner_map FROM revisions WHERE pad_id = ? ORDER BY id DESC LIMIT 100"# + } Query::Q017 => r#"SELECT token FROM published_pages WHERE pad_id = ?"#, Query::Q018 => r#"INSERT INTO published_pages (token, pad_id) VALUES (?, ?)"#, Query::Q019 => r#"SELECT token FROM published_pages WHERE note_id = ?"#, Query::Q020 => r#"INSERT INTO published_pages (token, note_id) VALUES (?, ?)"#, - Query::Q021 => r#"SELECT pp.token, pp.pad_id, pp.note_id, CASE WHEN pp.allow_task_updates THEN 1 ELSE 0 END AS allow_task_updates, COALESCE(p.title, n.title) AS title, COALESCE(p.content, n.content) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = ?"#, + Query::Q021 => { + r#"SELECT pp.token, pp.pad_id, pp.note_id, CASE WHEN pp.allow_task_updates THEN 1 ELSE 0 END AS allow_task_updates, COALESCE(p.title, n.title) AS title, COALESCE(p.content, n.content) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = ?"# + } Query::Q022 => r#"SELECT file_token FROM pads WHERE id = ?"#, Query::Q023 => r#"UPDATE pads SET file_token = ? WHERE id = ? AND file_token IS NULL"#, Query::Q024 => r#"SELECT file_token FROM notes WHERE id = ?"#, @@ -107,21 +255,41 @@ pub fn get(query: Query) -> &'static str { Query::Q029 => r#"SELECT content FROM revisions WHERE id = ? AND pad_id = ?"#, Query::Q030 => r#"SELECT owner_map FROM revisions WHERE id = ? AND pad_id = ?"#, Query::Q031 => r#"DELETE FROM notes WHERE id = ?"#, - Query::Q032 => r#"INSERT INTO note_files (note_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)"#, - Query::Q033 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE note_id = ? ORDER BY id DESC"#, + Query::Q032 => { + r#"INSERT INTO note_files (note_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)"# + } + Query::Q033 => { + r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE note_id = ? ORDER BY id DESC"# + } Query::Q034 => r#"UPDATE note_files SET is_attached = ?, detached_at = ? WHERE id = ?"#, - Query::Q035 => r#"INSERT INTO pad_files (pad_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)"#, - Query::Q036 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE pad_id = ? ORDER BY id DESC"#, + Query::Q035 => { + r#"INSERT INTO pad_files (pad_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)"# + } + Query::Q036 => { + r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE pad_id = ? ORDER BY id DESC"# + } Query::Q037 => r#"UPDATE pad_files SET is_attached = ?, detached_at = ? WHERE id = ?"#, - Query::Q038 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE id = ? AND note_id = ?"#, + Query::Q038 => { + r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE id = ? AND note_id = ?"# + } Query::Q039 => r#"DELETE FROM note_files WHERE id = ? AND note_id = ?"#, - Query::Q046 => r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE id = ? AND pad_id = ?"#, + Query::Q046 => { + r#"SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE id = ? AND pad_id = ?"# + } Query::Q047 => r#"DELETE FROM pad_files WHERE id = ? AND pad_id = ?"#, Query::Q040 => r#"UPDATE published_pages SET allow_task_updates = ? WHERE pad_id = ?"#, Query::Q041 => r#"UPDATE published_pages SET allow_task_updates = ? WHERE note_id = ?"#, - Query::Q042 => r#"UPDATE pads SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, - Query::Q043 => r#"UPDATE notes SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#, - Query::Q044 => r#"SELECT CASE WHEN allow_task_updates THEN 1 ELSE 0 END FROM published_pages WHERE pad_id = ?"#, - Query::Q045 => r#"SELECT CASE WHEN allow_task_updates THEN 1 ELSE 0 END FROM published_pages WHERE note_id = ?"#, + Query::Q042 => { + r#"UPDATE pads SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"# + } + Query::Q043 => { + r#"UPDATE notes SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"# + } + Query::Q044 => { + r#"SELECT CASE WHEN allow_task_updates THEN 1 ELSE 0 END FROM published_pages WHERE pad_id = ?"# + } + Query::Q045 => { + r#"SELECT CASE WHEN allow_task_updates THEN 1 ELSE 0 END FROM published_pages WHERE note_id = ?"# + } } } diff --git a/src/row_decode.rs b/src/row_decode.rs index 045be14..4aadb3d 100644 --- a/src/row_decode.rs +++ b/src/row_decode.rs @@ -1,4 +1,4 @@ -use sqlx::{any::AnyRow, ColumnIndex, Error, Row}; +use sqlx::{ColumnIndex, Error, Row, any::AnyRow}; pub fn text(row: &AnyRow, index: I) -> Result where diff --git a/src/websocket/mod.rs b/src/websocket/mod.rs index 232865a..4ac82aa 100644 --- a/src/websocket/mod.rs +++ b/src/websocket/mod.rs @@ -14,7 +14,6 @@ use serde::{Deserialize, Serialize}; use std::time::{Duration, Instant}; use tracing::{debug, info, warn}; - include!("messages.rs"); include!("note.rs"); include!("pad.rs");