feat: refresh token

This commit is contained in:
2026-08-29 12:58:34 +02:00
parent ec1bbf729f
commit a9194fa66c
22 changed files with 537 additions and 15 deletions

View File

@@ -30,6 +30,7 @@ impl From<DomainError> for ApiError {
match e {
DomainError::NotFound(msg) => Self::NotFound(msg),
DomainError::Forbidden(msg) => Self::Forbidden(msg),
DomainError::Unauthorized(_) => Self::Unauthorized,
DomainError::Conflict(msg) => Self::Conflict(msg),
DomainError::Validation(msg) => Self::Validation(msg),
DomainError::Repository(msg) => {

View File

@@ -1,4 +1,6 @@
use api_types::auth::{AuthResponse, LoginRequest, RegisterRequest, UserResponse};
use api_types::auth::{
AuthResponse, LoginRequest, LogoutRequest, RefreshRequest, RegisterRequest, UserResponse,
};
use utoipa::OpenApi;
#[derive(OpenApi)]
@@ -6,8 +8,17 @@ use utoipa::OpenApi;
paths(
crate::routes::auth::login_handler,
crate::routes::auth::register_handler,
crate::routes::auth::refresh_handler,
crate::routes::auth::logout_handler,
crate::routes::auth::me_handler,
),
components(schemas(LoginRequest, RegisterRequest, AuthResponse, UserResponse))
components(schemas(
LoginRequest,
RegisterRequest,
RefreshRequest,
LogoutRequest,
AuthResponse,
UserResponse
))
)]
pub struct AuthDoc;

View File

@@ -5,11 +5,14 @@ use axum::{
routing::{get, post},
};
use api_types::auth::{AuthResponse, LoginRequest, RegisterRequest, UserResponse};
use api_types::auth::{
AuthResponse, LoginRequest, LogoutRequest, RefreshRequest, RegisterRequest, UserResponse,
};
use application::auth::{
commands::{LoginCommand, RegisterCommand},
login, register,
login, logout, refresh, register,
};
use domain::auth::RefreshSession;
use crate::{
error::{ApiError, ApiResult},
@@ -22,6 +25,8 @@ pub fn router() -> Router<PresentationState> {
Router::new()
.route("/login", post(login_handler))
.route("/register", post(register_handler))
.route("/refresh", post(refresh_handler))
.route("/logout", post(logout_handler))
.route("/me", get(me_handler))
}
@@ -52,9 +57,20 @@ pub async fn login_handler(
.create_token(&user)
.map_err(|e| ApiError::internal(format!("jwt error: {e}")))?;
let session = RefreshSession::new(user.id, state.ctx.config.refresh_token_ttl_seconds);
let refresh_token = session.token().to_string();
state
.ctx
.repos
.refresh_session
.create(&session)
.await
.map_err(ApiError::from)?;
Ok(Json(AuthResponse {
user: user_response(user),
access_token: token,
refresh_token,
}))
}
@@ -90,15 +106,72 @@ pub async fn register_handler(
.create_token(&user)
.map_err(|e| ApiError::internal(format!("jwt error: {e}")))?;
let session = RefreshSession::new(user.id, state.ctx.config.refresh_token_ttl_seconds);
let refresh_token = session.token().to_string();
state
.ctx
.repos
.refresh_session
.create(&session)
.await
.map_err(ApiError::from)?;
Ok((
StatusCode::CREATED,
Json(AuthResponse {
user: user_response(user),
access_token: token,
refresh_token,
}),
))
}
#[utoipa::path(
post, path = "/api/v1/auth/refresh",
request_body = RefreshRequest,
responses(
(status = 200, body = AuthResponse),
(status = 401, body = api_types::errors::ErrorResponse, description = "Invalid or expired refresh token"),
)
)]
pub async fn refresh_handler(
State(state): State<PresentationState>,
Json(payload): Json<RefreshRequest>,
) -> ApiResult<Json<AuthResponse>> {
let result = refresh::execute(&state.ctx, &payload.refresh_token)
.await
.map_err(ApiError::from)?;
let token = state
.jwt_validator
.create_token(&result.user)
.map_err(|e| ApiError::internal(format!("jwt error: {e}")))?;
Ok(Json(AuthResponse {
user: user_response(result.user),
access_token: token,
refresh_token: result.refresh_token,
}))
}
#[utoipa::path(
post, path = "/api/v1/auth/logout",
request_body = LogoutRequest,
responses(
(status = 204, description = "Logged out"),
)
)]
pub async fn logout_handler(
State(state): State<PresentationState>,
Json(payload): Json<LogoutRequest>,
) -> ApiResult<StatusCode> {
logout::execute(&state.ctx, &payload.refresh_token)
.await
.map_err(ApiError::from)?;
Ok(StatusCode::NO_CONTENT)
}
#[utoipa::path(
get, path = "/api/v1/auth/me",
responses(