This commit is contained in:
Ember Moth
2026-07-05 20:27:58 +08:00
commit 2744c70c5c
837 changed files with 53059 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
//! 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
// ═══════════════════════════════════════════════════════════════════════════