Profilo utente (birthdate/sex/notes), kid agent, i18n, shared folders, UX

This commit is contained in:
2026-07-19 08:45:17 +01:00
parent 126886e309
commit c389962e3c
25 changed files with 809 additions and 117 deletions
+1 -6
View File
@@ -95,12 +95,7 @@ pub async fn me(
.ok_or_else(|| ApiError::not_found("user not found"))?;
let ui_mode = resolve_ui_mode(&skald, &user.role_id).await;
let default_locale = skald
.config()
.get(skald_core::i18n::DEFAULT_LOCALE_KEY)
.await?
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| "en".into());
let default_locale = skald_core::i18n::default_locale(skald.db()).await;
Ok(Json(MeResponse {
username: user.username,
+75
View File
@@ -25,6 +25,12 @@ pub struct CreateUserBody {
pub password: String,
#[serde(default)]
pub encrypted: bool,
#[serde(default)]
pub birthdate: Option<String>,
#[serde(default)]
pub sex: Option<String>,
#[serde(default)]
pub notes: Option<String>,
}
#[derive(Serialize)]
@@ -32,6 +38,36 @@ pub struct CreatedUser {
pub id: String,
}
/// Empty/whitespace strings normalize to `None` (the form clears a field by
/// blanking it), and the surviving values are validated: `birthdate` must be a
/// real ISO `YYYY-MM-DD` date, not in the future; the free-text fields are
/// length-capped so the prompt block stays sane.
fn normalize_profile_fields(
birthdate: Option<&str>,
sex: Option<&str>,
notes: Option<&str>,
) -> Result<(Option<String>, Option<String>, Option<String>), ApiError> {
let clean = |s: Option<&str>| s.map(str::trim).filter(|s| !s.is_empty()).map(str::to_owned);
let birthdate = clean(birthdate);
if let Some(b) = &birthdate {
let dob = chrono::NaiveDate::parse_from_str(b, "%Y-%m-%d")
.map_err(|_| ApiError::bad_request("birthdate must be a YYYY-MM-DD date"))?;
if dob > chrono::Utc::now().date_naive() {
return Err(ApiError::bad_request("birthdate cannot be in the future"));
}
}
let sex = clean(sex);
if sex.as_deref().is_some_and(|s| s.len() > 50) {
return Err(ApiError::bad_request("sex is too long (max 50 chars)"));
}
let notes = clean(notes);
if notes.as_deref().is_some_and(|s| s.len() > 2000) {
return Err(ApiError::bad_request("notes are too long (max 2000 chars)"));
}
Ok((birthdate, sex, notes))
}
pub async fn create(
State(skald): State<Arc<Skald>>,
Json(body): Json<CreateUserBody>,
@@ -43,11 +79,29 @@ pub async fn create(
if body.password.is_empty() {
return Err(ApiError::bad_request("password must not be empty"));
}
let (birthdate, sex, notes) = normalize_profile_fields(
body.birthdate.as_deref(),
body.sex.as_deref(),
body.notes.as_deref(),
)?;
let id = skald
.users()
.register_user(username, body.display_name.as_deref(), &body.role_id, Some(&body.password), body.encrypted)
.await?;
// Directory profile fields are not part of registration — set them in a
// follow-up write (keeps `UserManager::register_user`'s signature stable).
if birthdate.is_some() || sex.is_some() || notes.is_some() {
skald_core::db::users::set_directory_fields(
skald.db(),
&id,
birthdate.as_deref(),
sex.as_deref(),
notes.as_deref(),
)
.await?;
}
// Provision the user's container now (blueprint §6). Best-effort: a failure here
// is not fatal to user creation — boot reconciliation will retry.
if let Err(e) = skald.container().ensure(&id).await {
@@ -66,6 +120,12 @@ pub struct UpdateUserBody {
pub role_id: String,
#[serde(default)]
pub active: bool,
#[serde(default)]
pub birthdate: Option<String>,
#[serde(default)]
pub sex: Option<String>,
#[serde(default)]
pub notes: Option<String>,
}
pub async fn update(
@@ -77,6 +137,11 @@ pub async fn update(
if username.is_empty() {
return Err(ApiError::bad_request("username must not be empty"));
}
let (birthdate, sex, notes) = normalize_profile_fields(
body.birthdate.as_deref(),
body.sex.as_deref(),
body.notes.as_deref(),
)?;
skald_core::db::users::update_profile(
skald.db(),
&id,
@@ -89,6 +154,16 @@ pub async fn update(
// active is separate because it's a boolean flip
skald_core::db::users::set_active(skald.db(), &id, body.active).await?;
// Directory profile fields are a separate write too (same shape as active).
skald_core::db::users::set_directory_fields(
skald.db(),
&id,
birthdate.as_deref(),
sex.as_deref(),
notes.as_deref(),
)
.await?;
Ok(Json(serde_json::json!({ "ok": true })))
}