Update API Keys to be tied to a user, add apikey lru-cache, handle deactivating expired keys

This commit is contained in:
advplyr 2025-06-30 14:53:11 -05:00
parent af1ff12dbb
commit 4d32a22de9
13 changed files with 335 additions and 217 deletions

View file

@ -20,7 +20,19 @@ class ApiKeyController {
* @param {Response} res
*/
async getAll(req, res) {
const apiKeys = await Database.apiKeyModel.findAll()
const apiKeys = await Database.apiKeyModel.findAll({
include: [
{
model: Database.userModel,
attributes: ['id', 'username', 'type']
},
{
model: Database.userModel,
as: 'createdByUser',
attributes: ['id', 'username', 'type']
}
]
})
return res.json({
apiKeys: apiKeys.map((a) => a.toJSON())
@ -42,10 +54,21 @@ class ApiKeyController {
Logger.warn(`[ApiKeyController] create: Invalid expiresIn: ${req.body.expiresIn}`)
return res.sendStatus(400)
}
if (!req.body.userId || typeof req.body.userId !== 'string') {
Logger.warn(`[ApiKeyController] create: Invalid userId: ${req.body.userId}`)
return res.sendStatus(400)
}
const user = await Database.userModel.getUserById(req.body.userId)
if (!user) {
Logger.warn(`[ApiKeyController] create: User not found: ${req.body.userId}`)
return res.sendStatus(400)
}
if (user.type === 'root' && !req.user.isRoot) {
Logger.warn(`[ApiKeyController] create: Root user API key cannot be created by non-root user`)
return res.sendStatus(403)
}
const keyId = uuidv4() // Generate key id ahead of time to use in JWT
const permissions = Database.apiKeyModel.mergePermissionsWithDefault(req.body.permissions)
const apiKey = await Database.apiKeyModel.generateApiKey(keyId, req.body.name, req.body.expiresIn)
if (!apiKey) {
@ -60,9 +83,9 @@ class ApiKeyController {
id: keyId,
name: req.body.name,
expiresAt,
permissions,
userId: req.user.id,
isActive: !!req.body.isActive
userId: req.body.userId,
isActive: !!req.body.isActive,
createdByUserId: req.user.id
})
Logger.info(`[ApiKeyController] Created API key "${apiKeyInstance.name}"`)
@ -76,34 +99,64 @@ class ApiKeyController {
/**
* PATCH: /api/api-keys/:id
* Only isActive and permissions can be updated because name and expiresIn are in the JWT
* Only isActive and userId can be updated because name and expiresIn are in the JWT
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async update(req, res) {
const apiKey = await Database.apiKeyModel.findByPk(req.params.id)
const apiKey = await Database.apiKeyModel.findByPk(req.params.id, {
include: {
model: Database.userModel
}
})
if (!apiKey) {
return res.sendStatus(404)
}
// Only root user can update root user API keys
if (apiKey.user.type === 'root' && !req.user.isRoot) {
Logger.warn(`[ApiKeyController] update: Root user API key cannot be updated by non-root user`)
return res.sendStatus(403)
}
let hasUpdates = false
if (req.body.userId !== undefined) {
if (typeof req.body.userId !== 'string') {
Logger.warn(`[ApiKeyController] update: Invalid userId: ${req.body.userId}`)
return res.sendStatus(400)
}
const user = await Database.userModel.getUserById(req.body.userId)
if (!user) {
Logger.warn(`[ApiKeyController] update: User not found: ${req.body.userId}`)
return res.sendStatus(400)
}
if (user.type === 'root' && !req.user.isRoot) {
Logger.warn(`[ApiKeyController] update: Root user API key cannot be created by non-root user`)
return res.sendStatus(403)
}
if (apiKey.userId !== req.body.userId) {
apiKey.userId = req.body.userId
hasUpdates = true
}
}
if (req.body.isActive !== undefined) {
if (typeof req.body.isActive !== 'boolean') {
return res.sendStatus(400)
}
apiKey.isActive = req.body.isActive
if (apiKey.isActive !== req.body.isActive) {
apiKey.isActive = req.body.isActive
hasUpdates = true
}
}
if (req.body.permissions && Object.keys(req.body.permissions).length > 0) {
const permissions = Database.apiKeyModel.mergePermissionsWithDefault(req.body.permissions)
apiKey.permissions = permissions
if (hasUpdates) {
await apiKey.save()
Logger.info(`[ApiKeyController] Updated API key "${apiKey.name}"`)
} else {
Logger.info(`[ApiKeyController] No updates needed to API key "${apiKey.name}"`)
}
await apiKey.save()
Logger.info(`[ApiKeyController] Updated API key "${apiKey.name}"`)
return res.json({
apiKey: apiKey.toJSON()
})