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

@@ -23,4 +23,6 @@ utoipa.workspace = true
serde_json.workspace = true
serde.workspace = true
erp-server-migration = { path = "migration" }
erp-auth.workspace = true
anyhow.workspace = true
uuid.workspace = true

View File

@@ -4,13 +4,16 @@ mod handlers;
mod state;
use axum::Router;
use axum::middleware;
use config::AppConfig;
use erp_core::events::EventBus;
use erp_core::module::ModuleRegistry;
use erp_server_migration::MigratorTrait;
use erp_auth::middleware::jwt_auth_middleware_fn;
use state::AppState;
use tracing_subscriber::EnvFilter;
use erp_core::events::EventBus;
use erp_core::module::{ErpModule, ModuleRegistry};
use erp_server_migration::MigratorTrait;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Load config
@@ -41,10 +44,12 @@ async fn main() -> anyhow::Result<()> {
// Initialize event bus (capacity 1024 events)
let event_bus = EventBus::new(1024);
// Initialize module registry
let registry = ModuleRegistry::new();
// Phase 2+ will register modules here:
// let registry = registry.register(Box::new(erp_auth::AuthModule::new(db.clone())));
// Initialize auth module
let auth_module = erp_auth::AuthModule::new();
tracing::info!(module = auth_module.name(), version = auth_module.version(), "Auth module initialized");
// Initialize module registry and register auth module
let registry = ModuleRegistry::new().register(auth_module);
tracing::info!(module_count = registry.modules().len(), "Modules registered");
// Register event handlers
@@ -53,6 +58,9 @@ async fn main() -> anyhow::Result<()> {
let host = config.server.host.clone();
let port = config.server.port;
// Extract JWT secret for middleware construction
let jwt_secret = config.jwt.secret.clone();
// Build shared state
let state = AppState {
db,
@@ -61,11 +69,31 @@ async fn main() -> anyhow::Result<()> {
module_registry: registry,
};
// Build API router with versioning
let api_v1 = Router::new().merge(handlers::health::health_check_router());
// --- Build the router ---
//
// The router is split into two layers:
// 1. Public routes: no JWT required (health, login, refresh)
// 2. Protected routes: JWT required (user CRUD, logout)
//
// Both layers share the same AppState. The protected layer wraps routes
// with the jwt_auth_middleware_fn.
// Build application router
let app = Router::new().merge(api_v1).with_state(state);
// Public routes (no authentication)
let public_routes = Router::new()
.merge(handlers::health::health_check_router())
.merge(erp_auth::AuthModule::public_routes())
.with_state(state.clone());
// Protected routes (JWT authentication required)
let protected_routes = erp_auth::AuthModule::protected_routes()
.layer(middleware::from_fn(move |req, next| {
let secret = jwt_secret.clone();
async move { jwt_auth_middleware_fn(secret, req, next).await }
}))
.with_state(state.clone());
// Merge public + protected into the final application router
let app = Router::new().merge(public_routes).merge(protected_routes);
let addr = format!("{}:{}", host, port);
let listener = tokio::net::TcpListener::bind(&addr).await?;

View File

@@ -5,8 +5,8 @@ use crate::config::AppConfig;
use erp_core::events::EventBus;
use erp_core::module::ModuleRegistry;
/// Axum 共享应用状态
/// 所有 handler 通过 State<AppState> 获取数据库连接、配置等
/// Axum shared application state.
/// All handlers access database connections, configuration, etc. through `State<AppState>`.
#[derive(Clone)]
pub struct AppState {
pub db: DatabaseConnection,
@@ -15,9 +15,38 @@ pub struct AppState {
pub module_registry: ModuleRegistry,
}
/// 允许 handler 直接提取子字段
/// Allow handlers to extract `DatabaseConnection` directly from `State<AppState>`.
impl FromRef<AppState> for DatabaseConnection {
fn from_ref(state: &AppState) -> Self {
state.db.clone()
}
}
/// Allow handlers to extract `EventBus` directly from `State<AppState>`.
impl FromRef<AppState> for EventBus {
fn from_ref(state: &AppState) -> Self {
state.event_bus.clone()
}
}
/// Allow erp-auth handlers to extract their required state without depending on erp-server.
///
/// This bridges the gap: erp-auth defines `AuthState` with the fields it needs,
/// and erp-server fills them from `AppState`.
impl FromRef<AppState> for erp_auth::AuthState {
fn from_ref(state: &AppState) -> Self {
use erp_auth::auth_state::parse_ttl;
Self {
db: state.db.clone(),
event_bus: state.event_bus.clone(),
jwt_secret: state.config.jwt.secret.clone(),
access_ttl_secs: parse_ttl(&state.config.jwt.access_token_ttl),
refresh_ttl_secs: parse_ttl(&state.config.jwt.refresh_token_ttl),
// Default tenant ID: during bootstrap, use a well-known UUID.
// In production, tenant resolution middleware will override this.
default_tenant_id: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000000")
.unwrap(),
}
}
}