ab 8530016d35
Build and Publish / Build and Publish Docker Image (push) Successful in 2m47s
Reworked Artist page
2026-05-25 17:41:00 +03:00
2026-05-23 13:18:28 +03:00
2026-05-23 13:08:09 +03:00
2026-05-25 17:41:00 +03:00
2026-05-25 17:41:00 +03:00
2026-05-23 13:18:28 +03:00
2026-05-23 13:18:28 +03:00
2026-05-25 13:50:24 +03:00
2026-05-25 17:41:00 +03:00
2026-05-25 17:41:00 +03:00
2026-05-23 13:18:28 +03:00
2026-05-25 16:26:45 +03:00

furumusic

Reusable web-app boilerplate: auth, OIDC/SSO, admin panel, user management, i18n, PostgreSQL.

Built with Rust (cot framework).

Quick start

export FURU_DATABASE_URL=postgresql://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 defaultAppConfig::default()
  2. Database override — rows in the furumusic__config_entry table
  3. Environment variableFURU_<FIELD_NAME> (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 access and role mapping: The oidc_user_groups config field lists OIDC group names (comma-separated) allowed to access the service. When it is set, users outside both oidc_user_groups and oidc_admin_groups are denied before provisioning/login. The oidc_admin_groups config field lists OIDC group names 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 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:

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_OIDC_USER_GROUPS Comma-separated OIDC groups allowed to access the service. Empty means any authenticated SSO user is allowed. (empty)
FURU_SWAGGER_ENABLED Serve Swagger UI at /swagger/ false
S
Description
No description provided
Readme 6.8 MiB
Languages
Rust 64.2%
HTML 35.8%