diff --git a/apps/web/app/(dashboard)/home/(user)/billing/components/personal-account-checkout-form.tsx b/apps/web/app/(dashboard)/home/(user)/billing/_components/personal-account-checkout-form.tsx similarity index 100% rename from apps/web/app/(dashboard)/home/(user)/billing/components/personal-account-checkout-form.tsx rename to apps/web/app/(dashboard)/home/(user)/billing/_components/personal-account-checkout-form.tsx diff --git a/apps/web/app/(dashboard)/home/(user)/billing/page.tsx b/apps/web/app/(dashboard)/home/(user)/billing/page.tsx index c51007310..8f9460730 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/page.tsx +++ b/apps/web/app/(dashboard)/home/(user)/billing/page.tsx @@ -1,10 +1,9 @@ import { PageBody, PageHeader } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; +import { PersonalAccountCheckoutForm } from '~/(dashboard)/home/(user)/billing/_components/personal-account-checkout-form'; import { withI18n } from '~/lib/i18n/with-i18n'; -import { PersonalAccountCheckoutForm } from './components/personal-account-checkout-form'; - function PersonalAccountBillingPage() { return ( <> diff --git a/apps/web/app/(dashboard)/home/(user)/billing/return/page.tsx b/apps/web/app/(dashboard)/home/(user)/billing/return/page.tsx new file mode 100644 index 000000000..6fbf074e1 --- /dev/null +++ b/apps/web/app/(dashboard)/home/(user)/billing/return/page.tsx @@ -0,0 +1,3 @@ +// We reuse the page from the billing module +// as there is no need to create a new one. +export * from '../return/page'; diff --git a/apps/web/app/(dashboard)/home/(user)/billing/server-actions.ts b/apps/web/app/(dashboard)/home/(user)/billing/server-actions.ts index 03dc682fa..d4c34fb60 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/server-actions.ts +++ b/apps/web/app/(dashboard)/home/(user)/billing/server-actions.ts @@ -82,6 +82,10 @@ export async function createBillingPortalSession() { const customerId = await getCustomerIdFromAccountId(accountId); const returnUrl = getBillingPortalReturnUrl(); + if (!customerId) { + throw new Error('Customer not found'); + } + const { url } = await service.createBillingPortalSession({ customerId, returnUrl, diff --git a/apps/web/app/(dashboard)/home/(user)/layout.tsx b/apps/web/app/(dashboard)/home/(user)/layout.tsx index 067ff8e4b..b659d19f7 100644 --- a/apps/web/app/(dashboard)/home/(user)/layout.tsx +++ b/apps/web/app/(dashboard)/home/(user)/layout.tsx @@ -1,6 +1,6 @@ import { Page } from '@kit/ui/page'; -import { HomeSidebar } from '~/(dashboard)/home/components/home-sidebar'; +import { HomeSidebar } from '~/(dashboard)/home/_components/home-sidebar'; import { withI18n } from '~/lib/i18n/with-i18n'; function UserHomeLayout({ children }: React.PropsWithChildren) { diff --git a/apps/web/app/(dashboard)/home/(user)/account/actions.server.ts b/apps/web/app/(dashboard)/home/(user)/settings/actions.server.ts similarity index 100% rename from apps/web/app/(dashboard)/home/(user)/account/actions.server.ts rename to apps/web/app/(dashboard)/home/(user)/settings/actions.server.ts diff --git a/apps/web/app/(dashboard)/home/(user)/account/layout.tsx b/apps/web/app/(dashboard)/home/(user)/settings/layout.tsx similarity index 100% rename from apps/web/app/(dashboard)/home/(user)/account/layout.tsx rename to apps/web/app/(dashboard)/home/(user)/settings/layout.tsx diff --git a/apps/web/app/(dashboard)/home/(user)/account/loading.tsx b/apps/web/app/(dashboard)/home/(user)/settings/loading.tsx similarity index 100% rename from apps/web/app/(dashboard)/home/(user)/account/loading.tsx rename to apps/web/app/(dashboard)/home/(user)/settings/loading.tsx diff --git a/apps/web/app/(dashboard)/home/(user)/account/page.tsx b/apps/web/app/(dashboard)/home/(user)/settings/page.tsx similarity index 100% rename from apps/web/app/(dashboard)/home/(user)/account/page.tsx rename to apps/web/app/(dashboard)/home/(user)/settings/page.tsx diff --git a/apps/web/app/(dashboard)/home/[account]/(components)/app-header.tsx b/apps/web/app/(dashboard)/home/[account]/_components/app-header.tsx similarity index 92% rename from apps/web/app/(dashboard)/home/[account]/(components)/app-header.tsx rename to apps/web/app/(dashboard)/home/[account]/_components/app-header.tsx index 232c02042..35e49c8e8 100644 --- a/apps/web/app/(dashboard)/home/[account]/(components)/app-header.tsx +++ b/apps/web/app/(dashboard)/home/[account]/_components/app-header.tsx @@ -1,6 +1,6 @@ import { PageHeader } from '@kit/ui/page'; -import { MobileAppNavigation } from '~/(dashboard)/home/[account]/(components)/mobile-app-navigation'; +import { MobileAppNavigation } from '~/(dashboard)/home/[account]/_components/mobile-app-navigation'; export function AppHeader({ children, diff --git a/apps/web/app/(dashboard)/home/[account]/(components)/app-sidebar-navigation.tsx b/apps/web/app/(dashboard)/home/[account]/_components/app-sidebar-navigation.tsx similarity index 100% rename from apps/web/app/(dashboard)/home/[account]/(components)/app-sidebar-navigation.tsx rename to apps/web/app/(dashboard)/home/[account]/_components/app-sidebar-navigation.tsx diff --git a/apps/web/app/(dashboard)/home/[account]/(components)/app-sidebar.tsx b/apps/web/app/(dashboard)/home/[account]/_components/app-sidebar.tsx similarity index 91% rename from apps/web/app/(dashboard)/home/[account]/(components)/app-sidebar.tsx rename to apps/web/app/(dashboard)/home/[account]/_components/app-sidebar.tsx index 80221bab5..6de999cca 100644 --- a/apps/web/app/(dashboard)/home/[account]/(components)/app-sidebar.tsx +++ b/apps/web/app/(dashboard)/home/[account]/_components/app-sidebar.tsx @@ -2,6 +2,8 @@ import { useRouter } from 'next/navigation'; +import type { Session } from '@supabase/supabase-js'; + import { ArrowLeftCircleIcon, ArrowRightCircleIcon } from 'lucide-react'; import { AccountSelector } from '@kit/accounts/account-selector'; @@ -15,7 +17,7 @@ import { import { Trans } from '@kit/ui/trans'; import { cn } from '@kit/ui/utils'; -import { ProfileDropdownContainer } from '~/(dashboard)/home/components/personal-account-dropdown'; +import { ProfileDropdownContainer } from '~/(dashboard)/home/_components/personal-account-dropdown'; import featureFlagsConfig from '~/config/feature-flags.config'; import pathsConfig from '~/config/paths.config'; @@ -36,6 +38,7 @@ export function AppSidebar(props: { account: string; accounts: AccountModel[]; collapsed: boolean; + session: Session | null; }) { return ( @@ -45,6 +48,7 @@ export function AppSidebar(props: { setCollapsed={setCollapsed} account={props.account} accounts={props.accounts} + session={props.session} /> )} @@ -54,6 +58,7 @@ export function AppSidebar(props: { function SidebarContainer(props: { account: string; accounts: AccountModel[]; + session: Session | null; collapsed: boolean; setCollapsed: (collapsed: boolean) => void; }) { @@ -84,7 +89,10 @@ function SidebarContainer(props: {
- + + + + Manage your Team Plan + + + You can change your plan at any time. + + + + +
+ + + + + + Visit the billing portal to manage your subscription (update + payment method, cancel subscription, etc.) + +
+
+
+
+ ); +} diff --git a/apps/web/app/(dashboard)/home/[account]/billing/_components/team-account-checkout-form.tsx b/apps/web/app/(dashboard)/home/[account]/billing/_components/team-account-checkout-form.tsx new file mode 100644 index 000000000..4ece16b65 --- /dev/null +++ b/apps/web/app/(dashboard)/home/[account]/billing/_components/team-account-checkout-form.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { useState, useTransition } from 'react'; + +import { EmbeddedCheckout, PlanPicker } from '@kit/billing-gateway/components'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@kit/ui/card'; + +import billingConfig from '~/config/billing.config'; + +import { createTeamAccountCheckoutSession } from '../server-actions'; + +export function TeamAccountCheckoutForm(params: { accountId: string }) { + const [pending, startTransition] = useTransition(); + const [checkoutToken, setCheckoutToken] = useState(null); + + // If the checkout token is set, render the embedded checkout component + if (checkoutToken) { + return ( + + ); + } + + // Otherwise, render the plan picker component + return ( +
+ + + Manage your Team Plan + + + You can change your plan at any time. + + + + + { + startTransition(async () => { + const { checkoutToken } = + await createTeamAccountCheckoutSession({ + planId, + accountId: params.accountId, + }); + + setCheckoutToken(checkoutToken); + }); + }} + /> + + +
+ ); +} diff --git a/apps/web/app/(dashboard)/home/[account]/billing/page.tsx b/apps/web/app/(dashboard)/home/[account]/billing/page.tsx index 858dd39af..192851602 100644 --- a/apps/web/app/(dashboard)/home/[account]/billing/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/billing/page.tsx @@ -1,9 +1,25 @@ +import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; +import { If } from '@kit/ui/if'; import { PageBody, PageHeader } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; +import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/_lib/load-workspace'; +import { BillingPortalForm } from '~/(dashboard)/home/[account]/billing/_components/billing-portal-form'; import { withI18n } from '~/lib/i18n/with-i18n'; -function OrganizationAccountBillingPage() { +import { TeamAccountCheckoutForm } from './_components/team-account-checkout-form'; + +interface Params { + params: { + account: string; + }; +} + +async function OrganizationAccountBillingPage({ params }: Params) { + const workspace = await loadOrganizationWorkspace(params.account); + const accountId = workspace.account.id; + const customerId = await loadCustomerIdFromAccount(accountId); + return ( <> } /> - + + + + + + + ); } export default withI18n(OrganizationAccountBillingPage); + +async function loadCustomerIdFromAccount(accountId: string) { + const client = getSupabaseServerComponentClient(); + + const { data, error } = await client + .from('billing_customers') + .select('customer_id') + .eq('account_id', accountId) + .maybeSingle(); + + if (error) { + throw error; + } + + return data?.customer_id; +} diff --git a/apps/web/app/(dashboard)/home/[account]/billing/return/page.tsx b/apps/web/app/(dashboard)/home/[account]/billing/return/page.tsx new file mode 100644 index 000000000..a0443bdb2 --- /dev/null +++ b/apps/web/app/(dashboard)/home/[account]/billing/return/page.tsx @@ -0,0 +1,84 @@ +import dynamic from 'next/dynamic'; +import { notFound } from 'next/navigation'; + +import { getBillingGatewayProvider } from '@kit/billing-gateway'; +import { BillingSessionStatus } from '@kit/billing-gateway/components'; +import { requireAuth } from '@kit/supabase/require-auth'; +import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; + +import billingConfig from '~/config/billing.config'; +import pathsConfig from '~/config/paths.config'; +import { withI18n } from '~/lib/i18n/with-i18n'; + +interface SessionPageProps { + searchParams: { + session_id: string; + }; +} + +const LazyEmbeddedCheckout = dynamic(async () => { + const { EmbeddedCheckout } = await import('@kit/billing-gateway/components'); + + return EmbeddedCheckout; +}); + +async function ReturnStripeSessionPage({ searchParams }: SessionPageProps) { + const { customerEmail, checkoutToken } = await loadCheckoutSession( + searchParams.session_id, + ); + + if (checkoutToken) { + return ( + + ); + } + + return ( + <> +
+ +
+ +
+ + ); +} + +export default withI18n(ReturnStripeSessionPage); + +export async function loadCheckoutSession(sessionId: string) { + const client = getSupabaseServerComponentClient(); + + await requireAuth(client); + + const gateway = await getBillingGatewayProvider(client); + + const session = await gateway.retrieveCheckoutSession({ + sessionId, + }); + + if (!session) { + notFound(); + } + + const checkoutToken = session.isSessionOpen ? session.checkoutToken : null; + + // otherwise - we show the user the return page + // and display the details of the session + return { + status: session.status, + customerEmail: session.customer.email, + checkoutToken, + }; +} diff --git a/apps/web/app/(dashboard)/home/[account]/billing/server-actions.ts b/apps/web/app/(dashboard)/home/[account]/billing/server-actions.ts new file mode 100644 index 000000000..65f32d1d4 --- /dev/null +++ b/apps/web/app/(dashboard)/home/[account]/billing/server-actions.ts @@ -0,0 +1,179 @@ +'use server'; + +import { headers } from 'next/headers'; +import { redirect } from 'next/navigation'; + +import { z } from 'zod'; + +import { getProductPlanPairFromId } from '@kit/billing'; +import { getBillingGatewayProvider } from '@kit/billing-gateway'; +import { requireAuth } from '@kit/supabase/require-auth'; +import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; + +import billingConfig from '~/config/billing.config'; +import pathsConfig from '~/config/paths.config'; + +/** + * Creates a checkout session for a team account. + * + * @param {object} params - The parameters for creating the checkout session. + * @param {string} params.planId - The ID of the plan to be associated with the account. + */ +export async function createTeamAccountCheckoutSession(params: { + planId: string; + accountId: string; +}) { + const client = getSupabaseServerActionClient(); + + // we parse the plan ID from the parameters + // no need in continuing if the plan ID is not valid + const planId = z.string().min(1).parse(params.planId); + + // we require the user to be authenticated + const { data: session } = await requireAuth(client); + + if (!session) { + throw new Error('Authentication required'); + } + + const userId = session.user.id; + const accountId = params.accountId; + + const hasPermission = await getPermissionsForAccountId(userId, accountId); + + // if the user does not have permission to manage billing for the account + // then we should not proceed + if (!hasPermission) { + throw new Error('Permission denied'); + } + + // here we have confirmed that the user has permission to manage billing for the account + // so we go on and create a checkout session + const service = await getBillingGatewayProvider(client); + const productPlanPairFromId = getProductPlanPairFromId(billingConfig, planId); + + if (!productPlanPairFromId) { + throw new Error('Product not found'); + } + + // the return URL for the checkout session + const returnUrl = getCheckoutSessionReturnUrl(); + + // find the customer ID for the account if it exists + // (eg. if the account has been billed before) + const customerId = await getCustomerIdFromAccountId(client, accountId); + const customerEmail = session.user.email; + + // retrieve the product and plan from the billing configuration + const { product, plan } = productPlanPairFromId; + + // call the payment gateway to create the checkout session + const { checkoutToken } = await service.createCheckoutSession({ + accountId, + returnUrl, + planId, + customerEmail, + customerId, + paymentType: product.paymentType, + trialPeriodDays: plan.trialPeriodDays, + }); + + // return the checkout token to the client + // so we can call the payment gateway to complete the checkout + return { + checkoutToken, + }; +} + +export async function createBillingPortalSession(data: FormData) { + const client = getSupabaseServerActionClient(); + + const accountId = z + .object({ + accountId: z.string().min(1), + }) + .parse(Object.fromEntries(data)).accountId; + + const { data: session, error } = await requireAuth(client); + + if (error ?? !session) { + throw new Error('Authentication required'); + } + + const userId = session.user.id; + + // we require the user to have permissions to manage billing for the account + const hasPermission = await getPermissionsForAccountId(userId, accountId); + + // if the user does not have permission to manage billing for the account + // then we should not proceed + if (!hasPermission) { + throw new Error('Permission denied'); + } + + const service = await getBillingGatewayProvider(client); + const customerId = await getCustomerIdFromAccountId(client, accountId); + const returnUrl = getBillingPortalReturnUrl(); + + if (!customerId) { + throw new Error('Customer not found'); + } + + const { url } = await service.createBillingPortalSession({ + customerId, + returnUrl, + }); + + // redirect the user to the billing portal + return redirect(url); +} + +function getCheckoutSessionReturnUrl() { + const origin = headers().get('origin')!; + + return new URL(pathsConfig.app.accountBillingReturn, origin).toString(); +} + +function getBillingPortalReturnUrl() { + const origin = headers().get('origin')!; + + return new URL(pathsConfig.app.accountBilling, origin).toString(); +} + +/** + * Retrieves the permissions for a user on an account for managing billing. + * @param userId + * @param accountId + */ +async function getPermissionsForAccountId(userId: string, accountId: string) { + const client = getSupabaseServerActionClient(); + + const { data, error } = await client.rpc('has_permission', { + account_id: accountId, + user_id: userId, + permission_name: 'billing.manage', + }); + + if (error) { + throw error; + } + + return data; +} + +async function getCustomerIdFromAccountId( + client: ReturnType, + accountId: string, +) { + const { data, error } = await client + .from('billing_customers') + .select('customer_id') + .eq('account_id', accountId) + .maybeSingle(); + + if (error) { + throw error; + } + + return data?.customer_id; +} diff --git a/apps/web/app/(dashboard)/home/[account]/layout.tsx b/apps/web/app/(dashboard)/home/[account]/layout.tsx index 83a76c332..aa9f341a6 100644 --- a/apps/web/app/(dashboard)/home/[account]/layout.tsx +++ b/apps/web/app/(dashboard)/home/[account]/layout.tsx @@ -1,37 +1,45 @@ +import { use } from 'react'; + import { parseSidebarStateCookie } from '@kit/shared/cookies/sidebar-state.cookie'; import { parseThemeCookie } from '@kit/shared/cookies/theme.cookie'; +import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { Page } from '@kit/ui/page'; +import { AppSidebar } from '~/(dashboard)/home/[account]/_components/app-sidebar'; +import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/_lib/load-workspace'; import { withI18n } from '~/lib/i18n/with-i18n'; -import { AppSidebar } from './(components)/app-sidebar'; -import { loadOrganizationWorkspace } from './(lib)/load-workspace'; - interface Params { account: string; } -async function OrganizationWorkspaceLayout({ +function OrganizationWorkspaceLayout({ children, params, }: React.PropsWithChildren<{ params: Params; }>) { - const data = await loadOrganizationWorkspace(params.account); + const [data, session] = use( + Promise.all([loadOrganizationWorkspace(params.account), loadSession()]), + ); + const ui = getUIStateCookies(); const sidebarCollapsed = ui.sidebarState === 'collapsed'; + const accounts = data.accounts.map(({ name, slug, picture_url }) => ({ + label: name, + value: slug, + image: picture_url, + })); + return ( ({ - label: name, - value: slug, - image: picture_url, - }))} + session={session} + accounts={accounts} /> } > @@ -48,3 +56,18 @@ function getUIStateCookies() { sidebarState: parseSidebarStateCookie(), }; } + +async function loadSession() { + const client = getSupabaseServerComponentClient(); + + const { + data: { session }, + error, + } = await client.auth.getSession(); + + if (error) { + throw error; + } + + return session; +} diff --git a/apps/web/app/(dashboard)/home/[account]/members/page.tsx b/apps/web/app/(dashboard)/home/[account]/members/page.tsx index db207f8e9..01e5644b9 100644 --- a/apps/web/app/(dashboard)/home/[account]/members/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/members/page.tsx @@ -1,4 +1,4 @@ -import { PlusCircledIcon } from '@radix-ui/react-icons'; +import { PlusCircleIcon } from 'lucide-react'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { @@ -17,7 +17,7 @@ import { import { PageBody, PageHeader } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; -import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/(lib)/load-workspace'; +import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/_lib/load-workspace'; import { withI18n } from '~/lib/i18n/with-i18n'; interface Params { @@ -99,7 +99,7 @@ async function OrganizationAccountMembersPage({ params }: Params) { diff --git a/apps/web/app/(dashboard)/home/[account]/page.tsx b/apps/web/app/(dashboard)/home/[account]/page.tsx index 239328a39..33bc58cf1 100644 --- a/apps/web/app/(dashboard)/home/[account]/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/page.tsx @@ -7,11 +7,11 @@ import { PageBody } from '@kit/ui/page'; import Spinner from '@kit/ui/spinner'; import { Trans } from '@kit/ui/trans'; -import { AppHeader } from '~/(dashboard)/home/[account]/(components)/app-header'; +import { AppHeader } from '~/(dashboard)/home/[account]/_components/app-header'; import { withI18n } from '~/lib/i18n/with-i18n'; const DashboardDemo = loadDynamic( - () => import('~/(dashboard)/home/[account]/(components)/dashboard-demo'), + () => import('~/(dashboard)/home/[account]/_components/dashboard-demo'), { ssr: false, loading: () => ( diff --git a/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/components/recover-checkout.tsx b/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/components/recover-checkout.tsx deleted file mode 100644 index 03fbff1a6..000000000 --- a/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/components/recover-checkout.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import dynamic from 'next/dynamic'; -import { useRouter } from 'next/navigation'; - -const EmbeddedStripeCheckout = dynamic( - () => { - return import('../../components/embedded-stripe-checkout'); - }, - { - ssr: false, - }, -); - -function RecoverCheckout({ clientSecret }: { clientSecret: string }) { - const router = useRouter(); - - return ( - { - return router.replace('/settings/subscription'); - }} - /> - ); -} - -export default RecoverCheckout; diff --git a/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/page.tsx b/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/page.tsx deleted file mode 100644 index 0ea55ff44..000000000 --- a/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { notFound, redirect } from 'next/navigation'; - -import requireSession from '@/lib/user/require-session'; - -import { withI18n } from '@packages/i18n/with-i18n'; -import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; - -import createStripeClient from '@kit/stripe/get-stripe'; - -import { BillingSessionStatus } from './components/billing-session-status'; -import RecoverCheckout from './components/recover-checkout'; - -interface SessionPageProps { - searchParams: { - session_id: string; - }; -} - -async function ReturnStripeSessionPage({ searchParams }: SessionPageProps) { - const { status, customerEmail, clientSecret } = await loadStripeSession( - searchParams.session_id, - ); - - if (clientSecret) { - return ; - } - - return ( - <> -
- -
- -
- - ); -} - -export default withI18n(ReturnStripeSessionPage); - -export async function loadStripeSession(sessionId: string) { - await requireSession(getSupabaseServerComponentClient()); - - // now we fetch the session from Stripe - // and check if it's still open - const stripe = await createStripeClient(); - - const session = await stripe.checkout.sessions - .retrieve(sessionId) - .catch(() => undefined); - - if (!session) { - notFound(); - } - - const isSessionOpen = session.status === 'open'; - const clientSecret = isSessionOpen ? session.client_secret : null; - const isEmbeddedMode = session.ui_mode === 'embedded'; - - // if the session is still open, we redirect the user to the checkout page - // in Stripe self hosted mode - if (isSessionOpen && !isEmbeddedMode && session.url) { - redirect(session.url); - } - - // otherwise - we show the user the return page - // and display the details of the session - return { - status: session.status, - customerEmail: session.customer_details?.email, - clientSecret, - }; -} diff --git a/apps/web/app/(dashboard)/home/components/home-sidebar-account-selector.tsx b/apps/web/app/(dashboard)/home/_components/home-sidebar-account-selector.tsx similarity index 100% rename from apps/web/app/(dashboard)/home/components/home-sidebar-account-selector.tsx rename to apps/web/app/(dashboard)/home/_components/home-sidebar-account-selector.tsx diff --git a/apps/web/app/(dashboard)/home/components/home-sidebar.tsx b/apps/web/app/(dashboard)/home/_components/home-sidebar.tsx similarity index 67% rename from apps/web/app/(dashboard)/home/components/home-sidebar.tsx rename to apps/web/app/(dashboard)/home/_components/home-sidebar.tsx index 8912b9d60..80bb61a6f 100644 --- a/apps/web/app/(dashboard)/home/components/home-sidebar.tsx +++ b/apps/web/app/(dashboard)/home/_components/home-sidebar.tsx @@ -5,13 +5,16 @@ import { cookies } from 'next/headers'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { Sidebar, SidebarContent, SidebarNavigation } from '@kit/ui/sidebar'; -import { HomeSidebarAccountSelector } from '~/(dashboard)/home/components/home-sidebar-account-selector'; -import { ProfileDropdownContainer } from '~/(dashboard)/home/components/personal-account-dropdown'; +import { HomeSidebarAccountSelector } from '~/(dashboard)/home/_components/home-sidebar-account-selector'; +import { ProfileDropdownContainer } from '~/(dashboard)/home/_components/personal-account-dropdown'; import { personalAccountSidebarConfig } from '~/config/personal-account-sidebar.config'; export function HomeSidebar() { const collapsed = getSidebarCollapsed(); - const accounts = use(loadUserAccounts()); + + const [accounts, session] = use( + Promise.all([loadUserAccounts(), loadSession()]), + ); return ( @@ -25,7 +28,7 @@ export function HomeSidebar() {
- +
@@ -36,12 +39,27 @@ function getSidebarCollapsed() { return cookies().get('sidebar-collapsed')?.value === 'true'; } +async function loadSession() { + const client = getSupabaseServerComponentClient(); + + const { + data: { session }, + error, + } = await client.auth.getSession(); + + if (error) { + throw error; + } + + return session; +} + async function loadUserAccounts() { const client = getSupabaseServerComponentClient(); const { data: accounts, error } = await client .from('user_accounts') - .select('*'); + .select(`name, slug, picture_url`); if (error) { throw error; diff --git a/apps/web/app/(dashboard)/home/components/personal-account-dropdown.tsx b/apps/web/app/(dashboard)/home/_components/personal-account-dropdown.tsx similarity index 68% rename from apps/web/app/(dashboard)/home/components/personal-account-dropdown.tsx rename to apps/web/app/(dashboard)/home/_components/personal-account-dropdown.tsx index c111125d2..deffa2ed4 100644 --- a/apps/web/app/(dashboard)/home/components/personal-account-dropdown.tsx +++ b/apps/web/app/(dashboard)/home/_components/personal-account-dropdown.tsx @@ -1,15 +1,17 @@ 'use client'; +import type { Session } from '@supabase/supabase-js'; + import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown'; import { useSignOut } from '@kit/supabase/hooks/use-sign-out'; -import { useUserSession } from '@kit/supabase/hooks/use-user-session'; import pathsConfig from '~/config/paths.config'; -export function ProfileDropdownContainer(props: { collapsed: boolean }) { - const userSession = useUserSession(); +export function ProfileDropdownContainer(props: { + collapsed: boolean; + session: Session | null; +}) { const signOut = useSignOut(); - const session = userSession?.data ?? undefined; return (
@@ -19,7 +21,7 @@ export function ProfileDropdownContainer(props: { collapsed: boolean }) { }} className={'w-full'} showProfileName={!props.collapsed} - session={session} + session={props.session} signOutRequested={() => signOut.mutateAsync()} />
diff --git a/apps/web/app/(marketing)/components/grid-list.tsx b/apps/web/app/(marketing)/_components/grid-list.tsx similarity index 100% rename from apps/web/app/(marketing)/components/grid-list.tsx rename to apps/web/app/(marketing)/_components/grid-list.tsx diff --git a/apps/web/app/(marketing)/components/site-footer.tsx b/apps/web/app/(marketing)/_components/site-footer.tsx similarity index 100% rename from apps/web/app/(marketing)/components/site-footer.tsx rename to apps/web/app/(marketing)/_components/site-footer.tsx diff --git a/apps/web/app/(marketing)/_components/site-header-account-section.tsx b/apps/web/app/(marketing)/_components/site-header-account-section.tsx new file mode 100644 index 000000000..e313b0cff --- /dev/null +++ b/apps/web/app/(marketing)/_components/site-header-account-section.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { Suspense } from 'react'; + +import Link from 'next/link'; + +import type { Session } from '@supabase/supabase-js'; + +import { ChevronRightIcon } from 'lucide-react'; + +import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown'; +import { useSignOut } from '@kit/supabase/hooks/use-sign-out'; +import { useUserSession } from '@kit/supabase/hooks/use-user-session'; +import { Button } from '@kit/ui/button'; +import { If } from '@kit/ui/if'; +import { Trans } from '@kit/ui/trans'; + +import pathsConfig from '~/config/paths.config'; + +export function SiteHeaderAccountSection( + props: React.PropsWithChildren<{ + session: Session | null; + }>, +) { + return ( + }> + + + ); +} + +function SuspendedPersonalAccountDropdown(props: { session: Session | null }) { + const signOut = useSignOut(); + const userSession = useUserSession(props.session); + + return ( + }> + {(session) => ( + signOut.mutateAsync()} + /> + )} + + ); +} + +function AuthButtons() { + return ( +
+ + + + + +
+ ); +} diff --git a/apps/web/app/(marketing)/components/site-header.tsx b/apps/web/app/(marketing)/_components/site-header.tsx similarity index 63% rename from apps/web/app/(marketing)/components/site-header.tsx rename to apps/web/app/(marketing)/_components/site-header.tsx index 47426d6ef..cdccb4fb5 100644 --- a/apps/web/app/(marketing)/components/site-header.tsx +++ b/apps/web/app/(marketing)/_components/site-header.tsx @@ -1,8 +1,10 @@ -import { SiteHeaderAccountSection } from '~/(marketing)/components/site-header-account-section'; -import { SiteNavigation } from '~/(marketing)/components/site-navigation'; +import type { Session } from '@supabase/supabase-js'; + +import { SiteHeaderAccountSection } from '~/(marketing)/_components/site-header-account-section'; +import { SiteNavigation } from '~/(marketing)/_components/site-navigation'; import { AppLogo } from '~/components/app-logo'; -export function SiteHeader() { +export async function SiteHeader(props: { session: Session | null }) { return (
@@ -17,7 +19,7 @@ export function SiteHeader() {
- +
diff --git a/apps/web/app/(marketing)/components/site-navigation.tsx b/apps/web/app/(marketing)/_components/site-navigation.tsx similarity index 100% rename from apps/web/app/(marketing)/components/site-navigation.tsx rename to apps/web/app/(marketing)/_components/site-navigation.tsx diff --git a/apps/web/app/(marketing)/components/site-page-header.tsx b/apps/web/app/(marketing)/_components/site-page-header.tsx similarity index 100% rename from apps/web/app/(marketing)/components/site-page-header.tsx rename to apps/web/app/(marketing)/_components/site-page-header.tsx diff --git a/apps/web/app/(marketing)/blog/[slug]/page.tsx b/apps/web/app/(marketing)/blog/[slug]/page.tsx index 32f0989c8..581e77720 100644 --- a/apps/web/app/(marketing)/blog/[slug]/page.tsx +++ b/apps/web/app/(marketing)/blog/[slug]/page.tsx @@ -5,11 +5,10 @@ import Script from 'next/script'; import { allPosts } from 'contentlayer/generated'; +import Post from '~/(marketing)/blog/_components/post'; import appConfig from '~/config/app.config'; import { withI18n } from '~/lib/i18n/with-i18n'; -import Post from '../components/post'; - export async function generateMetadata({ params, }: { diff --git a/apps/web/app/(marketing)/blog/components/cover-image.tsx b/apps/web/app/(marketing)/blog/_components/cover-image.tsx similarity index 100% rename from apps/web/app/(marketing)/blog/components/cover-image.tsx rename to apps/web/app/(marketing)/blog/_components/cover-image.tsx diff --git a/apps/web/app/(marketing)/blog/components/date-formatter.tsx b/apps/web/app/(marketing)/blog/_components/date-formatter.tsx similarity index 100% rename from apps/web/app/(marketing)/blog/components/date-formatter.tsx rename to apps/web/app/(marketing)/blog/_components/date-formatter.tsx diff --git a/apps/web/app/(marketing)/blog/components/draft-post-badge.tsx b/apps/web/app/(marketing)/blog/_components/draft-post-badge.tsx similarity index 100% rename from apps/web/app/(marketing)/blog/components/draft-post-badge.tsx rename to apps/web/app/(marketing)/blog/_components/draft-post-badge.tsx diff --git a/apps/web/app/(marketing)/blog/components/post-header.tsx b/apps/web/app/(marketing)/blog/_components/post-header.tsx similarity index 86% rename from apps/web/app/(marketing)/blog/components/post-header.tsx rename to apps/web/app/(marketing)/blog/_components/post-header.tsx index 516ab71bc..45c207520 100644 --- a/apps/web/app/(marketing)/blog/components/post-header.tsx +++ b/apps/web/app/(marketing)/blog/_components/post-header.tsx @@ -3,10 +3,10 @@ import type { Post } from 'contentlayer/generated'; import { Heading } from '@kit/ui/heading'; import { If } from '@kit/ui/if'; -import { CoverImage } from '~/(marketing)/blog/components/cover-image'; -import { DateFormatter } from '~/(marketing)/blog/components/date-formatter'; +import { CoverImage } from '~/(marketing)/blog/_components/cover-image'; +import { DateFormatter } from '~/(marketing)/blog/_components/date-formatter'; -const PostHeader: React.FC<{ +export const PostHeader: React.FC<{ post: Post; }> = ({ post }) => { const { title, date, readingTime, description, image } = post; @@ -53,5 +53,3 @@ const PostHeader: React.FC<{
); }; - -export default PostHeader; diff --git a/apps/web/app/(marketing)/blog/components/post-preview.tsx b/apps/web/app/(marketing)/blog/_components/post-preview.tsx similarity index 89% rename from apps/web/app/(marketing)/blog/components/post-preview.tsx rename to apps/web/app/(marketing)/blog/_components/post-preview.tsx index 3b42e786b..8c113f50c 100644 --- a/apps/web/app/(marketing)/blog/components/post-preview.tsx +++ b/apps/web/app/(marketing)/blog/_components/post-preview.tsx @@ -4,8 +4,8 @@ import type { Post } from 'contentlayer/generated'; import { If } from '@kit/ui/if'; -import { CoverImage } from '~/(marketing)/blog/components/cover-image'; -import { DateFormatter } from '~/(marketing)/blog/components/date-formatter'; +import { CoverImage } from '~/(marketing)/blog/_components/cover-image'; +import { DateFormatter } from '~/(marketing)/blog/_components/date-formatter'; type Props = { post: Post; @@ -15,7 +15,7 @@ type Props = { const DEFAULT_IMAGE_HEIGHT = 250; -function PostPreview({ +export function PostPreview({ post, preloadImage, imageHeight, @@ -67,5 +67,3 @@ function PostPreview({
); } - -export default PostPreview; diff --git a/apps/web/app/(marketing)/blog/components/post.tsx b/apps/web/app/(marketing)/blog/_components/post.tsx similarity index 91% rename from apps/web/app/(marketing)/blog/components/post.tsx rename to apps/web/app/(marketing)/blog/_components/post.tsx index 54135fb19..72449b758 100644 --- a/apps/web/app/(marketing)/blog/components/post.tsx +++ b/apps/web/app/(marketing)/blog/_components/post.tsx @@ -4,7 +4,7 @@ import type { Post as PostType } from 'contentlayer/generated'; import { Mdx } from '@kit/ui/mdx'; -import PostHeader from './post-header'; +import { PostHeader } from './post-header'; export const Post: React.FC<{ post: PostType; diff --git a/apps/web/app/(marketing)/blog/page.tsx b/apps/web/app/(marketing)/blog/page.tsx index ff1f37569..89af584f1 100644 --- a/apps/web/app/(marketing)/blog/page.tsx +++ b/apps/web/app/(marketing)/blog/page.tsx @@ -2,19 +2,18 @@ import type { Metadata } from 'next'; import { allPosts } from 'contentlayer/generated'; -import PostPreview from '~/(marketing)/blog/components/post-preview'; -import { SitePageHeader } from '~/(marketing)/components/site-page-header'; +import { GridList } from '~/(marketing)/_components/grid-list'; +import { SitePageHeader } from '~/(marketing)/_components/site-page-header'; +import { PostPreview } from '~/(marketing)/blog/_components/post-preview'; import appConfig from '~/config/app.config'; import { withI18n } from '~/lib/i18n/with-i18n'; -import { GridList } from '../components/grid-list'; - export const metadata: Metadata = { title: `Blog - ${appConfig.name}`, description: `Tutorials, Guides and Updates from our team`, }; -async function BlogPage() { +function BlogPage() { const livePosts = allPosts.filter((post) => { const isProduction = appConfig.production; diff --git a/apps/web/app/(marketing)/components/site-header-account-section.tsx b/apps/web/app/(marketing)/components/site-header-account-section.tsx deleted file mode 100644 index 8870bd103..000000000 --- a/apps/web/app/(marketing)/components/site-header-account-section.tsx +++ /dev/null @@ -1,48 +0,0 @@ -'use client'; - -import Link from 'next/link'; - -import { ChevronRightIcon } from 'lucide-react'; - -import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown'; -import { useSignOut } from '@kit/supabase/hooks/use-sign-out'; -import { useUserSession } from '@kit/supabase/hooks/use-user-session'; -import { Button } from '@kit/ui/button'; - -import pathsConfig from '~/config/paths.config'; - -export function SiteHeaderAccountSection() { - const signOut = useSignOut(); - const userSession = useUserSession(); - - if (userSession.data) { - return ( - signOut.mutateAsync()} - /> - ); - } - - return ; -} - -function AuthButtons() { - return ( -
- - - - - -
- ); -} diff --git a/apps/web/app/(marketing)/docs/[...slug]/page.tsx b/apps/web/app/(marketing)/docs/[...slug]/page.tsx index e7a1b1323..0a4223cae 100644 --- a/apps/web/app/(marketing)/docs/[...slug]/page.tsx +++ b/apps/web/app/(marketing)/docs/[...slug]/page.tsx @@ -8,10 +8,10 @@ import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; import { If } from '@kit/ui/if'; import { Mdx } from '@kit/ui/mdx'; -import { SitePageHeader } from '~/(marketing)/components/site-page-header'; -import { DocsCards } from '~/(marketing)/docs/components/docs-cards'; -import { DocumentationPageLink } from '~/(marketing)/docs/components/documentation-page-link'; -import { getDocumentationPageTree } from '~/(marketing)/docs/utils/get-documentation-page-tree'; +import { SitePageHeader } from '~/(marketing)/_components/site-page-header'; +import { DocsCards } from '~/(marketing)/docs/_components/docs-cards'; +import { DocumentationPageLink } from '~/(marketing)/docs/_components/documentation-page-link'; +import { getDocumentationPageTree } from '~/(marketing)/docs/_lib/get-documentation-page-tree'; import { withI18n } from '~/lib/i18n/with-i18n'; const getPageBySlug = cache((slug: string) => { diff --git a/apps/web/app/(marketing)/docs/components/docs-card.tsx b/apps/web/app/(marketing)/docs/_components/docs-card.tsx similarity index 100% rename from apps/web/app/(marketing)/docs/components/docs-card.tsx rename to apps/web/app/(marketing)/docs/_components/docs-card.tsx diff --git a/apps/web/app/(marketing)/docs/components/docs-cards.tsx b/apps/web/app/(marketing)/docs/_components/docs-cards.tsx similarity index 100% rename from apps/web/app/(marketing)/docs/components/docs-cards.tsx rename to apps/web/app/(marketing)/docs/_components/docs-cards.tsx diff --git a/apps/web/app/(marketing)/docs/components/docs-navigation.tsx b/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx similarity index 98% rename from apps/web/app/(marketing)/docs/components/docs-navigation.tsx rename to apps/web/app/(marketing)/docs/_components/docs-navigation.tsx index b76fdefc7..1a9452662 100644 --- a/apps/web/app/(marketing)/docs/components/docs-navigation.tsx +++ b/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx @@ -13,7 +13,7 @@ import { Heading } from '@kit/ui/heading'; import { If } from '@kit/ui/if'; import { cn } from '@kit/ui/utils'; -import type { ProcessedDocumentationPage } from '../utils/build-documentation-tree'; +import type { ProcessedDocumentationPage } from '~/(marketing)/docs/_lib/build-documentation-tree'; const DocsNavLink: React.FC<{ label: string; diff --git a/apps/web/app/(marketing)/docs/components/documentation-page-link.tsx b/apps/web/app/(marketing)/docs/_components/documentation-page-link.tsx similarity index 100% rename from apps/web/app/(marketing)/docs/components/documentation-page-link.tsx rename to apps/web/app/(marketing)/docs/_components/documentation-page-link.tsx diff --git a/apps/web/app/(marketing)/docs/utils/build-documentation-tree.ts b/apps/web/app/(marketing)/docs/_lib/build-documentation-tree.ts similarity index 100% rename from apps/web/app/(marketing)/docs/utils/build-documentation-tree.ts rename to apps/web/app/(marketing)/docs/_lib/build-documentation-tree.ts diff --git a/apps/web/app/(marketing)/docs/utils/get-documentation-page-tree.ts b/apps/web/app/(marketing)/docs/_lib/get-documentation-page-tree.ts similarity index 100% rename from apps/web/app/(marketing)/docs/utils/get-documentation-page-tree.ts rename to apps/web/app/(marketing)/docs/_lib/get-documentation-page-tree.ts diff --git a/apps/web/app/(marketing)/docs/layout.tsx b/apps/web/app/(marketing)/docs/layout.tsx index 18d0c2727..204352079 100644 --- a/apps/web/app/(marketing)/docs/layout.tsx +++ b/apps/web/app/(marketing)/docs/layout.tsx @@ -1,8 +1,8 @@ import type { DocumentationPage } from 'contentlayer/generated'; import { allDocumentationPages } from 'contentlayer/generated'; -import DocsNavigation from './components/docs-navigation'; -import { buildDocumentationTree } from './utils/build-documentation-tree'; +import DocsNavigation from '~/(marketing)/docs/_components/docs-navigation'; +import { buildDocumentationTree } from '~/(marketing)/docs/_lib/build-documentation-tree'; function DocsLayout({ children }: React.PropsWithChildren) { const tree = buildDocumentationTree(allDocumentationPages); diff --git a/apps/web/app/(marketing)/docs/page.tsx b/apps/web/app/(marketing)/docs/page.tsx index 265448225..8455dea81 100644 --- a/apps/web/app/(marketing)/docs/page.tsx +++ b/apps/web/app/(marketing)/docs/page.tsx @@ -1,12 +1,11 @@ import { allDocumentationPages } from 'contentlayer/generated'; +import { SitePageHeader } from '~/(marketing)/_components/site-page-header'; +import { DocsCards } from '~/(marketing)/docs/_components/docs-cards'; +import { buildDocumentationTree } from '~/(marketing)/docs/_lib/build-documentation-tree'; import appConfig from '~/config/app.config'; import { withI18n } from '~/lib/i18n/with-i18n'; -import { SitePageHeader } from '../components/site-page-header'; -import { DocsCards } from './components/docs-cards'; -import { buildDocumentationTree } from './utils/build-documentation-tree'; - export const metadata = { title: `Documentation - ${appConfig.name}`, }; diff --git a/apps/web/app/(marketing)/faq/page.tsx b/apps/web/app/(marketing)/faq/page.tsx index c4c62d7b7..312feeab5 100644 --- a/apps/web/app/(marketing)/faq/page.tsx +++ b/apps/web/app/(marketing)/faq/page.tsx @@ -1,9 +1,8 @@ import { ChevronDownIcon } from 'lucide-react'; +import { SitePageHeader } from '~/(marketing)/_components/site-page-header'; import { withI18n } from '~/lib/i18n/with-i18n'; -import { SitePageHeader } from '../components/site-page-header'; - export const metadata = { title: 'FAQ', }; diff --git a/apps/web/app/(marketing)/layout.tsx b/apps/web/app/(marketing)/layout.tsx index 584451173..b5d4546c6 100644 --- a/apps/web/app/(marketing)/layout.tsx +++ b/apps/web/app/(marketing)/layout.tsx @@ -1,12 +1,19 @@ +import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; + +import { SiteFooter } from '~/(marketing)/_components/site-footer'; +import { SiteHeader } from '~/(marketing)/_components/site-header'; import { withI18n } from '~/lib/i18n/with-i18n'; -import { SiteFooter } from './components/site-footer'; -import { SiteHeader } from './components/site-header'; +async function SiteLayout(props: React.PropsWithChildren) { + const client = getSupabaseServerComponentClient(); + + const { + data: { session }, + } = await client.auth.getSession(); -function SiteLayout(props: React.PropsWithChildren) { return ( <> - + {props.children} diff --git a/apps/web/app/(marketing)/pricing/page.tsx b/apps/web/app/(marketing)/pricing/page.tsx index df15bbd1d..e9474bce3 100644 --- a/apps/web/app/(marketing)/pricing/page.tsx +++ b/apps/web/app/(marketing)/pricing/page.tsx @@ -1,11 +1,10 @@ import { PricingTable } from '@kit/billing/components/pricing-table'; +import { SitePageHeader } from '~/(marketing)/_components/site-page-header'; import billingConfig from '~/config/billing.config'; import pathsConfig from '~/config/paths.config'; import { withI18n } from '~/lib/i18n/with-i18n'; -import { SitePageHeader } from '../components/site-page-header'; - export const metadata = { title: 'Pricing', }; diff --git a/apps/web/app/admin/layout.tsx b/apps/web/app/admin/layout.tsx deleted file mode 100644 index 271a666ca..000000000 --- a/apps/web/app/admin/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { headers } from 'next/headers'; -import { notFound } from 'next/navigation'; - -import { Page } from '@/components/app/Page'; - -import AdminSidebar from '../../packages/admin/components/AdminSidebar'; -import isUserSuperAdmin from './utils/is-user-super-admin'; - -async function AdminLayout({ children }: React.PropsWithChildren) { - const isAdmin = await isUserSuperAdmin(); - - if (!isAdmin) { - notFound(); - } - - const csrfToken = headers().get('X-CSRF-Token'); - - return }>{children}; -} - -export default AdminLayout; diff --git a/apps/web/app/admin/lib/actions-utils.ts b/apps/web/app/admin/lib/actions-utils.ts deleted file mode 100644 index fd4d68fbe..000000000 --- a/apps/web/app/admin/lib/actions-utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { notFound } from 'next/navigation'; - -import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; - -import isUserSuperAdmin from '~/admin/utils/is-user-super-admin'; - -export function withAdminSession( - fn: (...params: Args) => Response, -) { - return async (...params: Args) => { - const isAdmin = await isUserSuperAdmin({ - client: getSupabaseServerActionClient(), - }); - - if (!isAdmin) { - notFound(); - } - - return fn(...params); - }; -} diff --git a/apps/web/app/admin/loading.tsx b/apps/web/app/admin/loading.tsx deleted file mode 100644 index 4ea53181d..000000000 --- a/apps/web/app/admin/loading.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { GlobalLoader } from '@kit/ui/global-loader'; - -export default GlobalLoader; diff --git a/apps/web/app/admin/organizations/@modal/[uid]/actions.server.ts b/apps/web/app/admin/organizations/@modal/[uid]/actions.server.ts deleted file mode 100644 index 043de7538..000000000 --- a/apps/web/app/admin/organizations/@modal/[uid]/actions.server.ts +++ /dev/null @@ -1,29 +0,0 @@ -'use server'; - -import { revalidatePath } from 'next/cache'; -import { redirect } from 'next/navigation'; - -import { Logger } from '@kit/shared/logger'; -import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; - -import { withAdminSession } from '~/admin/lib/actions-utils'; - -const getClient = () => getSupabaseServerActionClient({ admin: true }); - -export const deleteOrganizationAction = withAdminSession( - async ({ id }: { id: number; csrfToken: string }) => { - const client = getClient(); - - Logger.info({ id }, `Admin requested to delete Organization`); - - await deleteOrganization(client, { - organizationId: id, - }); - - revalidatePath('/admin/organizations', 'page'); - - Logger.info({ id }, `Organization account deleted`); - - redirect('/admin/organizations'); - }, -); diff --git a/apps/web/app/admin/organizations/@modal/[uid]/components/DeleteOrganizationModal.tsx b/apps/web/app/admin/organizations/@modal/[uid]/components/DeleteOrganizationModal.tsx deleted file mode 100644 index ec1d00673..000000000 --- a/apps/web/app/admin/organizations/@modal/[uid]/components/DeleteOrganizationModal.tsx +++ /dev/null @@ -1,95 +0,0 @@ -'use client'; - -import { useState, useTransition } from 'react'; - -import { useRouter } from 'next/navigation'; - -import type Organization from '@/lib/organizations/types/organization'; - -import useCsrfToken from '@kit/hooks/use-csrf-token'; -import { Button } from '@kit/ui/button'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from '@kit/ui/dialog'; -import { Input } from '@kit/ui/input'; -import { Label } from '@kit/ui/label'; - -import { deleteOrganizationAction } from '../actions.server'; - -function DeleteOrganizationModal({ - organization, -}: React.PropsWithChildren<{ - organization: Organization; -}>) { - const router = useRouter(); - const [isOpen, setIsOpen] = useState(true); - const [pending, startTransition] = useTransition(); - const csrfToken = useCsrfToken(); - - const onDismiss = () => { - router.back(); - - setIsOpen(false); - }; - - const onConfirm = () => { - startTransition(async () => { - await deleteOrganizationAction({ - id: organization.id, - csrfToken, - }); - - onDismiss(); - }); - }; - - return ( - - - - Deleting Organization - - -
-
-
-

- You are about to delete the organization{' '} - {organization.name}. -

- -

- Delete this organization will potentially delete the data - associated with it. -

- -

- This action is not reversible. -

- -

Are you sure you want to do this?

-
- -
- -
- -
- -
-
-
-
-
- ); -} - -export default DeleteOrganizationModal; diff --git a/apps/web/app/admin/organizations/@modal/[uid]/delete/page.tsx b/apps/web/app/admin/organizations/@modal/[uid]/delete/page.tsx deleted file mode 100644 index c3c63899f..000000000 --- a/apps/web/app/admin/organizations/@modal/[uid]/delete/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { getOrganizationByUid } from '@/lib/organizations/database/queries'; - -import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; - -import AdminGuard from '../../../../../../packages/admin/components/AdminGuard'; -import DeleteOrganizationModal from '../components/DeleteOrganizationModal'; - -interface Params { - params: { - uid: string; - }; -} - -async function DeleteOrganizationModalPage({ params }: Params) { - const client = getSupabaseServerComponentClient({ admin: true }); - const { data, error } = await getOrganizationByUid(client, params.uid); - - if (!data || error) { - throw new Error(`Organization not found`); - } - - return ; -} - -export default AdminGuard(DeleteOrganizationModalPage); diff --git a/apps/web/app/admin/organizations/@modal/default.tsx b/apps/web/app/admin/organizations/@modal/default.tsx deleted file mode 100644 index 6ddf1b76f..000000000 --- a/apps/web/app/admin/organizations/@modal/default.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Default() { - return null; -} diff --git a/apps/web/app/admin/organizations/[uid]/members/components/OrganizationsMembersTable.tsx b/apps/web/app/admin/organizations/[uid]/members/components/OrganizationsMembersTable.tsx deleted file mode 100644 index 4ac0a5e55..000000000 --- a/apps/web/app/admin/organizations/[uid]/members/components/OrganizationsMembersTable.tsx +++ /dev/null @@ -1,137 +0,0 @@ -'use client'; - -import Link from 'next/link'; -import { usePathname, useRouter } from 'next/navigation'; - -import { DataTable } from '@/components/app/DataTable'; -import type Membership from '@/lib/organizations/types/membership'; -import type { ColumnDef } from '@tanstack/react-table'; -import { EllipsisVerticalIcon } from 'lucide-react'; - -import type UserData from '@kit/session/types/user-data'; -import { Button } from '@kit/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@kit/ui/dropdown-menu'; - -import RoleBadge from '../../../../../(app)/[account]/account/organization/components/RoleBadge'; - -type Data = { - id: Membership['id']; - role: Membership['role']; - user: { - id: UserData['id']; - displayName: UserData['displayName']; - }; -}; - -const columns: ColumnDef[] = [ - { - header: 'Membership ID', - id: 'id', - accessorKey: 'id', - }, - { - header: 'User ID', - id: 'user-id', - cell: ({ row }) => { - const userId = row.original.user.id; - - return ( - - {userId} - - ); - }, - }, - { - header: 'Name', - id: 'name', - accessorKey: 'user.displayName', - }, - { - header: 'Role', - cell: ({ row }) => { - return ( -
- -
- ); - }, - }, - { - header: 'Actions', - cell: ({ row }) => { - const membership = row.original; - const userId = membership.user.id; - - return ( -
- - - - - - - - View User - - - - - Impersonate User - - - - -
- ); - }, - }, -]; - -function OrganizationsMembersTable({ - memberships, - page, - perPage, - pageCount, -}: React.PropsWithChildren<{ - memberships: Data[]; - page: number; - perPage: number; - pageCount: number; -}>) { - const data = memberships.filter((membership) => { - return membership.user; - }); - - const router = useRouter(); - const path = usePathname(); - - return ( - { - const { pathname } = new URL(path, window.location.origin); - const page = pageIndex + 1; - - router.push(pathname + '?page=' + page); - }} - pageCount={pageCount} - pageIndex={page - 1} - pageSize={perPage} - columns={columns} - data={data} - /> - ); -} - -export default OrganizationsMembersTable; diff --git a/apps/web/app/admin/organizations/[uid]/members/page.tsx b/apps/web/app/admin/organizations/[uid]/members/page.tsx deleted file mode 100644 index 7fdc0b022..000000000 --- a/apps/web/app/admin/organizations/[uid]/members/page.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { use } from 'react'; - -import Link from 'next/link'; - -import { PageBody } from '@/components/app/Page'; -import appConfig from '@/config/app.config'; -import { ChevronRightIcon } from 'lucide-react'; - -import AdminHeader from '@packages/admin/components/AdminHeader'; -import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; - -import getPageFromQueryParams from '../../../utils/get-page-from-query-param'; -import { getMembershipsByOrganizationUid } from '../../queries'; -import OrganizationsMembersTable from './components/OrganizationsMembersTable'; - -interface AdminMembersPageParams { - params: { - uid: string; - }; - - searchParams: { - page?: string; - }; -} - -export const metadata = { - title: `Members | ${appConfig.name}`, -}; - -function AdminMembersPage(params: AdminMembersPageParams) { - const adminClient = getSupabaseServerComponentClient({ admin: true }); - const uid = params.params.uid; - const perPage = 20; - const page = getPageFromQueryParams(params.searchParams.page); - - const { data: memberships, count } = use( - getMembershipsByOrganizationUid(adminClient, { uid, page, perPage }), - ); - - const pageCount = count ? Math.ceil(count / perPage) : 0; - - return ( -
- Manage Members - - -
- - - -
-
-
- ); -} - -export default AdminMembersPage; - -function Breadcrumbs() { - return ( -
-
- Admin -
- - - - Organizations - - - - Members -
- ); -} diff --git a/apps/web/app/admin/organizations/default.tsx b/apps/web/app/admin/organizations/default.tsx deleted file mode 100644 index 6ddf1b76f..000000000 --- a/apps/web/app/admin/organizations/default.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Default() { - return null; -} diff --git a/apps/web/app/admin/organizations/error.tsx b/apps/web/app/admin/organizations/error.tsx deleted file mode 100644 index 8f64c6fd4..000000000 --- a/apps/web/app/admin/organizations/error.tsx +++ /dev/null @@ -1,21 +0,0 @@ -'use client'; - -import { PageBody } from '@/components/app/Page'; - -import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; - -function OrganizationsAdminPageError() { - return ( - - - Could not load organizations - - There was an error loading the organizations. Please check your - console errors. - - - - ); -} - -export default OrganizationsAdminPageError; diff --git a/apps/web/app/admin/organizations/layout.tsx b/apps/web/app/admin/organizations/layout.tsx deleted file mode 100644 index 36c4ec27e..000000000 --- a/apps/web/app/admin/organizations/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -function OrganizationsLayout( - props: React.PropsWithChildren<{ - modal: React.ReactNode; - }>, -) { - return ( - <> - {props.children} - {props.modal} - - ); -} - -export default OrganizationsLayout; diff --git a/apps/web/app/admin/organizations/page.tsx b/apps/web/app/admin/organizations/page.tsx deleted file mode 100644 index d0aea73f3..000000000 --- a/apps/web/app/admin/organizations/page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { PageBody } from '@/components/app/Page'; -import appConfig from '@/config/app.config'; -import AdminGuard from '@/packages/admin/components/AdminGuard'; -import AdminHeader from '@/packages/admin/components/AdminHeader'; - -import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; - -import { Input } from '@kit/ui/input'; - -import OrganizationsTable from './components/OrganizationsTable'; -import { getOrganizations } from './queries'; - -interface OrganizationsAdminPageProps { - searchParams: { - page?: string; - search?: string; - }; -} - -export const metadata = { - title: `Organizations | ${appConfig.name}`, -}; - -async function OrganizationsAdminPage({ - searchParams, -}: OrganizationsAdminPageProps) { - const page = searchParams.page ? parseInt(searchParams.page, 10) : 1; - const client = getSupabaseServerComponentClient({ admin: true }); - const perPage = 10; - const search = searchParams.search || ''; - - const { organizations, count } = await getOrganizations( - client, - search, - page, - perPage, - ); - - const pageCount = count ? Math.ceil(count / perPage) : 0; - - return ( -
- Manage Organizations - - -
-
- -
- - -
-
-
- ); -} - -export default AdminGuard(OrganizationsAdminPage); diff --git a/apps/web/app/admin/organizations/queries.ts b/apps/web/app/admin/organizations/queries.ts deleted file mode 100644 index b96f79544..000000000 --- a/apps/web/app/admin/organizations/queries.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { SupabaseClient } from '@supabase/supabase-js'; - -import type { Database } from '@/database.types'; -import { MEMBERSHIPS_TABLE, ORGANIZATIONS_TABLE } from '@/lib/db-tables'; -import type { UserOrganizationData } from '@/lib/organizations/database/queries'; -import type MembershipRole from '@/lib/organizations/types/membership-role'; - -type Client = SupabaseClient; - -export async function getOrganizations( - client: Client, - search: string, - page = 1, - perPage = 20, -) { - const startOffset = (page - 1) * perPage; - const endOffset = startOffset - 1 + perPage; - - let query = client.from(ORGANIZATIONS_TABLE).select< - string, - UserOrganizationData['organization'] & { - memberships: { - userId: string; - role: MembershipRole; - code: string; - }[]; - } - >( - ` - id, - uuid, - name, - logoURL: logo_url, - memberships ( - userId: user_id, - role, - code - ), - subscription: organizations_subscriptions ( - customerId: customer_id, - data: subscription_id ( - id, - status, - currency, - interval, - cancelAtPeriodEnd: cancel_at_period_end, - intervalCount: interval_count, - priceId: price_id, - createdAt: created_at, - periodStartsAt: period_starts_at, - periodEndsAt: period_ends_at, - trialStartsAt: trial_starts_at, - trialEndsAt: trial_ends_at - ) - )`, - { - count: 'exact', - }, - ); - - if (search) { - query = query.ilike('name', `%${search}%`); - } - - const { - data: organizations, - count, - error, - } = await query.range(startOffset, endOffset); - - if (error) { - throw error; - } - - return { - organizations, - count, - }; -} - -export async function getMembershipsByOrganizationUid( - client: Client, - params: { - uid: string; - page: number; - perPage: number; - }, -) { - const startOffset = (params.page - 1) * params.perPage; - const endOffset = startOffset + params.perPage; - - const { data, error, count } = await client - .from(MEMBERSHIPS_TABLE) - .select< - string, - { - id: number; - role: MembershipRole; - user: { - id: string; - displayName: string; - photoURL: string; - }; - } - >( - ` - id, - role, - user: user_id ( - id, - displayName: display_name, - photoURL: photo_url - ), - organization: organization_id !inner ( - id, - uuid - )`, - { - count: 'exact', - }, - ) - .eq('organization.uuid', params.uid) - .is('code', null) - .range(startOffset, endOffset); - - if (error) { - throw error; - } - - return { data, count }; -} diff --git a/apps/web/app/admin/page.tsx b/apps/web/app/admin/page.tsx deleted file mode 100644 index 85c73b695..000000000 --- a/apps/web/app/admin/page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { PageBody } from '@/components/app/Page'; -import appConfig from '@/config/app.config'; - -import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; - -import AdminDashboard from '../../packages/admin/components/AdminDashboard'; -import AdminGuard from '../../packages/admin/components/AdminGuard'; -import AdminHeader from '../../packages/admin/components/AdminHeader'; - -export const metadata = { - title: `Admin | ${appConfig.name}`, -}; - -async function AdminPage() { - const data = await loadData(); - - return ( -
- Admin - - - - -
- ); -} - -export default AdminGuard(AdminPage); - -async function loadData() { - const client = getSupabaseServerComponentClient({ admin: true }); - - const { count: usersCount } = await client.from('users').select('*', { - count: 'exact', - head: true, - }); - - const { count: organizationsCount } = await client - .from('organizations') - .select('*', { - count: 'exact', - head: true, - }); - - const { count: activeSubscriptions } = await client - .from('subscriptions') - .select(`*`, { - count: 'exact', - head: true, - }) - .eq('status', 'active'); - - const { count: trialSubscriptions } = await client - .from('subscriptions') - .select(`*`, { - count: 'exact', - head: true, - }) - .eq('status', 'trialing'); - - return { - usersCount: usersCount || 0, - organizationsCount: organizationsCount || 0, - activeSubscriptions: activeSubscriptions || 0, - trialSubscriptions: trialSubscriptions || 0, - }; -} diff --git a/apps/web/app/admin/users/@modal/[uid]/actions.server.ts b/apps/web/app/admin/users/@modal/[uid]/actions.server.ts deleted file mode 100644 index 2b98d771c..000000000 --- a/apps/web/app/admin/users/@modal/[uid]/actions.server.ts +++ /dev/null @@ -1,127 +0,0 @@ -'use server'; - -import { revalidatePath } from 'next/cache'; -import { redirect } from 'next/navigation'; - -import { Logger } from '@kit/shared/logger'; -import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; - -import { withAdminSession } from '~/admin/lib/actions-utils'; - -const getClient = () => getSupabaseServerActionClient({ admin: true }); - -export const banUser = withAdminSession(async ({ userId }) => { - await setBanDuration(userId, `876600h`); -}); - -export const reactivateUser = withAdminSession(async ({ userId }) => { - await setBanDuration(userId, `none`); -}); - -export const impersonateUser = withAdminSession(async ({ userId }) => { - await assertUserIsNotCurrentSuperAdmin(userId); - - const client = getClient(); - - const { - data: { user }, - error, - } = await client.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 getClient().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, - }; -}); - -export const deleteUserAction = withAdminSession( - async ({ userId }: { userId: string; csrfToken: string }) => { - await assertUserIsNotCurrentSuperAdmin(userId); - - Logger.info({ userId }, `Admin requested to delete user account`); - - // we don't want to send an email to the user - const sendEmail = false; - - await deleteUser({ - client: getClient(), - userId, - sendEmail, - }); - - revalidatePath('/admin/users', 'page'); - - Logger.info({ userId }, `User account deleted`); - - redirect('/admin/users'); - }, -); - -async function setBanDuration(userId: string, banDuration: string) { - await assertUserIsNotCurrentSuperAdmin(userId); - - await getClient().auth.admin.updateUserById(userId, { - ban_duration: banDuration, - }); - - revalidatePath('/admin/users'); -} - -async function assertUserIsNotCurrentSuperAdmin(targetUserId: string) { - const { data: user } = await getSupabaseServerActionClient().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`, - ); - } -} diff --git a/apps/web/app/admin/users/@modal/[uid]/ban/page.tsx b/apps/web/app/admin/users/@modal/[uid]/ban/page.tsx deleted file mode 100644 index 7c5ab37a1..000000000 --- a/apps/web/app/admin/users/@modal/[uid]/ban/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { use } from 'react'; - -import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; - -import AdminGuard from '../../../../../../packages/admin/components/AdminGuard'; -import BanUserModal from '../components/BanUserModal'; - -interface Params { - params: { - uid: string; - }; -} - -function BanUserModalPage({ params }: Params) { - const client = getSupabaseServerComponentClient({ admin: true }); - const { data, error } = use(client.auth.admin.getUserById(params.uid)); - - if (!data || error) { - throw new Error(`User not found`); - } - - const user = data.user; - const isBanned = 'banned_until' in user && user.banned_until !== 'none'; - - if (isBanned) { - throw new Error(`The user is already banned`); - } - - return ; -} - -export default AdminGuard(BanUserModalPage); diff --git a/apps/web/app/admin/users/@modal/[uid]/components/BanUserModal.tsx b/apps/web/app/admin/users/@modal/[uid]/components/BanUserModal.tsx deleted file mode 100644 index 676cd9bf5..000000000 --- a/apps/web/app/admin/users/@modal/[uid]/components/BanUserModal.tsx +++ /dev/null @@ -1,111 +0,0 @@ -'use client'; - -import { useState } from 'react'; - -import { useFormStatus } from 'react-dom'; - -import { useRouter } from 'next/navigation'; - -import type { User } from '@supabase/gotrue-js'; - -import ErrorBoundary from '@/components/app/ErrorBoundary'; - -import useCsrfToken from '@kit/hooks/use-csrf-token'; -import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; -import { Button } from '@kit/ui/button'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from '@kit/ui/dialog'; -import { Input } from '@kit/ui/input'; -import { Label } from '@kit/ui/label'; - -import { banUser } from '../actions.server'; - -function BanUserModal({ - user, -}: React.PropsWithChildren<{ - user: User; -}>) { - const router = useRouter(); - const [isOpen, setIsOpen] = useState(true); - const csrfToken = useCsrfToken(); - const displayText = user.email ?? user.phone ?? ''; - - const onDismiss = () => { - router.back(); - - setIsOpen(false); - }; - - const onConfirm = async () => { - await banUser({ - userId: user.id, - csrfToken, - }); - - onDismiss(); - }; - - return ( - - - - Ban User - - }> -
-
-
-

- You are about to ban {displayText}. -

- -

- You can unban them later, but they will not be able to log - in or use their account until you do. -

- - - -

Are you sure you want to do this?

-
- -
- -
-
-
-
-
-
-
- ); -} - -function SubmitButton() { - const { pending } = useFormStatus(); - - return ( - - ); -} - -export default BanUserModal; - -function BanErrorAlert() { - return ( - - There was an error banning this user. - - Check the logs for more information. - - ); -} diff --git a/apps/web/app/admin/users/@modal/[uid]/components/DeleteUserModal.tsx b/apps/web/app/admin/users/@modal/[uid]/components/DeleteUserModal.tsx deleted file mode 100644 index ca0e1d0f4..000000000 --- a/apps/web/app/admin/users/@modal/[uid]/components/DeleteUserModal.tsx +++ /dev/null @@ -1,96 +0,0 @@ -'use client'; - -import { useState, useTransition } from 'react'; - -import { useRouter } from 'next/navigation'; - -import type { User } from '@supabase/gotrue-js'; - -import useCsrfToken from '@kit/hooks/use-csrf-token'; -import { Button } from '@kit/ui/button'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from '@kit/ui/dialog'; -import { Input } from '@kit/ui/input'; -import { Label } from '@kit/ui/label'; - -import { deleteUserAction } from '../actions.server'; - -function DeleteUserModal({ - user, -}: React.PropsWithChildren<{ - user: User; -}>) { - const router = useRouter(); - const [isOpen, setIsOpen] = useState(true); - const [pending, startTransition] = useTransition(); - const csrfToken = useCsrfToken(); - const displayText = user.email ?? user.phone ?? ''; - - const onDismiss = () => { - router.back(); - - setIsOpen(false); - }; - - const onConfirm = () => { - startTransition(async () => { - await deleteUserAction({ - userId: user.id, - csrfToken, - }); - - onDismiss(); - }); - }; - - return ( - - - - Deleting User - - -
-
-
-

- You are about to delete the user {displayText}. -

- -

- Delete this user will also delete the organizations they are a - Owner of, and potentially the data associated with those - organizations. -

- -

- This action is not reversible. -

- -

Are you sure you want to do this?

-
- -
- -
- -
- -
-
-
-
-
- ); -} - -export default DeleteUserModal; diff --git a/apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserAuthSetter.tsx b/apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserAuthSetter.tsx deleted file mode 100644 index 70cfd8cfc..000000000 --- a/apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserAuthSetter.tsx +++ /dev/null @@ -1,52 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; - -import { useRouter } from 'next/navigation'; - -import Spinner from '@/components/app/Spinner'; - -import useSupabase from '@kit/hooks/use-supabase'; - -function ImpersonateUserAuthSetter({ - tokens, -}: React.PropsWithChildren<{ - tokens: { - accessToken: string; - refreshToken: string; - }; -}>) { - const supabase = useSupabase(); - const router = useRouter(); - - useEffect(() => { - async function setAuth() { - await supabase.auth.setSession({ - refresh_token: tokens.refreshToken, - access_token: tokens.accessToken, - }); - - router.push('/dashboard'); - } - - void setAuth(); - }, [router, tokens, supabase.auth]); - - return ( -
-
- - -
-

Setting up your session...

-
-
-
- ); -} - -export default ImpersonateUserAuthSetter; diff --git a/apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserConfirmationModal.tsx b/apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserConfirmationModal.tsx deleted file mode 100644 index 4a2713ce3..000000000 --- a/apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserConfirmationModal.tsx +++ /dev/null @@ -1,128 +0,0 @@ -'use client'; - -import { useState, useTransition } from 'react'; - -import { useRouter } from 'next/navigation'; - -import type { User } from '@supabase/gotrue-js'; - -import If from '@/components/app/If'; -import LoadingOverlay from '@/components/app/LoadingOverlay'; - -import useCsrfToken from '@kit/hooks/use-csrf-token'; -import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; -import { Button } from '@kit/ui/button'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from '@kit/ui/dialog'; - -import { impersonateUser } from '../actions.server'; -import ImpersonateUserAuthSetter from '../components/ImpersonateUserAuthSetter'; - -function ImpersonateUserConfirmationModal({ - user, -}: React.PropsWithChildren<{ - user: User; -}>) { - const router = useRouter(); - const [isOpen, setIsOpen] = useState(true); - const [pending, startTransition] = useTransition(); - const csrfToken = useCsrfToken(); - const [error, setError] = useState(); - - const [tokens, setTokens] = useState<{ - accessToken: string; - refreshToken: string; - }>(); - - const displayText = user.email ?? user.phone ?? ''; - - const onDismiss = () => { - router.back(); - - setIsOpen(false); - }; - - const onConfirm = () => { - startTransition(async () => { - try { - const response = await impersonateUser({ - userId: user.id, - csrfToken, - }); - - setTokens(response); - } catch (e) { - setError(true); - } - }); - }; - - return ( - - - - Impersonate User - - - - {(tokens) => { - return ( - <> - - - Setting up your session... - - ); - }} - - - - - Impersonation Error - - Sorry, something went wrong. Please check the logs. - - - - - -
-
-

- You are about to impersonate the account belonging to{' '} - {displayText} with ID {user.id}. -

- -

- You will be able to log in as them, see and do everything they - can. To return to your own account, simply log out. -

- -

- Like Uncle Ben said, with great power comes great - responsibility. Use this power wisely. -

-
- -
- -
-
-
-
-
- ); -} - -export default ImpersonateUserConfirmationModal; diff --git a/apps/web/app/admin/users/@modal/[uid]/components/ReactivateUserModal.tsx b/apps/web/app/admin/users/@modal/[uid]/components/ReactivateUserModal.tsx deleted file mode 100644 index 10611be1a..000000000 --- a/apps/web/app/admin/users/@modal/[uid]/components/ReactivateUserModal.tsx +++ /dev/null @@ -1,76 +0,0 @@ -'use client'; - -import { useState, useTransition } from 'react'; - -import { useRouter } from 'next/navigation'; - -import type { User } from '@supabase/gotrue-js'; - -import useCsrfToken from '@kit/hooks/use-csrf-token'; -import { Button } from '@kit/ui/button'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from '@kit/ui/dialog'; - -import { reactivateUser } from '../actions.server'; - -function ReactivateUserModal({ - user, -}: React.PropsWithChildren<{ - user: User; -}>) { - const router = useRouter(); - const [isOpen, setIsOpen] = useState(true); - const [pending, startTransition] = useTransition(); - const csrfToken = useCsrfToken(); - const displayText = user.email ?? user.phone ?? ''; - - const onDismiss = () => { - router.back(); - - setIsOpen(false); - }; - - const onConfirm = () => { - startTransition(async () => { - await reactivateUser({ - userId: user.id, - csrfToken, - }); - - onDismiss(); - }); - }; - - return ( - - - - Reactivate User - - -
-
-

- You are about to reactivate the account belonging to{' '} - {displayText}. -

- -

Are you sure you want to do this?

-
- -
- -
-
-
-
- ); -} - -export default ReactivateUserModal; diff --git a/apps/web/app/admin/users/@modal/[uid]/delete/page.tsx b/apps/web/app/admin/users/@modal/[uid]/delete/page.tsx deleted file mode 100644 index 6c3f04396..000000000 --- a/apps/web/app/admin/users/@modal/[uid]/delete/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { use } from 'react'; - -import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; - -import AdminGuard from '../../../../../../packages/admin/components/AdminGuard'; -import DeleteUserModal from '../components/DeleteUserModal'; - -interface Params { - params: { - uid: string; - }; -} - -function DeleteUserModalPage({ params }: Params) { - const client = getSupabaseServerComponentClient({ admin: true }); - const { data, error } = use(client.auth.admin.getUserById(params.uid)); - - if (!data || error) { - throw new Error(`User not found`); - } - - return ; -} - -export default AdminGuard(DeleteUserModalPage); diff --git a/apps/web/app/admin/users/@modal/[uid]/impersonate/page.tsx b/apps/web/app/admin/users/@modal/[uid]/impersonate/page.tsx deleted file mode 100644 index dcd0856ff..000000000 --- a/apps/web/app/admin/users/@modal/[uid]/impersonate/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { use } from 'react'; - -import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; - -import AdminGuard from '../../../../../../packages/admin/components/AdminGuard'; -import ImpersonateUserConfirmationModal from '../components/ImpersonateUserConfirmationModal'; - -interface Params { - params: { - uid: string; - }; -} - -function ImpersonateUserModalPage({ params }: Params) { - const client = getSupabaseServerComponentClient({ admin: true }); - const { data, error } = use(client.auth.admin.getUserById(params.uid)); - - if (!data || error) { - throw new Error(`User not found`); - } - - return ; -} - -export default AdminGuard(ImpersonateUserModalPage); diff --git a/apps/web/app/admin/users/@modal/[uid]/reactivate/page.tsx b/apps/web/app/admin/users/@modal/[uid]/reactivate/page.tsx deleted file mode 100644 index 38b0d1149..000000000 --- a/apps/web/app/admin/users/@modal/[uid]/reactivate/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { use } from 'react'; - -import { redirect } from 'next/navigation'; - -import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; - -import AdminGuard from '../../../../../../packages/admin/components/AdminGuard'; -import ReactivateUserModal from '../components/ReactivateUserModal'; - -interface Params { - params: { - uid: string; - }; -} - -function ReactivateUserModalPage({ params }: Params) { - const client = getSupabaseServerComponentClient({ admin: true }); - const { data, error } = use(client.auth.admin.getUserById(params.uid)); - - if (!data || error) { - throw new Error(`User not found`); - } - - const user = data.user; - const isActive = !('banned_until' in user) || user.banned_until === 'none'; - - if (isActive) { - redirect(`/admin/users`); - } - - return ; -} - -export default AdminGuard(ReactivateUserModalPage); diff --git a/apps/web/app/admin/users/@modal/default.tsx b/apps/web/app/admin/users/@modal/default.tsx deleted file mode 100644 index 6ddf1b76f..000000000 --- a/apps/web/app/admin/users/@modal/default.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Default() { - return null; -} diff --git a/apps/web/app/admin/users/[uid]/components/UserActionsDropdown.tsx b/apps/web/app/admin/users/[uid]/components/UserActionsDropdown.tsx deleted file mode 100644 index 0db1b50d6..000000000 --- a/apps/web/app/admin/users/[uid]/components/UserActionsDropdown.tsx +++ /dev/null @@ -1,67 +0,0 @@ -'use client'; - -import Link from 'next/link'; - -import If from '@/components/app/If'; -import { EllipsisVerticalIcon } from 'lucide-react'; - -import { Button } from '@kit/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@kit/ui/dropdown-menu'; - -function UserActionsDropdown({ - uid, - isBanned, -}: React.PropsWithChildren<{ - uid: string; - isBanned: boolean; -}>) { - return ( - - - - - - - - Impersonate - - - - - - Ban - - - - - - - Reactivate - - - - - - Delete - - - - - ); -} - -export default UserActionsDropdown; diff --git a/apps/web/app/admin/users/[uid]/page.tsx b/apps/web/app/admin/users/[uid]/page.tsx deleted file mode 100644 index bb8c44859..000000000 --- a/apps/web/app/admin/users/[uid]/page.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import Link from 'next/link'; - -import { PageBody } from '@/components/app/Page'; -import configuration from '@/config/app.config'; -import type MembershipRole from '@/lib/organizations/types/membership-role'; -import { ChevronRightIcon } from 'lucide-react'; - -import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; - -import { Badge } from '@kit/ui/badge'; -import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; -import { Input } from '@kit/ui/input'; -import { Label } from '@kit/ui/label'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@kit/ui/table'; - -import RoleBadge from '../../../(app)/[account]/account/organization/components/RoleBadge'; -import AdminGuard from '../../../../packages/admin/components/AdminGuard'; -import AdminHeader from '../../../../packages/admin/components/AdminHeader'; -import UserActionsDropdown from './components/UserActionsDropdown'; - -interface Params { - params: { - uid: string; - }; -} - -export const metadata = { - title: `Manage User | ${configuration.name}`, -}; - -async function AdminUserPage({ params }: Params) { - const uid = params.uid; - - const data = await loadData(uid); - const { auth, user } = data; - const displayName = user?.displayName; - const authUser = auth?.user; - const email = authUser?.email; - const phone = authUser?.phone; - const organizations = data.organizations ?? []; - - const isBanned = Boolean( - authUser && 'banned_until' in authUser && authUser.banned_until !== 'none', - ); - - return ( -
- Manage User - - -
-
- - -
- -
-
- - - - User Details - - - -
-
- -
- -
- {isBanned ? ( - Banned - ) : ( - Active - )} -
-
- - - - - - -
-
- - - - Organizations - - - - - - - Organization ID - UUID - Organization - Role - - - - - {organizations.map((membership) => { - const organization = membership.organization; - const href = `/admin/organizations/${organization.uuid}/members`; - - return ( - - {organization.id} - {organization.uuid} - - - - {organization.name} - - - - -
- -
-
-
- ); - })} -
-
-
-
-
-
-
- ); -} - -export default AdminGuard(AdminUserPage); - -async function loadData(uid: string) { - const client = getSupabaseServerComponentClient({ admin: true }); - const authUser = client.auth.admin.getUserById(uid); - - const userData = client - .from('users') - .select( - ` - id, - displayName: display_name, - photoURL: photo_url, - onboarded - `, - ) - .eq('id', uid) - .single(); - - const organizationsQuery = client - .from('memberships') - .select< - string, - { - id: number; - role: MembershipRole; - organization: { - id: number; - uuid: string; - name: string; - }; - } - >( - ` - id, - role, - organization: organization_id !inner ( - id, - uuid, - name - ) - `, - ) - .eq('user_id', uid); - - const [auth, user, organizations] = await Promise.all([ - authUser, - userData, - organizationsQuery, - ]); - - return { - auth: auth.data, - user: user.data, - organizations: organizations.data, - }; -} - -function Breadcrumbs( - props: React.PropsWithChildren<{ - displayName: string; - }>, -) { - return ( -
- Admin - - Users - - {props.displayName} -
- ); -} diff --git a/apps/web/app/admin/users/default.tsx b/apps/web/app/admin/users/default.tsx deleted file mode 100644 index 6ddf1b76f..000000000 --- a/apps/web/app/admin/users/default.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Default() { - return null; -} diff --git a/apps/web/app/admin/users/error.tsx b/apps/web/app/admin/users/error.tsx deleted file mode 100644 index 5e83f1d7a..000000000 --- a/apps/web/app/admin/users/error.tsx +++ /dev/null @@ -1,21 +0,0 @@ -'use client'; - -import { PageBody } from '@/components/app/Page'; - -import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; - -function UsersAdminPageError() { - return ( - - - Could not load users - - There was an error loading the users. Please check your console - errors. - - - - ); -} - -export default UsersAdminPageError; diff --git a/apps/web/app/admin/users/layout.tsx b/apps/web/app/admin/users/layout.tsx deleted file mode 100644 index 28dd86068..000000000 --- a/apps/web/app/admin/users/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -function UserLayout( - props: React.PropsWithChildren<{ - modal: React.ReactNode; - }>, -) { - return ( - <> - {props.modal} - {props.children} - - ); -} - -export default UserLayout; diff --git a/apps/web/app/admin/users/page.tsx b/apps/web/app/admin/users/page.tsx deleted file mode 100644 index ce7401a26..000000000 --- a/apps/web/app/admin/users/page.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { PageBody } from '@/components/app/Page'; -import appConfig from '@/config/app.config'; - -import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; - -import type UserData from '@kit/session/types/user-data'; - -import AdminGuard from '../../../packages/admin/components/AdminGuard'; -import AdminHeader from '../../../packages/admin/components/AdminHeader'; -import getPageFromQueryParams from '../utils/get-page-from-query-param'; -import { getUsers } from './queries'; - -interface UsersAdminPageProps { - searchParams: { - page?: string; - }; -} - -export const metadata = { - title: `Users | ${appConfig.name}`, -}; - -async function UsersAdminPage({ searchParams }: UsersAdminPageProps) { - const page = getPageFromQueryParams(searchParams.page); - const perPage = 1; - const { users, total } = await loadUsers(page, perPage); - const pageCount = Math.ceil(total / perPage); - - return ( -
- Users - - -
- ); -} - -export default AdminGuard(UsersAdminPage); - -async function loadAuthUsers(page = 1, perPage = 20) { - const client = getSupabaseServerComponentClient({ admin: true }); - - const response = await client.auth.admin.listUsers({ - page, - perPage, - }); - - if (response.error) { - throw response.error; - } - - return response.data; -} - -async function loadUsers(page = 1, perPage = 20) { - const { users: authUsers, total } = await loadAuthUsers(page, perPage); - - const ids = authUsers.map((user) => user.id); - const usersData = await getUsers(ids); - - const users = authUsers - .map((user) => { - const data = usersData.find((u) => u.id === user.id) as UserData; - - const banDuration = - 'banned_until' in user ? (user.banned_until as string) : 'none'; - - return { - id: user.id, - email: user.email, - phone: user.phone, - createdAt: user.created_at, - updatedAt: user.updated_at, - lastSignInAt: user.last_sign_in_at, - banDuration, - data, - }; - }) - .filter(Boolean); - - return { - total, - users, - }; -} diff --git a/apps/web/app/admin/users/queries.ts b/apps/web/app/admin/users/queries.ts deleted file mode 100644 index 0acb10f24..000000000 --- a/apps/web/app/admin/users/queries.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { USERS_TABLE } from '@/lib/db-tables'; - -import getSupabaseServerComponentClient from '@packages/supabase/server-component-client'; - -export async function getUsers(ids: string[]) { - const client = getSupabaseServerComponentClient({ admin: true }); - - const { data: users, error } = await client - .from(USERS_TABLE) - .select( - ` - id, - photoURL: photo_url, - displayName: display_name, - onboarded - `, - ) - .in('id', ids); - - if (error) { - throw error; - } - - return users; -} diff --git a/apps/web/app/admin/utils/get-page-from-query-param.ts b/apps/web/app/admin/utils/get-page-from-query-param.ts deleted file mode 100644 index 3a93a1291..000000000 --- a/apps/web/app/admin/utils/get-page-from-query-param.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Get page from query params - * @name getPageFromQueryParams - * @param pageParam - */ -function getPageFromQueryParams(pageParam: string | undefined) { - const page = pageParam ? parseInt(pageParam) : 1; - - if (Number.isNaN(page) || page <= 0) { - return 1; - } - - return page; -} - -export default getPageFromQueryParams; diff --git a/apps/web/app/admin/utils/is-user-super-admin.ts b/apps/web/app/admin/utils/is-user-super-admin.ts deleted file mode 100644 index 53f587a45..000000000 --- a/apps/web/app/admin/utils/is-user-super-admin.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { SupabaseClient } from '@supabase/supabase-js'; - -import { Database } from '@kit/supabase/database'; - -/** - * @name ENFORCE_MFA - * @description Set this constant to true if you want the SuperAdmin user to - * sign in using MFA when accessing the Admin page - */ -const ENFORCE_MFA = false; - -/** - * @name isUserSuperAdmin - * @description Checks if the current user is an admin by checking the - * user_metadata.role field in Supabase Auth is set to a SuperAdmin role. - */ -const isUserSuperAdmin = async (params: { - client: SupabaseClient; - enforceMfa?: boolean; -}) => { - const enforceMfa = params.enforceMfa ?? ENFORCE_MFA; - const { data, error } = await params.client.auth.getUser(); - - if (error) { - return false; - } - - // If we enforce MFA, we need to check that the user is MFA authenticated. - if (enforceMfa) { - const isMfaAuthenticated = await verifyIsMultiFactorAuthenticated( - params.client, - ); - - if (!isMfaAuthenticated) { - return false; - } - } - - const adminMetadata = data.user?.app_metadata; - const role = adminMetadata?.role; - - return role === 'super-admin'; -}; - -export default isUserSuperAdmin; - -async function verifyIsMultiFactorAuthenticated(client: SupabaseClient) { - const { data, error } = - await client.auth.mfa.getAuthenticatorAssuranceLevel(); - - if (error || !data) { - return false; - } - - return data.currentLevel === 'aal2'; -} diff --git a/apps/web/app/error.tsx b/apps/web/app/error.tsx index b7af07fe5..613518207 100644 --- a/apps/web/app/error.tsx +++ b/apps/web/app/error.tsx @@ -8,7 +8,7 @@ import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; import { Trans } from '@kit/ui/trans'; -import { SiteHeader } from '~/(marketing)/components/site-header'; +import { SiteHeader } from '~/(marketing)/_components/site-header'; const ErrorPage = () => { return ( diff --git a/apps/web/app/join/_components/ExistingUserInviteForm.tsx b/apps/web/app/join/_components/ExistingUserInviteForm.tsx deleted file mode 100644 index ab4348584..000000000 --- a/apps/web/app/join/_components/ExistingUserInviteForm.tsx +++ /dev/null @@ -1,87 +0,0 @@ -'use client'; - -import { useCallback, useTransition } from 'react'; - -import type { Session } from '@supabase/gotrue-js'; - -import useRefreshRoute from '@kit/shared/hooks/use-refresh-route'; -import { useSignOut } from '@kit/supabase/hooks/use-sign-out'; -import { Button } from '@kit/ui/button'; -import { Trans } from '@kit/ui/trans'; - -function ExistingUserInviteForm( - props: React.PropsWithChildren<{ - session: Session; - code: string; - }>, -) { - const signOut = useSignOut(); - const refresh = useRefreshRoute(); - const [isSubmitting, startTransition] = useTransition(); - - const onSignOut = useCallback(async () => { - await signOut.mutateAsync(); - refresh(); - }, [refresh, signOut]); - - const onInviteAccepted = useCallback(() => { - return startTransition(async () => { - await acceptInviteAction({ - code: props.code, - }); - }); - }, [props.code, startTransition]); - - return ( - <> -
-

- }} - /> -

- - - -
-
-

- - - -

- -
- -
-
-
-
- - ); -} - -export default ExistingUserInviteForm; diff --git a/apps/web/app/join/_components/NewUserInviteForm.tsx b/apps/web/app/join/_components/NewUserInviteForm.tsx deleted file mode 100644 index 0a4acffbf..000000000 --- a/apps/web/app/join/_components/NewUserInviteForm.tsx +++ /dev/null @@ -1,103 +0,0 @@ -'use client'; - -import { useCallback, useState, useTransition } from 'react'; - -import { EmailOtpContainer } from '@kit/auth/src/components/email-otp-container'; -import { OauthProviders } from '@kit/auth/src/components/oauth-providers'; -import { PasswordSignInContainer } from '@kit/auth/src/components/password-sign-in-container'; -import { EmailPasswordSignUpContainer } from '@kit/auth/src/components/password-sign-up-container'; -import { isBrowser } from '@kit/shared/utils'; -import { Button } from '@kit/ui/button'; -import { If } from '@kit/ui/if'; -import { LoadingOverlay } from '@kit/ui/loading-overlay'; -import { Trans } from '@kit/ui/trans'; - -import authConfig from '~/config/auth.config'; - -enum Mode { - SignUp, - SignIn, -} - -function NewUserInviteForm( - props: React.PropsWithChildren<{ - code: string; - }>, -) { - const [mode, setMode] = useState(Mode.SignUp); - const [isSubmitting, startTransition] = useTransition(); - const oAuthReturnUrl = isBrowser() ? window.location.pathname : ''; - - const onInviteAccepted = useCallback( - async (userId?: string) => { - startTransition(async () => { - await acceptInviteAction({ - code: props.code, - userId, - }); - }); - }, - [props.code], - ); - - return ( - <> - - - Accepting invite. Please wait... - - - - - - - -
- - - -
-
- - -
- - - -
-
-
- - - - - - - - - - ); -} - -export default NewUserInviteForm; diff --git a/apps/web/app/join/page.tsx b/apps/web/app/join/page.tsx index f3c9c364b..03dbbf067 100644 --- a/apps/web/app/join/page.tsx +++ b/apps/web/app/join/page.tsx @@ -1,17 +1,10 @@ -import { headers } from 'next/headers'; import { notFound } from 'next/navigation'; -import type { SupabaseClient } from '@supabase/supabase-js'; - -import { Logger } from '@kit/shared/logger'; -import { Database } from '@kit/supabase/database'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { Heading } from '@kit/ui/heading'; import { If } from '@kit/ui/if'; import { Trans } from '@kit/ui/trans'; -import ExistingUserInviteForm from '~/join/_components/ExistingUserInviteForm'; -import NewUserInviteForm from '~/join/_components/NewUserInviteForm'; import { withI18n } from '~/lib/i18n/with-i18n'; interface Context { @@ -28,12 +21,10 @@ async function JoinTeamAccountPage({ searchParams }: Context) { const token = searchParams.invite_token; const data = await getInviteDataFromInviteToken(token); - if (!data.membership) { + if (!data) { notFound(); } - const organization = data.membership.organization; - return ( <> @@ -62,70 +53,26 @@ async function JoinTeamAccountPage({ searchParams }: Context) {

- - } - > - {(session) => } - ); } export default withI18n(JoinTeamAccountPage); -async function getInviteDataFromInviteToken(code: string) { - const client = getSupabaseServerComponentClient(); - +async function getInviteDataFromInviteToken(token: string) { // we use an admin client to be able to read the pending membership // without having to be logged in const adminClient = getSupabaseServerComponentClient({ admin: true }); - const { data: membership, error } = await getInvite(adminClient, code); + const { data: invitation, error } = await adminClient + .from('invitations') + .select('*') + .eq('invite_token', token) + .single(); - // if the invite wasn't found, it's 404 - if (error) { - Logger.warn( - { - code, - error, - }, - `User navigated to invite page, but it wasn't found. Redirecting to home page...`, - ); - - notFound(); + if (!invitation ?? error) { + return null; } - const { data: userSession } = await client.auth.getSession(); - const session = userSession?.session; - const csrfToken = headers().get('x-csrf-token'); - - return { - csrfToken, - session, - membership, - code, - }; -} - -function getInvite(adminClient: SupabaseClient, code: string) { - return getMembershipByInviteCode<{ - id: number; - code: string; - organization: { - name: string; - id: number; - }; - }>(adminClient, { - code, - query: ` - id, - code, - organization: organization_id ( - name, - id - ) - `, - }); + return invitation; } diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx index 49f086c1c..bbdd6a773 100644 --- a/apps/web/app/not-found.tsx +++ b/apps/web/app/not-found.tsx @@ -1,12 +1,12 @@ import Link from 'next/link'; -import { ArrowLeftIcon } from '@radix-ui/react-icons'; +import { ArrowLeftIcon } from 'lucide-react'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; import { Trans } from '@kit/ui/trans'; -import { SiteHeader } from '~/(marketing)/components/site-header'; +import { SiteHeader } from '~/(marketing)/_components/site-header'; import appConfig from '~/config/app.config'; import { withI18n } from '~/lib/i18n/with-i18n'; @@ -17,7 +17,7 @@ export const metadata = { const NotFoundPage = () => { return (
- +
{ return { - loc: join(siteUrl, url), + loc: new URL(siteUrl, url).href, lastmod: new Date().toISOString(), }; }); @@ -32,7 +32,7 @@ function getPostsSitemap() { return allPosts.map((post) => { return { - loc: join(siteUrl, post.url), + loc: new URL(siteUrl, post.url).href, lastmod: new Date().toISOString(), }; }); @@ -43,7 +43,7 @@ function getDocsSitemap() { return allDocumentationPages.map((page) => { return { - loc: join(siteUrl, page.url), + loc: new URL(siteUrl, page.url).href, lastmod: new Date().toISOString(), }; }); diff --git a/apps/web/app/update-password/page.tsx b/apps/web/app/update-password/page.tsx index 95566fa19..6dbb5295c 100644 --- a/apps/web/app/update-password/page.tsx +++ b/apps/web/app/update-password/page.tsx @@ -1,9 +1,10 @@ import { redirect } from 'next/navigation'; +import { PasswordResetForm } from '@kit/auth/password-reset'; import { AuthLayoutShell } from '@kit/auth/shared'; -import PasswordResetForm from '@kit/auth/src/components/password-reset-form'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; +import { AppLogo } from '~/components/app-logo'; import pathsConfig from '~/config/paths.config'; import { withI18n } from '~/lib/i18n/with-i18n'; @@ -16,9 +17,11 @@ async function PasswordResetPage() { redirect(pathsConfig.auth.passwordReset); } + const redirectTo = `/${pathsConfig.auth.callback}?next=${pathsConfig.app.home}`; + return ( - - + + ); } diff --git a/apps/web/components/root-providers.tsx b/apps/web/components/root-providers.tsx index 2b5d29234..c19cd04dc 100644 --- a/apps/web/components/root-providers.tsx +++ b/apps/web/components/root-providers.tsx @@ -1,6 +1,7 @@ 'use client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'; import { I18nProvider } from '@kit/i18n/provider'; import { AuthChangeListener } from '@kit/supabase/components/auth-change-listener'; @@ -8,21 +9,23 @@ import { AuthChangeListener } from '@kit/supabase/components/auth-change-listene import pathsConfig from '~/config/paths.config'; import { i18nResolver } from '~/lib/i18n/i18n.resolver'; +const queryClient = new QueryClient(); + export function RootProviders({ lang, children, }: React.PropsWithChildren<{ lang: string; }>) { - const queryClient = new QueryClient(); - return ( - - - {children} - - + + + + {children} + + + ); } diff --git a/apps/web/config/paths.config.ts b/apps/web/config/paths.config.ts index d35e6f6f4..c9bd9e605 100644 --- a/apps/web/config/paths.config.ts +++ b/apps/web/config/paths.config.ts @@ -33,7 +33,7 @@ const pathsConfig = PathsSchema.parse({ }, app: { home: '/home', - personalAccountSettings: '/home/account', + personalAccountSettings: '/home/settings', personalAccountBilling: '/home/billing', personalAccountBillingReturn: '/home/billing/return', accountHome: '/home/[account]', diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 2484cdfd1..ac16d8361 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -1,5 +1,8 @@ import withBundleAnalyzer from '@next/bundle-analyzer'; +const IS_PRODUCTION = process.env.NODE_ENV === 'production'; +const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL; + /** @type {import('next').NextConfig} */ const config = { reactStrictMode: true, @@ -17,6 +20,9 @@ const config = { '@kit/billing-gateway', ], pageExtensions: ['ts', 'tsx'], + images: { + remotePatterns: getRemotePatterns(), + }, experimental: { mdxRs: true, }, @@ -28,3 +34,25 @@ const config = { export default withBundleAnalyzer({ enabled: process.env.ANALYZE === 'true', })(config); + +function getRemotePatterns() { + // add here the remote patterns for your images + const remotePatterns = []; + + if (SUPABASE_URL) { + const hostname = new URL(SUPABASE_URL).hostname; + remotePatterns.push({ + protocol: 'https', + hostname, + }); + } + + return IS_PRODUCTION + ? remotePatterns + : [ + { + protocol: 'http', + hostname: '127.0.0.1', + }, + ]; +} diff --git a/apps/web/package.json b/apps/web/package.json index f0b28e9bd..e77f2b9e9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,12 +1,12 @@ { - "name": "@kit/web", + "name": "web", "version": "0.1.0", "private": true, "scripts": { "analyze": "ANALYZE=true pnpm run build", "build": "pnpm with-env next build", "clean": "git clean -xdf .next .turbo node_modules", - "dev": "pnpm with-env next dev", + "dev": "pnpm with-env next dev --turbo", "lint": "next lint", "format": "prettier --check \"**/*.{js,cjs,mjs,ts,tsx,md,json}\"", "start": "pnpm with-env next start", @@ -31,6 +31,7 @@ "@supabase/ssr": "^0.1.0", "@supabase/supabase-js": "^2.39.8", "@tanstack/react-query": "^5.17.15", + "@tanstack/react-query-next-experimental": "^5.28.6", "@tanstack/react-table": "^8.11.3", "@epic-web/invariant": "^1.0.0", "@radix-ui/react-icons": "^1.3.0", @@ -43,7 +44,8 @@ "next-contentlayer": "0.3.4", "react-i18next": "^14.1.0", "date-fns": "^3.2.0", - "next": "^14.1.0", + "next": "^14.2.0-canary.41", + "next-sitemap": "^4.2.3", "next-themes": "^0.2.1", "react": "18.2.0", "react-dom": "18.2.0", @@ -58,7 +60,7 @@ "@kit/prettier-config": "^0.1.0", "@kit/tailwind-config": "^0.1.0", "@kit/tsconfig": "^0.1.0", - "@next/bundle-analyzer": "^14.1.4", + "@next/bundle-analyzer": "^14.2.0-canary.41", "@types/mdx": "^2.0.10", "@types/node": "^20.11.5", "@types/react": "^18.2.48", diff --git a/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/components/billing-session-status.tsx b/packages/billing-gateway/src/components/billing-session-status.tsx similarity index 83% rename from apps/web/app/(dashboard)/home/[account]/settings/subscription/return/components/billing-session-status.tsx rename to packages/billing-gateway/src/components/billing-session-status.tsx index 4be6a9527..c553f6653 100644 --- a/apps/web/app/(dashboard)/home/[account]/settings/subscription/return/components/billing-session-status.tsx +++ b/packages/billing-gateway/src/components/billing-session-status.tsx @@ -3,14 +3,11 @@ import Link from 'next/link'; import { CheckIcon, ChevronRightIcon } from 'lucide-react'; -import type { Stripe } from 'stripe'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; import { Trans } from '@kit/ui/trans'; -import pathsConfig from '~/config/paths.config'; - /** * Retrieves the session status for a Stripe checkout session. * Since we should only arrive here for a successful checkout, we only check @@ -23,26 +20,34 @@ import pathsConfig from '~/config/paths.config'; */ export function BillingSessionStatus({ customerEmail, + redirectPath, }: React.PropsWithChildren<{ - status: Stripe.Checkout.Session['status']; customerEmail: string; + redirectPath: string; }>) { - return ; + return ( + + ); } function SuccessSessionStatus({ customerEmail, + redirectPath, }: React.PropsWithChildren<{ customerEmail: string; + redirectPath: string; }>) { return (
- ); - })} -
- ); -} - -function DefaultCheckoutButton( - props: React.PropsWithChildren<{ - plan: PricingItemProps['plan']; - recommended?: boolean; - }>, -) { - const signUpPath = pathsConfig.auth.signUp; - - const linkHref = - props.plan.href ?? `${signUpPath}?utm_source=${props.plan.stripePriceId}`; - - const label = props.plan.label ?? 'common:getStarted'; - - return ( -
- -
- ); -} diff --git a/packages/ui/src/makerkit/profile-avatar.tsx b/packages/ui/src/makerkit/profile-avatar.tsx index 7383e3229..afa215829 100644 --- a/packages/ui/src/makerkit/profile-avatar.tsx +++ b/packages/ui/src/makerkit/profile-avatar.tsx @@ -1,7 +1,7 @@ -import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar'; +import { Avatar, AvatarFallback, AvatarImage } from '../shadcn/avatar'; type SessionProps = { - displayName?: string | null; + displayName: string | null; pictureUrl?: string | null; }; @@ -31,7 +31,9 @@ export function ProfileAvatar(props: ProfileAvatarProps) { - {initials} + + {initials} + ); diff --git a/packages/ui/src/makerkit/sidebar.tsx b/packages/ui/src/makerkit/sidebar.tsx index 0f001592b..466b5105d 100644 --- a/packages/ui/src/makerkit/sidebar.tsx +++ b/packages/ui/src/makerkit/sidebar.tsx @@ -9,8 +9,7 @@ import { cva } from 'class-variance-authority'; import { ChevronDownIcon } from 'lucide-react'; import { z } from 'zod'; -import { Button } from '@kit/ui/button'; - +import { Button } from '../shadcn/button'; import { Tooltip, TooltipContent, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5960dae75..e9ffe5c06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ importers: '@tanstack/react-query': specifier: ^5.17.15 version: 5.28.6(react@18.2.0) + '@tanstack/react-query-next-experimental': + specifier: ^5.28.6 + version: 5.28.6(@tanstack/react-query@5.28.6)(next@14.2.0-canary.41)(react@18.2.0) '@tanstack/react-table': specifier: ^8.11.3 version: 8.14.0(react-dom@18.2.0)(react@18.2.0) @@ -97,7 +100,7 @@ importers: version: 3.6.0 edge-csrf: specifier: ^1.0.9 - version: 1.0.9(next@14.1.4) + version: 1.0.9(next@14.2.0-canary.41) i18next: specifier: ^23.10.1 version: 23.10.1 @@ -105,14 +108,17 @@ importers: specifier: ^1.2.0 version: 1.2.0 next: - specifier: ^14.1.0 - version: 14.1.4(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + specifier: ^14.2.0-canary.41 + version: 14.2.0-canary.41(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) next-contentlayer: specifier: 0.3.4 - version: 0.3.4(contentlayer@0.3.4)(esbuild@0.19.11)(next@14.1.4)(react-dom@18.2.0)(react@18.2.0) + version: 0.3.4(contentlayer@0.3.4)(esbuild@0.19.11)(next@14.2.0-canary.41)(react-dom@18.2.0)(react@18.2.0) + next-sitemap: + specifier: ^4.2.3 + version: 4.2.3(next@14.2.0-canary.41) next-themes: specifier: ^0.2.1 - version: 0.2.1(next@14.1.4)(react-dom@18.2.0)(react@18.2.0) + version: 0.2.1(next@14.2.0-canary.41)(react-dom@18.2.0)(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -157,8 +163,8 @@ importers: specifier: ^0.1.0 version: link:../../tooling/typescript '@next/bundle-analyzer': - specifier: ^14.1.4 - version: 14.1.4 + specifier: ^14.2.0-canary.41 + version: 14.2.0-canary.41 '@types/mdx': specifier: ^2.0.10 version: 2.0.11 @@ -424,7 +430,7 @@ importers: version: 1.2.0 next: specifier: ^14.1.4 - version: 14.1.4(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + version: 14.1.4(react-dom@18.2.0)(react@18.2.0) react-i18next: specifier: ^14.1.0 version: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0) @@ -1167,6 +1173,10 @@ packages: type-fest: 3.13.1 dev: false + /@corex/deepmerge@4.0.43: + resolution: {integrity: sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==} + dev: false + /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1751,8 +1761,8 @@ packages: - supports-color dev: false - /@next/bundle-analyzer@14.1.4: - resolution: {integrity: sha512-IpF/18HcAOcfHRr24tqPOUpMmVKIqvkCxIubMeRYWCXs3jm7niPGrt8Mu74yMDzfGlUwgQA6Xd6BUc5+jQxcEg==} + /@next/bundle-analyzer@14.2.0-canary.41: + resolution: {integrity: sha512-1+PP3XaC3lz0oE49D0jxGsiEJZOmwlDgqV3yantl64vXRNX8Ae3Gsk1KcDhp7JHKKPvFx4AF13/LF48sbH0zkw==} dependencies: webpack-bundle-analyzer: 4.10.1 transitivePeerDependencies: @@ -1760,6 +1770,10 @@ packages: - utf-8-validate dev: true + /@next/env@13.5.6: + resolution: {integrity: sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==} + dev: false + /@next/env@14.1.0: resolution: {integrity: sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==} dev: false @@ -1768,6 +1782,10 @@ packages: resolution: {integrity: sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ==} dev: false + /@next/env@14.2.0-canary.41: + resolution: {integrity: sha512-6bd8zNDEferyJ9qkJrCB0pTgGFaJ9XttMI+uj5jrSeQ88kxsgPcoOFqBEMDtDOEzWqi+17B29alThei0Cmw0dA==} + dev: false + /@next/eslint-plugin-next@14.1.4: resolution: {integrity: sha512-n4zYNLSyCo0Ln5b7qxqQeQ34OZKXwgbdcx6kmkQbywr+0k6M3Vinft0T72R6CDAcDrne2IAgSud4uWCzFgc5HA==} dependencies: @@ -1806,6 +1824,15 @@ packages: dev: false optional: true + /@next/swc-darwin-arm64@14.2.0-canary.41: + resolution: {integrity: sha512-cplMeo/uQcfSHwZH2naB87IWzJouOSPXFQqRk1/nwuQNPxLFT2xO2v7zP4L4W98qukvLylFxWQhqpIQqZnohIA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@next/swc-darwin-x64@14.1.0: resolution: {integrity: sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==} engines: {node: '>= 10'} @@ -1824,6 +1851,15 @@ packages: dev: false optional: true + /@next/swc-darwin-x64@14.2.0-canary.41: + resolution: {integrity: sha512-mnqPeUMFUg73DXEsNOxrKXy09ikcErLeFEzYVqn5XwaewVyqny9xwf8f02BYBcLEBrmk565xkocnTs7su45qqA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@next/swc-linux-arm64-gnu@14.1.0: resolution: {integrity: sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==} engines: {node: '>= 10'} @@ -1842,6 +1878,15 @@ packages: dev: false optional: true + /@next/swc-linux-arm64-gnu@14.2.0-canary.41: + resolution: {integrity: sha512-ns0/YS9yqqS71h4xZNGn+IDZC7q3fLrF9mIaZ3aXhkHpXfeyjACOAEC6/0l0E49w4VkPv0XK64aRvsxsZfFr9g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@next/swc-linux-arm64-musl@14.1.0: resolution: {integrity: sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==} engines: {node: '>= 10'} @@ -1860,6 +1905,15 @@ packages: dev: false optional: true + /@next/swc-linux-arm64-musl@14.2.0-canary.41: + resolution: {integrity: sha512-22nsQMLgJH3ZbxbgQmiwGbFtW8KtDRQJU8OSyWcO8MFTgPdL3IcRd/lTsR5r9qliosoGgHzFSYDAq/7M5I/w0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@next/swc-linux-x64-gnu@14.1.0: resolution: {integrity: sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==} engines: {node: '>= 10'} @@ -1878,6 +1932,15 @@ packages: dev: false optional: true + /@next/swc-linux-x64-gnu@14.2.0-canary.41: + resolution: {integrity: sha512-ZkcPQk+SV+i06i0k/UVmXKk+k70yKIwtTPrWl7JpeJ/Wc7sWoRqNCdpFqP227oOsy6dM0NKqJTEOATBYAefQLg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@next/swc-linux-x64-musl@14.1.0: resolution: {integrity: sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==} engines: {node: '>= 10'} @@ -1896,6 +1959,15 @@ packages: dev: false optional: true + /@next/swc-linux-x64-musl@14.2.0-canary.41: + resolution: {integrity: sha512-1Uofz0Bkec63oN7Xj9lHGevCqxZJrr6pruPx5JUb613CYqUVCVj6JA8H2JutYYwgcbwstjdA+9He37HlW1FGTw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@next/swc-win32-arm64-msvc@14.1.0: resolution: {integrity: sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==} engines: {node: '>= 10'} @@ -1914,6 +1986,15 @@ packages: dev: false optional: true + /@next/swc-win32-arm64-msvc@14.2.0-canary.41: + resolution: {integrity: sha512-gMzwgq1ZnKwxYx2I4qhuRtaGZ/6WZD8XV0cObnaWXiu8O7NCK1kEKa6+IWy+Manax5qwamHKzWND5uKbeddToQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@next/swc-win32-ia32-msvc@14.1.0: resolution: {integrity: sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==} engines: {node: '>= 10'} @@ -1932,6 +2013,15 @@ packages: dev: false optional: true + /@next/swc-win32-ia32-msvc@14.2.0-canary.41: + resolution: {integrity: sha512-U72bjcHUUoRToFTAouUqK9y/PaiKVo4rDX5/RwzIZThPghfHf5ALG6DYiw64sKWSRXSfFOv9DzhofjK1+CIgSA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@next/swc-win32-x64-msvc@14.1.0: resolution: {integrity: sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==} engines: {node: '>= 10'} @@ -1950,6 +2040,15 @@ packages: dev: false optional: true + /@next/swc-win32-x64-msvc@14.2.0-canary.41: + resolution: {integrity: sha512-cBgEDwdxfyv9bBoo5dKBtfxd7S7Dv3dTRKYx7agprJ3BdGc8gXs2zdi08lR/fBU0kjpsC4gw/tg5p+IDkuZUaw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -4056,6 +4155,13 @@ packages: tslib: 2.6.2 dev: false + /@swc/helpers@0.5.5: + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + dependencies: + '@swc/counter': 0.1.3 + tslib: 2.6.2 + dev: false + /@swc/types@0.1.6: resolution: {integrity: sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==} dependencies: @@ -4072,6 +4178,18 @@ packages: /@tanstack/query-core@5.28.6: resolution: {integrity: sha512-hnhotV+DnQtvtR3jPvbQMPNMW4KEK0J4k7c609zJ8muiNknm+yoDyMHmxTWM5ZnlZpsz0zOxYFr+mzRJNHWJsA==} + /@tanstack/react-query-next-experimental@5.28.6(@tanstack/react-query@5.28.6)(next@14.2.0-canary.41)(react@18.2.0): + resolution: {integrity: sha512-KHTR1nGChTXk/kELit2gaqF3cQuN68F5UJv0377Gz5DnllPnBegja6if2W9KtKxm3Z1xP0j8LQXplqlqny2SYw==} + peerDependencies: + '@tanstack/react-query': ^5.28.6 + next: ^13 || ^14 + react: ^18.0.0 + dependencies: + '@tanstack/react-query': 5.28.6(react@18.2.0) + next: 14.2.0-canary.41(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + dev: false + /@tanstack/react-query@5.28.6(react@18.2.0): resolution: {integrity: sha512-/DdYuDBSsA21Qbcder1R8Cr/3Nx0ZnA2lgtqKsLMvov8wL4+g0HBz/gWYZPlIsof7iyfQafyhg4wUVUsS3vWZw==} peerDependencies: @@ -5906,12 +6024,12 @@ packages: /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - /edge-csrf@1.0.9(next@14.1.4): + /edge-csrf@1.0.9(next@14.2.0-canary.41): resolution: {integrity: sha512-3F89YTh42UDdISr3s9AEcgJDLi4ysgjGfnybzF0LuZGaG2W31h1ZwgWwEQBLMj04lAklcP4XHZYi7vk9o8zcbg==} peerDependencies: next: ^13.0.0 || ^14.0.0 dependencies: - next: 14.1.4(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0-canary.41(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) dev: false /editorconfig@1.0.4: @@ -8657,7 +8775,7 @@ packages: engines: {node: '>= 0.4.0'} dev: true - /next-contentlayer@0.3.4(contentlayer@0.3.4)(esbuild@0.19.11)(next@14.1.4)(react-dom@18.2.0)(react@18.2.0): + /next-contentlayer@0.3.4(contentlayer@0.3.4)(esbuild@0.19.11)(next@14.2.0-canary.41)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UtUCwgAl159KwfhNaOwyiI7Lg6sdioyKMeh+E7jxx0CJ29JuXGxBEYmCI6+72NxFGIFZKx8lvttbbQhbnYWYSw==} peerDependencies: contentlayer: 0.3.4 @@ -8668,7 +8786,7 @@ packages: '@contentlayer/core': 0.3.4(esbuild@0.19.11) '@contentlayer/utils': 0.3.4 contentlayer: 0.3.4(esbuild@0.19.11) - next: 14.1.4(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0-canary.41(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: @@ -8678,14 +8796,28 @@ packages: - supports-color dev: false - /next-themes@0.2.1(next@14.1.4)(react-dom@18.2.0)(react@18.2.0): + /next-sitemap@4.2.3(next@14.2.0-canary.41): + resolution: {integrity: sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==} + engines: {node: '>=14.18'} + hasBin: true + peerDependencies: + next: '*' + dependencies: + '@corex/deepmerge': 4.0.43 + '@next/env': 13.5.6 + fast-glob: 3.3.2 + minimist: 1.2.8 + next: 14.2.0-canary.41(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + dev: false + + /next-themes@0.2.1(next@14.2.0-canary.41)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} peerDependencies: next: '*' react: '*' react-dom: '*' dependencies: - next: 14.1.4(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0-canary.41(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -8729,7 +8861,7 @@ packages: - babel-plugin-macros dev: false - /next@14.1.4(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0): + /next@14.1.4(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-1WTaXeSrUwlz/XcnhGTY7+8eiaFvdet5z9u3V2jb+Ek1vFo0VhHKSAIJvDWfQpttWjnyw14kBeq28TPq7bTeEQ==} engines: {node: '>=18.17.0'} hasBin: true @@ -8745,7 +8877,6 @@ packages: optional: true dependencies: '@next/env': 14.1.4 - '@opentelemetry/api': 1.8.0 '@swc/helpers': 0.5.2 busboy: 1.6.0 caniuse-lite: 1.0.30001599 @@ -8769,6 +8900,46 @@ packages: - babel-plugin-macros dev: false + /next@14.2.0-canary.41(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-jqNSTq1COP04WXpj88Bzt8kkCLXFNpCU4tHzQrICAMVWeNtHTZpK2WPR8560SWBw614bW2qHYY85k0tez3YLiA==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + sass: + optional: true + dependencies: + '@next/env': 14.2.0-canary.41 + '@opentelemetry/api': 1.8.0 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001599 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + styled-jsx: 5.1.1(react@18.2.0) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.0-canary.41 + '@next/swc-darwin-x64': 14.2.0-canary.41 + '@next/swc-linux-arm64-gnu': 14.2.0-canary.41 + '@next/swc-linux-arm64-musl': 14.2.0-canary.41 + '@next/swc-linux-x64-gnu': 14.2.0-canary.41 + '@next/swc-linux-x64-musl': 14.2.0-canary.41 + '@next/swc-win32-arm64-msvc': 14.2.0-canary.41 + '@next/swc-win32-ia32-msvc': 14.2.0-canary.41 + '@next/swc-win32-x64-msvc': 14.2.0-canary.41 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + /no-case@2.3.2: resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} dependencies: diff --git a/supabase/migrations/20221215192558_schema.sql b/supabase/migrations/20221215192558_schema.sql index 5336521c7..4aea119bf 100644 --- a/supabase/migrations/20221215192558_schema.sql +++ b/supabase/migrations/20221215192558_schema.sql @@ -1261,8 +1261,8 @@ grant execute on function public.get_account_members (text) to authenticated, postgres; - create or replace function public.get_account_invitations(account_slug text) returns table ( + id serial, email varchar(255), account_id uuid, invited_by uuid,