Backend: - Add ChangePasswordReq DTO with validation (current + new password) - Add AuthService::change_password() method with credential verification, password rehash, and token revocation - Add POST /api/v1/auth/change-password endpoint with utoipa annotation Frontend: - Add changePassword() API function in auth.ts - Add ChangePassword.tsx page with form validation and confirmation - Add "修改密码" tab in Settings page After password change, all refresh tokens are revoked and the user is redirected to the login page.
197 lines
6.6 KiB
Rust
197 lines
6.6 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};
|
|
|
|
/// 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))
|
|
}
|
|
|
|
/// 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),
|
|
)
|
|
.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")
|
|
.unwrap_or_else(|_| "Admin@2026".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
|
|
}
|
|
}
|