//! Endpoints scoped to the *current user*. Kept separate from `auth::handlers` because //! these aren't about acquiring / refreshing a session — they're about reading my own //! state once I'm already signed in. //! //! Current routes: //! - `GET /api/v1/me/context` — bundled profile + feature flags + privacy note. The //! account page loads this once on mount instead of issuing several round trips. //! - `GET /api/v1/me/quota` — live per-user storage quota estimate. use axum::Json; use axum::extract::State; use serde::Serialize; use crate::auth::middleware::AuthUser; use crate::error::AppError; use crate::handlers::upload::compute_storage_quota; use crate::models::user::{User, UserRole}; use crate::services::config; use crate::state::AppState; #[derive(Serialize)] pub struct QuotaDto { pub enabled: bool, pub used_bytes: i64, pub limit_bytes: Option, pub active_uploaders: i64, pub free_disk_bytes: i64, } pub async fn get_quota( State(state): State, auth: AuthUser, ) -> Result, AppError> { let user = User::find_by_id(&state.pool, auth.user_id) .await? .ok_or_else(|| AppError::NotFound("Benutzer nicht gefunden.".into()))?; let estimate = compute_storage_quota(&state).await; // Raw server telemetry (free disk, concurrent uploader count) is staff-only — it // must never reach a guest, even though the guest upload UI no longer renders it. // A guest still gets their own `used`/`limit` so enforcement stays transparent to // the code paths that consume it; only the server-wide fields are zeroed. let is_staff = matches!(auth.role, UserRole::Host | UserRole::Admin); Ok(Json(QuotaDto { enabled: estimate.limit_bytes.is_some(), used_bytes: user.total_upload_bytes, limit_bytes: estimate.limit_bytes, active_uploaders: if is_staff { estimate.active_uploaders } else { 0 }, free_disk_bytes: if is_staff { estimate.free_disk_bytes } else { 0 }, })) } #[derive(Serialize)] pub struct MeContextDto { pub user_id: uuid::Uuid, pub display_name: String, pub role: String, /// Plain-text Datenschutzhinweis set by the admin. Empty string when not configured. pub privacy_note: String, pub quota_enabled: bool, pub storage_quota_enabled: bool, /// Uploads are locked (event closed) — the composer should show a locked state live /// instead of letting a guest compose an upload only to eat a 403. pub uploads_locked: bool, /// The gallery has been released and the export snapshotted — uploads are permanently /// closed for this run (release ⇒ lock, and reopening regenerates). pub gallery_released: bool, /// This guest is banned: a deliberately READ-ONLY ban (see `handlers/host.rs`) — they keep /// the feed and the keepsake, but every write is refused. /// /// Exposed so the UI can SAY so. Without it the client had no idea, so the upload button, /// the like button and "Löschen" all rendered enabled and returned 403 "Du bist gesperrt." /// on every tap — a guest tapping upload repeatedly with nobody to ask. The lock case /// (`uploads_locked`) has always been surfaced for exactly this reason; a ban was not. pub is_banned: bool, } pub async fn get_context( State(state): State, auth: AuthUser, ) -> Result, AppError> { let user = User::find_by_id(&state.pool, auth.user_id) .await? .ok_or_else(|| AppError::NotFound("Benutzer nicht gefunden.".into()))?; let privacy_note = config::get_str(&state.config_cache, "privacy_note", "").await; let quota_enabled = config::get_bool(&state.config_cache, "quota_enabled", true).await; let storage_quota_enabled = config::get_bool(&state.config_cache, "storage_quota_enabled", true).await; let event = crate::models::event::Event::find_by_slug(&state.pool, &state.config.event_slug).await?; let uploads_locked = event .as_ref() .map(|e| e.uploads_locked_at.is_some()) .unwrap_or(false); let gallery_released = event .as_ref() .map(|e| e.export_released_at.is_some()) .unwrap_or(false); Ok(Json(MeContextDto { user_id: user.id, display_name: user.display_name, role: user.role.as_str().to_string(), privacy_note, quota_enabled, storage_quota_enabled, uploads_locked, gallery_released, is_banned: user.is_banned, })) }