184 lines
5.9 KiB
JavaScript
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 };
|