From cbd642c62cafc76ddbf52f5eeb816568f9641507 Mon Sep 17 00:00:00 2001 From: Priec Date: Tue, 19 May 2026 14:48:47 +0200 Subject: [PATCH] design is now terminal alike --- REWRITE_SPEC.md | 399 --------------------------------- assets/static/css/theme.css | 313 ++++++++++++++++++++++++-- assets/views/admin/base.html | 86 ++++--- assets/views/admin/index.html | 146 +++++++----- assets/views/admin/login.html | 27 ++- assets/views/audio/album.html | 103 ++++++--- assets/views/audio/albums.html | 78 ++++--- assets/views/audio/tracks.html | 58 +++-- assets/views/base.html | 90 +++++--- assets/views/blog/index.html | 85 +++---- assets/views/blog/show.html | 42 ++-- assets/views/home/index.html | 91 ++++---- assets/views/pages/about.html | 38 ++-- 13 files changed, 801 insertions(+), 755 deletions(-) delete mode 100644 REWRITE_SPEC.md diff --git a/REWRITE_SPEC.md b/REWRITE_SPEC.md deleted file mode 100644 index 5a4148d..0000000 --- a/REWRITE_SPEC.md +++ /dev/null @@ -1,399 +0,0 @@ -# Universal Web — Loco.rs Rewrite Specification - -This document is the implementation spec for re-building the existing **Axum** app -(parent directory `../`) on top of **Loco.rs**. It captures every feature so the -rewrite reaches parity. The GUI is being rewritten as part of this effort, so -templates/markup are *not* meant to be ported verbatim — only behaviour and routes. - -- **Source app:** `../` — Axum 0.8, sqlx, Askama, axum-login, OpenDAL. -- **Target app:** this directory — Loco 0.16 (SaaS, server-side rendered), SeaORM 1.1, Tera. -- **Scaffold already generated by `loco new`:** auth controller, `users` model + migration, - auth mailer, background worker example, Tera view engine, i18n. - ---- - -## 1. Stack mapping (old → Loco) - -| Concern | Old (Axum) | Loco target | -|--------------------|---------------------------------------------|----------------------------------------------------------| -| HTTP framework | Axum 0.8 + tower | Loco (Axum under the hood) — `controllers/` | -| DB access | sqlx 0.8 (raw SQL, `query_as`) | SeaORM 1.1 — entities in `models/_entities/` | -| Migrations | `migrations/*.sql` (sqlx-cli) | `migration/` crate (SeaORM, Rust DSL) | -| Templating | Askama (compile-time) | Tera (`assets/views/`) via `view_engine` initializer | -| Auth/session | axum-login + tower-sessions (PG store) | **Decision needed — see §3.1.** Loco ships JWT auth. | -| Password hashing | argon2 0.5 | Loco built-in (`AuthnUser`/`hash_password`) — argon2 | -| File storage | OpenDAL 0.50 (fs/S3/Azure/GCS) | Loco `storage` module (local/S3/multi-mirror) | -| Background jobs | none (manual) | Loco workers (`workers/`) — bg mode `async` selected | -| Email | none (verification stored, not sent) | Loco mailers (`mailers/`) — scaffold already present | -| API docs | utoipa + Swagger UI + Scalar | Manual — see §13 | -| Validation | garde | `validator` crate (already a dep) | -| Config | `.env` + `config.rs` singleton | `config/{development,production,test}.yaml` | -| Logging | tracing-subscriber | Loco logger (configured in yaml) | - ---- - -## 2. Feature inventory (parity checklist) - -The old app ships these feature modules. Each must exist in the rewrite: - -- [ ] **Auth** — register, login, logout, current-user, email verification -- [ ] **RBAC** — 4 roles, ~14 permissions, per-request permission loading -- [ ] **Admin** — dashboard, user management, role assignment, audit log -- [ ] **Blog** — articles CRUD, publish workflow, public listing, view counts -- [ ] **Audio dashboard** — albums + tracks + tags CRUD, publish workflow -- [x] **Audio streaming** — range-aware track/file streaming, raw upload -- [ ] **Audio player** — persistent bottom-bar player (frontend) -- [x] **Images** — upload + serve, used as cover/featured images -- [ ] **Theme** — per-user light/dark preference -- [x] **Storage** — pluggable backend (fs default, S3/Azure/GCS capable) -- [ ] **Home + layout** — landing page, dynamic navbar, footer -- [ ] **Swagger/OpenAPI** — API docs (optional, lower priority) - ---- - -## 3. Key migration decisions / gotchas - -### 3.1 Sessions vs JWT - -The old app uses **cookie sessions** (axum-login + `tower-sessions-sqlx-store`, -1-day inactivity timeout, PG-backed `session` table). Loco's SaaS starter ships -**JWT bearer auth** by default. - -Because this is a **server-side rendered** app (forms post HTML, navbar reflects -auth state), bearer tokens are awkward. Recommended path: - -- Keep Loco's `users` model + password hashing + the `auth` controller for the - *API-style* endpoints. -- Add **cookie-session middleware** for the HTML pages. Loco exposes the Axum - router, so `tower-sessions` can be layered in an initializer - (`src/initializers/`). Store the user id in the session. -- Alternatively store the JWT in an `HttpOnly` cookie and add an extractor that - reads it from the cookie instead of the `Authorization` header. - -Pick one and apply it consistently. The old behaviour to match: login sets a -cookie, navbar/`/auth/me` reflect it, logout clears it, ~1-day expiry. - -### 3.2 sqlx → SeaORM - -Every raw query becomes a SeaORM entity + `ActiveModel`. Generate entities from -migrations with `cargo loco db entities`. Slug-uniqueness and `ON CONFLICT DO -NOTHING` semantics must be re-expressed (unique index + handle the error). - -### 3.3 Askama → Tera - -Templates are NOT ported. Re-author markup as Tera under `assets/views/`. The new -GUI may keep HTMX + Alpine + DaisyUI (still works with server-rendered Tera) or go -a different direction — that is a GUI decision, not a backend one. - -### 3.4 OpenDAL → Loco storage - -Loco has its own `storage` abstraction (local, S3, multi-backend mirror/replica). -Map the old `Storage`/`ImageStorage`/`AudioStorage` wrappers onto it. Default to -local (`uploads/`); keep S3/Azure/GCS reachable via config only. - -### 3.5 Range requests for audio - -The old `stream_track` forwards the request so `ServeFile` honours `Range` -headers (seeking/progressive playback). The Loco equivalent must also emit -`Accept-Ranges`/`Content-Range` — use `tower-http` `ServeFile`/`ServeDir` or a -range-aware handler. Do not regress this; it is required for the player's seek bar. - ---- - -## 4. Database schema - -Old app: 8 migrations under `../migrations/`. Re-create as SeaORM migrations in -`migration/src/` (the `loco new` scaffold already created `m20220101_000001_users`). -Prefer Loco generators: `cargo loco generate model ...`. - -### 4.1 `users` -| column | type | notes | -|-------------------|--------------|----------------------------------------| -| id | UUID PK | `gen_random_uuid()` | -| email | VARCHAR(255) | UNIQUE NOT NULL | -| password_hash | TEXT | NOT NULL (argon2) | -| email_verified | BOOLEAN | default FALSE | -| email_verified_at | TIMESTAMPTZ | nullable | -| theme | VARCHAR(10) | default `'light'` | -| created_at | TIMESTAMPTZ | default NOW | -| updated_at | TIMESTAMPTZ | default NOW | - -> Loco's scaffold `users` table differs (has `pid`, `api_key`, reset/verification -> token columns). Reconcile: either adopt Loco's columns and add `theme`, or align -> Loco's model to this schema. Adopting Loco's is less work — it already wires -> tokens for verification/reset. - -### 4.2 `email_verifications` -`id UUID PK`, `user_id UUID FK→users ON DELETE CASCADE`, `token VARCHAR(255) UNIQUE`, -`expires_at TIMESTAMPTZ`, `created_at TIMESTAMPTZ`. Indexes on `token`, `user_id`. -> Loco's `users` table already carries verification tokens — this table may become -> redundant if you adopt Loco's auth columns. - -### 4.3 `user_roles` -`user_id UUID FK→users CASCADE`, `role VARCHAR(50)` (`user`|`moderator`|`admin`| -`super_admin`), `assigned_by UUID FK→users ON DELETE SET NULL`, `assigned_at -TIMESTAMPTZ`. **PK = (user_id, role).** Index on `user_id`. Migration back-fills -`user` role for every existing user. - -### 4.4 `blog_articles` -`id UUID PK`, `title VARCHAR(500)`, `slug VARCHAR(500) UNIQUE`, `content TEXT`, -`excerpt VARCHAR(1000) NULL`, `published BOOL default false`, `author_id UUID -FK→users CASCADE`, `featured_image_id VARCHAR(500) NULL`, `view_count INT default 0`, -`created_at`, `updated_at` (trigger-updated), `published_at TIMESTAMPTZ NULL`. -Indexes: `slug`, `(published, published_at DESC)`, `author_id`. - -### 4.5 `audit_logs` -`id UUID PK`, `admin_user_id UUID FK→users CASCADE`, `action VARCHAR(100)`, -`target_type VARCHAR(50) NULL`, `target_id UUID NULL`, `details JSONB NULL`, -`ip_address INET NULL`, `user_agent TEXT NULL`, `created_at TIMESTAMPTZ`. -Indexes: `admin_user_id`, `action`, `(target_type,target_id)`, `created_at DESC`. - -### 4.6 `audio_albums` -`id UUID PK`, `title VARCHAR(500)`, `slug VARCHAR(500) UNIQUE`, `description TEXT -NULL`, `cover_image_id VARCHAR(500) NULL`, `artist VARCHAR(500) NULL`, `release_date -DATE NULL`, `published BOOL default false`, `uploader_id UUID FK→users CASCADE`, -`view_count INT default 0`, `created_at`, `updated_at` (trigger), `published_at NULL`. -Indexes: `slug`, `published`, `uploader_id`. - -### 4.7 `audio_tracks` -`id UUID PK`, `album_id UUID FK→audio_albums CASCADE`, `title VARCHAR(500)`, `slug -VARCHAR(500)`, `audio_file_id VARCHAR(500) NOT NULL`, `track_number INT NULL`, -`duration INT NULL` (seconds), `featured BOOL default false`, `play_count INT -default 0`, `created_at`, `updated_at` (trigger). **UNIQUE(album_id, slug).** - -### 4.8 `audio_tags` + `audio_track_tags` -`audio_tags`: `id UUID PK`, `name VARCHAR(100) UNIQUE`, `slug VARCHAR(100) UNIQUE`, -`created_at`. `audio_track_tags` (M:N junction): `track_id UUID FK→audio_tracks -CASCADE`, `tag_id UUID FK→audio_tags CASCADE`, `created_at`, **PK=(track_id,tag_id)**. - -> The old schema uses `updated_at` triggers. SeaORM convention is to set -> `updated_at` in the `ActiveModel` (`before_save` hook) instead — either works. - ---- - -## 5. Routes (parity table) - -`method path → behaviour (auth requirement)`. Loco controllers live in -`src/controllers/`; register each with `.add_route(...)` in `app.rs`. - -### Auth (`/auth`) -- `GET /auth/login` → login page (public) -- `POST /auth/login` → login, HTML/HTMX response; sets session cookie -- `POST /auth/login/json` → login, JSON response -- `GET /auth/register` → register page (public) -- `POST /auth/register` → register, HTML/HTMX -- `POST /auth/register/json` → register, JSON -- `POST /auth/logout` → clear session -- `GET /auth/me` → current user (auth required) -- `GET /auth/verify?token=` → verify email -- `POST /auth/verify/resend` → resend verification (auth required) - -### RBAC (`/rbac`) -- `GET /rbac/permissions` → list all permissions (public) -- `GET /rbac/roles` → list all roles (public) -- `GET /rbac/me` → current user roles+permissions (auth) -- `GET /rbac/users/{id}/roles` → (auth) -- `GET /rbac/users/{id}/permissions` → (auth) -- `POST /rbac/users/{id}/roles` → assign role (SuperAdmin) -- `DELETE /rbac/users/{id}/roles/{role}` → remove role (SuperAdmin) - -### Blog -- `GET /blog` → published articles (public) -- `GET /blog/{slug}` → article, increments `view_count` (public) -- `GET /admin/blog` & `/admin/blog/articles` → admin list (Moderator+) -- `GET|POST /admin/blog/articles/create` → create form / submit (Moderator+) -- `GET|POST /admin/blog/articles/{id}/edit` → edit form / submit (Moderator+) -- `GET /admin/blog/articles/{id}/delete` → delete (Moderator+) - -### Admin (`/admin`) -- `GET /admin` → redirect to `/admin/dashboard` -- `GET /admin/dashboard` → dashboard + system stats (Moderator+) -- `GET /admin/users` & `/admin/users/list` → user list, filter+paginate (Admin+) -- `GET /admin/users/{id}` → user detail (Admin+) -- `GET /admin/users/{id}/roles` → roles form (Admin+) -- `POST /admin/users/{id}/roles/assign` → assign (Admin+) -- `POST /admin/users/{id}/roles/{role}/remove` → remove (Admin+) -- `GET /admin/audit-logs` → audit log list (Moderator+) - -### Audio (public + admin) -- `POST /audio/upload` → raw audio upload (auth) -- `GET /audio/stream/{filename}` → stream raw file, **range-aware** (public) -- `GET /audio/albums` → published albums (public) -- `GET /audio/albums/{slug}` → album + tracks (public) -- `GET /audio/tracks/{id}/stream` → stream track, **range-aware** (public) -- `GET /admin/audio/albums` → admin album list (Moderator+) -- `GET|POST /admin/audio/albums/create` → (Moderator+) -- `GET|POST /admin/audio/albums/{id}/edit` → (Moderator+) -- `GET /admin/audio/albums/{id}/delete` → (Moderator+) -- `GET /admin/audio/albums/{id}/tracks` → track list (Moderator+) -- `GET /admin/audio/albums/{id}/tracks/upload` → upload form (Moderator+) -- `POST /admin/audio/albums/{id}/tracks/upload-file` → multipart upload, 50 MB (Moderator+) -- `GET|POST /admin/audio/tracks/{id}/edit` → (Moderator+) -- `GET /admin/audio/tracks/{id}/delete` → (Moderator+) -- `GET /admin/audio/tags` → tag list (Moderator+) -- `POST /admin/audio/tags` → create tag (Moderator+) - -### Images -- `POST /images/upload` → upload, validate ext+MIME, max 10 MB (auth) -- `GET /images/serve/{filename}` → serve with MIME + cache headers (public) - -### Misc -- `GET /` → home (public) -- `GET /layout/navbar` → navbar fragment reflecting auth state (public) -- `GET /theme` → current theme; `GET /theme/set?...` → set theme (public) -- `GET /static/*` → static files -- `GET /swagger-ui/`, `GET /api-docs/openapi.json` → API docs - ---- - -## 6. Feature details - -### 6.1 Auth -- Register: validate email + password (8–64 chars), reject duplicate email, - argon2-hash password, insert user, assign default `user` role. -- Login: look up by email, verify password (constant-time), create session. -- **Admin bootstrap:** on successful login, if `user.email` (case-insensitive) - equals config `admin_email`, assign `SuperAdmin` role (idempotent) and write an - `audit_logs` row with `action="admin_bootstrap"`. This is how the first admin is - created — there is no seed user. Keep this behaviour. -- Email verification: token table + `email_verified`/`email_verified_at`. The old - app stored tokens but never emailed them — the Loco rewrite **should actually - send** the email via the scaffolded `mailers/auth`. -- Every response that changes auth state should let the navbar refresh - (`GET /layout/navbar`). - -### 6.2 RBAC -- **Roles:** `SuperAdmin`, `Admin` (same perms as SuperAdmin), `Moderator`, `User`. -- **Permissions (~14):** `users.{create,read,update,delete,manage_roles}`, - `roles.assign`, `content.{create,read,update,delete,moderate}`, - `images.{create,read,update,delete}`. -- Role→permission mapping: - - SuperAdmin / Admin → all permissions. - - Moderator → `content.read/update/moderate`, `users.read`, `images.read/delete`. - - User (default) → `content.read`, `images.create/read`. -- A **middleware** loads the current user's `UserPermissions` into request - extensions on every request so handlers/views can check access. In Loco, do this - with a middleware layer or a custom extractor. Provide guard helpers equivalent - to "Moderator+", "Admin+", "SuperAdmin". - -### 6.3 Admin -- Dashboard with system stats (user counts, content counts) — Moderator+. -- User management: list with search filter + pagination, detail view — Admin+. -- Role assign/remove per user, each writing an `audit_logs` entry — Admin+. -- Audit log viewer — Moderator+. -- `AuditLog` fields: action, target_type, target_id, JSONB details, ip_address, - user_agent. Populate IP + user-agent from request headers. - -### 6.4 Blog -- Public: list published articles (paginate), view by slug (increments `view_count`). -- Admin (Moderator+): create / edit / delete, draft↔publish (`published_at` set on - publish), drafts visible only in admin. -- Slug auto-generated from title: lowercase, alphanumeric + hyphens, collapse - repeats; uniqueness enforced by DB — surface a clear "already exists" error. -- `author_id` = current user. `featured_image_id` references an uploaded image id. - -### 6.5 Audio dashboard -- **Albums:** CRUD, publish workflow, public list shows published only, slug URLs, - `cover_image_id` references an image, `view_count`. -- **Tracks:** belong to an album, multipart upload (50 MB cap, extensions - `mp3 wav ogg flac aac m4a webm`), `track_number`, `duration` (seconds, nullable — - upload does not extract it; the player reads it from the audio element instead), - `featured`, `play_count`, slug unique per album. -- **Tags:** create + assign to tracks (M:N). Tag has unique name + slug. - -### 6.6 Audio streaming & player -- `stream_track` / `stream_handler` must honour `Range` requests (see §3.5). -- The old app has a **persistent bottom-bar player** (`templates/player/player.html`): - hidden `