@accounter/server
Version:
Accounter GraphQL server
128 lines • 5.3 kB
JavaScript
import { __decorate, __metadata } from "tslib";
import { createHash, randomBytes, randomUUID } from 'node:crypto';
import DataLoader from 'dataloader';
import { GraphQLError } from 'graphql';
import { Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js';
import { AuditLogsProvider } from '../../common/providers/audit-logs.provider.js';
import { BusinessUsersProvider } from './business-users.provider.js';
const INVITATION_EXPIRATION_PERIOD_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
const getActiveInvitationsByEmails = sql `
SELECT id, email
FROM accounter_schema.invitations
WHERE business_id = $ownerId
AND lower(email) = ANY(SELECT lower(u) FROM unnest($emails::varchar[]) AS u)
AND accepted_at IS NULL
AND expires_at > NOW();`;
const getInvitationsByTokens = sql `
SELECT id, business_id, role_id, auth0_user_id, accepted_at, expires_at, token_hash
FROM accounter_schema.invitations
WHERE token_hash IN $$tokenHashes;`;
const insertInvitation = sql `
INSERT INTO accounter_schema.invitations (
user_id,
business_id,
email,
role_id,
token_hash,
auth0_user_created,
auth0_user_id,
invited_by_user_id,
invited_by_business_id,
expires_at
)
VALUES ($userId, $ownerId, $email, $roleId, $tokenHash, $auth0UserCreated, $auth0UserId, $invitedByUserId, $invitedByBusinessId, $expiresAt)
RETURNING id, email, business_id, role_id, expires_at;
`;
let InvitationsProvider = class InvitationsProvider {
db;
adminContextProvider;
businessUsersProvider;
auditLogsProvider;
constructor(db, adminContextProvider, businessUsersProvider, auditLogsProvider) {
this.db = db;
this.adminContextProvider = adminContextProvider;
this.businessUsersProvider = businessUsersProvider;
this.auditLogsProvider = auditLogsProvider;
}
async batchInvitationsByEmails(emails) {
const { ownerId } = await this.adminContextProvider.getVerifiedAdminContext();
const invitations = await getActiveInvitationsByEmails.run({
emails: [...emails],
ownerId,
}, this.db);
return emails.map(email => invitations.filter(t => t.email.toLowerCase() === email.toLowerCase()));
}
getInvitationByEmailLoader = new DataLoader((emails) => this.batchInvitationsByEmails(emails), {
cacheKeyFn: email => email.toLowerCase(),
});
async batchInvitationsByTokens(tokenHashes) {
const invitations = await getInvitationsByTokens.run({
tokenHashes: [...tokenHashes],
}, this.db);
return tokenHashes.map(tokenHash => invitations.filter(t => t.token_hash === tokenHash));
}
getInvitationByTokensLoader = new DataLoader((tokenHashes) => this.batchInvitationsByTokens(tokenHashes));
async insertInvitation({ email, roleId, auth0UserId, invitedByUserId, ownerId, }) {
return this.db.transaction(async (client) => {
const token = randomBytes(32).toString('hex');
const tokenHash = createHash('sha256').update(token).digest('hex');
const localUserId = randomUUID();
const invitationParams = {
userId: localUserId,
email,
roleId,
tokenHash,
auth0UserCreated: true,
auth0UserId,
invitedByUserId,
invitedByBusinessId: ownerId,
ownerId,
expiresAt: new Date(Date.now() + INVITATION_EXPIRATION_PERIOD_MS),
};
await this.businessUsersProvider.insertBusinessUser({
userId: localUserId,
auth0UserId: null,
roleId,
ownerId,
});
const insertedInvitation = await insertInvitation.run(invitationParams, client);
const [invitation] = insertedInvitation;
if (!invitation) {
throw new GraphQLError('Invitation creation failed: no record returned from DB.', {
extensions: { code: 'INTERNAL_SERVER_ERROR' },
});
}
await this.auditLogsProvider.insertAuditLog({
ownerId: invitation.business_id,
userId: invitedByUserId,
auth0UserId,
action: 'INVITATION_CREATED',
entity: 'Invitation',
entityId: invitation.id,
details: { email, roleId, auth0UserId },
});
return {
id: invitation.id,
email: invitation.email,
roleId: invitation.role_id,
expiresAt: invitation.expires_at,
token,
};
});
}
};
InvitationsProvider = __decorate([
Injectable({
scope: Scope.Operation,
global: true,
}),
__metadata("design:paramtypes", [TenantAwareDBClient,
AdminContextProvider,
BusinessUsersProvider,
AuditLogsProvider])
], InvitationsProvider);
export { InvitationsProvider };
//# sourceMappingURL=invitations.provider.js.map