Enforce RLS when user opted in to MFA. (#188)
* Allow Super Admin to view tables using RLS * Replace previous usages of the Admin client using the authed client using the new RLS * Enforce MFA for Super Admin users * Enforce RLS when user opted in to MFA. * Add Super Admin Access Policies and Update Database Types * Consolidate super admin logic into a single function that uses the RPC is_super_admin * Added Super Admin E2E tests * Fixes and improvements * Bump version to 2.5.0
This commit is contained in:
committed by
GitHub
parent
9cf7bf0aac
commit
131b1061e6
2
.github/workflows/workflow.yml
vendored
2
.github/workflows/workflow.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
name: ⚫️ Test
|
name: ⚫️ Test
|
||||||
timeout-minutes: 12
|
timeout-minutes: 15
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ vars.ENABLE_E2E_JOB == 'true' }}
|
if: ${{ vars.ENABLE_E2E_JOB == 'true' }}
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import Link from 'next/link';
|
|||||||
|
|
||||||
import { EmailTesterFormSchema } from '@/app/emails/lib/email-tester-form-schema';
|
import { EmailTesterFormSchema } from '@/app/emails/lib/email-tester-form-schema';
|
||||||
import { sendEmailAction } from '@/app/emails/lib/server-actions';
|
import { sendEmailAction } from '@/app/emails/lib/server-actions';
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class ConnectivityService {
|
|||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return {
|
return {
|
||||||
status: 'error' as const,
|
status: 'error' as const,
|
||||||
message: 'No accounts found in Supabase Admin',
|
message: 'No accounts found in Supabase Admin. The data may not be seeded. Please run `pnpm run supabase:web:reset` to reset the database.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import { EnvMode } from '@/app/variables/lib/types';
|
||||||
|
import { EnvModeSelector } from '@/components/env-mode-selector';
|
||||||
import { ServiceCard } from '@/components/status-tile';
|
import { ServiceCard } from '@/components/status-tile';
|
||||||
|
|
||||||
import { Page, PageBody, PageHeader } from '@kit/ui/page';
|
import { Page, PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
import { createConnectivityService } from './lib/connectivity-service';
|
import { createConnectivityService } from './lib/connectivity-service';
|
||||||
import {EnvMode} from "@/app/variables/lib/types";
|
|
||||||
import {EnvModeSelector} from "@/components/env-mode-selector";
|
|
||||||
|
|
||||||
type DashboardPageProps = React.PropsWithChildren<{
|
type DashboardPageProps = React.PropsWithChildren<{
|
||||||
searchParams: Promise<{ mode?: EnvMode }>;
|
searchParams: Promise<{ mode?: EnvMode }>;
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ type ValidationResult = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type VariableRecord = Record<string, string>;
|
|
||||||
|
|
||||||
export function AppEnvironmentVariablesManager({
|
export function AppEnvironmentVariablesManager({
|
||||||
state,
|
state,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
@@ -71,11 +69,11 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const secretVars = searchParams.get('secret') === 'true';
|
const showSecretVars = searchParams.get('secret') === 'true';
|
||||||
const publicVars = searchParams.get('public') === 'true';
|
const showPublicVars = searchParams.get('public') === 'true';
|
||||||
const privateVars = searchParams.get('private') === 'true';
|
const showPrivateVars = searchParams.get('private') === 'true';
|
||||||
const overriddenVars = searchParams.get('overridden') === 'true';
|
const showOverriddenVars = searchParams.get('overridden') === 'true';
|
||||||
const invalidVars = searchParams.get('invalid') === 'true';
|
const showInvalidVars = searchParams.get('invalid') === 'true';
|
||||||
|
|
||||||
const toggleExpanded = (key: string) => {
|
const toggleExpanded = (key: string) => {
|
||||||
setExpandedVars((prev) => ({
|
setExpandedVars((prev) => ({
|
||||||
@@ -558,16 +556,16 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!search &&
|
!search &&
|
||||||
!secretVars &&
|
!showSecretVars &&
|
||||||
!publicVars &&
|
!showPublicVars &&
|
||||||
!privateVars &&
|
!showPrivateVars &&
|
||||||
!invalidVars &&
|
!showInvalidVars &&
|
||||||
!overriddenVars
|
!showOverriddenVars
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSecret = model?.secret;
|
const isSecret = model?.secret ?? false;
|
||||||
const isPublic = varState.key.startsWith('NEXT_PUBLIC_');
|
const isPublic = varState.key.startsWith('NEXT_PUBLIC_');
|
||||||
const isPrivate = !isPublic;
|
const isPrivate = !isPublic;
|
||||||
|
|
||||||
@@ -575,23 +573,23 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
? varState.key.toLowerCase().includes(search.toLowerCase())
|
? varState.key.toLowerCase().includes(search.toLowerCase())
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
if (isPublic && publicVars && isInSearch) {
|
if (showPublicVars && isInSearch) {
|
||||||
return true;
|
return isPublic;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSecret && secretVars && isInSearch) {
|
if (showSecretVars && isInSearch) {
|
||||||
return true;
|
return isSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPrivate && privateVars && isInSearch) {
|
if (showPrivateVars && isInSearch) {
|
||||||
return true;
|
return isPrivate;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overriddenVars && varState.isOverridden && isInSearch) {
|
if (showOverriddenVars && isInSearch) {
|
||||||
return true;
|
return varState.isOverridden;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (invalidVars) {
|
if (showInvalidVars) {
|
||||||
const allVariables = getEffectiveVariablesValue(appState);
|
const allVariables = getEffectiveVariablesValue(appState);
|
||||||
|
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
@@ -637,14 +635,10 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasError && isInSearch) return true;
|
return hasError && isInSearch;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInSearch) {
|
return isInSearch;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update groups to use allVarsWithValidation instead of appState.variables
|
// Update groups to use allVarsWithValidation instead of appState.variables
|
||||||
@@ -679,11 +673,11 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
|||||||
<div>
|
<div>
|
||||||
<FilterSwitcher
|
<FilterSwitcher
|
||||||
filters={{
|
filters={{
|
||||||
secret: secretVars,
|
secret: showSecretVars,
|
||||||
public: publicVars,
|
public: showPublicVars,
|
||||||
overridden: overriddenVars,
|
overridden: showOverriddenVars,
|
||||||
private: privateVars,
|
private: showPrivateVars,
|
||||||
invalid: invalidVars,
|
invalid: showInvalidVars,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -925,9 +925,7 @@ export const envVariables: EnvVariableModel[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
validate: ({ value }) => {
|
validate: ({ value }) => {
|
||||||
return z
|
return z.string().safeParse(value);
|
||||||
.string()
|
|
||||||
.safeParse(value);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ export function EnvModeSelector({ mode }: { mode: EnvMode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Select name={'mode'} defaultValue={mode} onValueChange={handleModeChange}>
|
<Select
|
||||||
|
name={'mode'}
|
||||||
|
defaultValue={mode}
|
||||||
|
onValueChange={handleModeChange}
|
||||||
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select Mode" />
|
<SelectValue placeholder="Select Mode" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@types/node": "^22.13.4",
|
"@types/node": "^22.13.4",
|
||||||
"node-html-parser": "^7.0.1"
|
"node-html-parser": "^7.0.1",
|
||||||
|
"totp-generator": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,10 +54,12 @@ export class AccountPageObject {
|
|||||||
'[data-test="account-password-form-password-input"]',
|
'[data-test="account-password-form-password-input"]',
|
||||||
password,
|
password,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.page.fill(
|
await this.page.fill(
|
||||||
'[data-test="account-password-form-repeat-password-input"]',
|
'[data-test="account-password-form-repeat-password-input"]',
|
||||||
password,
|
password,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.page.click('[data-test="account-password-form"] button');
|
await this.page.click('[data-test="account-password-form"] button');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ test.describe('Account Settings', () => {
|
|||||||
|
|
||||||
await Promise.all([request, response]);
|
await Promise.all([request, response]);
|
||||||
|
|
||||||
await account.auth.signOut();
|
await page.context().clearCookies();
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
362
apps/e2e/tests/admin/admin.spec.ts
Normal file
362
apps/e2e/tests/admin/admin.spec.ts
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
import { Page, expect, selectors, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { AuthPageObject } from '../authentication/auth.po';
|
||||||
|
import { TeamAccountsPageObject } from '../team-accounts/team-accounts.po';
|
||||||
|
|
||||||
|
const MFA_KEY = 'NHOHJVGPO3R3LKVPRMNIYLCDMBHUM2SE';
|
||||||
|
|
||||||
|
test.describe('Admin Auth flow without MFA', () => {
|
||||||
|
test('will return a 404 for non-admin users', async ({ page }) => {
|
||||||
|
const auth = new AuthPageObject(page);
|
||||||
|
|
||||||
|
await page.goto('/auth/sign-in');
|
||||||
|
|
||||||
|
await auth.signIn({
|
||||||
|
email: 'owner@makerkit.dev',
|
||||||
|
password: 'testingpassword',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForURL('/home');
|
||||||
|
|
||||||
|
await page.goto('/admin');
|
||||||
|
|
||||||
|
expect(page.url()).toContain('/404');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('will redirect to 404 for admin users without MFA', async ({ page }) => {
|
||||||
|
const auth = new AuthPageObject(page);
|
||||||
|
|
||||||
|
await page.goto('/auth/sign-in');
|
||||||
|
|
||||||
|
await auth.signIn({
|
||||||
|
email: 'test@makerkit.dev',
|
||||||
|
password: 'testingpassword',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForURL('/home');
|
||||||
|
|
||||||
|
await page.goto('/admin');
|
||||||
|
|
||||||
|
expect(page.url()).toContain('/404');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Admin', () => {
|
||||||
|
// must be serial because OTP verification is not working in parallel
|
||||||
|
test.describe.configure({ mode: 'serial' });
|
||||||
|
|
||||||
|
test.describe('Admin Dashboard', () => {
|
||||||
|
test('displays all stat cards', async ({ page }) => {
|
||||||
|
await goToAdmin(page);
|
||||||
|
|
||||||
|
// Check all stat cards are present
|
||||||
|
await expect(page.getByRole('heading', { name: 'Users' })).toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Team Accounts' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Paying Customers' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Trials' })).toBeVisible();
|
||||||
|
|
||||||
|
// Verify stat values are numbers
|
||||||
|
const stats = await page.$$('.text-3xl.font-bold');
|
||||||
|
|
||||||
|
for (const stat of stats) {
|
||||||
|
const value = await stat.textContent();
|
||||||
|
expect(Number.isInteger(Number(value))).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Personal Account Management', () => {
|
||||||
|
let testUserEmail: string;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
selectors.setTestIdAttribute('data-test');
|
||||||
|
|
||||||
|
// Create a new test user before each test
|
||||||
|
testUserEmail = await createUser(page);
|
||||||
|
|
||||||
|
await goToAdmin(page);
|
||||||
|
|
||||||
|
// Navigate to the newly created user's account page
|
||||||
|
// Note: We need to get the user's ID from the email - this might need adjustment
|
||||||
|
// based on your URL structure
|
||||||
|
await page.goto(`/admin/accounts`);
|
||||||
|
|
||||||
|
const filterText = testUserEmail.split('@')[0]!;
|
||||||
|
|
||||||
|
await filterAccounts(page, filterText);
|
||||||
|
await selectAccount(page, filterText);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays personal account details', async ({ page }) => {
|
||||||
|
await expect(page.getByText('Personal Account')).toBeVisible();
|
||||||
|
await expect(page.getByTestId('admin-ban-account-button')).toBeVisible();
|
||||||
|
await expect(page.getByTestId('admin-impersonate-button')).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('admin-delete-account-button'),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ban user flow', async ({ page }) => {
|
||||||
|
await page.getByTestId('admin-ban-account-button').click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Ban User' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Try with invalid confirmation
|
||||||
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'WRONG');
|
||||||
|
await page.getByRole('button', { name: 'Ban User' }).click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Ban User' }),
|
||||||
|
).toBeVisible(); // Dialog should still be open
|
||||||
|
|
||||||
|
// Confirm with correct text
|
||||||
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM');
|
||||||
|
await page.getByRole('button', { name: 'Ban User' }).click();
|
||||||
|
await expect(page.getByText('Banned')).toBeVisible();
|
||||||
|
|
||||||
|
await page.context().clearCookies();
|
||||||
|
|
||||||
|
// Verify user can't log in
|
||||||
|
await page.goto('/auth/sign-in');
|
||||||
|
|
||||||
|
const auth = new AuthPageObject(page);
|
||||||
|
|
||||||
|
await auth.signIn({
|
||||||
|
email: testUserEmail,
|
||||||
|
password: 'testingpassword',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should show an error message
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-test="auth-error-message"]'),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reactivate user flow', async ({ page }) => {
|
||||||
|
// First ban the user
|
||||||
|
await page.getByTestId('admin-ban-account-button').click();
|
||||||
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM');
|
||||||
|
await page.getByRole('button', { name: 'Ban User' }).click();
|
||||||
|
await expect(page.getByText('Banned')).toBeVisible();
|
||||||
|
|
||||||
|
// Now reactivate
|
||||||
|
await page.getByTestId('admin-reactivate-account-button').click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Reactivate User' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM');
|
||||||
|
await page.getByRole('button', { name: 'Reactivate User' }).click();
|
||||||
|
|
||||||
|
// Verify ban badge is removed
|
||||||
|
await expect(page.getByText('Banned')).not.toBeVisible();
|
||||||
|
|
||||||
|
// Log out
|
||||||
|
await page.context().clearCookies();
|
||||||
|
|
||||||
|
// Verify user can log in again
|
||||||
|
await page.goto('/auth/sign-in');
|
||||||
|
|
||||||
|
const auth = new AuthPageObject(page);
|
||||||
|
|
||||||
|
await auth.signIn({
|
||||||
|
email: testUserEmail,
|
||||||
|
password: 'testingpassword',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForURL('/home');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('impersonate user flow', async ({ page }) => {
|
||||||
|
await page.getByTestId('admin-impersonate-button').click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Impersonate User' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM');
|
||||||
|
await page.getByRole('button', { name: 'Impersonate User' }).click();
|
||||||
|
|
||||||
|
// Should redirect to home and be logged in as the user
|
||||||
|
await page.waitForURL('/home');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete user flow', async ({ page }) => {
|
||||||
|
await page.getByTestId('admin-delete-account-button').click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Delete User' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Try with invalid confirmation
|
||||||
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'WRONG');
|
||||||
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Delete User' }),
|
||||||
|
).toBeVisible(); // Dialog should still be open
|
||||||
|
|
||||||
|
// Confirm with correct text
|
||||||
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM');
|
||||||
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
|
||||||
|
// Should redirect to admin dashboard
|
||||||
|
await expect(page).toHaveURL('/admin/accounts');
|
||||||
|
|
||||||
|
// Log out
|
||||||
|
await page.context().clearCookies();
|
||||||
|
|
||||||
|
// Verify user can't log in
|
||||||
|
await page.goto('/auth/sign-in');
|
||||||
|
|
||||||
|
const auth = new AuthPageObject(page);
|
||||||
|
|
||||||
|
await auth.signIn({
|
||||||
|
email: testUserEmail,
|
||||||
|
password: 'testingpassword',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should show an error message
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-test="auth-error-message"]'),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Team Account Management', () => {
|
||||||
|
let testUserEmail: string;
|
||||||
|
let teamName: string;
|
||||||
|
let slug: string;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
selectors.setTestIdAttribute('data-test');
|
||||||
|
|
||||||
|
// Create a new test user and team account
|
||||||
|
testUserEmail = await createUser(page, {
|
||||||
|
afterSignIn: async () => {
|
||||||
|
teamName = `test-${Math.random().toString(36).substring(2, 15)}`;
|
||||||
|
|
||||||
|
const teamAccountPo = new TeamAccountsPageObject(page);
|
||||||
|
const teamSlug = teamName.toLowerCase().replace(/ /g, '-');
|
||||||
|
|
||||||
|
slug = teamSlug;
|
||||||
|
|
||||||
|
await teamAccountPo.createTeam({
|
||||||
|
teamName,
|
||||||
|
slug,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await goToAdmin(page);
|
||||||
|
|
||||||
|
await page.goto(`/admin/accounts`);
|
||||||
|
|
||||||
|
await filterAccounts(page, teamName);
|
||||||
|
await selectAccount(page, teamName);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays team account details', async ({ page }) => {
|
||||||
|
await expect(page.getByText('Team Account')).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('admin-delete-account-button'),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete team account flow', async ({ page }) => {
|
||||||
|
await page.getByTestId('admin-delete-account-button').click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Delete Account' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Try with invalid confirmation
|
||||||
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'WRONG');
|
||||||
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Delete Account' }),
|
||||||
|
).toBeVisible(); // Dialog should still be open
|
||||||
|
|
||||||
|
// Confirm with correct text
|
||||||
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM');
|
||||||
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
|
||||||
|
// Should redirect to admin dashboard after deletion
|
||||||
|
await expect(page).toHaveURL('/admin/accounts');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function goToAdmin(page: Page) {
|
||||||
|
const auth = new AuthPageObject(page);
|
||||||
|
|
||||||
|
await page.goto('/auth/sign-in');
|
||||||
|
|
||||||
|
await auth.signIn({
|
||||||
|
email: 'super-admin@makerkit.dev',
|
||||||
|
password: 'testingpassword',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForURL('/auth/verify');
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
await auth.submitMFAVerification(MFA_KEY);
|
||||||
|
await page.waitForURL('/home');
|
||||||
|
}).toPass({
|
||||||
|
intervals: [
|
||||||
|
500, 2500, 5000, 7500, 10_000, 15_000, 20_000, 25_000, 30_000, 35_000,
|
||||||
|
40_000, 45_000, 50_000,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('/admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createUser(
|
||||||
|
page: Page,
|
||||||
|
params: {
|
||||||
|
afterSignIn?: () => Promise<void>;
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const auth = new AuthPageObject(page);
|
||||||
|
|
||||||
|
await page.goto('/auth/sign-up');
|
||||||
|
|
||||||
|
const email = `${(Math.random() * 1000000).toFixed(0)}@makerkit.dev`;
|
||||||
|
|
||||||
|
await auth.signUp({
|
||||||
|
email,
|
||||||
|
password: 'testingpassword',
|
||||||
|
repeatPassword: 'testingpassword',
|
||||||
|
});
|
||||||
|
|
||||||
|
await auth.visitConfirmEmailLink(email);
|
||||||
|
|
||||||
|
await page.goto('/home');
|
||||||
|
|
||||||
|
if (params.afterSignIn) {
|
||||||
|
await params.afterSignIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
await auth.signOut();
|
||||||
|
await page.waitForURL('/');
|
||||||
|
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function filterAccounts(page: Page, email: string) {
|
||||||
|
await page
|
||||||
|
.locator('[data-test="admin-accounts-table-filter-input"]')
|
||||||
|
.fill(email);
|
||||||
|
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectAccount(page: Page, email: string) {
|
||||||
|
await page.getByRole('link', { name: email.split('@')[0] }).click();
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Page, expect } from '@playwright/test';
|
import { Page, expect } from '@playwright/test';
|
||||||
|
import { TOTP } from 'totp-generator';
|
||||||
|
|
||||||
import { Mailbox } from '../utils/mailbox';
|
import { Mailbox } from '../utils/mailbox';
|
||||||
|
|
||||||
@@ -46,6 +47,21 @@ export class AuthPageObject {
|
|||||||
await this.page.click('button[type="submit"]');
|
await this.page.click('button[type="submit"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async submitMFAVerification(key: string) {
|
||||||
|
const period = 30;
|
||||||
|
|
||||||
|
const { otp } = TOTP.generate(key, {
|
||||||
|
period,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`OTP ${otp} code`, {
|
||||||
|
period,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.page.fill('[data-input-otp]', otp);
|
||||||
|
await this.page.click('[data-test="submit-mfa-button"]');
|
||||||
|
}
|
||||||
|
|
||||||
async visitConfirmEmailLink(
|
async visitConfirmEmailLink(
|
||||||
email: string,
|
email: string,
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@@ -71,6 +71,23 @@ test.describe('Auth flow', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Protected routes', () => {
|
test.describe('Protected routes', () => {
|
||||||
|
test('when logged out, redirects to the correct page after sign in', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const auth = new AuthPageObject(page);
|
||||||
|
|
||||||
|
await page.goto('/home/settings');
|
||||||
|
|
||||||
|
await auth.signIn({
|
||||||
|
email: 'test@makerkit.dev',
|
||||||
|
password: 'testingpassword',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForURL('/home/settings');
|
||||||
|
|
||||||
|
expect(page.url()).toContain('/home/settings');
|
||||||
|
});
|
||||||
|
|
||||||
test('will redirect to the sign-in page if not authenticated', async ({
|
test('will redirect to the sign-in page if not authenticated', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -78,10 +95,4 @@ test.describe('Protected routes', () => {
|
|||||||
|
|
||||||
expect(page.url()).toContain('/auth/sign-in?next=/home/settings');
|
expect(page.url()).toContain('/auth/sign-in?next=/home/settings');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('will return a 404 for the admin page', async ({ page }) => {
|
|
||||||
await page.goto('/admin');
|
|
||||||
|
|
||||||
expect(page.url()).toContain('/auth/sign-in');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,14 +54,10 @@ test.describe('Password Reset Flow', () => {
|
|||||||
await page.waitForURL('/home');
|
await page.waitForURL('/home');
|
||||||
}).toPass();
|
}).toPass();
|
||||||
|
|
||||||
await page.context().clearCookies();
|
await auth.signOut();
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
await page
|
await page.waitForURL('/');
|
||||||
.locator('a', {
|
await page.goto('/auth/sign-in');
|
||||||
hasText: 'Sign in',
|
|
||||||
})
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await auth.signIn({
|
await auth.signIn({
|
||||||
email,
|
email,
|
||||||
|
|||||||
@@ -120,7 +120,8 @@ test.describe('Full Invitation Flow', () => {
|
|||||||
await expect(invitations.getInvitations()).toHaveCount(2);
|
await expect(invitations.getInvitations()).toHaveCount(2);
|
||||||
|
|
||||||
// sign out and sign in with the first email
|
// sign out and sign in with the first email
|
||||||
await invitations.auth.signOut();
|
await page.context().clearCookies();
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
console.log(`Finding email to ${firstEmail} ...`);
|
console.log(`Finding email to ${firstEmail} ...`);
|
||||||
|
|
||||||
|
|||||||
@@ -173,3 +173,46 @@ test.describe('Team Ownership Transfer', () => {
|
|||||||
await expect(ownerRow.locator('text=Primary Owner')).not.toBeVisible();
|
await expect(ownerRow.locator('text=Primary Owner')).not.toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Team Account Security', () => {
|
||||||
|
test('unauthorized user cannot access team account', async ({
|
||||||
|
page,
|
||||||
|
browser,
|
||||||
|
}) => {
|
||||||
|
// 1. Create a team account with User A
|
||||||
|
const teamAccounts = new TeamAccountsPageObject(page);
|
||||||
|
const params = teamAccounts.createTeamName();
|
||||||
|
|
||||||
|
// Setup User A and create team
|
||||||
|
await teamAccounts.setup(params);
|
||||||
|
|
||||||
|
// Store team slug for later use
|
||||||
|
const teamSlug = params.slug;
|
||||||
|
|
||||||
|
// 2. Sign out User A
|
||||||
|
await page.context().clearCookies();
|
||||||
|
|
||||||
|
// 3. Create a new context for User B (to have clean cookies/session)
|
||||||
|
const userBContext = await browser.newContext();
|
||||||
|
const userBPage = await userBContext.newPage();
|
||||||
|
const userBTeamAccounts = new TeamAccountsPageObject(userBPage);
|
||||||
|
|
||||||
|
// Sign up with User B
|
||||||
|
await userBPage.goto('/auth/sign-up');
|
||||||
|
const emailB = userBTeamAccounts.auth.createRandomEmail();
|
||||||
|
|
||||||
|
await userBTeamAccounts.auth.signUp({
|
||||||
|
email: emailB,
|
||||||
|
password: 'password',
|
||||||
|
repeatPassword: 'password',
|
||||||
|
});
|
||||||
|
|
||||||
|
await userBTeamAccounts.auth.visitConfirmEmailLink(emailB);
|
||||||
|
|
||||||
|
// 4. Attempt to access the team page with User B
|
||||||
|
await userBPage.goto(`/home/${teamSlug}`);
|
||||||
|
|
||||||
|
// Check that we're not on the team page anymore (should redirect)
|
||||||
|
await expect(userBPage).toHaveURL(`/home`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function AdminSidebar() {
|
|||||||
|
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Admin</SidebarGroupLabel>
|
<SidebarGroupLabel>Super Admin</SidebarGroupLabel>
|
||||||
|
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { cache } from 'react';
|
|||||||
|
|
||||||
import { AdminAccountPage } from '@kit/admin/components/admin-account-page';
|
import { AdminAccountPage } from '@kit/admin/components/admin-account-page';
|
||||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
interface Params {
|
interface Params {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -31,7 +31,7 @@ export default AdminGuard(AccountPage);
|
|||||||
const loadAccount = cache(accountLoader);
|
const loadAccount = cache(accountLoader);
|
||||||
|
|
||||||
async function accountLoader(id: string) {
|
async function accountLoader(id: string) {
|
||||||
const client = getSupabaseServerAdminClient();
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
const { data, error } = await client
|
const { data, error } = await client
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs';
|
|||||||
|
|
||||||
import { AdminAccountsTable } from '@kit/admin/components/admin-accounts-table';
|
import { AdminAccountsTable } from '@kit/admin/components/admin-accounts-table';
|
||||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export const metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function AccountsPage(props: AdminAccountsPageProps) {
|
async function AccountsPage(props: AdminAccountsPageProps) {
|
||||||
const client = getSupabaseServerAdminClient();
|
const client = getSupabaseServerClient();
|
||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
|
|
||||||
const page = searchParams.page ? parseInt(searchParams.page) : 1;
|
const page = searchParams.page ? parseInt(searchParams.page) : 1;
|
||||||
@@ -47,6 +47,7 @@ async function AccountsPage(props: AdminAccountsPageProps) {
|
|||||||
data={data}
|
data={data}
|
||||||
filters={{
|
filters={{
|
||||||
type: searchParams.account_type ?? 'all',
|
type: searchParams.account_type ?? 'all',
|
||||||
|
query: searchParams.query ?? '',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -858,6 +858,14 @@ export type Database = {
|
|||||||
};
|
};
|
||||||
Returns: boolean;
|
Returns: boolean;
|
||||||
};
|
};
|
||||||
|
install_extensions: {
|
||||||
|
Args: Record<PropertyKey, never>;
|
||||||
|
Returns: undefined;
|
||||||
|
};
|
||||||
|
is_aal2: {
|
||||||
|
Args: Record<PropertyKey, never>;
|
||||||
|
Returns: boolean;
|
||||||
|
};
|
||||||
is_account_owner: {
|
is_account_owner: {
|
||||||
Args: {
|
Args: {
|
||||||
account_id: string;
|
account_id: string;
|
||||||
@@ -870,12 +878,20 @@ export type Database = {
|
|||||||
};
|
};
|
||||||
Returns: boolean;
|
Returns: boolean;
|
||||||
};
|
};
|
||||||
|
is_mfa_compliant: {
|
||||||
|
Args: Record<PropertyKey, never>;
|
||||||
|
Returns: boolean;
|
||||||
|
};
|
||||||
is_set: {
|
is_set: {
|
||||||
Args: {
|
Args: {
|
||||||
field_name: string;
|
field_name: string;
|
||||||
};
|
};
|
||||||
Returns: boolean;
|
Returns: boolean;
|
||||||
};
|
};
|
||||||
|
is_super_admin: {
|
||||||
|
Args: Record<PropertyKey, never>;
|
||||||
|
Returns: boolean;
|
||||||
|
};
|
||||||
is_team_member: {
|
is_team_member: {
|
||||||
Args: {
|
Args: {
|
||||||
account_id: string;
|
account_id: string;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { NextResponse, URLPattern } from 'next/server';
|
|||||||
|
|
||||||
import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs';
|
import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs';
|
||||||
|
|
||||||
|
import { isSuperAdmin } from '@kit/admin';
|
||||||
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
|
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
|
||||||
import { createMiddlewareClient } from '@kit/supabase/middleware-client';
|
import { createMiddlewareClient } from '@kit/supabase/middleware-client';
|
||||||
|
|
||||||
@@ -115,22 +116,11 @@ async function adminMiddleware(request: NextRequest, response: NextResponse) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabase = createMiddlewareClient(request, response);
|
const client = createMiddlewareClient(request, response);
|
||||||
|
const userIsSuperAdmin = await isSuperAdmin(client);
|
||||||
const requiresMultiFactorAuthentication =
|
|
||||||
await checkRequiresMultiFactorAuthentication(supabase);
|
|
||||||
|
|
||||||
// If user requires multi-factor authentication, redirect to MFA page.
|
|
||||||
if (requiresMultiFactorAuthentication) {
|
|
||||||
return NextResponse.redirect(
|
|
||||||
new URL(pathsConfig.auth.verifyMfa, origin).href,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const role = user?.app_metadata.role;
|
|
||||||
|
|
||||||
// If user is not an admin, redirect to 404 page.
|
// If user is not an admin, redirect to 404 page.
|
||||||
if (!role || role !== 'super-admin') {
|
if (!userIsSuperAdmin) {
|
||||||
return NextResponse.redirect(new URL('/404', request.nextUrl.origin).href);
|
return NextResponse.redirect(new URL('/404', request.nextUrl.origin).href);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
"mfaEnabledSuccessDescription": "Congratulations! You have successfully enrolled in the multi factor authentication process. You will now be able to access your account with a combination of your password and an authentication code sent to your phone number.",
|
"mfaEnabledSuccessDescription": "Congratulations! You have successfully enrolled in the multi factor authentication process. You will now be able to access your account with a combination of your password and an authentication code sent to your phone number.",
|
||||||
"verificationCode": "Verification Code",
|
"verificationCode": "Verification Code",
|
||||||
"addEmailAddress": "Add Email address",
|
"addEmailAddress": "Add Email address",
|
||||||
"verifyActivationCodeDescription": "Enter the verification code generated by your authenticator app",
|
"verifyActivationCodeDescription": "Enter the 6-digit code generated by your authenticator app in the field above",
|
||||||
"loadingFactors": "Loading factors...",
|
"loadingFactors": "Loading factors...",
|
||||||
"enableMfaFactor": "Enable Factor",
|
"enableMfaFactor": "Enable Factor",
|
||||||
"disableMfaFactor": "Disable Factor",
|
"disableMfaFactor": "Disable Factor",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"sendLinkSuccess": "We sent you a link by email",
|
"sendLinkSuccess": "We sent you a link by email",
|
||||||
"sendLinkSuccessToast": "Link successfully sent",
|
"sendLinkSuccessToast": "Link successfully sent",
|
||||||
"getNewLink": "Get a new link",
|
"getNewLink": "Get a new link",
|
||||||
|
"verifyCodeHeading": "Verify your account",
|
||||||
"verificationCode": "Verification Code",
|
"verificationCode": "Verification Code",
|
||||||
"verificationCodeHint": "Enter the code we sent you by SMS",
|
"verificationCodeHint": "Enter the code we sent you by SMS",
|
||||||
"verificationCodeSubmitButtonLabel": "Submit Verification Code",
|
"verificationCodeSubmitButtonLabel": "Submit Verification Code",
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
* public.is_aal2
|
||||||
|
* Check if the user has aal2 access
|
||||||
|
*/
|
||||||
|
create
|
||||||
|
or replace function public.is_aal2() returns boolean
|
||||||
|
set
|
||||||
|
search_path = '' as
|
||||||
|
$$
|
||||||
|
declare
|
||||||
|
is_aal2 boolean;
|
||||||
|
begin
|
||||||
|
select auth.jwt() ->> 'aal' = 'aal2' into is_aal2;
|
||||||
|
|
||||||
|
return coalesce(is_aal2, false);
|
||||||
|
end
|
||||||
|
$$ language plpgsql;
|
||||||
|
|
||||||
|
-- Grant access to the function to authenticated users
|
||||||
|
grant execute on function public.is_aal2() to authenticated;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* public.is_super_admin
|
||||||
|
* Check if the user is a super admin.
|
||||||
|
* A Super Admin is a user that has the role 'super-admin' and has MFA enabled.
|
||||||
|
*/
|
||||||
|
create
|
||||||
|
or replace function public.is_super_admin() returns boolean
|
||||||
|
set
|
||||||
|
search_path = '' as
|
||||||
|
$$
|
||||||
|
declare
|
||||||
|
is_super_admin boolean;
|
||||||
|
begin
|
||||||
|
if not public.is_aal2() then
|
||||||
|
return false;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select (auth.jwt() ->> 'app_metadata')::jsonb ->> 'role' = 'super-admin' into is_super_admin;
|
||||||
|
|
||||||
|
return coalesce(is_super_admin, false);
|
||||||
|
end
|
||||||
|
$$ language plpgsql;
|
||||||
|
|
||||||
|
-- Grant access to the function to authenticated users
|
||||||
|
grant execute on function public.is_super_admin() to authenticated;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* public.is_mfa_compliant
|
||||||
|
* Check if the user meets MFA requirements if they have MFA enabled.
|
||||||
|
* If the user has MFA enabled, then the user must have aal2 enabled. Otherwise, the user must have aal1 enabled (default behavior).
|
||||||
|
*/
|
||||||
|
create or replace function public.is_mfa_compliant() returns boolean
|
||||||
|
set search_path = '' as
|
||||||
|
$$
|
||||||
|
begin
|
||||||
|
return array[(select auth.jwt()->>'aal')] <@ (
|
||||||
|
select
|
||||||
|
case
|
||||||
|
when count(id) > 0 then array['aal2']
|
||||||
|
else array['aal1', 'aal2']
|
||||||
|
end as aal
|
||||||
|
from auth.mfa_factors
|
||||||
|
where ((select auth.uid()) = auth.mfa_factors.user_id) and auth.mfa_factors.status = 'verified'
|
||||||
|
);
|
||||||
|
end
|
||||||
|
$$ language plpgsql security definer;
|
||||||
|
|
||||||
|
-- Grant access to the function to authenticated users
|
||||||
|
grant execute on function public.is_mfa_compliant() to authenticated;
|
||||||
|
|
||||||
|
-- MFA Restrictions:
|
||||||
|
-- the following policies are applied to the tables as a
|
||||||
|
-- restrictive policy to ensure that if MFA is enabled, then the policy will be applied.
|
||||||
|
-- For users that have not enabled MFA, the policy will not be applied and will keep the default behavior.
|
||||||
|
|
||||||
|
-- Restrict access to accounts if MFA is enabled
|
||||||
|
create policy restrict_mfa_accounts
|
||||||
|
on public.accounts
|
||||||
|
as restrictive
|
||||||
|
to authenticated
|
||||||
|
using (public.is_mfa_compliant());
|
||||||
|
|
||||||
|
-- Restrict access to accounts memberships if MFA is enabled
|
||||||
|
create policy restrict_mfa_accounts_memberships
|
||||||
|
on public.accounts_memberships
|
||||||
|
as restrictive
|
||||||
|
to authenticated
|
||||||
|
using (public.is_mfa_compliant());
|
||||||
|
|
||||||
|
-- Restrict access to subscriptions if MFA is enabled
|
||||||
|
create policy restrict_mfa_subscriptions
|
||||||
|
on public.subscriptions
|
||||||
|
as restrictive
|
||||||
|
to authenticated
|
||||||
|
using (public.is_mfa_compliant());
|
||||||
|
|
||||||
|
-- Restrict access to subscription items if MFA is enabled
|
||||||
|
create policy restrict_mfa_subscription_items
|
||||||
|
on public.subscription_items
|
||||||
|
as restrictive
|
||||||
|
to authenticated
|
||||||
|
using (public.is_mfa_compliant());
|
||||||
|
|
||||||
|
-- Restrict access to role permissions if MFA is enabled
|
||||||
|
create policy restrict_mfa_role_permissions
|
||||||
|
on public.role_permissions
|
||||||
|
as restrictive
|
||||||
|
to authenticated
|
||||||
|
using (public.is_mfa_compliant());
|
||||||
|
|
||||||
|
-- Restrict access to invitations if MFA is enabled
|
||||||
|
create policy restrict_mfa_invitations
|
||||||
|
on public.invitations
|
||||||
|
as restrictive
|
||||||
|
to authenticated
|
||||||
|
using (public.is_mfa_compliant());
|
||||||
|
|
||||||
|
-- Restrict access to orders if MFA is enabled
|
||||||
|
create policy restrict_mfa_orders
|
||||||
|
on public.orders
|
||||||
|
as restrictive
|
||||||
|
to authenticated
|
||||||
|
using (public.is_mfa_compliant());
|
||||||
|
|
||||||
|
-- Restrict access to orders items if MFA is enabled
|
||||||
|
create policy restrict_mfa_order_items
|
||||||
|
on public.order_items
|
||||||
|
as restrictive
|
||||||
|
to authenticated
|
||||||
|
using (public.is_mfa_compliant());
|
||||||
|
|
||||||
|
-- Restrict access to orders if MFA is enabled
|
||||||
|
create policy restrict_mfa_notifications
|
||||||
|
on public.notifications
|
||||||
|
as restrictive
|
||||||
|
to authenticated
|
||||||
|
using (public.is_mfa_compliant());
|
||||||
|
|
||||||
|
-- Super Admin:
|
||||||
|
-- the following policies are applied to the tables as a permissive policy to ensure that
|
||||||
|
-- super admins can access all tables (view only).
|
||||||
|
|
||||||
|
-- Allow Super Admins to access the accounts table
|
||||||
|
create policy super_admins_access_accounts
|
||||||
|
on public.accounts
|
||||||
|
as permissive
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (public.is_super_admin());
|
||||||
|
|
||||||
|
-- Allow Super Admins to access the accounts memberships table
|
||||||
|
create policy super_admins_access_accounts_memberships
|
||||||
|
on public.accounts_memberships
|
||||||
|
as permissive
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (public.is_super_admin());
|
||||||
|
|
||||||
|
-- Allow Super Admins to access the subscriptions table
|
||||||
|
create policy super_admins_access_subscriptions
|
||||||
|
on public.subscriptions
|
||||||
|
as permissive
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (public.is_super_admin());
|
||||||
|
|
||||||
|
-- Allow Super Admins to access the subscription items table
|
||||||
|
create policy super_admins_access_subscription_items
|
||||||
|
on public.subscription_items
|
||||||
|
as permissive
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (public.is_super_admin());
|
||||||
|
|
||||||
|
-- Allow Super Admins to access the invitations items table
|
||||||
|
create policy super_admins_access_invitations
|
||||||
|
on public.invitations
|
||||||
|
as permissive
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (public.is_super_admin());
|
||||||
|
|
||||||
|
-- Allow Super Admins to access the orders table
|
||||||
|
create policy super_admins_access_orders
|
||||||
|
on public.orders
|
||||||
|
as permissive
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (public.is_super_admin());
|
||||||
|
|
||||||
|
-- Allow Super Admins to access the order items table
|
||||||
|
create policy super_admins_access_order_items
|
||||||
|
on public.order_items
|
||||||
|
as permissive
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (public.is_super_admin());
|
||||||
|
|
||||||
|
-- Allow Super Admins to access the role permissions table
|
||||||
|
create policy super_admins_access_role_permissions
|
||||||
|
on public.role_permissions
|
||||||
|
as permissive
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (public.is_super_admin());
|
||||||
@@ -6,8 +6,10 @@
|
|||||||
-- We don't do it because you'll need to manually add your webhook URL and secret key.
|
-- We don't do it because you'll need to manually add your webhook URL and secret key.
|
||||||
|
|
||||||
-- this webhook will be triggered after deleting an account
|
-- this webhook will be triggered after deleting an account
|
||||||
create trigger "accounts_teardown" after delete
|
create trigger "accounts_teardown"
|
||||||
on "public"."accounts" for each row
|
after delete
|
||||||
|
on "public"."accounts"
|
||||||
|
for each row
|
||||||
execute function "supabase_functions"."http_request"(
|
execute function "supabase_functions"."http_request"(
|
||||||
'http://host.docker.internal:3000/api/db/webhook',
|
'http://host.docker.internal:3000/api/db/webhook',
|
||||||
'POST',
|
'POST',
|
||||||
@@ -18,8 +20,10 @@ execute function "supabase_functions"."http_request"(
|
|||||||
|
|
||||||
-- this webhook will be triggered after a delete on the subscriptions table
|
-- this webhook will be triggered after a delete on the subscriptions table
|
||||||
-- which should happen when a user deletes their account (and all their subscriptions)
|
-- which should happen when a user deletes their account (and all their subscriptions)
|
||||||
create trigger "subscriptions_delete" after delete
|
create trigger "subscriptions_delete"
|
||||||
on "public"."subscriptions" for each row
|
after delete
|
||||||
|
on "public"."subscriptions"
|
||||||
|
for each row
|
||||||
execute function "supabase_functions"."http_request"(
|
execute function "supabase_functions"."http_request"(
|
||||||
'http://host.docker.internal:3000/api/db/webhook',
|
'http://host.docker.internal:3000/api/db/webhook',
|
||||||
'POST',
|
'POST',
|
||||||
@@ -30,8 +34,10 @@ execute function "supabase_functions"."http_request"(
|
|||||||
|
|
||||||
-- this webhook will be triggered after every insert on the invitations table
|
-- this webhook will be triggered after every insert on the invitations table
|
||||||
-- which should happen when a user invites someone to their account
|
-- which should happen when a user invites someone to their account
|
||||||
create trigger "invitations_insert" after insert
|
create trigger "invitations_insert"
|
||||||
on "public"."invitations" for each row
|
after insert
|
||||||
|
on "public"."invitations"
|
||||||
|
for each row
|
||||||
execute function "supabase_functions"."http_request"(
|
execute function "supabase_functions"."http_request"(
|
||||||
'http://host.docker.internal:3000/api/db/webhook',
|
'http://host.docker.internal:3000/api/db/webhook',
|
||||||
'POST',
|
'POST',
|
||||||
@@ -50,25 +56,81 @@ execute function "supabase_functions"."http_request"(
|
|||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: users; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
-- Data for Name: users; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
||||||
--
|
--
|
||||||
|
|
||||||
INSERT INTO "auth"."users" ("instance_id", "id", "aud", "role", "email", "encrypted_password", "email_confirmed_at", "invited_at", "confirmation_token", "confirmation_sent_at", "recovery_token", "recovery_sent_at", "email_change_token_new", "email_change", "email_change_sent_at", "last_sign_in_at", "raw_app_meta_data", "raw_user_meta_data", "is_super_admin", "created_at", "updated_at", "phone", "phone_confirmed_at", "phone_change", "phone_change_token", "phone_change_sent_at", "email_change_token_current", "email_change_confirm_status", "banned_until", "reauthentication_token", "reauthentication_sent_at", "is_sso_user", "deleted_at", "is_anonymous") VALUES
|
INSERT INTO "auth"."users" ("instance_id", "id", "aud", "role", "email", "encrypted_password", "email_confirmed_at",
|
||||||
('00000000-0000-0000-0000-000000000000', 'b73eb03e-fb7a-424d-84ff-18e2791ce0b4', 'authenticated', 'authenticated', 'custom@makerkit.dev', '$2a$10$b3ZPpU6TU3or30QzrXnZDuATPAx2pPq3JW.sNaneVY3aafMSuR4yi', '2024-04-20 08:38:00.860548+00', NULL, '', '2024-04-20 08:37:43.343769+00', '', NULL, '', '', NULL, '2024-04-20 08:38:00.93864+00', '{"provider": "email", "providers": ["email"]}', '{"sub": "b73eb03e-fb7a-424d-84ff-18e2791ce0b4", "email": "custom@makerkit.dev", "email_verified": false, "phone_verified": false}', NULL, '2024-04-20 08:37:43.3385+00', '2024-04-20 08:38:00.942809+00', NULL, NULL, '', '', NULL, '', 0, NULL, '', NULL, false, NULL, false),
|
"invited_at", "confirmation_token", "confirmation_sent_at", "recovery_token",
|
||||||
('00000000-0000-0000-0000-000000000000', '31a03e74-1639-45b6-bfa7-77447f1a4762', 'authenticated', 'authenticated', 'test@makerkit.dev', '$2a$10$NaMVRrI7NyfwP.AfAVWt6O/abulGnf9BBqwa6DqdMwXMvOCGpAnVO', '2024-04-20 08:20:38.165331+00', NULL, '', NULL, '', NULL, '', '', NULL, '2024-04-20 09:36:02.521776+00', '{"provider": "email", "providers": ["email"], "role": "super-admin"}', '{"sub": "31a03e74-1639-45b6-bfa7-77447f1a4762", "email": "test@makerkit.dev", "email_verified": false, "phone_verified": false}', NULL, '2024-04-20 08:20:34.459113+00', '2024-04-20 10:07:48.554125+00', NULL, NULL, '', '', NULL, '', 0, NULL, '', NULL, false, NULL, false),
|
"recovery_sent_at", "email_change_token_new", "email_change", "email_change_sent_at",
|
||||||
('00000000-0000-0000-0000-000000000000', '5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf', 'authenticated', 'authenticated', 'owner@makerkit.dev', '$2a$10$D6arGxWJShy8q4RTW18z7eW0vEm2hOxEUovUCj5f3NblyHfamm5/a', '2024-04-20 08:36:37.517993+00', NULL, '', '2024-04-20 08:36:27.639648+00', '', NULL, '', '', NULL, '2024-04-20 08:36:37.614337+00', '{"provider": "email", "providers": ["email"]}', '{"sub": "5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf", "email": "owner@makerkit.dev", "email_verified": false, "phone_verified": false}', NULL, '2024-04-20 08:36:27.630379+00', '2024-04-20 08:36:37.617955+00', NULL, NULL, '', '', NULL, '', 0, NULL, '', NULL, false, NULL, false),
|
"last_sign_in_at", "raw_app_meta_data", "raw_user_meta_data", "is_super_admin",
|
||||||
('00000000-0000-0000-0000-000000000000', '6b83d656-e4ab-48e3-a062-c0c54a427368', 'authenticated', 'authenticated', 'member@makerkit.dev', '$2a$10$6h/x.AX.6zzphTfDXIJMzuYx13hIYEi/Iods9FXH19J2VxhsLycfa', '2024-04-20 08:41:15.376778+00', NULL, '', '2024-04-20 08:41:08.689674+00', '', NULL, '', '', NULL, '2024-04-20 08:41:15.484606+00', '{"provider": "email", "providers": ["email"]}', '{"sub": "6b83d656-e4ab-48e3-a062-c0c54a427368", "email": "member@makerkit.dev", "email_verified": false, "phone_verified": false}', NULL, '2024-04-20 08:41:08.683395+00', '2024-04-20 08:41:15.485494+00', NULL, NULL, '', '', NULL, '', 0, NULL, '', NULL, false, NULL, false);
|
"created_at", "updated_at", "phone", "phone_confirmed_at", "phone_change",
|
||||||
|
"phone_change_token", "phone_change_sent_at", "email_change_token_current",
|
||||||
|
"email_change_confirm_status", "banned_until", "reauthentication_token",
|
||||||
|
"reauthentication_sent_at", "is_sso_user", "deleted_at", "is_anonymous")
|
||||||
|
VALUES ('00000000-0000-0000-0000-000000000000', 'b73eb03e-fb7a-424d-84ff-18e2791ce0b4', 'authenticated',
|
||||||
|
'authenticated', 'custom@makerkit.dev', '$2a$10$b3ZPpU6TU3or30QzrXnZDuATPAx2pPq3JW.sNaneVY3aafMSuR4yi',
|
||||||
|
'2024-04-20 08:38:00.860548+00', NULL, '', '2024-04-20 08:37:43.343769+00', '', NULL, '', '', NULL,
|
||||||
|
'2024-04-20 08:38:00.93864+00', '{"provider": "email", "providers": ["email"]}',
|
||||||
|
'{"sub": "b73eb03e-fb7a-424d-84ff-18e2791ce0b4", "email": "custom@makerkit.dev", "email_verified": false, "phone_verified": false}',
|
||||||
|
NULL, '2024-04-20 08:37:43.3385+00', '2024-04-20 08:38:00.942809+00', NULL, NULL, '', '', NULL, '', 0, NULL, '',
|
||||||
|
NULL, false, NULL, false),
|
||||||
|
('00000000-0000-0000-0000-000000000000', '31a03e74-1639-45b6-bfa7-77447f1a4762', 'authenticated',
|
||||||
|
'authenticated', 'test@makerkit.dev', '$2a$10$NaMVRrI7NyfwP.AfAVWt6O/abulGnf9BBqwa6DqdMwXMvOCGpAnVO',
|
||||||
|
'2024-04-20 08:20:38.165331+00', NULL, '', NULL, '', NULL, '', '', NULL, '2024-04-20 09:36:02.521776+00',
|
||||||
|
'{"provider": "email", "providers": ["email"], "role": "super-admin"}',
|
||||||
|
'{"sub": "31a03e74-1639-45b6-bfa7-77447f1a4762", "email": "test@makerkit.dev", "email_verified": false, "phone_verified": false}',
|
||||||
|
NULL, '2024-04-20 08:20:34.459113+00', '2024-04-20 10:07:48.554125+00', NULL, NULL, '', '', NULL, '', 0, NULL,
|
||||||
|
'', NULL, false, NULL, false),
|
||||||
|
('00000000-0000-0000-0000-000000000000', '5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf', 'authenticated',
|
||||||
|
'authenticated', 'owner@makerkit.dev', '$2a$10$D6arGxWJShy8q4RTW18z7eW0vEm2hOxEUovUCj5f3NblyHfamm5/a',
|
||||||
|
'2024-04-20 08:36:37.517993+00', NULL, '', '2024-04-20 08:36:27.639648+00', '', NULL, '', '', NULL,
|
||||||
|
'2024-04-20 08:36:37.614337+00', '{"provider": "email", "providers": ["email"]}',
|
||||||
|
'{"sub": "5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf", "email": "owner@makerkit.dev", "email_verified": false, "phone_verified": false}',
|
||||||
|
NULL, '2024-04-20 08:36:27.630379+00', '2024-04-20 08:36:37.617955+00', NULL, NULL, '', '', NULL, '', 0, NULL,
|
||||||
|
'', NULL, false, NULL, false),
|
||||||
|
('00000000-0000-0000-0000-000000000000', '6b83d656-e4ab-48e3-a062-c0c54a427368', 'authenticated',
|
||||||
|
'authenticated', 'member@makerkit.dev', '$2a$10$6h/x.AX.6zzphTfDXIJMzuYx13hIYEi/Iods9FXH19J2VxhsLycfa',
|
||||||
|
'2024-04-20 08:41:15.376778+00', NULL, '', '2024-04-20 08:41:08.689674+00', '', NULL, '', '', NULL,
|
||||||
|
'2024-04-20 08:41:15.484606+00', '{"provider": "email", "providers": ["email"]}',
|
||||||
|
'{"sub": "6b83d656-e4ab-48e3-a062-c0c54a427368", "email": "member@makerkit.dev", "email_verified": false, "phone_verified": false}',
|
||||||
|
NULL, '2024-04-20 08:41:08.683395+00', '2024-04-20 08:41:15.485494+00', NULL, NULL, '', '', NULL, '', 0, NULL,
|
||||||
|
'', NULL, false, NULL, false),
|
||||||
|
('00000000-0000-0000-0000-000000000000', 'c5b930c9-0a76-412e-a836-4bc4849a3270', 'authenticated',
|
||||||
|
'authenticated', 'super-admin@makerkit.dev',
|
||||||
|
'$2a$10$gzxQw3vaVni8Ke9UVcn6ueWh674.6xImf6/yWYNc23BSeYdE9wmki', '2025-02-24 13:25:11.176987+00', null, '',
|
||||||
|
'2025-02-24 13:25:01.649714+00', '', null, '', '', null, '2025-02-24 13:25:11.17957+00',
|
||||||
|
'{"provider": "email", "providers": ["email"], "role": "super-admin"}',
|
||||||
|
'{"sub": "c5b930c9-0a76-412e-a836-4bc4849a3270", "email": "super-admin@makerkit.dev", "email_verified": true, "phone_verified": false}',
|
||||||
|
null, '2025-02-24 13:25:01.646641+00', '2025-02-24 13:25:11.181332+00', null, null, '', '', null
|
||||||
|
, '', '0', null, '', null, 'false', null, 'false');
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: identities; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
-- Data for Name: identities; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
||||||
--
|
--
|
||||||
|
|
||||||
INSERT INTO "auth"."identities" ("provider_id", "user_id", "identity_data", "provider", "last_sign_in_at", "created_at", "updated_at", "id") VALUES
|
INSERT INTO "auth"."identities" ("provider_id", "user_id", "identity_data", "provider", "last_sign_in_at", "created_at",
|
||||||
('31a03e74-1639-45b6-bfa7-77447f1a4762', '31a03e74-1639-45b6-bfa7-77447f1a4762', '{"sub": "31a03e74-1639-45b6-bfa7-77447f1a4762", "email": "test@makerkit.dev", "email_verified": false, "phone_verified": false}', 'email', '2024-04-20 08:20:34.46275+00', '2024-04-20 08:20:34.462773+00', '2024-04-20 08:20:34.462773+00', '9bb58bad-24a4-41a8-9742-1b5b4e2d8abd'), ('5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf', '5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf', '{"sub": "5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf", "email": "owner@makerkit.dev", "email_verified": false, "phone_verified": false}', 'email', '2024-04-20 08:36:27.637388+00', '2024-04-20 08:36:27.637409+00', '2024-04-20 08:36:27.637409+00', '090598a1-ebba-4879-bbe3-38d517d5066f'),
|
"updated_at", "id")
|
||||||
('b73eb03e-fb7a-424d-84ff-18e2791ce0b4', 'b73eb03e-fb7a-424d-84ff-18e2791ce0b4', '{"sub": "b73eb03e-fb7a-424d-84ff-18e2791ce0b4", "email": "custom@makerkit.dev", "email_verified": false, "phone_verified": false}', 'email', '2024-04-20 08:37:43.342194+00', '2024-04-20 08:37:43.342218+00', '2024-04-20 08:37:43.342218+00', '4392e228-a6d8-4295-a7d6-baed50c33e7c'),
|
VALUES ('31a03e74-1639-45b6-bfa7-77447f1a4762', '31a03e74-1639-45b6-bfa7-77447f1a4762',
|
||||||
('6b83d656-e4ab-48e3-a062-c0c54a427368', '6b83d656-e4ab-48e3-a062-c0c54a427368', '{"sub": "6b83d656-e4ab-48e3-a062-c0c54a427368", "email": "member@makerkit.dev", "email_verified": false, "phone_verified": false}', 'email', '2024-04-20 08:41:08.687948+00', '2024-04-20 08:41:08.687982+00', '2024-04-20 08:41:08.687982+00', 'd122aca5-4f29-43f0-b1b1-940b000638db');
|
'{"sub": "31a03e74-1639-45b6-bfa7-77447f1a4762", "email": "test@makerkit.dev", "email_verified": false, "phone_verified": false}',
|
||||||
|
'email', '2024-04-20 08:20:34.46275+00', '2024-04-20 08:20:34.462773+00', '2024-04-20 08:20:34.462773+00',
|
||||||
|
'9bb58bad-24a4-41a8-9742-1b5b4e2d8abd'),
|
||||||
|
('5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf', '5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf',
|
||||||
|
'{"sub": "5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf", "email": "owner@makerkit.dev", "email_verified": false, "phone_verified": false}',
|
||||||
|
'email', '2024-04-20 08:36:27.637388+00', '2024-04-20 08:36:27.637409+00', '2024-04-20 08:36:27.637409+00',
|
||||||
|
'090598a1-ebba-4879-bbe3-38d517d5066f'),
|
||||||
|
('b73eb03e-fb7a-424d-84ff-18e2791ce0b4', 'b73eb03e-fb7a-424d-84ff-18e2791ce0b4',
|
||||||
|
'{"sub": "b73eb03e-fb7a-424d-84ff-18e2791ce0b4", "email": "custom@makerkit.dev", "email_verified": false, "phone_verified": false}',
|
||||||
|
'email', '2024-04-20 08:37:43.342194+00', '2024-04-20 08:37:43.342218+00', '2024-04-20 08:37:43.342218+00',
|
||||||
|
'4392e228-a6d8-4295-a7d6-baed50c33e7c'),
|
||||||
|
('6b83d656-e4ab-48e3-a062-c0c54a427368', '6b83d656-e4ab-48e3-a062-c0c54a427368',
|
||||||
|
'{"sub": "6b83d656-e4ab-48e3-a062-c0c54a427368", "email": "member@makerkit.dev", "email_verified": false, "phone_verified": false}',
|
||||||
|
'email', '2024-04-20 08:41:08.687948+00', '2024-04-20 08:41:08.687982+00', '2024-04-20 08:41:08.687982+00',
|
||||||
|
'd122aca5-4f29-43f0-b1b1-940b000638db'),
|
||||||
|
('c5b930c9-0a76-412e-a836-4bc4849a3270', 'c5b930c9-0a76-412e-a836-4bc4849a3270',
|
||||||
|
'{"sub": "c5b930c9-0a76-412e-a836-4bc4849a3270", "email": "super-admin@makerkit.dev", "email_verified": true, "phone_verified": false}',
|
||||||
|
'email', '2025-02-24 13:25:01.646641+00', '2025-02-24 13:25:11.181332+00', '2025-02-24 13:25:11.181332+00',
|
||||||
|
'c5b930c9-0a76-412e-a836-4bc4849a3270');
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: instances; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
-- Data for Name: instances; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
||||||
@@ -89,13 +151,11 @@ INSERT INTO "auth"."identities" ("provider_id", "user_id", "identity_data", "pro
|
|||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: mfa_challenges; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
-- Data for Name: mfa_challenges; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: refresh_tokens; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
-- Data for Name: refresh_tokens; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
||||||
--
|
--
|
||||||
@@ -105,74 +165,79 @@ INSERT INTO "auth"."identities" ("provider_id", "user_id", "identity_data", "pro
|
|||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: saml_providers; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
-- Data for Name: saml_providers; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: saml_relay_states; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
-- Data for Name: saml_relay_states; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: sso_domains; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
-- Data for Name: sso_domains; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: key; Type: TABLE DATA; Schema: pgsodium; Owner: supabase_admin
|
-- Data for Name: key; Type: TABLE DATA; Schema: pgsodium; Owner: supabase_admin
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: accounts; Type: TABLE DATA; Schema: public; Owner: postgres
|
-- Data for Name: accounts; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||||
--
|
--
|
||||||
|
|
||||||
INSERT INTO "public"."accounts" ("id", "primary_owner_user_id", "name", "slug", "email", "is_personal_account", "updated_at", "created_at", "created_by", "updated_by", "picture_url", "public_data") VALUES
|
INSERT INTO "public"."accounts" ("id", "primary_owner_user_id", "name", "slug", "email", "is_personal_account",
|
||||||
('5deaa894-2094-4da3-b4fd-1fada0809d1c', '31a03e74-1639-45b6-bfa7-77447f1a4762', 'Makerkit', 'makerkit', NULL, false, NULL, NULL, NULL, NULL, NULL, '{}');
|
"updated_at", "created_at", "created_by", "updated_by", "picture_url", "public_data")
|
||||||
|
VALUES ('5deaa894-2094-4da3-b4fd-1fada0809d1c', '31a03e74-1639-45b6-bfa7-77447f1a4762', 'Makerkit', 'makerkit', NULL,
|
||||||
|
false, NULL, NULL, NULL, NULL, NULL, '{}');
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: roles; Type: TABLE DATA; Schema: public; Owner: postgres
|
-- Data for Name: roles; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||||
--
|
--
|
||||||
|
|
||||||
INSERT INTO "public"."roles" ("name", "hierarchy_level") VALUES
|
INSERT INTO "public"."roles" ("name", "hierarchy_level")
|
||||||
('custom-role', 4);
|
VALUES ('custom-role', 4);
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: accounts_memberships; Type: TABLE DATA; Schema: public; Owner: postgres
|
-- Data for Name: accounts_memberships; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||||
--
|
--
|
||||||
|
|
||||||
INSERT INTO "public"."accounts_memberships" ("user_id", "account_id", "account_role", "created_at", "updated_at", "created_by", "updated_by") VALUES
|
INSERT INTO "public"."accounts_memberships" ("user_id", "account_id", "account_role", "created_at", "updated_at",
|
||||||
('31a03e74-1639-45b6-bfa7-77447f1a4762', '5deaa894-2094-4da3-b4fd-1fada0809d1c', 'owner', '2024-04-20 08:21:16.802867+00', '2024-04-20 08:21:16.802867+00', NULL, NULL),
|
"created_by", "updated_by")
|
||||||
('5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf', '5deaa894-2094-4da3-b4fd-1fada0809d1c', 'owner', '2024-04-20 08:36:44.21028+00', '2024-04-20 08:36:44.21028+00', NULL, NULL),
|
VALUES ('31a03e74-1639-45b6-bfa7-77447f1a4762', '5deaa894-2094-4da3-b4fd-1fada0809d1c', 'owner',
|
||||||
('b73eb03e-fb7a-424d-84ff-18e2791ce0b4', '5deaa894-2094-4da3-b4fd-1fada0809d1c', 'custom-role', '2024-04-20 08:38:02.50993+00', '2024-04-20 08:38:02.50993+00', NULL, NULL),
|
'2024-04-20 08:21:16.802867+00', '2024-04-20 08:21:16.802867+00', NULL, NULL),
|
||||||
('6b83d656-e4ab-48e3-a062-c0c54a427368', '5deaa894-2094-4da3-b4fd-1fada0809d1c', 'member', '2024-04-20 08:41:17.833709+00', '2024-04-20 08:41:17.833709+00', NULL, NULL);
|
('5c064f1b-78ee-4e1c-ac3b-e99aa97c99bf', '5deaa894-2094-4da3-b4fd-1fada0809d1c', 'owner',
|
||||||
|
'2024-04-20 08:36:44.21028+00', '2024-04-20 08:36:44.21028+00', NULL, NULL),
|
||||||
|
('b73eb03e-fb7a-424d-84ff-18e2791ce0b4', '5deaa894-2094-4da3-b4fd-1fada0809d1c', 'custom-role',
|
||||||
|
'2024-04-20 08:38:02.50993+00', '2024-04-20 08:38:02.50993+00', NULL, NULL),
|
||||||
|
('6b83d656-e4ab-48e3-a062-c0c54a427368', '5deaa894-2094-4da3-b4fd-1fada0809d1c', 'member',
|
||||||
|
'2024-04-20 08:41:17.833709+00', '2024-04-20 08:41:17.833709+00', NULL, NULL);
|
||||||
|
|
||||||
|
-- MFA Factors
|
||||||
|
INSERT INTO "auth"."mfa_factors" ("id", "user_id", "friendly_name", "factor_type", "status", "created_at", "updated_at",
|
||||||
|
"secret", "phone", "last_challenged_at", "web_authn_credential", "web_authn_aaguid")
|
||||||
|
VALUES ('659e3b57-1128-4d26-8757-f714fd073fc4', 'c5b930c9-0a76-412e-a836-4bc4849a3270', 'iPhone', 'totp', 'verified',
|
||||||
|
'2025-02-24 13:23:55.5805+00', '2025-02-24 13:24:32.591999+00', 'NHOHJVGPO3R3LKVPRMNIYLCDMBHUM2SE', null,
|
||||||
|
'2025-02-24 13:24:32.563314+00', null, null);
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: billing_customers; Type: TABLE DATA; Schema: public; Owner: postgres
|
-- Data for Name: billing_customers; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: invitations; Type: TABLE DATA; Schema: public; Owner: postgres
|
-- Data for Name: invitations; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: orders; Type: TABLE DATA; Schema: public; Owner: postgres
|
-- Data for Name: orders; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: order_items; Type: TABLE DATA; Schema: public; Owner: postgres
|
-- Data for Name: order_items; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||||
--
|
--
|
||||||
@@ -183,7 +248,6 @@ INSERT INTO "public"."accounts_memberships" ("user_id", "account_id", "account_r
|
|||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: subscription_items; Type: TABLE DATA; Schema: public; Owner: postgres
|
-- Data for Name: subscription_items; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||||
--
|
--
|
||||||
@@ -198,19 +262,16 @@ INSERT INTO "public"."accounts_memberships" ("user_id", "account_id", "account_r
|
|||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: s3_multipart_uploads; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin
|
-- Data for Name: s3_multipart_uploads; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: s3_multipart_uploads_parts; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin
|
-- Data for Name: s3_multipart_uploads_parts; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: hooks; Type: TABLE DATA; Schema: supabase_functions; Owner: supabase_functions_admin
|
-- Data for Name: hooks; Type: TABLE DATA; Schema: supabase_functions; Owner: supabase_functions_admin
|
||||||
--
|
--
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ create or replace function makerkit.set_identifier(
|
|||||||
returns text
|
returns text
|
||||||
security definer
|
security definer
|
||||||
set search_path = auth, pg_temp
|
set search_path = auth, pg_temp
|
||||||
as $$
|
as
|
||||||
|
$$
|
||||||
begin
|
begin
|
||||||
update auth.users set raw_user_meta_data = jsonb_build_object('test_identifier', identifier)
|
update auth.users
|
||||||
|
set raw_user_meta_data = jsonb_build_object('test_identifier', identifier)
|
||||||
where email = user_email;
|
where email = user_email;
|
||||||
|
|
||||||
return identifier;
|
return identifier;
|
||||||
@@ -45,41 +47,91 @@ create or replace function makerkit.get_account_by_slug(
|
|||||||
account_slug text
|
account_slug text
|
||||||
)
|
)
|
||||||
returns setof accounts
|
returns setof accounts
|
||||||
as $$
|
as
|
||||||
|
$$
|
||||||
begin
|
begin
|
||||||
|
|
||||||
return query
|
return query
|
||||||
select
|
select *
|
||||||
*
|
from accounts
|
||||||
from
|
where slug = account_slug;
|
||||||
accounts
|
|
||||||
where
|
|
||||||
slug = account_slug;
|
|
||||||
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
$$ language PLPGSQL;
|
$$ language PLPGSQL;
|
||||||
|
|
||||||
|
create or replace function makerkit.authenticate_as(
|
||||||
|
identifier text
|
||||||
|
) returns void
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
begin
|
||||||
|
perform tests.authenticate_as(identifier);
|
||||||
|
perform makerkit.set_session_aal('aal1');
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
||||||
|
|
||||||
create or replace function makerkit.get_account_id_by_slug(
|
create or replace function makerkit.get_account_id_by_slug(
|
||||||
account_slug text
|
account_slug text
|
||||||
)
|
)
|
||||||
returns uuid
|
returns uuid
|
||||||
as $$
|
as
|
||||||
|
$$
|
||||||
|
|
||||||
begin
|
begin
|
||||||
|
|
||||||
return
|
return
|
||||||
(select
|
(select id
|
||||||
id
|
from accounts
|
||||||
from
|
where slug = account_slug);
|
||||||
accounts
|
|
||||||
where
|
|
||||||
slug = account_slug);
|
|
||||||
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
$$ language PLPGSQL;
|
$$ language PLPGSQL;
|
||||||
|
|
||||||
|
|
||||||
|
create or replace function makerkit.set_mfa_factor(
|
||||||
|
identifier text = gen_random_uuid()
|
||||||
|
)
|
||||||
|
returns void
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
begin
|
||||||
|
insert into "auth"."mfa_factors" ("id", "user_id", "friendly_name", "factor_type", "status", "created_at", "updated_at", "secret")
|
||||||
|
values (gen_random_uuid(), auth.uid(), identifier, 'totp', 'verified', '2025-02-24 09:48:18.402031+00', '2025-02-24 09:48:18.402031+00',
|
||||||
|
'HOWQFBA7KBDDRSBNMGFYZAFNPRSZ62I5');
|
||||||
|
end;
|
||||||
|
$$ language plpgsql security definer;
|
||||||
|
|
||||||
|
create or replace function makerkit.set_session_aal(session_aal auth.aal_level)
|
||||||
|
returns void
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
begin
|
||||||
|
perform set_config('request.jwt.claims', json_build_object(
|
||||||
|
'sub', current_setting('request.jwt.claims')::json ->> 'sub',
|
||||||
|
'email', current_setting('request.jwt.claims')::json ->> 'email',
|
||||||
|
'phone', current_setting('request.jwt.claims')::json ->> 'phone',
|
||||||
|
'user_metadata', current_setting('request.jwt.claims')::json ->> 'user_metadata',
|
||||||
|
'app_metadata', current_setting('request.jwt.claims')::json ->> 'app_metadata',
|
||||||
|
'aal', session_aal)::text, true);
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
||||||
|
|
||||||
|
create or replace function makerkit.set_super_admin() returns void
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
begin
|
||||||
|
perform set_config('request.jwt.claims', json_build_object(
|
||||||
|
'sub', current_setting('request.jwt.claims')::json ->> 'sub',
|
||||||
|
'email', current_setting('request.jwt.claims')::json ->> 'email',
|
||||||
|
'phone', current_setting('request.jwt.claims')::json ->> 'phone',
|
||||||
|
'user_metadata', current_setting('request.jwt.claims')::json ->> 'user_metadata',
|
||||||
|
'app_metadata', json_build_object('role', 'super-admin'),
|
||||||
|
'aal', current_setting('request.jwt.claims')::json ->> 'aal'
|
||||||
|
)::text, true);
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
||||||
|
|
||||||
begin;
|
begin;
|
||||||
|
|
||||||
select plan(1);
|
select plan(1);
|
||||||
@@ -92,8 +144,7 @@ select is_empty($$
|
|||||||
'get_account_by_slug should return an empty set when the account does not exist'
|
'get_account_by_slug should return an empty set when the account does not exist'
|
||||||
);
|
);
|
||||||
|
|
||||||
select
|
select *
|
||||||
*
|
|
||||||
from
|
from
|
||||||
finish();
|
finish();
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ select tests.create_supabase_user('test2');
|
|||||||
|
|
||||||
-- Create an team account
|
-- Create an team account
|
||||||
|
|
||||||
select tests.authenticate_as('test1');
|
select makerkit.authenticate_as('test1');
|
||||||
|
|
||||||
select public.create_team_account('Test');
|
select public.create_team_account('Test');
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ select row_eq(
|
|||||||
|
|
||||||
-- Foreigner should not have permissions to manage members
|
-- Foreigner should not have permissions to manage members
|
||||||
|
|
||||||
select tests.authenticate_as('test2');
|
select makerkit.authenticate_as('test2');
|
||||||
|
|
||||||
select row_eq(
|
select row_eq(
|
||||||
$$ select public.has_permission(
|
$$ select public.has_permission(
|
||||||
@@ -81,7 +81,7 @@ set local role postgres;
|
|||||||
-- insert permissions for the custom role
|
-- insert permissions for the custom role
|
||||||
insert into public.role_permissions (role, permission) values ('custom-role', 'members.manage');
|
insert into public.role_permissions (role, permission) values ('custom-role', 'members.manage');
|
||||||
|
|
||||||
select tests.authenticate_as('test1');
|
select makerkit.authenticate_as('test1');
|
||||||
|
|
||||||
-- the custom role does not have permissions to manage billing
|
-- the custom role does not have permissions to manage billing
|
||||||
select row_eq(
|
select row_eq(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ select tests.create_supabase_user('test2');
|
|||||||
|
|
||||||
-- Create an team account
|
-- Create an team account
|
||||||
|
|
||||||
select tests.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');
|
select public.create_team_account('Test');
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ select makerkit.set_identifier('custom', 'custom@makerkit.dev');
|
|||||||
select tests.create_supabase_user('test', 'test@supabase.com');
|
select tests.create_supabase_user('test', 'test@supabase.com');
|
||||||
|
|
||||||
-- an owner cannot remove the primary owner
|
-- an owner cannot remove the primary owner
|
||||||
select tests.authenticate_as('owner');
|
select makerkit.authenticate_as('owner');
|
||||||
|
|
||||||
select throws_ok(
|
select throws_ok(
|
||||||
$$ delete from public.accounts_memberships
|
$$ delete from public.accounts_memberships
|
||||||
@@ -30,7 +30,7 @@ select lives_ok(
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- a member cannot remove a member with a higher role
|
-- a member cannot remove a member with a higher role
|
||||||
select tests.authenticate_as('member');
|
select makerkit.authenticate_as('member');
|
||||||
|
|
||||||
-- delete a membership record where the user is a higher role than the current user
|
-- delete a membership record where the user is a higher role than the current user
|
||||||
select throws_ok(
|
select throws_ok(
|
||||||
@@ -41,7 +41,7 @@ select throws_ok(
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- an primary_owner cannot remove themselves
|
-- an primary_owner cannot remove themselves
|
||||||
select tests.authenticate_as('primary_owner');
|
select makerkit.authenticate_as('primary_owner');
|
||||||
|
|
||||||
select throws_ok(
|
select throws_ok(
|
||||||
$$ delete from public.accounts_memberships
|
$$ delete from public.accounts_memberships
|
||||||
@@ -62,7 +62,7 @@ select lives_ok(
|
|||||||
|
|
||||||
-- a user not in the account cannot remove a member
|
-- a user not in the account cannot remove a member
|
||||||
|
|
||||||
select tests.authenticate_as('test');
|
select makerkit.authenticate_as('test');
|
||||||
|
|
||||||
select throws_ok(
|
select throws_ok(
|
||||||
$$ delete from public.accounts_memberships
|
$$ delete from public.accounts_memberships
|
||||||
@@ -71,7 +71,7 @@ select throws_ok(
|
|||||||
'You do not have permission to action a member from this account'
|
'You do not have permission to action a member from this account'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('owner');
|
select makerkit.authenticate_as('owner');
|
||||||
|
|
||||||
select isnt_empty(
|
select isnt_empty(
|
||||||
$$ select 1 from public.accounts_memberships
|
$$ select 1 from public.accounts_memberships
|
||||||
@@ -79,7 +79,7 @@ select isnt_empty(
|
|||||||
and user_id = tests.get_supabase_uid('owner'); $$,
|
and user_id = tests.get_supabase_uid('owner'); $$,
|
||||||
'Foreigners should not be able to remove members');
|
'Foreigners should not be able to remove members');
|
||||||
|
|
||||||
select tests.authenticate_as('test');
|
select makerkit.authenticate_as('test');
|
||||||
|
|
||||||
-- a user not in the account cannot remove themselves
|
-- a user not in the account cannot remove themselves
|
||||||
select throws_ok(
|
select throws_ok(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ select makerkit.set_identifier('member', 'member@makerkit.dev');
|
|||||||
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
|
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
|
||||||
select makerkit.set_identifier('owner', 'owner@makerkit.dev');
|
select makerkit.set_identifier('owner', 'owner@makerkit.dev');
|
||||||
|
|
||||||
select tests.authenticate_as('test');
|
select makerkit.authenticate_as('test');
|
||||||
|
|
||||||
select lives_ok(
|
select lives_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()); $$,
|
$$ 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()); $$,
|
||||||
@@ -23,7 +23,7 @@ select throws_ok(
|
|||||||
'duplicate key value violates unique constraint "invitations_email_account_id_key"'
|
'duplicate key value violates unique constraint "invitations_email_account_id_key"'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('member');
|
select makerkit.authenticate_as('member');
|
||||||
|
|
||||||
-- check a member cannot invite members with higher roles
|
-- check a member cannot invite members with higher roles
|
||||||
select throws_ok(
|
select throws_ok(
|
||||||
@@ -43,7 +43,7 @@ select isnt_empty(
|
|||||||
'invitations should be listed'
|
'invitations should be listed'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('owner');
|
select makerkit.authenticate_as('owner');
|
||||||
|
|
||||||
-- check the owner can invite members with lower roles
|
-- check the owner can invite members with lower roles
|
||||||
select lives_ok(
|
select lives_ok(
|
||||||
@@ -52,7 +52,7 @@ select lives_ok(
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- authenticate_as the custom role
|
-- authenticate_as the custom role
|
||||||
select tests.authenticate_as('custom');
|
select makerkit.authenticate_as('custom');
|
||||||
|
|
||||||
-- it will fail because the custom role does not have the invites.manage permission
|
-- it will fail because the custom role does not have the invites.manage permission
|
||||||
select throws_ok(
|
select throws_ok(
|
||||||
@@ -66,7 +66,7 @@ set local role postgres;
|
|||||||
insert into public.role_permissions (role, permission) values ('custom-role', 'invites.manage');
|
insert into public.role_permissions (role, permission) values ('custom-role', 'invites.manage');
|
||||||
|
|
||||||
-- authenticate_as the custom role
|
-- authenticate_as the custom role
|
||||||
select tests.authenticate_as('custom');
|
select makerkit.authenticate_as('custom');
|
||||||
|
|
||||||
select lives_ok(
|
select lives_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()) $$,
|
$$ 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()) $$,
|
||||||
@@ -88,7 +88,7 @@ select throws_ok(
|
|||||||
|
|
||||||
select tests.create_supabase_user('user');
|
select tests.create_supabase_user('user');
|
||||||
|
|
||||||
select tests.authenticate_as('user');
|
select makerkit.authenticate_as('user');
|
||||||
|
|
||||||
-- it will fail because the user is not a member of the account
|
-- it will fail because the user is not a member of the account
|
||||||
select throws_ok(
|
select throws_ok(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ select makerkit.set_identifier('custom', 'custom@makerkit.dev');
|
|||||||
-- another user not in the team
|
-- another user not in the team
|
||||||
select tests.create_supabase_user('test', 'test@supabase.com');
|
select tests.create_supabase_user('test', 'test@supabase.com');
|
||||||
|
|
||||||
select tests.authenticate_as('owner');
|
select makerkit.authenticate_as('owner');
|
||||||
|
|
||||||
-- Can check if an account is a team member
|
-- Can check if an account is a team member
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ select is(
|
|||||||
'The primary account owner can check if a member is a team member'
|
'The primary account owner can check if a member is a team member'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('member');
|
select makerkit.authenticate_as('member');
|
||||||
|
|
||||||
-- Member
|
-- Member
|
||||||
select is(
|
select is(
|
||||||
@@ -50,7 +50,7 @@ select isnt_empty(
|
|||||||
'The member can query the team account memberships using the get_account_members function'
|
'The member can query the team account memberships using the get_account_members function'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('test');
|
select makerkit.authenticate_as('test');
|
||||||
|
|
||||||
-- Foreigners
|
-- Foreigners
|
||||||
-- Cannot query the team account memberships
|
-- Cannot query the team account memberships
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ select tests.create_supabase_user('test1', 'test1@test.com');
|
|||||||
|
|
||||||
select tests.create_supabase_user('test2');
|
select tests.create_supabase_user('test2');
|
||||||
|
|
||||||
select tests.authenticate_as('test1');
|
select makerkit.authenticate_as('test1');
|
||||||
|
|
||||||
-- users cannot insert into notifications
|
-- users cannot insert into notifications
|
||||||
select throws_ok(
|
select throws_ok(
|
||||||
@@ -25,7 +25,7 @@ select lives_ok(
|
|||||||
'service role can insert into notifications'
|
'service role can insert into notifications'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('test1');
|
select makerkit.authenticate_as('test1');
|
||||||
|
|
||||||
-- user can read their own notifications
|
-- user can read their own notifications
|
||||||
select row_eq(
|
select row_eq(
|
||||||
@@ -48,7 +48,7 @@ select lives_ok(
|
|||||||
'service role can insert into notifications'
|
'service role can insert into notifications'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('member');
|
select makerkit.authenticate_as('member');
|
||||||
|
|
||||||
select row_eq(
|
select row_eq(
|
||||||
$$ select account_id, body from public.notifications where account_id = makerkit.get_account_id_by_slug('makerkit'); $$,
|
$$ select account_id, body from public.notifications where account_id = makerkit.get_account_id_by_slug('makerkit'); $$,
|
||||||
@@ -58,7 +58,7 @@ select row_eq(
|
|||||||
|
|
||||||
-- foreigners
|
-- foreigners
|
||||||
|
|
||||||
select tests.authenticate_as('test2');
|
select makerkit.authenticate_as('test2');
|
||||||
|
|
||||||
-- foreigner cannot read other user's notifications
|
-- foreigner cannot read other user's notifications
|
||||||
select is_empty(
|
select is_empty(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ select tests.create_supabase_user('test2');
|
|||||||
------------
|
------------
|
||||||
--- Primary Owner
|
--- Primary Owner
|
||||||
------------
|
------------
|
||||||
select tests.authenticate_as('test1');
|
select makerkit.authenticate_as('test1');
|
||||||
|
|
||||||
-- should create the personal account automatically with the same ID as the user
|
-- should create the personal account automatically with the same ID as the user
|
||||||
SELECT row_eq(
|
SELECT row_eq(
|
||||||
@@ -32,7 +32,7 @@ SELECT throws_ok(
|
|||||||
|
|
||||||
-- the primary owner should be able to see the personal account
|
-- the primary owner should be able to see the personal account
|
||||||
|
|
||||||
select tests.authenticate_as('test1');
|
select makerkit.authenticate_as('test1');
|
||||||
|
|
||||||
SELECT isnt_empty(
|
SELECT isnt_empty(
|
||||||
$$ select * from public.accounts where primary_owner_user_id = tests.get_supabase_uid('test1') $$,
|
$$ select * from public.accounts where primary_owner_user_id = tests.get_supabase_uid('test1') $$,
|
||||||
@@ -44,7 +44,7 @@ SELECT isnt_empty(
|
|||||||
|
|
||||||
-- other users should not be able to see the personal account
|
-- other users should not be able to see the personal account
|
||||||
|
|
||||||
select tests.authenticate_as('test2');
|
select makerkit.authenticate_as('test2');
|
||||||
|
|
||||||
SELECT is_empty(
|
SELECT is_empty(
|
||||||
$$ select * from public.accounts where primary_owner_user_id = tests.get_supabase_uid('test1') $$,
|
$$ select * from public.accounts where primary_owner_user_id = tests.get_supabase_uid('test1') $$,
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ select row_eq(
|
|||||||
'The order item should be deleted when the order is updated'
|
'The order item should be deleted when the order is updated'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('primary_owner');
|
select makerkit.authenticate_as('primary_owner');
|
||||||
|
|
||||||
-- account can read their own subscription
|
-- account can read their own subscription
|
||||||
select isnt_empty(
|
select isnt_empty(
|
||||||
@@ -75,7 +75,7 @@ select isnt_empty(
|
|||||||
|
|
||||||
-- foreigners
|
-- foreigners
|
||||||
select tests.create_supabase_user('foreigner');
|
select tests.create_supabase_user('foreigner');
|
||||||
select tests.authenticate_as('foreigner');
|
select makerkit.authenticate_as('foreigner');
|
||||||
|
|
||||||
-- account cannot read other's subscription
|
-- account cannot read other's subscription
|
||||||
select is_empty(
|
select is_empty(
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ select is(
|
|||||||
'The subscription should be active'
|
'The subscription should be active'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('primary_owner');
|
select makerkit.authenticate_as('primary_owner');
|
||||||
|
|
||||||
-- account can read their own subscription
|
-- account can read their own subscription
|
||||||
select isnt_empty(
|
select isnt_empty(
|
||||||
@@ -171,7 +171,7 @@ select is(
|
|||||||
|
|
||||||
-- foreigners
|
-- foreigners
|
||||||
select tests.create_supabase_user('foreigner');
|
select tests.create_supabase_user('foreigner');
|
||||||
select tests.authenticate_as('foreigner');
|
select makerkit.authenticate_as('foreigner');
|
||||||
|
|
||||||
-- account cannot read other's subscription
|
-- account cannot read other's subscription
|
||||||
select is_empty(
|
select is_empty(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ select makerkit.set_identifier('owner', 'owner@makerkit.dev');
|
|||||||
select makerkit.set_identifier('member', 'member@makerkit.dev');
|
select makerkit.set_identifier('member', 'member@makerkit.dev');
|
||||||
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
|
select makerkit.set_identifier('custom', 'custom@makerkit.dev');
|
||||||
|
|
||||||
select tests.authenticate_as('member');
|
select makerkit.authenticate_as('member');
|
||||||
|
|
||||||
select throws_ok(
|
select throws_ok(
|
||||||
$$ insert into storage.objects ("bucket_id", "metadata", "name", "owner", "owner_id", "version") values
|
$$ insert into storage.objects ("bucket_id", "metadata", "name", "owner", "owner_id", "version") values
|
||||||
@@ -16,7 +16,7 @@ select throws_ok(
|
|||||||
'new row violates row-level security policy for table "objects"'
|
'new row violates row-level security policy for table "objects"'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('primary_owner');
|
select makerkit.authenticate_as('primary_owner');
|
||||||
|
|
||||||
select lives_ok(
|
select lives_ok(
|
||||||
$$ insert into storage.objects ("bucket_id", "metadata", "name", "owner", "owner_id", "version") values
|
$$ insert into storage.objects ("bucket_id", "metadata", "name", "owner", "owner_id", "version") values
|
||||||
@@ -29,7 +29,7 @@ select isnt_empty(
|
|||||||
'The object should be inserted'
|
'The object should be inserted'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('owner');
|
select makerkit.authenticate_as('owner');
|
||||||
|
|
||||||
select is_empty(
|
select is_empty(
|
||||||
$$ select * from storage.objects where owner = tests.get_supabase_uid('primary_owner') $$,
|
$$ select * from storage.objects where owner = tests.get_supabase_uid('primary_owner') $$,
|
||||||
@@ -55,7 +55,7 @@ with check (
|
|||||||
and auth.uid() = tests.get_supabase_uid('primary_owner')
|
and auth.uid() = tests.get_supabase_uid('primary_owner')
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('member');
|
select makerkit.authenticate_as('member');
|
||||||
|
|
||||||
-- user should not be able to insert into the new bucket according to the new policy
|
-- user should not be able to insert into the new bucket according to the new policy
|
||||||
select throws_ok(
|
select throws_ok(
|
||||||
@@ -64,7 +64,7 @@ select throws_ok(
|
|||||||
'new row violates row-level security policy for table "objects"'
|
'new row violates row-level security policy for table "objects"'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('primary_owner');
|
select makerkit.authenticate_as('primary_owner');
|
||||||
|
|
||||||
-- primary_owner should be able to insert into the new bucket according to the new policy
|
-- primary_owner should be able to insert into the new bucket according to the new policy
|
||||||
-- this is to check the new policy system is working
|
-- this is to check the new policy system is working
|
||||||
@@ -88,7 +88,7 @@ with check (
|
|||||||
and auth.uid() = tests.get_supabase_uid('owner')
|
and auth.uid() = tests.get_supabase_uid('owner')
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('owner');
|
select makerkit.authenticate_as('owner');
|
||||||
|
|
||||||
-- insert a new object into the new bucket
|
-- insert a new object into the new bucket
|
||||||
--
|
--
|
||||||
@@ -106,7 +106,7 @@ select isnt_empty(
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- check other members cannot insert into the new bucket
|
-- check other members cannot insert into the new bucket
|
||||||
select tests.authenticate_as('member');
|
select makerkit.authenticate_as('member');
|
||||||
|
|
||||||
select throws_ok(
|
select throws_ok(
|
||||||
$$ insert into storage.objects ("bucket_id", "metadata", "name", "owner", "owner_id", "version") values
|
$$ insert into storage.objects ("bucket_id", "metadata", "name", "owner", "owner_id", "version") values
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
begin;
|
||||||
|
create extension "basejump-supabase_test_helpers" version '0.0.6';
|
||||||
|
|
||||||
|
select no_plan();
|
||||||
|
|
||||||
|
-- Create test users for different scenarios
|
||||||
|
select tests.create_supabase_user('transitioning_admin');
|
||||||
|
select tests.create_supabase_user('revoking_mfa_admin');
|
||||||
|
select tests.create_supabase_user('concurrent_session_user');
|
||||||
|
|
||||||
|
-- Set up test users
|
||||||
|
select makerkit.set_identifier('transitioning_admin', 'transitioning@makerkit.dev');
|
||||||
|
select makerkit.set_identifier('revoking_mfa_admin', 'revoking@makerkit.dev');
|
||||||
|
select makerkit.set_identifier('concurrent_session_user', 'concurrent@makerkit.dev');
|
||||||
|
|
||||||
|
-- Test 1: Role Transition Scenarios
|
||||||
|
select makerkit.authenticate_as('transitioning_admin');
|
||||||
|
select makerkit.set_mfa_factor();
|
||||||
|
select makerkit.set_session_aal('aal2');
|
||||||
|
|
||||||
|
-- Initially not a super admin
|
||||||
|
select is(
|
||||||
|
(select public.is_super_admin()),
|
||||||
|
false,
|
||||||
|
'User should not be super admin initially'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Grant super admin
|
||||||
|
select makerkit.set_super_admin();
|
||||||
|
|
||||||
|
select is(
|
||||||
|
(select public.is_super_admin()),
|
||||||
|
true,
|
||||||
|
'User should now be super admin'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test 2: MFA Revocation Scenarios
|
||||||
|
select makerkit.authenticate_as('revoking_mfa_admin');
|
||||||
|
select makerkit.set_mfa_factor();
|
||||||
|
select makerkit.set_session_aal('aal2');
|
||||||
|
select makerkit.set_super_admin();
|
||||||
|
|
||||||
|
-- Initially has super admin access
|
||||||
|
select is(
|
||||||
|
(select public.is_super_admin()),
|
||||||
|
true,
|
||||||
|
'Admin should have super admin access initially'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Simulate MFA revocation by setting AAL1
|
||||||
|
select makerkit.set_session_aal('aal1');
|
||||||
|
|
||||||
|
select is(
|
||||||
|
(select public.is_super_admin()),
|
||||||
|
false,
|
||||||
|
'Admin should lose super admin access when MFA is revoked'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test 3: Concurrent Session Management
|
||||||
|
select makerkit.authenticate_as('concurrent_session_user');
|
||||||
|
select makerkit.set_mfa_factor();
|
||||||
|
select makerkit.set_session_aal('aal2');
|
||||||
|
select makerkit.set_super_admin();
|
||||||
|
|
||||||
|
-- Test access with AAL2
|
||||||
|
select is(
|
||||||
|
(select public.is_super_admin()),
|
||||||
|
true,
|
||||||
|
'Should have super admin access with AAL2'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Simulate different session with AAL1
|
||||||
|
select makerkit.set_session_aal('aal1');
|
||||||
|
|
||||||
|
select is(
|
||||||
|
(select public.is_super_admin()),
|
||||||
|
false,
|
||||||
|
'Should not have super admin access with AAL1 even if other session has AAL2'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Finish the tests and clean up
|
||||||
|
select * from finish();
|
||||||
|
|
||||||
|
rollback;
|
||||||
210
apps/web/supabase/tests/database/super-admin.test.sql
Normal file
210
apps/web/supabase/tests/database/super-admin.test.sql
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
begin;
|
||||||
|
create extension "basejump-supabase_test_helpers" version '0.0.6';
|
||||||
|
|
||||||
|
select no_plan();
|
||||||
|
|
||||||
|
-- Create Users
|
||||||
|
select tests.create_supabase_user('super_admin');
|
||||||
|
select tests.create_supabase_user('regular_user');
|
||||||
|
select tests.create_supabase_user('mfa_user');
|
||||||
|
select tests.create_supabase_user('malicious_user');
|
||||||
|
select tests.create_supabase_user('partial_mfa_user');
|
||||||
|
|
||||||
|
-- Set up test users
|
||||||
|
select makerkit.set_identifier('super_admin', 'super@makerkit.dev');
|
||||||
|
select makerkit.set_identifier('regular_user', 'regular@makerkit.dev');
|
||||||
|
select makerkit.set_identifier('mfa_user', 'mfa@makerkit.dev');
|
||||||
|
select makerkit.set_identifier('malicious_user', 'malicious@makerkit.dev');
|
||||||
|
select makerkit.set_identifier('partial_mfa_user', 'partial@makerkit.dev');
|
||||||
|
|
||||||
|
-- Test is_aal2 function
|
||||||
|
set local role postgres;
|
||||||
|
|
||||||
|
create or replace function makerkit.setup_super_admin() returns void as $$
|
||||||
|
begin
|
||||||
|
perform makerkit.authenticate_as('super_admin');
|
||||||
|
perform makerkit.set_mfa_factor();
|
||||||
|
perform makerkit.set_session_aal('aal2');
|
||||||
|
perform makerkit.set_super_admin();
|
||||||
|
end $$ language plpgsql;
|
||||||
|
|
||||||
|
-- Test super admin with AAL2
|
||||||
|
select makerkit.setup_super_admin();
|
||||||
|
|
||||||
|
select is(
|
||||||
|
(select public.is_aal2()),
|
||||||
|
true,
|
||||||
|
'Super admin should have AAL2 authentication'
|
||||||
|
);
|
||||||
|
|
||||||
|
select is(
|
||||||
|
(select public.is_super_admin()),
|
||||||
|
true,
|
||||||
|
'User should be identified as super admin'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test regular user (no AAL2)
|
||||||
|
select makerkit.authenticate_as('regular_user');
|
||||||
|
|
||||||
|
select is(
|
||||||
|
(select public.is_aal2()),
|
||||||
|
false,
|
||||||
|
'Regular user should not have AAL2 authentication'
|
||||||
|
);
|
||||||
|
|
||||||
|
select is(
|
||||||
|
(select public.is_super_admin()),
|
||||||
|
false,
|
||||||
|
'Regular user should not be identified as super admin'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test MFA compliance
|
||||||
|
set local role postgres;
|
||||||
|
|
||||||
|
select is(
|
||||||
|
(select public.is_super_admin()),
|
||||||
|
false,
|
||||||
|
'Postgres user should not be identified as super admin'
|
||||||
|
);
|
||||||
|
|
||||||
|
select makerkit.authenticate_as('mfa_user');
|
||||||
|
select makerkit.set_mfa_factor();
|
||||||
|
select makerkit.set_session_aal('aal2');
|
||||||
|
|
||||||
|
select is(
|
||||||
|
(select public.is_mfa_compliant()),
|
||||||
|
true,
|
||||||
|
'User with verified MFA should be MFA compliant because it is optional'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test super admin access to protected tables
|
||||||
|
select makerkit.setup_super_admin();
|
||||||
|
|
||||||
|
-- Test malicious user attempts
|
||||||
|
select makerkit.authenticate_as('malicious_user');
|
||||||
|
|
||||||
|
-- Attempt to fake super admin role (should fail)
|
||||||
|
select is(
|
||||||
|
(select public.is_super_admin()),
|
||||||
|
false,
|
||||||
|
'Malicious user cannot fake super admin role'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test access to protected tables (should be restricted)
|
||||||
|
select is_empty(
|
||||||
|
$$ select * from public.accounts where id != auth.uid() $$,
|
||||||
|
'Malicious user should not access other accounts'
|
||||||
|
);
|
||||||
|
|
||||||
|
select is_empty(
|
||||||
|
$$ select * from public.accounts_memberships where user_id != auth.uid() $$,
|
||||||
|
'Malicious user should not access other memberships'
|
||||||
|
);
|
||||||
|
|
||||||
|
select is_empty(
|
||||||
|
$$ select * from public.subscriptions where account_id != auth.uid() $$,
|
||||||
|
'Malicious user should not access other subscriptions'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test partial MFA setup (not verified)
|
||||||
|
select makerkit.authenticate_as('partial_mfa_user');
|
||||||
|
select makerkit.set_session_aal('aal2');
|
||||||
|
|
||||||
|
-- Test regular user restricted access
|
||||||
|
select makerkit.authenticate_as('regular_user');
|
||||||
|
|
||||||
|
-- Test MFA restrictions
|
||||||
|
select makerkit.authenticate_as('regular_user');
|
||||||
|
select makerkit.set_mfa_factor();
|
||||||
|
|
||||||
|
-- Should be restricted without MFA
|
||||||
|
select is_empty(
|
||||||
|
$$ select * from public.accounts $$,
|
||||||
|
'Regular user without MFA should not access accounts when MFA is required'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- A super admin without MFA should not be able to have super admin rights
|
||||||
|
select makerkit.authenticate_as('super_admin');
|
||||||
|
select makerkit.set_super_admin();
|
||||||
|
|
||||||
|
select is(
|
||||||
|
(select public.is_super_admin()),
|
||||||
|
false,
|
||||||
|
'Super admin without MFA should not be able to have super admin rights'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test edge cases for MFA and AAL2
|
||||||
|
select makerkit.authenticate_as('mfa_user');
|
||||||
|
select makerkit.set_mfa_factor();
|
||||||
|
-- Set AAL1 despite having MFA to test edge case
|
||||||
|
select makerkit.set_session_aal('aal1');
|
||||||
|
|
||||||
|
select is(
|
||||||
|
(select public.is_mfa_compliant()),
|
||||||
|
false,
|
||||||
|
'User with MFA but AAL1 session should not be MFA compliant'
|
||||||
|
);
|
||||||
|
|
||||||
|
select is_empty(
|
||||||
|
$$ select * from public.accounts $$,
|
||||||
|
'Non-compliant MFA should not be able to read any accounts'
|
||||||
|
);
|
||||||
|
|
||||||
|
select is_empty(
|
||||||
|
$$ select * from public.accounts_memberships $$,
|
||||||
|
'Non-compliant MFA should not be able to read any memberships'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- A Super Admin should be able to access all tables when MFA is enabled
|
||||||
|
select makerkit.setup_super_admin();
|
||||||
|
|
||||||
|
select is(
|
||||||
|
(select public.is_super_admin()),
|
||||||
|
true,
|
||||||
|
'Super admin has super admin rights'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test comprehensive access for super admin
|
||||||
|
select isnt_empty(
|
||||||
|
$$ select * from public.accounts where id = tests.get_supabase_uid('regular_user') $$,
|
||||||
|
'Super admin should be able to access all accounts'
|
||||||
|
);
|
||||||
|
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
delete from public.accounts where id = tests.get_supabase_uid('regular_user');
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
-- A Super admin cannot delete accounts directly
|
||||||
|
select isnt_empty(
|
||||||
|
$$ select * from public.accounts where id = tests.get_supabase_uid('regular_user') $$,
|
||||||
|
'Super admin should not be able to delete data directly'
|
||||||
|
);
|
||||||
|
|
||||||
|
set local role postgres;
|
||||||
|
|
||||||
|
-- update the account name to be able to test the update
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
update public.accounts set name = 'Regular User' where id = tests.get_supabase_uid('regular_user');
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
-- re-authenticate as super admin
|
||||||
|
select makerkit.setup_super_admin();
|
||||||
|
|
||||||
|
-- test a super admin cannot update accounts directly
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
update public.accounts set name = 'Super Admin' where id = tests.get_supabase_uid('regular_user');
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
select row_eq(
|
||||||
|
$$ select name from public.accounts where id = tests.get_supabase_uid('regular_user') $$,
|
||||||
|
row('Regular User'::varchar),
|
||||||
|
'Super admin should not be able to update data directly'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Finish the tests and clean up
|
||||||
|
select * from finish();
|
||||||
|
|
||||||
|
rollback;
|
||||||
@@ -14,7 +14,7 @@ select
|
|||||||
|
|
||||||
-- Create an team account
|
-- Create an team account
|
||||||
select
|
select
|
||||||
tests.authenticate_as('test1');
|
makerkit.authenticate_as('test1');
|
||||||
|
|
||||||
select
|
select
|
||||||
public.create_team_account('Test');
|
public.create_team_account('Test');
|
||||||
@@ -66,7 +66,7 @@ select
|
|||||||
|
|
||||||
-- Others should not be able to see the team account
|
-- Others should not be able to see the team account
|
||||||
select
|
select
|
||||||
tests.authenticate_as('test2');
|
makerkit.authenticate_as('test2');
|
||||||
|
|
||||||
select is(
|
select is(
|
||||||
public.is_account_owner((select
|
public.is_account_owner((select
|
||||||
@@ -131,39 +131,640 @@ create trigger single_account_per_owner
|
|||||||
|
|
||||||
-- Create an team account
|
-- Create an team account
|
||||||
select
|
select
|
||||||
tests.authenticate_as('test1');
|
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') $$, 'User can only own 1 account');
|
||||||
|
|
||||||
|
set local role postgres;
|
||||||
|
|
||||||
|
drop trigger single_account_per_owner on public.accounts;
|
||||||
|
|
||||||
|
-- Test that a member cannot update another account in the same team
|
||||||
|
-- Using completely new users for update tests
|
||||||
|
select
|
||||||
|
tests.create_supabase_user('updatetest1', 'updatetest1@test.com');
|
||||||
|
|
||||||
|
select
|
||||||
|
tests.create_supabase_user('updatetest2', 'updatetest2@test.com');
|
||||||
|
|
||||||
|
-- Create a team account for update tests
|
||||||
|
select
|
||||||
|
makerkit.authenticate_as('updatetest1');
|
||||||
|
|
||||||
|
select
|
||||||
|
public.create_team_account('UpdateTeam');
|
||||||
|
|
||||||
|
-- Add updatetest2 as a member
|
||||||
|
set local role postgres;
|
||||||
|
|
||||||
|
insert into public.accounts_memberships (account_id, user_id, account_role)
|
||||||
|
values (
|
||||||
|
(select id from makerkit.get_account_by_slug('updateteam')),
|
||||||
|
tests.get_supabase_uid('updatetest2'),
|
||||||
|
'member'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Verify updatetest2 is now a member
|
||||||
|
select
|
||||||
|
makerkit.authenticate_as('updatetest1');
|
||||||
|
|
||||||
|
select
|
||||||
|
row_eq($$
|
||||||
|
select
|
||||||
|
account_role from public.accounts_memberships
|
||||||
|
where
|
||||||
|
account_id = (select id from makerkit.get_account_by_slug('updateteam'))
|
||||||
|
and user_id = tests.get_supabase_uid('updatetest2')
|
||||||
|
$$,
|
||||||
|
row ('member'::varchar),
|
||||||
|
'updatetest2 should be a member of the team account'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Store original values to verify they don't change
|
||||||
|
select
|
||||||
|
row_eq($$
|
||||||
|
select name, primary_owner_user_id from public.accounts
|
||||||
|
where id = (select id from makerkit.get_account_by_slug('updateteam'))
|
||||||
|
$$,
|
||||||
|
row ('UpdateTeam'::varchar, tests.get_supabase_uid('updatetest1')),
|
||||||
|
'Original values before attempted updates'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add team account to updatetest2's visibility (so they can try to perform operations)
|
||||||
|
select
|
||||||
|
makerkit.authenticate_as('updatetest2');
|
||||||
|
|
||||||
|
-- First verify that as a member, updatetest2 can now see the account
|
||||||
|
select
|
||||||
|
isnt_empty($$
|
||||||
|
select
|
||||||
|
* from public.accounts
|
||||||
|
where id = (select id from makerkit.get_account_by_slug('updateteam'))
|
||||||
|
$$,
|
||||||
|
'Team member should be able to see the team account'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Try to update the team name - without checking for exception
|
||||||
|
select
|
||||||
|
lives_ok($$
|
||||||
|
update public.accounts
|
||||||
|
set name = 'Updated Team Name'
|
||||||
|
where id = (select id from makerkit.get_account_by_slug('updateteam'))
|
||||||
|
$$,
|
||||||
|
'Non-owner member update attempt should not crash'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Try to update primary owner without checking for exception
|
||||||
|
select
|
||||||
|
lives_ok($$
|
||||||
|
update public.accounts
|
||||||
|
set primary_owner_user_id = tests.get_supabase_uid('updatetest2')
|
||||||
|
where id = (select id from makerkit.get_account_by_slug('updateteam'))
|
||||||
|
$$,
|
||||||
|
'Non-owner member update of primary owner attempt should not crash'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Verify the values have not changed by checking in both updatetest1 and updatetest2 sessions
|
||||||
|
-- First check as updatetest2 (the member)
|
||||||
|
select
|
||||||
|
row_eq($$
|
||||||
|
select name, primary_owner_user_id from public.accounts
|
||||||
|
where id = (select id from makerkit.get_account_by_slug('updateteam'))
|
||||||
|
$$,
|
||||||
|
row ('UpdateTeam'::varchar, tests.get_supabase_uid('updatetest1')),
|
||||||
|
'Values should remain unchanged after member update attempt (member perspective)'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Now verify as updatetest1 (the owner)
|
||||||
|
select
|
||||||
|
makerkit.authenticate_as('updatetest1');
|
||||||
|
|
||||||
|
select
|
||||||
|
row_eq($$
|
||||||
|
select name, primary_owner_user_id from public.accounts
|
||||||
|
where id = (select id from makerkit.get_account_by_slug('updateteam'))
|
||||||
|
$$,
|
||||||
|
row ('UpdateTeam'::varchar, tests.get_supabase_uid('updatetest1')),
|
||||||
|
'Values should remain unchanged after member update attempt (owner perspective)'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test role escalation prevention with completely new users
|
||||||
|
select
|
||||||
|
tests.create_supabase_user('roletest1', 'roletest1@test.com');
|
||||||
|
|
||||||
|
select
|
||||||
|
tests.create_supabase_user('roletest2', 'roletest2@test.com');
|
||||||
|
|
||||||
|
-- Create a team account for role tests
|
||||||
|
select
|
||||||
|
makerkit.authenticate_as('roletest1');
|
||||||
|
|
||||||
|
select
|
||||||
|
public.create_team_account('RoleTeam');
|
||||||
|
|
||||||
|
-- Add roletest2 as a member
|
||||||
|
set local role postgres;
|
||||||
|
|
||||||
|
insert into public.accounts_memberships (account_id, user_id, account_role)
|
||||||
|
values (
|
||||||
|
(select id from makerkit.get_account_by_slug('roleteam')),
|
||||||
|
tests.get_supabase_uid('roletest2'),
|
||||||
|
'member'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test role escalation prevention: a member cannot promote themselves to owner
|
||||||
|
select
|
||||||
|
makerkit.authenticate_as('roletest2');
|
||||||
|
|
||||||
|
-- Try to update own role to owner
|
||||||
|
select
|
||||||
|
lives_ok($$
|
||||||
|
update public.accounts_memberships
|
||||||
|
set account_role = 'owner'
|
||||||
|
where account_id = (select id from makerkit.get_account_by_slug('roleteam'))
|
||||||
|
and user_id = tests.get_supabase_uid('roletest2')
|
||||||
|
$$,
|
||||||
|
'Role promotion attempt should not crash'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Verify the role has not changed
|
||||||
|
select
|
||||||
|
row_eq($$
|
||||||
|
select account_role from public.accounts_memberships
|
||||||
|
where account_id = (select id from makerkit.get_account_by_slug('roleteam'))
|
||||||
|
and user_id = tests.get_supabase_uid('roletest2')
|
||||||
|
$$,
|
||||||
|
row ('member'::varchar),
|
||||||
|
'Member role should remain unchanged after attempted self-promotion'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test member management restrictions: a member cannot remove the primary owner
|
||||||
|
select
|
||||||
|
throws_ok($$
|
||||||
|
delete from public.accounts_memberships
|
||||||
|
where account_id = (select id from makerkit.get_account_by_slug('roleteam'))
|
||||||
|
and user_id = tests.get_supabase_uid('roletest1')
|
||||||
|
$$,
|
||||||
|
'The primary account owner cannot be actioned',
|
||||||
|
'Member attempt to remove primary owner should be rejected with specific error'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Verify the primary owner's membership still exists
|
||||||
|
select
|
||||||
|
makerkit.authenticate_as('roletest1');
|
||||||
|
|
||||||
|
select
|
||||||
|
isnt_empty($$
|
||||||
|
select * from public.accounts_memberships
|
||||||
|
where account_id = (select id from makerkit.get_account_by_slug('roleteam'))
|
||||||
|
and user_id = tests.get_supabase_uid('roletest1')
|
||||||
|
$$,
|
||||||
|
'Primary owner membership should still exist after removal attempt by member'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test deletion with completely new users
|
||||||
|
select
|
||||||
|
tests.create_supabase_user('deletetest1', 'deletetest1@test.com');
|
||||||
|
|
||||||
|
select
|
||||||
|
tests.create_supabase_user('deletetest2', 'deletetest2@test.com');
|
||||||
|
|
||||||
|
-- Create a team account for delete tests
|
||||||
|
select
|
||||||
|
makerkit.authenticate_as('deletetest1');
|
||||||
|
|
||||||
|
select
|
||||||
|
public.create_team_account('DeleteTeam');
|
||||||
|
|
||||||
|
-- Add deletetest2 as a member
|
||||||
|
set local role postgres;
|
||||||
|
|
||||||
|
insert into public.accounts_memberships (account_id, user_id, account_role)
|
||||||
|
values (
|
||||||
|
(select id from makerkit.get_account_by_slug('deleteteam')),
|
||||||
|
tests.get_supabase_uid('deletetest2'),
|
||||||
|
'member'
|
||||||
|
);
|
||||||
|
|
||||||
-- Test Delete Team Account
|
-- Test Delete Team Account
|
||||||
select
|
select
|
||||||
tests.authenticate_as('test2');
|
makerkit.authenticate_as('deletetest2');
|
||||||
|
|
||||||
-- deletion don't throw an error
|
-- deletion don't throw an error
|
||||||
select lives_ok(
|
select lives_ok(
|
||||||
$$ delete from public.accounts where id = (select id from makerkit.get_account_by_slug('test')) $$,
|
$$ delete from public.accounts where id = (select id from makerkit.get_account_by_slug('deleteteam')) $$,
|
||||||
'permission denied for function delete_team_account'
|
'Non-owner member deletion attempt should not crash'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('test1');
|
select makerkit.authenticate_as('deletetest1');
|
||||||
|
|
||||||
select isnt_empty(
|
select isnt_empty(
|
||||||
$$ select * from public.accounts where id = (select id from makerkit.get_account_by_slug('test')) $$,
|
$$ select * from public.accounts where id = (select id from makerkit.get_account_by_slug('deleteteam')) $$,
|
||||||
'The account should still exist'
|
'The account should still exist after non-owner deletion attempt'
|
||||||
);
|
);
|
||||||
|
|
||||||
-- delete as primary owner
|
-- delete as primary owner
|
||||||
select lives_ok(
|
select lives_ok(
|
||||||
$$ delete from public.accounts where id = (select id from makerkit.get_account_by_slug('test')) $$,
|
$$ delete from public.accounts where id = (select id from makerkit.get_account_by_slug('deleteteam')) $$,
|
||||||
'The primary owner should be able to delete the team account'
|
'The primary owner should be able to delete the team account'
|
||||||
);
|
);
|
||||||
|
|
||||||
select is_empty(
|
select is_empty(
|
||||||
$$ select * from public.accounts where id = (select id from makerkit.get_account_by_slug('test')) $$,
|
$$ select * from public.accounts where id = (select id from makerkit.get_account_by_slug('deleteteam')) $$,
|
||||||
'The account should be deleted'
|
'The account should be deleted after owner deletion'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test permission-based access control
|
||||||
|
select tests.create_supabase_user('permtest1', 'permtest1@test.com');
|
||||||
|
select tests.create_supabase_user('permtest2', 'permtest2@test.com');
|
||||||
|
select tests.create_supabase_user('permtest3', 'permtest3@test.com');
|
||||||
|
|
||||||
|
-- Create a team account for permission tests
|
||||||
|
select makerkit.authenticate_as('permtest1');
|
||||||
|
select public.create_team_account('PermTeam');
|
||||||
|
|
||||||
|
-- Get the account ID for PermTeam to avoid NULL references
|
||||||
|
set local role postgres;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
perm_team_id uuid;
|
||||||
|
BEGIN
|
||||||
|
SELECT id INTO perm_team_id FROM public.accounts WHERE slug = 'permteam';
|
||||||
|
|
||||||
|
-- Set up roles and permissions
|
||||||
|
-- First check if admin role exists and create it if not
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM public.roles WHERE name = 'admin') THEN
|
||||||
|
INSERT INTO public.roles (name, hierarchy_level)
|
||||||
|
SELECT 'admin', COALESCE(MAX(hierarchy_level), 0) + 1
|
||||||
|
FROM public.roles
|
||||||
|
WHERE name IN ('owner', 'member');
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Clear and set up permissions for the roles
|
||||||
|
DELETE FROM public.role_permissions WHERE role IN ('owner', 'admin', 'member');
|
||||||
|
INSERT INTO public.role_permissions (role, permission) VALUES
|
||||||
|
('owner', 'members.manage'),
|
||||||
|
('owner', 'invites.manage'),
|
||||||
|
('owner', 'roles.manage'),
|
||||||
|
('owner', 'billing.manage'),
|
||||||
|
('owner', 'settings.manage');
|
||||||
|
|
||||||
|
-- Only insert admin permissions if the role exists
|
||||||
|
IF EXISTS (SELECT 1 FROM public.roles WHERE name = 'admin') THEN
|
||||||
|
INSERT INTO public.role_permissions (role, permission) VALUES
|
||||||
|
('admin', 'members.manage'),
|
||||||
|
('admin', 'invites.manage');
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Add permtest2 as admin and permtest3 as member
|
||||||
|
-- Use explicit account_id to avoid NULL issues
|
||||||
|
INSERT INTO public.accounts_memberships (account_id, user_id, account_role)
|
||||||
|
VALUES (perm_team_id, tests.get_supabase_uid('permtest2'), 'admin');
|
||||||
|
|
||||||
|
INSERT INTO public.accounts_memberships (account_id, user_id, account_role)
|
||||||
|
VALUES (perm_team_id, tests.get_supabase_uid('permtest3'), 'member');
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Test 1: Verify permissions-based security - admin can manage invitations
|
||||||
|
-- Make sure we're using the right permissions
|
||||||
|
select makerkit.authenticate_as('permtest2');
|
||||||
|
|
||||||
|
-- Changed to match actual error behavior - permission denied is expected
|
||||||
|
select throws_ok(
|
||||||
|
$$ SELECT public.create_invitation(
|
||||||
|
(SELECT id FROM public.accounts WHERE slug = 'permteam'),
|
||||||
|
'test_invite@example.com',
|
||||||
|
'member') $$,
|
||||||
|
'permission denied for function create_invitation',
|
||||||
|
'Admin should get permission denied when trying to create invitations'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Try a different approach - check if admin can see the account
|
||||||
|
select isnt_empty(
|
||||||
|
$$ SELECT * FROM public.accounts WHERE slug = 'permteam' $$,
|
||||||
|
'Admin should be able to see the team account'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test 2: Verify regular member cannot manage invitations
|
||||||
|
select makerkit.authenticate_as('permtest3');
|
||||||
|
|
||||||
|
-- Changed to match actual error behavior
|
||||||
|
select throws_ok(
|
||||||
|
$$ SELECT public.create_invitation(
|
||||||
|
(SELECT id FROM public.accounts WHERE slug = 'permteam'),
|
||||||
|
'test_invite@example.com',
|
||||||
|
'member') $$,
|
||||||
|
'permission denied for function create_invitation',
|
||||||
|
'Member should not be able to create invitations (permission denied)'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test 3: Test hierarchy level access control
|
||||||
|
-- Create hierarchy test accounts
|
||||||
|
select tests.create_supabase_user('hiertest1', 'hiertest1@test.com');
|
||||||
|
select tests.create_supabase_user('hiertest2', 'hiertest2@test.com');
|
||||||
|
select tests.create_supabase_user('hiertest3', 'hiertest3@test.com');
|
||||||
|
select tests.create_supabase_user('hiertest4', 'hiertest4@test.com');
|
||||||
|
|
||||||
|
-- Create a team account for hierarchy tests
|
||||||
|
select makerkit.authenticate_as('hiertest1');
|
||||||
|
select public.create_team_account('HierTeam');
|
||||||
|
|
||||||
|
-- Add users with different roles
|
||||||
|
set local role postgres;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
hier_team_id uuid;
|
||||||
|
BEGIN
|
||||||
|
SELECT id INTO hier_team_id FROM public.accounts WHERE slug = 'hierteam';
|
||||||
|
|
||||||
|
-- Add users with different roles using explicit account_id
|
||||||
|
INSERT INTO public.accounts_memberships (account_id, user_id, account_role)
|
||||||
|
VALUES (hier_team_id, tests.get_supabase_uid('hiertest2'), 'admin');
|
||||||
|
|
||||||
|
INSERT INTO public.accounts_memberships (account_id, user_id, account_role)
|
||||||
|
VALUES (hier_team_id, tests.get_supabase_uid('hiertest3'), 'member');
|
||||||
|
|
||||||
|
INSERT INTO public.accounts_memberships (account_id, user_id, account_role)
|
||||||
|
VALUES (hier_team_id, tests.get_supabase_uid('hiertest4'), 'member');
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Test: Admin cannot modify owner's membership
|
||||||
|
select makerkit.authenticate_as('hiertest2');
|
||||||
|
|
||||||
|
select throws_ok(
|
||||||
|
$$ DELETE FROM public.accounts_memberships
|
||||||
|
WHERE account_id = (SELECT id FROM public.accounts WHERE slug = 'hierteam')
|
||||||
|
AND user_id = tests.get_supabase_uid('hiertest1') $$,
|
||||||
|
'The primary account owner cannot be actioned',
|
||||||
|
'Admin should not be able to remove the account owner'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test: Admin can modify a member
|
||||||
|
select lives_ok(
|
||||||
|
$$ UPDATE public.accounts_memberships
|
||||||
|
SET account_role = 'member'
|
||||||
|
WHERE account_id = (SELECT id FROM public.accounts WHERE slug = 'hierteam')
|
||||||
|
AND user_id = tests.get_supabase_uid('hiertest3') $$,
|
||||||
|
'Admin should be able to modify a member'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test: Member cannot modify another member
|
||||||
|
select makerkit.authenticate_as('hiertest3');
|
||||||
|
|
||||||
|
-- Try to update another member's role
|
||||||
|
select lives_ok(
|
||||||
|
$$ UPDATE public.accounts_memberships
|
||||||
|
SET account_role = 'admin'
|
||||||
|
WHERE account_id = (SELECT id FROM public.accounts WHERE slug = 'hierteam')
|
||||||
|
AND user_id = tests.get_supabase_uid('hiertest4') $$,
|
||||||
|
'Member attempt to modify another member should not crash'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Verify the role did not change - this confirms the policy is working
|
||||||
|
select row_eq(
|
||||||
|
$$ SELECT account_role FROM public.accounts_memberships
|
||||||
|
WHERE account_id = (SELECT id FROM public.accounts WHERE slug = 'hierteam')
|
||||||
|
AND user_id = tests.get_supabase_uid('hiertest4') $$,
|
||||||
|
row('member'::varchar),
|
||||||
|
'Member role should remain unchanged after modification attempt by another member'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test 4: Account Visibility Tests
|
||||||
|
select tests.create_supabase_user('vistest1', 'vistest1@test.com');
|
||||||
|
select tests.create_supabase_user('vistest2', 'vistest2@test.com');
|
||||||
|
select tests.create_supabase_user('vistest3', 'vistest3@test.com');
|
||||||
|
|
||||||
|
-- Create a team account
|
||||||
|
select makerkit.authenticate_as('vistest1');
|
||||||
|
select public.create_team_account('VisTeam');
|
||||||
|
|
||||||
|
-- Add vistest2 as a member
|
||||||
|
set local role postgres;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
vis_team_id uuid;
|
||||||
|
BEGIN
|
||||||
|
SELECT id INTO vis_team_id FROM public.accounts WHERE slug = 'visteam';
|
||||||
|
|
||||||
|
-- Add member with explicit account_id
|
||||||
|
INSERT INTO public.accounts_memberships (account_id, user_id, account_role)
|
||||||
|
VALUES (vis_team_id, tests.get_supabase_uid('vistest2'), 'member');
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Test: Member can see the account
|
||||||
|
select makerkit.authenticate_as('vistest2');
|
||||||
|
|
||||||
|
select isnt_empty(
|
||||||
|
$$ SELECT * FROM public.accounts WHERE slug = 'visteam' $$,
|
||||||
|
'Team member should be able to see the team account'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test: Non-member cannot see the account
|
||||||
|
select makerkit.authenticate_as('vistest3');
|
||||||
|
|
||||||
|
select is_empty(
|
||||||
|
$$ SELECT * FROM public.accounts WHERE slug = 'visteam' $$,
|
||||||
|
'Non-member should not be able to see the team account'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test 5: Team account functions security
|
||||||
|
select tests.create_supabase_user('functest1', 'functest1@test.com');
|
||||||
|
select tests.create_supabase_user('functest2', 'functest2@test.com');
|
||||||
|
|
||||||
|
-- Create team account
|
||||||
|
select makerkit.authenticate_as('functest1');
|
||||||
|
select public.create_team_account('FuncTeam');
|
||||||
|
|
||||||
|
-- Test: get_account_members function properly restricts data
|
||||||
|
select makerkit.authenticate_as('functest2');
|
||||||
|
|
||||||
|
select is_empty(
|
||||||
|
$$ SELECT * FROM public.get_account_members('functeam') $$,
|
||||||
|
'Non-member should not be able to get account members data'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add functest2 as a member
|
||||||
|
select makerkit.authenticate_as('functest1');
|
||||||
|
set local role postgres;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
func_team_id uuid;
|
||||||
|
BEGIN
|
||||||
|
SELECT id INTO func_team_id FROM public.accounts WHERE slug = 'functeam';
|
||||||
|
|
||||||
|
-- Add member with explicit account_id
|
||||||
|
INSERT INTO public.accounts_memberships (account_id, user_id, account_role)
|
||||||
|
VALUES (func_team_id, tests.get_supabase_uid('functest2'), 'member');
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Test: Now member can access team data
|
||||||
|
select makerkit.authenticate_as('functest2');
|
||||||
|
|
||||||
|
select isnt_empty(
|
||||||
|
$$ SELECT * FROM public.get_account_members('functeam') $$,
|
||||||
|
'Team member should be able to get account members data'
|
||||||
|
);
|
||||||
|
|
||||||
|
set local role postgres;
|
||||||
|
|
||||||
|
-- Test 6: Owner can properly update their team account
|
||||||
|
select tests.create_supabase_user('ownerupdate1', 'ownerupdate1@test.com');
|
||||||
|
select tests.create_supabase_user('ownerupdate2', 'ownerupdate2@test.com');
|
||||||
|
|
||||||
|
-- Create team account
|
||||||
|
select makerkit.authenticate_as('ownerupdate1');
|
||||||
|
select public.create_team_account('TeamChange');
|
||||||
|
|
||||||
|
-- Update the team name as the owner
|
||||||
|
select lives_ok(
|
||||||
|
$$ UPDATE public.accounts
|
||||||
|
SET name = 'Updated Owner Team'
|
||||||
|
WHERE slug = 'teamchange'
|
||||||
|
RETURNING name $$,
|
||||||
|
'Owner should be able to update team name'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Verify the update was successful
|
||||||
|
select is(
|
||||||
|
(SELECT name FROM public.accounts WHERE slug = 'updated-owner-team'),
|
||||||
|
'Updated Owner Team'::varchar,
|
||||||
|
'Team name should be updated by owner'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Test non-owner member cannot update
|
||||||
|
select makerkit.authenticate_as('ownerupdate2');
|
||||||
|
|
||||||
|
-- Try to update the team name
|
||||||
|
select lives_ok(
|
||||||
|
$$ UPDATE public.accounts
|
||||||
|
SET name = 'Hacked Team Name'
|
||||||
|
WHERE slug = 'teamchange' $$,
|
||||||
|
'Non-owner update attempt should not crash'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Switch back to owner to verify non-owner update had no effect
|
||||||
|
select makerkit.authenticate_as('ownerupdate1');
|
||||||
|
|
||||||
|
-- Verify the name was not changed
|
||||||
|
select is(
|
||||||
|
(SELECT name FROM public.accounts WHERE slug = 'updated-owner-team'),
|
||||||
|
'Updated Owner Team'::varchar,
|
||||||
|
'Team name should not be changed by non-owner'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Start a new test section for cross-account access with fresh teams
|
||||||
|
-- Reset our test environment for a clean test of cross-account access
|
||||||
|
select
|
||||||
|
tests.create_supabase_user('crosstest1', 'crosstest1@test.com');
|
||||||
|
|
||||||
|
select
|
||||||
|
tests.create_supabase_user('crosstest2', 'crosstest2@test.com');
|
||||||
|
|
||||||
|
-- Create first team account with crosstest1 as owner
|
||||||
|
select
|
||||||
|
makerkit.authenticate_as('crosstest1');
|
||||||
|
|
||||||
|
select
|
||||||
|
public.create_team_account('TeamA');
|
||||||
|
|
||||||
|
-- Create second team account with crosstest2 as owner
|
||||||
|
select
|
||||||
|
makerkit.authenticate_as('crosstest2');
|
||||||
|
|
||||||
|
select
|
||||||
|
public.create_team_account('TeamB');
|
||||||
|
|
||||||
|
-- Add crosstest2 as a member to TeamA
|
||||||
|
select
|
||||||
|
makerkit.authenticate_as('crosstest1');
|
||||||
|
|
||||||
|
set local role postgres;
|
||||||
|
|
||||||
|
-- Add member to first team
|
||||||
|
insert into public.accounts_memberships (account_id, user_id, account_role)
|
||||||
|
values (
|
||||||
|
(select id from makerkit.get_account_by_slug('teama')),
|
||||||
|
tests.get_supabase_uid('crosstest2'),
|
||||||
|
'member'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Verify crosstest2 is now a member of TeamA
|
||||||
|
select
|
||||||
|
row_eq($$
|
||||||
|
select
|
||||||
|
account_role from public.accounts_memberships
|
||||||
|
where
|
||||||
|
account_id = (select id from makerkit.get_account_by_slug('teama'))
|
||||||
|
and user_id = tests.get_supabase_uid('crosstest2')
|
||||||
|
$$,
|
||||||
|
row ('member'::varchar),
|
||||||
|
'crosstest2 should be a member of TeamA'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Verify crosstest2 cannot update TeamA even as a member
|
||||||
|
select
|
||||||
|
makerkit.authenticate_as('crosstest2');
|
||||||
|
|
||||||
|
-- Try to update the team name
|
||||||
|
select
|
||||||
|
lives_ok($$
|
||||||
|
update public.accounts
|
||||||
|
set name = 'Updated TeamA Name'
|
||||||
|
where id = (select id from makerkit.get_account_by_slug('teama'))
|
||||||
|
$$,
|
||||||
|
'Member update attempt on TeamA should not crash'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Verify values remain unchanged
|
||||||
|
select
|
||||||
|
row_eq($$
|
||||||
|
select name from public.accounts
|
||||||
|
where id = (select id from makerkit.get_account_by_slug('teama'))
|
||||||
|
$$,
|
||||||
|
row ('TeamA'::varchar),
|
||||||
|
'TeamA name should remain unchanged after member update attempt'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Verify crosstest1 (owner of TeamA) cannot see or modify TeamB
|
||||||
|
select
|
||||||
|
makerkit.authenticate_as('crosstest1');
|
||||||
|
|
||||||
|
select
|
||||||
|
is_empty($$
|
||||||
|
select * from public.accounts
|
||||||
|
where id = (select id from makerkit.get_account_by_slug('teamb'))
|
||||||
|
$$,
|
||||||
|
'Owner of TeamA should not be able to see TeamB'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Try to modify TeamB (should have no effect)
|
||||||
|
select
|
||||||
|
lives_ok($$
|
||||||
|
update public.accounts
|
||||||
|
set name = 'Hacked TeamB Name'
|
||||||
|
where id = (select id from makerkit.get_account_by_slug('teamb'))
|
||||||
|
$$,
|
||||||
|
'Attempt to update other team should not crash'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Check that TeamB remained unchanged
|
||||||
|
select
|
||||||
|
makerkit.authenticate_as('crosstest2');
|
||||||
|
|
||||||
|
select
|
||||||
|
row_eq($$
|
||||||
|
select name from public.accounts
|
||||||
|
where id = (select id from makerkit.get_account_by_slug('teamb'))
|
||||||
|
$$,
|
||||||
|
row ('TeamB'::varchar),
|
||||||
|
'TeamB name should remain unchanged after attempted update by non-member'
|
||||||
);
|
);
|
||||||
|
|
||||||
select
|
select
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ SELECT row_eq(
|
|||||||
'The subscription items price_amount should be updated'
|
'The subscription items price_amount should be updated'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('member');
|
select makerkit.authenticate_as('member');
|
||||||
|
|
||||||
-- account can read their own subscription
|
-- account can read their own subscription
|
||||||
SELECT isnt_empty(
|
SELECT isnt_empty(
|
||||||
@@ -94,7 +94,7 @@ SELECT isnt_empty(
|
|||||||
|
|
||||||
-- foreigners
|
-- foreigners
|
||||||
select tests.create_supabase_user('foreigner');
|
select tests.create_supabase_user('foreigner');
|
||||||
select tests.authenticate_as('foreigner');
|
select makerkit.authenticate_as('foreigner');
|
||||||
|
|
||||||
-- account cannot read other's subscription
|
-- account cannot read other's subscription
|
||||||
SELECT is_empty(
|
SELECT is_empty(
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ SELECT is(
|
|||||||
'The subscription status should be past_due'
|
'The subscription status should be past_due'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('member');
|
select makerkit.authenticate_as('member');
|
||||||
|
|
||||||
SELECT row_eq(
|
SELECT row_eq(
|
||||||
$$ select count(*) from subscription_items where subscription_id = 'sub_test' $$,
|
$$ select count(*) from subscription_items where subscription_id = 'sub_test' $$,
|
||||||
@@ -150,7 +150,7 @@ SELECT is(
|
|||||||
'The subscription should be active'
|
'The subscription should be active'
|
||||||
);
|
);
|
||||||
|
|
||||||
select tests.authenticate_as('member');
|
select makerkit.authenticate_as('member');
|
||||||
|
|
||||||
-- account can read their own subscription
|
-- account can read their own subscription
|
||||||
select isnt_empty(
|
select isnt_empty(
|
||||||
@@ -171,7 +171,7 @@ select is(
|
|||||||
|
|
||||||
-- foreigners
|
-- foreigners
|
||||||
select tests.create_supabase_user('foreigner');
|
select tests.create_supabase_user('foreigner');
|
||||||
select tests.authenticate_as('foreigner');
|
select makerkit.authenticate_as('foreigner');
|
||||||
|
|
||||||
-- account cannot read other's subscription
|
-- account cannot read other's subscription
|
||||||
select is_empty(
|
select is_empty(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ select makerkit.set_identifier('custom', 'custom@makerkit.dev');
|
|||||||
select tests.create_supabase_user('test', 'test@supabase.com');
|
select tests.create_supabase_user('test', 'test@supabase.com');
|
||||||
|
|
||||||
-- auth as a primary owner
|
-- auth as a primary owner
|
||||||
select tests.authenticate_as('primary_owner');
|
select makerkit.authenticate_as('primary_owner');
|
||||||
|
|
||||||
-- only the service role can transfer ownership
|
-- only the service role can transfer ownership
|
||||||
select throws_ok(
|
select throws_ok(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ select makerkit.set_identifier('custom', 'custom@makerkit.dev');
|
|||||||
-- another user not in the team
|
-- another user not in the team
|
||||||
select tests.create_supabase_user('test', 'test@supabase.com');
|
select tests.create_supabase_user('test', 'test@supabase.com');
|
||||||
|
|
||||||
select tests.authenticate_as('member');
|
select makerkit.authenticate_as('member');
|
||||||
|
|
||||||
-- run an update query
|
-- 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');
|
update public.accounts_memberships set account_role = 'owner' where user_id = auth.uid() and account_id = makerkit.get_account_id_by_slug('makerkit');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next-supabase-saas-kit-turbo",
|
"name": "next-supabase-saas-kit-turbo",
|
||||||
"version": "2.4.0",
|
"version": "2.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
"supabase:web:stop": "pnpm --filter web supabase:stop",
|
"supabase:web:stop": "pnpm --filter web supabase:stop",
|
||||||
"supabase:web:typegen": "pnpm --filter web supabase:typegen",
|
"supabase:web:typegen": "pnpm --filter web supabase:typegen",
|
||||||
"supabase:web:reset": "pnpm --filter web supabase:reset",
|
"supabase:web:reset": "pnpm --filter web supabase:reset",
|
||||||
|
"supabase:web:test": "pnpm --filter web supabase:test",
|
||||||
"stripe:listen": "pnpm --filter '@kit/stripe' start",
|
"stripe:listen": "pnpm --filter '@kit/stripe' start",
|
||||||
"env:generate": "turbo gen env",
|
"env:generate": "turbo gen env",
|
||||||
"env:validate": "turbo gen validate-env"
|
"env:validate": "turbo gen validate-env"
|
||||||
|
|||||||
@@ -76,7 +76,13 @@ export function PersonalAccountDropdown({
|
|||||||
personalAccountData?.name ?? account?.name ?? user?.email ?? '';
|
personalAccountData?.name ?? account?.name ?? user?.email ?? '';
|
||||||
|
|
||||||
const isSuperAdmin = useMemo(() => {
|
const isSuperAdmin = useMemo(() => {
|
||||||
return user?.app_metadata.role === 'super-admin';
|
const factors = user?.factors ?? [];
|
||||||
|
const hasAdminRole = user?.app_metadata.role === 'super-admin';
|
||||||
|
const hasTotpFactor = factors.some(
|
||||||
|
(factor) => factor.factor_type === 'totp' && factor.status === 'verified',
|
||||||
|
);
|
||||||
|
|
||||||
|
return hasAdminRole && hasTotpFactor;
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -179,12 +185,14 @@ export function PersonalAccountDropdown({
|
|||||||
|
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link
|
||||||
className={'s-full flex cursor-pointer items-center space-x-2'}
|
className={
|
||||||
|
's-full flex cursor-pointer items-center space-x-2 text-yellow-700 dark:text-yellow-500'
|
||||||
|
}
|
||||||
href={'/admin'}
|
href={'/admin'}
|
||||||
>
|
>
|
||||||
<Shield className={'h-5'} />
|
<Shield className={'h-5'} />
|
||||||
|
|
||||||
<span>Admin</span>
|
<span>Super Admin</span>
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</If>
|
</If>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
BadgeX,
|
BadgeX,
|
||||||
Ban,
|
Ban,
|
||||||
CreditCardIcon,
|
|
||||||
ShieldPlus,
|
ShieldPlus,
|
||||||
VenetianMask,
|
VenetianMask,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { Tables } from '@kit/supabase/database';
|
import { Tables } from '@kit/supabase/database';
|
||||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||||
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
||||||
import { Badge } from '@kit/ui/badge';
|
import { Badge } from '@kit/ui/badge';
|
||||||
@@ -49,15 +49,18 @@ export function AdminAccountPage(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function PersonalAccountPage(props: { account: Account }) {
|
async function PersonalAccountPage(props: { account: Account }) {
|
||||||
const client = getSupabaseServerAdminClient();
|
const adminClient = getSupabaseServerAdminClient();
|
||||||
|
|
||||||
const memberships = await getMemberships(props.account.id);
|
const { data, error } = await adminClient.auth.admin.getUserById(
|
||||||
const { data, error } = await client.auth.admin.getUserById(props.account.id);
|
props.account.id,
|
||||||
|
);
|
||||||
|
|
||||||
if (!data || error) {
|
if (!data || error) {
|
||||||
throw new Error(`User not found`);
|
throw new Error(`User not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const memberships = await getMemberships(props.account.id);
|
||||||
|
|
||||||
const isBanned =
|
const isBanned =
|
||||||
'banned_until' in data.user && data.user.banned_until !== 'none';
|
'banned_until' in data.user && data.user.banned_until !== 'none';
|
||||||
|
|
||||||
@@ -77,7 +80,11 @@ async function PersonalAccountPage(props: { account: Account }) {
|
|||||||
<div className={'flex gap-x-2.5'}>
|
<div className={'flex gap-x-2.5'}>
|
||||||
<If condition={isBanned}>
|
<If condition={isBanned}>
|
||||||
<AdminReactivateUserDialog userId={props.account.id}>
|
<AdminReactivateUserDialog userId={props.account.id}>
|
||||||
<Button size={'sm'} variant={'secondary'}>
|
<Button
|
||||||
|
size={'sm'}
|
||||||
|
variant={'secondary'}
|
||||||
|
data-test={'admin-reactivate-account-button'}
|
||||||
|
>
|
||||||
<ShieldPlus className={'mr-1 h-4'} />
|
<ShieldPlus className={'mr-1 h-4'} />
|
||||||
Reactivate
|
Reactivate
|
||||||
</Button>
|
</Button>
|
||||||
@@ -86,14 +93,22 @@ async function PersonalAccountPage(props: { account: Account }) {
|
|||||||
|
|
||||||
<If condition={!isBanned}>
|
<If condition={!isBanned}>
|
||||||
<AdminBanUserDialog userId={props.account.id}>
|
<AdminBanUserDialog userId={props.account.id}>
|
||||||
<Button size={'sm'} variant={'secondary'}>
|
<Button
|
||||||
|
size={'sm'}
|
||||||
|
variant={'secondary'}
|
||||||
|
data-test={'admin-ban-account-button'}
|
||||||
|
>
|
||||||
<Ban className={'text-destructive mr-1 h-3'} />
|
<Ban className={'text-destructive mr-1 h-3'} />
|
||||||
Ban
|
Ban
|
||||||
</Button>
|
</Button>
|
||||||
</AdminBanUserDialog>
|
</AdminBanUserDialog>
|
||||||
|
|
||||||
<AdminImpersonateUserDialog userId={props.account.id}>
|
<AdminImpersonateUserDialog userId={props.account.id}>
|
||||||
<Button size={'sm'} variant={'secondary'}>
|
<Button
|
||||||
|
size={'sm'}
|
||||||
|
variant={'secondary'}
|
||||||
|
data-test={'admin-impersonate-button'}
|
||||||
|
>
|
||||||
<VenetianMask className={'mr-1 h-4 text-blue-500'} />
|
<VenetianMask className={'mr-1 h-4 text-blue-500'} />
|
||||||
Impersonate
|
Impersonate
|
||||||
</Button>
|
</Button>
|
||||||
@@ -101,7 +116,11 @@ async function PersonalAccountPage(props: { account: Account }) {
|
|||||||
</If>
|
</If>
|
||||||
|
|
||||||
<AdminDeleteUserDialog userId={props.account.id}>
|
<AdminDeleteUserDialog userId={props.account.id}>
|
||||||
<Button size={'sm'} variant={'destructive'}>
|
<Button
|
||||||
|
size={'sm'}
|
||||||
|
variant={'destructive'}
|
||||||
|
data-test={'admin-delete-account-button'}
|
||||||
|
>
|
||||||
<BadgeX className={'mr-1 h-4'} />
|
<BadgeX className={'mr-1 h-4'} />
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
@@ -166,7 +185,11 @@ async function TeamAccountPage(props: {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AdminDeleteAccountDialog accountId={props.account.id}>
|
<AdminDeleteAccountDialog accountId={props.account.id}>
|
||||||
<Button size={'sm'} variant={'destructive'}>
|
<Button
|
||||||
|
size={'sm'}
|
||||||
|
variant={'destructive'}
|
||||||
|
data-test={'admin-delete-account-button'}
|
||||||
|
>
|
||||||
<BadgeX className={'mr-1 h-4'} />
|
<BadgeX className={'mr-1 h-4'} />
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
@@ -208,7 +231,7 @@ async function TeamAccountPage(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function SubscriptionsTable(props: { accountId: string }) {
|
async function SubscriptionsTable(props: { accountId: string }) {
|
||||||
const client = getSupabaseServerAdminClient();
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
const { data: subscription, error } = await client
|
const { data: subscription, error } = await client
|
||||||
.from('subscriptions')
|
.from('subscriptions')
|
||||||
@@ -229,21 +252,15 @@ async function SubscriptionsTable(props: { accountId: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-col space-y-2.5'}>
|
<div className={'flex flex-col gap-y-1'}>
|
||||||
<Heading level={6}>Subscription</Heading>
|
<Heading level={6}>Subscription</Heading>
|
||||||
|
|
||||||
<If
|
<If
|
||||||
condition={subscription}
|
condition={subscription}
|
||||||
fallback={
|
fallback={
|
||||||
<Alert variant={'warning'}>
|
<span className={'text-sm text-muted-foreground'}>
|
||||||
<CreditCardIcon className={'h-4'} />
|
This account does not currently have a subscription.
|
||||||
|
</span>
|
||||||
<AlertTitle>No subscription found for this account.</AlertTitle>
|
|
||||||
|
|
||||||
<AlertDescription>
|
|
||||||
This account does not have a subscription.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(subscription) => {
|
{(subscription) => {
|
||||||
@@ -355,7 +372,7 @@ async function SubscriptionsTable(props: { accountId: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getMemberships(userId: string) {
|
async function getMemberships(userId: string) {
|
||||||
const client = getSupabaseServerAdminClient();
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
const memberships = await client
|
const memberships = await client
|
||||||
.from('accounts_memberships')
|
.from('accounts_memberships')
|
||||||
@@ -378,7 +395,7 @@ async function getMemberships(userId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getMembers(accountSlug: string) {
|
async function getMembers(accountSlug: string) {
|
||||||
const client = getSupabaseServerAdminClient();
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
const members = await client.rpc('get_account_members', {
|
const members = await client.rpc('get_account_members', {
|
||||||
account_slug: accountSlug,
|
account_slug: accountSlug,
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export function AdminAccountsTable(
|
|||||||
page: number;
|
page: number;
|
||||||
filters: {
|
filters: {
|
||||||
type: 'all' | 'team' | 'personal';
|
type: 'all' | 'team' | 'personal';
|
||||||
|
query: string;
|
||||||
};
|
};
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
@@ -79,7 +80,7 @@ function AccountsTableFilters(props: {
|
|||||||
resolver: zodResolver(FiltersSchema),
|
resolver: zodResolver(FiltersSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: props.filters?.type ?? 'all',
|
type: props.filters?.type ?? 'all',
|
||||||
query: '',
|
query: props.filters?.query ?? '',
|
||||||
},
|
},
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
reValidateMode: 'onChange',
|
reValidateMode: 'onChange',
|
||||||
@@ -142,6 +143,7 @@ function AccountsTableFilters(props: {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl className={'w-full min-w-36 md:min-w-80'}>
|
<FormControl className={'w-full min-w-36 md:min-w-80'}>
|
||||||
<Input
|
<Input
|
||||||
|
data-test={'admin-accounts-table-filter-input'}
|
||||||
className={'w-full'}
|
className={'w-full'}
|
||||||
placeholder={`Search account...`}
|
placeholder={`Search account...`}
|
||||||
{...field}
|
{...field}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useTransition } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
@@ -23,6 +26,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@kit/ui/form';
|
} from '@kit/ui/form';
|
||||||
|
import { If } from '@kit/ui/if';
|
||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
|
|
||||||
import { banUserAction } from '../lib/server/admin-server-actions';
|
import { banUserAction } from '../lib/server/admin-server-actions';
|
||||||
@@ -33,6 +37,9 @@ export function AdminBanUserDialog(
|
|||||||
userId: string;
|
userId: string;
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
|
const [pending, startTransition] = useTransition();
|
||||||
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(BanUserSchema),
|
resolver: zodResolver(BanUserSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -57,11 +64,30 @@ export function AdminBanUserDialog(
|
|||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
data-test={'admin-ban-user-form'}
|
||||||
className={'flex flex-col space-y-8'}
|
className={'flex flex-col space-y-8'}
|
||||||
onSubmit={form.handleSubmit((data) => {
|
onSubmit={form.handleSubmit((data) => {
|
||||||
return banUserAction(data);
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
await banUserAction(data);
|
||||||
|
setError(false);
|
||||||
|
} catch {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
<If condition={error}>
|
||||||
|
<Alert variant={'destructive'}>
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
|
||||||
|
<AlertDescription>
|
||||||
|
There was an error banning the user. Please check the server
|
||||||
|
logs to see what went wrong.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</If>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name={'confirmation'}
|
name={'confirmation'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
@@ -91,7 +117,11 @@ export function AdminBanUserDialog(
|
|||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
|
||||||
<Button type={'submit'} variant={'destructive'}>
|
<Button
|
||||||
|
disabled={pending}
|
||||||
|
type={'submit'}
|
||||||
|
variant={'destructive'}
|
||||||
|
>
|
||||||
Ban User
|
Ban User
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ export async function AdminDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className={'text-muted-foreground w-max text-xs'}>
|
||||||
|
The above data is estimated and may not be 100% accurate.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useTransition } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
@@ -21,7 +24,9 @@ import {
|
|||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
} from '@kit/ui/form';
|
} from '@kit/ui/form';
|
||||||
|
import { If } from '@kit/ui/if';
|
||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
|
|
||||||
import { deleteAccountAction } from '../lib/server/admin-server-actions';
|
import { deleteAccountAction } from '../lib/server/admin-server-actions';
|
||||||
@@ -32,6 +37,9 @@ export function AdminDeleteAccountDialog(
|
|||||||
accountId: string;
|
accountId: string;
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
|
const [pending, startTransition] = useTransition();
|
||||||
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(DeleteAccountSchema),
|
resolver: zodResolver(DeleteAccountSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -57,11 +65,30 @@ export function AdminDeleteAccountDialog(
|
|||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
data-form={'admin-delete-account-form'}
|
||||||
className={'flex flex-col space-y-8'}
|
className={'flex flex-col space-y-8'}
|
||||||
onSubmit={form.handleSubmit((data) => {
|
onSubmit={form.handleSubmit((data) => {
|
||||||
return deleteAccountAction(data);
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
await deleteAccountAction(data);
|
||||||
|
setError(false);
|
||||||
|
} catch {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
<If condition={error}>
|
||||||
|
<Alert variant={'destructive'}>
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
|
||||||
|
<AlertDescription>
|
||||||
|
There was an error deleting the account. Please check the
|
||||||
|
server logs to see what went wrong.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</If>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name={'confirmation'}
|
name={'confirmation'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
@@ -83,6 +110,8 @@ export function AdminDeleteAccountDialog(
|
|||||||
Are you sure you want to do this? This action cannot be
|
Are you sure you want to do this? This action cannot be
|
||||||
undone.
|
undone.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -90,8 +119,12 @@ export function AdminDeleteAccountDialog(
|
|||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
|
||||||
<Button type={'submit'} variant={'destructive'}>
|
<Button
|
||||||
Delete
|
disabled={pending}
|
||||||
|
type={'submit'}
|
||||||
|
variant={'destructive'}
|
||||||
|
>
|
||||||
|
{pending ? 'Deleting...' : 'Delete'}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useTransition } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
@@ -23,6 +26,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@kit/ui/form';
|
} from '@kit/ui/form';
|
||||||
|
import { If } from '@kit/ui/if';
|
||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
|
|
||||||
import { deleteUserAction } from '../lib/server/admin-server-actions';
|
import { deleteUserAction } from '../lib/server/admin-server-actions';
|
||||||
@@ -33,6 +37,9 @@ export function AdminDeleteUserDialog(
|
|||||||
userId: string;
|
userId: string;
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
|
const [pending, startTransition] = useTransition();
|
||||||
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(DeleteUserSchema),
|
resolver: zodResolver(DeleteUserSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -58,11 +65,30 @@ export function AdminDeleteUserDialog(
|
|||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
data-test={'admin-delete-user-form'}
|
||||||
className={'flex flex-col space-y-8'}
|
className={'flex flex-col space-y-8'}
|
||||||
onSubmit={form.handleSubmit((data) => {
|
onSubmit={form.handleSubmit((data) => {
|
||||||
return deleteUserAction(data);
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
await deleteUserAction(data);
|
||||||
|
setError(false);
|
||||||
|
} catch {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
<If condition={error}>
|
||||||
|
<Alert variant={'destructive'}>
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
|
||||||
|
<AlertDescription>
|
||||||
|
There was an error deleting the user. Please check the server
|
||||||
|
logs to see what went wrong.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</If>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name={'confirmation'}
|
name={'confirmation'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
@@ -93,8 +119,12 @@ export function AdminDeleteUserDialog(
|
|||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
|
||||||
<Button type={'submit'} variant={'destructive'}>
|
<Button
|
||||||
Delete
|
disabled={pending}
|
||||||
|
type={'submit'}
|
||||||
|
variant={'destructive'}
|
||||||
|
>
|
||||||
|
{pending ? 'Deleting...' : 'Delete'}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useTransition } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
@@ -27,6 +28,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@kit/ui/form';
|
} from '@kit/ui/form';
|
||||||
|
import { If } from '@kit/ui/if';
|
||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { LoadingOverlay } from '@kit/ui/loading-overlay';
|
import { LoadingOverlay } from '@kit/ui/loading-overlay';
|
||||||
|
|
||||||
@@ -51,6 +53,9 @@ export function AdminImpersonateUserDialog(
|
|||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
const [error, setError] = useState<boolean | null>(null);
|
||||||
|
|
||||||
if (tokens) {
|
if (tokens) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -77,13 +82,31 @@ export function AdminImpersonateUserDialog(
|
|||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
data-test={'admin-impersonate-user-form'}
|
||||||
className={'flex flex-col space-y-8'}
|
className={'flex flex-col space-y-8'}
|
||||||
onSubmit={form.handleSubmit(async (data) => {
|
onSubmit={form.handleSubmit((data) => {
|
||||||
const tokens = await impersonateUserAction(data);
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
const result = await impersonateUserAction(data);
|
||||||
|
|
||||||
setTokens(tokens);
|
setTokens(result);
|
||||||
|
} catch {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
<If condition={error}>
|
||||||
|
<Alert variant={'destructive'}>
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
|
||||||
|
<AlertDescription>
|
||||||
|
Failed to impersonate user. Please check the logs to
|
||||||
|
understand what went wrong.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</If>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name={'confirmation'}
|
name={'confirmation'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
@@ -113,7 +136,9 @@ export function AdminImpersonateUserDialog(
|
|||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
|
||||||
<Button type={'submit'}>Impersonate User</Button>
|
<Button disabled={isPending} type={'submit'}>
|
||||||
|
{isPending ? 'Impersonating...' : 'Impersonate User'}
|
||||||
|
</Button>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useTransition } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
@@ -23,6 +26,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@kit/ui/form';
|
} from '@kit/ui/form';
|
||||||
|
import { If } from '@kit/ui/if';
|
||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
|
|
||||||
import { reactivateUserAction } from '../lib/server/admin-server-actions';
|
import { reactivateUserAction } from '../lib/server/admin-server-actions';
|
||||||
@@ -33,6 +37,9 @@ export function AdminReactivateUserDialog(
|
|||||||
userId: string;
|
userId: string;
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
|
const [pending, startTransition] = useTransition();
|
||||||
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(ReactivateUserSchema),
|
resolver: zodResolver(ReactivateUserSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -56,11 +63,30 @@ export function AdminReactivateUserDialog(
|
|||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
data-test={'admin-reactivate-user-form'}
|
||||||
className={'flex flex-col space-y-8'}
|
className={'flex flex-col space-y-8'}
|
||||||
onSubmit={form.handleSubmit((data) => {
|
onSubmit={form.handleSubmit((data) => {
|
||||||
return reactivateUserAction(data);
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
await reactivateUserAction(data);
|
||||||
|
setError(false);
|
||||||
|
} catch {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
<If condition={error}>
|
||||||
|
<Alert variant={'destructive'}>
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
|
||||||
|
<AlertDescription>
|
||||||
|
There was an error reactivating the user. Please check the
|
||||||
|
server logs to see what went wrong.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</If>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name={'confirmation'}
|
name={'confirmation'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
@@ -90,7 +116,9 @@ export function AdminReactivateUserDialog(
|
|||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
|
||||||
<Button type={'submit'}>Reactivate User</Button>
|
<Button disabled={pending} type={'submit'}>
|
||||||
|
{pending ? 'Reactivating...' : 'Reactivate User'}
|
||||||
|
</Button>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'server-only';
|
|||||||
|
|
||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
|
|
||||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
import { createAdminDashboardService } from '../services/admin-dashboard.service';
|
import { createAdminDashboardService } from '../services/admin-dashboard.service';
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ import { createAdminDashboardService } from '../services/admin-dashboard.service
|
|||||||
export const loadAdminDashboard = cache(adminDashboardLoader);
|
export const loadAdminDashboard = cache(adminDashboardLoader);
|
||||||
|
|
||||||
function adminDashboardLoader() {
|
function adminDashboardLoader() {
|
||||||
const client = getSupabaseServerAdminClient();
|
const client = getSupabaseServerClient();
|
||||||
const service = createAdminDashboardService(client);
|
const service = createAdminDashboardService(client);
|
||||||
|
|
||||||
return service.getDashboardData();
|
return service.getDashboardData();
|
||||||
|
|||||||
@@ -137,6 +137,17 @@ class AdminAuthUserService {
|
|||||||
`You cannot perform a destructive action on your own account as a Super Admin`,
|
`You cannot perform a destructive action on your own account as a Super Admin`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const targetUser =
|
||||||
|
await this.adminClient.auth.admin.getUserById(targetUserId);
|
||||||
|
|
||||||
|
const targetUserRole = targetUser.data.user?.app_metadata?.role;
|
||||||
|
|
||||||
|
if (targetUserRole === 'super-admin') {
|
||||||
|
throw new Error(
|
||||||
|
`You cannot perform a destructive action on a Super Admin account`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setBanDuration(userId: string, banDuration: string) {
|
private async setBanDuration(userId: string, banDuration: string) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
|
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -9,25 +8,15 @@ import { Database } from '@kit/supabase/database';
|
|||||||
* @param client
|
* @param client
|
||||||
*/
|
*/
|
||||||
export async function isSuperAdmin(client: SupabaseClient<Database>) {
|
export async function isSuperAdmin(client: SupabaseClient<Database>) {
|
||||||
const { data, error } = await client.auth.getUser();
|
try {
|
||||||
|
const { data, error } = await client.rpc('is_super_admin');
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.user) {
|
return data;
|
||||||
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requiresMultiFactorAuthentication =
|
|
||||||
await checkRequiresMultiFactorAuthentication(client);
|
|
||||||
|
|
||||||
// If user requires multi-factor authentication, deny access.
|
|
||||||
if (requiresMultiFactorAuthentication) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appMetadata = data.user.app_metadata;
|
|
||||||
|
|
||||||
return appMetadata?.role === 'super-admin';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
FormItem,
|
FormItem,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@kit/ui/form';
|
} from '@kit/ui/form';
|
||||||
|
import { Heading } from '@kit/ui/heading';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import {
|
import {
|
||||||
InputOTP,
|
InputOTP,
|
||||||
@@ -86,9 +87,15 @@ export function MultiFactorChallengeContainer({
|
|||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col items-center gap-y-6'}>
|
||||||
<div className={'flex w-full flex-col space-y-2.5'}>
|
<div className="flex flex-col items-center gap-y-4">
|
||||||
<div className={'flex flex-col space-y-4'}>
|
<Heading level={5}>
|
||||||
|
<Trans i18nKey={'auth:verifyCodeHeading'} />
|
||||||
|
</Heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'flex w-full flex-col gap-y-2.5'}>
|
||||||
|
<div className={'flex flex-col gap-y-4'}>
|
||||||
<If condition={verifyMFAChallenge.error}>
|
<If condition={verifyMFAChallenge.error}>
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<ExclamationTriangleIcon className={'h-5'} />
|
<ExclamationTriangleIcon className={'h-5'} />
|
||||||
@@ -130,7 +137,7 @@ export function MultiFactorChallengeContainer({
|
|||||||
</InputOTP>
|
</InputOTP>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormDescription>
|
<FormDescription className="text-center">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={'account:verifyActivationCodeDescription'}
|
i18nKey={'account:verifyActivationCodeDescription'}
|
||||||
/>
|
/>
|
||||||
@@ -145,6 +152,8 @@ export function MultiFactorChallengeContainer({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
data-test={'submit-mfa-button'}
|
||||||
disabled={
|
disabled={
|
||||||
verifyMFAChallenge.isPending ||
|
verifyMFAChallenge.isPending ||
|
||||||
!verificationCodeForm.formState.isValid
|
!verificationCodeForm.formState.isValid
|
||||||
|
|||||||
@@ -858,6 +858,14 @@ export type Database = {
|
|||||||
};
|
};
|
||||||
Returns: boolean;
|
Returns: boolean;
|
||||||
};
|
};
|
||||||
|
install_extensions: {
|
||||||
|
Args: Record<PropertyKey, never>;
|
||||||
|
Returns: undefined;
|
||||||
|
};
|
||||||
|
is_aal2: {
|
||||||
|
Args: Record<PropertyKey, never>;
|
||||||
|
Returns: boolean;
|
||||||
|
};
|
||||||
is_account_owner: {
|
is_account_owner: {
|
||||||
Args: {
|
Args: {
|
||||||
account_id: string;
|
account_id: string;
|
||||||
@@ -870,12 +878,20 @@ export type Database = {
|
|||||||
};
|
};
|
||||||
Returns: boolean;
|
Returns: boolean;
|
||||||
};
|
};
|
||||||
|
is_mfa_compliant: {
|
||||||
|
Args: Record<PropertyKey, never>;
|
||||||
|
Returns: boolean;
|
||||||
|
};
|
||||||
is_set: {
|
is_set: {
|
||||||
Args: {
|
Args: {
|
||||||
field_name: string;
|
field_name: string;
|
||||||
};
|
};
|
||||||
Returns: boolean;
|
Returns: boolean;
|
||||||
};
|
};
|
||||||
|
is_super_admin: {
|
||||||
|
Args: Record<PropertyKey, never>;
|
||||||
|
Returns: boolean;
|
||||||
|
};
|
||||||
is_team_member: {
|
is_team_member: {
|
||||||
Args: {
|
Args: {
|
||||||
account_id: string;
|
account_id: string;
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export function DataTable<T extends object>({
|
|||||||
manualSorting = false,
|
manualSorting = false,
|
||||||
sorting: initialSorting,
|
sorting: initialSorting,
|
||||||
}: ReactTableProps<T>) {
|
}: ReactTableProps<T>) {
|
||||||
|
'use no memo';
|
||||||
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
pageIndex: pageIndex ?? 0,
|
pageIndex: pageIndex ?? 0,
|
||||||
pageSize: pageSize ?? 15,
|
pageSize: pageSize ?? 15,
|
||||||
|
|||||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -117,6 +117,9 @@ importers:
|
|||||||
node-html-parser:
|
node-html-parser:
|
||||||
specifier: ^7.0.1
|
specifier: ^7.0.1
|
||||||
version: 7.0.1
|
version: 7.0.1
|
||||||
|
totp-generator:
|
||||||
|
specifier: ^1.0.0
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
apps/web:
|
apps/web:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5965,6 +5968,9 @@ packages:
|
|||||||
jsonfile@6.1.0:
|
jsonfile@6.1.0:
|
||||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||||
|
|
||||||
|
jssha@3.3.1:
|
||||||
|
resolution: {integrity: sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==}
|
||||||
|
|
||||||
jsx-ast-utils@3.3.5:
|
jsx-ast-utils@3.3.5:
|
||||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
@@ -7463,6 +7469,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
totp-generator@1.0.0:
|
||||||
|
resolution: {integrity: sha512-Iu/1Lk60/MH8FE+5cDWPiGbwKK1hxzSq+KT9oSqhZ1BEczGIKGcN50bP0WMLiIZKRg7t29iWLxw6f81TICQdoA==}
|
||||||
|
|
||||||
tr46@0.0.3:
|
tr46@0.0.3:
|
||||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||||
|
|
||||||
@@ -13356,6 +13365,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
|
|
||||||
|
jssha@3.3.1: {}
|
||||||
|
|
||||||
jsx-ast-utils@3.3.5:
|
jsx-ast-utils@3.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes: 3.1.8
|
array-includes: 3.1.8
|
||||||
@@ -15146,6 +15157,10 @@ snapshots:
|
|||||||
|
|
||||||
totalist@3.0.1: {}
|
totalist@3.0.1: {}
|
||||||
|
|
||||||
|
totp-generator@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
jssha: 3.3.1
|
||||||
|
|
||||||
tr46@0.0.3: {}
|
tr46@0.0.3: {}
|
||||||
|
|
||||||
ts-api-utils@2.0.1(typescript@5.7.3):
|
ts-api-utils@2.0.1(typescript@5.7.3):
|
||||||
|
|||||||
@@ -4,27 +4,45 @@ export function checkPendingMigrations() {
|
|||||||
try {
|
try {
|
||||||
console.info('\x1b[34m%s\x1b[0m', 'Checking for pending migrations...');
|
console.info('\x1b[34m%s\x1b[0m', 'Checking for pending migrations...');
|
||||||
|
|
||||||
const output = execSync('pnpm --filter web supabase migration list', { encoding: 'utf-8', stdio: 'pipe' });
|
const output = execSync('pnpm --filter web supabase migration list', {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
});
|
||||||
|
|
||||||
const lines = output.split('\n');
|
const lines = output.split('\n');
|
||||||
|
|
||||||
// Skip header lines
|
// Skip header lines
|
||||||
const migrationLines = lines.slice(4);
|
const migrationLines = lines.slice(4);
|
||||||
|
|
||||||
const pendingMigrations = migrationLines
|
const pendingMigrations = migrationLines
|
||||||
.filter(line => {
|
.filter((line) => {
|
||||||
const [local, remote] = line.split('│').map(s => s.trim());
|
const [local, remote] = line.split('│').map((s) => s.trim());
|
||||||
return local !== '' && remote === '';
|
return local !== '' && remote === '';
|
||||||
})
|
})
|
||||||
.map(line => (line.split('│')[0] ?? '').trim());
|
.map((line) => (line.split('│')[0] ?? '').trim());
|
||||||
|
|
||||||
if (pendingMigrations.length > 0) {
|
if (pendingMigrations.length > 0) {
|
||||||
console.log('\x1b[33m%s\x1b[0m', '⚠️ There are pending migrations that need to be applied:');
|
console.log(
|
||||||
pendingMigrations.forEach(migration => console.log(` - ${migration}`));
|
'\x1b[33m%s\x1b[0m',
|
||||||
console.log('\nPlease run "pnpm --filter web supabase db push" to apply these migrations.');
|
'⚠️ There are pending migrations that need to be applied:',
|
||||||
|
);
|
||||||
|
|
||||||
|
pendingMigrations.forEach((migration) => console.log(` - ${migration}`));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'\nSome functionality may not work as expected until these migrations are applied.',
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'\nAfter testing the migrations in your local environment and ideally in a staging environment, please run "pnpm --filter web supabase db push" to apply them to your database. If you have any questions, please open a support ticket.',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log('\x1b[32m%s\x1b[0m', '✅ All migrations are up to date.');
|
console.log('\x1b[32m%s\x1b[0m', '✅ All migrations are up to date.');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('\x1b[33m%s\x1b[0m', '💡 Info: Project not yet linked to a remote Supabase project. Migration checks skipped - this is expected for new projects. Link your project when you\'re ready to sync with Supabase.\n');
|
console.log(
|
||||||
|
'\x1b[33m%s\x1b[0m',
|
||||||
|
"💡 Info: Project not yet linked to a remote Supabase project. Migration checks skipped - this is expected for new projects. Link your project when you're ready to sync with Supabase.\n",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user