Skip to main content

gize_db/
admin.rs

1//! Creating the first admin user (`gize createadmin`, ADR-017).
2//!
3//! Connects with SQLx's `AnyPool` (like [`crate::migrate`]) so one code path serves Postgres,
4//! SQLite and MySQL. Because the `Any` driver passes SQL through unchanged, the `INSERT` uses
5//! the dialect's own placeholder style and id strategy — and binds the id as raw bytes so the
6//! stored value is byte-for-byte what the generated app reads back for its `uuid::Uuid` id.
7
8use anyhow::{Context, Result, bail};
9use gize_core::Dialect;
10
11use crate::migrate::{connect, runtime};
12
13/// Insert an admin user (`is_admin = true`) into the `users` table. `password_hash` must already
14/// be an Argon2 PHC string (see `gize-auth`). Rejects a duplicate email up front and reports a
15/// missing `users` table with guidance to migrate first.
16pub fn create(
17    database_url: &str,
18    dialect: Dialect,
19    email: &str,
20    name: &str,
21    password_hash: &str,
22) -> Result<()> {
23    runtime()?.block_on(async {
24        let pool = connect(database_url).await?;
25
26        // Fail early (and clearly) if the schema has not been migrated yet.
27        let existing = sqlx::query(&format!(
28            "SELECT id FROM users WHERE email = {} LIMIT 1",
29            dialect.placeholder(1)
30        ))
31        .bind(email.to_string())
32        .fetch_optional(&pool)
33        .await
34        .context("querying the `users` table — does it exist? run `gize migrate` first")?;
35        if existing.is_some() {
36            bail!("an account with email `{email}` already exists");
37        }
38
39        // Postgres lets the `id` column default generate the UUID; SQLite/MySQL take the
40        // app-generated id as its 16 raw bytes, which `Any` stores as a BLOB / `BINARY(16)`.
41        let app_id = dialect.app_generates_id();
42        let columns = if app_id {
43            "id, name, email, password, is_admin"
44        } else {
45            "name, email, password, is_admin"
46        };
47        let count = if app_id { 5 } else { 4 };
48        let placeholders = (1..=count)
49            .map(|i| dialect.placeholder(i))
50            .collect::<Vec<_>>()
51            .join(", ");
52        let sql = format!("INSERT INTO users ({columns}) VALUES ({placeholders})");
53
54        let mut query = sqlx::query(&sql);
55        if app_id {
56            query = query.bind(uuid::Uuid::new_v4().as_bytes().to_vec());
57        }
58        query
59            .bind(name.to_string())
60            .bind(email.to_string())
61            .bind(password_hash.to_string())
62            .bind(true)
63            .execute(&pool)
64            .await
65            .context("inserting the admin user")?;
66
67        Ok(())
68    })
69}