'use strict'; const crypto = require('crypto'); const bcrypt = require('bcryptjs'); const ApiKey = require('../model/api_key'); const { AppAuthError, AppParamError, AppInputError } = require('../helpers/app_error'); const { Errors, UserTypes, HttpStatus, ApiKeyServices } = require('../helpers/constants'); const ObjectId = require('mongodb').ObjectId; const KEY_LENGTH_BYTES = 32; // 256-bit random key → 64-char hex string const BCRYPT_ROUNDS = 10; const MAX_KEYS_PER_OWNER = 10; /** * POST /api/keys * Body: { label: string } * Creates a new API key for the authenticated applicator. * Returns the plain key ONCE — it is never retrievable again. * Admin users may supply an optional `ownerId` to create a key on behalf of another account. */ async function createKey(req, res) { const input = req.body; if (!input || !input.label || !String(input.label).trim()) { return AppParamError.throw('label is required'); } const isAdmin = req.ut === UserTypes.ADMIN; let ownerId; if (isAdmin && input.ownerId) { if (!ObjectId.isValid(input.ownerId)) AppParamError.throw('invalid ownerId'); ownerId = ObjectId(input.ownerId); } else { ownerId = ObjectId(req.uid); } // Enforce per-owner key limit const existing = await ApiKey.countDocuments({ owner: ownerId, active: true }); if (existing >= MAX_KEYS_PER_OWNER) { return AppInputError.throw(`Maximum of ${MAX_KEYS_PER_OWNER} active keys allowed per account`); } const service = input.service || ApiKeyServices.DATA_EXPORT; if (!Object.values(ApiKeyServices).includes(service)) { return AppParamError.throw(`service must be one of: ${Object.values(ApiKeyServices).join(', ')}`); } const plainKey = crypto.randomBytes(KEY_LENGTH_BYTES).toString('hex'); const prefix = plainKey.substring(0, 8); const keyHash = await bcrypt.hash(plainKey, BCRYPT_ROUNDS); const apiKey = await ApiKey.create({ owner: ownerId, label: String(input.label).trim(), prefix, keyHash, service, managedBy: isAdmin && input.ownerId ? 'admin' : 'owner' }); // Populate owner so the client can display name/username/contact immediately await apiKey.populate('owner', 'username name contact'); // Return plain key once — include it only in the creation response res.status(HttpStatus.CREATED).json({ _id: apiKey._id, label: apiKey.label, prefix: apiKey.prefix, service: apiKey.service, active: apiKey.active, managedBy: apiKey.managedBy, createdAt: apiKey.createdAt, owner: apiKey.owner, // Plain key — shown once, not stored key: plainKey }); } /** * GET /api/keys * Returns all active and inactive keys belonging to the authenticated user. * Admin users may supply ?ownerId= to list another account's keys. */ async function listKeys(req, res) { const isAdmin = req?.ut === UserTypes.ADMIN; let filter; if (isAdmin && req.query.ownerId) { if (!ObjectId.isValid(req.query.ownerId)) AppParamError.throw('invalid ownerId'); filter = { owner: ObjectId(req.query.ownerId) }; } else if (isAdmin) { filter = {}; // Admin without ownerId → return all keys } else { filter = { owner: ObjectId(req.uid) }; } const query = ApiKey.find(filter, '-keyHash -__v').sort({ createdAt: -1 }); if (isAdmin) query.populate('owner', 'username name contact'); const keys = await query.lean(); res.json(keys); } /** * PATCH /api/keys/:keyId/revoke * Revokes (soft-deletes by setting active=false) the specified key. * Only system admin can revoke keys. */ async function revokeKey(req, res) { const keyId = req.params.keyId; if (!ObjectId.isValid(keyId)) AppParamError.throw('invalid keyId'); const isAdmin = req.ut === UserTypes.ADMIN; if (!isAdmin) { return AppAuthError.throw('Only system admin can revoke API keys'); } const result = await ApiKey.updateOne({ _id: ObjectId(keyId) }, { $set: { active: false } }); if (!result.matchedCount) { return res.status(HttpStatus.NOT_FOUND).json({ error: Errors.NOT_FOUND }); } res.status(HttpStatus.NO_CONTENT).end(); } /** * DELETE /api/keys/:keyId * Permanently deletes the specified key. * Owner can delete their own keys; admin can delete any. */ async function deleteKey(req, res) { const keyId = req.params.keyId; if (!ObjectId.isValid(keyId)) AppParamError.throw('invalid keyId'); const isAdmin = req.ut === UserTypes.ADMIN; const filter = { _id: ObjectId(keyId) }; if (!isAdmin) { filter.owner = ObjectId(req.uid); } const result = await ApiKey.deleteOne(filter); if (!result.deletedCount) { return res.status(HttpStatus.NOT_FOUND).json({ error: Errors.NOT_FOUND }); } res.status(HttpStatus.NO_CONTENT).end(); } /** * POST /api/keys/:keyId/regenerate * Generates a new secret for an existing key, replacing the old hash and prefix. * Owner can regenerate their own keys; admin can regenerate any. * Returns the new plain key ONCE — it is never retrievable again. */ async function regenerateKey(req, res) { const keyId = req.params.keyId; if (!ObjectId.isValid(keyId)) AppParamError.throw('invalid keyId'); const isAdmin = req.ut === UserTypes.ADMIN; const filter = { _id: ObjectId(keyId) }; if (!isAdmin) { filter.owner = ObjectId(req.uid); } const existing = await ApiKey.findOne(filter, '_id label prefix service active managedBy createdAt').lean(); if (!existing) { return res.status(HttpStatus.NOT_FOUND).json({ error: Errors.NOT_FOUND }); } const plainKey = crypto.randomBytes(KEY_LENGTH_BYTES).toString('hex'); const prefix = plainKey.substring(0, 8); const keyHash = await bcrypt.hash(plainKey, BCRYPT_ROUNDS); await ApiKey.updateOne({ _id: existing._id }, { $set: { prefix, keyHash, active: true } }); res.json({ _id: existing._id, label: existing.label, prefix, service: existing.service, active: true, managedBy: existing.managedBy, createdAt: existing.createdAt, key: plainKey }); } module.exports = { createKey, listKeys, revokeKey, deleteKey, regenerateKey };