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:
iven
2026-04-11 03:22:04 +08:00
parent edc41a1500
commit 3afd732de8
16 changed files with 667 additions and 15 deletions

View 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);
}
}