license and some functions

This commit is contained in:
Mateusz Gruszczyński
2026-07-29 14:03:46 +02:00
parent 0655cfe48c
commit ef8dfc67c1
56 changed files with 2586 additions and 547 deletions
+12 -1
View File
@@ -1,3 +1,12 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use chrono::Utc;
use tracing::{debug, info, warn};
@@ -316,7 +325,9 @@ async fn sync_directory_user(
identity: &LdapIdentity,
email: &str,
) -> Result<User, AuthError> {
let nickname = available_directory_nickname(state, &identity.nickname, Some(user.id)).await?;
// Nickname is user-managed after provisioning. Directory synchronization must not
// overwrite a custom value on every LDAP/AD login.
let nickname = user.nickname.clone();
sqlx::query(queries::get(
state.db.kind(),
queries::AUTH_UPDATE_DIRECTORY_USER,
+9
View File
@@ -1,3 +1,12 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use tracing::{debug, info, warn};
use crate::state::SharedState;
+51 -31
View File
@@ -1,3 +1,12 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
pub(crate) mod ldap;
mod local;
@@ -632,9 +641,16 @@ fn suggested_directory_nickname(display_name: Option<&str>, email: &str) -> Opti
.filter(|word| !word.is_empty())
.collect();
if words.len() >= 2 {
let first = words.first().copied().unwrap_or_default();
let first = words
.first()
.and_then(|word| word.chars().next())
.unwrap_or('u');
let last = words.last().copied().unwrap_or_default();
let candidate = format!("{}.{}", first, last).to_lowercase();
let candidate: String = format!("{first}.{last}")
.to_lowercase()
.chars()
.take(MAX_NICKNAME)
.collect();
if let Ok(value) = validate_nickname(&candidate) {
return Some(value);
}
@@ -644,7 +660,14 @@ fn suggested_directory_nickname(display_name: Option<&str>, email: &str) -> Opti
.split('@')
.next()
.filter(|value| !value.trim().is_empty())
.and_then(|value| validate_nickname(&value.to_lowercase()).ok())
.map(|value| {
value
.to_lowercase()
.chars()
.take(MAX_NICKNAME)
.collect::<String>()
})
.and_then(|value| validate_nickname(&value).ok())
}
pub async fn me(
@@ -842,11 +865,14 @@ pub async fn confirm_account_action(
) -> Result<Json<serde_json::Value>, AuthError> {
let now = Utc::now();
let hash = hash_token(req.token.trim());
let row = sqlx::query(queries::get(state.db.kind(), queries::AUTH_ACCOUNT_ACTION_BY_TOKEN))
.bind(&hash)
.fetch_optional(state.db.pool())
.await
.map_err(AuthError::database)?;
let row = sqlx::query(queries::get(
state.db.kind(),
queries::AUTH_ACCOUNT_ACTION_BY_TOKEN,
))
.bind(&hash)
.fetch_optional(state.db.pool())
.await
.map_err(AuthError::database)?;
let row = row.ok_or_else(|| {
AuthError::bad_request("The confirmation link is invalid or has expired.")
})?;
@@ -890,14 +916,12 @@ pub async fn confirm_account_action(
.map_err(AuthError::database)?;
"E-mail address changed."
} else if action == "delete" {
let original_nickname: Option<String> = sqlx::query_scalar(queries::get(
state.db.kind(),
queries::AUTH_NICKNAME_BY_ID,
))
.bind(user_id)
.fetch_optional(&mut *tx)
.await
.map_err(AuthError::database)?;
let original_nickname: Option<String> =
sqlx::query_scalar(queries::get(state.db.kind(), queries::AUTH_NICKNAME_BY_ID))
.bind(user_id)
.fetch_optional(&mut *tx)
.await
.map_err(AuthError::database)?;
let deleted_nickname = format!("Deleted user #{user_id}");
if let Some(original_nickname) = original_nickname {
sqlx::query(queries::get(
@@ -920,20 +944,17 @@ pub async fn confirm_account_action(
.execute(&mut *tx)
.await
.map_err(AuthError::database)?;
sqlx::query(queries::get(
state.db.kind(),
queries::AUTH_ANONYMIZE_USER,
))
.bind(&deleted_nickname)
.bind(normalize(&deleted_nickname))
.bind(&deleted_email)
.bind(normalize(&deleted_email))
.bind(deleted_password)
.bind(now.to_rfc3339())
.bind(user_id)
.execute(&mut *tx)
.await
.map_err(AuthError::database)?;
sqlx::query(queries::get(state.db.kind(), queries::AUTH_ANONYMIZE_USER))
.bind(&deleted_nickname)
.bind(normalize(&deleted_nickname))
.bind(&deleted_email)
.bind(normalize(&deleted_email))
.bind(deleted_password)
.bind(now.to_rfc3339())
.bind(user_id)
.execute(&mut *tx)
.await
.map_err(AuthError::database)?;
"Account deleted. Content has been preserved under an anonymized owner."
} else {
return Err(AuthError::bad_request("Unknown account action."));
@@ -1945,7 +1966,6 @@ async fn find_user_by_external_id(
.map_err(AuthError::database)
}
async fn find_user_by_share_identifier(
state: &SharedState,
identifier: &str,