# furumusic Reusable web-app boilerplate: auth, OIDC/SSO, admin panel, user management, i18n, PostgreSQL. Built with Rust ([cot](https://cot.rs) framework). ## Quick start ```bash export FURU_DATABASE_URL=postgres://user:pass@localhost/furumusic cargo run # Open http://localhost:8000/admin/setup to create the first admin account ``` ## Project structure ``` Cargo.toml Project manifest and dependencies build.rs Captures rustc version + target at compile time src/ main.rs Entrypoint; HTTP router, login/logout handlers, tracing init config.rs 3-tier config system (default → DB → env); FURU_* env vars auth.rs Session auth, Role enum (Admin/User), login/logout/guards user.rs User + OidcLink DB models, CRUD, password hashing, migrations oidc.rs OIDC/SSO flow: discovery, PKCE, token exchange, user provisioning i18n/ mod.rs Language resolution (cookie → Accept-Language → default), extractor phrases.rs All UI strings in English and Russian (translations! macro) api/ mod.rs JSON API endpoints (mounted at /api), session-based auth admin/ mod.rs Admin sub-app router: dashboard, settings, users, debug, setup views.rs Admin page handlers and templates templates/ base.html Root HTML layout with lang/title blocks login.html Login page (password + optional SSO button) admin/ layout.html Admin sidebar/nav wrapper index.html Admin dashboard debug.html Build info + config table (with secret redaction) settings.html OIDC and auth settings form setup.html First-run admin account creation users.html User list user_form.html User create/edit form ``` ## Architecture ### Config system (`src/config.rs`) Every setting lives in `AppConfig` and is resolved in three layers: 1. **Compiled default** — `AppConfig::default()` 2. **Database override** — rows in the `furumusic__config_entry` table 3. **Environment variable** — `FURU_` (highest priority) `ConfigSources` tracks where each field's effective value came from (shown in the admin debug page). **To add a new config field:** 1. Add the field to `AppConfig` struct 2. Set its default in `AppConfig::default()` 3. Add the field to `ConfigSources` struct and its `Default` impl 4. Add it to the `impl_env_overrides!(…)` invocation 5. Add an `apply_db_field!()` call in `apply_db_overrides` 6. Add an `entry!()` line in `admin/views.rs → config_display_entries()` ### Auth (`src/auth.rs`) Session-based authentication with two roles: - **`Role::Admin`** — full access to admin panel - **`Role::User`** — standard user Key functions: - `login(session, user_id)` — sets session, cycles session ID - `logout(session)` — flushes session - `get_session_user(session, db)` — returns `AuthenticatedUser` if active - `require_admin_or_redirect(session, db)` — guard that returns 403 or redirects to `/login` ### OIDC/SSO (`src/oidc.rs`) Full OpenID Connect authorization code flow with PKCE: 1. `GET /auth/oidc/start` — discovers provider, builds auth URL, stores CSRF/nonce/PKCE in session, redirects to IdP 2. `GET /auth/oidc/callback` — validates CSRF, exchanges code for tokens, verifies ID token, provisions user Provider metadata is cached for 1 hour and invalidated when OIDC config changes. **Group-to-role mapping:** The `oidc_admin_groups` config field lists OIDC group names (comma-separated) that grant the admin role. Groups are extracted from the `groups` claim in the ID token JWT payload. **User provisioning order:** 1. Find existing `OidcLink` by issuer+sub → update claims, update role 2. Find existing `User` by email → create OidcLink, update role 3. Create new user (no password) + OidcLink Stale links (pointing to deleted users) are cleaned up automatically. ### User model (`src/user.rs`) Two database models: - **`User`** — id, username (unique), password (optional for OIDC-only), email, display_name, avatar_url, role, is_active - **`OidcLink`** — id, user_id, issuer, sub, email, name, avatar_url; unique index on (issuer, sub) Migrations: M0003 (User table), M0004 (OidcLink table), M0005 (OidcLink indexes). ### i18n (`src/i18n/`) Compile-time bilingual UI (English + Russian). - `translations!` macro in `phrases.rs` generates a `Translations` struct with static `EN` and `RU` instances - Language resolution: `furu_lang` cookie → `Accept-Language` header → English default - `I18n` is a cot request extractor — handlers receive it automatically - `set_lang` endpoint (`/set-lang?lang=ru&next=/`) sets the cookie ### API (`src/api/`) JSON API mounted at `/api`. Uses the same session cookie as HTML pages — works automatically for same-origin frontend requests (no CORS, no tokens needed). Helpers in `api/mod.rs`: - `json_ok(value)` — 200 with `application/json` - `json_error(status, message)` — error response as `{"error": "..."}` | Route | Method | Description | |-------|--------|-------------| | `/api/me` | GET | Current user (id, name, role) or 401 | **Swagger UI** is available at `/swagger/` when `FURU_SWAGGER_ENABLED=true`. The OpenAPI spec is auto-generated from handler types. To add a new API endpoint: 1. Define request/response structs with `#[derive(Serialize, JsonSchema)]` 2. Write an async handler, return `Json(response).into_response()` 3. Add a `Route::with_api_handler_and_name(…, api_get(handler), …)` in `ApiApp::router()` 4. The endpoint appears automatically in Swagger UI ### Admin panel (`src/admin/`) Mounted at `/admin`. All routes (except `/admin/setup`) require `Role::Admin`. | Route | Purpose | |-------|---------| | `/admin/setup` | First-run: create initial admin (only works when zero users exist) | | `/admin/` | Dashboard | | `/admin/debug` | Build info, config values with sources, DB connectivity | | `/admin/settings` | OIDC config, auth toggles (saved to DB config table) | | `/admin/users` | User list | | `/admin/users/new` | Create user | | `/admin/users/{id}/edit` | Edit user | | `/admin/users/{id}/delete` | Delete user (POST) | ## How to extend ### 1. Add a config field See [Config system](#config-system-srcconfigrs) above — 6 locations to update. ### 2. Add a database model 1. Define a struct with `#[cot::db::model]` in a new or existing file 2. Write a migration struct implementing `cot::db::migrations::Migration` 3. Register the migration in the `AdminApp::migrations()` method in `src/admin/mod.rs` ### 3. Add a page 1. Create a template in `templates/` 2. Write a handler function that returns `Html` 3. Add a `Route::with_handler_and_name(…)` in the appropriate `router()` method 4. If admin-only, wrap with `require_admin_or_redirect` ### 4. Add a translation Add a line to the `translations!` macro in `src/i18n/phrases.rs`: ```rust my_key: "English text", "Русский текст"; ``` Access it in handlers/templates as `i18n.t.my_key` (or `t.my_key` in templates). ### 5. Add an API endpoint Same as adding a page, but return a JSON response instead of `Html`. The `json` feature is enabled in Cargo.toml. ## Environment variables All prefixed with `FURU_`. Priority: env var > DB override > compiled default. | Variable | Description | Default | |----------|-------------|---------| | `FURU_DATABASE_URL` | PostgreSQL connection URL | *(empty — required)* | | `FURU_LOG_LEVEL` | Tracing filter (e.g. `info`, `debug`, `warn,furumusic=trace`) | `info` | | `FURU_AUTH_PASSWORD_ENABLED` | Enable password login | `true` | | `FURU_AUTH_SSO_ENABLED` | Enable SSO/OIDC login | `false` | | `FURU_OIDC_ISSUER` | OIDC issuer URL | *(empty)* | | `FURU_OIDC_CLIENT_ID` | OIDC client ID | *(empty)* | | `FURU_OIDC_CLIENT_SECRET` | OIDC client secret | *(empty)* | | `FURU_OIDC_BUTTON_TEXT` | SSO button label | `Sign in with SSO` | | `FURU_OIDC_ADMIN_GROUPS` | Comma-separated OIDC groups that grant admin | *(empty)* | | `FURU_SWAGGER_ENABLED` | Serve Swagger UI at `/swagger/` | `false` |