feat: api keys

This commit is contained in:
Ahmed Bouhuolia
2025-07-01 23:45:38 +02:00
parent 84cb7693c8
commit 9457b3cda1
7 changed files with 87 additions and 20 deletions

View File

@@ -1,4 +1,4 @@
import { Controller, Post, Param, Get } from '@nestjs/common';
import { Controller, Post, Param, Get, Put } from '@nestjs/common';
import { GenerateApiKey } from './commands/GenerateApiKey.service';
import { GetApiKeysService } from './queries/GetApiKeys.service';
@@ -14,7 +14,7 @@ export class AuthApiKeysController {
return this.generateApiKeyService.generate();
}
@Post(':id/revoke')
@Put(':id/revoke')
async revoke(@Param('id') id: number) {
return this.generateApiKeyService.revoke(id);
}

View File

@@ -1,11 +1,18 @@
import { Inject, Injectable } from '@nestjs/common';
import { ApiKeyModel } from '../models/ApiKey.model';
import { ClsService } from 'nestjs-cls';
import { TenantModel } from '@/modules/System/models/TenantModel';
@Injectable()
export class AuthApiKeyAuthorizeService {
constructor(
private readonly clsService: ClsService,
@Inject(ApiKeyModel.name)
private readonly apikeyModel: typeof ApiKeyModel,
@Inject(TenantModel.name)
private readonly tenantModel: typeof TenantModel,
) {}
/**
@@ -28,6 +35,16 @@ export class AuthApiKeyAuthorizeService {
) {
return false;
}
const tenant = await this.tenantModel
.query()
.findById(apiKeyRecord.tenantId);
if (!tenant) return false;
this.clsService.set('tenantId', tenant.id);
this.clsService.set('organizationId', tenant.organizationId);
this.clsService.set('userId', apiKeyRecord.userId);
return true;
}
}

View File

@@ -12,8 +12,9 @@ export class GenerateApiKey {
) {}
/**
*
* @returns
* Generates a new secure API key for the current tenant and system user.
* The key is saved in the database and returned (only the key and id for security).
* @returns {Promise<{ key: string; id: number }>} The generated API key and its database id.
*/
async generate() {
const tenant = await this.tenancyContext.getTenant();
@@ -27,15 +28,23 @@ export class GenerateApiKey {
tenantId: tenant.id,
userId: user.id,
createdAt: new Date(),
revoked: false,
revokedAt: null,
});
// Return the created API key (not the full record for security)
return { key: apiKeyRecord.key, id: apiKeyRecord.id };
}
/**
* Revokes an API key by setting its revokedAt timestamp.
* @param {number} apiKeyId - The id of the API key to revoke.
* @returns {Promise<{ id: number; revoked: boolean }>} The id of the revoked API key and a revoked flag.
*/
async revoke(apiKeyId: number) {
// Set the revoked flag to true for the given API key
await ApiKeyModel.query().findById(apiKeyId).patch({ revoked: true });
await ApiKeyModel.query()
.findById(apiKeyId)
.patch({ revokedAt: new Date() });
return { id: apiKeyId, revoked: true };
}
}

View File

@@ -1,14 +1,23 @@
import { SystemModel } from '@/modules/System/models/SystemModel';
import { Model } from 'objection';
export class ApiKeyModel extends SystemModel {
readonly key: string;
readonly name?: string;
readonly createdAt: Date;
readonly expiresAt?: Date;
readonly revoked?: boolean;
readonly revokedAt?: Date;
readonly userId: number;
readonly tenantId: number;
get revoked() {
return !!this.revokedAt;
}
static get virtualAttributes() {
return ['revoked'];
}
/**
* Table name
*/
@@ -22,4 +31,42 @@ export class ApiKeyModel extends SystemModel {
get timestamps() {
return ['createdAt'];
}
/**
* Relation mappings for Objection.js
*/
static get relationMappings() {
const { SystemUser } = require('../../System/models/SystemUser');
const { TenantModel } = require('../../System/models/TenantModel');
return {
user: {
relation: Model.BelongsToOneRelation,
modelClass: SystemUser,
join: {
from: 'api_keys.userId',
to: 'users.id',
},
},
tenant: {
relation: Model.BelongsToOneRelation,
modelClass: TenantModel,
join: {
from: 'api_keys.tenantId',
to: 'tenants.id',
},
},
};
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
notRevoked(query) {
query.whereNull('revokedAt');
},
};
}
}

View File

@@ -18,6 +18,7 @@ export class GetApiKeysService {
const tenant = await this.tenancyContext.getTenant();
const apiKeys = await this.apiKeyModel
.query()
.modify('notRevoked')
.where({ tenantId: tenant.id });
return this.injectableTransformer.transform(

View File

@@ -1,16 +1,7 @@
import { Transformer } from '@/modules/Transformer/Transformer';
export class GetApiKeysTransformer extends Transformer {
public includeAttributes = (): string[] => {
return [
'id',
'key',
'name',
'createdAt',
'expiresAt',
'revoked',
'userId',
'tenantId',
];
public excludeAttributes = (): string[] => {
return ['tenantId'];
};
}