mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-08-29 05:52:08 -04:00
Initial
This commit is contained in:
@@ -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
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user