From 95793c42b4f1de3f1b184c70dbeb40b662f2ba74 Mon Sep 17 00:00:00 2001 From: giancarlo Date: Mon, 25 Mar 2024 15:40:43 +0800 Subject: [PATCH] Remove admin functionality related code The admin functionality related code has been removed which includes various user and organization functionalities like delete, update, ban etc. This includes action logic, UI components and supportive utility functions. Notable deletions include the server action files, dialog components for actions like banning and deleting, and related utility functions. This massive cleanup is aimed at simplifying the codebase and the commit reflects adherence to project restructuring. --- .../personal-account-checkout-form.tsx | 0 .../(dashboard)/home/(user)/billing/page.tsx | 3 +- .../home/(user)/billing/return/page.tsx | 3 + .../home/(user)/billing/server-actions.ts | 4 + .../app/(dashboard)/home/(user)/layout.tsx | 2 +- .../{account => settings}/actions.server.ts | 0 .../(user)/{account => settings}/layout.tsx | 0 .../(user)/{account => settings}/loading.tsx | 0 .../(user)/{account => settings}/page.tsx | 0 .../app-header.tsx | 2 +- .../app-sidebar-navigation.tsx | 0 .../app-sidebar.tsx | 12 +- .../dashboard-demo.tsx | 0 .../mobile-app-navigation.tsx | 0 .../{(lib) => _lib}/load-workspace.ts | 0 .../_components/billing-portal-form.tsx | 39 +++ .../team-account-checkout-form.tsx | 64 ++++ .../home/[account]/billing/page.tsx | 42 ++- .../home/[account]/billing/return/page.tsx | 84 +++++ .../home/[account]/billing/server-actions.ts | 179 ++++++++++ .../app/(dashboard)/home/[account]/layout.tsx | 43 ++- .../home/[account]/members/page.tsx | 6 +- .../app/(dashboard)/home/[account]/page.tsx | 4 +- .../return/components/recover-checkout.tsx | 26 -- .../settings/subscription/return/page.tsx | 81 ----- .../home-sidebar-account-selector.tsx | 0 .../home-sidebar.tsx | 28 +- .../personal-account-dropdown.tsx | 12 +- .../{components => _components}/grid-list.tsx | 0 .../site-footer.tsx | 0 .../site-header-account-section.tsx | 68 ++++ .../site-header.tsx | 10 +- .../site-navigation.tsx | 0 .../site-page-header.tsx | 0 apps/web/app/(marketing)/blog/[slug]/page.tsx | 3 +- .../cover-image.tsx | 0 .../date-formatter.tsx | 0 .../draft-post-badge.tsx | 0 .../post-header.tsx | 8 +- .../post-preview.tsx | 8 +- .../blog/{components => _components}/post.tsx | 2 +- apps/web/app/(marketing)/blog/page.tsx | 9 +- .../site-header-account-section.tsx | 48 --- .../app/(marketing)/docs/[...slug]/page.tsx | 8 +- .../{components => _components}/docs-card.tsx | 0 .../docs-cards.tsx | 0 .../docs-navigation.tsx | 2 +- .../documentation-page-link.tsx | 0 .../build-documentation-tree.ts | 0 .../get-documentation-page-tree.ts | 0 apps/web/app/(marketing)/docs/layout.tsx | 4 +- apps/web/app/(marketing)/docs/page.tsx | 7 +- apps/web/app/(marketing)/faq/page.tsx | 3 +- apps/web/app/(marketing)/layout.tsx | 15 +- apps/web/app/(marketing)/pricing/page.tsx | 3 +- apps/web/app/admin/layout.tsx | 21 -- apps/web/app/admin/lib/actions-utils.ts | 21 -- apps/web/app/admin/loading.tsx | 3 - .../@modal/[uid]/actions.server.ts | 29 -- .../components/DeleteOrganizationModal.tsx | 95 ------ .../@modal/[uid]/delete/page.tsx | 25 -- .../admin/organizations/@modal/default.tsx | 3 - .../components/OrganizationsMembersTable.tsx | 137 -------- .../organizations/[uid]/members/page.tsx | 80 ----- apps/web/app/admin/organizations/default.tsx | 3 - apps/web/app/admin/organizations/error.tsx | 21 -- apps/web/app/admin/organizations/layout.tsx | 14 - apps/web/app/admin/organizations/page.tsx | 67 ---- apps/web/app/admin/organizations/queries.ts | 131 -------- apps/web/app/admin/page.tsx | 67 ---- .../users/@modal/[uid]/actions.server.ts | 127 -------- .../app/admin/users/@modal/[uid]/ban/page.tsx | 32 -- .../@modal/[uid]/components/BanUserModal.tsx | 111 ------- .../[uid]/components/DeleteUserModal.tsx | 96 ------ .../components/ImpersonateUserAuthSetter.tsx | 52 --- .../ImpersonateUserConfirmationModal.tsx | 128 -------- .../[uid]/components/ReactivateUserModal.tsx | 76 ----- .../admin/users/@modal/[uid]/delete/page.tsx | 25 -- .../users/@modal/[uid]/impersonate/page.tsx | 25 -- .../users/@modal/[uid]/reactivate/page.tsx | 34 -- apps/web/app/admin/users/@modal/default.tsx | 3 - .../[uid]/components/UserActionsDropdown.tsx | 67 ---- apps/web/app/admin/users/[uid]/page.tsx | 238 -------------- apps/web/app/admin/users/default.tsx | 3 - apps/web/app/admin/users/error.tsx | 21 -- apps/web/app/admin/users/layout.tsx | 14 - apps/web/app/admin/users/page.tsx | 85 ----- apps/web/app/admin/users/queries.ts | 25 -- .../admin/utils/get-page-from-query-param.ts | 16 - .../app/admin/utils/is-user-super-admin.ts | 56 ---- apps/web/app/error.tsx | 2 +- .../_components/ExistingUserInviteForm.tsx | 87 ----- .../join/_components/NewUserInviteForm.tsx | 103 ------ apps/web/app/join/page.tsx | 73 +---- apps/web/app/not-found.tsx | 6 +- apps/web/app/server-sitemap.xml/route.ts | 10 +- apps/web/app/update-password/page.tsx | 9 +- apps/web/components/root-providers.tsx | 17 +- apps/web/config/paths.config.ts | 2 +- apps/web/next.config.mjs | 28 ++ apps/web/package.json | 10 +- .../components/billing-session-status.tsx | 23 +- .../src/components/embedded-checkout.tsx | 8 +- .../billing-gateway/src/components/index.ts | 1 + .../src/components/plan-picker.tsx | 20 +- .../billing-strategy-provider.service.ts | 10 +- .../components/personal-account-dropdown.tsx | 2 +- .../update-account-details-form.tsx | 3 +- .../update-account-image-container.tsx | 11 +- .../update-email-form-container.tsx | 7 +- .../update-password-container.tsx | 7 +- .../src/components/password-reset-form.tsx | 4 +- packages/features/auth/src/password-reset.ts | 1 + .../account-invitations-server-actions.ts | 25 +- .../invitations/account-invitations-table.tsx | 24 +- .../invitations/update-invitation-dialog.tsx | 9 +- .../members/account-members-table.tsx | 27 +- .../src/components/role-badge.tsx | 2 +- .../src/schema/delete-invitation.schema.ts | 5 + .../src/schema/update-invitation-schema.ts | 10 + .../services/account-invitations.service.ts | 23 +- .../stripe-billing-strategy.service.ts | 12 +- .../src/clients/server-actions.client.ts | 2 +- .../supabase/src/hooks/use-user-session.ts | 15 +- packages/ui/src/makerkit/data-table.tsx | 5 +- .../ui/src/makerkit/image-upload-input.tsx | 5 +- packages/ui/src/makerkit/image-uploader.tsx | 3 +- .../makerkit/language-dropdown-switcher.tsx | 3 +- .../makerkit/mobile-navigation-dropdown.tsx | 6 +- .../src/makerkit/mobile-navigation-menu.tsx | 4 +- packages/ui/src/makerkit/pricing-table.tsx | 307 ------------------ packages/ui/src/makerkit/profile-avatar.tsx | 8 +- packages/ui/src/makerkit/sidebar.tsx | 3 +- pnpm-lock.yaml | 207 +++++++++++- supabase/migrations/20221215192558_schema.sql | 2 +- 135 files changed, 1062 insertions(+), 2872 deletions(-) rename apps/web/app/(dashboard)/home/(user)/billing/{components => _components}/personal-account-checkout-form.tsx (100%) create mode 100644 apps/web/app/(dashboard)/home/(user)/billing/return/page.tsx rename apps/web/app/(dashboard)/home/(user)/{account => settings}/actions.server.ts (100%) rename apps/web/app/(dashboard)/home/(user)/{account => settings}/layout.tsx (100%) rename apps/web/app/(dashboard)/home/(user)/{account => settings}/loading.tsx (100%) rename apps/web/app/(dashboard)/home/(user)/{account => settings}/page.tsx (100%) rename apps/web/app/(dashboard)/home/[account]/{(components) => _components}/app-header.tsx (92%) rename apps/web/app/(dashboard)/home/[account]/{(components) => _components}/app-sidebar-navigation.tsx (100%) rename apps/web/app/(dashboard)/home/[account]/{(components) => _components}/app-sidebar.tsx (91%) rename apps/web/app/(dashboard)/home/[account]/{(components) => _components}/dashboard-demo.tsx (100%) rename apps/web/app/(dashboard)/home/[account]/{(components) => _components}/mobile-app-navigation.tsx (100%) rename apps/web/app/(dashboard)/home/[account]/{(lib) => _lib}/load-workspace.ts (100%) create mode 100644 apps/web/app/(dashboard)/home/[account]/billing/_components/billing-portal-form.tsx create mode 100644 apps/web/app/(dashboard)/home/[account]/billing/_components/team-account-checkout-form.tsx create mode 100644 apps/web/app/(dashboard)/home/[account]/billing/return/page.tsx create mode 100644 apps/web/app/(dashboard)/home/[account]/billing/server-actions.ts delete mode 100644 apps/web/app/(dashboard)/home/[account]/settings/subscription/return/components/recover-checkout.tsx delete mode 100644 apps/web/app/(dashboard)/home/[account]/settings/subscription/return/page.tsx rename apps/web/app/(dashboard)/home/{components => _components}/home-sidebar-account-selector.tsx (100%) rename apps/web/app/(dashboard)/home/{components => _components}/home-sidebar.tsx (67%) rename apps/web/app/(dashboard)/home/{components => _components}/personal-account-dropdown.tsx (68%) rename apps/web/app/(marketing)/{components => _components}/grid-list.tsx (100%) rename apps/web/app/(marketing)/{components => _components}/site-footer.tsx (100%) create mode 100644 apps/web/app/(marketing)/_components/site-header-account-section.tsx rename apps/web/app/(marketing)/{components => _components}/site-header.tsx (63%) rename apps/web/app/(marketing)/{components => _components}/site-navigation.tsx (100%) rename apps/web/app/(marketing)/{components => _components}/site-page-header.tsx (100%) rename apps/web/app/(marketing)/blog/{components => _components}/cover-image.tsx (100%) rename apps/web/app/(marketing)/blog/{components => _components}/date-formatter.tsx (100%) rename apps/web/app/(marketing)/blog/{components => _components}/draft-post-badge.tsx (100%) rename apps/web/app/(marketing)/blog/{components => _components}/post-header.tsx (86%) rename apps/web/app/(marketing)/blog/{components => _components}/post-preview.tsx (89%) rename apps/web/app/(marketing)/blog/{components => _components}/post.tsx (91%) delete mode 100644 apps/web/app/(marketing)/components/site-header-account-section.tsx rename apps/web/app/(marketing)/docs/{components => _components}/docs-card.tsx (100%) rename apps/web/app/(marketing)/docs/{components => _components}/docs-cards.tsx (100%) rename apps/web/app/(marketing)/docs/{components => _components}/docs-navigation.tsx (98%) rename apps/web/app/(marketing)/docs/{components => _components}/documentation-page-link.tsx (100%) rename apps/web/app/(marketing)/docs/{utils => _lib}/build-documentation-tree.ts (100%) rename apps/web/app/(marketing)/docs/{utils => _lib}/get-documentation-page-tree.ts (100%) delete mode 100644 apps/web/app/admin/layout.tsx delete mode 100644 apps/web/app/admin/lib/actions-utils.ts delete mode 100644 apps/web/app/admin/loading.tsx delete mode 100644 apps/web/app/admin/organizations/@modal/[uid]/actions.server.ts delete mode 100644 apps/web/app/admin/organizations/@modal/[uid]/components/DeleteOrganizationModal.tsx delete mode 100644 apps/web/app/admin/organizations/@modal/[uid]/delete/page.tsx delete mode 100644 apps/web/app/admin/organizations/@modal/default.tsx delete mode 100644 apps/web/app/admin/organizations/[uid]/members/components/OrganizationsMembersTable.tsx delete mode 100644 apps/web/app/admin/organizations/[uid]/members/page.tsx delete mode 100644 apps/web/app/admin/organizations/default.tsx delete mode 100644 apps/web/app/admin/organizations/error.tsx delete mode 100644 apps/web/app/admin/organizations/layout.tsx delete mode 100644 apps/web/app/admin/organizations/page.tsx delete mode 100644 apps/web/app/admin/organizations/queries.ts delete mode 100644 apps/web/app/admin/page.tsx delete mode 100644 apps/web/app/admin/users/@modal/[uid]/actions.server.ts delete mode 100644 apps/web/app/admin/users/@modal/[uid]/ban/page.tsx delete mode 100644 apps/web/app/admin/users/@modal/[uid]/components/BanUserModal.tsx delete mode 100644 apps/web/app/admin/users/@modal/[uid]/components/DeleteUserModal.tsx delete mode 100644 apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserAuthSetter.tsx delete mode 100644 apps/web/app/admin/users/@modal/[uid]/components/ImpersonateUserConfirmationModal.tsx delete mode 100644 apps/web/app/admin/users/@modal/[uid]/components/ReactivateUserModal.tsx delete mode 100644 apps/web/app/admin/users/@modal/[uid]/delete/page.tsx delete mode 100644 apps/web/app/admin/users/@modal/[uid]/impersonate/page.tsx delete mode 100644 apps/web/app/admin/users/@modal/[uid]/reactivate/page.tsx delete mode 100644 apps/web/app/admin/users/@modal/default.tsx delete mode 100644 apps/web/app/admin/users/[uid]/components/UserActionsDropdown.tsx delete mode 100644 apps/web/app/admin/users/[uid]/page.tsx delete mode 100644 apps/web/app/admin/users/default.tsx delete mode 100644 apps/web/app/admin/users/error.tsx delete mode 100644 apps/web/app/admin/users/layout.tsx delete mode 100644 apps/web/app/admin/users/page.tsx delete mode 100644 apps/web/app/admin/users/queries.ts delete mode 100644 apps/web/app/admin/utils/get-page-from-query-param.ts delete mode 100644 apps/web/app/admin/utils/is-user-super-admin.ts delete mode 100644 apps/web/app/join/_components/ExistingUserInviteForm.tsx delete mode 100644 apps/web/app/join/_components/NewUserInviteForm.tsx rename {apps/web/app/(dashboard)/home/[account]/settings/subscription/return => packages/billing-gateway/src}/components/billing-session-status.tsx (83%) create mode 100644 packages/features/team-accounts/src/schema/delete-invitation.schema.ts create mode 100644 packages/features/team-accounts/src/schema/update-invitation-schema.ts delete mode 100644 packages/ui/src/makerkit/pricing-table.tsx 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,