//! First-owner bootstrap for self-hosted (local-auth) deployments. //! //! There is no public signup route — a Clawmates workspace is invite-only //! once it exists. But a brand-new install has nobody to invite anyone, //! so the first Owner is provisioned from configuration on first boot. //! The operation keys on "does ANY workspace exist yet", so it runs //! exactly once no matter how many times the server restarts. use cm_domain::{Role, User, UserId, Workspace, WorkspaceId}; use crate::{AuthError, AuthService}; /// Provisions the initial workspace + Owner if the install is empty. /// Returns `true` when it created them, `false` when a workspace already /// existed (the common restart case). Never modifies an existing install. pub async fn bootstrap_owner( pool: &sqlx::PgPool, workspace_name: &str, owner_email: &str, owner_password: &str, starter_credits: i64, ) -> Result { let existing: i64 = sqlx::query_scalar("SELECT count(*) FROM workspaces") .fetch_one(pool) .await?; if existing > 0 { return Ok(false); } let workspace = Workspace { id: WorkspaceId::new(), name: workspace_name.to_owned(), plan: "team".into(), }; cm_db::repo::workspaces::insert(pool, &workspace) .await .map_err(map_db)?; let owner = User { id: UserId::new(), workspace_id: workspace.id, email: owner_email.to_owned(), role: Role::Owner, display_name: owner_email.split('@').next().unwrap_or("owner").to_owned(), created_at: time::OffsetDateTime::now_utc(), }; cm_db::repo::users::insert(pool, &owner) .await .map_err(map_db)?; AuthService::new(pool.clone()) .set_password(owner.id, owner_password) .await?; if starter_credits > 0 { cm_db::repo::credits::add_lot(pool, workspace.id, starter_credits, "bootstrap") .await .map_err(map_db)?; } Ok(true) } fn map_db(error: cm_db::DbError) -> AuthError { match error { cm_db::DbError::Other(e) => AuthError::Db(e), other => AuthError::Hashing(other.to_string()), } }