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:
@@ -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
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user