feat(openapi): implement missing routes

Admin:
- /api/admin/drives, /api/admin/drives/{id}, /api/admin/drives/{id}/members, /api/admin/drives/{id}/members/{kind}/{sid}
- /api/admin/jobs/{name}/pause, /api/admin/jobs/{name}/runs/{id}/findings, /api/admin/jobs/runs/purge
- /api/admin/smtp/info, /api/admin/smtp/test
- /api/admin/storage/entries/{name}/rotate
- /api/admin/users/{id}/promote-to-internal

Auth:
- /api/auth/dpop/bind
- /api/auth/magic-link/send
- /api/auth/me/profile
- /api/auth/upgrade-to-internal

Drives / grants / trash / users / dedup:
- /api/drives/{id}, /api/drives/{id}/members (get + delete), /api/drives/{id}/policies, /api/drives/{id}/quota
- /api/grants/{id}/notify
- /api/trash/drive/{drive_id}
- /api/users/{id}
- /api/dedup/check-batch

Faces
- /api/people — cluster list (PersonDto[])
- /api/people/{id}/photos — file ids for one person
- /api/people/{id} — rename (or clear name)
- /api/people/merge — merge two clusters
- /api/people/recluster — re-run clustering
- /api/people/data — nuke all face data
- /api/people/faces/{file_id} — face boxes per photo (FaceBoxDto[])
This commit is contained in:
Edouard Vanbelle
2026-08-09 17:42:32 +02:00
parent 4739506698
commit e0654cd848
5 changed files with 157 additions and 5 deletions
+4 -2
View File
@@ -1718,7 +1718,9 @@ async fn reextract_image_metadata(
security(("bearerAuth" = [])),
tag = "admin"
)]
async fn get_smtp_info(State(state): State<Arc<AppState>>) -> Result<impl IntoResponse, AppError> {
pub async fn get_smtp_info(
State(state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, AppError> {
let smtp = &state.core.config.smtp;
let info = SmtpInfoDto {
enabled: smtp.is_enabled() && state.email_sender.is_some(),
@@ -1802,7 +1804,7 @@ struct CapturedEmailQuery {
security(("bearerAuth" = [])),
tag = "admin"
)]
async fn send_smtp_test(
pub async fn send_smtp_test(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
Json(dto): Json<SendSmtpTestDto>,
+82 -2
View File
@@ -15,9 +15,11 @@ use axum::{
use serde::Deserialize;
use uuid::Uuid;
use crate::application::dtos::people_dto::{FaceBoxDto, PersonDto};
use crate::common::di::AppState;
use crate::interfaces::errors::AppError;
use crate::interfaces::middleware::auth::AuthUser;
use utoipa::ToSchema;
fn disabled() -> Response {
(
@@ -36,6 +38,15 @@ fn bad_id() -> Response {
}
/// GET /api/people — identity clusters for the caller.
#[utoipa::path(
get,
path = "/api/people",
responses(
(status = 200, description = "Identity clusters", body = [PersonDto]),
(status = 404, description = "People feature disabled"),
),
tag = "people"
)]
pub async fn list_people(State(state): State<Arc<AppState>>, auth_user: AuthUser) -> Response {
let Some(svc) = state.people_service.as_ref() else {
return disabled();
@@ -47,6 +58,17 @@ pub async fn list_people(State(state): State<Arc<AppState>>, auth_user: AuthUser
}
/// GET /api/people/{id}/photos — file ids of a person's photos.
#[utoipa::path(
get,
path = "/api/people/{id}/photos",
params(("id" = String, Path, description = "Person cluster id")),
responses(
(status = 200, description = "File ids where this person appears", body = [String]),
(status = 400, description = "Malformed person id"),
(status = 404, description = "People feature disabled"),
),
tag = "people"
)]
pub async fn person_photos(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
@@ -64,12 +86,26 @@ pub async fn person_photos(
}
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub struct RenameBody {
/// New display name for the cluster. `null` or omitted clears
/// the name, reverting the cluster to its "Unnamed" state.
pub name: Option<String>,
}
/// PATCH /api/people/{id} — name (or clear the name of) a person.
#[utoipa::path(
patch,
path = "/api/people/{id}",
params(("id" = String, Path, description = "Person cluster id")),
request_body = RenameBody,
responses(
(status = 204, description = "Renamed"),
(status = 400, description = "Malformed person id"),
(status = 404, description = "People feature disabled"),
),
tag = "people"
)]
pub async fn rename_person(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
@@ -88,13 +124,28 @@ pub async fn rename_person(
}
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub struct MergeBody {
/// Cluster id that will absorb the other one (`from`'s photos are
/// reattributed to `into`; `from` is deleted). Typically the
/// larger / named cluster wins.
pub into: String,
/// Cluster id being merged into `into`. Deleted after the merge.
pub from: String,
}
/// POST /api/people/merge — merge `from` into `into`.
#[utoipa::path(
post,
path = "/api/people/merge",
request_body = MergeBody,
responses(
(status = 204, description = "Merged"),
(status = 400, description = "Malformed cluster id(s)"),
(status = 404, description = "People feature disabled"),
),
tag = "people"
)]
pub async fn merge_people(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
@@ -113,6 +164,15 @@ pub async fn merge_people(
}
/// POST /api/people/recluster — re-run identity clustering for the caller.
#[utoipa::path(
post,
path = "/api/people/recluster",
responses(
(status = 200, description = "Recluster complete", body = serde_json::Value),
(status = 404, description = "People feature disabled"),
),
tag = "people"
)]
pub async fn recluster(State(state): State<Arc<AppState>>, auth_user: AuthUser) -> Response {
let Some(svc) = state.people_service.as_ref() else {
return disabled();
@@ -124,6 +184,15 @@ pub async fn recluster(State(state): State<Arc<AppState>>, auth_user: AuthUser)
}
/// DELETE /api/people/data — erase all of the caller's face data.
#[utoipa::path(
delete,
path = "/api/people/data",
responses(
(status = 204, description = "All face data erased"),
(status = 404, description = "People feature disabled"),
),
tag = "people"
)]
pub async fn delete_all(State(state): State<Arc<AppState>>, auth_user: AuthUser) -> Response {
let Some(svc) = state.people_service.as_ref() else {
return disabled();
@@ -135,6 +204,17 @@ pub async fn delete_all(State(state): State<Arc<AppState>>, auth_user: AuthUser)
}
/// GET /api/people/faces/{file_id} — face boxes within a photo (lightbox tags).
#[utoipa::path(
get,
path = "/api/people/faces/{file_id}",
params(("file_id" = String, Path, description = "Photo file id")),
responses(
(status = 200, description = "Face bounding boxes in the photo", body = [FaceBoxDto]),
(status = 400, description = "Malformed file id"),
(status = 404, description = "People feature disabled"),
),
tag = "people"
)]
pub async fn faces_for_file(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
+1 -1
View File
@@ -46,7 +46,7 @@ pub fn user_routes() -> Router<Arc<AppState>> {
security(("bearerAuth" = [])),
tag = "users",
)]
async fn get_user_profile(
pub async fn get_user_profile(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
Path(target_id): Path<Uuid>,
+62
View File
@@ -82,6 +82,15 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
handlers::auth_handler::oidc_backchannel_logout,
handlers::auth_handler::oidc_link_start,
handlers::auth_handler::oidc_unlink,
// DPoP post-redirect bind (Gate 3), magic-link SEND (the
// outbound half of the passwordless flow — /magic/v1/{token}
// redemption is a browser redirect, not an API endpoint),
// profile edit (PATCH — the read is via /me), and the
// external-user upgrade path.
handlers::auth_handler::dpop_bind,
handlers::auth_handler::send_magic_link,
handlers::auth_handler::update_profile,
handlers::auth_handler::upgrade_to_internal,
// File handlers (free functions — see file_handler.rs for why)
handlers::file_handler::list_files_query,
handlers::file_handler::upload_file_with_thumbnails,
@@ -125,6 +134,7 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
handlers::chunked_upload_handler::cancel_upload,
// Dedup handlers — all free functions for the same utoipa reason as chunked uploads.
handlers::dedup_handler::check_hash,
handlers::dedup_handler::check_hashes_batch,
handlers::dedup_handler::get_stats,
handlers::dedup_handler::get_blob,
handlers::dedup_handler::recalculate_stats,
@@ -135,6 +145,7 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
handlers::trash_handler::restore_from_trash,
handlers::trash_handler::delete_permanently,
handlers::trash_handler::empty_trash,
handlers::trash_handler::empty_trash_for_drive,
// Share handlers (free functions)
handlers::share_handler::create_shared_link,
handlers::share_handler::get_shared_link,
@@ -162,8 +173,29 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
// Photos handler (free function)
handlers::photos_handler::list_photos,
handlers::photos_handler::list_photos_geo,
// People / face-clustering handlers — mounted only when
// `OXICLOUD_ENABLE_FACES` is on; each handler is defensive
// (`disabled()` returns 404 otherwise). All work is strictly
// caller-scoped by `PeopleService`.
handlers::people_handler::list_people,
handlers::people_handler::person_photos,
handlers::people_handler::rename_person,
handlers::people_handler::merge_people,
handlers::people_handler::recluster,
handlers::people_handler::delete_all,
handlers::people_handler::faces_for_file,
// Drive handler (free function)
handlers::drive_handler::list_drives,
handlers::drive_handler::delete_drive,
handlers::drive_handler::list_drive_members,
handlers::drive_handler::add_drive_member,
handlers::drive_handler::update_drive_member,
handlers::drive_handler::remove_drive_member,
handlers::drive_handler::update_drive_policies,
handlers::drive_handler::update_drive_quota,
// Users handler — public profile lookup (auth-required, but
// returns the callee's public view, not `/me`'s self-view).
handlers::users_handler::get_user_profile,
// Batch handlers (free functions)
handlers::batch_handler::move_files_batch,
handlers::batch_handler::copy_files_batch,
@@ -240,6 +272,27 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
handlers::admin_handler::cancel_job,
handlers::admin_handler::list_job_runs,
handlers::admin_handler::get_job_run,
handlers::admin_handler::pause_job,
handlers::admin_handler::list_job_run_findings,
handlers::admin_handler::purge_job_runs,
// Admin drive management — full CRUD on drives + membership,
// distinct from the user-facing /api/drives surface (admin can
// touch any drive; user can touch only those they're an owner
// of).
handlers::admin_handler::list_all_drives,
handlers::admin_handler::delete_drive_admin,
handlers::admin_handler::list_drive_members_admin,
handlers::admin_handler::add_drive_member_admin,
handlers::admin_handler::update_drive_member_admin,
handlers::admin_handler::remove_drive_member_admin,
// Admin SMTP diagnostics + backend rotation + user promotion.
// The two SMTP fns were pub-only-for-utoipa (private in-router
// helpers before this branch); see the handler for the
// read-only vs test-send semantics.
handlers::admin_handler::get_smtp_info,
handlers::admin_handler::send_smtp_test,
handlers::admin_handler::trigger_backend_rotate,
handlers::admin_handler::admin_promote_external_to_internal,
// Admin sessions panel — list + revoke. Function names lack
// the `_admin_` suffix; the `/api/admin/` prefix comes from
// the router mount, not the handler name.
@@ -266,6 +319,7 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
handlers::grant_handler::list_outgoing,
handlers::grant_handler::list_my_shares,
handlers::grant_handler::list_on_resource,
handlers::grant_handler::notify_grant_recipient,
// Subject-group handlers (ReBAC named groups) — free functions
handlers::subject_group_handler::create_group,
handlers::subject_group_handler::list_groups,
@@ -342,6 +396,14 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
handlers::opaque_auth_handler::OpaqueLoginKe1Response,
handlers::opaque_auth_handler::OpaqueLoginKe3Dto,
handlers::opaque_auth_handler::OpaqueParamsResponse,
// People / face-clustering — response DTOs published by the
// /api/people/* endpoints (list_people, faces_for_file), plus
// the two request bodies (rename, merge). Face indexing pipeline
// is documented in `face_indexing_service` + `onnx_face_analyzer`.
crate::application::dtos::people_dto::PersonDto,
crate::application::dtos::people_dto::FaceBoxDto,
handlers::people_handler::RenameBody,
handlers::people_handler::MergeBody,
// Share schemas
ShareDto,
CreateShareDto,