300 lines
6.3 KiB
TypeScript
300 lines
6.3 KiB
TypeScript
import 'server-only';
|
|
|
|
import { SupabaseClient } from '@supabase/supabase-js';
|
|
|
|
import { addDays, formatISO } from 'date-fns';
|
|
import { z } from 'zod';
|
|
|
|
import { getLogger } from '@kit/shared/logger';
|
|
import { Database } from '@kit/supabase/database';
|
|
|
|
import type { DeleteInvitationSchema } from '../../schema/delete-invitation.schema';
|
|
import type { InviteMembersSchema } from '../../schema/invite-members.schema';
|
|
import type { UpdateInvitationSchema } from '../../schema/update-invitation.schema';
|
|
|
|
export function createAccountInvitationsService(
|
|
client: SupabaseClient<Database>,
|
|
) {
|
|
return new AccountInvitationsService(client);
|
|
}
|
|
|
|
/**
|
|
* @name AccountInvitationsService
|
|
* @description Service for managing account invitations.
|
|
*/
|
|
class AccountInvitationsService {
|
|
private readonly namespace = 'invitations';
|
|
|
|
constructor(private readonly client: SupabaseClient<Database>) {}
|
|
|
|
/**
|
|
* @name deleteInvitation
|
|
* @description Removes an invitation from the database.
|
|
* @param params
|
|
*/
|
|
async deleteInvitation(params: z.infer<typeof DeleteInvitationSchema>) {
|
|
const logger = await getLogger();
|
|
|
|
const ctx = {
|
|
name: this.namespace,
|
|
...params,
|
|
};
|
|
|
|
logger.info(ctx, 'Removing invitation...');
|
|
|
|
const { data, error } = await this.client
|
|
.from('invitations')
|
|
.delete()
|
|
.match({
|
|
id: params.invitationId,
|
|
});
|
|
|
|
if (error) {
|
|
logger.error(ctx, `Failed to remove invitation`);
|
|
|
|
throw error;
|
|
}
|
|
|
|
logger.info(ctx, 'Invitation successfully removed');
|
|
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* @name updateInvitation
|
|
* @param params
|
|
* @description Updates an invitation in the database.
|
|
*/
|
|
async updateInvitation(params: z.infer<typeof UpdateInvitationSchema>) {
|
|
const logger = await getLogger();
|
|
|
|
const ctx = {
|
|
name: this.namespace,
|
|
...params,
|
|
};
|
|
|
|
logger.info(ctx, 'Updating invitation...');
|
|
|
|
const { data, error } = await this.client
|
|
.from('invitations')
|
|
.update({
|
|
role: params.role,
|
|
})
|
|
.match({
|
|
id: params.invitationId,
|
|
});
|
|
|
|
if (error) {
|
|
logger.error(
|
|
{
|
|
...ctx,
|
|
error,
|
|
},
|
|
'Failed to update invitation',
|
|
);
|
|
|
|
throw error;
|
|
}
|
|
|
|
logger.info(ctx, 'Invitation successfully updated');
|
|
|
|
return data;
|
|
}
|
|
|
|
async validateInvitation(
|
|
invitation: z.infer<typeof InviteMembersSchema>['invitations'][number],
|
|
accountSlug: string,
|
|
) {
|
|
const { data: members, error } = await this.client.rpc(
|
|
'get_account_members',
|
|
{
|
|
account_slug: accountSlug,
|
|
},
|
|
);
|
|
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
|
|
const isUserAlreadyMember = members.find((member) => {
|
|
return (
|
|
member.email === invitation.email ||
|
|
member.personal_code === invitation.personal_code
|
|
);
|
|
});
|
|
|
|
if (isUserAlreadyMember) {
|
|
throw new Error('User already member of the team');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @name sendInvitations
|
|
* @description Sends invitations to join a team.
|
|
* @param accountSlug
|
|
* @param invitations
|
|
*/
|
|
async sendInvitations({
|
|
accountSlug,
|
|
invitations,
|
|
}: {
|
|
invitations: z.infer<typeof InviteMembersSchema>['invitations'];
|
|
accountSlug: string;
|
|
}) {
|
|
const logger = await getLogger();
|
|
|
|
const ctx = {
|
|
accountSlug,
|
|
name: this.namespace,
|
|
};
|
|
|
|
logger.info(ctx, 'Storing invitations...');
|
|
|
|
try {
|
|
await Promise.all(
|
|
invitations.map((invitation) =>
|
|
this.validateInvitation(invitation, accountSlug),
|
|
),
|
|
);
|
|
} catch (error) {
|
|
logger.error(
|
|
{
|
|
...ctx,
|
|
error: (error as Error).message,
|
|
},
|
|
'Error validating invitations',
|
|
);
|
|
|
|
throw error;
|
|
}
|
|
|
|
const accountResponse = await this.client
|
|
.from('accounts')
|
|
.select('name')
|
|
.eq('slug', accountSlug)
|
|
.single();
|
|
|
|
if (!accountResponse.data) {
|
|
logger.error(
|
|
ctx,
|
|
'Account not found in database. Cannot send invitations.',
|
|
);
|
|
|
|
throw new Error('Account not found');
|
|
}
|
|
|
|
const response = await this.client.rpc('add_invitations_to_account', {
|
|
invitations,
|
|
account_slug: accountSlug,
|
|
});
|
|
|
|
if (response.error) {
|
|
logger.error(
|
|
{
|
|
...ctx,
|
|
error: response.error,
|
|
},
|
|
`Failed to add invitations to account ${accountSlug}`,
|
|
);
|
|
|
|
throw response.error;
|
|
}
|
|
|
|
const responseInvitations = Array.isArray(response.data)
|
|
? response.data
|
|
: [response.data];
|
|
|
|
logger.info(
|
|
{
|
|
...ctx,
|
|
count: responseInvitations.length,
|
|
},
|
|
'Invitations added to account',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @name acceptInvitationToTeam
|
|
* @description Accepts an invitation to join a team.
|
|
*/
|
|
async acceptInvitationToTeam(
|
|
adminClient: SupabaseClient<Database>,
|
|
params: {
|
|
userId: string;
|
|
inviteToken: string;
|
|
},
|
|
) {
|
|
const logger = await getLogger();
|
|
const ctx = {
|
|
name: this.namespace,
|
|
...params,
|
|
};
|
|
|
|
logger.info(ctx, 'Accepting invitation to team');
|
|
|
|
const { error, data } = await adminClient.rpc('accept_invitation', {
|
|
token: params.inviteToken,
|
|
user_id: params.userId,
|
|
});
|
|
|
|
if (error) {
|
|
logger.error(
|
|
{
|
|
...ctx,
|
|
error,
|
|
},
|
|
'Failed to accept invitation to team',
|
|
);
|
|
|
|
throw error;
|
|
}
|
|
|
|
logger.info(ctx, 'Successfully accepted invitation to team');
|
|
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* @name renewInvitation
|
|
* @description Renews an invitation to join a team by extending the expiration date by 7 days.
|
|
* @param invitationId
|
|
*/
|
|
async renewInvitation(invitationId: number) {
|
|
const logger = await getLogger();
|
|
|
|
const ctx = {
|
|
invitationId,
|
|
name: this.namespace,
|
|
};
|
|
|
|
logger.info(ctx, 'Renewing invitation...');
|
|
|
|
const sevenDaysFromNow = formatISO(addDays(new Date(), 7));
|
|
|
|
const { data, error } = await this.client
|
|
.from('invitations')
|
|
.update({
|
|
expires_at: sevenDaysFromNow,
|
|
})
|
|
.match({
|
|
id: invitationId,
|
|
});
|
|
|
|
if (error) {
|
|
logger.error(
|
|
{
|
|
...ctx,
|
|
error,
|
|
},
|
|
'Failed to renew invitation',
|
|
);
|
|
|
|
throw error;
|
|
}
|
|
|
|
logger.info(ctx, 'Invitation successfully renewed');
|
|
|
|
return data;
|
|
}
|
|
}
|