feat(auth): add handlers, JWT middleware, RBAC, and module registration
- Auth handlers: login/refresh/logout + user CRUD with tenant isolation - JWT middleware: Bearer token validation → TenantContext injection - RBAC helpers: require_permission, require_any_permission, require_role - AuthModule: implements ErpModule with public/protected route split - AuthState: FromRef pattern avoids circular deps between erp-auth and erp-server - Server: public routes (health+login+refresh) + protected routes (JWT middleware) - ErpModule trait: added as_any() for downcast support - Workspace: added async-trait, sha2 dependencies
This commit is contained in:
77
crates/erp-auth/src/auth_state.rs
Normal file
77
crates/erp-auth/src/auth_state.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use erp_core::events::EventBus;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Auth-specific state extracted from the server's AppState via `FromRef`.
|
||||
///
|
||||
/// This avoids a circular dependency between erp-auth and erp-server.
|
||||
/// The server crate implements `FromRef<AppState> for AuthState` so that
|
||||
/// Axum handlers in erp-auth can extract `State<AuthState>` directly.
|
||||
///
|
||||
/// Contains everything the auth handlers need:
|
||||
/// - Database connection for user/credential lookups
|
||||
/// - EventBus for publishing domain events
|
||||
/// - JWT configuration for token signing and validation
|
||||
/// - Default tenant ID for the bootstrap phase
|
||||
#[derive(Clone)]
|
||||
pub struct AuthState {
|
||||
pub db: DatabaseConnection,
|
||||
pub event_bus: EventBus,
|
||||
pub jwt_secret: String,
|
||||
pub access_ttl_secs: i64,
|
||||
pub refresh_ttl_secs: i64,
|
||||
pub default_tenant_id: Uuid,
|
||||
}
|
||||
|
||||
/// Parse a human-readable TTL string (e.g. "15m", "7d", "1h", "900s") into seconds.
|
||||
///
|
||||
/// Falls back to parsing the raw string as seconds if no unit suffix is recognized.
|
||||
pub fn parse_ttl(ttl: &str) -> i64 {
|
||||
let ttl = ttl.trim();
|
||||
if ttl.ends_with('s') {
|
||||
ttl.trim_end_matches('s').parse::<i64>().unwrap_or(900)
|
||||
} else if ttl.ends_with('m') {
|
||||
ttl.trim_end_matches('m').parse::<i64>().unwrap_or(15) * 60
|
||||
} else if ttl.ends_with('h') {
|
||||
ttl.trim_end_matches('h').parse::<i64>().unwrap_or(1) * 3600
|
||||
} else if ttl.ends_with('d') {
|
||||
ttl.trim_end_matches('d').parse::<i64>().unwrap_or(1) * 86400
|
||||
} else {
|
||||
ttl.parse::<i64>().unwrap_or(900)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_ttl_seconds() {
|
||||
assert_eq!(parse_ttl("900s"), 900);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ttl_minutes() {
|
||||
assert_eq!(parse_ttl("15m"), 900);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ttl_hours() {
|
||||
assert_eq!(parse_ttl("1h"), 3600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ttl_days() {
|
||||
assert_eq!(parse_ttl("7d"), 604800);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ttl_raw_number() {
|
||||
assert_eq!(parse_ttl("300"), 300);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ttl_fallback_on_invalid() {
|
||||
assert_eq!(parse_ttl("invalid"), 900);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user