Skip to main content

isb_server/auth/
setup.rs

1//! First-run setup in the store: the first user is a platform admin and
2//! the owner of the `default` org, made once, on the host, with the setup
3//! token, or from an edge identity ([`super::edge`]).
4
5use rusqlite::{TransactionBehavior, params};
6
7use super::{
8    AuthError, AuthResult, AuthStore, User, clean_name, ensure_org_tx, external, normalize_email,
9};
10use crate::org::OrgId;
11
12impl AuthStore {
13    /// True until the first user exists.
14    pub fn setup_needed(&self) -> AuthResult<bool> {
15        let n: i64 = self
16            .db()
17            .query_row("SELECT COUNT(*) FROM users", [], |r| r.get(0))?;
18        Ok(n == 0)
19    }
20
21    /// Create the first user: a platform admin and owner of the `default`
22    /// org. Refused once any user exists.
23    pub fn create_first_admin(&self, email: &str, name: &str, password: &str) -> AuthResult<User> {
24        self.first_admin(email, name, Some(password), None)
25    }
26
27    /// The first admin from an edge identity ([`super::edge`]), linked to it so it
28    /// signs them in from then on. The password is optional: the edge is a
29    /// way in, and `isb user passwd` on the host is the way back.
30    pub fn claim_first_admin(
31        &self,
32        email: &str,
33        name: &str,
34        password: Option<&str>,
35        link: &external::ExternalIdentity,
36    ) -> AuthResult<User> {
37        self.first_admin(email, name, password, Some(link))
38    }
39
40    fn first_admin(
41        &self,
42        email: &str,
43        name: &str,
44        password: Option<&str>,
45        link: Option<&external::ExternalIdentity>,
46    ) -> AuthResult<User> {
47        let email = normalize_email(email)?;
48        let name = clean_name(name)?;
49        let hash = password.map(|p| self.hash_pw(p)).transpose()?;
50        let now = self.now();
51        let mut db = self.db();
52        let tx = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
53        let n: i64 = tx.query_row("SELECT COUNT(*) FROM users", [], |r| r.get(0))?;
54        if n > 0 {
55            return Err(AuthError::Conflict("setup is already done".into()));
56        }
57        tx.execute(
58            "INSERT INTO users (email, name, password_hash, platform_admin, created_at)
59             VALUES (?1, ?2, ?3, 1, ?4)",
60            params![email, name, hash, now],
61        )?;
62        let id = tx.last_insert_rowid();
63        if let Some(ext) = link {
64            let verified = ext.email_verified
65                && ext
66                    .email
67                    .as_deref()
68                    .is_some_and(|e| e.eq_ignore_ascii_case(&email));
69            let shown = ext.email.as_deref().and_then(|e| normalize_email(e).ok());
70            external::insert_identity(&tx, id, ext, shown.as_deref(), verified, now)?;
71        }
72        let org = OrgId::default_org();
73        ensure_org_tx(&tx, &org, now)?;
74        tx.execute(
75            "INSERT INTO memberships (user_id, org, role, created_at) VALUES (?1, ?2, 'owner', ?3)",
76            params![id, org.as_str(), now],
77        )?;
78        tx.commit()?;
79        drop(db);
80        self.user(id)
81    }
82}