diff --git a/src/interfaces/api/handlers/admin_handler.rs b/src/interfaces/api/handlers/admin_handler.rs index d960b04e..a4d4c3d7 100644 --- a/src/interfaces/api/handlers/admin_handler.rs +++ b/src/interfaces/api/handlers/admin_handler.rs @@ -108,6 +108,7 @@ async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(Uuid, Str (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn get_oidc_settings( @@ -138,6 +139,7 @@ pub async fn get_oidc_settings( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn save_oidc_settings( @@ -198,6 +200,7 @@ async fn test_oidc_connection( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn get_storage_settings( @@ -228,6 +231,7 @@ pub async fn get_storage_settings( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn save_storage_settings( @@ -288,6 +292,7 @@ async fn test_storage_connection( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn get_migration_status( @@ -309,6 +314,7 @@ pub async fn get_migration_status( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn start_migration( @@ -386,6 +392,7 @@ pub async fn start_migration( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn pause_migration( @@ -416,6 +423,7 @@ pub async fn pause_migration( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn resume_migration( @@ -447,6 +455,7 @@ pub async fn resume_migration( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn complete_migration( @@ -486,6 +495,7 @@ pub async fn complete_migration( (status = 403, description = "Admin required"), (status = 500, description = "Verification failed") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn verify_migration( @@ -563,6 +573,7 @@ fn migration_state_to_dto( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn generate_encryption_key( @@ -619,6 +630,7 @@ fn build_backend_from_config( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn get_general_settings( @@ -660,6 +672,7 @@ pub async fn get_general_settings( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn get_dashboard_stats( @@ -750,6 +763,7 @@ pub async fn get_dashboard_stats( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn list_users( @@ -798,6 +812,7 @@ pub async fn list_users( (status = 403, description = "Admin required"), (status = 404, description = "User not found") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn get_user( @@ -834,6 +849,7 @@ pub async fn get_user( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn delete_user( @@ -883,6 +899,7 @@ pub async fn delete_user( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn update_user_role( @@ -933,6 +950,7 @@ pub async fn update_user_role( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn update_user_active( @@ -987,6 +1005,7 @@ pub async fn update_user_active( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn update_user_quota( @@ -1032,6 +1051,7 @@ pub async fn update_user_quota( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn create_user( @@ -1072,6 +1092,7 @@ pub async fn create_user( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn reset_user_password( @@ -1121,6 +1142,7 @@ pub async fn reset_user_password( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn get_registration_setting( @@ -1151,6 +1173,7 @@ pub async fn get_registration_setting( (status = 401, description = "Unauthorized"), (status = 403, description = "Admin required") ), + security(("bearerAuth" = [])), tag = "admin" )] pub async fn set_registration_setting( diff --git a/src/interfaces/api/handlers/auth_handler.rs b/src/interfaces/api/handlers/auth_handler.rs index 550385ab..afb37d66 100644 --- a/src/interfaces/api/handlers/auth_handler.rs +++ b/src/interfaces/api/handlers/auth_handler.rs @@ -6,11 +6,12 @@ use axum::{ routing::{get, post, put}, }; use std::sync::Arc; +use utoipa::ToSchema; use uuid::Uuid; use crate::application::dtos::user_dto::{ - ChangePasswordDto, LoginDto, OidcCallbackQueryDto, OidcExchangeDto, OidcProviderInfoDto, - RefreshTokenDto, RegisterDto, SetupAdminDto, + AuthResponseDto, ChangePasswordDto, LoginDto, OidcCallbackQueryDto, OidcExchangeDto, + OidcProviderInfoDto, RefreshTokenDto, RegisterDto, SetupAdminDto, UserDto, }; use crate::application::services::auth_application_service::OidcCallbackResult; use crate::common::di::AppState; @@ -18,7 +19,6 @@ use crate::interfaces::api::cookie_auth; use crate::interfaces::errors::AppError; use crate::interfaces::middleware::auth::CurrentUserId; use serde::Deserialize; -use utoipa::ToSchema; /// Public auth routes — no authentication required. pub fn auth_public_routes() -> Router> { @@ -60,7 +60,20 @@ pub fn setup_route() -> Router> { Router::new().route("/setup", post(setup_admin)) } -async fn register( +/// Register a new user account. +#[utoipa::path( + post, + path = "/api/auth/register", + request_body = RegisterDto, + responses( + (status = 201, description = "User registered successfully", body = UserDto), + (status = 400, description = "Validation error"), + (status = 403, description = "Registration disabled"), + (status = 409, description = "Username or email already taken"), + ), + tag = "auth" +)] +pub async fn register( State(state): State>, Json(dto): Json, ) -> Result { @@ -122,7 +135,23 @@ async fn register( } } -async fn login( +/// Authenticate with username and password. +/// +/// On success, sets `oxicloud_access`, `oxicloud_refresh`, and `oxicloud_csrf` +/// HttpOnly cookies in addition to returning the tokens in the JSON body. +#[utoipa::path( + post, + path = "/api/auth/login", + request_body = LoginDto, + responses( + (status = 200, description = "Login successful — tokens in body and cookies", body = AuthResponseDto), + (status = 401, description = "Invalid credentials or password login disabled"), + (status = 403, description = "Account disabled"), + (status = 429, description = "Account temporarily locked (too many failed attempts)"), + ), + tag = "auth" +)] +pub async fn login( State(state): State>, headers: HeaderMap, Json(dto): Json, @@ -238,10 +267,25 @@ async fn login( } } -/// Token refresh — accepts the refresh token from **either**: -/// 1. JSON body `{ "refresh_token": "..." }` (API clients, backward compat) -/// 2. HttpOnly cookie `oxicloud_refresh` (browsers) -async fn refresh_token( +/// Refresh an access token. +/// +/// Accepts the refresh token from **either**: +/// 1. JSON body `{ "refresh_token": "..." }` (API clients / backward compat) +/// 2. HttpOnly `oxicloud_refresh` cookie (browsers) +/// +/// Issues new access + refresh tokens and rotates all three auth cookies. +#[utoipa::path( + post, + path = "/api/auth/refresh", + request_body(content = inline(RefreshTokenDto), + description = "Optional — omit when using the HttpOnly cookie"), + responses( + (status = 200, description = "New tokens issued", body = AuthResponseDto), + (status = 401, description = "Refresh token missing, expired, or revoked"), + ), + tag = "auth" +)] +pub async fn refresh_token( State(state): State>, headers: HeaderMap, body: axum::body::Bytes, @@ -283,7 +327,18 @@ async fn refresh_token( Ok(response) } -async fn get_current_user( +/// Return the authenticated user's profile, including live storage usage. +#[utoipa::path( + get, + path = "/api/auth/me", + responses( + (status = 200, description = "Current user profile", body = UserDto), + (status = 401, description = "Not authenticated"), + ), + security(("bearerAuth" = [])), + tag = "auth" +)] +pub async fn get_current_user( State(state): State>, CurrentUserId(user_id): CurrentUserId, ) -> Result { @@ -330,7 +385,20 @@ pub struct UpdateUserImageDto { pub image: Option, } -async fn change_password( +/// Change the current user's password. +#[utoipa::path( + put, + path = "/api/auth/change-password", + request_body = ChangePasswordDto, + responses( + (status = 200, description = "Password changed successfully"), + (status = 400, description = "New password does not meet requirements"), + (status = 401, description = "Not authenticated or current password incorrect"), + ), + security(("bearerAuth" = [])), + tag = "auth" +)] +pub async fn change_password( State(state): State>, CurrentUserId(user_id): CurrentUserId, Json(dto): Json, @@ -348,6 +416,7 @@ async fn change_password( Ok(StatusCode::OK) } +// TODO: add utoipa pub async fn update_user_image( State(state): State>, CurrentUserId(user_id): CurrentUserId, @@ -371,7 +440,24 @@ pub async fn update_user_image( } } -async fn logout( +/// Revoke the current session and clear auth cookies. +/// +/// Accepts the refresh token from **either** a JSON body +/// `{ "refresh_token": "..." }` (API clients) or the `oxicloud_refresh` +/// HttpOnly cookie (browsers). +#[utoipa::path( + post, + path = "/api/auth/logout", + request_body(content = inline(RefreshTokenDto), + description = "Optional — omit when using the HttpOnly cookie"), + responses( + (status = 200, description = "Logged out, auth cookies cleared"), + (status = 401, description = "Not authenticated or refresh token missing"), + ), + security(("bearerAuth" = [])), + tag = "auth" +)] +pub async fn logout( State(state): State>, CurrentUserId(user_id): CurrentUserId, headers: HeaderMap, @@ -405,16 +491,23 @@ async fn logout( Ok(response) } -/// POST /api/setup — One-time endpoint to create the first admin user. +/// One-time endpoint to create the first admin user. /// /// Available only when the system is not yet initialized (no admin exists). -/// Once the admin is created, the system is marked as initialized and this -/// endpoint returns 403 for all subsequent requests. -/// -/// Uses an atomic "claim" operation to prevent race conditions: even if two -/// requests arrive simultaneously, only one will succeed in marking the -/// system as initialized and creating the admin. -async fn setup_admin( +/// Once the admin is created the endpoint permanently returns 403. +/// Uses an atomic "claim" operation so concurrent requests cannot both succeed. +#[utoipa::path( + post, + path = "/api/setup", + request_body = SetupAdminDto, + responses( + (status = 201, description = "First admin created and system initialized", body = UserDto), + (status = 403, description = "System already initialized"), + (status = 503, description = "Auth service not configured"), + ), + tag = "auth" +)] +pub async fn setup_admin( State(state): State>, Json(dto): Json, ) -> Result { @@ -498,19 +591,28 @@ async fn setup_admin( Ok((StatusCode::CREATED, Json(user))) } -/// Get system status - returns whether admin is configured -/// This is a public endpoint used to determine if setup is needed -#[derive(serde::Serialize)] -struct SystemStatus { - /// Whether the system has been set up with an admin +/// System initialisation state, returned by `GET /api/auth/status`. +#[derive(serde::Serialize, ToSchema)] +pub struct SystemStatus { + /// Whether the system has been set up with an admin. initialized: bool, - /// Number of admin users in the system + /// Number of admin users in the system. admin_count: i64, - /// Whether registration is allowed (only if admin exists) + /// Whether self-registration is allowed. registration_allowed: bool, } -async fn get_system_status( +/// Return the system initialisation state (used by the UI before setup). +#[utoipa::path( + get, + path = "/api/auth/status", + responses( + (status = 200, description = "System status", body = SystemStatus), + (status = 503, description = "Auth service not configured"), + ), + tag = "auth" +)] +pub async fn get_system_status( State(state): State>, ) -> Result { let auth_service = state @@ -552,8 +654,21 @@ async fn get_system_status( // OIDC Handlers // ============================================================================ -/// GET /api/auth/oidc/providers — Returns OIDC provider info for the UI -async fn oidc_providers(State(state): State>) -> Result { +/// Return OIDC provider information for the login UI. +/// +/// Returns `enabled: false` when OIDC is not configured. +#[utoipa::path( + get, + path = "/api/auth/oidc/providers", + responses( + (status = 200, description = "OIDC provider info (enabled=false when OIDC not configured)", body = OidcProviderInfoDto), + (status = 503, description = "Auth service not configured"), + ), + tag = "auth" +)] +pub async fn oidc_providers( + State(state): State>, +) -> Result { let auth_service = state .auth_service .as_ref() @@ -580,8 +695,23 @@ async fn oidc_providers(State(state): State>) -> Result>) -> Result { +/// Initiate OIDC authorization — redirects to the configured identity provider. +/// +/// Generates PKCE, CSRF state, and nonce then issues a 302 redirect to the +/// provider's authorization endpoint. +#[utoipa::path( + get, + path = "/api/auth/oidc/authorize", + responses( + (status = 302, description = "Redirect to OIDC provider authorization URL"), + (status = 404, description = "OIDC not enabled"), + (status = 503, description = "Auth service not configured"), + ), + tag = "auth" +)] +pub async fn oidc_authorize( + State(state): State>, +) -> Result { let auth_service = state .auth_service .as_ref() @@ -605,8 +735,26 @@ async fn oidc_authorize(State(state): State>) -> Result>, Query(query): Query, ) -> Result { @@ -694,9 +842,21 @@ async fn oidc_callback( } } -/// POST /api/auth/oidc/exchange — Exchange one-time code for auth tokens -/// Request body: { "code": "" } -async fn oidc_exchange( +/// Exchange a one-time OIDC code for access + refresh tokens. +/// +/// The frontend calls this after being redirected back with `?oidc_code=…`. +/// The exchange code is valid for a single use and expires in 60 s. +#[utoipa::path( + post, + path = "/api/auth/oidc/exchange", + request_body = OidcExchangeDto, + responses( + (status = 200, description = "Tokens issued, auth cookies set", body = AuthResponseDto), + (status = 401, description = "Exchange code invalid or expired"), + ), + tag = "auth" +)] +pub async fn oidc_exchange( State(state): State>, Json(body): Json, ) -> Result { diff --git a/src/interfaces/api/handlers/batch_handler.rs b/src/interfaces/api/handlers/batch_handler.rs index 7ca55116..ff686653 100644 --- a/src/interfaces/api/handlers/batch_handler.rs +++ b/src/interfaces/api/handlers/batch_handler.rs @@ -145,6 +145,7 @@ where (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized") ), + security(("bearerAuth" = [])), tag = "batch" )] pub async fn move_files_batch( @@ -212,6 +213,7 @@ pub async fn move_files_batch( (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized") ), + security(("bearerAuth" = [])), tag = "batch" )] pub async fn copy_files_batch( @@ -279,6 +281,7 @@ pub async fn copy_files_batch( (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized") ), + security(("bearerAuth" = [])), tag = "batch" )] pub async fn delete_files_batch( @@ -354,6 +357,7 @@ pub async fn delete_files_batch( (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized") ), + security(("bearerAuth" = [])), tag = "batch" )] pub async fn delete_folders_batch( @@ -429,6 +433,7 @@ pub async fn delete_folders_batch( (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized") ), + security(("bearerAuth" = [])), tag = "batch" )] pub async fn create_folders_batch( @@ -503,6 +508,7 @@ pub async fn create_folders_batch( (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized") ), + security(("bearerAuth" = [])), tag = "batch" )] pub async fn get_files_batch( @@ -570,6 +576,7 @@ pub async fn get_files_batch( (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized") ), + security(("bearerAuth" = [])), tag = "batch" )] pub async fn get_folders_batch( @@ -677,6 +684,7 @@ impl From for BatchDownloadRequest { (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized") ), + security(("bearerAuth" = [])), tag = "batch" )] pub async fn trash_batch( @@ -800,6 +808,7 @@ pub async fn trash_batch( (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized") ), + security(("bearerAuth" = [])), tag = "batch" )] pub async fn move_folders_batch( @@ -884,6 +893,7 @@ impl From for CopiedFolderDto { (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized") ), + security(("bearerAuth" = [])), tag = "batch" )] pub async fn copy_folders_batch( @@ -951,6 +961,7 @@ pub async fn copy_folders_batch( (status = 401, description = "Unauthorized"), (status = 500, description = "ZIP creation failed") ), + security(("bearerAuth" = [])), tag = "batch" )] pub async fn download_batch_querystring( @@ -973,6 +984,7 @@ pub async fn download_batch_querystring( (status = 401, description = "Unauthorized"), (status = 500, description = "ZIP creation failed") ), + security(("bearerAuth" = [])), tag = "batch" )] pub async fn download_batch_post( diff --git a/src/interfaces/api/handlers/contacts_handler.rs b/src/interfaces/api/handlers/contacts_handler.rs index 479985d2..3e55092a 100644 --- a/src/interfaces/api/handlers/contacts_handler.rs +++ b/src/interfaces/api/handlers/contacts_handler.rs @@ -221,6 +221,7 @@ fn user_to_contact(user: UserDto) -> ContactDto { (status = 200, description = "List of address books"), (status = 500, description = "Internal server error"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn list_address_books( @@ -289,6 +290,7 @@ pub async fn list_address_books( (status = 201, description = "Address book created", body = AddressBookResponse), (status = 400, description = "Invalid input"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn create_address_book( @@ -336,6 +338,7 @@ pub async fn create_address_book( (status = 403, description = "Only the owner can update"), (status = 404, description = "Address book not found"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn update_address_book( @@ -391,6 +394,7 @@ pub async fn update_address_book( (status = 403, description = "Only the owner can delete"), (status = 404, description = "Address book not found"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn delete_address_book( @@ -431,6 +435,7 @@ pub async fn delete_address_book( (status = 403, description = "Access denied"), (status = 404, description = "Address book not found"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn list_contacts( @@ -487,6 +492,7 @@ pub async fn list_contacts( (status = 403, description = "Access denied or read-only book"), (status = 404, description = "Address book not found"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn create_contact( @@ -541,6 +547,7 @@ pub async fn create_contact( (status = 403, description = "Access denied"), (status = 404, description = "Contact not found"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn get_contact( @@ -607,6 +614,7 @@ pub async fn get_contact( (status = 404, description = "Contact not found"), (status = 412, description = "ETag mismatch — contact was modified"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn update_contact( @@ -687,6 +695,7 @@ pub async fn update_contact( (status = 404, description = "Contact not found"), (status = 412, description = "ETag mismatch"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn delete_contact( @@ -747,6 +756,7 @@ pub async fn delete_contact( (status = 403, description = "Access denied"), (status = 404, description = "Address book not found"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn list_groups( @@ -780,6 +790,7 @@ pub async fn list_groups( (status = 403, description = "Access denied or read-only book"), (status = 404, description = "Address book not found"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn create_group( @@ -818,6 +829,7 @@ pub async fn create_group( (status = 403, description = "Access denied"), (status = 404, description = "Group not found"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn get_group( @@ -858,6 +870,7 @@ pub async fn get_group( (status = 403, description = "Access denied or read-only book"), (status = 404, description = "Group not found"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn update_group( @@ -895,6 +908,7 @@ pub async fn update_group( (status = 403, description = "Access denied or read-only book"), (status = 404, description = "Group not found"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn delete_group( @@ -933,6 +947,7 @@ pub async fn delete_group( (status = 403, description = "Access denied"), (status = 404, description = "Group not found"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn list_contacts_in_group( @@ -974,6 +989,7 @@ pub async fn list_contacts_in_group( (status = 403, description = "Access denied or read-only book"), (status = 404, description = "Group or contact not found"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn add_contact_to_group( @@ -1016,6 +1032,7 @@ pub async fn add_contact_to_group( (status = 403, description = "Access denied or read-only book"), (status = 404, description = "Group or contact not found"), ), + security(("bearerAuth" = [])), tag = "contacts" )] pub async fn remove_contact_from_group( diff --git a/src/interfaces/api/handlers/favorites_handler.rs b/src/interfaces/api/handlers/favorites_handler.rs index b583c0fa..25ff376d 100644 --- a/src/interfaces/api/handlers/favorites_handler.rs +++ b/src/interfaces/api/handlers/favorites_handler.rs @@ -33,6 +33,7 @@ pub struct BatchFavoritesRequest { responses( (status = 200, description = "List of favorites", body = Vec) ), + security(("bearerAuth" = [])), tag = "favorites" )] pub async fn get_favorites( @@ -75,6 +76,7 @@ pub async fn get_favorites( (status = 201, description = "Item added to favorites"), (status = 400, description = "Invalid item type") ), + security(("bearerAuth" = [])), tag = "favorites" )] pub async fn add_favorite( @@ -131,6 +133,7 @@ pub async fn add_favorite( (status = 200, description = "Item removed from favorites"), (status = 404, description = "Item not in favorites") ), + security(("bearerAuth" = [])), tag = "favorites" )] pub async fn remove_favorite( @@ -184,6 +187,7 @@ pub async fn remove_favorite( (status = 200, description = "Batch add result", body = crate::application::dtos::favorites_dto::BatchFavoritesResult), (status = 400, description = "Invalid request") ), + security(("bearerAuth" = [])), tag = "favorites" )] pub async fn batch_add_favorites( diff --git a/src/interfaces/api/handlers/file_handler.rs b/src/interfaces/api/handlers/file_handler.rs index c664b55f..0a7866a9 100644 --- a/src/interfaces/api/handlers/file_handler.rs +++ b/src/interfaces/api/handlers/file_handler.rs @@ -1080,6 +1080,7 @@ pub(super) fn build_content_disposition(name: &str, mime: &str, force_inline: bo (status = 200, description = "List of files", body = Vec), (status = 304, description = "Not modified"), ), + security(("bearerAuth" = [])), tag = "files" )] pub async fn list_files_query( @@ -1100,6 +1101,7 @@ pub async fn list_files_query( (status = 400, description = "Invalid request"), (status = 507, description = "Storage quota exceeded"), ), + security(("bearerAuth" = [])), tag = "files" )] pub async fn upload_file_with_thumbnails( @@ -1125,6 +1127,7 @@ pub async fn upload_file_with_thumbnails( (status = 304, description = "Not modified"), (status = 404, description = "File not found"), ), + security(("bearerAuth" = [])), tag = "files" )] pub async fn download_file( @@ -1150,6 +1153,7 @@ pub async fn download_file( (status = 304, description = "Not modified"), (status = 404, description = "File not found"), ), + security(("bearerAuth" = [])), tag = "files" )] pub async fn get_thumbnail( @@ -1174,6 +1178,7 @@ pub async fn get_thumbnail( (status = 400, description = "Invalid image or size too large"), (status = 404, description = "File not found"), ), + security(("bearerAuth" = [])), tag = "files" )] pub async fn upload_thumbnail( @@ -1193,6 +1198,7 @@ pub async fn upload_thumbnail( (status = 200, description = "File metadata (EXIF, dimensions, duration, etc.)"), (status = 404, description = "File not found"), ), + security(("bearerAuth" = [])), tag = "files" )] pub async fn get_file_metadata( @@ -1211,6 +1217,7 @@ pub async fn get_file_metadata( (status = 204, description = "File deleted (moved to trash if enabled)"), (status = 404, description = "File not found"), ), + security(("bearerAuth" = [])), tag = "files" )] pub async fn delete_file( @@ -1230,6 +1237,7 @@ pub async fn delete_file( (status = 200, description = "Renamed file", body = FileDto), (status = 404, description = "File not found"), ), + security(("bearerAuth" = [])), tag = "files" )] pub async fn rename_file( @@ -1250,6 +1258,7 @@ pub async fn rename_file( (status = 200, description = "Moved file", body = FileDto), (status = 404, description = "File or destination not found"), ), + security(("bearerAuth" = [])), tag = "files" )] pub async fn move_file_simple( diff --git a/src/interfaces/api/handlers/folder_handler.rs b/src/interfaces/api/handlers/folder_handler.rs index 370722be..90f77bd5 100644 --- a/src/interfaces/api/handlers/folder_handler.rs +++ b/src/interfaces/api/handlers/folder_handler.rs @@ -420,6 +420,7 @@ impl FolderHandler { (status = 201, description = "Folder created", body = FolderDto), (status = 400, description = "Invalid request"), ), + security(("bearerAuth" = [])), tag = "folders" )] pub async fn create_folder( @@ -438,6 +439,7 @@ pub async fn create_folder( (status = 200, description = "Folder", body = FolderDto), (status = 404, description = "Folder not found"), ), + security(("bearerAuth" = [])), tag = "folders" )] pub async fn get_folder( @@ -454,6 +456,7 @@ pub async fn get_folder( responses( (status = 200, description = "List of root folders", body = Vec), ), + security(("bearerAuth" = [])), tag = "folders" )] pub async fn list_root_folders( @@ -471,6 +474,7 @@ pub async fn list_root_folders( (status = 200, description = "List of sub-folders", body = Vec), (status = 404, description = "Folder not found"), ), + security(("bearerAuth" = [])), tag = "folders" )] pub async fn list_folder_contents( @@ -488,6 +492,7 @@ pub async fn list_folder_contents( responses( (status = 200, description = "Paginated list of root folders"), ), + security(("bearerAuth" = [])), tag = "folders" )] pub async fn list_root_folders_paginated( @@ -509,6 +514,7 @@ pub async fn list_root_folders_paginated( (status = 200, description = "Paginated list of sub-folders"), (status = 404, description = "Folder not found"), ), + security(("bearerAuth" = [])), tag = "folders" )] pub async fn list_folder_contents_paginated( @@ -529,6 +535,7 @@ pub async fn list_folder_contents_paginated( (status = 304, description = "Not modified"), (status = 404, description = "Folder not found"), ), + security(("bearerAuth" = [])), tag = "folders" )] pub async fn list_folder_listing( @@ -549,6 +556,7 @@ pub async fn list_folder_listing( (status = 200, description = "Renamed folder", body = FolderDto), (status = 404, description = "Folder not found"), ), + security(("bearerAuth" = [])), tag = "folders" )] pub async fn rename_folder( @@ -569,6 +577,7 @@ pub async fn rename_folder( (status = 200, description = "Moved folder", body = FolderDto), (status = 404, description = "Folder or destination not found"), ), + security(("bearerAuth" = [])), tag = "folders" )] pub async fn move_folder( @@ -588,6 +597,7 @@ pub async fn move_folder( (status = 204, description = "Folder deleted"), (status = 404, description = "Folder not found"), ), + security(("bearerAuth" = [])), tag = "folders" )] pub async fn delete_folder_with_trash( @@ -607,6 +617,7 @@ pub async fn delete_folder_with_trash( (status = 404, description = "Folder not found"), (status = 501, description = "ZIP service not available"), ), + security(("bearerAuth" = [])), tag = "folders" )] pub async fn download_folder_zip( diff --git a/src/interfaces/api/handlers/grant_handler.rs b/src/interfaces/api/handlers/grant_handler.rs index eb813ccb..4db3fe4e 100644 --- a/src/interfaces/api/handlers/grant_handler.rs +++ b/src/interfaces/api/handlers/grant_handler.rs @@ -50,6 +50,7 @@ type AppStateRef = Arc; (status = 400, description = "Invalid input (both/neither of permissions+role provided)"), (status = 404, description = "Resource not found OR caller lacks Share permission"), ), + security(("bearerAuth" = [])), tag = "grants" )] pub async fn create_grant( @@ -125,6 +126,7 @@ pub async fn create_grant( (status = 204, description = "Grant revoked (or did not exist)"), (status = 404, description = "Caller lacks Share permission on the underlying resource"), ), + security(("bearerAuth" = [])), tag = "grants" )] pub async fn revoke_grant( @@ -174,6 +176,7 @@ pub async fn revoke_grant( (status = 200, description = "Role applied; returns the new full grant set", body = Vec), (status = 404, description = "Resource not found or caller lacks Share"), ), + security(("bearerAuth" = [])), tag = "grants" )] pub async fn set_role( @@ -261,6 +264,7 @@ pub struct IncomingQuery { responses( (status = 200, description = "Direct grants targeting the caller", body = Vec), ), + security(("bearerAuth" = [])), tag = "grants" )] pub async fn list_incoming( @@ -298,6 +302,7 @@ pub async fn list_incoming( last page.", body = SharedWithMeDto), ), + security(("bearerAuth" = [])), tag = "grants" )] pub async fn list_shared_with_me( @@ -456,6 +461,7 @@ pub async fn list_shared_with_me( responses( (status = 200, description = "Grants the caller has created", body = Vec), ), + security(("bearerAuth" = [])), tag = "grants" )] pub async fn list_outgoing( @@ -491,6 +497,7 @@ pub struct OnResourceQuery { (status = 200, description = "Grants on the specified resource", body = Vec), (status = 404, description = "Resource not found or caller lacks Share"), ), + security(("bearerAuth" = [])), tag = "grants" )] pub async fn list_on_resource( diff --git a/src/interfaces/api/handlers/music_handler.rs b/src/interfaces/api/handlers/music_handler.rs index 58bcce65..b1662d27 100644 --- a/src/interfaces/api/handlers/music_handler.rs +++ b/src/interfaces/api/handlers/music_handler.rs @@ -31,6 +31,7 @@ pub struct PaginationQuery { (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized") ), + security(("bearerAuth" = [])), tag = "playlists" )] pub async fn create_playlist( @@ -53,6 +54,7 @@ pub async fn create_playlist( (status = 401, description = "Unauthorized"), (status = 404, description = "Playlist not found") ), + security(("bearerAuth" = [])), tag = "playlists" )] pub async fn get_playlist( @@ -73,6 +75,7 @@ pub async fn get_playlist( (status = 200, description = "List of playlists"), (status = 401, description = "Unauthorized") ), + security(("bearerAuth" = [])), tag = "playlists" )] pub async fn list_playlists( @@ -101,6 +104,7 @@ pub struct IncludeSharedQuery { (status = 401, description = "Unauthorized"), (status = 404, description = "Playlist not found") ), + security(("bearerAuth" = [])), tag = "playlists" )] pub async fn update_playlist( @@ -127,6 +131,7 @@ pub async fn update_playlist( (status = 401, description = "Unauthorized"), (status = 404, description = "Playlist not found") ), + security(("bearerAuth" = [])), tag = "playlists" )] pub async fn delete_playlist( @@ -152,6 +157,7 @@ pub async fn delete_playlist( (status = 401, description = "Unauthorized"), (status = 404, description = "Playlist not found") ), + security(("bearerAuth" = [])), tag = "playlists" )] pub async fn add_tracks( @@ -181,6 +187,7 @@ pub async fn add_tracks( (status = 401, description = "Unauthorized"), (status = 404, description = "Playlist or track not found") ), + security(("bearerAuth" = [])), tag = "playlists" )] pub async fn remove_track( @@ -206,6 +213,7 @@ pub async fn remove_track( (status = 401, description = "Unauthorized"), (status = 404, description = "Playlist not found") ), + security(("bearerAuth" = [])), tag = "playlists" )] pub async fn reorder_tracks( @@ -232,6 +240,7 @@ pub async fn reorder_tracks( (status = 401, description = "Unauthorized"), (status = 404, description = "Playlist not found") ), + security(("bearerAuth" = [])), tag = "playlists" )] pub async fn list_playlist_tracks( @@ -257,6 +266,7 @@ pub async fn list_playlist_tracks( (status = 401, description = "Unauthorized"), (status = 404, description = "Playlist not found") ), + security(("bearerAuth" = [])), tag = "playlists" )] pub async fn share_playlist( @@ -286,6 +296,7 @@ pub async fn share_playlist( (status = 401, description = "Unauthorized"), (status = 404, description = "Playlist or share not found") ), + security(("bearerAuth" = [])), tag = "playlists" )] pub async fn remove_share( @@ -311,6 +322,7 @@ pub async fn remove_share( (status = 401, description = "Unauthorized"), (status = 404, description = "Playlist not found") ), + security(("bearerAuth" = [])), tag = "playlists" )] pub async fn get_playlist_shares( @@ -336,6 +348,7 @@ pub async fn get_playlist_shares( (status = 401, description = "Unauthorized"), (status = 404, description = "File not found") ), + security(("bearerAuth" = [])), tag = "playlists" )] pub async fn get_audio_metadata( diff --git a/src/interfaces/api/handlers/photos_handler.rs b/src/interfaces/api/handlers/photos_handler.rs index 83f8dab5..1eb6c8c3 100644 --- a/src/interfaces/api/handlers/photos_handler.rs +++ b/src/interfaces/api/handlers/photos_handler.rs @@ -38,6 +38,7 @@ pub struct PhotosQueryParams { (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error") ), + security(("bearerAuth" = [])), tag = "photos" )] pub async fn list_photos( diff --git a/src/interfaces/api/handlers/recent_handler.rs b/src/interfaces/api/handlers/recent_handler.rs index 08e41e37..fdadfd8c 100644 --- a/src/interfaces/api/handlers/recent_handler.rs +++ b/src/interfaces/api/handlers/recent_handler.rs @@ -26,6 +26,7 @@ pub struct GetRecentParams { responses( (status = 200, description = "List of recent items", body = Vec) ), + security(("bearerAuth" = [])), tag = "recent" )] pub async fn get_recent_items( @@ -65,6 +66,7 @@ pub async fn get_recent_items( (status = 200, description = "Access recorded"), (status = 400, description = "Invalid item type") ), + security(("bearerAuth" = [])), tag = "recent" )] pub async fn record_item_access( @@ -124,6 +126,7 @@ pub async fn record_item_access( (status = 200, description = "Item removed from recents"), (status = 404, description = "Item not in recents") ), + security(("bearerAuth" = [])), tag = "recent" )] pub async fn remove_from_recent( @@ -178,6 +181,7 @@ pub async fn remove_from_recent( responses( (status = 200, description = "Recent items cleared") ), + security(("bearerAuth" = [])), tag = "recent" )] pub async fn clear_recent_items( diff --git a/src/interfaces/api/handlers/search_handler.rs b/src/interfaces/api/handlers/search_handler.rs index f6bd8844..16609a1a 100644 --- a/src/interfaces/api/handlers/search_handler.rs +++ b/src/interfaces/api/handlers/search_handler.rs @@ -299,6 +299,7 @@ pub struct SuggestParams { (status = 200, description = "Search results", body = SearchResultsDto), (status = 503, description = "Search service unavailable"), ), + security(("bearerAuth" = [])), tag = "search" )] pub async fn search_files_get( @@ -317,6 +318,7 @@ pub async fn search_files_get( (status = 200, description = "Search results", body = SearchResultsDto), (status = 503, description = "Search service unavailable"), ), + security(("bearerAuth" = [])), tag = "search" )] pub async fn search_files_post( @@ -339,6 +341,7 @@ pub async fn search_files_post( (status = 200, description = "Suggestions", body = SearchSuggestionsDto), (status = 503, description = "Search service unavailable"), ), + security(("bearerAuth" = [])), tag = "search" )] pub async fn suggest_files( @@ -355,6 +358,7 @@ pub async fn suggest_files( (status = 200, description = "Cache cleared"), (status = 503, description = "Search service unavailable"), ), + security(("bearerAuth" = [])), tag = "search" )] pub async fn clear_search_cache(state: State>) -> impl IntoResponse { diff --git a/src/interfaces/api/handlers/share_handler.rs b/src/interfaces/api/handlers/share_handler.rs index 2cc97852..b19bd9ee 100644 --- a/src/interfaces/api/handlers/share_handler.rs +++ b/src/interfaces/api/handlers/share_handler.rs @@ -63,6 +63,7 @@ pub struct VerifyPasswordRequest { (status = 201, description = "Share created", body = crate::application::dtos::share_dto::ShareDto), (status = 400, description = "Bad request") ), + security(("bearerAuth" = [])), tag = "shares" )] pub async fn create_shared_link( @@ -85,6 +86,7 @@ pub async fn create_shared_link( (status = 200, description = "Share details", body = crate::application::dtos::share_dto::ShareDto), (status = 404, description = "Share not found") ), + security(("bearerAuth" = [])), tag = "shares" )] pub async fn get_shared_link( @@ -110,6 +112,7 @@ pub async fn get_shared_link( responses( (status = 200, description = "List of shares", body = Vec) ), + security(("bearerAuth" = [])), tag = "shares" )] pub async fn get_user_shares( @@ -163,6 +166,7 @@ pub async fn get_user_shares( (status = 200, description = "Share updated", body = crate::application::dtos::share_dto::ShareDto), (status = 404, description = "Share not found") ), + security(("bearerAuth" = [])), tag = "shares" )] pub async fn update_shared_link( @@ -193,6 +197,7 @@ pub async fn update_shared_link( (status = 204, description = "Share deleted"), (status = 404, description = "Share not found") ), + security(("bearerAuth" = [])), tag = "shares" )] pub async fn delete_shared_link( diff --git a/src/interfaces/api/handlers/trash_handler.rs b/src/interfaces/api/handlers/trash_handler.rs index 22d8f2fa..79b2c756 100644 --- a/src/interfaces/api/handlers/trash_handler.rs +++ b/src/interfaces/api/handlers/trash_handler.rs @@ -17,6 +17,7 @@ use std::sync::Arc; (status = 200, description = "List of trashed items"), (status = 501, description = "Trash feature not enabled") ), + security(("bearerAuth" = [])), tag = "trash" )] #[instrument(skip_all)] @@ -71,6 +72,7 @@ pub async fn get_trash_items( (status = 200, description = "File moved to trash"), (status = 501, description = "Trash feature not enabled") ), + security(("bearerAuth" = [])), tag = "trash" )] #[instrument(skip_all)] @@ -132,6 +134,7 @@ pub async fn move_file_to_trash( (status = 200, description = "Folder moved to trash"), (status = 501, description = "Trash feature not enabled") ), + security(("bearerAuth" = [])), tag = "trash" )] #[instrument(skip_all)] @@ -195,6 +198,7 @@ pub async fn move_folder_to_trash( (status = 200, description = "Item restored from trash"), (status = 501, description = "Trash feature not enabled") ), + security(("bearerAuth" = [])), tag = "trash" )] #[instrument(skip_all)] @@ -266,6 +270,7 @@ pub async fn restore_from_trash( (status = 200, description = "Item permanently deleted"), (status = 501, description = "Trash feature not enabled") ), + security(("bearerAuth" = [])), tag = "trash" )] #[instrument(skip_all)] @@ -338,6 +343,7 @@ pub async fn delete_permanently( (status = 200, description = "Trash emptied successfully"), (status = 501, description = "Trash feature not enabled") ), + security(("bearerAuth" = [])), tag = "trash" )] #[instrument(skip_all)] diff --git a/src/interfaces/api/mod.rs b/src/interfaces/api/mod.rs index 11ed606e..31c68bc1 100644 --- a/src/interfaces/api/mod.rs +++ b/src/interfaces/api/mod.rs @@ -7,7 +7,8 @@ pub use routes::create_api_routes; pub use routes::create_health_routes; pub use routes::create_public_api_routes; -use utoipa::OpenApi; +use utoipa::openapi::security::{Http, HttpAuthScheme, SecurityScheme}; +use utoipa::{Modify, OpenApi}; use crate::application::dtos::contact_dto::{ AddressDto, ContactDto, ContactGroupDto, EmailDto, PhoneDto, @@ -40,12 +41,13 @@ use crate::application::dtos::trash_dto::{ DeletePermanentlyRequest, MoveToTrashRequest, RestoreFromTrashRequest, TrashedItemDto, }; use crate::application::dtos::user_dto::{ - AuthResponseDto, ChangePasswordDto, LoginDto, RefreshTokenDto, RegisterDto, SetupAdminDto, - UserDto, + AuthResponseDto, ChangePasswordDto, LoginDto, OidcExchangeDto, OidcProviderInfoDto, + RefreshTokenDto, RegisterDto, SetupAdminDto, UserDto, }; use crate::application::ports::chunked_upload_ports::{ ChunkUploadResponseDto, CreateUploadResponseDto, UploadStatusResponseDto, }; +use crate::interfaces::api::handlers::auth_handler::SystemStatus; use crate::interfaces::api::handlers::chunked_upload_handler::{ CompleteUploadResponse, CreateUploadRequest, }; @@ -60,7 +62,21 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload; #[derive(OpenApi)] #[openapi( + modifiers(&SecurityAddon), paths( + // Auth handlers (public, protected, OIDC) + handlers::auth_handler::register, + handlers::auth_handler::login, + handlers::auth_handler::refresh_token, + handlers::auth_handler::get_current_user, + handlers::auth_handler::change_password, + handlers::auth_handler::logout, + handlers::auth_handler::setup_admin, + handlers::auth_handler::get_system_status, + handlers::auth_handler::oidc_providers, + handlers::auth_handler::oidc_authorize, + handlers::auth_handler::oidc_callback, + handlers::auth_handler::oidc_exchange, // File handlers (free functions — see file_handler.rs for why) handlers::file_handler::list_files_query, handlers::file_handler::upload_file_with_thumbnails, @@ -236,6 +252,9 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload; AuthResponseDto, ChangePasswordDto, RefreshTokenDto, + SystemStatus, + OidcProviderInfoDto, + OidcExchangeDto, // Share schemas ShareDto, SharePermissionsDto, @@ -302,6 +321,7 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload; ) ), tags( + (name = "auth", description = "Authentication and session management endpoints"), (name = "files", description = "File management endpoints"), (name = "folders", description = "Folder management endpoints"), (name = "trash", description = "Trash / recycle bin endpoints"), @@ -328,6 +348,19 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload; )] pub struct ApiDoc; +/// Injects the `bearerAuth` HTTP Bearer security scheme into the generated spec. +struct SecurityAddon; + +impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + let components = openapi.components.get_or_insert_with(Default::default); + components.add_security_scheme( + "bearerAuth", + SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)), + ); + } +} + #[cfg(test)] mod tests { use super::*;