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]>
71 lines
2.3 KiB
Rust
71 lines
2.3 KiB
Rust
//! Production first-owner bootstrap: a fresh local-auth deployment has no
|
|
//! users and no signup route, so the very first owner is provisioned once
|
|
//! from configuration. Idempotent — never clobbers an existing install.
|
|
|
|
use cm_auth::{bootstrap_owner, AuthService, AuthedUser};
|
|
use cm_domain::Role;
|
|
|
|
#[tokio::test]
|
|
async fn bootstrap_creates_the_first_owner_and_workspace_once() {
|
|
let pool = cm_testkit::test_pool().await;
|
|
|
|
// First boot: provisions the workspace + owner with a usable password.
|
|
let created = bootstrap_owner(&pool, "Acme", "[email protected]", "s3cret-pw", 1250)
|
|
.await
|
|
.unwrap();
|
|
assert!(created, "first call provisions");
|
|
|
|
let auth = AuthService::new(pool.clone());
|
|
let token = auth
|
|
.login_local("[email protected]", "s3cret-pw")
|
|
.await
|
|
.expect("the bootstrapped owner can sign in");
|
|
let AuthedUser { role, .. } = auth.authenticate(token.secret()).await.unwrap();
|
|
assert_eq!(role, Role::Owner);
|
|
|
|
// The starter credit grant landed.
|
|
let user = cm_db::repo::users::find_by_email(&pool, "[email protected]")
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
cm_db::repo::credits::balance(&pool, user.workspace_id)
|
|
.await
|
|
.unwrap(),
|
|
1250
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn bootstrap_is_idempotent_and_never_clobbers_an_existing_install() {
|
|
let pool = cm_testkit::test_pool().await;
|
|
assert!(
|
|
bootstrap_owner(&pool, "Acme", "[email protected]", "pw-one", 100)
|
|
.await
|
|
.unwrap()
|
|
);
|
|
|
|
// Re-running (e.g. every container restart) is a no-op: no second
|
|
// workspace, no password reset, no duplicate owner.
|
|
let created = bootstrap_owner(&pool, "Acme", "[email protected]", "pw-two", 100)
|
|
.await
|
|
.unwrap();
|
|
assert!(!created, "second call is a no-op");
|
|
|
|
let auth = AuthService::new(pool.clone());
|
|
assert!(
|
|
auth.login_local("[email protected]", "pw-two")
|
|
.await
|
|
.is_err(),
|
|
"a second owner is never created"
|
|
);
|
|
assert!(
|
|
auth.login_local("[email protected]", "pw-one").await.is_ok(),
|
|
"the original owner is untouched"
|
|
);
|
|
let workspaces: i64 = sqlx::query_scalar("SELECT count(*) FROM workspaces")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(workspaces, 1);
|
|
}
|