Refactor account deletion process and improve invitation functionalities
The account deletion process has been refactored to send account deletion emails from the AccountWebhooksService instead of from the deletePersonalAccount service. This has resulted in the addition of the AccountWebhooksService and modification of the seeds.sql file to trigger a webhook after account deletion. Along with this, the account invitation functionalities within the accountInvitations service have been considerably enhanced, making it much clearer and easier to use.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
type Invitation = Database['public']['Tables']['invitations']['Row'];
|
||||
|
||||
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 AccountInvitationsWebhookService {
|
||||
private namespace = 'accounts.invitations.webhook';
|
||||
|
||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||
|
||||
async handleInvitationWebhook(invitation: Invitation) {
|
||||
return this.dispatchInvitationEmail(invitation);
|
||||
}
|
||||
|
||||
private async dispatchInvitationEmail(invitation: Invitation) {
|
||||
const inviter = await this.client
|
||||
.from('accounts')
|
||||
.select('email, name')
|
||||
.eq('id', invitation.invited_by)
|
||||
.single();
|
||||
|
||||
if (inviter.error) {
|
||||
throw inviter.error;
|
||||
}
|
||||
|
||||
const team = await this.client
|
||||
.from('accounts')
|
||||
.select('name')
|
||||
.eq('id', invitation.account_id)
|
||||
.single();
|
||||
|
||||
if (team.error) {
|
||||
throw team.error;
|
||||
}
|
||||
|
||||
const logger = await getLogger();
|
||||
|
||||
try {
|
||||
const { renderInviteEmail } = await import('@kit/email-templates');
|
||||
const { getMailer } = await import('@kit/mailers');
|
||||
const mailer = await getMailer();
|
||||
|
||||
const html = renderInviteEmail({
|
||||
link: this.getInvitationLink(invitation.invite_token),
|
||||
invitedUserEmail: invitation.email,
|
||||
inviter: inviter.data.name ?? inviter.data.email ?? '',
|
||||
productName: env.productName,
|
||||
teamName: team.data.name,
|
||||
});
|
||||
|
||||
await mailer
|
||||
.sendEmail({
|
||||
from: env.emailSender,
|
||||
to: invitation.email,
|
||||
subject: 'You have been invited to join a team',
|
||||
html,
|
||||
})
|
||||
.then(() => {
|
||||
logger.info('Invitation email sent', {
|
||||
email: invitation.email,
|
||||
account: invitation.account_id,
|
||||
name: this.namespace,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.warn(
|
||||
{ error, name: this.namespace },
|
||||
'Failed to send invitation email',
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
{ error, name: this.namespace },
|
||||
'Failed to invite user to team',
|
||||
);
|
||||
|
||||
return {
|
||||
error,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private getInvitationLink(token: string) {
|
||||
return new URL(env.invitePath, env.siteURL).href + `?invite_token=${token}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
type Account = Database['public']['Tables']['accounts']['Row'];
|
||||
|
||||
export class AccountWebhooksService {
|
||||
private readonly namespace = 'accounts.webhooks';
|
||||
|
||||
async handleAccountDeletedWebhook(account: Account) {
|
||||
const logger = await getLogger();
|
||||
|
||||
const ctx = {
|
||||
accountId: account.id,
|
||||
namespace: this.namespace,
|
||||
};
|
||||
|
||||
logger.info(ctx, 'Received account deleted webhook. Processing...');
|
||||
|
||||
if (account.is_personal_account) {
|
||||
logger.info(ctx, `Account is personal. We send an email to the user.`);
|
||||
|
||||
await this.sendDeleteAccountEmail(account);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendDeleteAccountEmail(account: Account) {
|
||||
const userEmail = account.email;
|
||||
const userDisplayName = account.name ?? userEmail;
|
||||
|
||||
const emailSettings = this.getEmailSettings();
|
||||
|
||||
if (userEmail) {
|
||||
await this.sendAccountDeletionEmail({
|
||||
fromEmail: emailSettings.fromEmail,
|
||||
productName: emailSettings.productName,
|
||||
userDisplayName,
|
||||
userEmail,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async sendAccountDeletionEmail(params: {
|
||||
fromEmail: string;
|
||||
userEmail: string;
|
||||
userDisplayName: string;
|
||||
productName: string;
|
||||
}) {
|
||||
const { renderAccountDeleteEmail } = await import('@kit/email-templates');
|
||||
const { getMailer } = await import('@kit/mailers');
|
||||
const mailer = await getMailer();
|
||||
|
||||
const html = renderAccountDeleteEmail({
|
||||
userDisplayName: params.userDisplayName,
|
||||
productName: params.productName,
|
||||
});
|
||||
|
||||
return mailer.sendEmail({
|
||||
to: params.userEmail,
|
||||
from: params.fromEmail,
|
||||
subject: 'Account Deletion Request',
|
||||
html,
|
||||
});
|
||||
}
|
||||
|
||||
private getEmailSettings() {
|
||||
const productName = process.env.NEXT_PUBLIC_PRODUCT_NAME;
|
||||
const fromEmail = process.env.EMAIL_SENDER;
|
||||
|
||||
return z
|
||||
.object({
|
||||
productName: z.string(),
|
||||
fromEmail: z.string().email(),
|
||||
})
|
||||
.parse({
|
||||
productName,
|
||||
fromEmail,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './account-webhooks.service';
|
||||
export * from './account-invitations-webhook.service';
|
||||
Reference in New Issue
Block a user