Made the Docker Compose route turn-key for a real self-host, then stood the whole stack up and drove a live chat through it. - First-owner bootstrap (cm-auth::bootstrap_owner): a fresh local-auth install has no users and no signup route, so the initial Owner + workspace are provisioned ONCE from CLAWMATES_BOOTSTRAP_* env on first boot — idempotent, never clobbers an existing install (keys on 'any workspace exists'). Two real-Postgres tests (creates + signs in; second call is a no-op). Wired into server boot, guarded on a non-empty password - deploy/compose/README.md: full production bring-up — services, the security topology, every config knob, Anthropic vs local-LLM, the broker-key backup, ops, and TLS/SSE proxy notes - .env.example fleshed out (bootstrap, LLM, auth mode, OTLP); compose uses optional env_file so only the knobs you set are injected (unset options never override clawmates.toml with empty strings) - volume-init one-shot chowns the broker's named volumes so the non-root scratch broker can write its socket + generated master key Deployed locally and verified end to end: all 5 containers healthy, broker generated its key, server bootstrapped owner@…, login + /api/user/me work, and a real message streamed a live Anthropic response through the gateway. Captured screenshots of login, workspace home, chat, and the Computer panel. 166 Rust tests (+2 bootstrap). Co-Authored-By: Claude Fable 5 <[email protected]>
68 lines
2.2 KiB
Rust
68 lines
2.2 KiB
Rust
//! 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<bool, AuthError> {
|
|
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()),
|
|
}
|
|
}
|