4fdd56dae4
Backend (Rust API): - Add users and play_events tables (migration 0005) - Extract full user identity from JWT (sub, username, email, name) and pass AuthUser via request extensions to all handlers - Auto-upsert user in background on every authenticated request - POST /api/tracks/:slug/play endpoint to record play events - Allow POST method in CORS Frontend (Node player): - Call recordPlay() when a track starts playing - Add user profile avatar with dropdown menu (name, email, sign out) - Pass user info from App through FurumiPlayer to Header Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
59 lines
1.8 KiB
Rust
59 lines
1.8 KiB
Rust
pub mod api;
|
|
pub mod auth;
|
|
|
|
use std::sync::Arc;
|
|
use std::path::PathBuf;
|
|
use std::time::Duration;
|
|
|
|
use axum::{Router, routing::{get, post}, middleware};
|
|
use axum::http::{header, Method};
|
|
use sqlx::PgPool;
|
|
use tower_http::cors::{Any, CorsLayer};
|
|
|
|
#[derive(Clone)]
|
|
pub struct AppState {
|
|
pub pool: PgPool,
|
|
#[allow(dead_code)]
|
|
pub storage_dir: Arc<PathBuf>,
|
|
pub oidc: Option<Arc<auth::OidcState>>,
|
|
}
|
|
|
|
pub fn build_router(state: Arc<AppState>) -> Router {
|
|
let library = Router::new()
|
|
.route("/artists", get(api::list_artists))
|
|
.route("/artists/:slug", get(api::get_artist))
|
|
.route("/artists/:slug/albums", get(api::list_artist_albums))
|
|
.route("/artists/:slug/tracks", get(api::list_artist_all_tracks))
|
|
.route("/albums/:slug", get(api::get_album_tracks))
|
|
.route("/albums/:slug/cover", get(api::album_cover))
|
|
.route("/tracks/:slug", get(api::get_track_detail))
|
|
.route("/tracks/:slug/cover", get(api::track_cover))
|
|
.route("/stream/:slug", get(api::stream_track))
|
|
.route("/search", get(api::search))
|
|
.route("/tracks/:slug/play", post(api::record_play));
|
|
|
|
let api = Router::new()
|
|
.nest("/api", library);
|
|
|
|
let requires_auth = state.oidc.is_some();
|
|
|
|
let app = if requires_auth {
|
|
api.route_layer(middleware::from_fn_with_state(state.clone(), auth::require_auth))
|
|
} else {
|
|
api
|
|
};
|
|
|
|
let cors = CorsLayer::new()
|
|
.allow_origin(Any)
|
|
.allow_methods([Method::GET, Method::POST, Method::OPTIONS, Method::HEAD])
|
|
.allow_headers([header::ACCEPT, header::CONTENT_TYPE, header::AUTHORIZATION])
|
|
.max_age(Duration::from_secs(600));
|
|
|
|
Router::new()
|
|
.route("/auth/login", get(auth::oidc_login))
|
|
.route("/auth/callback", get(auth::oidc_callback))
|
|
.merge(app)
|
|
.layer(cors)
|
|
.with_state(state)
|
|
}
|