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.
This commit is contained in:
committed by
GitHub
parent
e1bfbc8106
commit
0636f8cf11
@@ -91,13 +91,23 @@ export class TeamAccountsPageObject {
|
|||||||
}).toPass();
|
}).toPass();
|
||||||
}
|
}
|
||||||
|
|
||||||
async tryCreateTeam(teamName: string) {
|
async tryCreateTeam(teamName: string, slug?: string) {
|
||||||
await this.page.locator('[data-test="create-team-form"] input').fill('');
|
const nameInput = this.page.locator(
|
||||||
await this.page.waitForTimeout(200);
|
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
||||||
|
);
|
||||||
|
|
||||||
await this.page
|
await nameInput.fill('');
|
||||||
.locator('[data-test="create-team-form"] input')
|
await nameInput.fill(teamName);
|
||||||
.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');
|
return this.page.click('[data-test="create-team-form"] button:last-child');
|
||||||
}
|
}
|
||||||
@@ -106,7 +116,14 @@ export class TeamAccountsPageObject {
|
|||||||
await this.openAccountsSelector();
|
await this.openAccountsSelector();
|
||||||
|
|
||||||
await this.page.click('[data-test="create-team-account-trigger"]');
|
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(
|
const click = this.page.click(
|
||||||
'[data-test="create-team-form"] button:last-child',
|
'[data-test="create-team-form"] button:last-child',
|
||||||
@@ -115,23 +132,77 @@ export class TeamAccountsPageObject {
|
|||||||
const response = this.page.waitForURL(`/home/${slug}`);
|
const response = this.page.waitForURL(`/home/${slug}`);
|
||||||
|
|
||||||
await Promise.all([click, response]);
|
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 expect(async () => {
|
||||||
await this.page.fill(
|
await this.page.fill(
|
||||||
'[data-test="update-team-account-name-form"] input',
|
'[data-test="update-team-account-name-form"] input',
|
||||||
name,
|
name,
|
||||||
);
|
);
|
||||||
|
|
||||||
const click = this.page.click(
|
await Promise.all([
|
||||||
'[data-test="update-team-account-name-form"] button',
|
this.page.click('[data-test="update-team-account-name-form"] button'),
|
||||||
|
this.page.waitForResponse((response) => {
|
||||||
|
return (
|
||||||
|
response.url().includes('settings') &&
|
||||||
|
response.request().method() === 'POST'
|
||||||
);
|
);
|
||||||
|
}),
|
||||||
// the slug should be updated to match the new team name
|
]);
|
||||||
const response = this.page.waitForURL(`**/home/${slug}/settings`);
|
|
||||||
|
|
||||||
return Promise.all([click, response]);
|
|
||||||
}).toPass();
|
}).toPass();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,8 +236,11 @@ export class TeamAccountsPageObject {
|
|||||||
await this.page.click(`[data-test="role-option-${newRole}"]`);
|
await this.page.click(`[data-test="role-option-${newRole}"]`);
|
||||||
|
|
||||||
// Wait for the update to complete and page to reload
|
// Wait for the update to complete and page to reload
|
||||||
const response = this.page.waitForResponse(response => {
|
const response = this.page.waitForResponse((response) => {
|
||||||
return response.url().includes('members') && response.request().method() === 'POST'
|
return (
|
||||||
|
response.url().includes('members') &&
|
||||||
|
response.request().method() === 'POST'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
|
|||||||
@@ -65,22 +65,20 @@ test.describe('Team Accounts', () => {
|
|||||||
await teamAccounts.setup();
|
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 teamAccounts = new TeamAccountsPageObject(page);
|
||||||
const { teamName, slug } = teamAccounts.createTeamName();
|
const newTeamName = `Updated-Team-${(Math.random() * 100000000).toFixed(0)}`;
|
||||||
|
|
||||||
await teamAccounts.goToSettings();
|
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
|
await page.waitForTimeout(500);
|
||||||
const newUrl = page.waitForURL(`**/home/${slug}/settings`);
|
|
||||||
|
|
||||||
await Promise.all([request, newUrl]);
|
|
||||||
|
|
||||||
await teamAccounts.openAccountsSelector();
|
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 ({
|
test('cannot create a Team account using reserved names', async ({
|
||||||
@@ -176,54 +174,73 @@ test.describe('Team Accounts', () => {
|
|||||||
await expectError();
|
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,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const teamAccounts = new TeamAccountsPageObject(page);
|
const teamAccounts = new TeamAccountsPageObject(page);
|
||||||
await teamAccounts.createTeam();
|
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 teamAccounts.openAccountsSelector();
|
||||||
await page.click('[data-test="create-team-account-trigger"]');
|
await page.click('[data-test="create-team-account-trigger"]');
|
||||||
|
|
||||||
function expectNonLatinError() {
|
await page.fill(
|
||||||
return expect(
|
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
||||||
page.getByText(
|
'テストチーム',
|
||||||
'This name can only contain Latin characters (a-z), numbers, spaces, and hyphens.',
|
);
|
||||||
),
|
|
||||||
).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test Cyrillic characters
|
// Wait for slug field to appear (triggered by non-Latin name)
|
||||||
await teamAccounts.tryCreateTeam('Тест Команда');
|
await expect(teamAccounts.getSlugField()).toBeVisible();
|
||||||
await expectNonLatinError();
|
|
||||||
|
|
||||||
// Test Chinese characters
|
// Test invalid slug with uppercase
|
||||||
await teamAccounts.tryCreateTeam('测试团队');
|
await page.fill(
|
||||||
await expectNonLatinError();
|
'[data-test="create-team-form"] [data-test="team-slug-input"]',
|
||||||
|
'Invalid-Slug',
|
||||||
|
);
|
||||||
|
|
||||||
// Test Japanese characters
|
await page.click('[data-test="create-team-form"] button:last-child');
|
||||||
await teamAccounts.tryCreateTeam('テストチーム');
|
|
||||||
await expectNonLatinError();
|
|
||||||
|
|
||||||
// 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(
|
await expect(
|
||||||
page.getByText(
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -743,7 +743,7 @@ export type Database = {
|
|||||||
Returns: Json
|
Returns: Json
|
||||||
}
|
}
|
||||||
create_team_account: {
|
create_team_account: {
|
||||||
Args: { account_name: string }
|
Args: { account_name: string; account_slug?: string }
|
||||||
Returns: {
|
Returns: {
|
||||||
created_at: string | null
|
created_at: string | null
|
||||||
created_by: string | null
|
created_by: string | null
|
||||||
|
|||||||
@@ -160,7 +160,11 @@
|
|||||||
"leaveTeamInputDescription": "By leaving the team, you will no longer have access to it.",
|
"leaveTeamInputDescription": "By leaving the team, you will no longer have access to it.",
|
||||||
"reservedNameError": "This name is reserved. Please choose a different one.",
|
"reservedNameError": "This name is reserved. Please choose a different one.",
|
||||||
"specialCharactersError": "This name cannot contain special characters. 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...",
|
"checkingPolicies": "Loading. Please wait...",
|
||||||
"policyCheckError": "We are unable to verify invitations restrictions. Please try again.",
|
"policyCheckError": "We are unable to verify invitations restrictions. Please try again.",
|
||||||
"invitationsBlockedMultiple": "Invitations are currently not allowed for the following reasons:",
|
"invitationsBlockedMultiple": "Invitations are currently not allowed for the following reasons:",
|
||||||
|
|||||||
@@ -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"();
|
||||||
@@ -223,37 +223,6 @@ $$ language plpgsql;
|
|||||||
grant
|
grant
|
||||||
execute on function public.get_upper_system_role () to service_role;
|
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 a trigger to update the account email when the primary owner email is updated
|
||||||
create
|
create
|
||||||
or replace function kit.handle_update_user_email () returns trigger language plpgsql security definer
|
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"
|
-- 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
|
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
|
set
|
||||||
search_path = '' as $$
|
search_path = '' as $$
|
||||||
declare
|
declare
|
||||||
new_account public.accounts;
|
new_account public.accounts;
|
||||||
|
owner_role varchar(50);
|
||||||
begin
|
begin
|
||||||
if (not public.is_set('enable_team_accounts')) then
|
if (not public.is_set('enable_team_accounts')) then
|
||||||
raise exception 'Team accounts are not enabled';
|
raise exception 'Team accounts are not enabled';
|
||||||
end if;
|
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(
|
insert into public.accounts(
|
||||||
name,
|
name,
|
||||||
is_personal_account)
|
slug,
|
||||||
|
is_personal_account,
|
||||||
|
primary_owner_user_id)
|
||||||
values (
|
values (
|
||||||
account_name,
|
account_name,
|
||||||
false)
|
account_slug,
|
||||||
returning
|
false,
|
||||||
* into new_account;
|
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;
|
return new_account;
|
||||||
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
$$ language plpgsql;
|
$$;
|
||||||
|
|
||||||
grant
|
grant
|
||||||
execute on function public.create_team_account (text) to authenticated,
|
execute on function public.create_team_account (text, uuid, text) to service_role;
|
||||||
service_role;
|
|
||||||
|
|
||||||
-- RLS(public.accounts)
|
-- RLS(public.accounts)
|
||||||
-- Authenticated users can delete team accounts
|
-- Authenticated users can delete team accounts
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ select tests.create_supabase_user('test1', 'test1@test.com');
|
|||||||
|
|
||||||
select tests.create_supabase_user('test2');
|
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 makerkit.authenticate_as('test1');
|
||||||
|
|
||||||
select public.create_team_account('Test');
|
|
||||||
|
|
||||||
-- the owner account has permissions to manage members
|
-- the owner account has permissions to manage members
|
||||||
select row_eq(
|
select row_eq(
|
||||||
$$ select public.has_permission(
|
$$ select public.has_permission(
|
||||||
|
|||||||
@@ -9,14 +9,16 @@ select tests.create_supabase_user('test1', 'test1@test.com');
|
|||||||
|
|
||||||
select tests.create_supabase_user('test2');
|
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 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
|
-- should automatically create slugs for the accounts
|
||||||
select row_eq(
|
select row_eq(
|
||||||
$$ select slug from public.accounts where name = 'Test' and slug = 'test' $$,
|
$$ select slug from public.accounts where name = 'Test' and slug = 'test' $$,
|
||||||
|
|||||||
@@ -12,12 +12,19 @@ select
|
|||||||
select
|
select
|
||||||
tests.create_supabase_user('test2');
|
tests.create_supabase_user('test2');
|
||||||
|
|
||||||
-- Create an team account
|
-- Create an team account (without explicit slug, should auto-generate)
|
||||||
select
|
-- DON'T authenticate first - the add_current_user_to_new_account trigger
|
||||||
makerkit.authenticate_as('test1');
|
-- 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
|
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
|
select
|
||||||
row_eq($$
|
row_eq($$
|
||||||
@@ -28,6 +35,44 @@ select
|
|||||||
'test'::text, 'Test'::varchar),
|
'test'::text, 'Test'::varchar),
|
||||||
'Users can create a team account');
|
'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
|
-- Should be the primary owner of the team account by default
|
||||||
select
|
select
|
||||||
row_eq($$
|
row_eq($$
|
||||||
@@ -106,12 +151,13 @@ create or replace function kit.single_account_per_owner()
|
|||||||
declare
|
declare
|
||||||
total_accounts int;
|
total_accounts int;
|
||||||
begin
|
begin
|
||||||
|
-- Check if this user already owns an account by checking NEW.primary_owner_user_id
|
||||||
select
|
select
|
||||||
count(id)
|
count(id)
|
||||||
from
|
from
|
||||||
public.accounts
|
public.accounts
|
||||||
where
|
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
|
if total_accounts > 0 then
|
||||||
raise exception 'User can only own 1 account';
|
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
|
before insert on public.accounts for each row
|
||||||
execute function kit.single_account_per_owner();
|
execute function kit.single_account_per_owner();
|
||||||
|
|
||||||
-- Create an team account
|
-- Try to create another team account for the same owner (should fail due to trigger)
|
||||||
select
|
set local role service_role;
|
||||||
makerkit.authenticate_as('test1');
|
|
||||||
|
|
||||||
select
|
select
|
||||||
throws_ok(
|
throws_ok(
|
||||||
$$ select
|
$$ 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;
|
set local role postgres;
|
||||||
|
|
||||||
@@ -151,11 +196,10 @@ select
|
|||||||
tests.create_supabase_user('updatetest2', 'updatetest2@test.com');
|
tests.create_supabase_user('updatetest2', 'updatetest2@test.com');
|
||||||
|
|
||||||
-- Create a team account for update tests
|
-- Create a team account for update tests
|
||||||
select
|
set local role service_role;
|
||||||
makerkit.authenticate_as('updatetest1');
|
|
||||||
|
|
||||||
select
|
select
|
||||||
public.create_team_account('UpdateTeam');
|
public.create_team_account('UpdateTeam', tests.get_supabase_uid('updatetest1'));
|
||||||
|
|
||||||
-- Add updatetest2 as a member
|
-- Add updatetest2 as a member
|
||||||
set local role postgres;
|
set local role postgres;
|
||||||
@@ -259,11 +303,10 @@ select
|
|||||||
tests.create_supabase_user('roletest2', 'roletest2@test.com');
|
tests.create_supabase_user('roletest2', 'roletest2@test.com');
|
||||||
|
|
||||||
-- Create a team account for role tests
|
-- Create a team account for role tests
|
||||||
select
|
set local role service_role;
|
||||||
makerkit.authenticate_as('roletest1');
|
|
||||||
|
|
||||||
select
|
select
|
||||||
public.create_team_account('RoleTeam');
|
public.create_team_account('RoleTeam', tests.get_supabase_uid('roletest1'));
|
||||||
|
|
||||||
-- Add roletest2 as a member
|
-- Add roletest2 as a member
|
||||||
set local role postgres;
|
set local role postgres;
|
||||||
@@ -333,11 +376,10 @@ select
|
|||||||
tests.create_supabase_user('deletetest2', 'deletetest2@test.com');
|
tests.create_supabase_user('deletetest2', 'deletetest2@test.com');
|
||||||
|
|
||||||
-- Create a team account for delete tests
|
-- Create a team account for delete tests
|
||||||
select
|
set local role service_role;
|
||||||
makerkit.authenticate_as('deletetest1');
|
|
||||||
|
|
||||||
select
|
select
|
||||||
public.create_team_account('DeleteTeam');
|
public.create_team_account('DeleteTeam', tests.get_supabase_uid('deletetest1'));
|
||||||
|
|
||||||
-- Add deletetest2 as a member
|
-- Add deletetest2 as a member
|
||||||
set local role postgres;
|
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');
|
select tests.create_supabase_user('permtest3', 'permtest3@test.com');
|
||||||
|
|
||||||
-- Create a team account for permission tests
|
-- Create a team account for permission tests
|
||||||
select makerkit.authenticate_as('permtest1');
|
set local role service_role;
|
||||||
select public.create_team_account('PermTeam');
|
select public.create_team_account('PermTeam', tests.get_supabase_uid('permtest1'));
|
||||||
|
|
||||||
-- Get the account ID for PermTeam to avoid NULL references
|
-- Get the account ID for PermTeam to avoid NULL references
|
||||||
set local role postgres;
|
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');
|
select tests.create_supabase_user('hiertest4', 'hiertest4@test.com');
|
||||||
|
|
||||||
-- Create a team account for hierarchy tests
|
-- Create a team account for hierarchy tests
|
||||||
select makerkit.authenticate_as('hiertest1');
|
set local role service_role;
|
||||||
select public.create_team_account('HierTeam');
|
select public.create_team_account('HierTeam', tests.get_supabase_uid('hiertest1'));
|
||||||
|
|
||||||
-- Add users with different roles
|
-- Add users with different roles
|
||||||
set local role postgres;
|
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');
|
select tests.create_supabase_user('vistest3', 'vistest3@test.com');
|
||||||
|
|
||||||
-- Create a team account
|
-- Create a team account
|
||||||
select makerkit.authenticate_as('vistest1');
|
set local role service_role;
|
||||||
select public.create_team_account('VisTeam');
|
select public.create_team_account('VisTeam', tests.get_supabase_uid('vistest1'));
|
||||||
|
|
||||||
-- Add vistest2 as a member
|
-- Add vistest2 as a member
|
||||||
set local role postgres;
|
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');
|
select tests.create_supabase_user('functest2', 'functest2@test.com');
|
||||||
|
|
||||||
-- Create team account
|
-- Create team account
|
||||||
select makerkit.authenticate_as('functest1');
|
set local role service_role;
|
||||||
select public.create_team_account('FuncTeam');
|
select public.create_team_account('FuncTeam', tests.get_supabase_uid('functest1'));
|
||||||
|
|
||||||
-- Test: get_account_members function properly restricts data
|
-- Test: get_account_members function properly restricts data
|
||||||
select makerkit.authenticate_as('functest2');
|
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');
|
select tests.create_supabase_user('ownerupdate2', 'ownerupdate2@test.com');
|
||||||
|
|
||||||
-- Create team account
|
-- Create team account
|
||||||
select makerkit.authenticate_as('ownerupdate1');
|
set local role service_role;
|
||||||
select public.create_team_account('TeamChange');
|
select public.create_team_account('TeamChange', tests.get_supabase_uid('ownerupdate1'));
|
||||||
|
|
||||||
-- Update the team name as the owner
|
-- Update the team name as the owner
|
||||||
|
select makerkit.authenticate_as('ownerupdate1');
|
||||||
select lives_ok(
|
select lives_ok(
|
||||||
$$ UPDATE public.accounts
|
$$ UPDATE public.accounts
|
||||||
SET name = 'Updated Owner Team'
|
SET name = 'Updated Owner Team'
|
||||||
@@ -668,23 +711,16 @@ select
|
|||||||
tests.create_supabase_user('crosstest2', 'crosstest2@test.com');
|
tests.create_supabase_user('crosstest2', 'crosstest2@test.com');
|
||||||
|
|
||||||
-- Create first team account with crosstest1 as owner
|
-- Create first team account with crosstest1 as owner
|
||||||
select
|
set local role service_role;
|
||||||
makerkit.authenticate_as('crosstest1');
|
|
||||||
|
|
||||||
select
|
select
|
||||||
public.create_team_account('TeamA');
|
public.create_team_account('TeamA', tests.get_supabase_uid('crosstest1'));
|
||||||
|
|
||||||
-- Create second team account with crosstest2 as owner
|
-- Create second team account with crosstest2 as owner
|
||||||
select
|
select
|
||||||
makerkit.authenticate_as('crosstest2');
|
public.create_team_account('TeamB', tests.get_supabase_uid('crosstest2'));
|
||||||
|
|
||||||
select
|
|
||||||
public.create_team_account('TeamB');
|
|
||||||
|
|
||||||
-- Add crosstest2 as a member to TeamA
|
-- Add crosstest2 as a member to TeamA
|
||||||
select
|
|
||||||
makerkit.authenticate_as('crosstest1');
|
|
||||||
|
|
||||||
set local role postgres;
|
set local role postgres;
|
||||||
|
|
||||||
-- Add member to first team
|
-- Add member to first team
|
||||||
@@ -767,6 +803,31 @@ select
|
|||||||
'TeamB name should remain unchanged after attempted update by non-member'
|
'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
|
select
|
||||||
*
|
*
|
||||||
from
|
from
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ select plan(12);
|
|||||||
--- Create test users
|
--- Create test users
|
||||||
select tests.create_supabase_user('trigger_test_user1', 'test1@example.com');
|
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
|
--- Test accounts table timestamp triggers - INSERT
|
||||||
------------
|
------------
|
||||||
|
|
||||||
INSERT INTO public.accounts (name, is_personal_account)
|
-- Use service_role to insert (create_org_account policy was removed)
|
||||||
VALUES ('Test Account', false);
|
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 ok(
|
||||||
(SELECT created_at IS NOT NULL FROM public.accounts WHERE name = 'Test Account'),
|
(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
|
--- Test invitations table timestamp triggers - INSERT
|
||||||
------------
|
------------
|
||||||
|
|
||||||
-- Create a team account for invitation testing
|
-- Create a team account for invitation testing (still in service_role from above)
|
||||||
INSERT INTO public.accounts (name, is_personal_account)
|
INSERT INTO public.accounts (name, is_personal_account, primary_owner_user_id)
|
||||||
VALUES ('Invitation Test Team', false);
|
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)
|
-- Switch to service_role to insert invitations (INSERT policy removed, handled by server action)
|
||||||
set role service_role;
|
set role service_role;
|
||||||
@@ -56,9 +56,7 @@ VALUES (
|
|||||||
now() + interval '7 days'
|
now() + interval '7 days'
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Switch back to authenticated user for assertion
|
-- Stay in service_role for assertions (testing triggers, not RLS)
|
||||||
select makerkit.authenticate_as('trigger_test_user1');
|
|
||||||
|
|
||||||
SELECT ok(
|
SELECT ok(
|
||||||
(SELECT created_at IS NOT NULL FROM public.invitations WHERE email = 'invitee@example.com'),
|
(SELECT created_at IS NOT NULL FROM public.invitations WHERE email = 'invitee@example.com'),
|
||||||
'invitations: created_at should be set automatically on insert'
|
'invitations: created_at should be set automatically on insert'
|
||||||
|
|||||||
@@ -13,12 +13,19 @@ select tests.create_supabase_user('user_tracking_test1', 'tracking1@example.com'
|
|||||||
--- Test accounts table user tracking triggers - INSERT
|
--- 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');
|
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
|
-- Test INSERT: created_by and updated_by should be set to current user
|
||||||
INSERT INTO public.accounts (name, is_personal_account)
|
INSERT INTO public.accounts (name, is_personal_account, primary_owner_user_id)
|
||||||
VALUES ('User Tracking Test Account', false);
|
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 ok(
|
||||||
(SELECT created_by = tests.get_supabase_uid('user_tracking_test1')
|
(SELECT created_by = tests.get_supabase_uid('user_tracking_test1')
|
||||||
|
|||||||
@@ -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_owner', 'update-owner@test.com');
|
||||||
select tests.create_supabase_user('update_test_member', 'update-member@test.com');
|
select tests.create_supabase_user('update_test_member', 'update-member@test.com');
|
||||||
|
|
||||||
-- Authenticate as owner to create team account
|
-- Create team account using service_role and create_team_account function
|
||||||
select makerkit.authenticate_as('update_test_owner');
|
-- 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)
|
select public.create_team_account('Update Test Team', tests.get_supabase_uid('update_test_owner'));
|
||||||
insert into public.accounts (name, is_personal_account)
|
|
||||||
values ('Update Test Team', false);
|
|
||||||
|
|
||||||
-- Add member to the team with 'member' role using service_role
|
-- Add member to the team with 'member' role (still in service_role)
|
||||||
set role service_role;
|
|
||||||
|
|
||||||
insert into public.accounts_memberships (account_id, user_id, account_role)
|
insert into public.accounts_memberships (account_id, user_id, account_role)
|
||||||
values (
|
values (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next-supabase-saas-kit-turbo",
|
"name": "next-supabase-saas-kit-turbo",
|
||||||
"version": "2.23.1",
|
"version": "2.23.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useMemo, useState, useTransition } from 'react';
|
||||||
|
|
||||||
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
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 { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
@@ -29,7 +29,10 @@ import { If } from '@kit/ui/if';
|
|||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { Trans } from '@kit/ui/trans';
|
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';
|
import { createTeamAccountAction } from '../server/actions/create-team-account-server-actions';
|
||||||
|
|
||||||
export function CreateTeamAccountDialog(
|
export function CreateTeamAccountDialog(
|
||||||
@@ -67,10 +70,18 @@ function CreateOrganizationAccountForm(props: { onClose: () => void }) {
|
|||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: '',
|
name: '',
|
||||||
|
slug: '',
|
||||||
},
|
},
|
||||||
resolver: zodResolver(CreateTeamSchema),
|
resolver: zodResolver(CreateTeamSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nameValue = useWatch({ control: form.control, name: 'name' });
|
||||||
|
|
||||||
|
const showSlugField = useMemo(
|
||||||
|
() => NON_LATIN_REGEX.test(nameValue ?? ''),
|
||||||
|
[nameValue],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -107,7 +118,7 @@ function CreateOrganizationAccountForm(props: { onClose: () => void }) {
|
|||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
data-test={'create-team-name-input'}
|
data-test={'team-name-input'}
|
||||||
required
|
required
|
||||||
minLength={2}
|
minLength={2}
|
||||||
maxLength={50}
|
maxLength={50}
|
||||||
@@ -126,6 +137,38 @@ function CreateOrganizationAccountForm(props: { onClose: () => void }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<If condition={showSlugField}>
|
||||||
|
<FormField
|
||||||
|
name={'slug'}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans i18nKey={'teams:teamSlugLabel'} />
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
data-test={'team-slug-input'}
|
||||||
|
required
|
||||||
|
minLength={2}
|
||||||
|
maxLength={50}
|
||||||
|
placeholder={'my-team'}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormDescription>
|
||||||
|
<Trans i18nKey={'teams:teamSlugDescription'} />
|
||||||
|
</FormDescription>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
|
|
||||||
<div className={'flex justify-end space-x-2'}>
|
<div className={'flex justify-end space-x-2'}>
|
||||||
<Button
|
<Button
|
||||||
variant={'outline'}
|
variant={'outline'}
|
||||||
|
|||||||
@@ -5,18 +5,21 @@ import { useTransition } from 'react';
|
|||||||
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Building } from 'lucide-react';
|
import { Building, Link } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@kit/ui/form';
|
} from '@kit/ui/form';
|
||||||
|
import { If } from '@kit/ui/if';
|
||||||
import {
|
import {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
@@ -25,6 +28,7 @@ import {
|
|||||||
import { toast } from '@kit/ui/sonner';
|
import { toast } from '@kit/ui/sonner';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import { containsNonLatinCharacters } from '../../schema/create-team.schema';
|
||||||
import { TeamNameFormSchema } from '../../schema/update-team-name.schema';
|
import { TeamNameFormSchema } from '../../schema/update-team-name.schema';
|
||||||
import { updateTeamAccountName } from '../../server/actions/team-details-server-actions';
|
import { updateTeamAccountName } from '../../server/actions/team-details-server-actions';
|
||||||
|
|
||||||
@@ -43,9 +47,13 @@ export const UpdateTeamAccountNameForm = (props: {
|
|||||||
resolver: zodResolver(TeamNameFormSchema),
|
resolver: zodResolver(TeamNameFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: props.account.name,
|
name: props.account.name,
|
||||||
|
newSlug: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nameValue = useWatch({ control: form.control, name: 'name' });
|
||||||
|
const showSlugField = containsNonLatinCharacters(nameValue || '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'space-y-8'}>
|
<div className={'space-y-8'}>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -60,6 +68,7 @@ export const UpdateTeamAccountNameForm = (props: {
|
|||||||
const result = await updateTeamAccountName({
|
const result = await updateTeamAccountName({
|
||||||
slug: props.account.slug,
|
slug: props.account.slug,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
newSlug: data.newSlug || undefined,
|
||||||
path: props.path,
|
path: props.path,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,6 +76,10 @@ export const UpdateTeamAccountNameForm = (props: {
|
|||||||
toast.success(t('updateTeamSuccessMessage'), {
|
toast.success(t('updateTeamSuccessMessage'), {
|
||||||
id: toastId,
|
id: toastId,
|
||||||
});
|
});
|
||||||
|
} else if (result.error) {
|
||||||
|
toast.error(t(result.error), {
|
||||||
|
id: toastId,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.error(t('updateTeamErrorMessage'), {
|
toast.error(t('updateTeamErrorMessage'), {
|
||||||
id: toastId,
|
id: toastId,
|
||||||
@@ -91,6 +104,10 @@ export const UpdateTeamAccountNameForm = (props: {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans i18nKey={'teams:teamNameLabel'} />
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<InputGroup className="dark:bg-background">
|
<InputGroup className="dark:bg-background">
|
||||||
<InputGroupAddon align="inline-start">
|
<InputGroupAddon align="inline-start">
|
||||||
@@ -112,6 +129,42 @@ export const UpdateTeamAccountNameForm = (props: {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<If condition={showSlugField}>
|
||||||
|
<FormField
|
||||||
|
name={'newSlug'}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans i18nKey={'teams:teamSlugLabel'} />
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<InputGroup className="dark:bg-background">
|
||||||
|
<InputGroupAddon align="inline-start">
|
||||||
|
<Link className="h-4 w-4" />
|
||||||
|
</InputGroupAddon>
|
||||||
|
|
||||||
|
<InputGroupInput
|
||||||
|
data-test={'team-slug-input'}
|
||||||
|
required
|
||||||
|
placeholder={'my-team'}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormDescription>
|
||||||
|
<Trans i18nKey={'teams:teamSlugDescription'} />
|
||||||
|
</FormDescription>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className={'w-full md:w-auto'}
|
className={'w-full md:w-auto'}
|
||||||
|
|||||||
@@ -15,12 +15,51 @@ const RESERVED_NAMES_ARRAY = [
|
|||||||
const SPECIAL_CHARACTERS_REGEX = /[!@#$%^&*()+=[\]{};':"\\|,.<>/?]/;
|
const SPECIAL_CHARACTERS_REGEX = /[!@#$%^&*()+=[\]{};':"\\|,.<>/?]/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Regex that matches only Latin characters (a-z, A-Z), numbers, spaces, and hyphens
|
* Regex that detects non-Latin scripts (Korean, Japanese, Chinese, Cyrillic, Arabic, Hebrew, Thai)
|
||||||
|
* Does NOT match extended Latin characters like café, naïve, Zürich
|
||||||
*/
|
*/
|
||||||
const LATIN_ONLY_REGEX = /^[a-zA-Z0-9\s-]+$/;
|
export const NON_LATIN_REGEX =
|
||||||
|
/[\u0400-\u04FF\u0590-\u05FF\u0600-\u06FF\u0E00-\u0E7F\u1100-\u11FF\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\uAC00-\uD7AF]/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex for valid slugs: lowercase letters, numbers, and hyphens
|
||||||
|
* Must start and end with alphanumeric, hyphens only in middle
|
||||||
|
*/
|
||||||
|
const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name containsNonLatinCharacters
|
||||||
|
* @description Checks if a string contains non-Latin characters
|
||||||
|
*/
|
||||||
|
export function containsNonLatinCharacters(value: string): boolean {
|
||||||
|
return NON_LATIN_REGEX.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name SlugSchema
|
||||||
|
* @description Schema for validating URL-friendly slugs
|
||||||
|
*/
|
||||||
|
export const SlugSchema = z
|
||||||
|
.string({
|
||||||
|
description: 'URL-friendly identifier for the team',
|
||||||
|
})
|
||||||
|
.min(2)
|
||||||
|
.max(50)
|
||||||
|
.regex(SLUG_REGEX, {
|
||||||
|
message: 'teams:invalidSlugError',
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(slug) => {
|
||||||
|
return !RESERVED_NAMES_ARRAY.includes(slug.toLowerCase());
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'teams:reservedNameError',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name TeamNameSchema
|
* @name TeamNameSchema
|
||||||
|
* @description Schema for team name - allows non-Latin characters
|
||||||
*/
|
*/
|
||||||
export const TeamNameSchema = z
|
export const TeamNameSchema = z
|
||||||
.string({
|
.string({
|
||||||
@@ -36,14 +75,6 @@ export const TeamNameSchema = z
|
|||||||
message: 'teams:specialCharactersError',
|
message: 'teams:specialCharactersError',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.refine(
|
|
||||||
(name) => {
|
|
||||||
return LATIN_ONLY_REGEX.test(name);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'teams:nonLatinCharactersError',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.refine(
|
.refine(
|
||||||
(name) => {
|
(name) => {
|
||||||
return !RESERVED_NAMES_ARRAY.includes(name.toLowerCase());
|
return !RESERVED_NAMES_ARRAY.includes(name.toLowerCase());
|
||||||
@@ -56,7 +87,27 @@ export const TeamNameSchema = z
|
|||||||
/**
|
/**
|
||||||
* @name CreateTeamSchema
|
* @name CreateTeamSchema
|
||||||
* @description Schema for creating a team account
|
* @description Schema for creating a team account
|
||||||
|
* When the name contains non-Latin characters, a slug is required
|
||||||
*/
|
*/
|
||||||
export const CreateTeamSchema = z.object({
|
export const CreateTeamSchema = z
|
||||||
|
.object({
|
||||||
name: TeamNameSchema,
|
name: TeamNameSchema,
|
||||||
});
|
// Transform empty strings to undefined before validation
|
||||||
|
slug: z.preprocess(
|
||||||
|
(val) => (val === '' ? undefined : val),
|
||||||
|
SlugSchema.optional(),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (containsNonLatinCharacters(data.name)) {
|
||||||
|
return !!data.slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'teams:slugRequiredForNonLatinName',
|
||||||
|
path: ['slug'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,12 +1,34 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { TeamNameSchema } from './create-team.schema';
|
import {
|
||||||
|
SlugSchema,
|
||||||
|
TeamNameSchema,
|
||||||
|
containsNonLatinCharacters,
|
||||||
|
} from './create-team.schema';
|
||||||
|
|
||||||
export const TeamNameFormSchema = z.object({
|
export const TeamNameFormSchema = z
|
||||||
|
.object({
|
||||||
name: TeamNameSchema,
|
name: TeamNameSchema,
|
||||||
});
|
// Transform empty strings to undefined before validation
|
||||||
|
newSlug: z.preprocess(
|
||||||
|
(val) => (val === '' ? undefined : val),
|
||||||
|
SlugSchema.optional(),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (containsNonLatinCharacters(data.name)) {
|
||||||
|
return !!data.newSlug;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'teams:slugRequiredForNonLatinName',
|
||||||
|
path: ['newSlug'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const UpdateTeamNameSchema = TeamNameFormSchema.merge(
|
export const UpdateTeamNameSchema = TeamNameFormSchema.and(
|
||||||
z.object({
|
z.object({
|
||||||
slug: z.string().min(1).max(255),
|
slug: z.string().min(1).max(255),
|
||||||
path: z.string().min(1).max(255),
|
path: z.string().min(1).max(255),
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
|
import 'server-only';
|
||||||
|
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { enhanceAction } from '@kit/next/actions';
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
|
||||||
|
|
||||||
import { CreateTeamSchema } from '../../schema/create-team.schema';
|
import { CreateTeamSchema } from '../../schema/create-team.schema';
|
||||||
import { createAccountCreationPolicyEvaluator } from '../policies';
|
import { createAccountCreationPolicyEvaluator } from '../policies';
|
||||||
import { createCreateTeamAccountService } from '../services/create-team-account.service';
|
import { createCreateTeamAccountService } from '../services/create-team-account.service';
|
||||||
|
|
||||||
export const createTeamAccountAction = enhanceAction(
|
export const createTeamAccountAction = enhanceAction(
|
||||||
async ({ name }, user) => {
|
async ({ name, slug }, user) => {
|
||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
const client = getSupabaseServerClient();
|
const service = createCreateTeamAccountService();
|
||||||
const service = createCreateTeamAccountService(client);
|
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
name: 'team-accounts.create',
|
name: 'team-accounts.create',
|
||||||
@@ -52,12 +52,19 @@ export const createTeamAccountAction = enhanceAction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service throws on error, so no need to check for error
|
const { data, error } = await service.createNewOrganizationAccount({
|
||||||
const { data } = await service.createNewOrganizationAccount({
|
|
||||||
name,
|
name,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
slug,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (error === 'duplicate_slug') {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
message: 'teams:duplicateSlugError',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(ctx, `Team account created`);
|
logger.info(ctx, `Team account created`);
|
||||||
|
|
||||||
const accountHomePath = '/home/' + data.slug;
|
const accountHomePath = '/home/' + data.slug;
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ export const updateTeamAccountName = enhanceAction(
|
|||||||
async (params) => {
|
async (params) => {
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
const { name, path, slug } = params;
|
const { name, path, slug, newSlug } = params;
|
||||||
|
|
||||||
|
const slugToUpdate = newSlug ?? slug;
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
name: 'team-accounts.update',
|
name: 'team-accounts.update',
|
||||||
@@ -25,7 +27,7 @@ export const updateTeamAccountName = enhanceAction(
|
|||||||
.from('accounts')
|
.from('accounts')
|
||||||
.update({
|
.update({
|
||||||
name,
|
name,
|
||||||
slug,
|
slug: slugToUpdate,
|
||||||
})
|
})
|
||||||
.match({
|
.match({
|
||||||
slug,
|
slug,
|
||||||
@@ -34,17 +36,25 @@ export const updateTeamAccountName = enhanceAction(
|
|||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
// Handle duplicate slug error
|
||||||
|
if (error.code === '23505') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'teams:duplicateSlugError',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
logger.error({ ...ctx, error }, `Failed to update team name`);
|
logger.error({ ...ctx, error }, `Failed to update team name`);
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSlug = data.slug;
|
const updatedSlug = data.slug;
|
||||||
|
|
||||||
logger.info(ctx, `Team name updated`);
|
logger.info(ctx, `Team name updated`);
|
||||||
|
|
||||||
if (newSlug) {
|
if (updatedSlug && updatedSlug !== slug) {
|
||||||
const nextPath = path.replace('[account]', newSlug);
|
const nextPath = path.replace('[account]', updatedSlug);
|
||||||
|
|
||||||
redirect(nextPath);
|
redirect(nextPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,57 @@
|
|||||||
import 'server-only';
|
import 'server-only';
|
||||||
|
|
||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
|
||||||
|
|
||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
import { Database } from '@kit/supabase/database';
|
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||||
|
|
||||||
export function createCreateTeamAccountService(
|
export function createCreateTeamAccountService() {
|
||||||
client: SupabaseClient<Database>,
|
return new CreateTeamAccountService();
|
||||||
) {
|
|
||||||
return new CreateTeamAccountService(client);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateTeamAccountService {
|
class CreateTeamAccountService {
|
||||||
private readonly namespace = 'accounts.create-team-account';
|
private readonly namespace = 'accounts.create-team-account';
|
||||||
|
|
||||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
async createNewOrganizationAccount(params: {
|
||||||
|
name: string;
|
||||||
async createNewOrganizationAccount(params: { name: string; userId: string }) {
|
userId: string;
|
||||||
|
slug?: string;
|
||||||
|
}) {
|
||||||
|
const client = getSupabaseServerAdminClient();
|
||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
const ctx = { ...params, namespace: this.namespace };
|
const ctx = { ...params, namespace: this.namespace };
|
||||||
|
|
||||||
logger.info(ctx, `Creating new team account...`);
|
logger.info(ctx, `Creating new team account...`);
|
||||||
|
|
||||||
const { error, data } = await this.client.rpc('create_team_account', {
|
// Call the RPC function which handles:
|
||||||
|
// 1. Checking if team accounts are enabled
|
||||||
|
// 2. Creating the account with name, slug, and primary_owner_user_id
|
||||||
|
// 3. Creating membership for the owner (atomic transaction)
|
||||||
|
const { error, data } = await client.rpc('create_team_account', {
|
||||||
account_name: params.name,
|
account_name: params.name,
|
||||||
|
user_id: params.userId,
|
||||||
|
account_slug: params.slug,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error(
|
// Handle duplicate slug error
|
||||||
{
|
if (error.code === '23505' && error.message.includes('slug')) {
|
||||||
error,
|
logger.warn(
|
||||||
...ctx,
|
{ ...ctx, slug: params.slug },
|
||||||
},
|
`Duplicate slug detected, rejecting team creation`,
|
||||||
`Error creating team account`,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
error: 'duplicate_slug' as const,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error({ error, ...ctx }, `Error creating team account`);
|
||||||
|
|
||||||
throw new Error('Error creating team account');
|
throw new Error('Error creating team account');
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(ctx, `Team account created successfully`);
|
logger.info(ctx, `Team account created successfully`);
|
||||||
|
|
||||||
return { data, error };
|
return { data, error: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user