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

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