ecrs-auth-core 1.0.61 → 1.0.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,6 +19,7 @@ const roles_guard_1 = require("./guards/roles.guard");
19
19
  const feature_guard_1 = require("./guards/feature.guard");
20
20
  const route_guard_1 = require("./guards/route.guard");
21
21
  const permission_guard_1 = require("./guards/permission.guard");
22
+ const api_key_guard_1 = require("./guards/api-key.guard");
22
23
  exports.AUTH_CORE_OPTIONS = 'AUTH_CORE_OPTIONS';
23
24
  // @Global()
24
25
  // @Module({})
@@ -109,6 +110,11 @@ let AuthCoreModule = AuthCoreModule_1 = class AuthCoreModule {
109
110
  useFactory: (opts) => opts.moduleConfig || {},
110
111
  inject: [exports.AUTH_CORE_OPTIONS],
111
112
  },
113
+ {
114
+ provide: 'API_KEY_REPOSITORY',
115
+ useFactory: (opts) => opts.repositories?.apiKeyRepo || null,
116
+ inject: [exports.AUTH_CORE_OPTIONS],
117
+ },
112
118
  auth_service_1.AuthService,
113
119
  jwt_strategy_1.JwtStrategy,
114
120
  jwt_guard_1.JwtAuthGuard,
@@ -117,12 +123,14 @@ let AuthCoreModule = AuthCoreModule_1 = class AuthCoreModule {
117
123
  feature_guard_1.FeatureGuard,
118
124
  route_guard_1.RouteGuard,
119
125
  permission_guard_1.PermissionGuard,
126
+ api_key_guard_1.ApiKeyGuard,
120
127
  ],
121
128
  controllers: [auth_controller_1.AuthController],
122
129
  exports: [
123
130
  // ⬇️ export these so Superadmin can resolve guard deps
124
131
  exports.AUTH_CORE_OPTIONS,
125
132
  'MODULE_CONFIG',
133
+ 'API_KEY_REPOSITORY',
126
134
  jwt_1.JwtModule,
127
135
  auth_service_1.AuthService,
128
136
  jwt_strategy_1.JwtStrategy,
@@ -132,6 +140,7 @@ let AuthCoreModule = AuthCoreModule_1 = class AuthCoreModule {
132
140
  feature_guard_1.FeatureGuard,
133
141
  route_guard_1.RouteGuard,
134
142
  permission_guard_1.PermissionGuard,
143
+ api_key_guard_1.ApiKeyGuard,
135
144
  ],
136
145
  };
137
146
  }
@@ -74,7 +74,6 @@ let AuthService = class AuthService {
74
74
  async hasModuleAccess(userId, moduleId) {
75
75
  if (!Number.isFinite(moduleId))
76
76
  return false;
77
- // Check both isDeleted and status flags for active access
78
77
  const access = await this.moduleAccessRepo.findOne({
79
78
  where: { userId, moduleId, isDeleted: 0, status: 1 },
80
79
  });
@@ -0,0 +1,5 @@
1
+ export interface ApiKeyOptions {
2
+ scope?: string;
3
+ endpoints?: string[];
4
+ }
5
+ export declare const ApiKey: (options?: ApiKeyOptions) => import("@nestjs/common").CustomDecorator<string>;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiKey = void 0;
4
+ const common_1 = require("@nestjs/common");
5
+ const ApiKey = (options) => {
6
+ return (0, common_1.SetMetadata)('api_key_options', options || {});
7
+ };
8
+ exports.ApiKey = ApiKey;
@@ -0,0 +1,16 @@
1
+ export declare class ApiKeyEntity {
2
+ id: number;
3
+ key: string;
4
+ clientName: string;
5
+ clientDescription: string | null;
6
+ scope: string;
7
+ allowedIps: string | null;
8
+ allowedEndpoints: string | null;
9
+ isActive: boolean;
10
+ rateLimit: number | null;
11
+ lastUsedAt: number | null;
12
+ createdAt: Date;
13
+ updatedAt: Date;
14
+ createdBy: string | null;
15
+ metadata: string | null;
16
+ }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ApiKeyEntity = void 0;
13
+ const typeorm_1 = require("typeorm");
14
+ let ApiKeyEntity = class ApiKeyEntity {
15
+ };
16
+ exports.ApiKeyEntity = ApiKeyEntity;
17
+ __decorate([
18
+ (0, typeorm_1.PrimaryGeneratedColumn)(),
19
+ __metadata("design:type", Number)
20
+ ], ApiKeyEntity.prototype, "id", void 0);
21
+ __decorate([
22
+ (0, typeorm_1.Column)({ type: 'varchar', length: 255, unique: true }),
23
+ __metadata("design:type", String)
24
+ ], ApiKeyEntity.prototype, "key", void 0);
25
+ __decorate([
26
+ (0, typeorm_1.Column)({ type: 'varchar', length: 255 }),
27
+ __metadata("design:type", String)
28
+ ], ApiKeyEntity.prototype, "clientName", void 0);
29
+ __decorate([
30
+ (0, typeorm_1.Column)({ type: 'varchar', length: 255, nullable: true }),
31
+ __metadata("design:type", Object)
32
+ ], ApiKeyEntity.prototype, "clientDescription", void 0);
33
+ __decorate([
34
+ (0, typeorm_1.Column)({ type: 'varchar', length: 50, default: 'common' }),
35
+ __metadata("design:type", String)
36
+ ], ApiKeyEntity.prototype, "scope", void 0);
37
+ __decorate([
38
+ (0, typeorm_1.Column)({ type: 'text', nullable: true }),
39
+ __metadata("design:type", Object)
40
+ ], ApiKeyEntity.prototype, "allowedIps", void 0);
41
+ __decorate([
42
+ (0, typeorm_1.Column)({ type: 'text', nullable: true }),
43
+ __metadata("design:type", Object)
44
+ ], ApiKeyEntity.prototype, "allowedEndpoints", void 0);
45
+ __decorate([
46
+ (0, typeorm_1.Column)({ type: 'boolean', default: true }),
47
+ __metadata("design:type", Boolean)
48
+ ], ApiKeyEntity.prototype, "isActive", void 0);
49
+ __decorate([
50
+ (0, typeorm_1.Column)({ type: 'bigint', nullable: true }),
51
+ __metadata("design:type", Object)
52
+ ], ApiKeyEntity.prototype, "rateLimit", void 0);
53
+ __decorate([
54
+ (0, typeorm_1.Column)({ type: 'bigint', nullable: true }),
55
+ __metadata("design:type", Object)
56
+ ], ApiKeyEntity.prototype, "lastUsedAt", void 0);
57
+ __decorate([
58
+ (0, typeorm_1.CreateDateColumn)(),
59
+ __metadata("design:type", Date)
60
+ ], ApiKeyEntity.prototype, "createdAt", void 0);
61
+ __decorate([
62
+ (0, typeorm_1.UpdateDateColumn)(),
63
+ __metadata("design:type", Date)
64
+ ], ApiKeyEntity.prototype, "updatedAt", void 0);
65
+ __decorate([
66
+ (0, typeorm_1.Column)({ type: 'varchar', length: 255, nullable: true }),
67
+ __metadata("design:type", Object)
68
+ ], ApiKeyEntity.prototype, "createdBy", void 0);
69
+ __decorate([
70
+ (0, typeorm_1.Column)({ type: 'text', nullable: true }),
71
+ __metadata("design:type", Object)
72
+ ], ApiKeyEntity.prototype, "metadata", void 0);
73
+ exports.ApiKeyEntity = ApiKeyEntity = __decorate([
74
+ (0, typeorm_1.Entity)({ name: 'tbl_c_api_keys' })
75
+ ], ApiKeyEntity);
@@ -0,0 +1,15 @@
1
+ import { CanActivate, ExecutionContext } from '@nestjs/common';
2
+ import { Reflector } from '@nestjs/core';
3
+ import { Repository } from 'typeorm';
4
+ import { ApiKeyEntity } from '../entities/api-key.entity';
5
+ export declare class ApiKeyGuard implements CanActivate {
6
+ private readonly reflector;
7
+ private apiKeyRepo;
8
+ private rateLimitMap;
9
+ constructor(reflector: Reflector, apiKeyRepository?: Repository<ApiKeyEntity>);
10
+ canActivate(context: ExecutionContext): Promise<boolean>;
11
+ private extractApiKey;
12
+ private validateApiKey;
13
+ private getClientIp;
14
+ private checkRateLimit;
15
+ }
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ApiKeyGuard = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const core_1 = require("@nestjs/core");
18
+ const typeorm_1 = require("typeorm");
19
+ let ApiKeyGuard = class ApiKeyGuard {
20
+ constructor(reflector, apiKeyRepository) {
21
+ this.reflector = reflector;
22
+ this.rateLimitMap = new Map();
23
+ if (apiKeyRepository) {
24
+ this.apiKeyRepo = apiKeyRepository;
25
+ }
26
+ }
27
+ async canActivate(context) {
28
+ const options = this.reflector.get('api_key_options', context.getHandler());
29
+ // If no @ApiKey decorator is present, skip this guard
30
+ if (!options) {
31
+ return true;
32
+ }
33
+ const request = context.switchToHttp().getRequest();
34
+ const apiKey = this.extractApiKey(request);
35
+ if (!apiKey) {
36
+ throw new common_1.UnauthorizedException('API key is missing');
37
+ }
38
+ // Validate API key
39
+ const validation = await this.validateApiKey(apiKey, request, options);
40
+ if (!validation.valid) {
41
+ throw new common_1.UnauthorizedException(validation.message || 'Invalid API key');
42
+ }
43
+ // Attach API key info to request for downstream use
44
+ request.apiKey = validation.apiKey;
45
+ request.apiKeyClient = {
46
+ id: validation.apiKey?.id,
47
+ clientName: validation.apiKey?.clientName,
48
+ scope: validation.apiKey?.scope,
49
+ };
50
+ return true;
51
+ }
52
+ extractApiKey(request) {
53
+ // Check 'X-API-Key' header
54
+ const headerKey = request.headers['x-api-key'];
55
+ if (headerKey) {
56
+ return headerKey;
57
+ }
58
+ // Check 'api_key' query parameter
59
+ if (request.query?.api_key) {
60
+ return request.query.api_key;
61
+ }
62
+ // Check Authorization header (format: 'ApiKey <key>')
63
+ const authHeader = request.headers.authorization;
64
+ if (authHeader && authHeader.startsWith('ApiKey ')) {
65
+ return authHeader.substring(7);
66
+ }
67
+ return null;
68
+ }
69
+ async validateApiKey(apiKey, request, options) {
70
+ if (!this.apiKeyRepo) {
71
+ return { valid: false, message: 'API key validation not configured' };
72
+ }
73
+ const record = await this.apiKeyRepo.findOne({
74
+ where: { key: apiKey, isActive: true },
75
+ });
76
+ if (!record) {
77
+ return { valid: false, message: 'API key not found or inactive' };
78
+ }
79
+ // Check scope
80
+ const requiredScope = options.scope || 'common';
81
+ if (record.scope !== 'common' && record.scope !== requiredScope) {
82
+ return {
83
+ valid: false,
84
+ message: `API key scope '${record.scope}' does not match required scope '${requiredScope}'`,
85
+ };
86
+ }
87
+ // Check IP whitelist
88
+ if (record.allowedIps) {
89
+ const clientIp = this.getClientIp(request);
90
+ const allowedIps = record.allowedIps.split(',').map((ip) => ip.trim());
91
+ if (!allowedIps.includes(clientIp)) {
92
+ return {
93
+ valid: false,
94
+ message: `Client IP '${clientIp}' is not whitelisted`,
95
+ };
96
+ }
97
+ }
98
+ // Check endpoint restriction
99
+ if (record.allowedEndpoints) {
100
+ const requestEndpoint = `${request.method} ${request.path}`;
101
+ const allowedEndpoints = record.allowedEndpoints
102
+ .split(',')
103
+ .map((ep) => ep.trim());
104
+ const isAllowed = allowedEndpoints.some((ep) => {
105
+ // Support wildcard matching (e.g., 'GET /spot/*')
106
+ if (ep.includes('*')) {
107
+ const pattern = ep.replace(/\*/g, '.*');
108
+ return new RegExp(`^${pattern}$`).test(requestEndpoint);
109
+ }
110
+ return ep === requestEndpoint;
111
+ });
112
+ if (!isAllowed) {
113
+ return {
114
+ valid: false,
115
+ message: `Endpoint '${requestEndpoint}' is not allowed for this API key`,
116
+ };
117
+ }
118
+ }
119
+ // Check rate limit
120
+ if (record.rateLimit) {
121
+ const isRateLimited = this.checkRateLimit(apiKey, record.rateLimit);
122
+ if (isRateLimited) {
123
+ return {
124
+ valid: false,
125
+ message: `Rate limit exceeded (${record.rateLimit} requests per minute)`,
126
+ };
127
+ }
128
+ }
129
+ // Update last used timestamp
130
+ if (this.apiKeyRepo) {
131
+ await this.apiKeyRepo.update(record.id, {
132
+ lastUsedAt: Date.now(),
133
+ });
134
+ }
135
+ return { valid: true, apiKey: record };
136
+ }
137
+ getClientIp(request) {
138
+ // Support common proxy headers
139
+ const xForwardedFor = request.headers['x-forwarded-for'];
140
+ if (xForwardedFor) {
141
+ return xForwardedFor.split(',')[0].trim();
142
+ }
143
+ const xRealIp = request.headers['x-real-ip'];
144
+ if (xRealIp) {
145
+ return xRealIp;
146
+ }
147
+ return request.ip || request.connection.remoteAddress || 'unknown';
148
+ }
149
+ checkRateLimit(apiKey, rateLimit) {
150
+ const now = Date.now();
151
+ const limitData = this.rateLimitMap.get(apiKey);
152
+ if (!limitData) {
153
+ // First request, initialize
154
+ this.rateLimitMap.set(apiKey, {
155
+ count: 1,
156
+ resetTime: now + 60000, // 1 minute
157
+ });
158
+ return false;
159
+ }
160
+ if (now > limitData.resetTime) {
161
+ // Reset window expired
162
+ this.rateLimitMap.set(apiKey, {
163
+ count: 1,
164
+ resetTime: now + 60000,
165
+ });
166
+ return false;
167
+ }
168
+ // Still within window
169
+ if (limitData.count >= rateLimit) {
170
+ return true;
171
+ }
172
+ limitData.count++;
173
+ return false;
174
+ }
175
+ };
176
+ exports.ApiKeyGuard = ApiKeyGuard;
177
+ exports.ApiKeyGuard = ApiKeyGuard = __decorate([
178
+ (0, common_1.Injectable)(),
179
+ __param(1, (0, common_1.Inject)('API_KEY_REPOSITORY')),
180
+ __metadata("design:paramtypes", [core_1.Reflector,
181
+ typeorm_1.Repository])
182
+ ], ApiKeyGuard);
package/dist/index.d.ts CHANGED
@@ -7,11 +7,13 @@ export * from './decorators/feature.decorator';
7
7
  export * from './decorators/has-permission.decorator';
8
8
  export * from './decorators/roles.decorator';
9
9
  export * from './decorators/route-permission.decorator';
10
+ export * from './decorators/api-key.decorator';
10
11
  export * from './guards/module.guard';
11
12
  export * from './guards/roles.guard';
12
13
  export * from './guards/feature.guard';
13
14
  export * from './guards/route.guard';
14
15
  export * from './guards/permission.guard';
16
+ export * from './guards/api-key.guard';
15
17
  export * from './jwt/jwt.guard';
16
18
  export * from './jwt/jwt.strategy';
17
19
  export * from './interfaces/auth-core-options.interface';
@@ -23,3 +25,4 @@ export * from './entities/module-route.entity';
23
25
  export * from './entities/user-feature-access.entity';
24
26
  export * from './entities/user-module-access.entity';
25
27
  export * from './entities/module-screen-permission.entity';
28
+ export * from './entities/api-key.entity';
package/dist/index.js CHANGED
@@ -26,12 +26,14 @@ __exportStar(require("./decorators/feature.decorator"), exports);
26
26
  __exportStar(require("./decorators/has-permission.decorator"), exports);
27
27
  __exportStar(require("./decorators/roles.decorator"), exports);
28
28
  __exportStar(require("./decorators/route-permission.decorator"), exports);
29
+ __exportStar(require("./decorators/api-key.decorator"), exports);
29
30
  // Guards
30
31
  __exportStar(require("./guards/module.guard"), exports);
31
32
  __exportStar(require("./guards/roles.guard"), exports);
32
33
  __exportStar(require("./guards/feature.guard"), exports);
33
34
  __exportStar(require("./guards/route.guard"), exports);
34
35
  __exportStar(require("./guards/permission.guard"), exports);
36
+ __exportStar(require("./guards/api-key.guard"), exports);
35
37
  // JWT
36
38
  __exportStar(require("./jwt/jwt.guard"), exports);
37
39
  __exportStar(require("./jwt/jwt.strategy"), exports);
@@ -46,3 +48,4 @@ __exportStar(require("./entities/module-route.entity"), exports);
46
48
  __exportStar(require("./entities/user-feature-access.entity"), exports);
47
49
  __exportStar(require("./entities/user-module-access.entity"), exports);
48
50
  __exportStar(require("./entities/module-screen-permission.entity"), exports);
51
+ __exportStar(require("./entities/api-key.entity"), exports);
@@ -7,6 +7,7 @@ import { ModuleRoute } from '../entities/module-route.entity';
7
7
  import { UserFeatureAccess } from '../entities/user-feature-access.entity';
8
8
  import { UserModuleAccess } from '../entities/user-module-access.entity';
9
9
  import { ModuleScreenPermission } from '../entities/module-screen-permission.entity';
10
+ import { ApiKeyEntity } from '../entities/api-key.entity';
10
11
  export interface Repositories {
11
12
  userRepo: Repository<User>;
12
13
  roleRepo: Repository<Role>;
@@ -16,6 +17,7 @@ export interface Repositories {
16
17
  featureRepo: Repository<Feature>;
17
18
  routeRepo: Repository<ModuleRoute>;
18
19
  moduleAccessRepo: Repository<UserModuleAccess>;
20
+ apiKeyRepo?: Repository<ApiKeyEntity>;
19
21
  }
20
22
  export interface AuthModuleConfig {
21
23
  enable2FA?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecrs-auth-core",
3
- "version": "1.0.61",
3
+ "version": "1.0.62",
4
4
  "description": "Centralized authentication and authorization module for ECRS apps",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",