- 新增 wechat_users 表迁移和 SeaORM Entity - 实现微信登录 Service(code→openid→绑定状态查询) - 实现手机号绑定 Service(创建/关联 user + 签发 JWT) - 添加公开路由 POST /auth/wechat/login 和 /auth/wechat/bind-phone - 新增 WechatConfig 到 AppConfig(appid/secret 通过环境变量配置) - 添加 reqwest 依赖用于调用微信 jscode2session API
215 lines
7.4 KiB
Rust
215 lines
7.4 KiB
Rust
use axum::Router;
|
||
use uuid::Uuid;
|
||
|
||
use erp_core::error::AppResult;
|
||
use erp_core::events::EventBus;
|
||
use erp_core::module::ErpModule;
|
||
|
||
use crate::handler::{auth_handler, org_handler, role_handler, user_handler, wechat_handler};
|
||
|
||
/// Auth module implementing the `ErpModule` trait.
|
||
///
|
||
/// Manages identity, authentication, and user CRUD within the ERP platform.
|
||
/// This module has no dependencies on other business modules.
|
||
pub struct AuthModule;
|
||
|
||
impl AuthModule {
|
||
pub fn new() -> Self {
|
||
Self
|
||
}
|
||
|
||
/// Build public (unauthenticated) routes for the auth module.
|
||
///
|
||
/// These routes do not require a valid JWT token.
|
||
/// The caller wraps this into whatever state type the application uses.
|
||
pub fn public_routes<S>() -> Router<S>
|
||
where
|
||
crate::auth_state::AuthState: axum::extract::FromRef<S>,
|
||
S: Clone + Send + Sync + 'static,
|
||
{
|
||
Router::new()
|
||
.route("/auth/login", axum::routing::post(auth_handler::login))
|
||
.route("/auth/refresh", axum::routing::post(auth_handler::refresh))
|
||
.route(
|
||
"/auth/wechat/login",
|
||
axum::routing::post(wechat_handler::wechat_login),
|
||
)
|
||
.route(
|
||
"/auth/wechat/bind-phone",
|
||
axum::routing::post(wechat_handler::wechat_bind_phone),
|
||
)
|
||
}
|
||
|
||
/// Build protected (authenticated) routes for the auth module.
|
||
///
|
||
/// These routes require a valid JWT token, verified by the middleware layer.
|
||
/// The caller wraps this into whatever state type the application uses.
|
||
pub fn protected_routes<S>() -> Router<S>
|
||
where
|
||
crate::auth_state::AuthState: axum::extract::FromRef<S>,
|
||
S: Clone + Send + Sync + 'static,
|
||
{
|
||
Router::new()
|
||
.route("/auth/logout", axum::routing::post(auth_handler::logout))
|
||
.route(
|
||
"/auth/change-password",
|
||
axum::routing::post(auth_handler::change_password),
|
||
)
|
||
.route(
|
||
"/users",
|
||
axum::routing::get(user_handler::list_users).post(user_handler::create_user),
|
||
)
|
||
.route(
|
||
"/users/{id}",
|
||
axum::routing::get(user_handler::get_user)
|
||
.put(user_handler::update_user)
|
||
.delete(user_handler::delete_user),
|
||
)
|
||
.route(
|
||
"/users/{id}/roles",
|
||
axum::routing::post(user_handler::assign_roles),
|
||
)
|
||
.route(
|
||
"/roles",
|
||
axum::routing::get(role_handler::list_roles).post(role_handler::create_role),
|
||
)
|
||
// 精确匹配 /roles/permissions,必须在 /roles/{id} 之前注册
|
||
.route(
|
||
"/roles/permissions",
|
||
axum::routing::get(role_handler::list_permissions),
|
||
)
|
||
.route(
|
||
"/roles/{id}",
|
||
axum::routing::get(role_handler::get_role)
|
||
.put(role_handler::update_role)
|
||
.delete(role_handler::delete_role),
|
||
)
|
||
.route(
|
||
"/roles/{id}/permissions",
|
||
axum::routing::get(role_handler::get_role_permissions)
|
||
.post(role_handler::assign_permissions),
|
||
)
|
||
.route(
|
||
"/permissions",
|
||
axum::routing::get(role_handler::list_permissions),
|
||
)
|
||
// Organization routes
|
||
.route(
|
||
"/organizations",
|
||
axum::routing::get(org_handler::list_organizations)
|
||
.post(org_handler::create_organization),
|
||
)
|
||
.route(
|
||
"/organizations/{id}",
|
||
axum::routing::put(org_handler::update_organization)
|
||
.delete(org_handler::delete_organization),
|
||
)
|
||
// Department routes (nested under organization)
|
||
.route(
|
||
"/organizations/{org_id}/departments",
|
||
axum::routing::get(org_handler::list_departments)
|
||
.post(org_handler::create_department),
|
||
)
|
||
.route(
|
||
"/departments/{id}",
|
||
axum::routing::put(org_handler::update_department)
|
||
.delete(org_handler::delete_department),
|
||
)
|
||
// Position routes (nested under department)
|
||
.route(
|
||
"/departments/{dept_id}/positions",
|
||
axum::routing::get(org_handler::list_positions).post(org_handler::create_position),
|
||
)
|
||
.route(
|
||
"/positions/{id}",
|
||
axum::routing::put(org_handler::update_position)
|
||
.delete(org_handler::delete_position),
|
||
)
|
||
}
|
||
}
|
||
|
||
impl Default for AuthModule {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl ErpModule for AuthModule {
|
||
fn name(&self) -> &str {
|
||
"auth"
|
||
}
|
||
|
||
fn version(&self) -> &str {
|
||
env!("CARGO_PKG_VERSION")
|
||
}
|
||
|
||
fn dependencies(&self) -> Vec<&str> {
|
||
// Auth is a foundational module with no business-module dependencies.
|
||
vec![]
|
||
}
|
||
|
||
fn register_event_handlers(&self, _bus: &EventBus) {
|
||
// Auth 模块暂无跨模块事件订阅需求
|
||
}
|
||
|
||
async fn on_tenant_created(
|
||
&self,
|
||
tenant_id: Uuid,
|
||
db: &sea_orm::DatabaseConnection,
|
||
_event_bus: &EventBus,
|
||
) -> AppResult<()> {
|
||
let password = std::env::var("ERP__SUPER_ADMIN_PASSWORD")
|
||
.map_err(|_| {
|
||
tracing::error!("环境变量 ERP__SUPER_ADMIN_PASSWORD 未设置,无法初始化租户认证");
|
||
erp_core::error::AppError::Internal(
|
||
"ERP__SUPER_ADMIN_PASSWORD 未设置".to_string(),
|
||
)
|
||
})?;
|
||
crate::service::seed::seed_tenant_auth(db, tenant_id, &password)
|
||
.await
|
||
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
|
||
tracing::info!(tenant_id = %tenant_id, "Tenant auth initialized");
|
||
Ok(())
|
||
}
|
||
|
||
async fn on_tenant_deleted(
|
||
&self,
|
||
tenant_id: Uuid,
|
||
db: &sea_orm::DatabaseConnection,
|
||
) -> AppResult<()> {
|
||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||
use chrono::Utc;
|
||
|
||
let now = Utc::now();
|
||
|
||
// 软删除该租户下所有用户
|
||
let users = crate::entity::user::Entity::find()
|
||
.filter(crate::entity::user::Column::TenantId.eq(tenant_id))
|
||
.filter(crate::entity::user::Column::DeletedAt.is_null())
|
||
.all(db)
|
||
.await
|
||
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
|
||
|
||
for user_model in users {
|
||
let current_version = user_model.version;
|
||
let active: crate::entity::user::ActiveModel = user_model.into();
|
||
let mut to_update: crate::entity::user::ActiveModel = active;
|
||
to_update.deleted_at = Set(Some(now));
|
||
to_update.updated_at = Set(now);
|
||
to_update.version = Set(current_version + 1);
|
||
let _ = to_update
|
||
.update(db)
|
||
.await
|
||
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
|
||
}
|
||
|
||
tracing::info!(tenant_id = %tenant_id, "Tenant users soft-deleted");
|
||
Ok(())
|
||
}
|
||
|
||
fn as_any(&self) -> &dyn std::any::Any {
|
||
self
|
||
}
|
||
}
|