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
@@ -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:",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-supabase-saas-kit-turbo",
|
||||
"version": "2.22.0",
|
||||
"version": "2.23.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"engines": {
|
||||
@@ -48,7 +48,7 @@
|
||||
"@turbo/gen": "^2.7.0",
|
||||
"cross-env": "^10.0.0",
|
||||
"prettier": "^3.7.4",
|
||||
"turbo": "2.7.1",
|
||||
"turbo": "2.7.3",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@supabase/supabase-js": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"lucide-react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-hook-form": "catalog:",
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
"./components": "./src/components/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stripe/react-stripe-js": "^5.4.1",
|
||||
"@stripe/stripe-js": "^8.6.0",
|
||||
"stripe": "^20.1.0"
|
||||
"@stripe/react-stripe-js": "catalog:",
|
||||
"@stripe/stripe-js": "catalog:",
|
||||
"stripe": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/billing": "workspace:*",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "1.0.2"
|
||||
"@react-email/components": "1.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"lucide-react": "^0.562.0",
|
||||
"lucide-react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"next-themes": "0.4.6",
|
||||
"react": "catalog:",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@types/react": "catalog:",
|
||||
"lucide-react": "^0.562.0",
|
||||
"lucide-react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
"@kit/supabase": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"@marsidev/react-turnstile": "^1.4.0",
|
||||
"@marsidev/react-turnstile": "catalog:",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@supabase/supabase-js": "catalog:",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"lucide-react": "^0.562.0",
|
||||
"lucide-react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"react-hook-form": "catalog:",
|
||||
"react-i18next": "catalog:",
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
|
||||
import {
|
||||
WeakPasswordError,
|
||||
WeakPasswordReason,
|
||||
} from '@kit/supabase/hooks/use-sign-up-with-email-password';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
function isWeakPasswordError(error: unknown): error is WeakPasswordError {
|
||||
return error instanceof Error && error.name === 'WeakPasswordError';
|
||||
}
|
||||
|
||||
/**
|
||||
* @name AuthErrorAlert
|
||||
* @param error This error comes from Supabase as the code returned on errors
|
||||
@@ -20,6 +28,11 @@ export function AuthErrorAlert({
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle weak password errors specially
|
||||
if (isWeakPasswordError(error)) {
|
||||
return <WeakPasswordErrorAlert reasons={error.reasons} />;
|
||||
}
|
||||
|
||||
const DefaultError = <Trans i18nKey="auth:errors.default" />;
|
||||
const errorCode = error instanceof Error ? error.message : error;
|
||||
|
||||
@@ -41,3 +54,36 @@ export function AuthErrorAlert({
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
function WeakPasswordErrorAlert({
|
||||
reasons,
|
||||
}: {
|
||||
reasons: WeakPasswordReason[];
|
||||
}) {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<ExclamationTriangleIcon className={'w-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'auth:errors.weakPassword.title'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription data-test={'auth-error-message'}>
|
||||
<Trans i18nKey={'auth:errors.weakPassword.description'} />
|
||||
|
||||
{reasons.length > 0 && (
|
||||
<ul className="mt-2 list-inside list-disc space-y-1 text-xs">
|
||||
{reasons.map((reason) => (
|
||||
<li key={reason}>
|
||||
<Trans
|
||||
i18nKey={`auth:errors.weakPassword.reasons.${reason}`}
|
||||
defaults={reason}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@supabase/supabase-js": "catalog:",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"lucide-react": "^0.562.0",
|
||||
"lucide-react": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-i18next": "catalog:"
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"@types/react-dom": "catalog:",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"lucide-react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
|
||||
@@ -37,8 +37,10 @@ export function TransferOwnershipDialog({
|
||||
userId: string;
|
||||
targetDisplayName: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
@@ -56,6 +58,7 @@ export function TransferOwnershipDialog({
|
||||
accountId={accountId}
|
||||
userId={userId}
|
||||
targetDisplayName={targetDisplayName}
|
||||
onSuccess={() => setOpen(false)}
|
||||
/>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
@@ -66,10 +69,12 @@ function TransferOrganizationOwnershipForm({
|
||||
accountId,
|
||||
userId,
|
||||
targetDisplayName,
|
||||
onSuccess,
|
||||
}: {
|
||||
userId: string;
|
||||
accountId: string;
|
||||
targetDisplayName: string;
|
||||
onSuccess: () => unknown;
|
||||
}) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>();
|
||||
@@ -115,6 +120,8 @@ function TransferOrganizationOwnershipForm({
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await transferOwnershipAction(data);
|
||||
|
||||
onSuccess();
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
|
||||
@@ -45,9 +45,12 @@ export function UpdateMemberRoleDialog({
|
||||
userRole: Role;
|
||||
userRoleHierarchy: number;
|
||||
}>) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
@@ -66,6 +69,7 @@ export function UpdateMemberRoleDialog({
|
||||
teamAccountId={teamAccountId}
|
||||
userRole={userRole}
|
||||
roles={data}
|
||||
onSuccess={() => setOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</RolesDataProvider>
|
||||
@@ -79,11 +83,13 @@ function UpdateMemberForm({
|
||||
userRole,
|
||||
teamAccountId,
|
||||
roles,
|
||||
onSuccess,
|
||||
}: React.PropsWithChildren<{
|
||||
userId: string;
|
||||
userRole: Role;
|
||||
teamAccountId: string;
|
||||
roles: Role[];
|
||||
onSuccess: () => unknown;
|
||||
}>) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>();
|
||||
@@ -97,6 +103,8 @@ function UpdateMemberForm({
|
||||
userId,
|
||||
role,
|
||||
});
|
||||
|
||||
onSuccess();
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from 'zod';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { JWTUserData } from '@kit/supabase/types';
|
||||
@@ -34,8 +35,52 @@ export const createInvitationsAction = enhanceAction(
|
||||
'User requested to send invitations',
|
||||
);
|
||||
|
||||
// Evaluate invitation policies
|
||||
const policiesResult = await evaluateInvitationsPolicies(params, user);
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
// Get account ID from slug (needed for permission checks and policies)
|
||||
const { data: account, error: accountError } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
.eq('slug', params.accountSlug)
|
||||
.single();
|
||||
|
||||
if (accountError || !account) {
|
||||
logger.error(
|
||||
{ accountSlug: params.accountSlug, error: accountError },
|
||||
'Account not found',
|
||||
);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
reasons: ['Account not found'],
|
||||
};
|
||||
}
|
||||
|
||||
// Check invitation permissions (replaces RLS policy checks)
|
||||
const permissionsResult = await checkInvitationPermissions(
|
||||
account.id,
|
||||
user.id,
|
||||
params.invitations,
|
||||
);
|
||||
|
||||
if (!permissionsResult.allowed) {
|
||||
logger.info(
|
||||
{ reason: permissionsResult.reason, userId: user.id },
|
||||
'Invitations blocked by permission check',
|
||||
);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
reasons: permissionsResult.reason ? [permissionsResult.reason] : [],
|
||||
};
|
||||
}
|
||||
|
||||
// Evaluate custom invitation policies
|
||||
const policiesResult = await evaluateInvitationsPolicies(
|
||||
params,
|
||||
user,
|
||||
account.id,
|
||||
);
|
||||
|
||||
// If the invitations are not allowed, throw an error
|
||||
if (!policiesResult.allowed) {
|
||||
@@ -51,11 +96,15 @@ export const createInvitationsAction = enhanceAction(
|
||||
}
|
||||
|
||||
// invitations are allowed, so continue with the action
|
||||
const client = getSupabaseServerClient();
|
||||
const service = createAccountInvitationsService(client);
|
||||
// Use admin client since we've already validated permissions
|
||||
const adminClient = getSupabaseServerAdminClient();
|
||||
const service = createAccountInvitationsService(adminClient);
|
||||
|
||||
try {
|
||||
await service.sendInvitations(params);
|
||||
await service.sendInvitations({
|
||||
...params,
|
||||
invitedBy: user.id,
|
||||
});
|
||||
|
||||
revalidateMemberPage();
|
||||
|
||||
@@ -194,10 +243,13 @@ function revalidateMemberPage() {
|
||||
* @name evaluateInvitationsPolicies
|
||||
* @description Evaluates invitation policies with performance optimization.
|
||||
* @param params - The invitations to evaluate (emails and roles).
|
||||
* @param user - The user performing the invitation.
|
||||
* @param accountId - The account ID (already fetched to avoid duplicate queries).
|
||||
*/
|
||||
async function evaluateInvitationsPolicies(
|
||||
params: z.infer<typeof InviteMembersSchema> & { accountSlug: string },
|
||||
user: JWTUserData,
|
||||
accountId: string,
|
||||
) {
|
||||
const evaluator = createInvitationsPolicyEvaluator();
|
||||
const hasPolicies = await evaluator.hasPoliciesForStage('submission');
|
||||
@@ -212,7 +264,92 @@ async function evaluateInvitationsPolicies(
|
||||
|
||||
const client = getSupabaseServerClient();
|
||||
const builder = createInvitationContextBuilder(client);
|
||||
const context = await builder.buildContext(params, user);
|
||||
const context = await builder.buildContextWithAccountId(
|
||||
params,
|
||||
user,
|
||||
accountId,
|
||||
);
|
||||
|
||||
return evaluator.canInvite(context, 'submission');
|
||||
}
|
||||
|
||||
/**
|
||||
* @name checkInvitationPermissions
|
||||
* @description Checks if the user has permission to invite members and
|
||||
* validates role hierarchy for each invitation.
|
||||
* Optimized to batch all checks in parallel.
|
||||
*/
|
||||
async function checkInvitationPermissions(
|
||||
accountId: string,
|
||||
userId: string,
|
||||
invitations: z.infer<typeof InviteMembersSchema>['invitations'],
|
||||
): Promise<{
|
||||
allowed: boolean;
|
||||
reason?: string;
|
||||
}> {
|
||||
const client = getSupabaseServerClient();
|
||||
const logger = await getLogger();
|
||||
|
||||
const ctx = {
|
||||
name: 'checkInvitationPermissions',
|
||||
userId,
|
||||
accountId,
|
||||
};
|
||||
|
||||
// Get unique roles from invitations to minimize RPC calls
|
||||
const uniqueRoles = [...new Set(invitations.map((inv) => inv.role))];
|
||||
|
||||
// Run all checks in parallel: permission check + role hierarchy checks for each unique role
|
||||
const [permissionResult, ...roleResults] = await Promise.all([
|
||||
client.rpc('has_permission', {
|
||||
user_id: userId,
|
||||
account_id: accountId,
|
||||
permission_name:
|
||||
'invites.manage' as Database['public']['Enums']['app_permissions'],
|
||||
}),
|
||||
...uniqueRoles.map((role) =>
|
||||
Promise.all([
|
||||
client.rpc('has_more_elevated_role', {
|
||||
target_user_id: userId,
|
||||
target_account_id: accountId,
|
||||
role_name: role,
|
||||
}),
|
||||
client.rpc('has_same_role_hierarchy_level', {
|
||||
target_user_id: userId,
|
||||
target_account_id: accountId,
|
||||
role_name: role,
|
||||
}),
|
||||
]).then(([elevated, sameLevel]) => ({
|
||||
role,
|
||||
allowed: elevated.data || sameLevel.data,
|
||||
})),
|
||||
),
|
||||
]);
|
||||
|
||||
// Check permission first
|
||||
if (!permissionResult.data) {
|
||||
logger.info(ctx, 'User does not have invites.manage permission');
|
||||
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'You do not have permission to invite members',
|
||||
};
|
||||
}
|
||||
|
||||
// Check role hierarchy results
|
||||
const failedRole = roleResults.find((result) => !result.allowed);
|
||||
|
||||
if (failedRole) {
|
||||
logger.info(
|
||||
{ ...ctx, role: failedRole.role },
|
||||
'User cannot invite to a role higher than their own',
|
||||
);
|
||||
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `You cannot invite members with the "${failedRole.role}" role`,
|
||||
};
|
||||
}
|
||||
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
@@ -35,10 +35,22 @@ class InvitationContextBuilder {
|
||||
// Fetch all data in parallel for optimal performance
|
||||
const account = await this.getAccount(params.accountSlug);
|
||||
|
||||
// Fetch subscription and member count in parallel using account ID
|
||||
return this.buildContextWithAccountId(params, user, account.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build policy context when account ID is already known
|
||||
* (avoids duplicate account lookup)
|
||||
*/
|
||||
async buildContextWithAccountId(
|
||||
params: z.infer<typeof InviteMembersSchema> & { accountSlug: string },
|
||||
user: JWTUserData,
|
||||
accountId: string,
|
||||
): Promise<FeaturePolicyInvitationContext> {
|
||||
// Fetch subscription and member count in parallel
|
||||
const [subscription, memberCount] = await Promise.all([
|
||||
this.getSubscription(account.id),
|
||||
this.getMemberCount(account.id),
|
||||
this.getSubscription(accountId),
|
||||
this.getMemberCount(accountId),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -52,7 +64,7 @@ class InvitationContextBuilder {
|
||||
|
||||
// Invitation-specific fields
|
||||
accountSlug: params.accountSlug,
|
||||
accountId: account.id,
|
||||
accountId,
|
||||
subscription,
|
||||
currentMemberCount: memberCount,
|
||||
invitations: params.invitations,
|
||||
|
||||
@@ -139,9 +139,11 @@ class AccountInvitationsService {
|
||||
async sendInvitations({
|
||||
accountSlug,
|
||||
invitations,
|
||||
invitedBy,
|
||||
}: {
|
||||
invitations: z.infer<typeof InviteMembersSchema>['invitations'];
|
||||
accountSlug: string;
|
||||
invitedBy: string;
|
||||
}) {
|
||||
const logger = await getLogger();
|
||||
|
||||
@@ -188,6 +190,7 @@ class AccountInvitationsService {
|
||||
const response = await this.client.rpc('add_invitations_to_account', {
|
||||
invitations,
|
||||
account_slug: accountSlug,
|
||||
invited_by: invitedBy,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,21 @@ interface Credentials {
|
||||
captchaToken?: string;
|
||||
}
|
||||
|
||||
const _WeakPasswordReasons = ['length', 'characters', 'pwned'] as const;
|
||||
|
||||
export type WeakPasswordReason = (typeof _WeakPasswordReasons)[number];
|
||||
|
||||
export class WeakPasswordError extends Error {
|
||||
readonly code = 'weak_password';
|
||||
readonly reasons: WeakPasswordReason[];
|
||||
|
||||
constructor(reasons: WeakPasswordReason[]) {
|
||||
super('weak_password');
|
||||
this.name = 'WeakPasswordError';
|
||||
this.reasons = reasons;
|
||||
}
|
||||
}
|
||||
|
||||
export function useSignUpWithEmailAndPassword() {
|
||||
const client = useSupabase();
|
||||
const mutationKey = ['auth', 'sign-up-with-email-password'];
|
||||
@@ -25,6 +40,15 @@ export function useSignUpWithEmailAndPassword() {
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
// Handle weak password errors specially (AuthWeakPasswordError from Supabase)
|
||||
if (response.error.code === 'weak_password') {
|
||||
const errorObj = response.error as unknown as {
|
||||
reasons?: WeakPasswordReason[];
|
||||
};
|
||||
|
||||
throw new WeakPasswordError(errorObj.reasons ?? []);
|
||||
}
|
||||
|
||||
throw response.error.message;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"input-otp": "1.4.2",
|
||||
"lucide-react": "^0.562.0",
|
||||
"lucide-react": "catalog:",
|
||||
"radix-ui": "1.4.3",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-top-loading-bar": "3.0.2",
|
||||
|
||||
@@ -23,9 +23,9 @@ let version: string | null = null;
|
||||
|
||||
/**
|
||||
* Default interval time in seconds to check for new version
|
||||
* By default, it is set to 120 seconds
|
||||
* By default, it is set to 60 seconds
|
||||
*/
|
||||
const DEFAULT_REFETCH_INTERVAL = 120;
|
||||
const DEFAULT_REFETCH_INTERVAL = 60;
|
||||
|
||||
/**
|
||||
* Default interval time in seconds to check for new version
|
||||
@@ -99,7 +99,9 @@ function useVersionUpdater(props: { intervalTimeInSecond?: number } = {}) {
|
||||
refetchInterval,
|
||||
initialData: null,
|
||||
queryFn: async () => {
|
||||
const response = await fetch('/version');
|
||||
const url = new URL('/api/version', process.env.NEXT_PUBLIC_SITE_URL);
|
||||
const response = await fetch(url.toString());
|
||||
|
||||
const currentVersion = await response.text();
|
||||
const oldVersion = version;
|
||||
|
||||
|
||||
661
pnpm-lock.yaml
generated
661
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -4,21 +4,26 @@ packages:
|
||||
- tooling/*
|
||||
|
||||
catalog:
|
||||
'@marsidev/react-turnstile': 1.4.1
|
||||
'@next/bundle-analyzer': 16.1.1
|
||||
'@next/eslint-plugin-next': 16.1.1
|
||||
'@stripe/react-stripe-js': 5.4.1
|
||||
'@stripe/stripe-js': 8.6.1
|
||||
'@supabase/supabase-js': 2.89.0
|
||||
'@tailwindcss/postcss': 4.1.18
|
||||
'@tanstack/react-query': 5.90.12
|
||||
'@tanstack/react-query': 5.90.16
|
||||
'@types/node': 25.0.3
|
||||
'@types/react': 19.2.7
|
||||
'@types/react-dom': 19.2.3
|
||||
eslint-config-next: 16.1.1
|
||||
lucide-react: 0.562.0
|
||||
next: 16.1.1
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3
|
||||
react-hook-form: 7.69.0
|
||||
react-i18next: 16.5.0
|
||||
supabase: 2.70.3
|
||||
react-hook-form: 7.70.0
|
||||
react-i18next: 16.5.1
|
||||
stripe: 20.1.1
|
||||
supabase: 2.71.1
|
||||
tailwindcss: 4.1.18
|
||||
tw-animate-css: 1.4.0
|
||||
zod: 3.25.76
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"@next/eslint-plugin-next": "catalog:",
|
||||
"@types/eslint": "9.6.1",
|
||||
"eslint-config-next": "catalog:",
|
||||
"eslint-config-turbo": "^2.7.1"
|
||||
"eslint-config-turbo": "^2.7.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "6.0.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "6.0.2",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user