agmission/Development/server/controllers/api_key.js

184 lines
5.9 KiB
JavaScript

'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 };