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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user