From 0636f8cf1115cea8b9c4103fe8dfa8ddc08fb460 Mon Sep 17 00:00:00 2001 From: Giancarlo Buomprisco Date: Thu, 8 Jan 2026 14:18:13 +0100 Subject: [PATCH] chore: bump version to 2.23.2 and enhance team account creation (#440) * chore: bump version to 2.23.2 and enhance team account creation - Updated application version from 2.23.1 to 2.23.2 in package.json. - Enhanced team account creation to support slugs for non-Latin names, including validation and UI updates. - Updated localization files to reflect new slug requirements and error messages. - Refactored related schemas and server actions to accommodate slug handling in team account creation and updates. * refactor: remove old trigger and function for adding current user to new account - Dropped the trigger "add_current_user_to_new_account" and the associated function from the database schema. - Updated permissions for the function public.create_team_account to ensure proper access control. --- .../tests/team-accounts/team-accounts.po.ts | 110 +- .../tests/team-accounts/team-accounts.spec.ts | 99 +- apps/web/lib/database.types.ts | 2 +- apps/web/public/locales/en/teams.json | 6 +- .../20260108114816_create-team-with-slug.sql | 67 + apps/web/supabase/schemas/03-accounts.sql | 75 +- .../database/account-permissions.test.sql | 8 +- .../tests/database/account-slug.test.sql | 12 +- .../tests/database/team-accounts.test.sql | 137 +- .../triggers-timestamps-simple.test.sql | 20 +- .../triggers-user-tracking-accounts.test.sql | 13 +- .../tests/database/update-membership.test.sql | 14 +- package.json | 2 +- .../components/create-team-account-dialog.tsx | 51 +- .../update-team-account-name-form.tsx | 57 +- .../src/schema/create-team.schema.ts | 77 +- .../src/schema/update-team-name.schema.ts | 32 +- .../create-team-account-server-actions.ts | 19 +- .../actions/team-details-server-actions.ts | 20 +- .../services/create-team-account.service.ts | 50 +- packages/supabase/src/database.types.ts | 2790 ++++++++--------- 21 files changed, 2042 insertions(+), 1619 deletions(-) create mode 100644 apps/web/supabase/migrations/20260108114816_create-team-with-slug.sql diff --git a/apps/e2e/tests/team-accounts/team-accounts.po.ts b/apps/e2e/tests/team-accounts/team-accounts.po.ts index cc6280f1b..9d8db3964 100644 --- a/apps/e2e/tests/team-accounts/team-accounts.po.ts +++ b/apps/e2e/tests/team-accounts/team-accounts.po.ts @@ -91,13 +91,23 @@ export class TeamAccountsPageObject { }).toPass(); } - async tryCreateTeam(teamName: string) { - await this.page.locator('[data-test="create-team-form"] input').fill(''); - await this.page.waitForTimeout(200); + async tryCreateTeam(teamName: string, slug?: string) { + const nameInput = this.page.locator( + '[data-test="create-team-form"] [data-test="team-name-input"]', + ); - await this.page - .locator('[data-test="create-team-form"] input') - .fill(teamName); + await nameInput.fill(''); + await nameInput.fill(teamName); + + // If slug is provided (for non-Latin names), fill the slug field + if (slug) { + const slugInput = this.page.locator( + '[data-test="create-team-form"] [data-test="team-slug-input"]', + ); + + await expect(slugInput).toBeVisible(); + await slugInput.fill(slug); + } return this.page.click('[data-test="create-team-form"] button:last-child'); } @@ -106,7 +116,14 @@ export class TeamAccountsPageObject { await this.openAccountsSelector(); await this.page.click('[data-test="create-team-account-trigger"]'); - await this.page.fill('[data-test="create-team-form"] input', teamName); + + await this.page.fill( + '[data-test="create-team-form"] [data-test="team-name-input"]', + teamName, + ); + + // Slug field is only shown for non-Latin names, so we don't fill it for Latin names + // The database trigger will auto-generate the slug from the name const click = this.page.click( '[data-test="create-team-form"] button:last-child', @@ -115,23 +132,77 @@ export class TeamAccountsPageObject { const response = this.page.waitForURL(`/home/${slug}`); await Promise.all([click, response]); + + // Verify user landed on the team page + await expect(this.page).toHaveURL(`/home/${slug}`); + + // Verify the team was created and appears in the selector + await this.openAccountsSelector(); + await expect(this.getTeamFromSelector(teamName)).toBeVisible(); + + // Close the selector + await this.page.keyboard.press('Escape'); } - async updateName(name: string, slug: string) { + async createTeamWithNonLatinName(teamName: string, slug: string) { + await this.openAccountsSelector(); + + await this.page.click('[data-test="create-team-account-trigger"]'); + + await this.page.fill( + '[data-test="create-team-form"] [data-test="team-name-input"]', + teamName, + ); + + // Wait for slug field to appear (triggered by non-Latin name) + await expect(this.getSlugField()).toBeVisible(); + + await this.page.fill( + '[data-test="create-team-form"] [data-test="team-slug-input"]', + slug, + ); + + const click = this.page.click( + '[data-test="create-team-form"] button:last-child', + ); + + const response = this.page.waitForURL(`/home/${slug}`); + + await Promise.all([click, response]); + + // Verify user landed on the team page + await expect(this.page).toHaveURL(`/home/${slug}`); + + // Verify the team was created and appears in the selector + await this.openAccountsSelector(); + await expect(this.getTeamFromSelector(teamName)).toBeVisible(); + + // Close the selector + await this.page.keyboard.press('Escape'); + } + + getSlugField() { + return this.page.locator( + '[data-test="create-team-form"] [data-test="team-slug-input"]', + ); + } + + async updateTeamName(name: string) { await expect(async () => { await this.page.fill( '[data-test="update-team-account-name-form"] input', name, ); - const click = this.page.click( - '[data-test="update-team-account-name-form"] button', - ); - - // the slug should be updated to match the new team name - const response = this.page.waitForURL(`**/home/${slug}/settings`); - - return Promise.all([click, response]); + await Promise.all([ + this.page.click('[data-test="update-team-account-name-form"] button'), + this.page.waitForResponse((response) => { + return ( + response.url().includes('settings') && + response.request().method() === 'POST' + ); + }), + ]); }).toPass(); } @@ -165,8 +236,11 @@ export class TeamAccountsPageObject { await this.page.click(`[data-test="role-option-${newRole}"]`); // Wait for the update to complete and page to reload - const response = this.page.waitForResponse(response => { - return response.url().includes('members') && response.request().method() === 'POST' + const response = this.page.waitForResponse((response) => { + return ( + response.url().includes('members') && + response.request().method() === 'POST' + ); }); return Promise.all([ diff --git a/apps/e2e/tests/team-accounts/team-accounts.spec.ts b/apps/e2e/tests/team-accounts/team-accounts.spec.ts index bf7876fc4..7073be682 100644 --- a/apps/e2e/tests/team-accounts/team-accounts.spec.ts +++ b/apps/e2e/tests/team-accounts/team-accounts.spec.ts @@ -65,22 +65,20 @@ test.describe('Team Accounts', () => { await teamAccounts.setup(); }); - test('user can update their team name (and slug)', async ({ page }) => { + test('user can update their team name', async ({ page }) => { const teamAccounts = new TeamAccountsPageObject(page); - const { teamName, slug } = teamAccounts.createTeamName(); + const newTeamName = `Updated-Team-${(Math.random() * 100000000).toFixed(0)}`; await teamAccounts.goToSettings(); - const request = teamAccounts.updateName(teamName, slug); + // Update just the name (slug stays the same for Latin names) + await teamAccounts.updateTeamName(newTeamName); - // the slug should be updated to match the new team name - const newUrl = page.waitForURL(`**/home/${slug}/settings`); - - await Promise.all([request, newUrl]); + await page.waitForTimeout(500); await teamAccounts.openAccountsSelector(); - await expect(teamAccounts.getTeamFromSelector(teamName)).toBeVisible(); + await expect(teamAccounts.getTeamFromSelector(newTeamName)).toBeVisible(); }); test('cannot create a Team account using reserved names', async ({ @@ -176,54 +174,73 @@ test.describe('Team Accounts', () => { await expectError(); }); - test('cannot create a Team account using non-latin characters', async ({ + test('can create a Team account with non-Latin name when providing a slug', async ({ page, }) => { const teamAccounts = new TeamAccountsPageObject(page); await teamAccounts.createTeam(); + const random = (Math.random() * 100000000).toFixed(0); + const slug = `korean-team-${random}`; + + // Create team with Korean name + await teamAccounts.createTeamWithNonLatinName('한국 팀', slug); + + // Verify we're on the team page + await expect(page).toHaveURL(`/home/${slug}`); + + // Verify team appears in selector + await teamAccounts.openAccountsSelector(); + await expect(teamAccounts.getTeamFromSelector('한국 팀')).toBeVisible(); + }); + + test('slug validation shows error for invalid characters', async ({ + page, + }) => { + const teamAccounts = new TeamAccountsPageObject(page); + await teamAccounts.createTeam(); + + // Use non-Latin name to trigger the slug field visibility await teamAccounts.openAccountsSelector(); await page.click('[data-test="create-team-account-trigger"]'); - function expectNonLatinError() { - return expect( - page.getByText( - 'This name can only contain Latin characters (a-z), numbers, spaces, and hyphens.', - ), - ).toBeVisible(); - } + await page.fill( + '[data-test="create-team-form"] [data-test="team-name-input"]', + 'テストチーム', + ); - // Test Cyrillic characters - await teamAccounts.tryCreateTeam('Тест Команда'); - await expectNonLatinError(); + // Wait for slug field to appear (triggered by non-Latin name) + await expect(teamAccounts.getSlugField()).toBeVisible(); - // Test Chinese characters - await teamAccounts.tryCreateTeam('测试团队'); - await expectNonLatinError(); + // Test invalid slug with uppercase + await page.fill( + '[data-test="create-team-form"] [data-test="team-slug-input"]', + 'Invalid-Slug', + ); - // Test Japanese characters - await teamAccounts.tryCreateTeam('テストチーム'); - await expectNonLatinError(); + await page.click('[data-test="create-team-form"] button:last-child'); - // Test Arabic characters - await teamAccounts.tryCreateTeam('فريق اختبار'); - await expectNonLatinError(); - - // Test mixed Latin and non-Latin - await teamAccounts.tryCreateTeam('Test Команда'); - await expectNonLatinError(); - - // Test emoji - await teamAccounts.tryCreateTeam('Test Team 🚀'); - await expectNonLatinError(); - - // Ensure valid Latin names still work (should NOT show error) - await teamAccounts.tryCreateTeam('Valid Team Name 123'); await expect( page.getByText( - 'This name can only contain Latin characters (a-z), numbers, spaces, and hyphens.', + 'Only English letters (a-z), numbers (0-9), and hyphens (-) are allowed', + { exact: true }, ), - ).not.toBeVisible(); + ).toBeVisible(); + + // Test invalid slug with non-Latin characters + await page.fill( + '[data-test="create-team-form"] [data-test="team-slug-input"]', + 'тест-slug', + ); + + await page.click('[data-test="create-team-form"] button:last-child'); + + await expect( + page.getByText( + 'Only English letters (a-z), numbers (0-9), and hyphens (-) are allowed', + { exact: true }, + ), + ).toBeVisible(); }); }); diff --git a/apps/web/lib/database.types.ts b/apps/web/lib/database.types.ts index dc409ca8c..caa98a686 100644 --- a/apps/web/lib/database.types.ts +++ b/apps/web/lib/database.types.ts @@ -743,7 +743,7 @@ export type Database = { Returns: Json } create_team_account: { - Args: { account_name: string } + Args: { account_name: string; account_slug?: string } Returns: { created_at: string | null created_by: string | null diff --git a/apps/web/public/locales/en/teams.json b/apps/web/public/locales/en/teams.json index 4faa8fe0f..39cd287aa 100644 --- a/apps/web/public/locales/en/teams.json +++ b/apps/web/public/locales/en/teams.json @@ -160,7 +160,11 @@ "leaveTeamInputDescription": "By leaving the team, you will no longer have access to it.", "reservedNameError": "This name is reserved. Please choose a different one.", "specialCharactersError": "This name cannot contain special characters. Please choose a different one.", - "nonLatinCharactersError": "This name can only contain Latin characters (a-z), numbers, spaces, and hyphens.", + "teamSlugLabel": "Team URL", + "teamSlugDescription": "Only English letters (a-z), numbers (0-9), and hyphens (-) are allowed. Example: my-team-name", + "slugRequiredForNonLatinName": "Since your team name uses non-English characters, please provide a URL using only English letters", + "invalidSlugError": "Only English letters (a-z), numbers (0-9), and hyphens (-) are allowed", + "duplicateSlugError": "This URL is already taken. Please choose a different one.", "checkingPolicies": "Loading. Please wait...", "policyCheckError": "We are unable to verify invitations restrictions. Please try again.", "invitationsBlockedMultiple": "Invitations are currently not allowed for the following reasons:", diff --git a/apps/web/supabase/migrations/20260108114816_create-team-with-slug.sql b/apps/web/supabase/migrations/20260108114816_create-team-with-slug.sql new file mode 100644 index 000000000..1af8016d0 --- /dev/null +++ b/apps/web/supabase/migrations/20260108114816_create-team-with-slug.sql @@ -0,0 +1,67 @@ +drop policy "create_org_account" on "public"."accounts"; + +drop function if exists "public"."create_team_account"(text); + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION public.create_team_account(account_name text, user_id uuid, account_slug text DEFAULT NULL::text) + RETURNS public.accounts + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO '' +AS $function$ +declare + new_account public.accounts; + owner_role varchar(50); +begin + if (not public.is_set('enable_team_accounts')) then + raise exception 'Team accounts are not enabled'; + end if; + + -- Get the highest system role for the owner + select public.get_upper_system_role() into owner_role; + + -- Insert the new team account + -- The slug will be auto-generated from name by the "set_slug_from_account_name" + -- trigger if account_slug is null + insert into public.accounts( + name, + slug, + is_personal_account, + primary_owner_user_id) + values ( + account_name, + account_slug, + false, + user_id) + returning * into new_account; + + -- Create membership for the owner (atomic with account creation) + insert into public.accounts_memberships( + account_id, + user_id, + account_role) + values ( + new_account.id, + user_id, + coalesce(owner_role, 'owner')); + + return new_account; + +end; + +$function$ +; + + + +-- Revoke from all roles first to ensure exclusivity +revoke all on function public.create_team_account(text, uuid, text) from public; +revoke all on function public.create_team_account(text, uuid, text) from authenticated; + +-- Grant only to service_role +grant execute on function public.create_team_account(text, uuid, text) to service_role; + +-- Drop trigger (handled by the new function) +drop trigger if exists "add_current_user_to_new_account" on "public"."accounts"; +drop function if exists "kit"."add_current_user_to_new_account"(); \ No newline at end of file diff --git a/apps/web/supabase/schemas/03-accounts.sql b/apps/web/supabase/schemas/03-accounts.sql index d1be10337..a23f5b9de 100644 --- a/apps/web/supabase/schemas/03-accounts.sql +++ b/apps/web/supabase/schemas/03-accounts.sql @@ -223,37 +223,6 @@ $$ language plpgsql; grant execute on function public.get_upper_system_role () to service_role; --- Function "kit.add_current_user_to_new_account" --- Trigger to add the current user to a new account as the primary owner -create -or replace function kit.add_current_user_to_new_account () returns trigger language plpgsql security definer -set - search_path = '' as $$ -begin - if new.primary_owner_user_id = auth.uid() then - insert into public.accounts_memberships( - account_id, - user_id, - account_role) - values( - new.id, - auth.uid(), - public.get_upper_system_role()); - - end if; - - return NEW; - -end; - -$$; - --- trigger the function whenever a new account is created -create trigger "add_current_user_to_new_account" -after insert on public.accounts for each row -when (new.is_personal_account = false) -execute function kit.add_current_user_to_new_account (); - -- create a trigger to update the account email when the primary owner email is updated create or replace function kit.handle_update_user_email () returns trigger language plpgsql security definer @@ -470,36 +439,62 @@ execute procedure kit.setup_new_user (); * ------------------------------------------------------- */ -- Function "public.create_team_account" --- Create a team account if team accounts are enabled +-- Create a team account with membership in a single transaction +-- Called by service_role only (Policies API enforced in application layer) create -or replace function public.create_team_account (account_name text) returns public.accounts +or replace function public.create_team_account ( + account_name text, + user_id uuid, + account_slug text default null +) returns public.accounts +language plpgsql +security definer set search_path = '' as $$ declare new_account public.accounts; + owner_role varchar(50); begin if (not public.is_set('enable_team_accounts')) then raise exception 'Team accounts are not enabled'; end if; + -- Get the highest system role for the owner + select public.get_upper_system_role() into owner_role; + + -- Insert the new team account + -- The slug will be auto-generated from name by the "set_slug_from_account_name" + -- trigger if account_slug is null insert into public.accounts( name, - is_personal_account) + slug, + is_personal_account, + primary_owner_user_id) values ( account_name, - false) -returning - * into new_account; + account_slug, + false, + user_id) + returning * into new_account; + + -- Create membership for the owner (atomic with account creation) + insert into public.accounts_memberships( + account_id, + user_id, + account_role) + values ( + new_account.id, + user_id, + coalesce(owner_role, 'owner')); return new_account; end; -$$ language plpgsql; +$$; grant -execute on function public.create_team_account (text) to authenticated, -service_role; +execute on function public.create_team_account (text, uuid, text) to service_role; -- RLS(public.accounts) -- Authenticated users can delete team accounts diff --git a/apps/web/supabase/tests/database/account-permissions.test.sql b/apps/web/supabase/tests/database/account-permissions.test.sql index de5562922..0ab2971c5 100644 --- a/apps/web/supabase/tests/database/account-permissions.test.sql +++ b/apps/web/supabase/tests/database/account-permissions.test.sql @@ -9,12 +9,14 @@ select tests.create_supabase_user('test1', 'test1@test.com'); select tests.create_supabase_user('test2'); --- Create an team account +-- Create team account using service_role (function is now service_role only) +set local role service_role; +select public.create_team_account('Test', tests.get_supabase_uid('test1')); + +-- Switch back to authenticated user for testing select makerkit.authenticate_as('test1'); -select public.create_team_account('Test'); - -- the owner account has permissions to manage members select row_eq( $$ select public.has_permission( diff --git a/apps/web/supabase/tests/database/account-slug.test.sql b/apps/web/supabase/tests/database/account-slug.test.sql index 1035619ba..6ce7a1dff 100644 --- a/apps/web/supabase/tests/database/account-slug.test.sql +++ b/apps/web/supabase/tests/database/account-slug.test.sql @@ -9,14 +9,16 @@ select tests.create_supabase_user('test1', 'test1@test.com'); select tests.create_supabase_user('test2'); --- Create an team account +-- Create team accounts using service_role (function is now service_role only) +set local role service_role; +select public.create_team_account('Test', tests.get_supabase_uid('test1')); +select public.create_team_account('Test', tests.get_supabase_uid('test1')); +select public.create_team_account('Test', tests.get_supabase_uid('test1')); + +-- Switch back to authenticated user for testing select makerkit.authenticate_as('test1'); -select public.create_team_account('Test'); -select public.create_team_account('Test'); -select public.create_team_account('Test'); - -- should automatically create slugs for the accounts select row_eq( $$ select slug from public.accounts where name = 'Test' and slug = 'test' $$, diff --git a/apps/web/supabase/tests/database/team-accounts.test.sql b/apps/web/supabase/tests/database/team-accounts.test.sql index 16a301ada..70dce48c2 100644 --- a/apps/web/supabase/tests/database/team-accounts.test.sql +++ b/apps/web/supabase/tests/database/team-accounts.test.sql @@ -12,12 +12,19 @@ select select tests.create_supabase_user('test2'); --- Create an team account -select - makerkit.authenticate_as('test1'); +-- Create an team account (without explicit slug, should auto-generate) +-- DON'T authenticate first - the add_current_user_to_new_account trigger +-- would also create a membership if auth.uid() = primary_owner_user_id +-- The function already creates the membership, so we avoid duplicate by keeping auth.uid() NULL +set local role service_role; select - public.create_team_account('Test'); + public.create_team_account('Test', tests.get_supabase_uid('test1')); + +-- Reset to postgres and then authenticate as test1 for proper RLS context +set local role postgres; +select + makerkit.authenticate_as('test1'); select row_eq($$ @@ -28,6 +35,44 @@ select 'test'::text, 'Test'::varchar), 'Users can create a team account'); +-- Test creating team account with explicit slug parameter +select + tests.create_supabase_user('slugtest1', 'slugtest1@test.com'); + +-- Switch to service_role to call the function +set local role service_role; + +select + public.create_team_account('Custom Team Name', tests.get_supabase_uid('slugtest1'), 'custom-slug-123'); + +-- Switch back to authenticated user for testing +select + makerkit.authenticate_as('slugtest1'); + +select + row_eq($$ + select + primary_owner_user_id, is_personal_account, slug, name + from makerkit.get_account_by_slug('custom-slug-123') $$, + row (tests.get_supabase_uid('slugtest1'), false, + 'custom-slug-123'::text, 'Custom Team Name'::varchar), + 'Users can create a team account with custom slug'); + +-- Verify membership is created for custom slug team +select + row_eq($$ + select + account_role from public.accounts_memberships + where + account_id = (select id from public.accounts where slug = 'custom-slug-123') + and user_id = tests.get_supabase_uid('slugtest1') + $$, row ('owner'::varchar), + 'The primary owner should have the owner role for team with custom slug'); + +-- Switch back to test1 for testing the original 'test' account +select + makerkit.authenticate_as('test1'); + -- Should be the primary owner of the team account by default select row_eq($$ @@ -106,12 +151,13 @@ create or replace function kit.single_account_per_owner() declare total_accounts int; begin + -- Check if this user already owns an account by checking NEW.primary_owner_user_id select count(id) from public.accounts where - primary_owner_user_id = auth.uid() into total_accounts; + primary_owner_user_id = NEW.primary_owner_user_id into total_accounts; if total_accounts > 0 then raise exception 'User can only own 1 account'; @@ -129,14 +175,13 @@ create trigger single_account_per_owner before insert on public.accounts for each row execute function kit.single_account_per_owner(); --- Create an team account -select - makerkit.authenticate_as('test1'); +-- Try to create another team account for the same owner (should fail due to trigger) +set local role service_role; select throws_ok( $$ select - public.create_team_account('Test2') $$, 'User can only own 1 account'); + public.create_team_account('Test2', tests.get_supabase_uid('test1')) $$, 'User can only own 1 account'); set local role postgres; @@ -151,11 +196,10 @@ select tests.create_supabase_user('updatetest2', 'updatetest2@test.com'); -- Create a team account for update tests -select - makerkit.authenticate_as('updatetest1'); +set local role service_role; select - public.create_team_account('UpdateTeam'); + public.create_team_account('UpdateTeam', tests.get_supabase_uid('updatetest1')); -- Add updatetest2 as a member set local role postgres; @@ -259,11 +303,10 @@ select tests.create_supabase_user('roletest2', 'roletest2@test.com'); -- Create a team account for role tests -select - makerkit.authenticate_as('roletest1'); +set local role service_role; select - public.create_team_account('RoleTeam'); + public.create_team_account('RoleTeam', tests.get_supabase_uid('roletest1')); -- Add roletest2 as a member set local role postgres; @@ -333,11 +376,10 @@ select tests.create_supabase_user('deletetest2', 'deletetest2@test.com'); -- Create a team account for delete tests -select - makerkit.authenticate_as('deletetest1'); +set local role service_role; select - public.create_team_account('DeleteTeam'); + public.create_team_account('DeleteTeam', tests.get_supabase_uid('deletetest1')); -- Add deletetest2 as a member set local role postgres; @@ -383,8 +425,8 @@ select tests.create_supabase_user('permtest2', 'permtest2@test.com'); select tests.create_supabase_user('permtest3', 'permtest3@test.com'); -- Create a team account for permission tests -select makerkit.authenticate_as('permtest1'); -select public.create_team_account('PermTeam'); +set local role service_role; +select public.create_team_account('PermTeam', tests.get_supabase_uid('permtest1')); -- Get the account ID for PermTeam to avoid NULL references set local role postgres; @@ -470,8 +512,8 @@ select tests.create_supabase_user('hiertest3', 'hiertest3@test.com'); select tests.create_supabase_user('hiertest4', 'hiertest4@test.com'); -- Create a team account for hierarchy tests -select makerkit.authenticate_as('hiertest1'); -select public.create_team_account('HierTeam'); +set local role service_role; +select public.create_team_account('HierTeam', tests.get_supabase_uid('hiertest1')); -- Add users with different roles set local role postgres; @@ -540,8 +582,8 @@ select tests.create_supabase_user('vistest2', 'vistest2@test.com'); select tests.create_supabase_user('vistest3', 'vistest3@test.com'); -- Create a team account -select makerkit.authenticate_as('vistest1'); -select public.create_team_account('VisTeam'); +set local role service_role; +select public.create_team_account('VisTeam', tests.get_supabase_uid('vistest1')); -- Add vistest2 as a member set local role postgres; @@ -578,8 +620,8 @@ select tests.create_supabase_user('functest1', 'functest1@test.com'); select tests.create_supabase_user('functest2', 'functest2@test.com'); -- Create team account -select makerkit.authenticate_as('functest1'); -select public.create_team_account('FuncTeam'); +set local role service_role; +select public.create_team_account('FuncTeam', tests.get_supabase_uid('functest1')); -- Test: get_account_members function properly restricts data select makerkit.authenticate_as('functest2'); @@ -619,10 +661,11 @@ select tests.create_supabase_user('ownerupdate1', 'ownerupdate1@test.com'); select tests.create_supabase_user('ownerupdate2', 'ownerupdate2@test.com'); -- Create team account -select makerkit.authenticate_as('ownerupdate1'); -select public.create_team_account('TeamChange'); +set local role service_role; +select public.create_team_account('TeamChange', tests.get_supabase_uid('ownerupdate1')); -- Update the team name as the owner +select makerkit.authenticate_as('ownerupdate1'); select lives_ok( $$ UPDATE public.accounts SET name = 'Updated Owner Team' @@ -668,23 +711,16 @@ select tests.create_supabase_user('crosstest2', 'crosstest2@test.com'); -- Create first team account with crosstest1 as owner -select - makerkit.authenticate_as('crosstest1'); +set local role service_role; select - public.create_team_account('TeamA'); + public.create_team_account('TeamA', tests.get_supabase_uid('crosstest1')); -- Create second team account with crosstest2 as owner select - makerkit.authenticate_as('crosstest2'); - -select - public.create_team_account('TeamB'); + public.create_team_account('TeamB', tests.get_supabase_uid('crosstest2')); -- Add crosstest2 as a member to TeamA -select - makerkit.authenticate_as('crosstest1'); - set local role postgres; -- Add member to first team @@ -767,6 +803,31 @@ select 'TeamB name should remain unchanged after attempted update by non-member' ); +-- Test 7: Security - Public/anon role cannot execute create_team_account +select + tests.create_supabase_user('securitytest1', 'securitytest1@test.com'); + +-- Test as anon role (public) - should get permission denied (either for schema or function) +set local role anon; + +select + throws_ok( + $$ select public.create_team_account('SecurityTeam', tests.get_supabase_uid('securitytest1')) $$, + 'permission denied for schema public', + 'Anonymous/public role should not be able to execute create_team_account' + ); + +-- Test as authenticated role (still should fail - only service_role is allowed) +select + makerkit.authenticate_as('securitytest1'); + +select + throws_ok( + $$ select public.create_team_account('SecurityTeam', tests.get_supabase_uid('securitytest1')) $$, + 'permission denied for function create_team_account', + 'Authenticated role should not be able to execute create_team_account directly' + ); + select * from diff --git a/apps/web/supabase/tests/database/triggers-timestamps-simple.test.sql b/apps/web/supabase/tests/database/triggers-timestamps-simple.test.sql index 47f19c966..0efc0a3b5 100644 --- a/apps/web/supabase/tests/database/triggers-timestamps-simple.test.sql +++ b/apps/web/supabase/tests/database/triggers-timestamps-simple.test.sql @@ -9,15 +9,15 @@ select plan(12); --- Create test users select tests.create_supabase_user('trigger_test_user1', 'test1@example.com'); --- Authenticate as test user -select makerkit.authenticate_as('trigger_test_user1'); - ------------ --- Test accounts table timestamp triggers - INSERT ------------ -INSERT INTO public.accounts (name, is_personal_account) -VALUES ('Test Account', false); +-- Use service_role to insert (create_org_account policy was removed) +set role service_role; + +INSERT INTO public.accounts (name, is_personal_account, primary_owner_user_id) +VALUES ('Test Account', false, tests.get_supabase_uid('trigger_test_user1')); SELECT ok( (SELECT created_at IS NOT NULL FROM public.accounts WHERE name = 'Test Account'), @@ -38,9 +38,9 @@ SELECT ok( --- Test invitations table timestamp triggers - INSERT ------------ --- Create a team account for invitation testing -INSERT INTO public.accounts (name, is_personal_account) -VALUES ('Invitation Test Team', false); +-- Create a team account for invitation testing (still in service_role from above) +INSERT INTO public.accounts (name, is_personal_account, primary_owner_user_id) +VALUES ('Invitation Test Team', false, tests.get_supabase_uid('trigger_test_user1')); -- Switch to service_role to insert invitations (INSERT policy removed, handled by server action) set role service_role; @@ -56,9 +56,7 @@ VALUES ( now() + interval '7 days' ); --- Switch back to authenticated user for assertion -select makerkit.authenticate_as('trigger_test_user1'); - +-- Stay in service_role for assertions (testing triggers, not RLS) SELECT ok( (SELECT created_at IS NOT NULL FROM public.invitations WHERE email = 'invitee@example.com'), 'invitations: created_at should be set automatically on insert' diff --git a/apps/web/supabase/tests/database/triggers-user-tracking-accounts.test.sql b/apps/web/supabase/tests/database/triggers-user-tracking-accounts.test.sql index 833005be3..8be723423 100644 --- a/apps/web/supabase/tests/database/triggers-user-tracking-accounts.test.sql +++ b/apps/web/supabase/tests/database/triggers-user-tracking-accounts.test.sql @@ -13,12 +13,19 @@ select tests.create_supabase_user('user_tracking_test1', 'tracking1@example.com' --- Test accounts table user tracking triggers - INSERT ------------ --- Authenticate as first user for insert +-- Authenticate first to set JWT claims (for auth.uid() in triggers) select makerkit.authenticate_as('user_tracking_test1'); +-- Switch to service_role for INSERT (create_org_account policy was removed) +-- but JWT claims are preserved so auth.uid() still works in triggers +set local role service_role; + -- Test INSERT: created_by and updated_by should be set to current user -INSERT INTO public.accounts (name, is_personal_account) -VALUES ('User Tracking Test Account', false); +INSERT INTO public.accounts (name, is_personal_account, primary_owner_user_id) +VALUES ('User Tracking Test Account', false, tests.get_supabase_uid('user_tracking_test1')); + +-- Switch back to authenticated for assertions +select makerkit.authenticate_as('user_tracking_test1'); SELECT ok( (SELECT created_by = tests.get_supabase_uid('user_tracking_test1') diff --git a/apps/web/supabase/tests/database/update-membership.test.sql b/apps/web/supabase/tests/database/update-membership.test.sql index f895db07a..2faa16170 100644 --- a/apps/web/supabase/tests/database/update-membership.test.sql +++ b/apps/web/supabase/tests/database/update-membership.test.sql @@ -7,15 +7,15 @@ select no_plan(); select tests.create_supabase_user('update_test_owner', 'update-owner@test.com'); select tests.create_supabase_user('update_test_member', 'update-member@test.com'); --- Authenticate as owner to create team account -select makerkit.authenticate_as('update_test_owner'); +-- Create team account using service_role and create_team_account function +-- DON'T authenticate first - the add_current_user_to_new_account trigger +-- would also create a membership if auth.uid() = primary_owner_user_id +-- The function already creates the membership, so we avoid duplicate by keeping auth.uid() NULL +set local role service_role; --- Create a team account (owner is added automatically via trigger) -insert into public.accounts (name, is_personal_account) -values ('Update Test Team', false); +select public.create_team_account('Update Test Team', tests.get_supabase_uid('update_test_owner')); --- Add member to the team with 'member' role using service_role -set role service_role; +-- Add member to the team with 'member' role (still in service_role) insert into public.accounts_memberships (account_id, user_id, account_role) values ( diff --git a/package.json b/package.json index 6b9769869..6b8b9a76a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "next-supabase-saas-kit-turbo", - "version": "2.23.1", + "version": "2.23.2", "private": true, "sideEffects": false, "engines": { diff --git a/packages/features/team-accounts/src/components/create-team-account-dialog.tsx b/packages/features/team-accounts/src/components/create-team-account-dialog.tsx index 52ef15e20..b58235a21 100644 --- a/packages/features/team-accounts/src/components/create-team-account-dialog.tsx +++ b/packages/features/team-accounts/src/components/create-team-account-dialog.tsx @@ -1,11 +1,11 @@ 'use client'; -import { useState, useTransition } from 'react'; +import { useMemo, useState, useTransition } from 'react'; import { isRedirectError } from 'next/dist/client/components/redirect-error'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; +import { useForm, useWatch } from 'react-hook-form'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Button } from '@kit/ui/button'; @@ -29,7 +29,10 @@ import { If } from '@kit/ui/if'; import { Input } from '@kit/ui/input'; import { Trans } from '@kit/ui/trans'; -import { CreateTeamSchema } from '../schema/create-team.schema'; +import { + CreateTeamSchema, + NON_LATIN_REGEX, +} from '../schema/create-team.schema'; import { createTeamAccountAction } from '../server/actions/create-team-account-server-actions'; export function CreateTeamAccountDialog( @@ -67,10 +70,18 @@ function CreateOrganizationAccountForm(props: { onClose: () => void }) { const form = useForm({ defaultValues: { name: '', + slug: '', }, resolver: zodResolver(CreateTeamSchema), }); + const nameValue = useWatch({ control: form.control, name: 'name' }); + + const showSlugField = useMemo( + () => NON_LATIN_REGEX.test(nameValue ?? ''), + [nameValue], + ); + return (
void }) { void }) { }} /> + + { + return ( + + + + + + + + + + + + + + + + ); + }} + /> + +