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:
parent
7a75a523f0
commit
6410e0a86d
39 changed files with 3450 additions and 48 deletions
|
|
@ -10,6 +10,7 @@ tower = "0.5"
|
|||
tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
rand = "0.9"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
sb-core = { path = "../sb-core" }
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
pub mod routes;
|
||||
pub mod ws;
|
||||
pub mod middleware;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
|
|
@ -17,9 +16,9 @@ async fn main() {
|
|||
))
|
||||
.init();
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", get(health))
|
||||
.merge(routes::build());
|
||||
let state = routes::seed_users();
|
||||
|
||||
let app = routes::build(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
|
||||
.await
|
||||
|
|
@ -28,7 +27,3 @@ async fn main() {
|
|||
tracing::info!("listening on {:?}", listener.local_addr());
|
||||
axum::serve(listener, app).await.expect("server error");
|
||||
}
|
||||
|
||||
async fn health() -> &'static str {
|
||||
"ok"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue