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
|
|
@ -6,6 +6,7 @@ edition = "2024"
|
|||
[dependencies]
|
||||
jsonwebtoken = "9"
|
||||
argon2 = "0.5"
|
||||
getrandom = "0.3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
sb-shared = { path = "../sb-shared" }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
use argon2::{
|
||||
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||
Argon2,
|
||||
};
|
||||
|
||||
pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
|
||||
let mut salt_bytes = [0u8; 16];
|
||||
getrandom::fill(&mut salt_bytes).expect("failed to generate salt");
|
||||
let salt = SaltString::encode_b64(&salt_bytes).expect("valid salt");
|
||||
let argon2 = Argon2::default();
|
||||
let hash = argon2.hash_password(password.as_bytes(), &salt)?;
|
||||
Ok(hash.to_string())
|
||||
}
|
||||
|
||||
pub fn verify_password(password: &str, hash: &str) -> bool {
|
||||
let parsed = match PasswordHash::new(hash) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return false,
|
||||
};
|
||||
Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok()
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{Utc, Duration};
|
||||
|
||||
fn jwt_secret() -> Vec<u8> {
|
||||
std::env::var("JWT_SECRET")
|
||||
.map(|s| s.into_bytes())
|
||||
.unwrap_or_else(|_| b"skara_brae_dev_secret_change_in_prod".to_vec())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
pub sub: u32,
|
||||
pub name: String,
|
||||
pub role: String,
|
||||
pub exp: usize,
|
||||
}
|
||||
|
||||
pub fn create_token(user_id: u32, name: &str, role: &str) -> Result<String, jsonwebtoken::errors::Error> {
|
||||
let exp = Utc::now()
|
||||
.checked_add_signed(Duration::hours(24))
|
||||
.expect("valid timestamp")
|
||||
.timestamp() as usize;
|
||||
|
||||
let claims = Claims {
|
||||
sub: user_id,
|
||||
name: name.to_string(),
|
||||
role: role.to_string(),
|
||||
exp,
|
||||
};
|
||||
|
||||
jsonwebtoken::encode(
|
||||
&Header::default(),
|
||||
&claims,
|
||||
&EncodingKey::from_secret(&jwt_secret()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn verify_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
|
||||
let mut validation = Validation::default();
|
||||
validation.leeway = 60;
|
||||
jsonwebtoken::decode::<Claims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(&jwt_secret()),
|
||||
&validation,
|
||||
).map(|d| d.claims)
|
||||
}
|
||||
|
|
@ -1,3 +1,2 @@
|
|||
pub mod jwt;
|
||||
pub mod hash;
|
||||
pub mod session;
|
||||
pub mod jwt;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue