Implement auth, persistence, typed responses, and injectable RNG

Auth: JWT tokens (sb-auth) with argon2 password hashing, login/register
endpoints, Bearer token middleware protecting all routes except auth.
Authorization checks ensure users can only access their own characters.

Persistence: SQLite via rusqlite (sb-db) with write-through on all
mutations. Optional via SB_DB_PATH env var; falls back to in-memory.

Typed responses: ~20 response DTOs in sb-shared replacing ad-hoc JSON.

Combat: Injectable RNG via &mut impl Rng for deterministic tests.

Clean Architecture: Combat types and response DTOs in sb-shared; domain
services in sb-core; auth in sb-auth; persistence in sb-db.
This commit is contained in:
Jan-Peter von Hunnius 2026-08-09 16:51:52 +02:00
parent 7a75a523f0
commit 6410e0a86d
39 changed files with 3450 additions and 48 deletions

View file

@ -0,0 +1,53 @@
use axum::{
extract::Request,
http::{header, StatusCode},
middleware::Next,
response::Response,
};
use sb_auth::jwt::verify_token;
#[derive(Clone)]
pub struct AuthUser {
pub user_id: u32,
pub name: String,
pub role: String,
}
pub async fn auth_middleware(
mut request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let auth_header = request
.headers()
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let token = auth_header
.strip_prefix("Bearer ")
.unwrap_or(auth_header);
if token.is_empty() {
return Err(StatusCode::UNAUTHORIZED);
}
let claims = verify_token(token).map_err(|_| StatusCode::UNAUTHORIZED)?;
request.extensions_mut().insert(AuthUser {
user_id: claims.sub,
name: claims.name,
role: claims.role,
});
Ok(next.run(request).await)
}
impl std::fmt::Debug for AuthUser {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AuthUser")
.field("user_id", &self.user_id)
.field("name", &self.name)
.field("role", &self.role)
.finish()
}
}