Add track, release, and queue sharing with post-login redirects; support shared playlist links and highlighted shared tracks. Add local synchronized playback for jams, constrain HTTP metrics to known routes, and refine mobile player controls/layout.
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:
- Compiled default —
AppConfig::default() - Database override — rows in the
furumusic__config_entrytable - Environment variable —
FURU_<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:
- Add the field to
AppConfigstruct - Set its default in
AppConfig::default() - Add the field to
ConfigSourcesstruct and itsDefaultimpl - Add it to the
impl_env_overrides!(…)invocation - Add an
apply_db_field!()call inapply_db_overrides - Add an
entry!()line inadmin/views.rs → config_display_entries()
Auth (src/auth.rs)
Session-based authentication with two roles:
Role::Admin— full access to admin panelRole::User— standard user
Key functions:
login(session, user_id)— sets session, cycles session IDlogout(session)— flushes sessionget_session_user(session, db)— returnsAuthenticatedUserif activerequire_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:
GET /auth/oidc/start— discovers provider, builds auth URL, stores CSRF/nonce/PKCE in session, redirects to IdPGET /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:
- Find existing
OidcLinkby issuer+sub → update claims, update role - Find existing
Userby email → create OidcLink, update role - 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_activeOidcLink— 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 inphrases.rsgenerates aTranslationsstruct with staticENandRUinstances- Language resolution:
furu_langcookie →Accept-Languageheader → English default I18nis a cot request extractor — handlers receive it automaticallyset_langendpoint (/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 withapplication/jsonjson_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:
- Define request/response structs with
#[derive(Serialize, JsonSchema)] - Write an async handler, return
Json(response).into_response() - Add a
Route::with_api_handler_and_name(…, api_get(handler), …)inApiApp::router() - 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
- Define a struct with
#[cot::db::model]in a new or existing file - Write a migration struct implementing
cot::db::migrations::Migration - Register the migration in the
AdminApp::migrations()method insrc/admin/mod.rs
3. Add a page
- Create a template in
templates/ - Write a handler function that returns
Html - Add a
Route::with_handler_and_name(…)in the appropriaterouter()method - 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 |