mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +00:00
feat: api keys
This commit is contained in:
@@ -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 { GenerateApiKey } from './commands/GenerateApiKey.service';
|
||||||
import { GetApiKeysService } from './queries/GetApiKeys.service';
|
import { GetApiKeysService } from './queries/GetApiKeys.service';
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export class AuthApiKeysController {
|
|||||||
return this.generateApiKeyService.generate();
|
return this.generateApiKeyService.generate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/revoke')
|
@Put(':id/revoke')
|
||||||
async revoke(@Param('id') id: number) {
|
async revoke(@Param('id') id: number) {
|
||||||
return this.generateApiKeyService.revoke(id);
|
return this.generateApiKeyService.revoke(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { ApiKeyModel } from '../models/ApiKey.model';
|
import { ApiKeyModel } from '../models/ApiKey.model';
|
||||||
|
import { ClsService } from 'nestjs-cls';
|
||||||
|
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthApiKeyAuthorizeService {
|
export class AuthApiKeyAuthorizeService {
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly clsService: ClsService,
|
||||||
|
|
||||||
@Inject(ApiKeyModel.name)
|
@Inject(ApiKeyModel.name)
|
||||||
private readonly apikeyModel: typeof ApiKeyModel,
|
private readonly apikeyModel: typeof ApiKeyModel,
|
||||||
|
|
||||||
|
@Inject(TenantModel.name)
|
||||||
|
private readonly tenantModel: typeof TenantModel,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,6 +35,16 @@ export class AuthApiKeyAuthorizeService {
|
|||||||
) {
|
) {
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ export class GenerateApiKey {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Generates a new secure API key for the current tenant and system user.
|
||||||
* @returns
|
* 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() {
|
async generate() {
|
||||||
const tenant = await this.tenancyContext.getTenant();
|
const tenant = await this.tenancyContext.getTenant();
|
||||||
@@ -27,15 +28,23 @@ export class GenerateApiKey {
|
|||||||
tenantId: tenant.id,
|
tenantId: tenant.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
revoked: false,
|
revokedAt: null,
|
||||||
});
|
});
|
||||||
// Return the created API key (not the full record for security)
|
// Return the created API key (not the full record for security)
|
||||||
return { key: apiKeyRecord.key, id: apiKeyRecord.id };
|
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) {
|
async revoke(apiKeyId: number) {
|
||||||
// Set the revoked flag to true for the given API key
|
// 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 };
|
return { id: apiKeyId, revoked: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
import { SystemModel } from '@/modules/System/models/SystemModel';
|
import { SystemModel } from '@/modules/System/models/SystemModel';
|
||||||
|
import { Model } from 'objection';
|
||||||
|
|
||||||
export class ApiKeyModel extends SystemModel {
|
export class ApiKeyModel extends SystemModel {
|
||||||
readonly key: string;
|
readonly key: string;
|
||||||
readonly name?: string;
|
readonly name?: string;
|
||||||
readonly createdAt: Date;
|
readonly createdAt: Date;
|
||||||
readonly expiresAt?: Date;
|
readonly expiresAt?: Date;
|
||||||
readonly revoked?: boolean;
|
readonly revokedAt?: Date;
|
||||||
readonly userId: number;
|
readonly userId: number;
|
||||||
readonly tenantId: number;
|
readonly tenantId: number;
|
||||||
|
|
||||||
|
get revoked() {
|
||||||
|
return !!this.revokedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return ['revoked'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
@@ -22,4 +31,42 @@ export class ApiKeyModel extends SystemModel {
|
|||||||
get timestamps() {
|
get timestamps() {
|
||||||
return ['createdAt'];
|
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');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export class GetApiKeysService {
|
|||||||
const tenant = await this.tenancyContext.getTenant();
|
const tenant = await this.tenancyContext.getTenant();
|
||||||
const apiKeys = await this.apiKeyModel
|
const apiKeys = await this.apiKeyModel
|
||||||
.query()
|
.query()
|
||||||
|
.modify('notRevoked')
|
||||||
.where({ tenantId: tenant.id });
|
.where({ tenantId: tenant.id });
|
||||||
|
|
||||||
return this.injectableTransformer.transform(
|
return this.injectableTransformer.transform(
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||||
|
|
||||||
export class GetApiKeysTransformer extends Transformer {
|
export class GetApiKeysTransformer extends Transformer {
|
||||||
public includeAttributes = (): string[] => {
|
public excludeAttributes = (): string[] => {
|
||||||
return [
|
return ['tenantId'];
|
||||||
'id',
|
|
||||||
'key',
|
|
||||||
'name',
|
|
||||||
'createdAt',
|
|
||||||
'expiresAt',
|
|
||||||
'revoked',
|
|
||||||
'userId',
|
|
||||||
'tenantId',
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isEmpty } from 'lodash';
|
||||||
import {
|
import {
|
||||||
Injectable,
|
Injectable,
|
||||||
CanActivate,
|
CanActivate,
|
||||||
@@ -24,11 +25,12 @@ export class TenancyGlobalGuard implements CanActivate {
|
|||||||
canActivate(context: ExecutionContext): boolean {
|
canActivate(context: ExecutionContext): boolean {
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
const organizationId = request.headers['organization-id'];
|
const organizationId = request.headers['organization-id'];
|
||||||
|
const authorization = request.headers['authorization']?.trim();
|
||||||
|
|
||||||
const isPublic = this.reflector.getAllAndOverride<boolean>(
|
const isPublic = this.reflector.getAllAndOverride<boolean>(
|
||||||
IS_PUBLIC_ROUTE,
|
IS_PUBLIC_ROUTE,
|
||||||
[context.getHandler(), context.getClass()],
|
[context.getHandler(), context.getClass()],
|
||||||
)
|
);
|
||||||
const isTenantAgnostic = this.reflector.getAllAndOverride<boolean>(
|
const isTenantAgnostic = this.reflector.getAllAndOverride<boolean>(
|
||||||
IS_TENANT_AGNOSTIC,
|
IS_TENANT_AGNOSTIC,
|
||||||
[context.getHandler(), context.getClass()],
|
[context.getHandler(), context.getClass()],
|
||||||
@@ -36,7 +38,7 @@ export class TenancyGlobalGuard implements CanActivate {
|
|||||||
if (isPublic || isTenantAgnostic) {
|
if (isPublic || isTenantAgnostic) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!organizationId) {
|
if (!isEmpty(authorization) && !organizationId) {
|
||||||
throw new UnauthorizedException('Organization ID is required.');
|
throw new UnauthorizedException('Organization ID is required.');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
Reference in New Issue
Block a user