The per-user quota widget was shown to everyone and the /me/quota payload returned free_disk_bytes (raw server free space) and active_uploaders to any authenticated guest. Gate the widget to staff (host/admin) on the upload and account pages, and zero the server-wide telemetry fields for non-staff in the handler. Guests still get their own used/limit so enforcement stays transparent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
115 lines
3.9 KiB
Rust
115 lines
3.9 KiB
Rust
//! 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<i64>,
|
|
pub active_uploaders: i64,
|
|
pub free_disk_bytes: i64,
|
|
}
|
|
|
|
pub async fn get_quota(
|
|
State(state): State<AppState>,
|
|
auth: AuthUser,
|
|
) -> Result<Json<QuotaDto>, 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,
|
|
}
|
|
|
|
pub async fn get_context(
|
|
State(state): State<AppState>,
|
|
auth: AuthUser,
|
|
) -> Result<Json<MeContextDto>, 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,
|
|
}))
|
|
}
|