diff --git a/crates/api-types/src/auth.rs b/crates/api-types/src/auth.rs index 0e10667..fb1a31d 100644 --- a/crates/api-types/src/auth.rs +++ b/crates/api-types/src/auth.rs @@ -10,6 +10,7 @@ pub struct LoginRequest { #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct LoginResponse { pub token: String, + pub refresh_token: String, pub user_id: Uuid, pub email: String, pub expires_at: String, @@ -22,3 +23,20 @@ pub struct RegisterRequest { pub username: String, pub password: String, } + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RefreshRequest { + pub refresh_token: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RefreshResponse { + pub token: String, + pub refresh_token: String, + pub expires_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct LogoutRequest { + pub refresh_token: String, +} diff --git a/crates/presentation/src/handlers/auth.rs b/crates/presentation/src/handlers/auth.rs index 2266325..9e40715 100644 --- a/crates/presentation/src/handlers/auth.rs +++ b/crates/presentation/src/handlers/auth.rs @@ -17,7 +17,7 @@ use crate::{ render::render_page, state::AppState, }; -use api_types::{LoginRequest, LoginResponse, RegisterRequest}; +use api_types::{LoginRequest, LoginResponse, LogoutRequest, RefreshRequest, RefreshResponse, RegisterRequest}; use application::ports::HtmlPageContext; use template_askama::{LoginTemplate, RegisterTemplate}; @@ -68,6 +68,7 @@ pub async fn login( .await?; Ok(Json(LoginResponse { token: result.token, + refresh_token: result.refresh_token, user_id: result.user_id, email: result.email, expires_at: result.expires_at.to_rfc3339(), @@ -100,6 +101,41 @@ pub async fn register( Ok(StatusCode::CREATED) } +#[utoipa::path( + post, path = "/api/v1/auth/refresh", + request_body = RefreshRequest, + responses( + (status = 200, body = RefreshResponse), + (status = 401, description = "Invalid or expired refresh token"), + ) +)] +pub async fn refresh( + State(state): State, + Json(req): Json, +) -> Result, ApiError> { + let result = application::auth::refresh::execute(&state.app_ctx, &req.refresh_token).await?; + Ok(Json(RefreshResponse { + token: result.token, + refresh_token: result.refresh_token, + expires_at: result.expires_at.to_rfc3339(), + })) +} + +#[utoipa::path( + post, path = "/api/v1/auth/logout", + request_body = LogoutRequest, + responses( + (status = 204, description = "Logged out"), + ) +)] +pub async fn api_logout( + State(state): State, + Json(req): Json, +) -> StatusCode { + let _ = application::auth::logout::execute(&state.app_ctx, &req.refresh_token).await; + StatusCode::NO_CONTENT +} + // ── HTML ───────────────────────────────────────────────────────────────────── pub async fn get_login_page( diff --git a/crates/presentation/src/routes.rs b/crates/presentation/src/routes.rs index 15722f2..79ecd79 100644 --- a/crates/presentation/src/routes.rs +++ b/crates/presentation/src/routes.rs @@ -326,6 +326,8 @@ fn api_routes(rate_limit: u64) -> Router { ) .route("/auth/login", routing::post(handlers::auth::login)) .route("/auth/register", routing::post(handlers::auth::register)) + .route("/auth/refresh", routing::post(handlers::auth::refresh)) + .route("/auth/logout", routing::post(handlers::auth::api_logout)) .route("/diary/export", routing::get(handlers::diary::export_diary)) .route( "/activity-feed",