@accounter/server
Version:
Accounter GraphQL server
155 lines • 5.82 kB
JavaScript
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