Refactor account server actions using the enhanced action helper

The enhanced action helper has been utilized to refactor account-related server actions across the codebase. This change aims to streamline the server-side handling of user accounts, team accounts, and related functionality. As a result, various account-related server actions have now been wrapped with the helper, providing uniformity and consistency in action handling.
This commit is contained in:
giancarlo
2024-04-27 18:31:11 +07:00
parent ec59d02fb0
commit 0616d3b288
14 changed files with 388 additions and 409 deletions

View File

@@ -28,7 +28,8 @@ export const createPersonalAccountCheckoutSession = enhanceAction(
* @name createPersonalAccountBillingPortalSession * @name createPersonalAccountBillingPortalSession
* @description Creates a billing Portal session for a personal account * @description Creates a billing Portal session for a personal account
*/ */
export async function createPersonalAccountBillingPortalSession() { export const createPersonalAccountBillingPortalSession = enhanceAction(
async () => {
const client = getSupabaseServerActionClient(); const client = getSupabaseServerActionClient();
const service = createUserBillingService(client); const service = createUserBillingService(client);
@@ -36,4 +37,6 @@ export async function createPersonalAccountBillingPortalSession() {
const url = await service.createBillingPortalSession(); const url = await service.createBillingPortalSession();
return redirect(url); return redirect(url);
} },
{},
);

View File

@@ -2,8 +2,7 @@
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { z } from 'zod'; import { enhanceAction } from '@kit/next/actions';
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
// billing imports // billing imports
@@ -17,23 +16,25 @@ import { createTeamBillingService } from './team-billing.service';
* @name createTeamAccountCheckoutSession * @name createTeamAccountCheckoutSession
* @description Creates a checkout session for a team account. * @description Creates a checkout session for a team account.
*/ */
export async function createTeamAccountCheckoutSession( export const createTeamAccountCheckoutSession = enhanceAction(
params: z.infer<typeof TeamCheckoutSchema>, (data) => {
) {
const data = TeamCheckoutSchema.parse(params);
const client = getSupabaseServerActionClient(); const client = getSupabaseServerActionClient();
const service = createTeamBillingService(client); const service = createTeamBillingService(client);
return service.createCheckout(data); return service.createCheckout(data);
} },
{
schema: TeamCheckoutSchema,
},
);
/** /**
* @name createBillingPortalSession * @name createBillingPortalSession
* @description Creates a Billing Session Portal and redirects the user to the * @description Creates a Billing Session Portal and redirects the user to the
* provider's hosted instance * provider's hosted instance
*/ */
export async function createBillingPortalSession(formData: FormData) { export const createBillingPortalSession = enhanceAction(
async (formData: FormData) => {
const params = TeamBillingPortalSchema.parse(Object.fromEntries(formData)); const params = TeamBillingPortalSchema.parse(Object.fromEntries(formData));
const client = getSupabaseServerActionClient(); const client = getSupabaseServerActionClient();
@@ -43,4 +44,6 @@ export async function createBillingPortalSession(formData: FormData) {
const url = await service.createBillingPortalSession(params); const url = await service.createBillingPortalSession(params);
return redirect(url); return redirect(url);
} },
{},
);

View File

@@ -25,6 +25,7 @@
"@kit/eslint-config": "workspace:*", "@kit/eslint-config": "workspace:*",
"@kit/mailers": "workspace:^", "@kit/mailers": "workspace:^",
"@kit/monitoring": "workspace:^", "@kit/monitoring": "workspace:^",
"@kit/next": "workspace:^",
"@kit/prettier-config": "workspace:*", "@kit/prettier-config": "workspace:*",
"@kit/shared": "workspace:^", "@kit/shared": "workspace:^",
"@kit/supabase": "workspace:^", "@kit/supabase": "workspace:^",

View File

@@ -5,8 +5,7 @@ import { redirect } from 'next/navigation';
import { z } from 'zod'; import { z } from 'zod';
import { getLogger } from '@kit/shared/logger'; import { enhanceAction } from '@kit/next/actions';
import { requireUser } from '@kit/supabase/require-user';
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
import { DeletePersonalAccountSchema } from '../schema/delete-personal-account.schema'; import { DeletePersonalAccountSchema } from '../schema/delete-personal-account.schema';
@@ -22,7 +21,8 @@ export async function refreshAuthSession() {
return {}; return {};
} }
export async function deletePersonalAccountAction(formData: FormData) { export const deletePersonalAccountAction = enhanceAction(
async (formData: FormData, user) => {
// validate the form data // validate the form data
const { success } = DeletePersonalAccountSchema.safeParse( const { success } = DeletePersonalAccountSchema.safeParse(
Object.fromEntries(formData.entries()), Object.fromEntries(formData.entries()),
@@ -33,24 +33,6 @@ export async function deletePersonalAccountAction(formData: FormData) {
} }
const client = getSupabaseServerActionClient(); 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.`,
);
redirect(auth.redirectTo);
}
// retrieve user ID and email
const userId = auth.data.id;
const userEmail = auth.data.email ?? null;
// create a new instance of the personal accounts service // create a new instance of the personal accounts service
const service = createDeletePersonalAccountService(); const service = createDeletePersonalAccountService();
@@ -58,8 +40,8 @@ export async function deletePersonalAccountAction(formData: FormData) {
// delete the user's account and cancel all subscriptions // delete the user's account and cancel all subscriptions
await service.deletePersonalAccount({ await service.deletePersonalAccount({
adminClient: getSupabaseServerActionClient({ admin: true }), adminClient: getSupabaseServerActionClient({ admin: true }),
userId, userId: user.id,
userEmail, userEmail: user.email ?? null,
emailSettings, emailSettings,
}); });
@@ -71,7 +53,9 @@ export async function deletePersonalAccountAction(formData: FormData) {
// redirect to the home page // redirect to the home page
redirect('/'); redirect('/');
} },
{},
);
function getEmailSettingsFromEnvironment() { function getEmailSettingsFromEnvironment() {
return z return z

View File

@@ -24,6 +24,7 @@
"@kit/eslint-config": "workspace:*", "@kit/eslint-config": "workspace:*",
"@kit/mailers": "workspace:^", "@kit/mailers": "workspace:^",
"@kit/monitoring": "workspace:*", "@kit/monitoring": "workspace:*",
"@kit/next": "workspace:^",
"@kit/prettier-config": "workspace:*", "@kit/prettier-config": "workspace:*",
"@kit/shared": "workspace:^", "@kit/shared": "workspace:^",
"@kit/supabase": "workspace:^", "@kit/supabase": "workspace:^",

View File

@@ -4,7 +4,7 @@ import { redirect } from 'next/navigation';
import { z } from 'zod'; 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 { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
import { CreateTeamSchema } from '../../schema/create-team.schema'; import { CreateTeamSchema } from '../../schema/create-team.schema';
@@ -17,24 +17,14 @@ const TEAM_ACCOUNTS_HOME_PATH = z
.min(1) .min(1)
.parse(process.env.TEAM_ACCOUNTS_HOME_PATH); .parse(process.env.TEAM_ACCOUNTS_HOME_PATH);
export async function createOrganizationAccountAction( export const createOrganizationAccountAction = enhanceAction(
params: z.infer<typeof CreateTeamSchema>, async (params, user) => {
) {
const { name: accountName } = CreateTeamSchema.parse(params);
const client = getSupabaseServerActionClient(); const client = getSupabaseServerActionClient();
const auth = await requireUser(client);
if (auth.error) {
redirect(auth.redirectTo);
}
const userId = auth.data.id;
const service = createCreateTeamAccountService(client); const service = createCreateTeamAccountService(client);
const { data, error } = await service.createNewOrganizationAccount({ const { data, error } = await service.createNewOrganizationAccount({
name: accountName, name: params.name,
userId, userId: user.id,
}); });
if (error) { if (error) {
@@ -44,4 +34,8 @@ export async function createOrganizationAccountAction(
const accountHomePath = TEAM_ACCOUNTS_HOME_PATH + '/' + data.slug; const accountHomePath = TEAM_ACCOUNTS_HOME_PATH + '/' + data.slug;
redirect(accountHomePath); redirect(accountHomePath);
} },
{
schema: CreateTeamSchema,
},
);

View File

@@ -4,27 +4,28 @@ import { redirect } from 'next/navigation';
import { SupabaseClient } from '@supabase/supabase-js'; import { SupabaseClient } from '@supabase/supabase-js';
import { enhanceAction } from '@kit/next/actions';
import { Database } from '@kit/supabase/database'; import { Database } from '@kit/supabase/database';
import { requireUser } from '@kit/supabase/require-user';
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
import { DeleteTeamAccountSchema } from '../../schema/delete-team-account.schema'; import { DeleteTeamAccountSchema } from '../../schema/delete-team-account.schema';
import { createDeleteTeamAccountService } from '../services/delete-team-account.service'; import { createDeleteTeamAccountService } from '../services/delete-team-account.service';
export async function deleteTeamAccountAction(formData: FormData) { export const deleteTeamAccountAction = enhanceAction(
async (formData: FormData, user) => {
const params = DeleteTeamAccountSchema.parse( const params = DeleteTeamAccountSchema.parse(
Object.fromEntries(formData.entries()), Object.fromEntries(formData.entries()),
); );
const client = getSupabaseServerActionClient(); const client = getSupabaseServerActionClient();
const auth = await requireUser(client); 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 // Check if the user has the necessary permissions to delete the team account
await assertUserPermissionsToDeleteTeamAccount(client, params.accountId); await assertUserPermissionsToDeleteTeamAccount(client, {
accountId,
userId,
});
// Get the Supabase client and create a new service instance. // Get the Supabase client and create a new service instance.
const service = createDeleteTeamAccountService(); const service = createDeleteTeamAccountService();
@@ -35,32 +36,29 @@ export async function deleteTeamAccountAction(formData: FormData) {
admin: true, admin: true,
}), }),
{ {
accountId: params.accountId, accountId,
userId: auth.data.id, userId,
}, },
); );
return redirect('/home'); return redirect('/home');
} },
{},
);
async function assertUserPermissionsToDeleteTeamAccount( async function assertUserPermissionsToDeleteTeamAccount(
client: SupabaseClient<Database>, client: SupabaseClient<Database>,
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 const { data, error } = await client
.from('accounts') .from('accounts')
.select('id') .select('id')
.eq('primary_owner_user_id', userId) .eq('primary_owner_user_id', params.userId)
.eq('is_personal_account', false) .eq('is_personal_account', false)
.eq('id', accountId); .eq('id', params.accountId);
if (error ?? !data) { if (error ?? !data) {
throw new Error('Account not found'); throw new Error('Account not found');

View File

@@ -3,33 +3,29 @@
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation'; 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 { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
import { LeaveTeamAccountSchema } from '../../schema/leave-team-account.schema'; import { LeaveTeamAccountSchema } from '../../schema/leave-team-account.schema';
import { createLeaveTeamAccountService } from '../services/leave-team-account.service'; import { createLeaveTeamAccountService } from '../services/leave-team-account.service';
export async function leaveTeamAccountAction(formData: FormData) { export const leaveTeamAccountAction = enhanceAction(
async (formData: FormData, user) => {
const body = Object.fromEntries(formData.entries()); const body = Object.fromEntries(formData.entries());
const params = LeaveTeamAccountSchema.parse(body); const params = LeaveTeamAccountSchema.parse(body);
const client = getSupabaseServerActionClient();
const auth = await requireUser(client);
if (auth.error) {
throw new Error('Authentication required');
}
const service = createLeaveTeamAccountService( const service = createLeaveTeamAccountService(
getSupabaseServerActionClient({ admin: true }), getSupabaseServerActionClient({ admin: true }),
); );
await service.leaveTeamAccount({ await service.leaveTeamAccount({
accountId: params.accountId, accountId: params.accountId,
userId: auth.data.id, userId: user.id,
}); });
revalidatePath('/home/[account]', 'layout'); revalidatePath('/home/[account]', 'layout');
return redirect('/home'); return redirect('/home');
} },
{},
);

View File

@@ -2,17 +2,15 @@
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { z } from 'zod'; import { enhanceAction } from '@kit/next/actions';
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
import { UpdateTeamNameSchema } from '../../schema/update-team-name.schema'; import { UpdateTeamNameSchema } from '../../schema/update-team-name.schema';
export async function updateTeamAccountName( export const updateTeamAccountName = enhanceAction(
params: z.infer<typeof UpdateTeamNameSchema>, async (params) => {
) {
const client = getSupabaseServerComponentClient(); const client = getSupabaseServerComponentClient();
const { name, slug, path } = UpdateTeamNameSchema.parse(params); const { name, path, slug } = params;
const { error, data } = await client const { error, data } = await client
.from('accounts') .from('accounts')
@@ -39,4 +37,8 @@ export async function updateTeamAccountName(
} }
return { success: true }; return { success: true };
} },
{
schema: UpdateTeamNameSchema,
},
);

View File

@@ -3,12 +3,9 @@
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { SupabaseClient } from '@supabase/supabase-js';
import { z } from 'zod'; import { z } from 'zod';
import { Database } from '@kit/supabase/database'; import { enhanceAction } from '@kit/next/actions';
import { requireUser } from '@kit/supabase/require-user';
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
import { AcceptInvitationSchema } from '../../schema/accept-invitation.schema'; import { AcceptInvitationSchema } from '../../schema/accept-invitation.schema';
@@ -20,93 +17,91 @@ import { createAccountInvitationsService } from '../services/account-invitations
import { createAccountPerSeatBillingService } from '../services/account-per-seat-billing.service'; 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: { export const createInvitationsAction = enhanceAction(
accountSlug: string; async (params) => {
invitations: z.infer<typeof InviteMembersSchema>['invitations'];
}) {
const client = getSupabaseServerActionClient(); const client = getSupabaseServerActionClient();
await assertSession(client);
const { invitations } = InviteMembersSchema.parse({
invitations: params.invitations,
});
// Create the service // Create the service
const service = createAccountInvitationsService(client); const service = createAccountInvitationsService(client);
// send invitations // send invitations
await service.sendInvitations({ await service.sendInvitations(params);
invitations,
accountSlug: params.accountSlug,
});
revalidateMemberPage(); revalidateMemberPage();
return { return {
success: true, success: true,
}; };
} },
{
schema: InviteMembersSchema.and(
z.object({
accountSlug: z.string().min(1),
}),
),
},
);
/** /**
* Deletes an invitation specified by the invitation ID. * @name deleteInvitationAction
* * @description 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.
*/ */
export async function deleteInvitationAction( export const deleteInvitationAction = enhanceAction(
params: z.infer<typeof DeleteInvitationSchema>, async (data) => {
) {
const invitation = DeleteInvitationSchema.parse(params);
const client = getSupabaseServerActionClient(); const client = getSupabaseServerActionClient();
const { data, error } = await client.auth.getUser();
if (error ?? !data.user) {
throw new Error(`Authentication required`);
}
const service = createAccountInvitationsService(client); const service = createAccountInvitationsService(client);
// Delete the invitation // Delete the invitation
await service.deleteInvitation(invitation); await service.deleteInvitation(data);
revalidateMemberPage(); revalidateMemberPage();
return { success: true }; return {
} success: true,
};
},
{
schema: DeleteInvitationSchema,
},
);
export async function updateInvitationAction( /**
params: z.infer<typeof UpdateInvitationSchema>, * @name updateInvitationAction
) { * @description Updates an invitation.
*/
export const updateInvitationAction = enhanceAction(
async (invitation) => {
const client = getSupabaseServerActionClient(); const client = getSupabaseServerActionClient();
const invitation = UpdateInvitationSchema.parse(params);
await assertSession(client);
const service = createAccountInvitationsService(client); const service = createAccountInvitationsService(client);
await service.updateInvitation(invitation); await service.updateInvitation(invitation);
revalidateMemberPage(); revalidateMemberPage();
return { success: true }; return {
} success: true,
};
},
{
schema: UpdateInvitationSchema,
},
);
export async function acceptInvitationAction(data: FormData) { /**
* @name acceptInvitationAction
* @description Accepts an invitation to join a team.
*/
export const acceptInvitationAction = enhanceAction(
async (data: FormData, user) => {
const client = getSupabaseServerActionClient(); const client = getSupabaseServerActionClient();
const { inviteToken, nextPath } = AcceptInvitationSchema.parse( const { inviteToken, nextPath } = AcceptInvitationSchema.parse(
Object.fromEntries(data), Object.fromEntries(data),
); );
// Ensure the user is authenticated
const user = await assertSession(client);
// create the services // create the services
const perSeatBillingService = createAccountPerSeatBillingService(client); const perSeatBillingService = createAccountPerSeatBillingService(client);
const service = createAccountInvitationsService(client); const service = createAccountInvitationsService(client);
@@ -129,16 +124,19 @@ export async function acceptInvitationAction(data: FormData) {
await perSeatBillingService.increaseSeats(accountId); await perSeatBillingService.increaseSeats(accountId);
return redirect(nextPath); return redirect(nextPath);
} },
{},
);
export async function renewInvitationAction( /**
params: z.infer<typeof RenewInvitationSchema>, * @name renewInvitationAction
) { * @description Renews an invitation.
*/
export const renewInvitationAction = enhanceAction(
async (params) => {
const client = getSupabaseServerActionClient(); const client = getSupabaseServerActionClient();
const { invitationId } = RenewInvitationSchema.parse(params); const { invitationId } = RenewInvitationSchema.parse(params);
await assertSession(client);
const service = createAccountInvitationsService(client); const service = createAccountInvitationsService(client);
// Renew the invitation // Renew the invitation
@@ -149,17 +147,11 @@ export async function renewInvitationAction(
return { return {
success: true, success: true,
}; };
} },
{
async function assertSession(client: SupabaseClient<Database>) { schema: RenewInvitationSchema,
const { error, data } = await requireUser(client); },
);
if (error) {
throw new Error(`Authentication required`);
}
return data;
}
function revalidateMemberPage() { function revalidateMemberPage() {
revalidatePath('/home/[account]/members', 'page'); revalidatePath('/home/[account]/members', 'page');

View File

@@ -2,11 +2,7 @@
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { SupabaseClient } from '@supabase/supabase-js'; import { enhanceAction } from '@kit/next/actions';
import { z } from 'zod';
import { Database } from '@kit/supabase/database';
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
import { RemoveMemberSchema } from '../../schema/remove-member.schema'; import { RemoveMemberSchema } from '../../schema/remove-member.schema';
@@ -14,18 +10,13 @@ import { TransferOwnershipConfirmationSchema } from '../../schema/transfer-owner
import { UpdateMemberRoleSchema } from '../../schema/update-member-role.schema'; import { UpdateMemberRoleSchema } from '../../schema/update-member-role.schema';
import { createAccountMembersService } from '../services/account-members.service'; import { createAccountMembersService } from '../services/account-members.service';
export async function removeMemberFromAccountAction( /**
params: z.infer<typeof RemoveMemberSchema>, * @name removeMemberFromAccountAction
) { * @description Removes a member from an account.
*/
export const removeMemberFromAccountAction = enhanceAction(
async ({ accountId, userId }) => {
const client = getSupabaseServerActionClient(); const client = getSupabaseServerActionClient();
const { data, error } = await client.auth.getUser();
if (error ?? !data.user) {
throw new Error(`Authentication required`);
}
const { accountId, userId } = RemoveMemberSchema.parse(params);
const service = createAccountMembersService(client); const service = createAccountMembersService(client);
await service.removeMemberFromAccount({ await service.removeMemberFromAccount({
@@ -37,48 +28,46 @@ export async function removeMemberFromAccountAction(
revalidatePath('/home/[account]', 'layout'); revalidatePath('/home/[account]', 'layout');
return { success: true }; return { success: true };
} },
{
schema: RemoveMemberSchema,
},
);
export async function updateMemberRoleAction( /**
params: z.infer<typeof UpdateMemberRoleSchema>, * @name updateMemberRoleAction
) { * @description Updates the role of a member in an account.
*/
export const updateMemberRoleAction = enhanceAction(
async (data) => {
const client = getSupabaseServerActionClient(); const client = getSupabaseServerActionClient();
await assertSession(client);
const service = createAccountMembersService(client); const service = createAccountMembersService(client);
const adminClient = getSupabaseServerActionClient({ admin: true }); const adminClient = getSupabaseServerActionClient({ admin: true });
// update the role of the member // update the role of the member
await service.updateMemberRole( await service.updateMemberRole(data, adminClient);
{
accountId: params.accountId,
userId: params.userId,
role: params.role,
},
adminClient,
);
// revalidate all pages that depend on the account // revalidate all pages that depend on the account
revalidatePath('/home/[account]', 'layout'); revalidatePath('/home/[account]', 'layout');
return { success: true }; return { success: true };
} },
{
schema: UpdateMemberRoleSchema,
},
);
export async function transferOwnershipAction( /**
params: z.infer<typeof TransferOwnershipConfirmationSchema>, * @name transferOwnershipAction
) { * @description Transfers the ownership of an account to another member.
*/
export const transferOwnershipAction = enhanceAction(
async (data) => {
const client = getSupabaseServerActionClient(); 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 // assert that the user is the owner of the account
const { data: isOwner, error } = await client.rpc('is_account_owner', { const { data: isOwner, error } = await client.rpc('is_account_owner', {
account_id: accountId, account_id: data.accountId,
}); });
if (error ?? !isOwner) { if (error ?? !isOwner) {
@@ -93,14 +82,8 @@ export async function transferOwnershipAction(
// so we proceed with the transfer of ownership with admin privileges // so we proceed with the transfer of ownership with admin privileges
const adminClient = getSupabaseServerActionClient({ admin: true }); const adminClient = getSupabaseServerActionClient({ admin: true });
await service.transferOwnership( // transfer the ownership of the account
{ await service.transferOwnership(data, adminClient);
accountId,
userId,
confirmation: params.confirmation,
},
adminClient,
);
// revalidate all pages that depend on the account // revalidate all pages that depend on the account
revalidatePath('/home/[account]', 'layout'); revalidatePath('/home/[account]', 'layout');
@@ -108,12 +91,8 @@ export async function transferOwnershipAction(
return { return {
success: true, success: true,
}; };
} },
{
async function assertSession(client: SupabaseClient<Database>) { schema: TransferOwnershipConfirmationSchema,
const { data, error } = await client.auth.getUser(); },
);
if (error ?? !data.user) {
throw new Error(`Authentication required`);
}
}

View File

@@ -4,7 +4,7 @@ import { redirect } from 'next/navigation';
import type { User } from '@supabase/supabase-js'; import type { User } from '@supabase/supabase-js';
import { z } from 'zod'; import { ZodType, z } from 'zod';
import { verifyCaptchaToken } from '@kit/auth/captcha/server'; import { verifyCaptchaToken } from '@kit/auth/captcha/server';
import { requireUser } from '@kit/supabase/require-user'; import { requireUser } from '@kit/supabase/require-user';
@@ -12,6 +12,12 @@ import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-clie
import { captureException, zodParseFactory } from '../utils'; 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 * @name enhanceAction
@@ -24,19 +30,21 @@ export function enhanceAction<
auth?: boolean; auth?: boolean;
captcha?: boolean; captcha?: boolean;
captureException?: boolean; captureException?: boolean;
schema: z.ZodType< schema?: z.ZodType<
Config['captcha'] extends true ? Args & { captchaToken: string } : Args, Config['captcha'] extends true ? Args & { captchaToken: string } : Args,
z.ZodTypeDef z.ZodTypeDef
>; >;
}, },
>( >(
fn: ( fn: (
params: z.infer<Config['schema']>, params: Config['schema'] extends ZodType ? z.infer<Config['schema']> : Args,
user: Config['auth'] extends false ? undefined : User, user: Config['auth'] extends false ? undefined : User,
) => Response | Promise<Response>, ) => Response | Promise<Response>,
config: Config, config: Config,
) { ) {
return async (params: z.infer<Config['schema']>) => { return async (
params: Config['schema'] extends ZodType ? z.infer<Config['schema']> : Args,
) => {
type UserParam = Config['auth'] extends false ? undefined : User; type UserParam = Config['auth'] extends false ? undefined : User;
const requireAuth = config.auth ?? true; const requireAuth = config.auth ?? true;
@@ -56,11 +64,15 @@ export function enhanceAction<
} }
// validate the schema // validate the schema
const parsed = zodParseFactory(config.schema); const data = config.schema
const data = parsed(params); ? zodParseFactory(config.schema)(params)
: params;
// verify captcha unless it's explicitly disabled
// verify the captcha token if required // 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; const token = (data as Args & { captchaToken: string }).captchaToken;
// Verify the CAPTCHA token. It will throw an error if the token is invalid. // Verify the CAPTCHA token. It will throw an error if the token is invalid.

View File

@@ -19,6 +19,12 @@ interface HandlerParams<Body> {
body: Body; 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. * 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. * This function takes a request object as an argument and returns a response object.
*/ */
return async function routeHandler(request: NextRequest) { return async function routeHandler(request: NextRequest) {
// Verify the captcha token if required const shouldVerifyCaptcha = params?.captcha ?? IS_CAPTCHA_SETUP;
if (params?.captcha) {
// Verify the captcha token if required and setup
if (shouldVerifyCaptcha) {
const token = captchaTokenGetter(request); const token = captchaTokenGetter(request);
// If the captcha token is not provided, return a 400 response. // If the captcha token is not provided, return a 400 response.

6
pnpm-lock.yaml generated
View File

@@ -561,6 +561,9 @@ importers:
'@kit/monitoring': '@kit/monitoring':
specifier: workspace:^ specifier: workspace:^
version: link:../../monitoring/api version: link:../../monitoring/api
'@kit/next':
specifier: workspace:^
version: link:../../next
'@kit/prettier-config': '@kit/prettier-config':
specifier: workspace:* specifier: workspace:*
version: link:../../../tooling/prettier version: link:../../../tooling/prettier
@@ -772,6 +775,9 @@ importers:
'@kit/monitoring': '@kit/monitoring':
specifier: workspace:* specifier: workspace:*
version: link:../../monitoring/api version: link:../../monitoring/api
'@kit/next':
specifier: workspace:^
version: link:../../next
'@kit/prettier-config': '@kit/prettier-config':
specifier: workspace:* specifier: workspace:*
version: link:../../../tooling/prettier version: link:../../../tooling/prettier