feat: auth hardening + codebase quality sweep

Refresh tokens: RefreshToken entity, PostgresRefreshTokenRepository,
login returns refresh token, POST /auth/refresh (rotation), POST /auth/logout,
JWT expiry 24h→1h, configurable via with_expiry().

Route protection: require_auth middleware on protected routes,
public routes split (register, login, refresh, sharing/access).

Authorization: caller_id added to ReadAssetFileQuery, ReadDerivativeQuery,
GetStackQuery, DeleteStackCommand with ownership checks. Admin-only gates
on processing, storage, sidecar, duplicates handlers.

Quality fixes: visibility filtering bypass in search(), unwrap panics in
date parsing, DRY auth header parsing, centralized parsers module,
email validation via email_address crate, value objects (Username, MimeType,
RelativePath), domain events (UserCreated, UserDeleted, AlbumCreated,
TagCreated, DuplicateDetected), postgres error mapping for constraint
violations, OptionExt::or_not_found helper, in_memory_repo! macro,
GetStackQuery moved to queries, album add_entry 200→201.
This commit is contained in:
2026-05-31 22:26:02 +02:00
parent 84fb410316
commit c6f82090d2
71 changed files with 2311 additions and 563 deletions

View File

@@ -4,10 +4,13 @@ use crate::{
state::AppState,
};
use api_types::{
requests::{LoginRequest, RegisterRequest},
requests::{LoginRequest, RefreshTokenRequest, RegisterRequest},
responses::{AuthResponse, UserResponse},
};
use application::identity::{GetProfileQuery, LoginUserCommand, RegisterUserCommand};
use application::identity::{
GetProfileQuery, LoginUserCommand, RefreshTokenCommand, RegisterUserCommand,
generate_refresh_token,
};
use axum::{Json, extract::State, http::StatusCode};
#[utoipa::path(
@@ -34,10 +37,13 @@ pub async fn register(
.issue(&user.id, "user")
.await
.map_err(AppError::from)?;
let (refresh_token, _) =
generate_refresh_token(&state.identity.refresh_token_repo, &user.id).await?;
Ok((
StatusCode::CREATED,
Json(AuthResponse {
token,
refresh_token,
user: UserResponse::from_domain(&user),
}),
))
@@ -59,9 +65,10 @@ pub async fn login(
email: req.email,
password: req.password,
};
let (user, token) = state.identity.login.execute(cmd).await?;
let (user, token, refresh_token) = state.identity.login.execute(cmd).await?;
Ok(Json(AuthResponse {
token,
refresh_token,
user: UserResponse::from_domain(&user),
}))
}
@@ -84,3 +91,32 @@ pub async fn me(
let user = state.identity.get_profile.execute(query).await?;
Ok(Json(UserResponse::from_domain(&user)))
}
pub async fn refresh(
State(state): State<AppState>,
ValidatedJson(req): ValidatedJson<RefreshTokenRequest>,
) -> Result<Json<AuthResponse>, AppError> {
let cmd = RefreshTokenCommand {
refresh_token: req.refresh_token,
};
let (access_token, refresh_token) = state.identity.refresh.execute(cmd).await?;
let (user_id, _) = state.token_issuer.verify(&access_token).await?;
let user = state
.identity
.get_profile
.execute(GetProfileQuery { user_id })
.await?;
Ok(Json(AuthResponse {
token: access_token,
refresh_token,
user: UserResponse::from_domain(&user),
}))
}
pub async fn logout(
State(state): State<AppState>,
claims: JwtClaims,
) -> Result<StatusCode, AppError> {
state.identity.logout.execute(&claims.user_id).await?;
Ok(StatusCode::NO_CONTENT)
}