From 01a35f7ba600a40913dd1f9a3fccafe27bc836a2 Mon Sep 17 00:00:00 2001 From: Oleg Lukasonok Date: Sun, 8 Nov 2026 19:34:48 +0200 Subject: [PATCH] Single sign-on servers need no setup step With single sign-on there is no first-start code and no /setup form: the identity provider says who may sign in and who is admin, and the first allowed sign-in makes the organisation, named after ARCHIFY_SYNC_NAME. Server 0.1.1, chart 0.1.2. --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- README.md | 4 ++-- charts/archify-sync/Chart.yaml | 4 ++-- crates/server/src/pages.rs | 9 ++++++--- crates/server/src/state.rs | 4 +++- crates/store/src/lib.rs | 28 ++++++++++++++++++++++++++++ 7 files changed, 45 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd288f3..9ce9727 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,7 +43,7 @@ dependencies = [ [[package]] name = "archify-sync-protocol" -version = "0.1.0" +version = "0.1.1" dependencies = [ "chrono", "serde", @@ -53,7 +53,7 @@ dependencies = [ [[package]] name = "archify-sync-server" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "archify-sync-protocol", @@ -83,7 +83,7 @@ dependencies = [ [[package]] name = "archify-sync-store" -version = "0.1.0" +version = "0.1.1" dependencies = [ "chrono", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index d7bcd69..93ba85d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["crates/protocol", "crates/store", "crates/server"] [workspace.package] -version = "0.1.0" +version = "0.1.1" edition = "2024" license = "MIT" repository = "https://gitea.lego-cloud.eu/vscode-extensions/archify-vscode-ext-sync" diff --git a/README.md b/README.md index a0e755f..db1361a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ One Rust process and PostgreSQL. The design, with its decisions, is the Archify - **Restore:** brings an earlier version back as a new edit and a new version; history is never rewritten. - **Update log:** taking a version compacts it. - **Sign-in.** The editor opens `/authorize` in the browser and gets a one-time code back through its URI handler. It exchanges the code with its PKCE verifier for an access token (JWT, 15 minutes) and a refresh token. The refresh token is single use: reusing one signs out the whole session. -- **First start.** A server without accounts prints a first-start code in its log. The first person to use it at `/setup` becomes the admin. +- **First start.** With single sign-on there is no setup step: the identity provider says who may sign in and who is admin, and the first sign-in makes the organisation (named after `ARCHIFY_SYNC_NAME`). Without single sign-on, a server without accounts prints a first-start code in its log; the first person to use it at `/setup` becomes the admin. - **Invites.** An invite link (`/join/`) opens the extension. Someone new makes an account on the sign-in page, so no mail server is needed. ## Layout @@ -36,7 +36,7 @@ docker run -d --name archify-pg -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=archify ARCHIFY_SYNC_DATABASE_URL=postgres://postgres:dev@127.0.0.1:5432/archify cargo run -p archify-sync-server ``` -The log shows the first-start code. Open `http://localhost:8080/setup` and enter it. +Without single sign-on, the log shows the first-start code. Open `http://localhost:8080/setup` and enter it. Or the server and its database in containers: diff --git a/charts/archify-sync/Chart.yaml b/charts/archify-sync/Chart.yaml index c8b519b..a667de4 100644 --- a/charts/archify-sync/Chart.yaml +++ b/charts/archify-sync/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: archify-sync description: Archify Sync, live and shared Archify models for VS Code type: application -version: 0.1.1 -appVersion: "0.1.0" +version: 0.1.2 +appVersion: "0.1.1" annotations: artifacthub.io/license: MIT sources: diff --git a/crates/server/src/pages.rs b/crates/server/src/pages.rs index 376c815..3166c87 100644 --- a/crates/server/src/pages.rs +++ b/crates/server/src/pages.rs @@ -531,9 +531,12 @@ async fn sso_account( .invite_organisation(&auth::sha256(invite)) .await? .map(|(org, _)| org), - None if !config.allowed_groups.is_empty() || admin => { - state.store.first_organisation().await? - } + None if !config.allowed_groups.is_empty() || admin => Some( + state + .store + .organisation_or_create(&state.config.server_name) + .await?, + ), None => None, }; let Some(organisation) = organisation else { diff --git a/crates/server/src/state.rs b/crates/server/src/state.rs index c04c344..cedb1fd 100644 --- a/crates/server/src/state.rs +++ b/crates/server/src/state.rs @@ -42,7 +42,9 @@ impl AppState { let secret = STANDARD.decode(stored.as_str().unwrap_or_default())?; anyhow::ensure!(secret.len() >= 32, "the stored signing key is too short"); - let first_start = if store.account_count().await? == 0 { + // With single sign-on the identity provider says who is admin; there + // is no first-start code and no setup page. + let first_start = if config.oidc.is_none() && store.account_count().await? == 0 { let code = auth::first_start_code(); tracing::warn!( "first start: no accounts yet. Set up the first admin at {}/setup with code {code}", diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 917c082..28cfefc 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -313,6 +313,34 @@ impl Store { Ok(()) } + /// The server's organisation, made with `name` when there is none yet + /// (a single sign-on server has no setup step). + pub async fn organisation_or_create(&self, name: &str) -> Result { + let mut tx = self.pool.begin().await?; + // Serialises the first sign-ins, so only one organisation is made. + sqlx::query("LOCK TABLE organisations IN EXCLUSIVE MODE") + .execute(&mut *tx) + .await?; + let found: Option = + sqlx::query_scalar("SELECT id FROM organisations ORDER BY created_at LIMIT 1") + .fetch_optional(&mut *tx) + .await?; + let id = match found { + Some(id) => id, + None => { + let id = Uuid::now_v7(); + sqlx::query("INSERT INTO organisations (id, name) VALUES ($1, $2)") + .bind(id) + .bind(name) + .execute(&mut *tx) + .await?; + id + } + }; + tx.commit().await?; + Ok(id) + } + /// The server's organisation: the one made at setup. pub async fn first_organisation(&self) -> Result> { Ok(