feat(admin): per-user + per-enclave storage quotas (LC-93) #153
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-93-storage-quotas"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements LC-93. Admins can cap storage per user and per enclave; uploads or message-attachments that would push a user or an enclave over its cap return 413.
0031_storage_quotas.sql:enclaves.quota_bytes(nullable = unlimited) anduser_storage_quotas(absence of row = unlimited).db::quota: get/set helpers for both, plus live SUMs (sum_user_usage,sum_enclave_usage). Usage excludesmessages.is_system = 1rows per the spec; orphan uploads count toward the uploader but not toward any enclave.routes/uploads.rs: check after streaming, before the image re-encode, so a rejected upload does not burn CPU on the pipeline.routes/room.rs: enclave quota is checked when an attachment is being attached to a message. DMs (enclave_id IS NULL) skip. NewAppError::PayloadTooLargereturns 413./admin/usersand/admin/enclavesgrow a Storage column with the current usage + aquota_mibinput.POST /admin/users/{id}/quota(HTMX row replace) andPOST /admin/enclaves/{id}/quota(redirect). Both audit-log viamod_actions(quota_set_user/quota_set_enclave)./settingsgains a Storage usage card with usage + cap (or "no quota set").Open questions from the issue, resolved before coding:
messages.is_systemjoin filter).Test plan
just check(both feature builds, clippy -D warnings, fmt --check).just test: newroutes_quotas(10 cases) plus existing 49 binaries green. Hand-rolled-migration test files (7) all updated to include the new migration;common::poolauto-picks it viasqlx::migrate!.just test-saas: green; the three admin-route tests are#[cfg(feature = "standalone")]per the CLAUDE.md test-drift guide. Pre-existing flakededup_upload_heals_missing_preview_on_disk(CLAUDE.md "out of scope") passes in isolation.Quotas live in chat.db so the upload-time check stays single-domain. `user_storage_quotas` holds per-user caps (absence = unlimited); the new `enclaves.quota_bytes` column does the same for enclaves. Usage is a live SUM over `file_uploads` rather than a cached counter: SQLite handles the size on a self-hosted deployment fine, and live counts auto-recompute on delete with no triggers or sweep coordination. System messages are excluded from both the user and enclave usage figures per the LC-93 acceptance criterion. Enforcement: - `routes/uploads.rs::post_upload` checks the per-user quota right after streaming to the temp file but before the (expensive) image re-encode, so a rejected upload does not burn CPU. - `routes/room.rs::post_message` checks the per-enclave quota after validating attachment ownership; DMs (`enclave_id IS NULL`) skip it. New `AppError::PayloadTooLarge` returns 413 from the existing `IntoResponse` impl. Admin UI: - `POST /admin/users/{id}/quota` and `POST /admin/enclaves/{id}/quota` accept a `quota_mib` field (empty = clear the cap). The user form re-renders the row HTMX-style; the enclave form redirects back to the page. Both write a `quota_set_user` / `quota_set_enclave` row to `mod_actions`. - `/admin/users` and `/admin/enclaves` grow a Storage column showing current usage alongside the form input. User-facing: - `/settings` shows a "Storage usage" card with the current usage and either the user's quota or an "no quota set" note.