diff --git a/crates/erp-auth/src/service/mod.rs b/crates/erp-auth/src/service/mod.rs index ed40a5b..b0392f3 100644 --- a/crates/erp-auth/src/service/mod.rs +++ b/crates/erp-auth/src/service/mod.rs @@ -1,4 +1,5 @@ pub mod auth_service; pub mod password; +pub mod seed; pub mod token_service; pub mod user_service; diff --git a/crates/erp-auth/src/service/seed.rs b/crates/erp-auth/src/service/seed.rs new file mode 100644 index 0000000..8c6b878 --- /dev/null +++ b/crates/erp-auth/src/service/seed.rs @@ -0,0 +1,265 @@ +use sea_orm::{ActiveModelTrait, Set}; +use uuid::Uuid; + +use crate::entity::{permission, role, role_permission, user, user_credential, user_role}; +use crate::error::AuthError; + +use super::password; + +/// Permission definitions to seed for every new tenant. +/// Each tuple is: (code, name, resource, action, description) +const DEFAULT_PERMISSIONS: &[(&str, &str, &str, &str, &str)] = &[ + ("user:create", "创建用户", "user", "create", "创建新用户"), + ("user:read", "查看用户", "user", "read", "查看用户信息"), + ("user:update", "编辑用户", "user", "update", "编辑用户信息"), + ("user:delete", "删除用户", "user", "delete", "软删除用户"), + ("role:create", "创建角色", "role", "create", "创建新角色"), + ("role:read", "查看角色", "role", "read", "查看角色信息"), + ("role:update", "编辑角色", "role", "update", "编辑角色"), + ("role:delete", "删除角色", "role", "delete", "删除角色"), + ( + "permission:read", + "查看权限", + "permission", + "read", + "查看权限列表", + ), + ( + "organization:create", + "创建组织", + "organization", + "create", + "创建组织", + ), + ( + "organization:read", + "查看组织", + "organization", + "read", + "查看组织架构", + ), + ( + "organization:update", + "编辑组织", + "organization", + "update", + "编辑组织", + ), + ( + "organization:delete", + "删除组织", + "organization", + "delete", + "删除组织", + ), + ( + "department:create", + "创建部门", + "department", + "create", + "创建部门", + ), + ( + "department:read", + "查看部门", + "department", + "read", + "查看部门", + ), + ( + "department:update", + "编辑部门", + "department", + "update", + "编辑部门", + ), + ( + "department:delete", + "删除部门", + "department", + "delete", + "删除部门", + ), + ("position:create", "创建岗位", "position", "create", "创建岗位"), + ("position:read", "查看岗位", "position", "read", "查看岗位"), + ("position:update", "编辑岗位", "position", "update", "编辑岗位"), + ("position:delete", "删除岗位", "position", "delete", "删除岗位"), +]; + +/// Indices of read-only permissions within DEFAULT_PERMISSIONS. +const READ_PERM_INDICES: &[usize] = &[1, 5, 9, 11, 15, 19]; + +/// Seed default auth data for a new tenant. +/// +/// Creates: +/// - 21 permissions covering user/role/permission/organization/department/position CRUD +/// - An "admin" system role with all permissions +/// - A "viewer" system role with read-only permissions +/// - A super-admin user with the admin role and a password credential +pub async fn seed_tenant_auth( + db: &sea_orm::DatabaseConnection, + tenant_id: Uuid, + super_admin_password: &str, +) -> Result<(), AuthError> { + let now = chrono::Utc::now(); + let system_user_id = Uuid::nil(); + + // 1. Create permissions + let mut perm_ids: Vec = Vec::with_capacity(DEFAULT_PERMISSIONS.len()); + for (code, name, resource, action, desc) in DEFAULT_PERMISSIONS { + let perm_id = Uuid::now_v7(); + perm_ids.push(perm_id); + + let perm = permission::ActiveModel { + id: Set(perm_id), + tenant_id: Set(tenant_id), + code: Set(code.to_string()), + name: Set(name.to_string()), + resource: Set(resource.to_string()), + action: Set(action.to_string()), + description: Set(Some(desc.to_string())), + created_at: Set(now), + updated_at: Set(now), + created_by: Set(system_user_id), + updated_by: Set(system_user_id), + deleted_at: Set(None), + version: Set(1), + }; + perm.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + } + + // 2. Create "admin" role with all permissions + let admin_role_id = Uuid::now_v7(); + let admin_role = role::ActiveModel { + id: Set(admin_role_id), + tenant_id: Set(tenant_id), + name: Set("管理员".to_string()), + code: Set("admin".to_string()), + description: Set(Some("系统管理员,拥有所有权限".to_string())), + is_system: Set(true), + created_at: Set(now), + updated_at: Set(now), + created_by: Set(system_user_id), + updated_by: Set(system_user_id), + deleted_at: Set(None), + version: Set(1), + }; + admin_role.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + + // Assign all permissions to admin role + // role_permission uses composite PK (role_id, permission_id) -- no separate id column + for perm_id in &perm_ids { + let rp = role_permission::ActiveModel { + role_id: Set(admin_role_id), + permission_id: Set(*perm_id), + tenant_id: Set(tenant_id), + created_at: Set(now), + updated_at: Set(now), + created_by: Set(system_user_id), + updated_by: Set(system_user_id), + deleted_at: Set(None), + version: Set(1), + }; + rp.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + } + + // 3. Create "viewer" role with read-only permissions + let viewer_role_id = Uuid::now_v7(); + let viewer_role = role::ActiveModel { + id: Set(viewer_role_id), + tenant_id: Set(tenant_id), + name: Set("查看者".to_string()), + code: Set("viewer".to_string()), + description: Set(Some("只读用户,可查看所有数据".to_string())), + is_system: Set(true), + created_at: Set(now), + updated_at: Set(now), + created_by: Set(system_user_id), + updated_by: Set(system_user_id), + deleted_at: Set(None), + version: Set(1), + }; + viewer_role.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + + // Assign read permissions to viewer role + for idx in READ_PERM_INDICES { + if *idx < perm_ids.len() { + let rp = role_permission::ActiveModel { + role_id: Set(viewer_role_id), + permission_id: Set(perm_ids[*idx]), + tenant_id: Set(tenant_id), + created_at: Set(now), + updated_at: Set(now), + created_by: Set(system_user_id), + updated_by: Set(system_user_id), + deleted_at: Set(None), + version: Set(1), + }; + rp.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + } + } + + // 4. Create super admin user + let admin_user_id = Uuid::now_v7(); + let password_hash = password::hash_password(super_admin_password)?; + + let admin_user = user::ActiveModel { + id: Set(admin_user_id), + tenant_id: Set(tenant_id), + username: Set("admin".to_string()), + email: Set(None), + phone: Set(None), + display_name: Set(Some("系统管理员".to_string())), + avatar_url: Set(None), + status: Set("active".to_string()), + last_login_at: Set(None), + created_at: Set(now), + updated_at: Set(now), + created_by: Set(system_user_id), + updated_by: Set(system_user_id), + deleted_at: Set(None), + version: Set(1), + }; + admin_user.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + + // Create password credential for admin user + let cred = user_credential::ActiveModel { + id: Set(Uuid::now_v7()), + tenant_id: Set(tenant_id), + user_id: Set(admin_user_id), + credential_type: Set("password".to_string()), + credential_data: Set(Some(serde_json::json!({ "hash": password_hash }))), + verified: Set(true), + created_at: Set(now), + updated_at: Set(now), + created_by: Set(system_user_id), + updated_by: Set(system_user_id), + deleted_at: Set(None), + version: Set(1), + }; + cred.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + + // 5. Assign admin role to admin user + // user_role uses composite PK (user_id, role_id) -- no separate id column + let user_role_assignment = user_role::ActiveModel { + user_id: Set(admin_user_id), + role_id: Set(admin_role_id), + tenant_id: Set(tenant_id), + created_at: Set(now), + updated_at: Set(now), + created_by: Set(system_user_id), + updated_by: Set(system_user_id), + deleted_at: Set(None), + version: Set(1), + }; + user_role_assignment.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; + + tracing::info!( + tenant_id = %tenant_id, + admin_user_id = %admin_user_id, + "Seeded tenant auth: admin user, 2 roles, {} permissions", + DEFAULT_PERMISSIONS.len() + ); + + Ok(()) +} diff --git a/crates/erp-server/config/default.toml b/crates/erp-server/config/default.toml index 29d8d1e..601ccec 100644 --- a/crates/erp-server/config/default.toml +++ b/crates/erp-server/config/default.toml @@ -15,5 +15,8 @@ secret = "change-me-in-production" access_token_ttl = "15m" refresh_token_ttl = "7d" +[auth] +super_admin_password = "Admin@2026" + [log] level = "info" diff --git a/crates/erp-server/src/config.rs b/crates/erp-server/src/config.rs index be91477..bc6629e 100644 --- a/crates/erp-server/src/config.rs +++ b/crates/erp-server/src/config.rs @@ -6,6 +6,7 @@ pub struct AppConfig { pub database: DatabaseConfig, pub redis: RedisConfig, pub jwt: JwtConfig, + pub auth: AuthConfig, pub log: LogConfig, } @@ -39,6 +40,11 @@ pub struct LogConfig { pub level: String, } +#[derive(Debug, Clone, Deserialize)] +pub struct AuthConfig { + pub super_admin_password: String, +} + impl AppConfig { pub fn load() -> anyhow::Result { let config = config::Config::builder() diff --git a/crates/erp-server/src/main.rs b/crates/erp-server/src/main.rs index 77f0c8c..e9534a8 100644 --- a/crates/erp-server/src/main.rs +++ b/crates/erp-server/src/main.rs @@ -13,6 +13,7 @@ use tracing_subscriber::EnvFilter; use erp_core::events::EventBus; use erp_core::module::{ErpModule, ModuleRegistry}; use erp_server_migration::MigratorTrait; +use sea_orm::{ConnectionTrait, FromQueryResult}; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -37,6 +38,58 @@ async fn main() -> anyhow::Result<()> { erp_server_migration::Migrator::up(&db, None).await?; tracing::info!("Database migrations applied"); + // Seed default tenant and auth data if not present + { + #[derive(sea_orm::FromQueryResult)] + struct TenantId { + id: uuid::Uuid, + } + + let existing = TenantId::find_by_statement(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + "SELECT id FROM tenants WHERE deleted_at IS NULL LIMIT 1".to_string(), + )) + .one(&db) + .await + .map_err(|e| anyhow::anyhow!("Failed to query tenants: {}", e))?; + + match existing { + Some(row) => { + tracing::info!(tenant_id = %row.id, "Default tenant already exists, skipping seed"); + } + None => { + let new_tenant_id = uuid::Uuid::now_v7(); + + // Insert default tenant using raw SQL (no tenant entity in erp-server) + db.execute(sea_orm::Statement::from_sql_and_values( + sea_orm::DatabaseBackend::Postgres, + "INSERT INTO tenants (id, name, code, status, created_at, updated_at) VALUES ($1, $2, $3, $4, NOW(), NOW())", + [ + new_tenant_id.into(), + "Default Tenant".into(), + "default".into(), + "active".into(), + ], + )) + .await + .map_err(|e| anyhow::anyhow!("Failed to create default tenant: {}", e))?; + + tracing::info!(tenant_id = %new_tenant_id, "Created default tenant"); + + // Seed auth data (permissions, roles, admin user) + erp_auth::service::seed::seed_tenant_auth( + &db, + new_tenant_id, + &config.auth.super_admin_password, + ) + .await + .map_err(|e| anyhow::anyhow!("Failed to seed auth data: {}", e))?; + + tracing::info!(tenant_id = %new_tenant_id, "Default tenant ready with auth seed data"); + } + } + } + // Connect to Redis let _redis_client = redis::Client::open(&config.redis.url[..])?; tracing::info!("Redis client created");