Files
myeasycms-v2/packages/features/admin/src/components/admin-account-page.tsx
giancarlo a9eaaafd3d Update packages and refactor logging in diverse services
This commit updates diverse packages such as "@makerkit/data-loader-supabase-core" and "@makerkit/data-loader-supabase-nextjs" to the new versions in the package.json files. Also, several refactorings were done in logging within services and loaders by progressing 'server-only' imports and improving context handling. Additionally, type annotations have been added to several exported functions for better code readability and maintainability.
2024-04-09 17:23:48 +08:00

358 lines
10 KiB
TypeScript

import { BadgeX, Ban, ShieldPlus, VenetianMask } from 'lucide-react';
import { Database } from '@kit/supabase/database';
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Heading } from '@kit/ui/heading';
import { If } from '@kit/ui/if';
import { PageBody, PageHeader } from '@kit/ui/page';
import { ProfileAvatar } from '@kit/ui/profile-avatar';
import { Separator } from '@kit/ui/separator';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@kit/ui/table';
import { AdminBanUserDialog } from './admin-ban-user-dialog';
import { AdminDeleteAccountDialog } from './admin-delete-account-dialog';
import { AdminDeleteUserDialog } from './admin-delete-user-dialog';
import { AdminImpersonateUserDialog } from './admin-impersonate-user-dialog';
import { AdminMembersTable } from './admin-members-table';
import { AdminMembershipsTable } from './admin-memberships-table';
import { AdminReactivateUserDialog } from './admin-reactivate-user-dialog';
type Db = Database['public']['Tables'];
type Account = Db['accounts']['Row'];
type Membership = Db['accounts_memberships']['Row'];
export function AdminAccountPage(props: {
account: Account & { memberships: Membership[] };
}) {
if (props.account.is_personal_account) {
return <PersonalAccountPage account={props.account} />;
}
return <TeamAccountPage account={props.account} />;
}
async function PersonalAccountPage(props: { account: Account }) {
const client = getSupabaseServerComponentClient({
admin: true,
});
const memberships = await getMemberships(props.account.id);
const { data, error } = await client.auth.admin.getUserById(props.account.id);
if (!data || error) {
throw new Error(`User not found`);
}
const isBanned =
'banned_until' in data.user && data.user.banned_until !== 'none';
return (
<>
<PageHeader
title={
<div className={'flex items-center space-x-2.5'}>
<ProfileAvatar
pictureUrl={props.account.picture_url}
displayName={props.account.name}
/>
<span>{props.account.name}</span>
<Badge variant={'outline'}>Personal Account</Badge>
<If condition={isBanned}>
<Badge variant={'destructive'}>Banned</Badge>
</If>
</div>
}
>
<div className={'flex space-x-1'}>
<If condition={isBanned}>
<AdminReactivateUserDialog userId={props.account.id}>
<Button size={'sm'} variant={'ghost'}>
<ShieldPlus className={'mr-1 h-4'} />
Reactivate
</Button>
</AdminReactivateUserDialog>
</If>
<If condition={!isBanned}>
<AdminBanUserDialog userId={props.account.id}>
<Button size={'sm'} variant={'ghost'}>
<Ban className={'mr-1 h-4'} />
Ban
</Button>
</AdminBanUserDialog>
<AdminImpersonateUserDialog userId={props.account.id}>
<Button size={'sm'} variant={'ghost'}>
<VenetianMask className={'mr-1 h-4'} />
Impersonate
</Button>
</AdminImpersonateUserDialog>
</If>
<AdminDeleteUserDialog userId={props.account.id}>
<Button size={'sm'} variant={'destructive'}>
<BadgeX className={'mr-1 h-4'} />
Delete
</Button>
</AdminDeleteUserDialog>
</div>
</PageHeader>
<PageBody>
<div className={'flex flex-col space-y-8'}>
<SubscriptionsTable accountId={props.account.id} />
<div className={'divider-divider-x flex flex-col space-y-2.5'}>
<Heading level={6}>
This user is a member of the following teams:
</Heading>
<div>
<AdminMembershipsTable memberships={memberships} />
</div>
</div>
</div>
</PageBody>
</>
);
}
async function TeamAccountPage(props: {
account: Account & { memberships: Membership[] };
}) {
const members = await getMembers(props.account.slug ?? '');
return (
<>
<PageHeader
title={
<div className={'flex items-center space-x-2.5'}>
<ProfileAvatar
pictureUrl={props.account.picture_url}
displayName={props.account.name}
/>
<span>{props.account.name}</span>
<Badge variant={'outline'}>Team Account</Badge>
</div>
}
>
<AdminDeleteAccountDialog accountId={props.account.id}>
<Button size={'sm'} variant={'destructive'}>
<BadgeX className={'mr-1 h-4'} />
Delete
</Button>
</AdminDeleteAccountDialog>
</PageHeader>
<PageBody>
<div className={'flex flex-col space-y-8'}>
<SubscriptionsTable accountId={props.account.id} />
<Separator />
<div className={'flex flex-col space-y-2.5'}>
<Heading level={6}>This team has the following members:</Heading>
<AdminMembersTable members={members} />
</div>
</div>
</PageBody>
</>
);
}
async function SubscriptionsTable(props: { accountId: string }) {
const client = getSupabaseServerComponentClient({
admin: true,
});
const { data: subscription, error } = await client
.from('subscriptions')
.select('*, subscription_items !inner (*)')
.eq('account_id', props.accountId)
.maybeSingle();
if (error) {
return (
<Alert variant={'destructive'}>
<AlertTitle>There was an error loading subscription.</AlertTitle>
<AlertDescription>
Please check the logs for more information or try again later.
</AlertDescription>
</Alert>
);
}
return (
<div className={'flex flex-col space-y-2.5'}>
<Heading level={6}>Subscription</Heading>
<If
condition={subscription}
fallback={<>This account does not have an active subscription.</>}
>
{(subscription) => {
return (
<div className={'flex flex-col space-y-4'}>
<Table>
<TableHeader>
<TableHead>Subscription ID</TableHead>
<TableHead>Provider</TableHead>
<TableHead>Customer ID</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created At</TableHead>
<TableHead>Period Starts At</TableHead>
<TableHead>Ends At</TableHead>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>
<span>{subscription.id}</span>
</TableCell>
<TableCell>
<span>{subscription.billing_provider}</span>
</TableCell>
<TableCell>
<span>{subscription.billing_customer_id}</span>
</TableCell>
<TableCell>
<span>{subscription.status}</span>
</TableCell>
<TableCell>
<span>{subscription.created_at}</span>
</TableCell>
<TableCell>
<span>{subscription.period_starts_at}</span>
</TableCell>
<TableCell>
<span>{subscription.period_ends_at}</span>
</TableCell>
</TableRow>
</TableBody>
</Table>
<Table>
<TableHeader>
<TableHead>Product ID</TableHead>
<TableHead>Variant ID</TableHead>
<TableHead>Quantity</TableHead>
<TableHead>Price</TableHead>
<TableHead>Interval</TableHead>
<TableHead>Type</TableHead>
</TableHeader>
<TableBody>
{subscription.subscription_items.map((item) => {
return (
<TableRow key={item.variant_id}>
<TableCell>
<span>{item.product_id}</span>
</TableCell>
<TableCell>
<span>{item.variant_id}</span>
</TableCell>
<TableCell>
<span>{item.quantity}</span>
</TableCell>
<TableCell>
<span>{item.price_amount}</span>
</TableCell>
<TableCell>
<span>{item.interval}</span>
</TableCell>
<TableCell>
<span>{item.type}</span>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
}}
</If>
</div>
);
}
async function getMemberships(userId: string) {
const client = getSupabaseServerComponentClient({
admin: true,
});
const memberships = await client
.from('accounts_memberships')
.select<
string,
Membership & {
account: {
id: string;
name: string;
};
}
>('*, account: account_id !inner (id, name)')
.eq('user_id', userId);
if (memberships.error) {
throw memberships.error;
}
return memberships.data;
}
async function getMembers(accountSlug: string) {
const client = getSupabaseServerComponentClient({
admin: true,
});
const members = await client.rpc('get_account_members', {
account_slug: accountSlug,
});
if (members.error) {
throw members.error;
}
return members.data;
}