- handlers/me.rs (new): GET /api/v1/me/context (profile + role + privacy_note
+ quota toggle state, fetched once on app bootstrap) and GET /api/v1/me/quota
(live used / limit / active uploaders / free disk).
- handlers/upload.rs:
- quota enforcement via the dynamic formula
floor((free_disk * tolerance) / max(active_uploaders, 1)),
gated by quota_enabled + storage_quota_enabled toggles
- new GET /api/v1/upload/{id}/original — unauthed by design
(matches /media/previews/* — URL is the secret) so it works as
<img src> / <video src> / window.open
- rate-limit toggle wiring (rate_limits_enabled + upload_rate_enabled)
- handlers/host.rs:
- POST /api/v1/host/users/{id}/pin-reset — Host may reset guest PINs,
Admin may reset guest + host PINs (never another admin or self).
Returns the freshly-generated plaintext PIN once; emits a global
pin-reset SSE so the affected user's device can clear its localStorage.
- set_role guard expanded so hosts can demote other hosts (not self,
never admins) — backend match for the doc'd permission model.
- handlers/admin.rs: ALLOWED_KEYS split into NUMERIC_KEYS / BOOL_KEYS /
TEXT_KEYS with per-kind validation; saving privacy_note broadcasts an
event-updated SSE so other clients refresh live.
- handlers/feed.rs, handlers/admin.rs (export), auth/handlers.rs:
rate-limit toggle wiring at every limiter call site.
- auth/handlers.rs: when an expired PIN lockout is detected on /recover,
reset failed_pin_attempts to zero before the bcrypt check — without
this every wrong PIN re-locked the user after the cooldown.
- main.rs: wire startup_recovery + spawn_periodic_tasks, register the
new /me/context, /me/quota, /upload/{id}/original, and
/host/users/{id}/pin-reset routes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
81 lines
2.6 KiB
Rust
81 lines
2.6 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::extract::State;
|
|
use axum::Json;
|
|
use serde::Serialize;
|
|
|
|
use crate::auth::middleware::AuthUser;
|
|
use crate::error::AppError;
|
|
use crate::handlers::upload::compute_storage_quota;
|
|
use crate::models::user::User;
|
|
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;
|
|
|
|
Ok(Json(QuotaDto {
|
|
enabled: estimate.limit_bytes.is_some(),
|
|
used_bytes: user.total_upload_bytes,
|
|
limit_bytes: estimate.limit_bytes,
|
|
active_uploaders: estimate.active_uploaders,
|
|
free_disk_bytes: estimate.free_disk_bytes,
|
|
}))
|
|
}
|
|
|
|
#[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,
|
|
}
|
|
|
|
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.pool, "privacy_note", "").await;
|
|
let quota_enabled = config::get_bool(&state.pool, "quota_enabled", true).await;
|
|
let storage_quota_enabled = config::get_bool(&state.pool, "storage_quota_enabled", true).await;
|
|
|
|
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,
|
|
}))
|
|
}
|