feat(username|email): pass1: normalize auth.user data
username: now optional, if defined 2..64 chars
password: now optional (no mode __NO_PASSWORD...__)
oidc: now optional
important: if need Nextcloud, username must be defined
This commit is contained in:
@@ -60,6 +60,33 @@ async fn create_app_password(
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
// Require a claimed username. NextCloud Basic Auth resolves users by
|
||||
// username; an app password is unusable without one. UserDto carries
|
||||
// an empty string when the underlying `users.username` is NULL — the
|
||||
// entity rejects empty strings on construction, so empty here is an
|
||||
// unambiguous signal that the column is NULL.
|
||||
if let Some(auth_svc) = state.auth_service.as_ref() {
|
||||
let user_dto = auth_svc
|
||||
.auth_application_service
|
||||
.get_user_by_id(user.id)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
if user_dto.username.is_none() {
|
||||
tracing::info!(
|
||||
target: "audit",
|
||||
event = "auth.app_password_create_rejected",
|
||||
reason = "no_username",
|
||||
caller_id = %user.id,
|
||||
"App-password creation requires a claimed username"
|
||||
);
|
||||
return Err(AppError::new(
|
||||
axum::http::StatusCode::CONFLICT,
|
||||
"Claim a username on your profile before creating an app password.",
|
||||
"UsernameRequired",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let service = state
|
||||
.app_password_service
|
||||
.as_ref()
|
||||
|
||||
@@ -878,7 +878,11 @@ pub async fn oidc_exchange(
|
||||
|
||||
tracing::info!(
|
||||
"OIDC token exchange successful for user: {}",
|
||||
auth_response.user.username
|
||||
auth_response
|
||||
.user
|
||||
.username
|
||||
.as_deref()
|
||||
.unwrap_or(&auth_response.user.email)
|
||||
);
|
||||
|
||||
// Set HttpOnly cookies for the browser
|
||||
|
||||
@@ -188,11 +188,13 @@ fn if_match_passes(if_match: Option<&str>, stored_etag: &str) -> bool {
|
||||
/// they're present, prefer a "First Last" full name; otherwise fall
|
||||
/// back to the username (which is always present).
|
||||
fn user_to_contact(user: UserDto) -> ContactDto {
|
||||
// Display fallback chain: given+family name → username → email.
|
||||
// Username is `Option<String>` post PR 16; externals start with None.
|
||||
let full_name = match (user.given_name.as_deref(), user.family_name.as_deref()) {
|
||||
(Some(g), Some(f)) => format!("{g} {f}"),
|
||||
(Some(g), None) => g.to_string(),
|
||||
(None, Some(f)) => f.to_string(),
|
||||
(None, None) => user.username.clone(),
|
||||
(None, None) => user.username.clone().unwrap_or_else(|| user.email.clone()),
|
||||
};
|
||||
ContactDto {
|
||||
id: user.id.clone(),
|
||||
|
||||
Reference in New Issue
Block a user