diff --git a/apps/api/internal/store/postgres/postgres.go b/apps/api/internal/store/postgres/postgres.go index 8340a0de..c7ad50be 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 bdb1c109..4830ddfb 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 ed2ba518..1c4cba72 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") } }