@accounter/server
Version:
Accounter GraphQL server
179 lines • 7.62 kB
JavaScript
import { __decorate, __metadata } from "tslib";
import { createHash } from 'node:crypto';
import { Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
import { DBProvider } from '../../app-providers/db.provider.js';
import { AuditLogsProvider } from '../../common/providers/audit-logs.provider.js';
import { alreadyAcceptedError, expiredTokenError, invalidTokenError, mapAuth0Error, } from '../helpers/invitations.helper.js';
import { Auth0ManagementProvider } from './auth0-management.provider.js';
const updateInvitationAcceptance = sql `
UPDATE accounter_schema.invitations
SET accepted_at = NOW()
WHERE id = $id;
`;
const getInvitationForAcceptance = sql `
SELECT id, user_id, business_id, role_id, email, auth0_user_id, accepted_at, expires_at
FROM accounter_schema.invitations
WHERE token_hash = $tokenHash
AND accepted_at IS NULL
AND expires_at > NOW()
FOR UPDATE;
`;
const getInvitationByToken = sql `
SELECT id, user_id, business_id, role_id, email, auth0_user_id, accepted_at, expires_at
FROM accounter_schema.invitations
WHERE token_hash = $tokenHash;
`;
const getUserIdByAuth0UserId = sql `
SELECT user_id
FROM accounter_schema.business_users
WHERE auth0_user_id = $auth0UserId
LIMIT 1;
`;
const insertAcceptedBusinessUser = sql `
INSERT INTO accounter_schema.business_users (user_id, auth0_user_id, business_id, role_id)
VALUES ($userId, $auth0UserId, $ownerId, $roleId);
`;
const updateBusinessUserAuth0Id = sql `
UPDATE accounter_schema.business_users
SET auth0_user_id = $auth0UserId, updated_at = NOW()
WHERE user_id = $userId
AND business_id = $ownerId;
`;
let AcceptInvitationsProvider = class AcceptInvitationsProvider {
dbProvider;
auth0ManagementProvider;
auditLogsProvider;
constructor(dbProvider, auth0ManagementProvider, auditLogsProvider) {
this.dbProvider = dbProvider;
this.auth0ManagementProvider = auth0ManagementProvider;
this.auditLogsProvider = auditLogsProvider;
}
async acceptInvitation(token, auth0UserId, authenticatedUserEmail) {
const tokenHash = createHash('sha256').update(token).digest('hex');
const client = await this.dbProvider.pool.connect();
try {
await client.query('BEGIN');
const activeInvitationResult = await getInvitationForAcceptance.run({ tokenHash }, client);
if (activeInvitationResult.length === 0) {
const invitationByTokenResult = await getInvitationByToken.run({ tokenHash }, client);
if (invitationByTokenResult.length === 0) {
throw invalidTokenError();
}
const staleInvitation = invitationByTokenResult[0];
if (staleInvitation.accepted_at) {
throw alreadyAcceptedError();
}
if (new Date(staleInvitation.expires_at).getTime() <= Date.now()) {
throw expiredTokenError();
}
throw invalidTokenError();
}
const invitation = activeInvitationResult[0];
const effectiveAuth0UserId = auth0UserId ?? invitation.auth0_user_id;
if (!effectiveAuth0UserId) {
throw invalidTokenError();
}
// Defense in depth: authenticated users can claim only invitations for their own email.
if (auth0UserId) {
const normalizedInvitationEmail = invitation.email?.trim().toLowerCase();
let normalizedAuthenticatedEmail = authenticatedUserEmail?.trim().toLowerCase() ?? null;
// Access tokens for custom APIs often omit `email` claims.
// In that case, resolve the primary email from Auth0 profile for comparison.
if (!normalizedAuthenticatedEmail) {
const identity = await this.auth0ManagementProvider.getUserEmailById(auth0UserId);
if (identity?.emailVerified && identity.email) {
normalizedAuthenticatedEmail = identity.email.trim().toLowerCase();
}
}
if (!normalizedInvitationEmail ||
!normalizedAuthenticatedEmail ||
normalizedInvitationEmail !== normalizedAuthenticatedEmail) {
throw invalidTokenError();
}
}
if (!invitation.user_id) {
throw invalidTokenError();
}
const assignAuth0UserToInvitedUser = async () => {
await updateBusinessUserAuth0Id.run({
auth0UserId: effectiveAuth0UserId,
userId: invitation.user_id,
ownerId: invitation.business_id,
}, client);
};
let userId;
if (auth0UserId) {
const existingUserResult = await getUserIdByAuth0UserId.run({ auth0UserId }, client);
if (existingUserResult.length > 0) {
userId = existingUserResult[0].user_id;
await insertAcceptedBusinessUser.run({
userId,
auth0UserId: effectiveAuth0UserId,
ownerId: invitation.business_id,
roleId: invitation.role_id,
}, client);
}
else {
userId = invitation.user_id;
await assignAuth0UserToInvitedUser();
}
}
else {
userId = invitation.user_id;
await assignAuth0UserToInvitedUser();
}
if (invitation.auth0_user_id) {
try {
if (auth0UserId && auth0UserId !== invitation.auth0_user_id) {
await this.auth0ManagementProvider.deleteUser(invitation.auth0_user_id);
}
else {
await this.auth0ManagementProvider.unblockUser(invitation.auth0_user_id);
}
}
catch (error) {
throw mapAuth0Error(error);
}
}
await updateInvitationAcceptance.run({ id: invitation.id }, client);
await this.auditLogsProvider.log({
ownerId: invitation.business_id,
userId,
auth0UserId: effectiveAuth0UserId,
action: 'INVITATION_ACCEPTED',
entity: 'Invitation',
entityId: invitation.id,
details: {
auth0_user_id: effectiveAuth0UserId,
business_id: invitation.business_id,
role_id: invitation.role_id,
},
}, client);
await client.query('COMMIT');
return {
success: true,
businessId: invitation.business_id,
roleId: invitation.role_id,
};
}
catch (error) {
await client.query('ROLLBACK').catch(() => null);
throw error;
}
finally {
client.release();
}
}
};
AcceptInvitationsProvider = __decorate([
Injectable({
scope: Scope.Operation,
global: true,
}),
__metadata("design:paramtypes", [DBProvider,
Auth0ManagementProvider,
AuditLogsProvider])
], AcceptInvitationsProvider);
export { AcceptInvitationsProvider };
//# sourceMappingURL=accept-invitations.provider.js.map