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:
@@ -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);
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
6
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user