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 index cd419bd8e..170312cd5 100644 --- 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 @@ -20,7 +20,7 @@ import { Trans } from '@kit/ui/trans'; import billingConfig from '~/config/billing.config'; -import { createPersonalAccountCheckoutSession } from '../server-actions'; +import { createPersonalAccountCheckoutSession } from '../_lib/server/server-actions'; const EmbeddedCheckout = dynamic( async () => { diff --git a/apps/web/app/(dashboard)/home/(user)/billing/server-actions.ts b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/server-actions.ts similarity index 84% rename from apps/web/app/(dashboard)/home/(user)/billing/server-actions.ts rename to apps/web/app/(dashboard)/home/(user)/billing/_lib/server/server-actions.ts index 485168c92..f3a692bc5 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/server-actions.ts +++ b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/server-actions.ts @@ -5,8 +5,8 @@ import { redirect } from 'next/navigation'; import { enhanceAction } from '@kit/next/actions'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; -import { PersonalAccountCheckoutSchema } from './_lib/schema/personal-account-checkout.schema'; -import { UserBillingService } from './_lib/server/user-billing.service'; +import { PersonalAccountCheckoutSchema } from '../schema/personal-account-checkout.schema'; +import { UserBillingService } from './user-billing.service'; /** * @name createPersonalAccountCheckoutSession diff --git a/apps/web/app/(dashboard)/home/(user)/billing/page.tsx b/apps/web/app/(dashboard)/home/(user)/billing/page.tsx index 3c54dfab4..c11b65169 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/page.tsx +++ b/apps/web/app/(dashboard)/home/(user)/billing/page.tsx @@ -16,7 +16,7 @@ import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; import { UserAccountHeader } from '../_components/user-account-header'; -import { createPersonalAccountBillingPortalSession } from '../billing/server-actions'; +import { createPersonalAccountBillingPortalSession } from '../billing/_lib/server/server-actions'; import { PersonalAccountCheckoutForm } from './_components/personal-account-checkout-form'; // user billing imports import { loadPersonalAccountBillingPageData } from './_lib/server/personal-account-billing-page.loader'; 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 index 3ce782c03..6152ac09f 100644 --- 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 @@ -17,7 +17,7 @@ import { Trans } from '@kit/ui/trans'; import billingConfig from '~/config/billing.config'; -import { createTeamAccountCheckoutSession } from '../server-actions'; +import { createTeamAccountCheckoutSession } from '../_lib/server/server-actions'; const EmbeddedCheckout = dynamic( async () => { diff --git a/apps/web/app/(dashboard)/home/[account]/_lib/schema/team-billing.schema.ts b/apps/web/app/(dashboard)/home/[account]/billing/_lib/schema/team-billing.schema.ts similarity index 100% rename from apps/web/app/(dashboard)/home/[account]/_lib/schema/team-billing.schema.ts rename to apps/web/app/(dashboard)/home/[account]/billing/_lib/schema/team-billing.schema.ts diff --git a/apps/web/app/(dashboard)/home/[account]/billing/server-actions.ts b/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/server-actions.ts similarity index 90% rename from apps/web/app/(dashboard)/home/[account]/billing/server-actions.ts rename to apps/web/app/(dashboard)/home/[account]/billing/_lib/server/server-actions.ts index 1de169330..d762a22d2 100644 --- a/apps/web/app/(dashboard)/home/[account]/billing/server-actions.ts +++ b/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/server-actions.ts @@ -10,8 +10,8 @@ import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-clie import { TeamBillingPortalSchema, TeamCheckoutSchema, -} from '../_lib/schema/team-billing.schema'; -import { TeamBillingService } from '../_lib/server/team-billing.service'; +} from '../schema/team-billing.schema'; +import { TeamBillingService } from './team-billing.service'; /** * @name createTeamAccountCheckoutSession diff --git a/apps/web/app/(dashboard)/home/[account]/_lib/server/team-billing.service.ts b/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/team-billing.service.ts similarity index 99% rename from apps/web/app/(dashboard)/home/[account]/_lib/server/team-billing.service.ts rename to apps/web/app/(dashboard)/home/[account]/billing/_lib/server/team-billing.service.ts index 3f89d77e3..0554a24d2 100644 --- a/apps/web/app/(dashboard)/home/[account]/_lib/server/team-billing.service.ts +++ b/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/team-billing.service.ts @@ -16,7 +16,7 @@ import billingConfig from '~/config/billing.config'; import pathsConfig from '~/config/paths.config'; import { Database } from '~/lib/database.types'; -import { TeamCheckoutSchema } from '../../_lib/schema/team-billing.schema'; +import { TeamCheckoutSchema } from '../schema/team-billing.schema'; export class TeamBillingService { private readonly namespace = 'billing.team-account'; diff --git a/apps/web/app/(dashboard)/home/[account]/billing/page.tsx b/apps/web/app/(dashboard)/home/[account]/billing/page.tsx index 0a0da8f16..cfef36d16 100644 --- a/apps/web/app/(dashboard)/home/[account]/billing/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/billing/page.tsx @@ -18,8 +18,8 @@ import { withI18n } from '~/lib/i18n/with-i18n'; import { AccountLayoutHeader } from '../_components/account-layout-header'; import { loadTeamAccountBillingPage } from '../_lib/server/team-account-billing-page.loader'; import { loadTeamWorkspace } from '../_lib/server/team-account-workspace.loader'; -import { createBillingPortalSession } from '../billing/server-actions'; import { TeamAccountCheckoutForm } from './_components/team-account-checkout-form'; +import { createBillingPortalSession } from './_lib/server/server-actions'; interface Params { params: { diff --git a/apps/web/app/join/_lib/server/join-team.service.ts b/apps/web/app/join/_lib/server/join-team.service.ts deleted file mode 100644 index a77ebc08a..000000000 --- a/apps/web/app/join/_lib/server/join-team.service.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; - -export class JoinTeamService { - async isCurrentUserAlreadyInAccount(accountId: string) { - const client = getSupabaseServerComponentClient(); - - const { data } = await client - .from('accounts') - .select('id') - .eq('id', accountId) - .maybeSingle(); - - return !!data?.id; - } - - async 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: invitation, error } = await adminClient - .from('invitations') - .select< - string, - { - id: string; - account: { - id: string; - name: string; - slug: string; - picture_url: string; - }; - } - >( - 'id, expires_at, account: account_id !inner (id, name, slug, picture_url)', - ) - .eq('invite_token', token) - .gte('expires_at', new Date().toISOString()) - .single(); - - if (!invitation ?? error) { - return null; - } - - return invitation; - } -} diff --git a/apps/web/app/join/page.tsx b/apps/web/app/join/page.tsx index 3ecbe38bc..0dedd984d 100644 --- a/apps/web/app/join/page.tsx +++ b/apps/web/app/join/page.tsx @@ -5,6 +5,7 @@ import { ArrowLeft } from 'lucide-react'; import { requireUser } from '@kit/supabase/require-user'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; +import { createTeamAccountsApi } from '@kit/team-accounts/api'; import { AcceptInvitationContainer } from '@kit/team-accounts/components'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; @@ -14,8 +15,6 @@ import pathsConfig from '~/config/paths.config'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; -import { JoinTeamService } from './_lib/server/join-team.service'; - interface Context { searchParams: { invite_token?: string; @@ -50,10 +49,12 @@ async function JoinTeamAccountPage({ searchParams }: Context) { permanentRedirect(path); } - const service = new JoinTeamService(); + // get api to interact with team accounts + const adminClient = getSupabaseServerComponentClient({ admin: true }); + const api = createTeamAccountsApi(client); // the user is logged in, we can now check if the token is valid - const invitation = await service.getInviteDataFromInviteToken(token); + const invitation = await api.getInvitation(adminClient, token); // the invitation is not found or expired if (!invitation) { @@ -61,10 +62,12 @@ async function JoinTeamAccountPage({ searchParams }: Context) { } // we need to verify the user isn't already in the account - const isSignedInUserPartOfAccount = - await service.isCurrentUserAlreadyInAccount(invitation.account.id); + // we do so by checking if the user can read the account + // if the user can read the account, then they are already in the account + const account = await api.getTeamAccountById(invitation.account.id); - if (isSignedInUserPartOfAccount) { + // if the user is already in the account redirect to the home page + if (account) { const { getLogger } = await import('@kit/shared/logger'); const logger = await getLogger(); diff --git a/packages/features/team-accounts/src/server/api.ts b/packages/features/team-accounts/src/server/api.ts index e611d9d69..b9abc2e2d 100644 --- a/packages/features/team-accounts/src/server/api.ts +++ b/packages/features/team-accounts/src/server/api.ts @@ -10,6 +10,25 @@ import { Database } from '@kit/supabase/database'; export class TeamAccountsApi { constructor(private readonly client: SupabaseClient) {} + /** + * @name getTeamAccountById + * @description Check if the user is already in the account. + * @param accountId + */ + async getTeamAccountById(accountId: string) { + const { data, error } = await this.client + .from('accounts') + .select('*') + .eq('id', accountId) + .maybeSingle(); + + if (error) { + throw error; + } + + return data; + } + /** * @name getAccountWorkspace * @description Get the account workspace data. @@ -135,6 +154,40 @@ export class TeamAccountsApi { return data?.customer_id; } + + /** + * @name getInvitation + * @description Get the invitation data from the invite token. + * @param adminClient - The admin client instance. Since the user is not yet part of the account, we need to use an admin client to read the pending membership + * @param token - The invitation token. + */ + async getInvitation(adminClient: SupabaseClient, token: string) { + const { data: invitation, error } = await adminClient + .from('invitations') + .select< + string, + { + id: string; + account: { + id: string; + name: string; + slug: string; + picture_url: string; + }; + } + >( + 'id, expires_at, account: account_id !inner (id, name, slug, picture_url)', + ) + .eq('invite_token', token) + .gte('expires_at', new Date().toISOString()) + .single(); + + if (!invitation ?? error) { + return null; + } + + return invitation; + } } export function createTeamAccountsApi(client: SupabaseClient) {