Add Super Admin layout and update subscription functionalities
The key changes made in this code include the addition of a Super Admin layout. Also, subscription functionalities are updated and optimized. This ensures read, write permissions are specific to the relevant user and a helper function has been implemented to check if an account has an active subscription. Furthermore, UI enhancements have been made to the accounts table in the administration section. The seed data has also been modified.
This commit is contained in:
@@ -28,7 +28,7 @@ export async function AdminSidebar() {
|
||||
<AppLogo href={'/admin'} />
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarContent>
|
||||
<SidebarContent className={'mt-5'}>
|
||||
<SidebarGroup label={'Admin'} collapsible={false}>
|
||||
<SidebarItem end path={'/admin'} Icon={<Home className={'h-4'} />}>
|
||||
Home
|
||||
|
||||
30
apps/web/app/admin/_components/mobile-navigation.tsx
Normal file
30
apps/web/app/admin/_components/mobile-navigation.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Menu } from 'lucide-react';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@kit/ui/dropdown-menu';
|
||||
|
||||
export function AdminMobileNavigation() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Menu className={'h-8 w-8'} />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<Link href={'/admin'}>Home</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem>
|
||||
<Link href={'/admin/accounts'}>Accounts</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs';
|
||||
import { AdminAccountsTable } from '@kit/admin/components/admin-accounts-table';
|
||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
|
||||
interface SearchParams {
|
||||
page?: string;
|
||||
@@ -25,33 +24,26 @@ function AccountsPage({ searchParams }: { searchParams: SearchParams }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title={'Accounts'}
|
||||
description={`Manage your accounts, view their details, and more.`}
|
||||
/>
|
||||
|
||||
<PageBody>
|
||||
<ServerDataLoader
|
||||
table={'accounts'}
|
||||
client={client}
|
||||
page={page}
|
||||
where={filters}
|
||||
>
|
||||
{({ data, page, pageSize, pageCount }) => {
|
||||
return (
|
||||
<AdminAccountsTable
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
pageCount={pageCount}
|
||||
data={data}
|
||||
filters={{
|
||||
type: searchParams.account_type ?? 'all',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</ServerDataLoader>
|
||||
</PageBody>
|
||||
<ServerDataLoader
|
||||
table={'accounts'}
|
||||
client={client}
|
||||
page={page}
|
||||
where={filters}
|
||||
>
|
||||
{({ data, page, pageSize, pageCount }) => {
|
||||
return (
|
||||
<AdminAccountsTable
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
pageCount={pageCount}
|
||||
data={data}
|
||||
filters={{
|
||||
type: searchParams.account_type ?? 'all',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</ServerDataLoader>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import { Page } from '@kit/ui/page';
|
||||
import { Page, PageBody, PageHeader } from '@kit/ui/page';
|
||||
|
||||
import { AdminSidebar } from '~/admin/_components/admin-sidebar';
|
||||
import { AdminMobileNavigation } from '~/admin/_components/mobile-navigation';
|
||||
|
||||
export const metadata = {
|
||||
title: `Admin`,
|
||||
title: `Super Admin`,
|
||||
};
|
||||
|
||||
export default function AdminLayout(props: React.PropsWithChildren) {
|
||||
return <Page sidebar={<AdminSidebar />}>{props.children}</Page>;
|
||||
return (
|
||||
<Page sidebar={<AdminSidebar />}>
|
||||
<PageHeader
|
||||
mobileNavigation={<AdminMobileNavigation />}
|
||||
title={'Super Admin'}
|
||||
description={`Your SaaS stats at a glance`}
|
||||
/>
|
||||
|
||||
<PageBody>{props.children}</PageBody>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { AdminDashboard } from '@kit/admin/components/admin-dashboard';
|
||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
|
||||
function AdminPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={'Admin'} description={`Your SaaS stats at a glance`} />
|
||||
|
||||
<PageBody>
|
||||
<AdminDashboard />
|
||||
</PageBody>
|
||||
<AdminDashboard />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,18 @@ export async function POST(request: Request) {
|
||||
);
|
||||
|
||||
try {
|
||||
await service.handleWebhookEvent(request);
|
||||
await service.handleWebhookEvent(request, {
|
||||
onEvent(event: string, data: unknown) {
|
||||
logger.info(
|
||||
{
|
||||
...ctx,
|
||||
event,
|
||||
data,
|
||||
},
|
||||
`Received billing event`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(ctx, `Successfully processed billing webhook`);
|
||||
|
||||
|
||||
@@ -90,4 +90,8 @@ Optimize dropdowns for mobile
|
||||
|
||||
[data-radix-menu-content] {
|
||||
@apply rounded-none md:rounded-lg;
|
||||
}
|
||||
|
||||
[data-radix-menu-content] [role="menuitem"] {
|
||||
@apply md:min-h-0 min-h-12;
|
||||
}
|
||||
@@ -1410,9 +1410,7 @@ on conflict (
|
||||
intv_count
|
||||
from
|
||||
item_data
|
||||
on conflict (subscription_id,
|
||||
product_id,
|
||||
variant_id)
|
||||
on conflict (id)
|
||||
do update set
|
||||
price_amount = excluded.price_amount,
|
||||
quantity = excluded.quantity,
|
||||
@@ -2154,6 +2152,28 @@ language plpgsql;
|
||||
grant execute on function public.add_invitations_to_account(text,
|
||||
public.invitation[]) to authenticated, service_role;
|
||||
|
||||
create or replace function public.has_active_subscription(target_account_id uuid)
|
||||
returns boolean
|
||||
as $$
|
||||
begin
|
||||
return exists (
|
||||
select
|
||||
1
|
||||
from
|
||||
public.subscriptions
|
||||
where
|
||||
account_id = target_account_id
|
||||
and active = true);
|
||||
|
||||
end;
|
||||
|
||||
$$
|
||||
language plpgsql;
|
||||
|
||||
grant execute on function public.has_active_subscription(uuid) to
|
||||
authenticated, service_role;
|
||||
|
||||
|
||||
-- Storage
|
||||
-- Account Image
|
||||
insert into storage.buckets(
|
||||
|
||||
@@ -57,7 +57,7 @@ execute function "supabase_functions"."http_request"(
|
||||
|
||||
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
|
||||
('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"]}', '{"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', '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);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ VALUES (tests.get_supabase_uid('primary_owner'), 'stripe', 'cus_test');
|
||||
-- Call the upsert_subscription function
|
||||
SELECT public.upsert_subscription(tests.get_supabase_uid('primary_owner'), 'cus_test', 'sub_test', true, 'active', 'stripe', false, 'usd', now(), now() + interval '1 month', '[
|
||||
{
|
||||
"id": "sub_123",
|
||||
"product_id": "prod_test",
|
||||
"variant_id": "var_test",
|
||||
"type": "flat",
|
||||
@@ -24,6 +25,7 @@ SELECT public.upsert_subscription(tests.get_supabase_uid('primary_owner'), 'cus_
|
||||
"interval_count": 1
|
||||
},
|
||||
{
|
||||
"id": "sub_456",
|
||||
"product_id": "prod_test_2",
|
||||
"variant_id": "var_test_2",
|
||||
"type": "flat",
|
||||
@@ -57,6 +59,7 @@ SELECT is(
|
||||
-- Call the upsert_subscription function again to update the subscription
|
||||
SELECT public.upsert_subscription(tests.get_supabase_uid('primary_owner'), 'cus_test', 'sub_test', false, 'past_due', 'stripe', true, 'usd', now(), now() + interval '1 month', '[
|
||||
{
|
||||
"id": "sub_123",
|
||||
"product_id": "prod_test",
|
||||
"variant_id": "var_test",
|
||||
"type": "flat",
|
||||
@@ -66,6 +69,7 @@ SELECT public.upsert_subscription(tests.get_supabase_uid('primary_owner'), 'cus_
|
||||
"interval_count": 1
|
||||
},
|
||||
{
|
||||
"id": "sub_456",
|
||||
"product_id": "prod_test_2",
|
||||
"variant_id": "var_test_2",
|
||||
"type": "flat",
|
||||
@@ -132,21 +136,33 @@ select throws_ok(
|
||||
'permission denied for function upsert_subscription'
|
||||
);
|
||||
|
||||
select is(
|
||||
(public.has_active_subscription(tests.get_supabase_uid('primary_owner'))),
|
||||
true,
|
||||
'The function public.has_active_subscription should return true when the account has a subscription'
|
||||
);
|
||||
|
||||
-- foreigners
|
||||
select tests.create_supabase_user('foreigner');
|
||||
select tests.authenticate_as('foreigner');
|
||||
|
||||
-- account cannot read other's subscription
|
||||
SELECT is_empty(
|
||||
select is_empty(
|
||||
$$ select 1 from subscriptions where id = 'sub_test' $$,
|
||||
'The account cannot read the other account subscriptions'
|
||||
);
|
||||
|
||||
SELECT is_empty(
|
||||
select is_empty(
|
||||
$$ select 1 from subscription_items where subscription_id = 'sub_test' $$,
|
||||
'The account cannot read the other account subscription items'
|
||||
);
|
||||
|
||||
select is(
|
||||
(public.has_active_subscription(tests.get_supabase_uid('primary_owner'))),
|
||||
false,
|
||||
'The function public.has_active_subscription should return false when a foreigner is querying the account subscription'
|
||||
);
|
||||
|
||||
-- Finish the tests and clean up
|
||||
select * from finish();
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ VALUES (makerkit.get_account_id_by_slug('makerkit'), 'stripe', 'cus_test');
|
||||
-- Call the upsert_subscription function
|
||||
SELECT public.upsert_subscription(makerkit.get_account_id_by_slug('makerkit'), 'cus_test', 'sub_test', true, 'active', 'stripe', false, 'usd', now(), now() + interval '1 month', '[
|
||||
{
|
||||
"id": "sub_123",
|
||||
"product_id": "prod_test",
|
||||
"variant_id": "var_test",
|
||||
"type": "flat",
|
||||
@@ -24,6 +25,7 @@ SELECT public.upsert_subscription(makerkit.get_account_id_by_slug('makerkit'), '
|
||||
"interval_count": 1
|
||||
},
|
||||
{
|
||||
"id": "sub_456",
|
||||
"product_id": "prod_test_2",
|
||||
"variant_id": "var_test_2",
|
||||
"type": "flat",
|
||||
@@ -57,6 +59,7 @@ SELECT is(
|
||||
-- Call the upsert_subscription function again to update the subscription
|
||||
SELECT public.upsert_subscription(makerkit.get_account_id_by_slug('makerkit'), 'cus_test', 'sub_test', false, 'past_due', 'stripe', true, 'usd', now(), now() + interval '1 month', '[
|
||||
{
|
||||
"id": "sub_123",
|
||||
"product_id": "prod_test",
|
||||
"variant_id": "var_test",
|
||||
"type": "flat",
|
||||
@@ -66,6 +69,7 @@ SELECT public.upsert_subscription(makerkit.get_account_id_by_slug('makerkit'), '
|
||||
"interval_count": 1
|
||||
},
|
||||
{
|
||||
"id": "sub_456",
|
||||
"product_id": "prod_test_2",
|
||||
"variant_id": "var_test_2",
|
||||
"type": "flat",
|
||||
@@ -116,31 +120,43 @@ SELECT is(
|
||||
select tests.authenticate_as('member');
|
||||
|
||||
-- account can read their own subscription
|
||||
SELECT isnt_empty(
|
||||
select isnt_empty(
|
||||
$$ select 1 from subscriptions where id = 'sub_test' $$,
|
||||
'The account can read their own subscription'
|
||||
);
|
||||
|
||||
SELECT isnt_empty(
|
||||
select isnt_empty(
|
||||
$$ select * from subscription_items where subscription_id = 'sub_test' $$,
|
||||
'The account can read their own subscription items'
|
||||
);
|
||||
|
||||
select is(
|
||||
(public.has_active_subscription(makerkit.get_account_id_by_slug('makerkit'))),
|
||||
true,
|
||||
'The function public.has_active_subscription should return true when the account has a subscription'
|
||||
);
|
||||
|
||||
-- foreigners
|
||||
select tests.create_supabase_user('foreigner');
|
||||
select tests.authenticate_as('foreigner');
|
||||
|
||||
-- account cannot read other's subscription
|
||||
SELECT is_empty(
|
||||
select is_empty(
|
||||
$$ select 1 from subscriptions where id = 'sub_test' $$,
|
||||
'The account cannot read the other account subscriptions'
|
||||
);
|
||||
|
||||
SELECT is_empty(
|
||||
select is_empty(
|
||||
$$ select 1 from subscription_items where subscription_id = 'sub_test' $$,
|
||||
'The account cannot read the other account subscription items'
|
||||
);
|
||||
|
||||
select is(
|
||||
(public.has_active_subscription(makerkit.get_account_id_by_slug('makerkit'))),
|
||||
false,
|
||||
'The function public.has_active_subscription should return false when a foreigner is querying the account subscription'
|
||||
);
|
||||
|
||||
-- Finish the tests and clean up
|
||||
select * from finish();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user