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.
123 lines
3.6 KiB
Rust
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)
|
|
}
|