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,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);
},
{},
);

View File

@@ -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<typeof TeamCheckoutSchema>,
) {
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);
},
{},
);

View File

@@ -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:^",

View File

@@ -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

View File

@@ -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:^",

View File

@@ -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<typeof CreateTeamSchema>,
) {
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,
},
);

View File

@@ -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<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
.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');

View File

@@ -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');
},
{},
);

View File

@@ -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<typeof UpdateTeamNameSchema>,
) {
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,
},
);

View File

@@ -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<typeof InviteMembersSchema>['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<typeof DeleteInvitationSchema>,
) {
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<typeof UpdateInvitationSchema>,
) {
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<typeof RenewInvitationSchema>,
) {
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<Database>) {
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');

View File

@@ -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<typeof RemoveMemberSchema>,
) {
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<typeof UpdateMemberRoleSchema>,
) {
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<typeof TransferOwnershipConfirmationSchema>,
) {
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<Database>) {
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,
},
);

View File

@@ -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<Config['schema']>,
params: Config['schema'] extends ZodType ? z.infer<Config['schema']> : Args,
user: Config['auth'] extends false ? undefined : User,
) => Response | Promise<Response>,
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;
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.

View File

@@ -19,6 +19,12 @@ interface HandlerParams<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.
*
@@ -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.

6
pnpm-lock.yaml generated
View File

@@ -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