From d9718d80c950c3d7c2fdddc5333a8bb1ff51c01c Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Thu, 3 Sep 2026 15:14:41 +0200 Subject: [PATCH] refactor: create the workspace in onboarding rather than at signup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signup no longer makes a personal workspace, so the last onboarding step creates one instead of renaming it — the same one-field `SimpleCreateWorkspace` the workspace picker falls back to, so a user who leaves onboarding early meets the form again rather than something new. The id now comes from the name they type rather than from their email, and there is one creation path instead of two. `insert_workspace` goes back to being private: the extraction existed only so the EE signup path could call it, and nothing outside `create_workspace` does now. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 --- backend/ee-repo-ref.txt | 2 +- .../windmill-api-workspaces/src/workspaces.rs | 229 ++++++++---------- .../user/(user)/onboarding/+page.svelte | 164 +++---------- 3 files changed, 146 insertions(+), 249 deletions(-) diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index bda6c38646..eaaec7f38c 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -f65a124b024852b329b4a4c0dd036d3004f7e02d +e1851154b61494928ba698d58ba7a50a8f4e991f diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index 6eafcd8e3a..2aa59b20ff 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -5574,121 +5574,6 @@ async fn _check_nb_of_archived_workspaces(db: &DB) -> Result<()> { return Ok(()); } -/// Insert a workspace and everything it needs to be usable: settings, encryption key, -/// the owner as its admin, and the `all` / `wm_deployers` groups. Returns the owner's -/// username in the new workspace. -/// -/// The caller owns the transaction and the audit log: the signup path creates a -/// workspace before the user has an `ApiAuthed` to audit under. -/// -/// It owns the authorization too. `CREATE_WORKSPACE_REQUIRE_SUPERADMIN`, the OSS workspace -/// count and the cloud cap of ten per owner all stay in the `create_workspace` handler — -/// this writes the rows for a workspace someone has already decided may exist. -pub async fn insert_workspace<'c>( - tx: &mut Transaction<'c, Postgres>, - id: &str, - name: &str, - owner_email: &str, - color: Option<&str>, - error_handler_fallback_to_instance_alerts: bool, - requested_username: Option, -) -> Result { - validate_workspace_name(name)?; - check_w_id_conflict(tx, id).await?; - sqlx::query!( - "INSERT INTO workspace - (id, name, owner) - VALUES ($1, $2, $3)", - id, - name, - owner_email, - ) - .execute(&mut **tx) - .await?; - if error_handler_fallback_to_instance_alerts { - ensure_instance_alert_fallback_allowed(tx, id).await?; - } - sqlx::query!( - "INSERT INTO workspace_settings - (workspace_id, color, error_handler_fallback_to_instance_alerts) - VALUES ($1, $2, $3)", - id, - color, - error_handler_fallback_to_instance_alerts, - ) - .execute(&mut **tx) - .await?; - let key = rd_string(64); - sqlx::query!( - "INSERT INTO workspace_key - (workspace_id, kind, key) - VALUES ($1, 'cloud', $2)", - id, - &key - ) - .execute(&mut **tx) - .await?; - - let automate_username_creation = sqlx::query_scalar!( - "SELECT value FROM global_settings WHERE name = $1", - AUTOMATE_USERNAME_CREATION_SETTING, - ) - .fetch_optional(&mut **tx) - .await? - .map(|v| v.as_bool()) - .flatten() - .unwrap_or(true); - - let username = if automate_username_creation { - if requested_username.is_some_and(|u| u.len() > 0) { - return Err(Error::BadRequest( - "username is not allowed when username creation is automated".to_string(), - )); - } - get_instance_username_or_create_pending(tx, owner_email).await? - } else { - requested_username.ok_or(Error::BadRequest("username is required".to_string()))? - }; - - sqlx::query!( - "INSERT INTO usr - (workspace_id, email, username, is_admin) - VALUES ($1, $2, $3, true)", - id, - owner_email, - username, - ) - .execute(&mut **tx) - .await?; - - sqlx::query!( - "INSERT INTO group_ - VALUES ($1, 'all', 'The group that always contains all users of this workspace')", - id - ) - .execute(&mut **tx) - .await?; - - sqlx::query!( - "INSERT INTO group_ - VALUES ($1, 'wm_deployers', 'Members can preserve the original author when deploying to this workspace')", - id - ) - .execute(&mut **tx) - .await?; - - sqlx::query!( - "INSERT INTO usr_to_group - VALUES ($1, 'all', $2)", - id, - username - ) - .execute(&mut **tx) - .await?; - - Ok(username) -} - async fn create_workspace( authed: ApiAuthed, Extension(db): Extension, @@ -5716,17 +5601,115 @@ async fn create_workspace( } } + validate_workspace_name(&nw.name)?; + let mut tx: Transaction<'_, Postgres> = db.begin().await?; - insert_workspace( - &mut tx, - &nw.id, - &nw.name, - &authed.email, - nw.color.as_deref(), - nw.error_handler_fallback_to_instance_alerts, - nw.username, + check_w_id_conflict(&mut tx, &nw.id).await?; + sqlx::query!( + "INSERT INTO workspace + (id, name, owner) + VALUES ($1, $2, $3)", + nw.id, + nw.name, + authed.email, ) + .execute(&mut *tx) + .await?; + if nw.error_handler_fallback_to_instance_alerts { + ensure_instance_alert_fallback_allowed(&mut tx, &nw.id).await?; + } + sqlx::query!( + "INSERT INTO workspace_settings + (workspace_id, color, error_handler_fallback_to_instance_alerts) + VALUES ($1, $2, $3)", + nw.id, + nw.color, + nw.error_handler_fallback_to_instance_alerts, + ) + .execute(&mut *tx) + .await?; + let key = rd_string(64); + sqlx::query!( + "INSERT INTO workspace_key + (workspace_id, kind, key) + VALUES ($1, 'cloud', $2)", + nw.id, + &key + ) + .execute(&mut *tx) + .await?; + + // let mc = magic_crypt::new_magic_crypt!(key, 256); + // sqlx::query!( + // "INSERT INTO variable + // (workspace_id, path, value, is_secret, description) + // VALUES ($1, 'g/all/pretty_secret', $2, true, 'This item is secret'), + // ($3, 'g/all/not_secret', $4, false, 'This item is not secret')", + // nw.id, + // crate::variables::encrypt(&mc, "pretty secret value"), + // nw.id, + // "finland does not actually exist", + // ) + // .execute(&mut *tx) + // .await?; + + let automate_username_creation = sqlx::query_scalar!( + "SELECT value FROM global_settings WHERE name = $1", + AUTOMATE_USERNAME_CREATION_SETTING, + ) + .fetch_optional(&mut *tx) + .await? + .map(|v| v.as_bool()) + .flatten() + .unwrap_or(true); + + let username = if automate_username_creation { + if nw.username.is_some() && nw.username.unwrap().len() > 0 { + return Err(Error::BadRequest( + "username is not allowed when username creation is automated".to_string(), + )); + } + get_instance_username_or_create_pending(&mut tx, &authed.email).await? + } else { + nw.username + .ok_or(Error::BadRequest("username is required".to_string()))? + }; + + sqlx::query!( + "INSERT INTO usr + (workspace_id, email, username, is_admin) + VALUES ($1, $2, $3, true)", + nw.id, + authed.email, + username, + ) + .execute(&mut *tx) + .await?; + + sqlx::query!( + "INSERT INTO group_ + VALUES ($1, 'all', 'The group that always contains all users of this workspace')", + nw.id + ) + .execute(&mut *tx) + .await?; + + sqlx::query!( + "INSERT INTO group_ + VALUES ($1, 'wm_deployers', 'Members can preserve the original author when deploying to this workspace')", + nw.id + ) + .execute(&mut *tx) + .await?; + + sqlx::query!( + "INSERT INTO usr_to_group + VALUES ($1, 'all', $2)", + nw.id, + username + ) + .execute(&mut *tx) .await?; audit_log( diff --git a/frontend/src/routes/(root)/(logged)/user/(user)/onboarding/+page.svelte b/frontend/src/routes/(root)/(logged)/user/(user)/onboarding/+page.svelte index 6ac6e8287d..1273affa60 100644 --- a/frontend/src/routes/(root)/(logged)/user/(user)/onboarding/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/user/(user)/onboarding/+page.svelte @@ -1,15 +1,12 @@