feat(auth): add tenant seed data and bootstrap logic
- seed.rs: creates 21 permissions, admin+viewer roles, admin user with Argon2 password - AuthConfig added to server config with default password Admin@2026 - Server startup: auto-creates default tenant and seeds auth data if not exists - Idempotent: checks for existing tenant before seeding
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
pub mod auth_service;
|
pub mod auth_service;
|
||||||
pub mod password;
|
pub mod password;
|
||||||
|
pub mod seed;
|
||||||
pub mod token_service;
|
pub mod token_service;
|
||||||
pub mod user_service;
|
pub mod user_service;
|
||||||
|
|||||||
265
crates/erp-auth/src/service/seed.rs
Normal file
265
crates/erp-auth/src/service/seed.rs
Normal file
@@ -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<Uuid> = 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(())
|
||||||
|
}
|
||||||
@@ -15,5 +15,8 @@ secret = "change-me-in-production"
|
|||||||
access_token_ttl = "15m"
|
access_token_ttl = "15m"
|
||||||
refresh_token_ttl = "7d"
|
refresh_token_ttl = "7d"
|
||||||
|
|
||||||
|
[auth]
|
||||||
|
super_admin_password = "Admin@2026"
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
level = "info"
|
level = "info"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ pub struct AppConfig {
|
|||||||
pub database: DatabaseConfig,
|
pub database: DatabaseConfig,
|
||||||
pub redis: RedisConfig,
|
pub redis: RedisConfig,
|
||||||
pub jwt: JwtConfig,
|
pub jwt: JwtConfig,
|
||||||
|
pub auth: AuthConfig,
|
||||||
pub log: LogConfig,
|
pub log: LogConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +40,11 @@ pub struct LogConfig {
|
|||||||
pub level: String,
|
pub level: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct AuthConfig {
|
||||||
|
pub super_admin_password: String,
|
||||||
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
pub fn load() -> anyhow::Result<Self> {
|
pub fn load() -> anyhow::Result<Self> {
|
||||||
let config = config::Config::builder()
|
let config = config::Config::builder()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use tracing_subscriber::EnvFilter;
|
|||||||
use erp_core::events::EventBus;
|
use erp_core::events::EventBus;
|
||||||
use erp_core::module::{ErpModule, ModuleRegistry};
|
use erp_core::module::{ErpModule, ModuleRegistry};
|
||||||
use erp_server_migration::MigratorTrait;
|
use erp_server_migration::MigratorTrait;
|
||||||
|
use sea_orm::{ConnectionTrait, FromQueryResult};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
@@ -37,6 +38,58 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
erp_server_migration::Migrator::up(&db, None).await?;
|
erp_server_migration::Migrator::up(&db, None).await?;
|
||||||
tracing::info!("Database migrations applied");
|
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
|
// Connect to Redis
|
||||||
let _redis_client = redis::Client::open(&config.redis.url[..])?;
|
let _redis_client = redis::Client::open(&config.redis.url[..])?;
|
||||||
tracing::info!("Redis client created");
|
tracing::info!("Redis client created");
|
||||||
|
|||||||
Reference in New Issue
Block a user