loco rewrite
This commit is contained in:
399
REWRITE_SPEC.md
Normal file
399
REWRITE_SPEC.md
Normal file
@@ -0,0 +1,399 @@
|
||||
# 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
|
||||
- [ ] **Audio streaming** — range-aware track/file streaming, raw upload
|
||||
- [ ] **Audio player** — persistent bottom-bar player (frontend)
|
||||
- [ ] **Images** — upload + serve, used as cover/featured images
|
||||
- [ ] **Theme** — per-user light/dark preference
|
||||
- [ ] **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 <name> ...`.
|
||||
|
||||
### 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 `<audio>` + Alpine.js, play/pause, prev/next, seek bar, volume persisted to
|
||||
`localStorage`, auto-advance on track end. It is a sibling of the main content so
|
||||
HTMX navigation never tears it down — music keeps playing across pages.
|
||||
- Pages drive it via one global JS API: `MusicPlayer.playQueue(containerSelector,
|
||||
startIndex)`, reading `data-*` attributes off track rows (no JSON in markup).
|
||||
- This is a **frontend concern** — reproduce the behaviour in the new GUI; the
|
||||
backend only needs the range-aware stream endpoints + track metadata.
|
||||
|
||||
### 6.7 Images
|
||||
- Upload: multipart, validate extension (`jpg jpeg png webp gif`) and MIME, max
|
||||
10 MB, store via storage backend, return a file id.
|
||||
- Serve: public, correct MIME (guessed from extension), long-cache headers.
|
||||
- Image ids are referenced by `blog_articles.featured_image_id` and
|
||||
`audio_albums.cover_image_id` (stored as strings, not FKs).
|
||||
|
||||
### 6.8 Theme
|
||||
- Per-user `theme` column (`light`/`dark`); also a cookie for anonymous users.
|
||||
- `GET /theme` returns current, `GET /theme/set` toggles/sets it.
|
||||
- Maps to DaisyUI themes in the old GUI (`winter`/`dracula`). New GUI decides its
|
||||
own theming, but keep the per-user persisted preference.
|
||||
|
||||
### 6.9 Storage
|
||||
- Default backend: local filesystem under `uploads/` (`uploads/images`,
|
||||
`uploads/audio`). Keep S3/Azure/GCS reachable via config only.
|
||||
- Path-traversal protection on every user-supplied filename/path.
|
||||
- Use Loco's `storage` module; configure backends in `config/*.yaml`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Configuration / env
|
||||
|
||||
Old `.env`:
|
||||
```
|
||||
DATABASE_URL=postgres://uni_web_user:1@localhost/universal_web
|
||||
ADMIN_EMAIL=filippriec@gmail.com
|
||||
```
|
||||
In Loco, move these into `config/development.yaml` (DB url under `database:`) and
|
||||
add a custom `settings:` entry for `admin_email` (read via `ctx.config.settings`).
|
||||
The old default bind address is `0.0.0.0:3000` — set under `server:` in the yaml.
|
||||
|
||||
---
|
||||
|
||||
## 8. What the Loco scaffold already gives you
|
||||
|
||||
Already generated in this directory — reuse, don't rebuild:
|
||||
- `src/controllers/auth.rs` — register / login / verify / forgot / reset endpoints.
|
||||
- `src/models/users.rs` + `_entities/users.rs` — user model with password hashing.
|
||||
- `src/mailers/auth/` — welcome / forgot-password / magic-link email templates.
|
||||
- `src/workers/downloader.rs` — example background worker (bg mode = `async`).
|
||||
- `src/initializers/view_engine.rs` — Tera view engine + i18n wiring.
|
||||
- `migration/` — SeaORM migration crate.
|
||||
- `config/{development,production,test}.yaml` — config files.
|
||||
|
||||
---
|
||||
|
||||
## 9. Suggested implementation order
|
||||
|
||||
1. **DB & models** — port all 8 tables as SeaORM migrations; `cargo loco db
|
||||
entities`; reconcile `users` with the scaffold (§4.1).
|
||||
2. **Auth + sessions** — settle §3.1, get register/login/logout/me working,
|
||||
including admin-bootstrap.
|
||||
3. **RBAC** — roles/permissions, the permission-loading middleware, guard helpers.
|
||||
4. **Storage + images** — storage backend, image upload/serve (unblocks blog/audio
|
||||
cover images).
|
||||
5. **Blog** — CRUD + publish + public pages.
|
||||
6. **Audio dashboard** — albums, tracks (multipart upload), tags.
|
||||
7. **Audio streaming + player** — range-aware endpoints, then the player in the GUI.
|
||||
8. **Admin** — dashboard, user management, role UI, audit log.
|
||||
9. **Theme**, **home/layout/navbar**.
|
||||
10. **Swagger/OpenAPI** (optional), tests, polish.
|
||||
|
||||
---
|
||||
|
||||
## 10. Testing
|
||||
|
||||
The old app has 19 tests (17 integration hitting a live server + 2 multipart
|
||||
upload) plus unit tests; see `../TESTING.md`. Loco has a first-class testing story
|
||||
(`#[tokio::test]` + `loco-rs` testing feature, `serial_test`, `insta` snapshots —
|
||||
all already dev-deps). Re-create integration coverage for every route in §5,
|
||||
especially: public pages return 200, protected routes return 401/403 without auth,
|
||||
multipart uploads, and range requests on the streaming endpoints.
|
||||
|
||||
---
|
||||
|
||||
## 11. Notes / known quirks carried over
|
||||
|
||||
- Track `duration` is frequently `NULL` (upload never extracts it). The player
|
||||
reads duration from the `<audio>` element's metadata, so the seek bar works
|
||||
anyway — don't block on server-side duration extraction.
|
||||
- `Admin` and `SuperAdmin` have identical permission sets; only role-assignment
|
||||
endpoints are SuperAdmin-gated.
|
||||
- Image references (`featured_image_id`, `cover_image_id`) are plain strings, not
|
||||
foreign keys — the rewrite may upgrade these to real FKs if desired.
|
||||
Reference in New Issue
Block a user