diff --git a/packages/features/admin/package.json b/packages/features/admin/package.json index 5d16fa729..57194265c 100644 --- a/packages/features/admin/package.json +++ b/packages/features/admin/package.json @@ -11,6 +11,7 @@ "prettier": "@kit/prettier-config", "peerDependencies": { "@hookform/resolvers": "^3.3.4", + "@kit/next": "workspace:*", "@kit/supabase": "workspace:*", "@kit/ui": "workspace:*", "@makerkit/data-loader-supabase-core": "0.0.5", @@ -22,6 +23,7 @@ "devDependencies": { "@hookform/resolvers": "^3.3.4", "@kit/eslint-config": "workspace:*", + "@kit/next": "*", "@kit/prettier-config": "workspace:*", "@kit/supabase": "workspace:^", "@kit/tailwind-config": "workspace:*", diff --git a/packages/features/admin/src/components/admin-account-page.tsx b/packages/features/admin/src/components/admin-account-page.tsx index 743b8c0ce..fbae14cc1 100644 --- a/packages/features/admin/src/components/admin-account-page.tsx +++ b/packages/features/admin/src/components/admin-account-page.tsx @@ -1,10 +1,29 @@ 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']; @@ -21,21 +40,71 @@ export function AdminAccountPage(props: { } 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 ( <> + title={ +
+ + + {props.account.name} + + Personal Account +
+ } + > +
+ + + + + + + + + + + + + + + + + + + +
+
-
- Memberships +
+ -
- +
+ + This user is a member of the following teams: + + +
+ +
@@ -51,17 +120,180 @@ async function TeamAccountPage(props: { return ( <> + title={ +
+ + + {props.account.name} + + Team Account +
+ } + > + + + +
- +
+ + + + +
+ This team has the following members: + + +
+
); } +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 ( + + There was an error loading subscription. + + + Please check the logs for more information or try again later. + + + ); + } + + return ( +
+ Subscription + + This account does not have an active subscription.} + > + {(subscription) => { + return ( +
+ + + Subscription ID + + Provider + + Customer ID + + Status + + Created At + + Period Starts At + + Ends At + + + + + + {subscription.id} + + + + {subscription.billing_provider} + + + + {subscription.billing_customer_id} + + + + {subscription.status} + + + + {subscription.created_at} + + + + {subscription.period_starts_at} + + + + {subscription.period_ends_at} + + + +
+ + + + Product ID + + Variant ID + + Quantity + + Price + + Interval + + Type + + + + {subscription.subscription_items.map((item) => { + return ( + + + {item.product_id} + + + + {item.variant_id} + + + + {item.quantity} + + + + {item.price_amount} + + + + {item.interval} + + + + {item.type} + + + ); + })} + +
+
+ ); + }} +
+
+ ); +} + async function getMemberships(userId: string) { const client = getSupabaseServerComponentClient({ admin: true, diff --git a/packages/features/admin/src/components/admin-accounts-table.tsx b/packages/features/admin/src/components/admin-accounts-table.tsx index 26be21e10..ffc625f84 100644 --- a/packages/features/admin/src/components/admin-accounts-table.tsx +++ b/packages/features/admin/src/components/admin-accounts-table.tsx @@ -33,6 +33,10 @@ import { SelectValue, } from '@kit/ui/select'; +import { AdminDeleteAccountDialog } from './admin-delete-account-dialog'; +import { AdminDeleteUserDialog } from './admin-delete-user-dialog'; +import { AdminImpersonateUserDialog } from './admin-impersonate-user-dialog'; + type Account = Database['public']['Tables']['accounts']['Row']; const FiltersSchema = z.object({ @@ -194,6 +198,7 @@ function getColumns(): ColumnDef[] { header: '', cell: ({ row }) => { const isPersonalAccount = row.original.is_personal_account; + const userId = row.original.id; return ( @@ -208,18 +213,35 @@ function getColumns(): ColumnDef[] { Actions - View + + View + - - Ban - + + e.preventDefault()}> + Impersonate User + + + + + e.preventDefault()}> + Delete Personal Account + + - - Delete - + + + e.preventDefault()}> + Delete Team Account + + + diff --git a/packages/features/admin/src/components/admin-ban-user-dialog.tsx b/packages/features/admin/src/components/admin-ban-user-dialog.tsx new file mode 100644 index 000000000..f7fbe7e51 --- /dev/null +++ b/packages/features/admin/src/components/admin-ban-user-dialog.tsx @@ -0,0 +1,100 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; + +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@kit/ui/alert-dialog'; +import { Button } from '@kit/ui/button'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@kit/ui/form'; +import { Input } from '@kit/ui/input'; + +import { banUser } from '../lib/server/admin-server-actions'; +import { DeleteUserSchema } from '../lib/server/schema/admin-actions.schema'; + +export function AdminBanUserDialog( + props: React.PropsWithChildren<{ + userId: string; + }>, +) { + const form = useForm({ + resolver: zodResolver(DeleteUserSchema), + defaultValues: { + userId: props.userId, + confirmation: '', + }, + }); + + return ( + + {props.children} + + + + Ban User + + + Are you sure you want to ban this user? + + + +
+ + ( + + + Type CONFIRM to confirm + + + + + + + + Are you sure you want to do this? + + + + + )} + /> + + + Cancel + + + + + +
+
+ ); +} diff --git a/packages/features/admin/src/components/admin-delete-account-dialog.tsx b/packages/features/admin/src/components/admin-delete-account-dialog.tsx new file mode 100644 index 000000000..bdce327b0 --- /dev/null +++ b/packages/features/admin/src/components/admin-delete-account-dialog.tsx @@ -0,0 +1,100 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; + +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@kit/ui/alert-dialog'; +import { Button } from '@kit/ui/button'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, +} from '@kit/ui/form'; +import { Input } from '@kit/ui/input'; + +import { deleteAccount } from '../lib/server/admin-server-actions'; +import { DeleteAccountSchema } from '../lib/server/schema/admin-actions.schema'; + +export function AdminDeleteAccountDialog( + props: React.PropsWithChildren<{ + accountId: string; + }>, +) { + const form = useForm({ + resolver: zodResolver(DeleteAccountSchema), + defaultValues: { + accountId: props.accountId, + confirmation: '', + }, + }); + + return ( + + {props.children} + + + + Delete Account + + + Are you sure you want to delete this account? All the data + associated with this account will be permanently deleted. Any active + subscriptions will be canceled. + + + +
+ + ( + + + Type CONFIRM to confirm + + + + + + + + Are you sure you want to do this? This action cannot be + undone. + + + )} + /> + + + Cancel + + + + + +
+
+ ); +} diff --git a/packages/features/admin/src/components/admin-delete-user-dialog.tsx b/packages/features/admin/src/components/admin-delete-user-dialog.tsx new file mode 100644 index 000000000..7223cd106 --- /dev/null +++ b/packages/features/admin/src/components/admin-delete-user-dialog.tsx @@ -0,0 +1,103 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; + +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@kit/ui/alert-dialog'; +import { Button } from '@kit/ui/button'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@kit/ui/form'; +import { Input } from '@kit/ui/input'; + +import { deleteUser } from '../lib/server/admin-server-actions'; +import { DeleteUserSchema } from '../lib/server/schema/admin-actions.schema'; + +export function AdminDeleteUserDialog( + props: React.PropsWithChildren<{ + userId: string; + }>, +) { + const form = useForm({ + resolver: zodResolver(DeleteUserSchema), + defaultValues: { + userId: props.userId, + confirmation: '', + }, + }); + + return ( + + {props.children} + + + + Delete User + + + Are you sure you want to delete this user? All the data associated + with this user will be permanently deleted. Any active subscriptions + will be canceled. + + + +
+ + ( + + + Type CONFIRM to confirm + + + + + + + + Are you sure you want to do this? This action cannot be + undone. + + + + + )} + /> + + + Cancel + + + + + +
+
+ ); +} diff --git a/packages/features/admin/src/components/admin-impersonate-user-dialog.tsx b/packages/features/admin/src/components/admin-impersonate-user-dialog.tsx new file mode 100644 index 000000000..5f0ca2b16 --- /dev/null +++ b/packages/features/admin/src/components/admin-impersonate-user-dialog.tsx @@ -0,0 +1,155 @@ +'use client'; + +import { useState } from 'react'; + +import { useRouter } from 'next/navigation'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useQuery } from '@tanstack/react-query'; +import { useForm } from 'react-hook-form'; + +import { useSupabase } from '@kit/supabase/hooks/use-supabase'; +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@kit/ui/alert-dialog'; +import { Button } from '@kit/ui/button'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@kit/ui/form'; +import { Input } from '@kit/ui/input'; +import { LoadingOverlay } from '@kit/ui/loading-overlay'; + +import { impersonateUser } from '../lib/server/admin-server-actions'; +import { DeleteUserSchema } from '../lib/server/schema/admin-actions.schema'; + +export function AdminImpersonateUserDialog( + props: React.PropsWithChildren<{ + userId: string; + }>, +) { + const form = useForm({ + resolver: zodResolver(DeleteUserSchema), + defaultValues: { + userId: props.userId, + confirmation: '', + }, + }); + + const [tokens, setTokens] = useState<{ + accessToken: string; + refreshToken: string; + }>(); + + if (tokens) { + return ( + <> + + + Setting up your session... + + ); + } + + return ( + + {props.children} + + + + Impersonate User + + + Are you sure you want to impersonate this user? You will be logged + in as this user. To stop impersonating, log out. + + + +
+ { + const tokens = await impersonateUser(data); + + setTokens(tokens); + })} + > + ( + + + Type CONFIRM to confirm + + + + + + + + Are you sure you want to impersonate this user? + + + + + )} + /> + + + Cancel + + + + + +
+
+ ); +} + +function ImpersonateUserAuthSetter({ + tokens, +}: React.PropsWithChildren<{ + tokens: { + accessToken: string; + refreshToken: string; + }; +}>) { + useSetSession(tokens); + + return Setting up your session...; +} + +function useSetSession(tokens: { accessToken: string; refreshToken: string }) { + const supabase = useSupabase(); + const router = useRouter(); + + return useQuery({ + queryKey: ['impersonate-user', tokens.accessToken, tokens.refreshToken], + queryFn: async () => { + await supabase.auth.setSession({ + refresh_token: tokens.refreshToken, + access_token: tokens.accessToken, + }); + + router.push('/home'); + }, + }); +} diff --git a/packages/features/admin/src/components/admin-reactivate-user-dialog.tsx b/packages/features/admin/src/components/admin-reactivate-user-dialog.tsx new file mode 100644 index 000000000..a13e3feb0 --- /dev/null +++ b/packages/features/admin/src/components/admin-reactivate-user-dialog.tsx @@ -0,0 +1,96 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; + +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@kit/ui/alert-dialog'; +import { Button } from '@kit/ui/button'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@kit/ui/form'; +import { Input } from '@kit/ui/input'; + +import { reactivateUser } from '../lib/server/admin-server-actions'; +import { DeleteUserSchema } from '../lib/server/schema/admin-actions.schema'; + +export function AdminReactivateUserDialog( + props: React.PropsWithChildren<{ + userId: string; + }>, +) { + const form = useForm({ + resolver: zodResolver(DeleteUserSchema), + defaultValues: { + userId: props.userId, + confirmation: '', + }, + }); + + return ( + + {props.children} + + + + Reactivate User + + + Are you sure you want to reactivate this user? + + + +
+ + ( + + + Type CONFIRM to confirm + + + + + + + + Are you sure you want to do this? + + + + + )} + /> + + + Cancel + + + + + +
+
+ ); +} diff --git a/packages/features/admin/src/lib/server/admin-server-actions.ts b/packages/features/admin/src/lib/server/admin-server-actions.ts new file mode 100644 index 000000000..c32ab069d --- /dev/null +++ b/packages/features/admin/src/lib/server/admin-server-actions.ts @@ -0,0 +1,127 @@ +'use server'; + +import { revalidatePath } from 'next/cache'; + +import { enhanceAction } from '@kit/next/actions'; +import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; + +import { enhanceAdminAction } from './enhance-admin-action'; +import { + BanUserSchema, + DeleteAccountSchema, + DeleteUserSchema, + ImpersonateUserSchema, + ReactivateUserSchema, +} from './schema/admin-actions.schema'; +import { AdminAccountsService } from './services/admin-accounts.service'; +import { AdminAuthUserService } from './services/admin-auth-user.service'; + +/** + * @name banUser + * @description Ban a user from the system. + */ +export const banUser = enhanceAdminAction( + enhanceAction( + async ({ userId }) => { + const service = getAdminAuthService(); + + await service.banUser(userId); + + revalidateAdmin(); + }, + { + schema: BanUserSchema, + }, + ), +); + +/** + * @name reactivateUser + * @description Reactivate a user in the system. + */ +export const reactivateUser = enhanceAdminAction( + enhanceAction( + async ({ userId }) => { + const service = getAdminAuthService(); + + await service.reactivateUser(userId); + + revalidateAdmin(); + }, + { + schema: ReactivateUserSchema, + }, + ), +); + +/** + * @name impersonateUser + * @description Impersonate a user in the system. + */ +export const impersonateUser = enhanceAdminAction( + enhanceAction( + async ({ userId }) => { + const service = getAdminAuthService(); + + return await service.impersonateUser(userId); + }, + { + schema: ImpersonateUserSchema, + }, + ), +); + +/** + * @name deleteUser + * @description Delete a user from the system. + */ +export const deleteUser = enhanceAdminAction( + enhanceAction( + async ({ userId }) => { + const service = getAdminAuthService(); + + await service.deleteUser(userId); + + revalidateAdmin(); + }, + { + schema: DeleteUserSchema, + }, + ), +); + +/** + * @name deleteAccount + * @description Delete an account from the system. + */ +export const deleteAccount = enhanceAdminAction( + enhanceAction( + async ({ accountId }) => { + const service = getAdminAccountsService(); + + await service.deleteAccount(accountId); + + revalidateAdmin(); + }, + { + schema: DeleteAccountSchema, + }, + ), +); + +function getAdminAuthService() { + const client = getSupabaseServerActionClient(); + const adminClient = getSupabaseServerActionClient({ admin: true }); + + return new AdminAuthUserService(client, adminClient); +} + +function getAdminAccountsService() { + const adminClient = getSupabaseServerActionClient({ admin: true }); + + return new AdminAccountsService(adminClient); +} + +function revalidateAdmin() { + revalidatePath('/admin', 'layout'); +} diff --git a/packages/features/admin/src/lib/server/enhance-admin-action.ts b/packages/features/admin/src/lib/server/enhance-admin-action.ts new file mode 100644 index 000000000..56c733b24 --- /dev/null +++ b/packages/features/admin/src/lib/server/enhance-admin-action.ts @@ -0,0 +1,19 @@ +import { notFound } from 'next/navigation'; + +import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; + +import { isSuperAdmin } from './is-super-admin'; + +export function enhanceAdminAction( + fn: (params: Args) => Response, +) { + return async (params: Args) => { + const isAdmin = await isSuperAdmin(getSupabaseServerActionClient()); + + if (!isAdmin) { + notFound(); + } + + return fn(params); + }; +} diff --git a/packages/features/admin/src/lib/server/schema/admin-actions.schema.ts b/packages/features/admin/src/lib/server/schema/admin-actions.schema.ts new file mode 100644 index 000000000..debf89ce8 --- /dev/null +++ b/packages/features/admin/src/lib/server/schema/admin-actions.schema.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; + +const confirmationSchema = z.object({ + confirmation: z.custom((value) => value === 'CONFIRM'), +}); + +const UserIdSchema = confirmationSchema.extend({ + userId: z.string().uuid(), +}); + +export const BanUserSchema = UserIdSchema; +export const ReactivateUserSchema = UserIdSchema; +export const ImpersonateUserSchema = UserIdSchema; +export const DeleteUserSchema = UserIdSchema; + +export const DeleteAccountSchema = confirmationSchema.extend({ + accountId: z.string().uuid(), +}); diff --git a/packages/features/admin/src/lib/server/services/admin-accounts.service.ts b/packages/features/admin/src/lib/server/services/admin-accounts.service.ts new file mode 100644 index 000000000..1746aab4d --- /dev/null +++ b/packages/features/admin/src/lib/server/services/admin-accounts.service.ts @@ -0,0 +1,18 @@ +import { SupabaseClient } from '@supabase/supabase-js'; + +import { Database } from '@kit/supabase/database'; + +export class AdminAccountsService { + constructor(private adminClient: SupabaseClient) {} + + async deleteAccount(accountId: string) { + const { error } = await this.adminClient + .from('accounts') + .delete() + .eq('id', accountId); + + if (error) { + throw error; + } + } +} diff --git a/packages/features/admin/src/lib/server/services/admin-auth-user.service.ts b/packages/features/admin/src/lib/server/services/admin-auth-user.service.ts new file mode 100644 index 000000000..f441e07e2 --- /dev/null +++ b/packages/features/admin/src/lib/server/services/admin-auth-user.service.ts @@ -0,0 +1,116 @@ +import { SupabaseClient } from '@supabase/supabase-js'; + +import { Database } from '@kit/supabase/database'; + +/** + * @name AdminAuthUserService + * @description Service for performing admin actions on users in the system. + * This service only interacts with the Supabase Auth Admin API. + */ +export class AdminAuthUserService { + constructor( + private readonly client: SupabaseClient, + private readonly adminClient: SupabaseClient, + ) {} + + async assertUserIsNotCurrentSuperAdmin(targetUserId: string) { + const { data: user } = await this.client.auth.getUser(); + const currentUserId = user.user?.id; + + if (!currentUserId) { + throw new Error(`Error fetching user`); + } + + if (currentUserId === targetUserId) { + throw new Error( + `You cannot perform a destructive action on your own account as a Super Admin`, + ); + } + } + + async deleteUser(userId: string) { + await this.assertUserIsNotCurrentSuperAdmin(userId); + + const deleteUserResponse = + await this.adminClient.auth.admin.deleteUser(userId); + + if (deleteUserResponse.error) { + throw new Error(`Error deleting user record or auth record.`); + } + } + + async banUser(userId: string) { + await this.assertUserIsNotCurrentSuperAdmin(userId); + + return this.setBanDuration(userId, `876600h`); + } + + async reactivateUser(userId: string) { + await this.assertUserIsNotCurrentSuperAdmin(userId); + + return this.setBanDuration(userId, `none`); + } + + async impersonateUser(userId: string) { + const { + data: { user }, + error, + } = await this.adminClient.auth.admin.getUserById(userId); + + if (error ?? !user) { + throw new Error(`Error fetching user`); + } + + const email = user.email; + + if (!email) { + throw new Error(`User has no email. Cannot impersonate`); + } + + const { error: linkError, data } = + await this.adminClient.auth.admin.generateLink({ + type: 'magiclink', + email, + options: { + redirectTo: `/`, + }, + }); + + if (linkError ?? !data) { + throw new Error(`Error generating magic link`); + } + + const response = await fetch(data.properties?.action_link, { + method: 'GET', + redirect: 'manual', + }); + + const location = response.headers.get('Location'); + + if (!location) { + throw new Error(`Error generating magic link. Location header not found`); + } + + const hash = new URL(location).hash.substring(1); + const query = new URLSearchParams(hash); + const accessToken = query.get('access_token'); + const refreshToken = query.get('refresh_token'); + + if (!accessToken || !refreshToken) { + throw new Error( + `Error generating magic link. Tokens not found in URL hash.`, + ); + } + + return { + accessToken, + refreshToken, + }; + } + + private async setBanDuration(userId: string, banDuration: string) { + await this.adminClient.auth.admin.updateUserById(userId, { + ban_duration: banDuration, + }); + } +} diff --git a/packages/ui/src/shadcn/form.tsx b/packages/ui/src/shadcn/form.tsx index 335659df3..939e7cc1b 100644 --- a/packages/ui/src/shadcn/form.tsx +++ b/packages/ui/src/shadcn/form.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as React from 'react'; import type * as LabelPrimitive from '@radix-ui/react-label'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19a9c76aa..38796c605 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -191,7 +191,7 @@ importers: version: 18.2.22 autoprefixer: specifier: ^10.4.19 - version: 10.4.19(postcss@8.4.38) + version: 10.4.19(postcss@8.4.33) dotenv-cli: specifier: ^7.4.1 version: 7.4.1 @@ -544,6 +544,9 @@ importers: '@kit/eslint-config': specifier: workspace:* version: link:../../../tooling/eslint + '@kit/next': + specifier: '*' + version: link:../../next '@kit/prettier-config': specifier: workspace:* version: link:../../../tooling/prettier @@ -2032,16 +2035,6 @@ packages: eslint: 8.57.0 eslint-visitor-keys: 3.4.3 - /@eslint-community/eslint-utils@4.4.0(eslint@9.0.0): - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - dependencies: - eslint: 9.0.0 - eslint-visitor-keys: 3.4.3 - dev: false - /@eslint-community/regexpp@4.10.0: resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -2062,32 +2055,10 @@ packages: transitivePeerDependencies: - supports-color - /@eslint/eslintrc@3.0.2: - resolution: {integrity: sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 10.0.1 - globals: 14.0.0 - ignore: 5.3.1 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: false - /@eslint/js@8.57.0: resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - /@eslint/js@9.0.0: - resolution: {integrity: sha512-RThY/MnKrhubF6+s1JflwUjPEsnCEmYCWwqa/aRISKWNXGZ9epUwft4bUMM35SdKF9xvBrLydAM1RDHd1Z//ZQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dev: false - /@fal-works/esbuild-plugin-global-externals@2.1.2: resolution: {integrity: sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==} dev: false @@ -2161,17 +2132,6 @@ packages: transitivePeerDependencies: - supports-color - /@humanwhocodes/config-array@0.12.3: - resolution: {integrity: sha512-jsNnTBlMWuTpDkeE3on7+dWJi0D6fdDfeANj/w7MpS8ztROCoLvIO2nG0CcFj+E4k8j4QrSTh4Oryi3i2G669g==} - engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: false - /@humanwhocodes/module-importer@1.0.1: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -2179,10 +2139,6 @@ packages: /@humanwhocodes/object-schema@2.0.2: resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} - /@humanwhocodes/object-schema@2.0.3: - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - dev: false - /@ianvs/prettier-plugin-sort-imports@4.2.1(prettier@3.2.5): resolution: {integrity: sha512-NKN1LVFWUDGDGr3vt+6Ey3qPeN/163uR1pOPAlkWpgvAqgxQ6kSdUf1F0it8aHUtKRUzEGcK38Wxd07O61d7+Q==} peerDependencies: @@ -6718,23 +6674,6 @@ packages: picocolors: 1.0.0 postcss: 8.4.33 postcss-value-parser: 4.2.0 - dev: false - - /autoprefixer@10.4.19(postcss@8.4.38): - resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - dependencies: - browserslist: 4.23.0 - caniuse-lite: 1.0.30001600 - fraction.js: 4.3.7 - normalize-range: 0.1.2 - picocolors: 1.0.0 - postcss: 8.4.38 - postcss-value-parser: 4.2.0 - dev: true /available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} @@ -7930,13 +7869,13 @@ packages: source-map: 0.6.1 dev: false - /eslint-config-prettier@9.0.0(eslint@9.0.0): + /eslint-config-prettier@9.0.0(eslint@8.57.0): resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 9.0.0 + eslint: 8.57.0 dev: false /eslint-config-prettier@9.1.0(eslint@8.57.0): @@ -7948,13 +7887,13 @@ packages: eslint: 8.57.0 dev: false - /eslint-config-turbo@1.10.12(eslint@9.0.0): + /eslint-config-turbo@1.10.12(eslint@8.57.0): resolution: {integrity: sha512-z3jfh+D7UGYlzMWGh+Kqz++hf8LOE96q3o5R8X4HTjmxaBWlLAWG+0Ounr38h+JLR2TJno0hU9zfzoPNkR9BdA==} peerDependencies: eslint: '>6.6.0' dependencies: - eslint: 9.0.0 - eslint-plugin-turbo: 1.10.12(eslint@9.0.0) + eslint: 8.57.0 + eslint-plugin-turbo: 1.10.12(eslint@8.57.0) dev: false /eslint-config-turbo@1.13.0(eslint@8.57.0): @@ -8076,13 +8015,13 @@ packages: string.prototype.matchall: 4.0.11 dev: false - /eslint-plugin-turbo@1.10.12(eslint@9.0.0): + /eslint-plugin-turbo@1.10.12(eslint@8.57.0): resolution: {integrity: sha512-uNbdj+ohZaYo4tFJ6dStRXu2FZigwulR1b3URPXe0Q8YaE7thuekKNP+54CHtZPH9Zey9dmDx5btAQl9mfzGOw==} peerDependencies: eslint: '>6.6.0' dependencies: dotenv: 16.0.3 - eslint: 9.0.0 + eslint: 8.57.0 dev: false /eslint-plugin-turbo@1.13.0(eslint@8.57.0): @@ -8109,23 +8048,10 @@ packages: esrecurse: 4.3.0 estraverse: 5.3.0 - /eslint-scope@8.0.1: - resolution: {integrity: sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - dev: false - /eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - /eslint-visitor-keys@4.0.0: - resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dev: false - /eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -8172,58 +8098,6 @@ packages: transitivePeerDependencies: - supports-color - /eslint@9.0.0: - resolution: {integrity: sha512-IMryZ5SudxzQvuod6rUdIUz29qFItWx281VhtFVc2Psy/ZhlCeD/5DT6lBIJ4H3G+iamGJoTln1v+QSuPw0p7Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0) - '@eslint-community/regexpp': 4.10.0 - '@eslint/eslintrc': 3.0.2 - '@eslint/js': 9.0.0 - '@humanwhocodes/config-array': 0.12.3 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - escape-string-regexp: 4.0.0 - eslint-scope: 8.0.1 - eslint-visitor-keys: 4.0.0 - espree: 10.0.1 - esquery: 1.5.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - graphemer: 1.4.0 - ignore: 5.3.1 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.3 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - dev: false - - /espree@10.0.1: - resolution: {integrity: sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - acorn: 8.11.3 - acorn-jsx: 5.3.2(acorn@8.11.3) - eslint-visitor-keys: 4.0.0 - dev: false - /espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -8427,13 +8301,6 @@ packages: dependencies: flat-cache: 3.2.0 - /file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - dependencies: - flat-cache: 4.0.1 - dev: false - /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -8463,14 +8330,6 @@ packages: keyv: 4.5.4 rimraf: 3.0.2 - /flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - dependencies: - flatted: 3.3.1 - keyv: 4.5.4 - dev: false - /flat@6.0.1: resolution: {integrity: sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw==} engines: {node: '>=18'} @@ -8742,11 +8601,6 @@ packages: dependencies: type-fest: 0.20.2 - /globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - dev: false - /globalthis@1.0.3: resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} engines: {node: '>= 0.4'} @@ -9897,7 +9751,7 @@ packages: dependencies: marked: 7.0.4 react: 18.2.0 - react-email: 2.1.1(eslint@9.0.0) + react-email: 2.1.1(eslint@8.57.0) dev: false /md-to-react-email@5.0.2(react@18.2.0): @@ -11198,18 +11052,6 @@ packages: read-cache: 1.0.0 resolve: 1.22.8 - /postcss-import@15.1.0(postcss@8.4.35): - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} - engines: {node: '>=14.0.0'} - peerDependencies: - postcss: ^8.0.0 - dependencies: - postcss: 8.4.35 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.8 - dev: false - /postcss-js@4.0.1(postcss@8.4.33): resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} engines: {node: ^12 || ^14 || >= 16} @@ -11219,16 +11061,6 @@ packages: camelcase-css: 2.0.1 postcss: 8.4.33 - /postcss-js@4.0.1(postcss@8.4.35): - resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} - engines: {node: ^12 || ^14 || >= 16} - peerDependencies: - postcss: ^8.4.21 - dependencies: - camelcase-css: 2.0.1 - postcss: 8.4.35 - dev: false - /postcss-load-config@4.0.2(postcss@8.4.33): resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} engines: {node: '>= 14'} @@ -11245,23 +11077,6 @@ packages: postcss: 8.4.33 yaml: 2.4.1 - /postcss-load-config@4.0.2(postcss@8.4.35): - resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} - engines: {node: '>= 14'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - dependencies: - lilconfig: 3.1.1 - postcss: 8.4.35 - yaml: 2.4.1 - dev: false - /postcss-nested@6.0.1(postcss@8.4.33): resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} engines: {node: '>=12.0'} @@ -11271,16 +11086,6 @@ packages: postcss: 8.4.33 postcss-selector-parser: 6.0.16 - /postcss-nested@6.0.1(postcss@8.4.35): - resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.2.14 - dependencies: - postcss: 8.4.35 - postcss-selector-parser: 6.0.16 - dev: false - /postcss-selector-parser@6.0.16: resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==} engines: {node: '>=4'} @@ -11310,20 +11115,11 @@ packages: /postcss@8.4.35: resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==} engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.7 - picocolors: 1.0.0 - source-map-js: 1.0.2 - dev: false - - /postcss@8.4.38: - resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} - engines: {node: ^10 || ^12 || >=14} dependencies: nanoid: 3.3.7 picocolors: 1.0.0 source-map-js: 1.2.0 - dev: true + dev: false /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} @@ -11539,7 +11335,7 @@ packages: react: 18.2.0 scheduler: 0.23.0 - /react-email@2.1.1(eslint@9.0.0): + /react-email@2.1.1(eslint@8.57.0): resolution: {integrity: sha512-09oMVl/jN0/Re0bT0sEqYjyyFSCN/Tg0YmzjC9wfYpnMx02Apk40XXitySDfUBMR9EgTdr6T4lYknACqiLK3mg==} engines: {node: '>=18.0.0'} hasBin: true @@ -11565,8 +11361,8 @@ packages: commander: 11.1.0 debounce: 2.0.0 esbuild: 0.19.11 - eslint-config-prettier: 9.0.0(eslint@9.0.0) - eslint-config-turbo: 1.10.12(eslint@9.0.0) + eslint-config-prettier: 9.0.0(eslint@8.57.0) + eslint-config-turbo: 1.10.12(eslint@8.57.0) framer-motion: 10.17.4(react-dom@18.2.0)(react@18.2.0) glob: 10.3.4 log-symbols: 4.1.0 @@ -12691,7 +12487,7 @@ packages: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 - chokidar: 3.5.3 + chokidar: 3.6.0 didyoumean: 1.2.2 dlv: 1.1.3 fast-glob: 3.3.2 @@ -12703,11 +12499,11 @@ packages: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.0 - postcss: 8.4.35 - postcss-import: 15.1.0(postcss@8.4.35) - postcss-js: 4.0.1(postcss@8.4.35) - postcss-load-config: 4.0.2(postcss@8.4.35) - postcss-nested: 6.0.1(postcss@8.4.35) + postcss: 8.4.33 + postcss-import: 15.1.0(postcss@8.4.33) + postcss-js: 4.0.1(postcss@8.4.33) + postcss-load-config: 4.0.2(postcss@8.4.33) + postcss-nested: 6.0.1(postcss@8.4.33) postcss-selector-parser: 6.0.16 resolve: 1.22.8 sucrase: 3.35.0