feat: add profile language preferences and refine toast, dropdown and history UI
This commit is contained in:
+34
-2
@@ -11,7 +11,7 @@ mod pages;
|
||||
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{DefaultBodyLimit, Request},
|
||||
extract::{DefaultBodyLimit, Path, Request},
|
||||
http::{HeaderMap, HeaderValue, Method, StatusCode, header},
|
||||
middleware::{self, Next},
|
||||
response::{IntoResponse, Response},
|
||||
@@ -79,6 +79,8 @@ pub fn router(
|
||||
.route("/w/{workspace_slug}", get(workspace))
|
||||
.route("/w/{workspace_slug}/n/{note_slug}", get(note))
|
||||
.route("/health", get(health))
|
||||
.route("/lang", get(language_catalog))
|
||||
.route("/lang/{code}", get(language_bundle))
|
||||
.route("/robots.txt", get(robots_txt))
|
||||
.route("/favicon.ico", get(favicon))
|
||||
.route("/favicon.svg", get(favicon_svg))
|
||||
@@ -240,6 +242,36 @@ pub fn router(
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
|
||||
async fn language_catalog() -> Response {
|
||||
let mut response = Json(crate::i18n::metadata()).into_response();
|
||||
response.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("public, max-age=300"),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
async fn language_bundle(Path(code): Path<String>) -> Response {
|
||||
let Some(source) = crate::i18n::source(&code) else {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": "Language not found" })),
|
||||
)
|
||||
.into_response();
|
||||
};
|
||||
let mut response = source.into_response();
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/json; charset=utf-8"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("public, max-age=86400"),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
async fn require_csrf_token(request: Request, next: Next) -> Response {
|
||||
let method = request.method();
|
||||
let unsafe_method = method == Method::POST
|
||||
@@ -324,7 +356,7 @@ fn is_file_path(path: &str) -> bool {
|
||||
}
|
||||
|
||||
fn is_asset_path(path: &str) -> bool {
|
||||
path == "/assets" || path.starts_with("/assets/")
|
||||
path == "/assets" || path.starts_with("/assets/") || path == "/lang" || path.starts_with("/lang/")
|
||||
}
|
||||
|
||||
fn is_icon_path(path: &str) -> bool {
|
||||
|
||||
+29
-9
@@ -130,6 +130,7 @@ fn render_editor_page(
|
||||
entrypoint: &str,
|
||||
resource_kind: &str,
|
||||
document_title: &str,
|
||||
document_title_i18n: Option<&str>,
|
||||
parent_title: &str,
|
||||
parent_url: &str,
|
||||
parent_class: &str,
|
||||
@@ -156,6 +157,13 @@ fn render_editor_page(
|
||||
.replace("__RESOURCE_ARIA_LABEL__", resource_aria_label)
|
||||
.replace("__RESOURCE_BREADCRUMB__", &resource_breadcrumb)
|
||||
.replace("__DOCUMENT_TITLE__", &escape_html(document_title))
|
||||
.replace(
|
||||
"__DOCUMENT_TITLE_I18N__",
|
||||
document_title_i18n
|
||||
.map(|key| format!(r#"data-i18n="{}""#, escape_html(key)))
|
||||
.as_deref()
|
||||
.unwrap_or(""),
|
||||
)
|
||||
.replace("__PARENT_TITLE__", &escape_html(parent_title))
|
||||
.replace("__PARENT_URL__", &escape_html(parent_url))
|
||||
.replace("__PARENT_CLASS__", parent_class)
|
||||
@@ -289,6 +297,7 @@ pub(super) async fn pad(
|
||||
"pad",
|
||||
"pad",
|
||||
&pad.title,
|
||||
None,
|
||||
"RustPad",
|
||||
"/",
|
||||
"home-brand",
|
||||
@@ -363,14 +372,19 @@ pub(super) async fn workspace(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let html = include_str!("../../static/workspace.html").replace(
|
||||
"__WORKSPACE_TITLE__",
|
||||
&escape_html(if workspace.is_private != 0 {
|
||||
"Workspace"
|
||||
} else {
|
||||
&workspace.title
|
||||
}),
|
||||
);
|
||||
let html = include_str!("../../static/workspace.html")
|
||||
.replace(
|
||||
"__WORKSPACE_TITLE__",
|
||||
&escape_html(if workspace.is_private != 0 {
|
||||
"Workspace"
|
||||
} else {
|
||||
&workspace.title
|
||||
}),
|
||||
)
|
||||
.replace(
|
||||
"__WORKSPACE_TITLE_I18N__",
|
||||
if workspace.is_private != 0 { r#"data-i18n="common.workspace""# } else { "" },
|
||||
);
|
||||
assets::render_html(
|
||||
&html,
|
||||
&state.asset_version,
|
||||
@@ -446,6 +460,7 @@ pub(super) async fn note(
|
||||
} else {
|
||||
¬e.title
|
||||
},
|
||||
if workspace.is_private != 0 { Some("common.note") } else { None },
|
||||
if workspace.is_private != 0 {
|
||||
"Workspace"
|
||||
} else {
|
||||
@@ -531,11 +546,16 @@ pub(super) fn error_response(
|
||||
asset_version: &str,
|
||||
) -> Response {
|
||||
let html = include_str!("../../static/error.html")
|
||||
.replace("__APP_THEME_BOOTSTRAP__", assets::theme_bootstrap())
|
||||
.replace(
|
||||
"__APP_THEME_BOOTSTRAP__",
|
||||
&format!("{}{}", assets::theme_bootstrap(), assets::language_bootstrap()),
|
||||
)
|
||||
.replace(
|
||||
"__APP_STYLESHEET__",
|
||||
&assets::stylesheet_tag(asset_version, "styles"),
|
||||
)
|
||||
.replace("__APP_IMPORT_MAP__", &assets::import_map_tag(asset_version))
|
||||
.replace("__APP_I18N__", assets::i18n_entrypoint())
|
||||
.replace("__ERROR_CODE__", &escape_html(code))
|
||||
.replace("__ERROR_TITLE__", &escape_html(title))
|
||||
.replace("__ERROR_MESSAGE__", &escape_html(message))
|
||||
|
||||
+17
-3
@@ -24,6 +24,7 @@ const MODULES: &[&str] = &[
|
||||
"emoji-picker",
|
||||
"image-alias",
|
||||
"image-upload",
|
||||
"i18n",
|
||||
"logger",
|
||||
"line-links",
|
||||
"markdown",
|
||||
@@ -64,7 +65,7 @@ pub fn render_html(
|
||||
urls.stylesheet_path("libs/rustpad-player/player.css"),
|
||||
);
|
||||
let html = template
|
||||
.replace("__APP_THEME_BOOTSTRAP__", theme_bootstrap())
|
||||
.replace("__APP_THEME_BOOTSTRAP__", &format!("{}{}", theme_bootstrap(), language_bootstrap()))
|
||||
.replace("__APP_STYLESHEET__", &app_stylesheets)
|
||||
.replace("__APP_IMPORT_MAP__", &urls.import_map())
|
||||
.replace("__APP_ENTRYPOINT__", &urls.entrypoint(entrypoint))
|
||||
@@ -95,10 +96,22 @@ pub fn theme_bootstrap() -> &'static str {
|
||||
r#"<script>(()=>{const key="rustpad:theme";let theme=matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";try{const saved=localStorage.getItem(key);if(saved==="light"||saved==="dark")theme=saved}catch{}const root=document.documentElement;root.dataset.theme=theme;root.style.colorScheme=theme;const meta=document.querySelector('meta[name="color-scheme"]');if(meta)meta.content=theme})();</script>"#
|
||||
}
|
||||
|
||||
pub fn language_bootstrap() -> &'static str {
|
||||
r#"<script>(()=>{const key="rustpad:language";let lang="en";try{if(localStorage.getItem("rustpad:auth-state")==="1"){const saved=localStorage.getItem(key);if(saved)lang=saved}}catch{}const root=document.documentElement;root.lang=lang;if(lang!=="en"){root.dataset.i18nPending="true";root.style.visibility="hidden"}})();</script>"#
|
||||
}
|
||||
|
||||
pub fn stylesheet_tag(asset_version: &str, name: &str) -> String {
|
||||
AssetUrls::new(asset_version).stylesheet(name)
|
||||
}
|
||||
|
||||
pub fn import_map_tag(asset_version: &str) -> String {
|
||||
AssetUrls::new(asset_version).import_map()
|
||||
}
|
||||
|
||||
pub fn i18n_entrypoint() -> &'static str {
|
||||
r#"<script type="module">import { initI18n } from "@rustpad/i18n";await initI18n();</script>"#
|
||||
}
|
||||
|
||||
fn frontend_config(
|
||||
frontend_log_level: &str,
|
||||
upload_max_size_bytes: usize,
|
||||
@@ -136,9 +149,10 @@ impl<'a> AssetUrls<'a> {
|
||||
}
|
||||
|
||||
fn entrypoint(&self, name: &str) -> String {
|
||||
let entrypoint = self.url(&format!("js/{name}.js"));
|
||||
format!(
|
||||
r#"<script type="module" src="{}"></script>"#,
|
||||
self.url(&format!("js/{name}.js"))
|
||||
r#"<script type="module">import {{ initI18n }} from "@rustpad/i18n";await initI18n();await import("{}");</script>"#,
|
||||
escape_js_string(&entrypoint)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ pub struct User {
|
||||
pub confirmed_at: Option<String>,
|
||||
pub is_active: i64,
|
||||
pub theme: String,
|
||||
pub language: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -99,6 +100,8 @@ pub struct ProfileUpdateRequest {
|
||||
editor_color: Option<String>,
|
||||
#[serde(default)]
|
||||
theme: Option<String>,
|
||||
#[serde(default)]
|
||||
language: Option<String>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteAccountRequest {
|
||||
@@ -252,6 +255,7 @@ impl<'r> sqlx::FromRow<'r, AnyRow> for User {
|
||||
confirmed_at: crate::row_decode::optional_text(row, "confirmed_at")?,
|
||||
is_active: row.try_get("is_active")?,
|
||||
theme: crate::row_decode::text(row, "theme")?,
|
||||
language: crate::row_decode::text(row, "language")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -367,6 +371,7 @@ pub struct SessionResponse {
|
||||
suggested_nickname: Option<String>,
|
||||
editor_color: Option<String>,
|
||||
theme: String,
|
||||
language: String,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
pub struct IdentityResponse {
|
||||
@@ -380,6 +385,7 @@ pub struct RegisterResponse {
|
||||
expires_at: Option<String>,
|
||||
confirmation_required: bool,
|
||||
theme: String,
|
||||
language: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
@@ -531,6 +537,7 @@ pub async fn register(
|
||||
expires_at: None,
|
||||
confirmation_required: true,
|
||||
theme: user.theme,
|
||||
language: user.language,
|
||||
message:
|
||||
"Account created. Check your e-mail and confirm the account before logging in."
|
||||
.into(),
|
||||
@@ -550,6 +557,7 @@ pub async fn register(
|
||||
expires_at: Some(session.expires_at),
|
||||
confirmation_required: false,
|
||||
theme: session.theme,
|
||||
language: session.language,
|
||||
message: "Account created.".into(),
|
||||
}),
|
||||
)
|
||||
@@ -845,6 +853,7 @@ pub async fn me(
|
||||
suggested_nickname,
|
||||
editor_color,
|
||||
theme: user.theme,
|
||||
language: user.language,
|
||||
},
|
||||
state.user_session_ttl_days,
|
||||
))
|
||||
@@ -972,11 +981,24 @@ pub async fn update_profile(
|
||||
.map_err(AuthError::database)?;
|
||||
}
|
||||
|
||||
let mut language = user.language.clone();
|
||||
if let Some(value) = req.language.as_deref() {
|
||||
language = validate_language(value)?.to_owned();
|
||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_UPDATE_LANGUAGE))
|
||||
.bind(&language)
|
||||
.bind(Utc::now().to_rfc3339())
|
||||
.bind(user.id)
|
||||
.execute(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
}
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"ok": true,
|
||||
"nickname": nickname,
|
||||
"editor_color": req.editor_color.as_deref(),
|
||||
"theme": theme,
|
||||
"language": language,
|
||||
"email_pending": email_pending,
|
||||
"message": if email_pending {
|
||||
"Profile updated. Confirm the new e-mail address using the link sent to it."
|
||||
@@ -2560,6 +2582,7 @@ async fn create_session(state: &SharedState, user: &User) -> Result<SessionRespo
|
||||
suggested_nickname,
|
||||
editor_color,
|
||||
theme: user.theme.clone(),
|
||||
language: user.language.clone(),
|
||||
})
|
||||
}
|
||||
async fn find_user_by_nickname(
|
||||
@@ -2620,6 +2643,11 @@ fn validate_theme(value: &str) -> Result<&str, AuthError> {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_language(value: &str) -> Result<&'static str, AuthError> {
|
||||
crate::i18n::canonical_code(value)
|
||||
.ok_or_else(|| AuthError::bad_request("Unknown interface language."))
|
||||
}
|
||||
|
||||
fn validate_editor_color(value: &str) -> Result<String, AuthError> {
|
||||
let value = value.trim();
|
||||
if value.len() == 7
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
|
||||
* Source-Available Code / Dual-Licensed.
|
||||
*/
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/languages.rs"));
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct LanguageMeta {
|
||||
pub code: String,
|
||||
pub name: String,
|
||||
pub native_name: String,
|
||||
pub locale: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LanguageFile {
|
||||
meta: LanguageMeta,
|
||||
translations: serde_json::Map<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct EmbeddedLanguage {
|
||||
meta: LanguageMeta,
|
||||
source: &'static str,
|
||||
}
|
||||
|
||||
static LANGUAGES: OnceLock<Vec<EmbeddedLanguage>> = OnceLock::new();
|
||||
|
||||
fn embedded_languages() -> &'static [EmbeddedLanguage] {
|
||||
LANGUAGES.get_or_init(|| {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut parsed_files = EMBEDDED_LANGUAGE_FILES
|
||||
.iter()
|
||||
.map(|(filename, source)| {
|
||||
let parsed: LanguageFile = serde_json::from_str(source)
|
||||
.unwrap_or_else(|error| panic!("invalid language file {filename}: {error}"));
|
||||
let code = parsed.meta.code.trim();
|
||||
assert!(!code.is_empty(), "language file {filename} has an empty meta.code");
|
||||
assert!(
|
||||
filename.strip_suffix(".json") == Some(code),
|
||||
"language file {filename} must use the same name as meta.code ({code})"
|
||||
);
|
||||
assert!(
|
||||
!parsed.meta.name.trim().is_empty()
|
||||
&& !parsed.meta.native_name.trim().is_empty()
|
||||
&& !parsed.meta.locale.trim().is_empty(),
|
||||
"language file {filename} has incomplete metadata"
|
||||
);
|
||||
assert!(
|
||||
!parsed.translations.is_empty(),
|
||||
"language file {filename} has no translations"
|
||||
);
|
||||
assert!(
|
||||
parsed.translations.values().all(serde_json::Value::is_string),
|
||||
"language file {filename} contains a non-string translation value"
|
||||
);
|
||||
assert!(
|
||||
seen.insert(parsed.meta.code.clone()),
|
||||
"duplicate language code {}",
|
||||
parsed.meta.code
|
||||
);
|
||||
let keys = parsed.translations.keys().cloned().collect::<std::collections::HashSet<_>>();
|
||||
(*filename, parsed.meta, *source, keys)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let fallback_keys = parsed_files
|
||||
.iter()
|
||||
.find(|(_, meta, _, _)| meta.code == "en")
|
||||
.map(|(_, _, _, keys)| keys.clone())
|
||||
.expect("lang/en.json with meta.code=en is required as the fallback language");
|
||||
|
||||
for (filename, _, _, keys) in &parsed_files {
|
||||
let missing = fallback_keys.difference(keys).take(8).cloned().collect::<Vec<_>>();
|
||||
let extra = keys.difference(&fallback_keys).take(8).cloned().collect::<Vec<_>>();
|
||||
assert!(
|
||||
missing.is_empty() && extra.is_empty(),
|
||||
"language file {filename} does not match lang/en.json keys; missing: {missing:?}; extra: {extra:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let mut languages = parsed_files
|
||||
.drain(..)
|
||||
.map(|(_, meta, source, _)| EmbeddedLanguage { meta, source })
|
||||
.collect::<Vec<_>>();
|
||||
languages.sort_by(|left, right| left.meta.native_name.cmp(&right.meta.native_name));
|
||||
languages
|
||||
})
|
||||
}
|
||||
|
||||
pub fn metadata() -> Vec<LanguageMeta> {
|
||||
embedded_languages()
|
||||
.iter()
|
||||
.map(|language| language.meta.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn source(code: &str) -> Option<&'static str> {
|
||||
let code = code.trim().trim_end_matches(".json");
|
||||
embedded_languages()
|
||||
.iter()
|
||||
.find(|language| language.meta.code.eq_ignore_ascii_case(code))
|
||||
.map(|language| language.source)
|
||||
}
|
||||
|
||||
pub fn canonical_code(code: &str) -> Option<&'static str> {
|
||||
let code = code.trim().trim_end_matches(".json");
|
||||
embedded_languages()
|
||||
.iter()
|
||||
.find(|language| language.meta.code.eq_ignore_ascii_case(code))
|
||||
.map(|language| language.meta.code.as_str())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn english_fallback_is_embedded() {
|
||||
assert!(source("en").is_some());
|
||||
assert!(source("en.json").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_has_unique_codes() {
|
||||
let metadata = metadata();
|
||||
let mut codes = metadata.iter().map(|item| item.code.as_str()).collect::<Vec<_>>();
|
||||
let len = codes.len();
|
||||
codes.sort_unstable();
|
||||
codes.dedup();
|
||||
assert_eq!(codes.len(), len);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_language_code_uses_embedded_catalog() {
|
||||
assert_eq!(canonical_code("EN"), Some("en"));
|
||||
assert_eq!(canonical_code("pl.json"), Some("pl"));
|
||||
assert_eq!(canonical_code("missing"), None);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ mod config;
|
||||
mod database;
|
||||
mod db;
|
||||
mod file_urls;
|
||||
mod i18n;
|
||||
mod queries;
|
||||
mod row_decode;
|
||||
mod security;
|
||||
|
||||
@@ -27,6 +27,7 @@ pub enum Query {
|
||||
AUTH_UPDATE_NICKNAME,
|
||||
AUTH_UPDATE_EDITOR_COLOR,
|
||||
AUTH_UPDATE_THEME,
|
||||
AUTH_UPDATE_LANGUAGE,
|
||||
AUTH_EDITOR_COLOR_BY_USER,
|
||||
RESOURCE_COLOR_BY_USER,
|
||||
RESOURCE_COLOR_DELETE,
|
||||
@@ -190,6 +191,7 @@ pub const AUTH_LATEST_CONFIRMATION_CREATED_AT: Query = Query::AUTH_LATEST_CONFIR
|
||||
pub const AUTH_UPDATE_NICKNAME: Query = Query::AUTH_UPDATE_NICKNAME;
|
||||
pub const AUTH_UPDATE_EDITOR_COLOR: Query = Query::AUTH_UPDATE_EDITOR_COLOR;
|
||||
pub const AUTH_UPDATE_THEME: Query = Query::AUTH_UPDATE_THEME;
|
||||
pub const AUTH_UPDATE_LANGUAGE: Query = Query::AUTH_UPDATE_LANGUAGE;
|
||||
pub const AUTH_EDITOR_COLOR_BY_USER: Query = Query::AUTH_EDITOR_COLOR_BY_USER;
|
||||
pub const RESOURCE_COLOR_BY_USER: Query = Query::RESOURCE_COLOR_BY_USER;
|
||||
pub const RESOURCE_COLOR_DELETE: Query = Query::RESOURCE_COLOR_DELETE;
|
||||
|
||||
@@ -32,6 +32,7 @@ pub fn get(query: Query) -> &'static str {
|
||||
r#"UPDATE users SET editor_color = ?, updated_at = ? WHERE id = ?"#
|
||||
}
|
||||
Query::AUTH_UPDATE_THEME => r#"UPDATE users SET theme = ?, updated_at = ? WHERE id = ?"#,
|
||||
Query::AUTH_UPDATE_LANGUAGE => r#"UPDATE users SET language = ?, 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 = ?"#
|
||||
@@ -104,7 +105,7 @@ pub fn get(query: Query) -> &'static str {
|
||||
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, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS SIGNED) AS is_active, CAST(theme AS CHAR CHARACTER SET utf8mb4) AS theme FROM users WHERE auth_provider = ? AND external_id = ?"#
|
||||
r#"SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS SIGNED) AS is_active, CAST(theme AS CHAR CHARACTER SET utf8mb4) AS theme, CAST(language AS CHAR CHARACTER SET utf8mb4) AS language FROM users WHERE auth_provider = ? AND external_id = ?"#
|
||||
}
|
||||
Query::AUTH_DELETE_USER => r#"DELETE FROM users WHERE id = ?"#,
|
||||
Query::AUTH_ANONYMIZE_USER => {
|
||||
@@ -151,19 +152,19 @@ pub fn get(query: Query) -> &'static str {
|
||||
}
|
||||
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, CAST(CASE WHEN u.is_active THEN 1 ELSE 0 END AS SIGNED) AS is_active, CAST(u.theme AS CHAR CHARACTER SET utf8mb4) AS theme FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ? AND s.expires_at > ? AND u.is_active = 1"#
|
||||
r#"SELECT u.id, u.nickname, u.email, CAST(u.password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, u.confirmed_at, CAST(CASE WHEN u.is_active THEN 1 ELSE 0 END AS SIGNED) AS is_active, CAST(u.theme AS CHAR CHARACTER SET utf8mb4) AS theme, CAST(u.language AS CHAR CHARACTER SET utf8mb4) AS language FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ? AND s.expires_at > ? AND u.is_active = 1"#
|
||||
}
|
||||
Query::AUTH_INSERT_SESSION => {
|
||||
r#"INSERT INTO user_sessions (token, user_id, expires_at) VALUES (?, ?, ?)"#
|
||||
}
|
||||
Query::AUTH_USER_BY_NICKNAME => {
|
||||
r#"SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS SIGNED) AS is_active, CAST(theme AS CHAR CHARACTER SET utf8mb4) AS theme FROM users WHERE nickname_key = ?"#
|
||||
r#"SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS SIGNED) AS is_active, CAST(theme AS CHAR CHARACTER SET utf8mb4) AS theme, CAST(language AS CHAR CHARACTER SET utf8mb4) AS language FROM users WHERE nickname_key = ?"#
|
||||
}
|
||||
Query::AUTH_USER_BY_EMAIL => {
|
||||
r#"SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS SIGNED) AS is_active, CAST(theme AS CHAR CHARACTER SET utf8mb4) AS theme FROM users WHERE email_key = ?"#
|
||||
r#"SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS SIGNED) AS is_active, CAST(theme AS CHAR CHARACTER SET utf8mb4) AS theme, CAST(language AS CHAR CHARACTER SET utf8mb4) AS language FROM users WHERE email_key = ?"#
|
||||
}
|
||||
Query::AUTH_USER_BY_SHARE_IDENTIFIER => {
|
||||
r#"SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS SIGNED) AS is_active, CAST(theme AS CHAR CHARACTER SET utf8mb4) AS theme FROM users JOIN (SELECT ? AS identifier) lookup ON 1 = 1 WHERE is_active = 1 AND (email_key = lookup.identifier OR LOWER(directory_username) = lookup.identifier OR LOWER(external_id) = lookup.identifier) LIMIT 1"#
|
||||
r#"SELECT id, nickname, email, CAST(password_hash AS CHAR CHARACTER SET utf8mb4) AS password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS SIGNED) AS is_active, CAST(theme AS CHAR CHARACTER SET utf8mb4) AS theme, CAST(language AS CHAR CHARACTER SET utf8mb4) AS language FROM users JOIN (SELECT ? AS identifier) lookup ON 1 = 1 WHERE is_active = 1 AND (email_key = lookup.identifier OR LOWER(directory_username) = lookup.identifier OR LOWER(external_id) = lookup.identifier) LIMIT 1"#
|
||||
}
|
||||
Query::USER_ATTACH_WORKSPACE => {
|
||||
r#"INSERT INTO user_workspaces (user_id, workspace_id) SELECT ?, id FROM workspaces WHERE slug = ?"#
|
||||
|
||||
@@ -32,6 +32,7 @@ pub fn get(query: Query) -> &'static str {
|
||||
r#"UPDATE users SET editor_color = $1, updated_at = $2 WHERE id = $3"#
|
||||
}
|
||||
Query::AUTH_UPDATE_THEME => r#"UPDATE users SET theme = $1, updated_at = $2 WHERE id = $3"#,
|
||||
Query::AUTH_UPDATE_LANGUAGE => r#"UPDATE users SET language = $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"#
|
||||
@@ -104,7 +105,7 @@ pub fn get(query: Query) -> &'static str {
|
||||
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, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS BIGINT) AS is_active, theme FROM users WHERE auth_provider = $1 AND external_id = $2"#
|
||||
r#"SELECT id, nickname, email, password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS BIGINT) AS is_active, theme, language FROM users WHERE auth_provider = $1 AND external_id = $2"#
|
||||
}
|
||||
Query::AUTH_DELETE_USER => r#"DELETE FROM users WHERE id = $1"#,
|
||||
Query::AUTH_ANONYMIZE_USER => {
|
||||
@@ -153,19 +154,19 @@ pub fn get(query: Query) -> &'static str {
|
||||
}
|
||||
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, CAST(CASE WHEN u.is_active THEN 1 ELSE 0 END AS BIGINT) AS is_active, u.theme FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.token = $1 AND s.expires_at > $2 AND u.is_active = TRUE"#
|
||||
r#"SELECT u.id, u.nickname, u.email, u.password_hash, u.confirmed_at, CAST(CASE WHEN u.is_active THEN 1 ELSE 0 END AS BIGINT) AS is_active, u.theme, u.language FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.token = $1 AND s.expires_at > $2 AND u.is_active = TRUE"#
|
||||
}
|
||||
Query::AUTH_INSERT_SESSION => {
|
||||
r#"INSERT INTO user_sessions (token, user_id, expires_at) VALUES ($1, $2, $3)"#
|
||||
}
|
||||
Query::AUTH_USER_BY_NICKNAME => {
|
||||
r#"SELECT id, nickname, email, password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS BIGINT) AS is_active, theme FROM users WHERE nickname_key = $1"#
|
||||
r#"SELECT id, nickname, email, password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS BIGINT) AS is_active, theme, language FROM users WHERE nickname_key = $1"#
|
||||
}
|
||||
Query::AUTH_USER_BY_EMAIL => {
|
||||
r#"SELECT id, nickname, email, password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS BIGINT) AS is_active, theme FROM users WHERE email_key = $1"#
|
||||
r#"SELECT id, nickname, email, password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS BIGINT) AS is_active, theme, language FROM users WHERE email_key = $1"#
|
||||
}
|
||||
Query::AUTH_USER_BY_SHARE_IDENTIFIER => {
|
||||
r#"SELECT id, nickname, email, password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS BIGINT) AS is_active, theme FROM users WHERE is_active = TRUE AND (email_key = $1 OR LOWER(directory_username) = $1 OR LOWER(external_id) = $1) LIMIT 1"#
|
||||
r#"SELECT id, nickname, email, password_hash, confirmed_at, CAST(CASE WHEN is_active THEN 1 ELSE 0 END AS BIGINT) AS is_active, theme, language FROM users WHERE is_active = TRUE AND (email_key = $1 OR LOWER(directory_username) = $1 OR LOWER(external_id) = $1) LIMIT 1"#
|
||||
}
|
||||
Query::USER_ATTACH_WORKSPACE => {
|
||||
r#"INSERT INTO user_workspaces (user_id, workspace_id) SELECT $1, id FROM workspaces WHERE slug = $2"#
|
||||
|
||||
@@ -32,6 +32,7 @@ pub fn get(query: Query) -> &'static str {
|
||||
r#"UPDATE users SET editor_color = ?, updated_at = ? WHERE id = ?"#
|
||||
}
|
||||
Query::AUTH_UPDATE_THEME => r#"UPDATE users SET theme = ?, updated_at = ? WHERE id = ?"#,
|
||||
Query::AUTH_UPDATE_LANGUAGE => r#"UPDATE users SET language = ?, 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 = ?"#
|
||||
@@ -104,7 +105,7 @@ pub fn get(query: Query) -> &'static str {
|
||||
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, is_active, theme FROM users WHERE auth_provider = ? AND external_id = ?"#
|
||||
r#"SELECT id, nickname, email, password_hash, confirmed_at, is_active, theme, language FROM users WHERE auth_provider = ? AND external_id = ?"#
|
||||
}
|
||||
Query::AUTH_DELETE_USER => r#"DELETE FROM users WHERE id = ?"#,
|
||||
Query::AUTH_ANONYMIZE_USER => {
|
||||
@@ -151,19 +152,19 @@ pub fn get(query: Query) -> &'static str {
|
||||
}
|
||||
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, u.is_active, u.theme FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ? AND s.expires_at > ? AND u.is_active = 1"#
|
||||
r#"SELECT u.id, u.nickname, u.email, u.password_hash, u.confirmed_at, u.is_active, u.theme, u.language FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ? AND s.expires_at > ? AND u.is_active = 1"#
|
||||
}
|
||||
Query::AUTH_INSERT_SESSION => {
|
||||
r#"INSERT INTO user_sessions (token, user_id, expires_at) VALUES (?, ?, ?)"#
|
||||
}
|
||||
Query::AUTH_USER_BY_NICKNAME => {
|
||||
r#"SELECT id, nickname, email, password_hash, confirmed_at, is_active, theme FROM users WHERE nickname_key = ?"#
|
||||
r#"SELECT id, nickname, email, password_hash, confirmed_at, is_active, theme, language FROM users WHERE nickname_key = ?"#
|
||||
}
|
||||
Query::AUTH_USER_BY_EMAIL => {
|
||||
r#"SELECT id, nickname, email, password_hash, confirmed_at, is_active, theme FROM users WHERE email_key = ?"#
|
||||
r#"SELECT id, nickname, email, password_hash, confirmed_at, is_active, theme, language FROM users WHERE email_key = ?"#
|
||||
}
|
||||
Query::AUTH_USER_BY_SHARE_IDENTIFIER => {
|
||||
r#"SELECT id, nickname, email, password_hash, confirmed_at, CASE WHEN is_active THEN 1 ELSE 0 END AS is_active, theme FROM users WHERE is_active = 1 AND (email_key = ?1 OR LOWER(directory_username) = ?1 OR LOWER(external_id) = ?1) LIMIT 1"#
|
||||
r#"SELECT id, nickname, email, password_hash, confirmed_at, CASE WHEN is_active THEN 1 ELSE 0 END AS is_active, theme, language FROM users WHERE is_active = 1 AND (email_key = ?1 OR LOWER(directory_username) = ?1 OR LOWER(external_id) = ?1) LIMIT 1"#
|
||||
}
|
||||
Query::USER_ATTACH_WORKSPACE => {
|
||||
r#"INSERT INTO user_workspaces (user_id, workspace_id) SELECT ?, id FROM workspaces WHERE slug = ?"#
|
||||
|
||||
Reference in New Issue
Block a user