From 75d4f7018b99a73a4f13178491347e6f3b8a1046 Mon Sep 17 00:00:00 2001 From: Sergio Cadavid Date: Thu, 27 Aug 2026 18:43:56 -0500 Subject: [PATCH] fix(workspaces): keep a workspace with a reserved slug editable The Access-provisioned default workspace owns the reserved slug "clickclack". Its settings form submits the current slug together with the name, and normalizeWorkspaceSettings rejected any reserved slug regardless of whether it changed, so the workspace could never be renamed or given an icon ("workspace slug is reserved"). Reject a reserved slug only when it differs from the workspace's current slug; moving to a reserved slug, or taking one from a regular workspace, stays rejected. --- apps/api/internal/store/postgres/postgres.go | 5 ++- .../internal/store/sqlite/mutations_test.go | 39 +++++++++++++++++++ apps/api/internal/store/sqlite/sqlite.go | 5 ++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/apps/api/internal/store/postgres/postgres.go b/apps/api/internal/store/postgres/postgres.go index 8340a0de1..c7ad50bea 100644 --- a/apps/api/internal/store/postgres/postgres.go +++ b/apps/api/internal/store/postgres/postgres.go @@ -1220,7 +1220,10 @@ func normalizeWorkspaceSettings(current store.Workspace, input store.UpdateWorks if workspaceSlug == "" { return "", "", "", errors.New("workspace slug is required") } - if isReservedWorkspaceSlug(workspaceSlug) { + // A workspace that already owns a reserved slug (the Access-provisioned + // default workspace) must stay editable: rejecting its unchanged slug + // would block every name and icon update from the settings form. + if workspaceSlug != current.Slug && isReservedWorkspaceSlug(workspaceSlug) { return "", "", "", errors.New("workspace slug is reserved") } } diff --git a/apps/api/internal/store/sqlite/mutations_test.go b/apps/api/internal/store/sqlite/mutations_test.go index bdb1c1092..4830ddfb0 100644 --- a/apps/api/internal/store/sqlite/mutations_test.go +++ b/apps/api/internal/store/sqlite/mutations_test.go @@ -700,3 +700,42 @@ func TestMutationsReturnOutboxErrors(t *testing.T) { t.Fatal("expected message delete outbox error") } } + +func TestUpdateWorkspaceKeepsReservedSlugEditable(t *testing.T) { + t.Parallel() + ctx := context.Background() + st := newTestStore(t) + owner, err := st.EnsureBootstrap(ctx, "Owner", "owner-reserved-slug@example.com") + if err != nil { + t.Fatal(err) + } + workspace, err := st.EnsureDefaultWorkspaceMember(ctx, owner.ID) + if err != nil { + t.Fatal(err) + } + if !isReservedWorkspaceSlug(workspace.Slug) { + t.Fatalf("expected the provisioned default workspace to own a reserved slug, got %q", workspace.Slug) + } + // The settings form always submits the current slug alongside the name. + name := "Renamed default" + sameSlug := workspace.Slug + updated, _, err := st.UpdateWorkspace(ctx, store.UpdateWorkspaceInput{WorkspaceID: workspace.ID, ActorUserID: owner.ID, Name: &name, Slug: &sameSlug}) + if err != nil { + t.Fatalf("expected an unchanged reserved slug to stay editable: %v", err) + } + if updated.Name != name || updated.Slug != workspace.Slug { + t.Fatalf("unexpected workspace after rename: %#v", updated) + } + otherReserved := "guests" + if _, _, err := st.UpdateWorkspace(ctx, store.UpdateWorkspaceInput{WorkspaceID: workspace.ID, ActorUserID: owner.ID, Slug: &otherReserved}); err == nil { + t.Fatal("expected a move to another reserved slug to be rejected") + } + regular, err := st.CreateWorkspace(ctx, store.CreateWorkspaceInput{Name: "Regular", Slug: "regular"}, owner.ID) + if err != nil { + t.Fatal(err) + } + takeReserved := workspace.Slug + if _, _, err := st.UpdateWorkspace(ctx, store.UpdateWorkspaceInput{WorkspaceID: regular.ID, ActorUserID: owner.ID, Slug: &takeReserved}); err == nil { + t.Fatal("expected a regular workspace to be refused a reserved slug") + } +} diff --git a/apps/api/internal/store/sqlite/sqlite.go b/apps/api/internal/store/sqlite/sqlite.go index ed2ba5183..1c4cba72f 100644 --- a/apps/api/internal/store/sqlite/sqlite.go +++ b/apps/api/internal/store/sqlite/sqlite.go @@ -1232,7 +1232,10 @@ func normalizeWorkspaceSettings(current store.Workspace, input store.UpdateWorks if workspaceSlug == "" { return "", "", "", errors.New("workspace slug is required") } - if isReservedWorkspaceSlug(workspaceSlug) { + // A workspace that already owns a reserved slug (the Access-provisioned + // default workspace) must stay editable: rejecting its unchanged slug + // would block every name and icon update from the settings form. + if workspaceSlug != current.Slug && isReservedWorkspaceSlug(workspaceSlug) { return "", "", "", errors.New("workspace slug is reserved") } }