UNPKG

@accounter/server

Version:
155 lines 5.82 kB
import { __decorate, __metadata } from "tslib"; import { createHash, randomBytes } from 'node:crypto'; import { GraphQLError } from 'graphql'; import { Injectable, Scope } from 'graphql-modules'; import { sql } from '@pgtyped/runtime'; import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js'; import { AuditLogsProvider } from '../../common/providers/audit-logs.provider.js'; import { ALLOWED_API_KEY_ROLES } from '../helpers/api-keys.helper.js'; import { AuthContextProvider } from './auth-context.provider.js'; const insertApiKey = sql ` INSERT INTO accounter_schema.api_keys (business_id, role_id, key_hash, name) VALUES ($ownerId, $roleId, $keyHash, $name) RETURNING id, name, role_id, last_used_at, created_at; `; const listApiKeys = sql ` SELECT id, name, role_id, last_used_at, created_at FROM accounter_schema.api_keys WHERE business_id = $ownerId AND revoked_at IS NULL ORDER BY created_at DESC; `; const revokeApiKey = sql ` UPDATE accounter_schema.api_keys SET revoked_at = NOW() WHERE id = $id AND business_id = $ownerId AND revoked_at IS NULL RETURNING id; `; let ApiKeysProvider = class ApiKeysProvider { db; authContextProvider; auditLogsProvider; constructor(db, authContextProvider, auditLogsProvider) { this.db = db; this.authContextProvider = authContextProvider; this.auditLogsProvider = auditLogsProvider; } async generateApiKey(name, roleId) { const authContext = await this.requireBusinessOwner(); if (!ALLOWED_API_KEY_ROLES.includes(roleId)) { throw new GraphQLError(`Invalid role for API key. Must be one of: ${ALLOWED_API_KEY_ROLES.join(', ')}`, { extensions: { code: 'INVALID_ARGUMENT' }, }); } const plaintextKey = randomBytes(64).toString('hex'); const keyHash = createHash('sha256').update(plaintextKey).digest('hex'); return this.db.transaction(async (client) => { const result = await insertApiKey.run({ ownerId: authContext.tenant.businessId, roleId, keyHash, name, }, client); const [row] = result; if (!row) { throw new GraphQLError('API key creation failed: no record returned from DB.', { extensions: { code: 'INTERNAL_SERVER_ERROR' }, }); } const record = { id: row.id, name: row.name, roleId: row.role_id, lastUsedAt: row.last_used_at, createdAt: row.created_at, }; await this.auditLogsProvider.log({ ownerId: authContext.tenant.businessId, userId: authContext.user.userId, auth0UserId: authContext.user.auth0UserId ?? undefined, action: 'API_KEY_CREATED', entity: 'ApiKey', entityId: record.id, details: { name: record.name, roleId: record.roleId, }, }, client); return { apiKey: plaintextKey, record, }; }); } async listApiKeys() { const authContext = await this.requireBusinessOwner(); const rows = await listApiKeys.run({ ownerId: authContext.tenant.businessId }, this.db); return rows.map(row => ({ id: row.id, name: row.name, roleId: row.role_id, lastUsedAt: row.last_used_at, createdAt: row.created_at, })); } async revokeApiKey(id) { const authContext = await this.requireBusinessOwner(); return this.db.transaction(async (client) => { const rows = await revokeApiKey.run({ id, ownerId: authContext.tenant.businessId }, client); const revoked = rows.length > 0; if (!revoked) { return false; } await this.auditLogsProvider.log({ ownerId: authContext.tenant.businessId, userId: authContext.user.userId, auth0UserId: authContext.user.auth0UserId ?? undefined, action: 'API_KEY_REVOKED', entity: 'ApiKey', entityId: id, details: { revoked: true, }, }, client); return true; }); } async requireBusinessOwner() { const authContext = await this.authContextProvider.getAuthContext(); if (!authContext) { throw new GraphQLError('Authentication required', { extensions: { code: 'UNAUTHENTICATED' }, }); } const { user, tenant, ...restOfAuthContext } = authContext; if (!tenant?.businessId) { throw new GraphQLError('Authentication required', { extensions: { code: 'UNAUTHENTICATED' }, }); } if (!user) { throw new GraphQLError('Authentication required', { extensions: { code: 'UNAUTHENTICATED' }, }); } if (user.roleId !== 'business_owner') { throw new GraphQLError('Requires role: business_owner', { extensions: { code: 'FORBIDDEN' }, }); } return { ...restOfAuthContext, user, tenant }; } }; ApiKeysProvider = __decorate([ Injectable({ scope: Scope.Operation, global: true, }), __metadata("design:paramtypes", [TenantAwareDBClient, AuthContextProvider, AuditLogsProvider]) ], ApiKeysProvider); export { ApiKeysProvider }; //# sourceMappingURL=api-keys.provider.js.map