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:
committed by
GitHub
parent
5237d34e6f
commit
d5dc6f2528
@@ -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
|
||||
10
apps/web/app/api/version/route.ts
Normal file
10
apps/web/app/api/version/route.ts
Normal 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' },
|
||||
});
|
||||
};
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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($$
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user