Remove team account related services and actions
Removed services and actions related to team account deletion as well as updated paths within other dependent files, better reflecting their new locations. Also, added a new service titled 'AccountBillingService' for handling billing-related operations and restructured the form layout and handled translation in 'team-account-danger-zone' component.
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import 'server-only';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Mailer } from '@kit/mailers';
|
||||
import { Logger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
import { DeleteInvitationSchema } from '../../schema/delete-invitation.schema';
|
||||
import { InviteMembersSchema } from '../../schema/invite-members.schema';
|
||||
import { UpdateInvitationSchema } from '../../schema/update-invitation-schema';
|
||||
|
||||
const invitePath = process.env.INVITATION_PAGE_PATH;
|
||||
const siteURL = process.env.NEXT_PUBLIC_SITE_URL;
|
||||
const productName = process.env.NEXT_PUBLIC_PRODUCT_NAME ?? '';
|
||||
const emailSender = process.env.EMAIL_SENDER;
|
||||
|
||||
const env = z
|
||||
.object({
|
||||
invitePath: z.string().min(1),
|
||||
siteURL: z.string().min(1),
|
||||
productName: z.string(),
|
||||
emailSender: z.string().email(),
|
||||
})
|
||||
.parse({
|
||||
invitePath,
|
||||
siteURL,
|
||||
productName,
|
||||
emailSender,
|
||||
});
|
||||
|
||||
export class AccountInvitationsService {
|
||||
private namespace = 'accounts.invitations';
|
||||
|
||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||
|
||||
async deleteInvitation(params: z.infer<typeof DeleteInvitationSchema>) {
|
||||
Logger.info('Removing invitation', {
|
||||
name: this.namespace,
|
||||
...params,
|
||||
});
|
||||
|
||||
const { data, error } = await this.client
|
||||
.from('invitations')
|
||||
.delete()
|
||||
.match({
|
||||
id: params.invitationId,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
Logger.info('Invitation successfully removed', {
|
||||
...params,
|
||||
name: this.namespace,
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async updateInvitation(params: z.infer<typeof UpdateInvitationSchema>) {
|
||||
Logger.info('Updating invitation', {
|
||||
...params,
|
||||
name: this.namespace,
|
||||
});
|
||||
|
||||
const { data, error } = await this.client
|
||||
.from('invitations')
|
||||
.update({
|
||||
role: params.role,
|
||||
})
|
||||
.match({
|
||||
id: params.invitationId,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
Logger.info('Invitation successfully updated', {
|
||||
...params,
|
||||
name: this.namespace,
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async sendInvitations({
|
||||
account,
|
||||
invitations,
|
||||
}: {
|
||||
invitations: z.infer<typeof InviteMembersSchema>['invitations'];
|
||||
account: string;
|
||||
}) {
|
||||
Logger.info(
|
||||
{ account, invitations, name: this.namespace },
|
||||
'Storing invitations',
|
||||
);
|
||||
|
||||
const mailer = new Mailer();
|
||||
|
||||
const { user } = await this.getUser();
|
||||
|
||||
const accountResponse = await this.client
|
||||
.from('accounts')
|
||||
.select('name')
|
||||
.eq('slug', account)
|
||||
.single();
|
||||
|
||||
if (!accountResponse.data) {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
|
||||
const response = await this.client.rpc('add_invitations_to_account', {
|
||||
invitations,
|
||||
account_slug: account,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw response.error;
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
|
||||
const responseInvitations = Array.isArray(response.data)
|
||||
? response.data
|
||||
: [response.data];
|
||||
|
||||
Logger.info(
|
||||
{
|
||||
account,
|
||||
count: responseInvitations.length,
|
||||
name: this.namespace,
|
||||
},
|
||||
'Invitations added to account',
|
||||
);
|
||||
|
||||
Logger.info(
|
||||
{
|
||||
account,
|
||||
count: responseInvitations.length,
|
||||
name: this.namespace,
|
||||
},
|
||||
'Sending invitation emails...',
|
||||
);
|
||||
|
||||
for (const invitation of responseInvitations) {
|
||||
const promise = async () => {
|
||||
try {
|
||||
const { renderInviteEmail } = await import('@kit/email-templates');
|
||||
|
||||
const html = renderInviteEmail({
|
||||
link: this.getInvitationLink(invitation.invite_token),
|
||||
invitedUserEmail: invitation.email,
|
||||
inviter: user.email,
|
||||
productName: env.productName,
|
||||
teamName: accountResponse.data.name,
|
||||
});
|
||||
|
||||
await mailer.sendEmail({
|
||||
from: env.emailSender,
|
||||
to: invitation.email,
|
||||
subject: 'You have been invited to join a team',
|
||||
html,
|
||||
});
|
||||
|
||||
Logger.info('Invitation email sent', {
|
||||
email: invitation.email,
|
||||
account,
|
||||
name: this.namespace,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
Logger.warn(
|
||||
{ account, error, name: this.namespace },
|
||||
'Failed to send invitation email',
|
||||
);
|
||||
|
||||
return {
|
||||
error,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
const responses = await Promise.all(promises.map((promise) => promise()));
|
||||
const success = responses.filter((response) => response.success).length;
|
||||
|
||||
Logger.info(
|
||||
{
|
||||
name: this.namespace,
|
||||
account,
|
||||
success,
|
||||
failed: responses.length - success,
|
||||
},
|
||||
`Invitations processed`,
|
||||
);
|
||||
}
|
||||
|
||||
private async getUser() {
|
||||
const { data, error } = await this.client.auth.getUser();
|
||||
|
||||
if (error ?? !data) {
|
||||
throw new Error('Authentication required');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private getInvitationLink(token: string) {
|
||||
return new URL(env.invitePath, env.siteURL).href + `?token=${token}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import 'server-only';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
export class AccountMembersService {
|
||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||
|
||||
async removeMemberFromAccount(params: { accountId: string; userId: string }) {
|
||||
const { data, error } = await this.client
|
||||
.from('accounts_memberships')
|
||||
.delete()
|
||||
.match({
|
||||
id: params.accountId,
|
||||
user_id: params.userId,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async updateMemberRole(params: {
|
||||
accountId: string;
|
||||
userId: string;
|
||||
role: Database['public']['Enums']['account_role'];
|
||||
}) {
|
||||
const { data, error } = await this.client
|
||||
.from('accounts_memberships')
|
||||
.update({
|
||||
account_role: params.role,
|
||||
})
|
||||
.match({
|
||||
account_id: params.accountId,
|
||||
user_id: params.userId,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async transferOwnership(params: { accountId: string; userId: string }) {
|
||||
const { data, error } = await this.client
|
||||
.from('accounts')
|
||||
.update({
|
||||
primary_owner_user_id: params.userId,
|
||||
})
|
||||
.match({
|
||||
id: params.accountId,
|
||||
user_id: params.userId,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { Logger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
export class CreateTeamAccountService {
|
||||
private readonly namespace = 'accounts.create-team-account';
|
||||
|
||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||
|
||||
createNewOrganizationAccount(params: { name: string; userId: string }) {
|
||||
Logger.info(
|
||||
{ ...params, namespace: this.namespace },
|
||||
`Creating new team account...`,
|
||||
);
|
||||
|
||||
return this.client.rpc('create_account', {
|
||||
account_name: params.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import 'server-only';
|
||||
|
||||
import { AccountBillingService } from '@kit/billing-gateway';
|
||||
import { Logger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
export class DeleteTeamAccountService {
|
||||
private readonly namespace = 'accounts.delete';
|
||||
|
||||
/**
|
||||
* Deletes a team account. Permissions are not checked here, as they are
|
||||
* checked in the server action.
|
||||
*
|
||||
* USE WITH CAUTION. THE USER MUST HAVE THE NECESSARY PERMISSIONS.
|
||||
*
|
||||
* @param adminClient
|
||||
* @param params
|
||||
*/
|
||||
async deleteTeamAccount(
|
||||
adminClient: SupabaseClient<Database>,
|
||||
params: { accountId: string },
|
||||
) {
|
||||
Logger.info(
|
||||
{
|
||||
name: this.namespace,
|
||||
accountId: params.accountId,
|
||||
},
|
||||
`Requested team account deletion. Processing...`,
|
||||
);
|
||||
|
||||
Logger.info(
|
||||
{
|
||||
name: this.namespace,
|
||||
accountId: params.accountId,
|
||||
},
|
||||
`Deleting all account subscriptions...`,
|
||||
);
|
||||
|
||||
// First - we want to cancel all Stripe active subscriptions
|
||||
const billingService = new AccountBillingService(adminClient);
|
||||
|
||||
await billingService.cancelAllAccountSubscriptions(params.accountId);
|
||||
|
||||
// now we can use the admin client to delete the account.
|
||||
const { error } = await adminClient
|
||||
.from('accounts')
|
||||
.delete()
|
||||
.eq('id', params.accountId);
|
||||
|
||||
if (error) {
|
||||
Logger.error(
|
||||
{
|
||||
name: this.namespace,
|
||||
accountId: params.accountId,
|
||||
error,
|
||||
},
|
||||
'Failed to delete team account',
|
||||
);
|
||||
|
||||
throw new Error('Failed to delete team account');
|
||||
}
|
||||
|
||||
Logger.info(
|
||||
{
|
||||
name: this.namespace,
|
||||
accountId: params.accountId,
|
||||
},
|
||||
'Successfully deleted team account',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import 'server-only';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
export class LeaveAccountService {
|
||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||
|
||||
async leaveTeamAccount(params: { accountId: string; userId: string }) {
|
||||
// TODO
|
||||
// implement this method
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user