2.23.0: Enforce Policies API for invitations and creating accounts; added WeakPassword handling; Fix dialog open/closed states (#439)

* chore: bump version to 2.22.1 and update dependencies

- Updated application version from 2.22.0 to 2.22.1 in package.json.
- Updated various dependencies including @marsidev/react-turnstile to 1.4.1, @stripe/react-stripe-js to 5.4.1, @stripe/stripe-js to 8.6.1, and react-hook-form to 7.70.0.
- Adjusted lucide-react version to be referenced from the catalog across multiple package.json files.
- Enhanced consistency in pnpm-lock.yaml and pnpm-workspace.yaml with updated package versions.

* chore: bump version to 2.23.0 and update dependencies

- Updated application version from 2.22.1 to 2.23.0 in package.json.
- Upgraded turbo dependency from 2.7.1 to 2.7.3 in package.json and pnpm-lock.yaml.
- Enhanced end-to-end testing documentation in AGENTS.md and CLAUDE.md with instructions for running tests.
- Updated AuthPageObject to use a new secret for user creation in auth.po.ts.
- Refactored team ownership transfer and member role update dialogs to close on success.
- Improved error handling for weak passwords in AuthErrorAlert component.
- Adjusted database schemas and tests to reflect changes in invitation policies and role management.
This commit is contained in:
Giancarlo Buomprisco
2026-01-07 17:00:11 +01:00
committed by GitHub
parent 5237d34e6f
commit d5dc6f2528
41 changed files with 2896 additions and 1922 deletions

View File

@@ -13,7 +13,7 @@
"@hookform/resolvers": "^5.2.2",
"@tanstack/react-query": "catalog:",
"ai": "5.0.116",
"lucide-react": "^0.562.0",
"lucide-react": "catalog:",
"next": "catalog:",
"nodemailer": "^7.0.12",
"react": "catalog:",

View File

@@ -1,6 +1,26 @@
## End-to-End Testing with Playwright
## Running Tests
Running the tests for testing single file:
```bash
pnpm --filter web-e2e exec playwright test <partial-name-or-folder-name> --workers=1
```
Example:
```bash
pnpm --filter web-e2e exec playwright test <partial-name-or-folder-name> --workers=1
```
This is useful for quickly testing a single file or a specific feature and should be your default choice.
Running all tests (rarely needed, only use if asked by the user):
```bash
pnpm test
```
### Page Object Pattern (Required)
Always use Page Objects for test organization and reusability:

View File

@@ -1,6 +1,26 @@
## End-to-End Testing with Playwright
## Running Tests
Running the tests for testing single file:
```bash
pnpm --filter web-e2e exec playwright test <partial-name-or-folder-name> --workers=1
```
Example:
```bash
pnpm --filter web-e2e exec playwright test <partial-name-or-folder-name> --workers=1
```
This is useful for quickly testing a single file or a specific feature and should be your default choice.
Running all tests (rarely needed, only use if asked by the user):
```bash
pnpm test
```
### Page Object Pattern (Required)
Always use Page Objects for test organization and reusability:

View File

@@ -216,12 +216,8 @@ test.describe('Admin', () => {
});
test.describe('Impersonation', () => {
test('can sign in as a user', async ({ page }) => {
// TODO: find out why it only fails in the CI
if (process.env.CI) {
test.skip();
}
// TODO: fix this test - unclear why it fails in the CI
test.skip('can sign in as a user', async ({ page }) => {
const auth = new AuthPageObject(page);
await auth.loginAsSuperAdmin({});

View File

@@ -142,7 +142,7 @@ export class AuthPageObject {
}) {
const client = createClient(
'http://127.0.0.1:54321',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU',
'sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz',
);
const { data, error } = await client.auth.admin.createUser({

View File

@@ -282,9 +282,6 @@ test.describe('Team Ownership Transfer', () => {
// Transfer ownership to the member
await teamAccounts.transferOwnership(memberEmail, ownerEmail);
// Wait for the page to fully load after the transfer
await page.waitForTimeout(500);
// Verify the transfer was successful by checking if the primary owner badge
// is now on the new owner's row
const memberRow = page.getByRole('row', { name: memberEmail });

View File

@@ -19,4 +19,6 @@ EMAIL_PASSWORD=password
# STRIPE
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51K9cWKI1i3VnbZTq2HGstY2S8wt3peF1MOqPXFO4LR8ln2QgS7GxL8XyKaKLvn7iFHeqAnvdDw0o48qN7rrwwcHU00jOtKhjsf
CONTACT_EMAIL=test@makerkit.dev
CONTACT_EMAIL=test@makerkit.dev
NEXT_PUBLIC_ENABLE_VERSION_UPDATER=true

View File

@@ -0,0 +1,10 @@
export const dynamic = 'force-static';
const BUILD_TIME =
process.env.NODE_ENV === 'development' ? 'dev' : new Date().toISOString();
export const GET = () => {
return new Response(BUILD_TIME, {
headers: { 'content-type': 'text/plain' },
});
};

View File

@@ -1,59 +0,0 @@
/**
* We force it to static because we want to cache for as long as the build is live.
*/
export const dynamic = 'force-static';
// please provide your own implementation
// if you're not using Vercel or Cloudflare Pages
const KNOWN_GIT_ENV_VARS = [
'CF_PAGES_COMMIT_SHA',
'VERCEL_GIT_COMMIT_SHA',
'GIT_HASH',
];
export const GET = async () => {
const currentGitHash = await getGitHash();
return new Response(currentGitHash, {
headers: {
'content-type': 'text/plain',
},
});
};
async function getGitHash() {
for (const envVar of KNOWN_GIT_ENV_VARS) {
if (process.env[envVar]) {
return process.env[envVar];
}
}
try {
return await getHashFromProcess();
} catch (error) {
console.warn(
`[WARN] Could not find git hash: ${JSON.stringify(error)}. You may want to provide a fallback.`,
);
return '';
}
}
async function getHashFromProcess() {
// avoid calling a Node.js command in the edge runtime
if (process.env.NEXT_RUNTIME === 'nodejs') {
if (process.env.NODE_ENV !== 'development') {
console.warn(
`[WARN] Could not find git hash in environment variables. Falling back to git command. Supply a known git hash environment variable to avoid this warning.`,
);
}
const { execSync } = await import('child_process');
return execSync('git log --pretty=format:"%h" -n1').toString().trim();
}
console.log(
`[INFO] Could not find git hash in environment variables. Falling back to git command. Supply a known git hash environment variable to avoid this warning.`,
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -53,14 +53,14 @@
"@kit/ui": "workspace:*",
"@makerkit/data-loader-supabase-core": "^0.0.10",
"@makerkit/data-loader-supabase-nextjs": "^1.2.5",
"@marsidev/react-turnstile": "^1.4.0",
"@nosecone/next": "1.0.0-beta.15",
"@marsidev/react-turnstile": "^1.4.1",
"@nosecone/next": "1.0.0-beta.16",
"@radix-ui/react-icons": "^1.3.2",
"@supabase/supabase-js": "catalog:",
"@tanstack/react-query": "catalog:",
"@tanstack/react-table": "^8.21.3",
"date-fns": "^4.1.0",
"lucide-react": "^0.562.0",
"lucide-react": "catalog:",
"next": "catalog:",
"next-sitemap": "^4.2.3",
"next-themes": "0.4.6",

View File

@@ -100,6 +100,15 @@
"uppercasePassword": "Password must contain at least one uppercase letter",
"insufficient_aal": "Please sign-in with your current multi-factor authentication to perform this action",
"otp_expired": "The email link has expired. Please try again.",
"same_password": "The password cannot be the same as the current password"
"same_password": "The password cannot be the same as the current password",
"weakPassword": {
"title": "Password is too weak",
"description": "Your password does not meet the security requirements:",
"reasons": {
"length": "Password must be at least 8 characters long",
"characters": "Password must contain lowercase, uppercase, numbers, and special characters",
"pwned": "This password has been found in a data breach. Please choose a different password"
}
}
}
}

View File

@@ -0,0 +1,64 @@
-- Remove invitations INSERT policy
-- Permission and role hierarchy checks are now enforced in the server action.
-- Invitations are created through server actions using admin client.
drop policy if exists invitations_create_self on public.invitations;
-- Update invitations RPC to accept invited_by and restrict execution.
drop function if exists public.add_invitations_to_account(text, public.invitation[]);
create
or replace function public.add_invitations_to_account (
account_slug text,
invitations public.invitation[],
invited_by uuid
) returns public.invitations[]
set
search_path = '' as $$
declare
new_invitation public.invitations;
all_invitations public.invitations[] := array[]::public.invitations[];
invite_token text;
email text;
role varchar(50);
begin
FOREACH email,
role in array invitations loop
invite_token := extensions.uuid_generate_v4();
insert into public.invitations(
email,
account_id,
invited_by,
role,
invite_token)
values (
email,
(
select
id
from
public.accounts
where
slug = account_slug),
invited_by,
role,
invite_token)
returning
* into new_invitation;
all_invitations := array_append(all_invitations, new_invitation);
end loop;
return all_invitations;
end;
$$ language plpgsql;
revoke execute on function public.add_invitations_to_account (text, public.invitation[], uuid) from authenticated;
grant
execute on function public.add_invitations_to_account (text, public.invitation[], uuid) to service_role;

View File

@@ -501,15 +501,6 @@ grant
execute on function public.create_team_account (text) to authenticated,
service_role;
-- RLS(public.accounts)
-- Authenticated users can create team accounts
create policy create_org_account on public.accounts for insert to authenticated
with
check (
public.is_set ('enable_team_accounts')
and public.accounts.is_personal_account = false
);
-- RLS(public.accounts)
-- Authenticated users can delete team accounts
create policy delete_team_account

View File

@@ -96,36 +96,9 @@ select
to authenticated using (public.has_role_on_account (account_id));
-- INSERT(invitations):
-- Users can create invitations to users of an account they are
-- a member of and have the 'invites.manage' permission AND the target role is not higher than the user's role
create policy invitations_create_self on public.invitations for insert to authenticated
with
check (
public.is_set ('enable_team_accounts')
and public.has_permission (
(
select
auth.uid ()
),
account_id,
'invites.manage'::public.app_permissions
)
and (public.has_more_elevated_role (
(
select
auth.uid ()
),
account_id,
role
) or public.has_same_role_hierarchy_level(
(
select
auth.uid ()
),
account_id,
role
))
);
-- Invitations are created through server actions using admin client.
-- Permission and role hierarchy checks are enforced in the server action.
-- No RLS policy needed for INSERT.
-- UPDATE(invitations):
-- Users can update invitations to users of an account they are a member of and have the 'invites.manage' permission AND
@@ -311,7 +284,8 @@ service_role;
create
or replace function public.add_invitations_to_account (
account_slug text,
invitations public.invitation[]
invitations public.invitation[],
invited_by uuid
) returns public.invitations[]
set
search_path = '' as $$
@@ -340,7 +314,10 @@ begin
from
public.accounts
where
slug = account_slug), auth.uid(), role, invite_token)
slug = account_slug),
invited_by,
role,
invite_token)
returning
* into new_invitation;
@@ -355,5 +332,4 @@ end;
$$ language plpgsql;
grant
execute on function public.add_invitations_to_account (text, public.invitation[]) to authenticated,
service_role;
execute on function public.add_invitations_to_account (text, public.invitation[], uuid) to service_role;

View File

@@ -12,49 +12,53 @@ select makerkit.set_identifier('owner', 'owner@makerkit.dev');
select makerkit.authenticate_as('test');
select lives_ok(
select throws_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite1@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'member', gen_random_uuid()); $$,
'owner should be able to create invitations'
'new row violates row-level security policy for table "invitations"',
'direct inserts should be blocked'
);
-- check two invitations to the same email/account are not allowed
-- direct inserts are blocked even for duplicates
select throws_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite1@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'member', gen_random_uuid()) $$,
'duplicate key value violates unique constraint "invitations_email_account_id_key"'
'new row violates row-level security policy for table "invitations"',
'direct inserts should be blocked'
);
select makerkit.authenticate_as('member');
-- check a member cannot invite members with higher roles
-- direct inserts are blocked regardless of role
select throws_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite2@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'owner', gen_random_uuid()) $$,
'new row violates row-level security policy for table "invitations"'
);
-- check a member can invite members with the same or lower roles
select lives_ok(
-- direct inserts are blocked regardless of role
select throws_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite2@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'member', gen_random_uuid()) $$,
'member should be able to create invitations for members or lower roles'
'new row violates row-level security policy for table "invitations"',
'direct inserts should be blocked'
);
-- test invite exists
select isnt_empty(
-- direct inserts should not create invitations
select is_empty(
$$ select * from public.invitations where account_id = makerkit.get_account_id_by_slug('makerkit') $$,
'invitations should be listed'
'invitations should not be listed when inserts are blocked'
);
select makerkit.authenticate_as('owner');
-- check the owner can invite members with lower roles
select lives_ok(
-- direct inserts are blocked regardless of role
select throws_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite3@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'member', gen_random_uuid()) $$,
'owner should be able to create invitations'
'new row violates row-level security policy for table "invitations"',
'direct inserts should be blocked'
);
-- authenticate_as the custom role
select makerkit.authenticate_as('custom');
-- it will fail because the custom role does not have the invites.manage permission
-- direct inserts are blocked regardless of role
select throws_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite3@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'custom-role', gen_random_uuid()) $$,
'new row violates row-level security policy for table "invitations"'
@@ -62,26 +66,28 @@ select throws_ok(
set local role postgres;
-- add permissions to invite members to the custom role
-- adding permissions should not bypass direct insert restrictions
insert into public.role_permissions (role, permission) values ('custom-role', 'invites.manage');
-- authenticate_as the custom role
select makerkit.authenticate_as('custom');
select lives_ok(
select throws_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite4@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'custom-role', gen_random_uuid()) $$,
'custom role should be able to create invitations'
);
select lives_ok(
$$ SELECT public.add_invitations_to_account('makerkit', ARRAY[ROW('example@makerkit.dev', 'custom-role')::public.invitation]); $$,
'custom role should be able to create invitations using the function public.add_invitations_to_account'
'new row violates row-level security policy for table "invitations"',
'direct inserts should be blocked'
);
select throws_ok(
$$ SELECT public.add_invitations_to_account('makerkit', ARRAY[ROW('example2@makerkit.dev', 'owner')::public.invitation]); $$,
'new row violates row-level security policy for table "invitations"',
'cannot invite members with higher roles'
$$ SELECT public.add_invitations_to_account('makerkit', ARRAY[ROW('example@makerkit.dev', 'custom-role')::public.invitation], auth.uid()); $$,
'permission denied for function add_invitations_to_account',
'authenticated users cannot call add_invitations_to_account'
);
select throws_ok(
$$ SELECT public.add_invitations_to_account('makerkit', ARRAY[ROW('example2@makerkit.dev', 'owner')::public.invitation], auth.uid()); $$,
'permission denied for function add_invitations_to_account',
'authenticated users cannot call add_invitations_to_account'
);
-- Foreigners should not be able to create invitations
@@ -90,15 +96,15 @@ select tests.create_supabase_user('user');
select makerkit.authenticate_as('user');
-- it will fail because the user is not a member of the account
-- direct inserts are blocked regardless of membership
select throws_ok(
$$ insert into public.invitations (email, invited_by, account_id, role, invite_token) values ('invite4@makerkit.dev', auth.uid(), makerkit.get_account_id_by_slug('makerkit'), 'member', gen_random_uuid()) $$,
'new row violates row-level security policy for table "invitations"'
);
select throws_ok(
$$ SELECT public.add_invitations_to_account('makerkit', ARRAY[ROW('example@example.com', 'member')::public.invitation]); $$,
'new row violates row-level security policy for table "invitations"'
$$ SELECT public.add_invitations_to_account('makerkit', ARRAY[ROW('example@example.com', 'member')::public.invitation], auth.uid()); $$,
'permission denied for function add_invitations_to_account'
);
select is_empty($$

View File

@@ -42,6 +42,9 @@ SELECT ok(
INSERT INTO public.accounts (name, is_personal_account)
VALUES ('Invitation Test Team', false);
-- Switch to service_role to insert invitations (INSERT policy removed, handled by server action)
set role service_role;
-- Test invitation insert
INSERT INTO public.invitations (email, account_id, invited_by, role, invite_token, expires_at)
VALUES (
@@ -53,6 +56,9 @@ VALUES (
now() + interval '7 days'
);
-- Switch back to authenticated user for assertion
select makerkit.authenticate_as('trigger_test_user1');
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'

View File

@@ -3,25 +3,42 @@ create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();
select makerkit.set_identifier('primary_owner', 'test@makerkit.dev');
select makerkit.set_identifier('owner', 'owner@makerkit.dev');
select makerkit.set_identifier('member', 'member@makerkit.dev');
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
-- Create fresh test users
select tests.create_supabase_user('update_test_owner', 'update-owner@test.com');
select tests.create_supabase_user('update_test_member', 'update-member@test.com');
-- another user not in the team
select tests.create_supabase_user('test', 'test@supabase.com');
-- Authenticate as owner to create team account
select makerkit.authenticate_as('update_test_owner');
select makerkit.authenticate_as('member');
-- Create a team account (owner is added automatically via trigger)
insert into public.accounts (name, is_personal_account)
values ('Update Test Team', false);
-- run an update query
update public.accounts_memberships set account_role = 'owner' where user_id = auth.uid() and account_id = makerkit.get_account_id_by_slug('makerkit');
-- Add member to the team with 'member' role using service_role
set role service_role;
insert into public.accounts_memberships (account_id, user_id, account_role)
values (
(select id from public.accounts where name = 'Update Test Team'),
tests.get_supabase_uid('update_test_member'),
'member'
);
-- Authenticate as member
select makerkit.authenticate_as('update_test_member');
-- Member tries to update their own role to 'owner' - should fail silently
update public.accounts_memberships
set account_role = 'owner'
where user_id = auth.uid()
and account_id = (select id from public.accounts where name = 'Update Test Team');
select row_eq(
$$ select account_role from public.accounts_memberships where user_id = auth.uid() and account_id = makerkit.get_account_id_by_slug('makerkit'); $$,
$$ select account_role from public.accounts_memberships where user_id = auth.uid() and account_id = (select id from public.accounts where name = 'Update Test Team'); $$,
row('member'::varchar),
'Updates fail silently to any field of the accounts_membership table'
);
select * from finish();
rollback;
rollback;