diff --git a/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/server-actions.ts b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/server-actions.ts index f45232c4f..f23eeae61 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/server-actions.ts +++ b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/server-actions.ts @@ -28,12 +28,15 @@ export const createPersonalAccountCheckoutSession = enhanceAction( * @name createPersonalAccountBillingPortalSession * @description Creates a billing Portal session for a personal account */ -export async function createPersonalAccountBillingPortalSession() { - const client = getSupabaseServerActionClient(); - const service = createUserBillingService(client); +export const createPersonalAccountBillingPortalSession = enhanceAction( + async () => { + const client = getSupabaseServerActionClient(); + const service = createUserBillingService(client); - // get url to billing portal - const url = await service.createBillingPortalSession(); + // get url to billing portal + const url = await service.createBillingPortalSession(); - return redirect(url); -} + return redirect(url); + }, + {}, +); diff --git a/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/server-actions.ts b/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/server-actions.ts index 27bc130fb..7b6a34c51 100644 --- a/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/server-actions.ts +++ b/apps/web/app/(dashboard)/home/[account]/billing/_lib/server/server-actions.ts @@ -2,8 +2,7 @@ import { redirect } from 'next/navigation'; -import { z } from 'zod'; - +import { enhanceAction } from '@kit/next/actions'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; // billing imports @@ -17,30 +16,34 @@ import { createTeamBillingService } from './team-billing.service'; * @name createTeamAccountCheckoutSession * @description Creates a checkout session for a team account. */ -export async function createTeamAccountCheckoutSession( - params: z.infer, -) { - const data = TeamCheckoutSchema.parse(params); +export const createTeamAccountCheckoutSession = enhanceAction( + (data) => { + const client = getSupabaseServerActionClient(); + const service = createTeamBillingService(client); - const client = getSupabaseServerActionClient(); - const service = createTeamBillingService(client); - - return service.createCheckout(data); -} + return service.createCheckout(data); + }, + { + schema: TeamCheckoutSchema, + }, +); /** * @name createBillingPortalSession * @description Creates a Billing Session Portal and redirects the user to the * provider's hosted instance */ -export async function createBillingPortalSession(formData: FormData) { - const params = TeamBillingPortalSchema.parse(Object.fromEntries(formData)); +export const createBillingPortalSession = enhanceAction( + async (formData: FormData) => { + const params = TeamBillingPortalSchema.parse(Object.fromEntries(formData)); - const client = getSupabaseServerActionClient(); - const service = createTeamBillingService(client); + const client = getSupabaseServerActionClient(); + const service = createTeamBillingService(client); - // get url to billing portal - const url = await service.createBillingPortalSession(params); + // get url to billing portal + const url = await service.createBillingPortalSession(params); - return redirect(url); -} + return redirect(url); + }, + {}, +); diff --git a/packages/features/accounts/package.json b/packages/features/accounts/package.json index bbfecd3fc..71bf4c16e 100644 --- a/packages/features/accounts/package.json +++ b/packages/features/accounts/package.json @@ -25,6 +25,7 @@ "@kit/eslint-config": "workspace:*", "@kit/mailers": "workspace:^", "@kit/monitoring": "workspace:^", + "@kit/next": "workspace:^", "@kit/prettier-config": "workspace:*", "@kit/shared": "workspace:^", "@kit/supabase": "workspace:^", diff --git a/packages/features/accounts/src/server/personal-accounts-server-actions.ts b/packages/features/accounts/src/server/personal-accounts-server-actions.ts index e9d30f55b..494f72254 100644 --- a/packages/features/accounts/src/server/personal-accounts-server-actions.ts +++ b/packages/features/accounts/src/server/personal-accounts-server-actions.ts @@ -5,8 +5,7 @@ import { redirect } from 'next/navigation'; import { z } from 'zod'; -import { getLogger } from '@kit/shared/logger'; -import { requireUser } from '@kit/supabase/require-user'; +import { enhanceAction } from '@kit/next/actions'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { DeletePersonalAccountSchema } from '../schema/delete-personal-account.schema'; @@ -22,56 +21,41 @@ export async function refreshAuthSession() { return {}; } -export async function deletePersonalAccountAction(formData: FormData) { - // validate the form data - const { success } = DeletePersonalAccountSchema.safeParse( - Object.fromEntries(formData.entries()), - ); - - if (!success) { - throw new Error('Invalid form data'); - } - - const client = getSupabaseServerActionClient(); - const auth = await requireUser(client); - - if (auth.error) { - const logger = await getLogger(); - - logger.error( - { - error: auth.error, - }, - `User is not authenticated. Redirecting to login page.`, +export const deletePersonalAccountAction = enhanceAction( + async (formData: FormData, user) => { + // validate the form data + const { success } = DeletePersonalAccountSchema.safeParse( + Object.fromEntries(formData.entries()), ); - redirect(auth.redirectTo); - } + if (!success) { + throw new Error('Invalid form data'); + } - // retrieve user ID and email - const userId = auth.data.id; - const userEmail = auth.data.email ?? null; + const client = getSupabaseServerActionClient(); - // create a new instance of the personal accounts service - const service = createDeletePersonalAccountService(); + // create a new instance of the personal accounts service + const service = createDeletePersonalAccountService(); - // delete the user's account and cancel all subscriptions - await service.deletePersonalAccount({ - adminClient: getSupabaseServerActionClient({ admin: true }), - userId, - userEmail, - emailSettings, - }); + // delete the user's account and cancel all subscriptions + await service.deletePersonalAccount({ + adminClient: getSupabaseServerActionClient({ admin: true }), + userId: user.id, + userEmail: user.email ?? null, + emailSettings, + }); - // sign out the user after deleting their account - await client.auth.signOut(); + // sign out the user after deleting their account + await client.auth.signOut(); - // clear the cache for all pages - revalidatePath('/', 'layout'); + // clear the cache for all pages + revalidatePath('/', 'layout'); - // redirect to the home page - redirect('/'); -} + // redirect to the home page + redirect('/'); + }, + {}, +); function getEmailSettingsFromEnvironment() { return z diff --git a/packages/features/team-accounts/package.json b/packages/features/team-accounts/package.json index b1c7c0fee..923fac7c5 100644 --- a/packages/features/team-accounts/package.json +++ b/packages/features/team-accounts/package.json @@ -24,6 +24,7 @@ "@kit/eslint-config": "workspace:*", "@kit/mailers": "workspace:^", "@kit/monitoring": "workspace:*", + "@kit/next": "workspace:^", "@kit/prettier-config": "workspace:*", "@kit/shared": "workspace:^", "@kit/supabase": "workspace:^", diff --git a/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts b/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts index ad2a32785..4756eb92e 100644 --- a/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/create-team-account-server-actions.ts @@ -4,7 +4,7 @@ import { redirect } from 'next/navigation'; import { z } from 'zod'; -import { requireUser } from '@kit/supabase/require-user'; +import { enhanceAction } from '@kit/next/actions'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { CreateTeamSchema } from '../../schema/create-team.schema'; @@ -17,31 +17,25 @@ const TEAM_ACCOUNTS_HOME_PATH = z .min(1) .parse(process.env.TEAM_ACCOUNTS_HOME_PATH); -export async function createOrganizationAccountAction( - params: z.infer, -) { - const { name: accountName } = CreateTeamSchema.parse(params); +export const createOrganizationAccountAction = enhanceAction( + async (params, user) => { + const client = getSupabaseServerActionClient(); + const service = createCreateTeamAccountService(client); - const client = getSupabaseServerActionClient(); - const auth = await requireUser(client); + const { data, error } = await service.createNewOrganizationAccount({ + name: params.name, + userId: user.id, + }); - if (auth.error) { - redirect(auth.redirectTo); - } + if (error) { + throw new Error('Error creating team account'); + } - const userId = auth.data.id; - const service = createCreateTeamAccountService(client); + const accountHomePath = TEAM_ACCOUNTS_HOME_PATH + '/' + data.slug; - const { data, error } = await service.createNewOrganizationAccount({ - name: accountName, - userId, - }); - - if (error) { - throw new Error('Error creating team account'); - } - - const accountHomePath = TEAM_ACCOUNTS_HOME_PATH + '/' + data.slug; - - redirect(accountHomePath); -} + redirect(accountHomePath); + }, + { + schema: CreateTeamSchema, + }, +); diff --git a/packages/features/team-accounts/src/server/actions/delete-team-account-server-actions.ts b/packages/features/team-accounts/src/server/actions/delete-team-account-server-actions.ts index a3bbafcef..3cf3c001f 100644 --- a/packages/features/team-accounts/src/server/actions/delete-team-account-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/delete-team-account-server-actions.ts @@ -4,63 +4,61 @@ import { redirect } from 'next/navigation'; import { SupabaseClient } from '@supabase/supabase-js'; +import { enhanceAction } from '@kit/next/actions'; import { Database } from '@kit/supabase/database'; -import { requireUser } from '@kit/supabase/require-user'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { DeleteTeamAccountSchema } from '../../schema/delete-team-account.schema'; import { createDeleteTeamAccountService } from '../services/delete-team-account.service'; -export async function deleteTeamAccountAction(formData: FormData) { - const params = DeleteTeamAccountSchema.parse( - Object.fromEntries(formData.entries()), - ); +export const deleteTeamAccountAction = enhanceAction( + async (formData: FormData, user) => { + const params = DeleteTeamAccountSchema.parse( + Object.fromEntries(formData.entries()), + ); - const client = getSupabaseServerActionClient(); - const auth = await requireUser(client); + const client = getSupabaseServerActionClient(); + const userId = user.id; + const accountId = params.accountId; - if (auth.error) { - throw new Error('Authentication required'); - } + // Check if the user has the necessary permissions to delete the team account + await assertUserPermissionsToDeleteTeamAccount(client, { + accountId, + userId, + }); - // Check if the user has the necessary permissions to delete the team account - await assertUserPermissionsToDeleteTeamAccount(client, params.accountId); + // Get the Supabase client and create a new service instance. + const service = createDeleteTeamAccountService(); - // Get the Supabase client and create a new service instance. - const service = createDeleteTeamAccountService(); + // Delete the team account and all associated data. + await service.deleteTeamAccount( + getSupabaseServerActionClient({ + admin: true, + }), + { + accountId, + userId, + }, + ); - // Delete the team account and all associated data. - await service.deleteTeamAccount( - getSupabaseServerActionClient({ - admin: true, - }), - { - accountId: params.accountId, - userId: auth.data.id, - }, - ); - - return redirect('/home'); -} + return redirect('/home'); + }, + {}, +); async function assertUserPermissionsToDeleteTeamAccount( client: SupabaseClient, - accountId: string, + params: { + accountId: string; + userId: string; + }, ) { - const auth = await requireUser(client); - - if (auth.error ?? !auth.data.id) { - throw new Error('Authentication required'); - } - - const userId = auth.data.id; - const { data, error } = await client .from('accounts') .select('id') - .eq('primary_owner_user_id', userId) + .eq('primary_owner_user_id', params.userId) .eq('is_personal_account', false) - .eq('id', accountId); + .eq('id', params.accountId); if (error ?? !data) { throw new Error('Account not found'); diff --git a/packages/features/team-accounts/src/server/actions/leave-team-account-server-actions.ts b/packages/features/team-accounts/src/server/actions/leave-team-account-server-actions.ts index bdaaad484..5fdb34223 100644 --- a/packages/features/team-accounts/src/server/actions/leave-team-account-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/leave-team-account-server-actions.ts @@ -3,33 +3,29 @@ import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; -import { requireUser } from '@kit/supabase/require-user'; +import { enhanceAction } from '@kit/next/actions'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { LeaveTeamAccountSchema } from '../../schema/leave-team-account.schema'; import { createLeaveTeamAccountService } from '../services/leave-team-account.service'; -export async function leaveTeamAccountAction(formData: FormData) { - const body = Object.fromEntries(formData.entries()); - const params = LeaveTeamAccountSchema.parse(body); +export const leaveTeamAccountAction = enhanceAction( + async (formData: FormData, user) => { + const body = Object.fromEntries(formData.entries()); + const params = LeaveTeamAccountSchema.parse(body); - const client = getSupabaseServerActionClient(); - const auth = await requireUser(client); + const service = createLeaveTeamAccountService( + getSupabaseServerActionClient({ admin: true }), + ); - if (auth.error) { - throw new Error('Authentication required'); - } + await service.leaveTeamAccount({ + accountId: params.accountId, + userId: user.id, + }); - const service = createLeaveTeamAccountService( - getSupabaseServerActionClient({ admin: true }), - ); + revalidatePath('/home/[account]', 'layout'); - await service.leaveTeamAccount({ - accountId: params.accountId, - userId: auth.data.id, - }); - - revalidatePath('/home/[account]', 'layout'); - - return redirect('/home'); -} + return redirect('/home'); + }, + {}, +); diff --git a/packages/features/team-accounts/src/server/actions/team-details-server-actions.ts b/packages/features/team-accounts/src/server/actions/team-details-server-actions.ts index a05686f8d..f20420e47 100644 --- a/packages/features/team-accounts/src/server/actions/team-details-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/team-details-server-actions.ts @@ -2,41 +2,43 @@ import { redirect } from 'next/navigation'; -import { z } from 'zod'; - +import { enhanceAction } from '@kit/next/actions'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { UpdateTeamNameSchema } from '../../schema/update-team-name.schema'; -export async function updateTeamAccountName( - params: z.infer, -) { - const client = getSupabaseServerComponentClient(); - const { name, slug, path } = UpdateTeamNameSchema.parse(params); +export const updateTeamAccountName = enhanceAction( + async (params) => { + const client = getSupabaseServerComponentClient(); + const { name, path, slug } = params; - const { error, data } = await client - .from('accounts') - .update({ - name, - slug, - }) - .match({ - slug, - }) - .select('slug') - .single(); + const { error, data } = await client + .from('accounts') + .update({ + name, + slug, + }) + .match({ + slug, + }) + .select('slug') + .single(); - if (error) { - throw error; - } + if (error) { + throw error; + } - const newSlug = data.slug; + const newSlug = data.slug; - if (newSlug) { - const nextPath = path.replace('[account]', newSlug); + if (newSlug) { + const nextPath = path.replace('[account]', newSlug); - redirect(nextPath); - } + redirect(nextPath); + } - return { success: true }; -} + return { success: true }; + }, + { + schema: UpdateTeamNameSchema, + }, +); diff --git a/packages/features/team-accounts/src/server/actions/team-invitations-server-actions.ts b/packages/features/team-accounts/src/server/actions/team-invitations-server-actions.ts index 9f1a317aa..a31b7dc68 100644 --- a/packages/features/team-accounts/src/server/actions/team-invitations-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/team-invitations-server-actions.ts @@ -3,12 +3,9 @@ import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; -import { SupabaseClient } from '@supabase/supabase-js'; - import { z } from 'zod'; -import { Database } from '@kit/supabase/database'; -import { requireUser } from '@kit/supabase/require-user'; +import { enhanceAction } from '@kit/next/actions'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { AcceptInvitationSchema } from '../../schema/accept-invitation.schema'; @@ -20,146 +17,141 @@ import { createAccountInvitationsService } from '../services/account-invitations import { createAccountPerSeatBillingService } from '../services/account-per-seat-billing.service'; /** - * Creates invitations for inviting members. + * @name createInvitationsAction + * @description Creates invitations for inviting members. */ -export async function createInvitationsAction(params: { - accountSlug: string; - invitations: z.infer['invitations']; -}) { - const client = getSupabaseServerActionClient(); +export const createInvitationsAction = enhanceAction( + async (params) => { + const client = getSupabaseServerActionClient(); - await assertSession(client); + // Create the service + const service = createAccountInvitationsService(client); - const { invitations } = InviteMembersSchema.parse({ - invitations: params.invitations, - }); + // send invitations + await service.sendInvitations(params); - // Create the service - const service = createAccountInvitationsService(client); + revalidateMemberPage(); - // send invitations - await service.sendInvitations({ - invitations, - accountSlug: params.accountSlug, - }); - - revalidateMemberPage(); - - return { - success: true, - }; -} + return { + success: true, + }; + }, + { + schema: InviteMembersSchema.and( + z.object({ + accountSlug: z.string().min(1), + }), + ), + }, +); /** - * Deletes an invitation specified by the invitation ID. - * - * @param {Object} params - The parameters for the method. - * @param {string} params.invitationId - The ID of the invitation to be deleted. - * - * @return {Object} - The result of the delete operation. + * @name deleteInvitationAction + * @description Deletes an invitation specified by the invitation ID. */ -export async function deleteInvitationAction( - params: z.infer, -) { - const invitation = DeleteInvitationSchema.parse(params); +export const deleteInvitationAction = enhanceAction( + async (data) => { + const client = getSupabaseServerActionClient(); + const service = createAccountInvitationsService(client); - const client = getSupabaseServerActionClient(); - const { data, error } = await client.auth.getUser(); + // Delete the invitation + await service.deleteInvitation(data); - if (error ?? !data.user) { - throw new Error(`Authentication required`); - } + revalidateMemberPage(); - const service = createAccountInvitationsService(client); + return { + success: true, + }; + }, + { + schema: DeleteInvitationSchema, + }, +); - // Delete the invitation - await service.deleteInvitation(invitation); +/** + * @name updateInvitationAction + * @description Updates an invitation. + */ +export const updateInvitationAction = enhanceAction( + async (invitation) => { + const client = getSupabaseServerActionClient(); + const service = createAccountInvitationsService(client); - revalidateMemberPage(); + await service.updateInvitation(invitation); - return { success: true }; -} + revalidateMemberPage(); -export async function updateInvitationAction( - params: z.infer, -) { - const client = getSupabaseServerActionClient(); - const invitation = UpdateInvitationSchema.parse(params); + return { + success: true, + }; + }, + { + schema: UpdateInvitationSchema, + }, +); - await assertSession(client); +/** + * @name acceptInvitationAction + * @description Accepts an invitation to join a team. + */ +export const acceptInvitationAction = enhanceAction( + async (data: FormData, user) => { + const client = getSupabaseServerActionClient(); - const service = createAccountInvitationsService(client); + const { inviteToken, nextPath } = AcceptInvitationSchema.parse( + Object.fromEntries(data), + ); - await service.updateInvitation(invitation); + // create the services + const perSeatBillingService = createAccountPerSeatBillingService(client); + const service = createAccountInvitationsService(client); - revalidateMemberPage(); + // Accept the invitation + const accountId = await service.acceptInvitationToTeam( + getSupabaseServerActionClient({ admin: true }), + { + inviteToken, + userId: user.id, + }, + ); - return { success: true }; -} + // If the account ID is not present, throw an error + if (!accountId) { + throw new Error('Failed to accept invitation'); + } -export async function acceptInvitationAction(data: FormData) { - const client = getSupabaseServerActionClient(); + // Increase the seats for the account + await perSeatBillingService.increaseSeats(accountId); - const { inviteToken, nextPath } = AcceptInvitationSchema.parse( - Object.fromEntries(data), - ); + return redirect(nextPath); + }, + {}, +); - // Ensure the user is authenticated - const user = await assertSession(client); +/** + * @name renewInvitationAction + * @description Renews an invitation. + */ +export const renewInvitationAction = enhanceAction( + async (params) => { + const client = getSupabaseServerActionClient(); + const { invitationId } = RenewInvitationSchema.parse(params); - // create the services - const perSeatBillingService = createAccountPerSeatBillingService(client); - const service = createAccountInvitationsService(client); + const service = createAccountInvitationsService(client); - // Accept the invitation - const accountId = await service.acceptInvitationToTeam( - getSupabaseServerActionClient({ admin: true }), - { - inviteToken, - userId: user.id, - }, - ); + // Renew the invitation + await service.renewInvitation(invitationId); - // If the account ID is not present, throw an error - if (!accountId) { - throw new Error('Failed to accept invitation'); - } + revalidateMemberPage(); - // Increase the seats for the account - await perSeatBillingService.increaseSeats(accountId); - - return redirect(nextPath); -} - -export async function renewInvitationAction( - params: z.infer, -) { - const client = getSupabaseServerActionClient(); - const { invitationId } = RenewInvitationSchema.parse(params); - - await assertSession(client); - - const service = createAccountInvitationsService(client); - - // Renew the invitation - await service.renewInvitation(invitationId); - - revalidateMemberPage(); - - return { - success: true, - }; -} - -async function assertSession(client: SupabaseClient) { - const { error, data } = await requireUser(client); - - if (error) { - throw new Error(`Authentication required`); - } - - return data; -} + return { + success: true, + }; + }, + { + schema: RenewInvitationSchema, + }, +); function revalidateMemberPage() { revalidatePath('/home/[account]/members', 'page'); diff --git a/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts b/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts index ba13e8faa..fcd754386 100644 --- a/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts @@ -2,11 +2,7 @@ import { revalidatePath } from 'next/cache'; -import { SupabaseClient } from '@supabase/supabase-js'; - -import { z } from 'zod'; - -import { Database } from '@kit/supabase/database'; +import { enhanceAction } from '@kit/next/actions'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { RemoveMemberSchema } from '../../schema/remove-member.schema'; @@ -14,106 +10,89 @@ import { TransferOwnershipConfirmationSchema } from '../../schema/transfer-owner import { UpdateMemberRoleSchema } from '../../schema/update-member-role.schema'; import { createAccountMembersService } from '../services/account-members.service'; -export async function removeMemberFromAccountAction( - params: z.infer, -) { - const client = getSupabaseServerActionClient(); - const { data, error } = await client.auth.getUser(); +/** + * @name removeMemberFromAccountAction + * @description Removes a member from an account. + */ +export const removeMemberFromAccountAction = enhanceAction( + async ({ accountId, userId }) => { + const client = getSupabaseServerActionClient(); + const service = createAccountMembersService(client); - if (error ?? !data.user) { - throw new Error(`Authentication required`); - } - - const { accountId, userId } = RemoveMemberSchema.parse(params); - - const service = createAccountMembersService(client); - - await service.removeMemberFromAccount({ - accountId, - userId, - }); - - // revalidate all pages that depend on the account - revalidatePath('/home/[account]', 'layout'); - - return { success: true }; -} - -export async function updateMemberRoleAction( - params: z.infer, -) { - const client = getSupabaseServerActionClient(); - - await assertSession(client); - - const service = createAccountMembersService(client); - const adminClient = getSupabaseServerActionClient({ admin: true }); - - // update the role of the member - await service.updateMemberRole( - { - accountId: params.accountId, - userId: params.userId, - role: params.role, - }, - adminClient, - ); - - // revalidate all pages that depend on the account - revalidatePath('/home/[account]', 'layout'); - - return { success: true }; -} - -export async function transferOwnershipAction( - params: z.infer, -) { - const client = getSupabaseServerActionClient(); - - const { accountId, userId } = - TransferOwnershipConfirmationSchema.parse(params); - - // assert that the user is authenticated - await assertSession(client); - - // assert that the user is the owner of the account - const { data: isOwner, error } = await client.rpc('is_account_owner', { - account_id: accountId, - }); - - if (error ?? !isOwner) { - throw new Error( - `You must be the owner of the account to transfer ownership`, - ); - } - - const service = createAccountMembersService(client); - - // at this point, the user is authenticated and is the owner of the account - // so we proceed with the transfer of ownership with admin privileges - const adminClient = getSupabaseServerActionClient({ admin: true }); - - await service.transferOwnership( - { + await service.removeMemberFromAccount({ accountId, userId, - confirmation: params.confirmation, - }, - adminClient, - ); + }); - // revalidate all pages that depend on the account - revalidatePath('/home/[account]', 'layout'); + // revalidate all pages that depend on the account + revalidatePath('/home/[account]', 'layout'); - return { - success: true, - }; -} + return { success: true }; + }, + { + schema: RemoveMemberSchema, + }, +); -async function assertSession(client: SupabaseClient) { - const { data, error } = await client.auth.getUser(); +/** + * @name updateMemberRoleAction + * @description Updates the role of a member in an account. + */ +export const updateMemberRoleAction = enhanceAction( + async (data) => { + const client = getSupabaseServerActionClient(); + const service = createAccountMembersService(client); + const adminClient = getSupabaseServerActionClient({ admin: true }); - if (error ?? !data.user) { - throw new Error(`Authentication required`); - } -} + // update the role of the member + await service.updateMemberRole(data, adminClient); + + // revalidate all pages that depend on the account + revalidatePath('/home/[account]', 'layout'); + + return { success: true }; + }, + { + schema: UpdateMemberRoleSchema, + }, +); + +/** + * @name transferOwnershipAction + * @description Transfers the ownership of an account to another member. + */ +export const transferOwnershipAction = enhanceAction( + async (data) => { + const client = getSupabaseServerActionClient(); + + // assert that the user is the owner of the account + const { data: isOwner, error } = await client.rpc('is_account_owner', { + account_id: data.accountId, + }); + + if (error ?? !isOwner) { + throw new Error( + `You must be the owner of the account to transfer ownership`, + ); + } + + const service = createAccountMembersService(client); + + // at this point, the user is authenticated and is the owner of the account + // so we proceed with the transfer of ownership with admin privileges + const adminClient = getSupabaseServerActionClient({ admin: true }); + + // transfer the ownership of the account + await service.transferOwnership(data, adminClient); + + // revalidate all pages that depend on the account + revalidatePath('/home/[account]', 'layout'); + + return { + success: true, + }; + }, + { + schema: TransferOwnershipConfirmationSchema, + }, +); diff --git a/packages/next/src/actions/index.ts b/packages/next/src/actions/index.ts index 1eb8cb041..1b2a8b5a6 100644 --- a/packages/next/src/actions/index.ts +++ b/packages/next/src/actions/index.ts @@ -4,7 +4,7 @@ import { redirect } from 'next/navigation'; import type { User } from '@supabase/supabase-js'; -import { z } from 'zod'; +import { ZodType, z } from 'zod'; import { verifyCaptchaToken } from '@kit/auth/captcha/server'; import { requireUser } from '@kit/supabase/require-user'; @@ -12,6 +12,12 @@ import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-clie import { captureException, zodParseFactory } from '../utils'; +/** + * @name IS_CAPTCHA_SETUP + * @description Check if the CAPTCHA is setup + */ +const IS_CAPTCHA_SETUP = !!process.env.CAPTCHA_SECRET_TOKEN; + /** * * @name enhanceAction @@ -24,19 +30,21 @@ export function enhanceAction< auth?: boolean; captcha?: boolean; captureException?: boolean; - schema: z.ZodType< + schema?: z.ZodType< Config['captcha'] extends true ? Args & { captchaToken: string } : Args, z.ZodTypeDef >; }, >( fn: ( - params: z.infer, + params: Config['schema'] extends ZodType ? z.infer : Args, user: Config['auth'] extends false ? undefined : User, ) => Response | Promise, config: Config, ) { - return async (params: z.infer) => { + return async ( + params: Config['schema'] extends ZodType ? z.infer : Args, + ) => { type UserParam = Config['auth'] extends false ? undefined : User; const requireAuth = config.auth ?? true; @@ -56,11 +64,15 @@ export function enhanceAction< } // validate the schema - const parsed = zodParseFactory(config.schema); - const data = parsed(params); + const data = config.schema + ? zodParseFactory(config.schema)(params) + : params; + // verify captcha unless it's explicitly disabled // verify the captcha token if required - if (config.captcha) { + const verifyCaptcha = config.captcha ?? IS_CAPTCHA_SETUP; + + if (verifyCaptcha) { const token = (data as Args & { captchaToken: string }).captchaToken; // Verify the CAPTCHA token. It will throw an error if the token is invalid. diff --git a/packages/next/src/routes/index.ts b/packages/next/src/routes/index.ts index 56b56e7c9..0a6744f95 100644 --- a/packages/next/src/routes/index.ts +++ b/packages/next/src/routes/index.ts @@ -19,6 +19,12 @@ interface HandlerParams { body: Body; } +/** + * @name IS_CAPTCHA_SETUP + * @description Check if the CAPTCHA is setup + */ +const IS_CAPTCHA_SETUP = !!process.env.CAPTCHA_SECRET_TOKEN; + /** * Enhanced route handler function. * @@ -63,8 +69,10 @@ export const enhanceRouteHandler = < * This function takes a request object as an argument and returns a response object. */ return async function routeHandler(request: NextRequest) { - // Verify the captcha token if required - if (params?.captcha) { + const shouldVerifyCaptcha = params?.captcha ?? IS_CAPTCHA_SETUP; + + // Verify the captcha token if required and setup + if (shouldVerifyCaptcha) { const token = captchaTokenGetter(request); // If the captcha token is not provided, return a 400 response. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d0c41f3d..e5c3ccde9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -561,6 +561,9 @@ importers: '@kit/monitoring': specifier: workspace:^ version: link:../../monitoring/api + '@kit/next': + specifier: workspace:^ + version: link:../../next '@kit/prettier-config': specifier: workspace:* version: link:../../../tooling/prettier @@ -772,6 +775,9 @@ importers: '@kit/monitoring': specifier: workspace:* version: link:../../monitoring/api + '@kit/next': + specifier: workspace:^ + version: link:../../next '@kit/prettier-config': specifier: workspace:* version: link:../../../tooling/prettier