presentation: HTTP server crate w/ handlers, routes, background tasks

Axum binary that wires all clean-arch crates together:
- AppState holds pre-built Deps structs (auth, channels, schedule, library, etc.)
- JWT extractors (CurrentUser, AdminUser, OptionalCurrentUser)
- Handlers delegate to application use cases, map to api-types DTOs
- Routes: auth, channels, schedule, library, admin, providers, config, iptv
- Background: auto-scheduler, broadcast poller, webhook consumer, library sync
- Factory builds everything from Config + DbPool
- SimpleProviderRegistry impl of IProviderRegistry trait
- NoopMediaProvider fallback
This commit is contained in:
2026-07-12 03:23:20 +02:00
parent afed5c01b4
commit 56d742a74c
25 changed files with 3186 additions and 5 deletions

View File

@@ -0,0 +1,149 @@
//! Authentication handlers.
use axum::Json;
use axum::extract::State;
use api_types::{LoginRequest, RefreshRequest, RegisterRequest, TokenResponse, UserResponse};
use application::auth::{LoginCommand, RegisterCommand};
use crate::errors::ApiError;
use crate::extractors::CurrentUser;
use crate::state::AppState;
/// POST /auth/register
pub async fn register(
State(state): State<AppState>,
Json(req): Json<RegisterRequest>,
) -> Result<Json<UserResponse>, ApiError> {
let cmd = RegisterCommand {
email: req.email,
password: req.password,
};
let user = application::auth::register::execute(&state.auth_deps, cmd).await?;
Ok(Json(UserResponse::from(user)))
}
/// POST /auth/login
pub async fn login(
State(state): State<AppState>,
Json(req): Json<LoginRequest>,
) -> Result<Json<TokenResponse>, ApiError> {
let cmd = LoginCommand {
email: req.email,
password: req.password,
};
let user = application::auth::login::execute(&state.auth_deps, cmd).await?;
let (access_token, refresh_token) = create_tokens(&user, &state, req.remember_me)?;
Ok(Json(TokenResponse {
access_token,
token_type: "Bearer".to_string(),
expires_in: state.config.jwt_expiry_hours * 3600,
refresh_token,
}))
}
/// POST /auth/logout — no-op for JWT (stateless)
pub async fn logout() -> Result<Json<serde_json::Value>, ApiError> {
Ok(Json(serde_json::json!({"message": "logged out"})))
}
/// GET /auth/me
pub async fn me(
CurrentUser(user): CurrentUser,
) -> Result<Json<UserResponse>, ApiError> {
Ok(Json(UserResponse::from(user)))
}
/// POST /auth/token — exchange credentials for tokens
#[cfg(feature = "auth-jwt")]
pub async fn get_token(
State(state): State<AppState>,
Json(req): Json<LoginRequest>,
) -> Result<Json<TokenResponse>, ApiError> {
let cmd = LoginCommand {
email: req.email,
password: req.password,
};
let user = application::auth::login::execute(&state.auth_deps, cmd).await?;
let (access_token, refresh_token) = create_tokens(&user, &state, req.remember_me)?;
Ok(Json(TokenResponse {
access_token,
token_type: "Bearer".to_string(),
expires_in: state.config.jwt_expiry_hours * 3600,
refresh_token,
}))
}
/// POST /auth/refresh — refresh an access token
#[cfg(feature = "auth-jwt")]
pub async fn refresh_token(
State(state): State<AppState>,
Json(req): Json<RefreshRequest>,
) -> Result<Json<TokenResponse>, ApiError> {
let validator = state
.jwt_validator
.as_ref()
.ok_or_else(|| ApiError::Internal("JWT not configured".to_string()))?;
let claims = validator.validate_refresh_token(&req.refresh_token).map_err(|e| {
tracing::debug!("Refresh token validation failed: {:?}", e);
ApiError::Unauthorized("Invalid refresh token".to_string())
})?;
let user_id: uuid::Uuid = claims
.sub
.parse()
.map_err(|_| ApiError::Unauthorized("Invalid user ID in token".to_string()))?;
let user = state
.auth_deps
.user_query
.find_by_id(domain::UserId::from(user_id))
.await
.map_err(|e| ApiError::Internal(format!("Failed to fetch user: {}", e)))?
.ok_or_else(|| ApiError::Unauthorized("User not found".to_string()))?;
let (access_token, refresh_token) = create_tokens(&user, &state, true)?;
Ok(Json(TokenResponse {
access_token,
token_type: "Bearer".to_string(),
expires_in: state.config.jwt_expiry_hours * 3600,
refresh_token,
}))
}
fn create_tokens(
user: &domain::User,
state: &AppState,
remember_me: bool,
) -> Result<(String, Option<String>), ApiError> {
#[cfg(feature = "auth-jwt")]
{
let validator = state
.jwt_validator
.as_ref()
.ok_or_else(|| ApiError::Internal("JWT not configured".to_string()))?;
let access = validator
.create_token(user)
.map_err(|e| ApiError::Internal(format!("Failed to create token: {}", e)))?;
let refresh = if remember_me {
Some(
validator
.create_refresh_token(user)
.map_err(|e| ApiError::Internal(format!("Failed to create refresh token: {}", e)))?,
)
} else {
None
};
Ok((access, refresh))
}
#[cfg(not(feature = "auth-jwt"))]
{
let _ = (user, state, remember_me);
Err(ApiError::Internal("JWT feature not enabled".to_string()))
}
}