diff --git a/Cargo.lock b/Cargo.lock index 1375ff6..73ff75f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1311,7 +1311,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "picloud" -version = "0.5.1" +version = "0.6.0" dependencies = [ "anyhow", "async-trait", @@ -1335,7 +1335,7 @@ dependencies = [ [[package]] name = "picloud-executor" -version = "0.5.1" +version = "0.6.0" dependencies = [ "anyhow", "picloud-executor-core", @@ -1347,7 +1347,7 @@ dependencies = [ [[package]] name = "picloud-executor-core" -version = "0.5.1" +version = "0.6.0" dependencies = [ "chrono", "picloud-shared", @@ -1361,7 +1361,7 @@ dependencies = [ [[package]] name = "picloud-manager" -version = "0.5.1" +version = "0.6.0" dependencies = [ "anyhow", "picloud-manager-core", @@ -1373,7 +1373,7 @@ dependencies = [ [[package]] name = "picloud-manager-core" -version = "0.5.1" +version = "0.6.0" dependencies = [ "argon2", "async-trait", @@ -1397,7 +1397,7 @@ dependencies = [ [[package]] name = "picloud-orchestrator" -version = "0.5.1" +version = "0.6.0" dependencies = [ "anyhow", "picloud-orchestrator-core", @@ -1409,7 +1409,7 @@ dependencies = [ [[package]] name = "picloud-orchestrator-core" -version = "0.5.1" +version = "0.6.0" dependencies = [ "async-trait", "axum", @@ -1428,7 +1428,7 @@ dependencies = [ [[package]] name = "picloud-shared" -version = "0.5.1" +version = "0.6.0" dependencies = [ "async-trait", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 7952bb0..d02b263 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ ] [workspace.package] -version = "0.5.1" +version = "0.6.0" edition = "2021" rust-version = "1.92" license = "MIT OR Apache-2.0" diff --git a/crates/picloud/src/main.rs b/crates/picloud/src/main.rs index 64fa70f..8e675af 100644 --- a/crates/picloud/src/main.rs +++ b/crates/picloud/src/main.rs @@ -45,6 +45,7 @@ async fn run_server() -> anyhow::Result<()> { let auth = AuthDeps::from_pool(pool.clone()); bootstrap_first_admin(&*auth.users).await?; + warn_on_multi_owner_install(&*auth.users).await; // Seed Hello World into the default app when this is a fresh // install (no scripts and no routes). Idempotent on upgrades. @@ -79,6 +80,31 @@ async fn run_server() -> anyhow::Result<()> { Ok(()) } +/// Multi-owner startup warning — Phase 3.5 migration upgraded every +/// pre-existing admin_users row to `Owner` via DEFAULT, which for +/// installs with several Phase 3a admins means several co-owners. +/// Surface this once at boot so the operator can demote extras via +/// `PATCH /api/v1/admin/admins/{id}` with `instance_role: "admin"`. +/// Soft-fail: a DB blip should not block startup. +async fn warn_on_multi_owner_install(users: &dyn AdminUserRepository) { + match users.list_active_owners().await { + Ok(owners) if owners.len() > 1 => { + let names: Vec = owners.into_iter().map(|u| u.username).collect(); + tracing::warn!( + count = names.len(), + owners = ?names, + "multiple active owners detected — Phase 3.5 promoted every \ + pre-existing admin to owner. Demote extras via \ + PATCH /api/v1/admin/admins/{{id}} with instance_role." + ); + } + Ok(_) => {} + Err(err) => { + tracing::warn!(?err, "could not count active owners for multi-owner startup check"); + } + } +} + fn spawn_session_pruner(sessions: Arc) { tokio::spawn(async move { let mut ticker = tokio::time::interval(Duration::from_secs(600)); diff --git a/crates/picloud/tests/api.rs b/crates/picloud/tests/api.rs index cb4b75a..d183dbe 100644 --- a/crates/picloud/tests/api.rs +++ b/crates/picloud/tests/api.rs @@ -822,7 +822,7 @@ async fn version_includes_public_base_url(pool: PgPool) { let v: Value = r.json(); assert!(v["public_base_url"].is_string()); assert_eq!(v["api"], 1); - assert_eq!(v["schema"], 5); + assert_eq!(v["schema"], 6); assert_eq!(v["sdk"], "1.1"); } diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index c77c214..7f738a4 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -1,12 +1,12 @@ { "name": "picloud-dashboard", - "version": "0.5.1", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "picloud-dashboard", - "version": "0.5.1", + "version": "0.6.0", "dependencies": { "@codemirror/autocomplete": "^6.20.2", "@codemirror/commands": "^6.10.3", diff --git a/dashboard/package.json b/dashboard/package.json index 92ac4c5..4b0b515 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "picloud-dashboard", - "version": "0.5.1", + "version": "0.6.0", "private": true, "type": "module", "scripts": { diff --git a/docs/versioning.md b/docs/versioning.md index 94d2607..5efba07 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -126,10 +126,10 @@ A surface can hit its own `1.0` independently of the product. The SDK in particu | | Version | |---|---| -| Product | `0.5.1` | +| Product | `0.6.0` | | SDK | `1.1` (adds `ctx.request.params`, `ctx.request.query`, `ctx.request.rest`) | -| API | `1` (additive: `Script.app_id`, `Route.app_id`, `ExecutionLog.app_id`, new `/api/v1/admin/apps/*` endpoints, `?app=` filter on script list) | -| Schema | `5` (matches `migrations/0005_apps.sql`) | +| API | `1` (additive: `Script.app_id`, `Route.app_id`, `ExecutionLog.app_id`, new `/api/v1/admin/apps/*` and `/api/v1/admin/api-keys/*` endpoints, `?app=` filter on script list, `Authorization: Bearer pic_…` credential type, 403 responses on previously-401-only admin endpoints when the caller lacks the required capability) | +| Schema | `6` (matches `migrations/0006_users_authz.sql`) | | Wire | `1` (reserved; cluster mode not implemented) | Read live from `GET /version` on any running instance.