feat(username|email): pass2: accept login via email orusername
- login via (username or email) + password
- hurl test to cover the feature
This commit is contained in:
@@ -67,6 +67,13 @@ impl From<User> for UserDto {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct LoginDto {
|
||||
/// Identifier the user typed. Accepts BOTH a username (no `@`) and
|
||||
/// an email address (`@` present). The server dispatches on
|
||||
/// `@`-in-input: with `@` it looks up by email; without, by
|
||||
/// username. The two namespaces are provably disjoint (PR 16
|
||||
/// forbids `@` in usernames), so a single field handles both
|
||||
/// without ambiguity. The frontend submits whatever the user
|
||||
/// typed in the "Username or email" field as-is.
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
@@ -432,26 +432,30 @@ impl AuthApplicationService {
|
||||
}
|
||||
|
||||
pub async fn login(&self, dto: LoginDto) -> Result<AuthResponseDto, DomainError> {
|
||||
// Find user
|
||||
let mut user = self
|
||||
.user_storage
|
||||
.get_user_by_username(&dto.username)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
// Audit: unknown-username login attempt. Reason key kept
|
||||
// stable so log search can aggregate without parsing the
|
||||
// human-readable message. Caller's client IP + request id
|
||||
// are attached automatically by the request-scope span.
|
||||
tracing::info!(
|
||||
target: "audit",
|
||||
event = "auth.login_rejected",
|
||||
reason = "unknown_user",
|
||||
attempted_username = %dto.username,
|
||||
"🔐 login rejected: no such user '{}'",
|
||||
dto.username,
|
||||
);
|
||||
DomainError::new(ErrorKind::AccessDenied, "Auth", "Invalid credentials")
|
||||
})?;
|
||||
// Dispatch on `@` in the input: presence of `@` means an email
|
||||
// was typed, absence means a username. The two namespaces are
|
||||
// provably disjoint (PR 16 forbids `@` in usernames), so this
|
||||
// is unambiguous — one DB lookup, no fallback chain.
|
||||
let lookup = if dto.username.contains('@') {
|
||||
self.user_storage.get_user_by_email(&dto.username).await
|
||||
} else {
|
||||
self.user_storage.get_user_by_username(&dto.username).await
|
||||
};
|
||||
let mut user = lookup.map_err(|_| {
|
||||
// Audit: unknown-identifier login attempt. Reason key kept
|
||||
// stable so log search can aggregate without parsing the
|
||||
// human-readable message. Caller's client IP + request id
|
||||
// are attached automatically by the request-scope span.
|
||||
tracing::info!(
|
||||
target: "audit",
|
||||
event = "auth.login_rejected",
|
||||
reason = "unknown_user",
|
||||
attempted_username = %dto.username,
|
||||
"🔐 login rejected: no such user '{}'",
|
||||
dto.username,
|
||||
);
|
||||
DomainError::new(ErrorKind::AccessDenied, "Auth", "Invalid credentials")
|
||||
})?;
|
||||
|
||||
// Check if user is active
|
||||
if !user.is_active() {
|
||||
|
||||
Reference in New Issue
Block a user