Files
k-photos/crates/presentation/src/handlers/auth.rs
Gabriel Kaszewski c6f82090d2 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.
2026-05-31 22:26:02 +02:00

123 lines
3.6 KiB
Rust

use crate::{
errors::AppError,
extractors::{JwtClaims, ValidatedJson},
state::AppState,
};
use api_types::{
requests::{LoginRequest, RefreshTokenRequest, RegisterRequest},
responses::{AuthResponse, UserResponse},
};
use application::identity::{
GetProfileQuery, LoginUserCommand, RefreshTokenCommand, RegisterUserCommand,
generate_refresh_token,
};
use axum::{Json, extract::State, http::StatusCode};
#[utoipa::path(
post, path = "/api/v1/auth/register",
request_body = RegisterRequest,
responses(
(status = 201, description = "User registered", body = AuthResponse),
(status = 409, description = "Email already taken"),
(status = 422, description = "Validation error")
)
)]
pub async fn register(
State(state): State<AppState>,
ValidatedJson(req): ValidatedJson<RegisterRequest>,
) -> Result<(StatusCode, Json<AuthResponse>), AppError> {
let cmd = RegisterUserCommand {
username: req.username,
email: req.email,
password: req.password,
};
let user = state.identity.register.execute(cmd).await?;
let token = state
.token_issuer
.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),
}),
))
}
#[utoipa::path(
post, path = "/api/v1/auth/login",
request_body = LoginRequest,
responses(
(status = 200, description = "Login successful", body = AuthResponse),
(status = 401, description = "Invalid credentials")
)
)]
pub async fn login(
State(state): State<AppState>,
ValidatedJson(req): ValidatedJson<LoginRequest>,
) -> Result<Json<AuthResponse>, AppError> {
let cmd = LoginUserCommand {
email: req.email,
password: req.password,
};
let (user, token, refresh_token) = state.identity.login.execute(cmd).await?;
Ok(Json(AuthResponse {
token,
refresh_token,
user: UserResponse::from_domain(&user),
}))
}
#[utoipa::path(
get, path = "/api/v1/auth/me",
security(("bearer_token" = [])),
responses(
(status = 200, description = "Current user profile", body = UserResponse),
(status = 401, description = "Unauthorized")
)
)]
pub async fn me(
State(state): State<AppState>,
claims: JwtClaims,
) -> Result<Json<UserResponse>, AppError> {
let query = GetProfileQuery {
user_id: claims.user_id,
};
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)
}