mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-08-29 05:52:08 -04:00
195 lines
6.5 KiB
Rust
195 lines
6.5 KiB
Rust
//! Database migration runner and bootstrap utilities.
|
|
//!
|
|
//! Embeds both MySQL and PostgreSQL migration files at compile time via
|
|
//! `sqlx::migrate!` and selects the correct set at runtime based on the
|
|
//! [`Dialect`] detected from configuration.
|
|
//!
|
|
//! **NOTE**: This is the Rust rewrite of the Go backend. The Go version is
|
|
//! deprecated and will be replaced by this Rust implementation.
|
|
|
|
mod mysql_migrations {
|
|
#![allow(unused)]
|
|
pub(super) const MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("migrations/mysql");
|
|
}
|
|
|
|
mod postgres_migrations {
|
|
#![allow(unused)]
|
|
pub(super) const MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("migrations/postgres");
|
|
}
|
|
|
|
pub use crate::repository::Dialect;
|
|
|
|
use crate::repository::Db;
|
|
|
|
/// Run all pending migrations for the detected database dialect.
|
|
pub async fn run_migrations(db: &Db) -> Result<(), sqlx::migrate::MigrateError> {
|
|
match db {
|
|
Db::Postgres(pool) => postgres_migrations::MIGRATOR.run(pool).await,
|
|
Db::Mysql(pool) => mysql_migrations::MIGRATOR.run(pool).await,
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Bootstrap: initial admin account
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
pub async fn create_admin_user(
|
|
db: &Db,
|
|
email: &str,
|
|
password: &str,
|
|
) -> Result<(), sqlx::Error> {
|
|
match db {
|
|
Db::Postgres(pool) => create_admin_user_pg(pool, email, password).await,
|
|
Db::Mysql(pool) => create_admin_user_mysql(pool, email, password).await,
|
|
}
|
|
}
|
|
|
|
async fn create_admin_user_pg(
|
|
pool: &sqlx::PgPool,
|
|
email: &str,
|
|
password: &str,
|
|
) -> Result<(), sqlx::Error> {
|
|
let exists: bool =
|
|
sqlx::query_scalar(r#"SELECT EXISTS(SELECT 1 FROM "user")"#)
|
|
.fetch_one(pool)
|
|
.await?;
|
|
|
|
if exists {
|
|
tracing::info!("User already exists, skip creating administrator account");
|
|
return Ok(());
|
|
}
|
|
|
|
let now = chrono::Utc::now().timestamp_millis();
|
|
let password_hash = hash_password_pbkdf2(password);
|
|
let refer_code = generate_invite_code();
|
|
|
|
sqlx::query(
|
|
r#"INSERT INTO "user" (password, algo, is_admin, refer_code, balance, commission,
|
|
gift_amount, enable, enable_balance_notify,
|
|
enable_login_notify, enable_subscribe_notify,
|
|
enable_trade_notify, created_at, updated_at)
|
|
VALUES ($1, 'default', 1, $2, 0, 0, 0, 1, 1, 1, 1, 1, $3, $4)"#,
|
|
)
|
|
.bind(&password_hash)
|
|
.bind(&refer_code)
|
|
.bind(now)
|
|
.bind(now)
|
|
.execute(pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Failed to create admin user: {e}");
|
|
e
|
|
})?;
|
|
|
|
let user_id: i64 =
|
|
sqlx::query_scalar(r#"SELECT id FROM "user" WHERE refer_code = $1"#)
|
|
.bind(&refer_code)
|
|
.fetch_one(pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Failed to fetch admin user id: {e}");
|
|
e
|
|
})?;
|
|
|
|
sqlx::query(
|
|
r#"INSERT INTO user_auth_methods (user_id, auth_type, auth_identifier, verified, created_at, updated_at)
|
|
VALUES ($1, 'email', $2, 1, $3, $4)"#,
|
|
)
|
|
.bind(user_id)
|
|
.bind(email)
|
|
.bind(now)
|
|
.bind(now)
|
|
.execute(pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Failed to create admin auth method: {e}");
|
|
e
|
|
})?;
|
|
|
|
tracing::info!("Administrator account created: {email}");
|
|
Ok(())
|
|
}
|
|
|
|
async fn create_admin_user_mysql(
|
|
pool: &sqlx::MySqlPool,
|
|
email: &str,
|
|
password: &str,
|
|
) -> Result<(), sqlx::Error> {
|
|
let exists: bool =
|
|
sqlx::query_scalar(r#"SELECT EXISTS(SELECT 1 FROM `user`)"#)
|
|
.fetch_one(pool)
|
|
.await?;
|
|
|
|
if exists {
|
|
tracing::info!("User already exists, skip creating administrator account");
|
|
return Ok(());
|
|
}
|
|
|
|
let now = chrono::Utc::now().timestamp_millis();
|
|
let password_hash = hash_password_pbkdf2(password);
|
|
let refer_code = generate_invite_code();
|
|
|
|
sqlx::query(
|
|
r#"INSERT INTO `user` (password, algo, is_admin, refer_code, balance, commission,
|
|
gift_amount, enable, enable_balance_notify,
|
|
enable_login_notify, enable_subscribe_notify,
|
|
enable_trade_notify, created_at, updated_at)
|
|
VALUES (?, 'default', 1, ?, 0, 0, 0, 1, 1, 1, 1, 1, ?, ?)"#,
|
|
)
|
|
.bind(&password_hash)
|
|
.bind(&refer_code)
|
|
.bind(now)
|
|
.bind(now)
|
|
.execute(pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Failed to create admin user: {e}");
|
|
e
|
|
})?;
|
|
|
|
let user_id: i64 =
|
|
sqlx::query_scalar(r#"SELECT id FROM `user` WHERE refer_code = ?"#)
|
|
.bind(&refer_code)
|
|
.fetch_one(pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Failed to fetch admin user id: {e}");
|
|
e
|
|
})?;
|
|
|
|
sqlx::query(
|
|
r#"INSERT INTO user_auth_methods (user_id, auth_type, auth_identifier, verified, created_at, updated_at)
|
|
VALUES (?, 'email', ?, 1, ?, ?)"#,
|
|
)
|
|
.bind(user_id)
|
|
.bind(email)
|
|
.bind(now)
|
|
.bind(now)
|
|
.execute(pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Failed to create admin auth method: {e}");
|
|
e
|
|
})?;
|
|
|
|
tracing::info!("Administrator account created: {email}");
|
|
Ok(())
|
|
}
|
|
|
|
/// PBKDF2-SHA512 matching Go's format: `$pbkdf2-sha512${salt_hex}${hash_hex}`
|
|
///
|
|
/// **Iteration count is intentionally 100** to stay byte-compatible with the
|
|
/// Go original. See `AGENTS.md` for rationale.
|
|
fn hash_password_pbkdf2(password: &str) -> String {
|
|
password::encode_password(password).expect("Failed to encode password")
|
|
}
|
|
|
|
fn generate_invite_code() -> String {
|
|
format!("u{}", &uuid::Uuid::new_v4().to_string().replace('-', "")[..12])
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Bootstrap: initial admin account
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|