@flusys/nestjs-auth 0.1.0-beta.3 → 1.0.0-rc
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.
- package/README.md +274 -301
- package/cjs/config/auth.constants.js +22 -4
- package/cjs/controllers/authentication.controller.js +222 -38
- package/cjs/controllers/branch.controller.js +46 -4
- package/cjs/controllers/company-selection.controller.js +27 -20
- package/cjs/controllers/company.controller.js +46 -4
- package/cjs/controllers/user-permission.controller.js +40 -37
- package/cjs/controllers/user.controller.js +222 -23
- package/cjs/dtos/authentication.dto.js +119 -13
- package/cjs/dtos/user-permission.dto.js +28 -0
- package/cjs/dtos/user.dto.js +19 -4
- package/cjs/entities/index.js +5 -1
- package/cjs/entities/pending-registration.entity.js +125 -0
- package/cjs/entities/user-company-permission.entity.js +0 -6
- package/cjs/entities/user-token.entity.js +129 -0
- package/cjs/guards/auth-throttler.guard.js +53 -0
- package/cjs/interceptors/clear-token.interceptor.js +3 -6
- package/cjs/interceptors/set-token.interceptor.js +38 -31
- package/cjs/interfaces/auth-email-provider.interface.js +45 -0
- package/cjs/interfaces/authentication.interface.js +0 -3
- package/cjs/interfaces/index.js +2 -0
- package/cjs/interfaces/user-enricher.interface.js +4 -0
- package/cjs/modules/auth.module.js +8 -20
- package/cjs/services/auth-config.service.js +42 -47
- package/cjs/services/auth-datasource.provider.js +18 -34
- package/cjs/services/auth-email.service.js +471 -0
- package/cjs/services/authentication.service.js +240 -153
- package/cjs/services/branch.service.js +15 -48
- package/cjs/services/company-selection-session.service.js +1 -1
- package/cjs/services/company.service.js +7 -36
- package/cjs/services/index.js +1 -0
- package/cjs/services/user-permission.service.js +9 -29
- package/cjs/services/user.service.js +228 -107
- package/cjs/strategies/jwt.strategy.js +14 -1
- package/cjs/validators/password.validator.js +45 -0
- package/config/auth.constants.d.ts +5 -0
- package/controllers/authentication.controller.d.ts +13 -9
- package/controllers/company-selection.controller.d.ts +6 -2
- package/controllers/user.controller.d.ts +33 -0
- package/dtos/authentication.dto.d.ts +21 -3
- package/dtos/company-branch.dto.d.ts +0 -1
- package/dtos/company.dto.d.ts +0 -1
- package/dtos/user-permission.dto.d.ts +4 -0
- package/dtos/user.dto.d.ts +1 -2
- package/entities/index.d.ts +7 -3
- package/entities/pending-registration.entity.d.ts +17 -0
- package/entities/user-company-permission.entity.d.ts +0 -2
- package/entities/user-token.entity.d.ts +15 -0
- package/fesm/config/auth.constants.js +11 -8
- package/fesm/controllers/authentication.controller.js +224 -40
- package/fesm/controllers/branch.controller.js +49 -7
- package/fesm/controllers/company-selection.controller.js +28 -21
- package/fesm/controllers/company.controller.js +49 -7
- package/fesm/controllers/user-permission.controller.js +41 -38
- package/fesm/controllers/user.controller.js +225 -26
- package/fesm/dtos/authentication.dto.js +123 -111
- package/fesm/dtos/user-permission.dto.js +28 -0
- package/fesm/dtos/user.dto.js +20 -5
- package/fesm/entities/index.js +7 -1
- package/fesm/entities/pending-registration.entity.js +115 -0
- package/fesm/entities/user-company-permission.entity.js +0 -6
- package/fesm/entities/user-token.entity.js +113 -0
- package/fesm/guards/auth-throttler.guard.js +43 -0
- package/fesm/interceptors/clear-token.interceptor.js +2 -5
- package/fesm/interceptors/set-token.interceptor.js +37 -30
- package/fesm/interfaces/auth-email-provider.interface.js +38 -0
- package/fesm/interfaces/authentication.interface.js +1 -7
- package/fesm/interfaces/index.js +2 -0
- package/fesm/interfaces/user-enricher.interface.js +26 -0
- package/fesm/modules/auth.module.js +10 -22
- package/fesm/services/auth-config.service.js +42 -47
- package/fesm/services/auth-datasource.provider.js +19 -35
- package/fesm/services/auth-email.service.js +420 -0
- package/fesm/services/authentication.service.js +238 -151
- package/fesm/services/branch.service.js +16 -49
- package/fesm/services/company-selection-session.service.js +1 -1
- package/fesm/services/company.service.js +8 -37
- package/fesm/services/index.js +1 -0
- package/fesm/services/user-permission.service.js +10 -30
- package/fesm/services/user.service.js +229 -108
- package/fesm/strategies/jwt.strategy.js +14 -1
- package/fesm/validators/password.validator.js +48 -0
- package/guards/auth-throttler.guard.d.ts +7 -0
- package/interceptors/set-token.interceptor.d.ts +1 -0
- package/interfaces/auth-email-provider.interface.d.ts +6 -0
- package/interfaces/auth-module-options.interface.d.ts +4 -1
- package/interfaces/authentication.interface.d.ts +14 -0
- package/interfaces/index.d.ts +2 -0
- package/interfaces/user-enricher.interface.d.ts +64 -0
- package/interfaces/user.interface.d.ts +1 -0
- package/modules/auth.module.d.ts +0 -1
- package/package.json +4 -4
- package/services/auth-config.service.d.ts +5 -1
- package/services/auth-email.service.d.ts +51 -0
- package/services/authentication.service.d.ts +21 -12
- package/services/branch.service.d.ts +2 -5
- package/services/company-selection-session.service.d.ts +1 -1
- package/services/company.service.d.ts +1 -4
- package/services/index.d.ts +1 -0
- package/services/user-permission.service.d.ts +0 -1
- package/services/user.service.d.ts +20 -6
- package/validators/password.validator.d.ts +10 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
function _define_property(obj, key, value) {
|
|
2
|
+
if (key in obj) {
|
|
3
|
+
Object.defineProperty(obj, key, {
|
|
4
|
+
value: value,
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true
|
|
8
|
+
});
|
|
9
|
+
} else {
|
|
10
|
+
obj[key] = value;
|
|
11
|
+
}
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
14
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
15
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
16
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
17
|
+
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;
|
|
18
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
19
|
+
}
|
|
20
|
+
function _ts_metadata(k, v) {
|
|
21
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
|
+
}
|
|
23
|
+
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
|
24
|
+
/**
|
|
25
|
+
* Token types for various user actions
|
|
26
|
+
*/ export var TokenType = /*#__PURE__*/ function(TokenType) {
|
|
27
|
+
TokenType["PASSWORD_RESET"] = "password_reset";
|
|
28
|
+
TokenType["EMAIL_VERIFICATION"] = "email_verification";
|
|
29
|
+
TokenType["EMAIL_CHANGE"] = "email_change";
|
|
30
|
+
return TokenType;
|
|
31
|
+
}({});
|
|
32
|
+
export class UserToken {
|
|
33
|
+
constructor(){
|
|
34
|
+
_define_property(this, "id", void 0);
|
|
35
|
+
_define_property(this, "userId", void 0);
|
|
36
|
+
_define_property(this, "type", void 0);
|
|
37
|
+
_define_property(this, "token", void 0);
|
|
38
|
+
_define_property(this, "expiresAt", void 0);
|
|
39
|
+
_define_property(this, "usedAt", void 0);
|
|
40
|
+
_define_property(this, "metadata", void 0);
|
|
41
|
+
_define_property(this, "createdAt", void 0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
_ts_decorate([
|
|
45
|
+
PrimaryGeneratedColumn('uuid'),
|
|
46
|
+
_ts_metadata("design:type", String)
|
|
47
|
+
], UserToken.prototype, "id", void 0);
|
|
48
|
+
_ts_decorate([
|
|
49
|
+
Column({
|
|
50
|
+
type: 'uuid',
|
|
51
|
+
name: 'user_id'
|
|
52
|
+
}),
|
|
53
|
+
_ts_metadata("design:type", String)
|
|
54
|
+
], UserToken.prototype, "userId", void 0);
|
|
55
|
+
_ts_decorate([
|
|
56
|
+
Column({
|
|
57
|
+
type: 'varchar',
|
|
58
|
+
length: 32
|
|
59
|
+
}),
|
|
60
|
+
_ts_metadata("design:type", String)
|
|
61
|
+
], UserToken.prototype, "type", void 0);
|
|
62
|
+
_ts_decorate([
|
|
63
|
+
Column({
|
|
64
|
+
type: 'varchar',
|
|
65
|
+
length: 64
|
|
66
|
+
}),
|
|
67
|
+
_ts_metadata("design:type", String)
|
|
68
|
+
], UserToken.prototype, "token", void 0);
|
|
69
|
+
_ts_decorate([
|
|
70
|
+
Column({
|
|
71
|
+
type: 'timestamp',
|
|
72
|
+
name: 'expires_at'
|
|
73
|
+
}),
|
|
74
|
+
_ts_metadata("design:type", typeof Date === "undefined" ? Object : Date)
|
|
75
|
+
], UserToken.prototype, "expiresAt", void 0);
|
|
76
|
+
_ts_decorate([
|
|
77
|
+
Column({
|
|
78
|
+
type: 'timestamp',
|
|
79
|
+
name: 'used_at',
|
|
80
|
+
nullable: true
|
|
81
|
+
}),
|
|
82
|
+
_ts_metadata("design:type", Object)
|
|
83
|
+
], UserToken.prototype, "usedAt", void 0);
|
|
84
|
+
_ts_decorate([
|
|
85
|
+
Column({
|
|
86
|
+
type: 'varchar',
|
|
87
|
+
length: 255,
|
|
88
|
+
nullable: true
|
|
89
|
+
}),
|
|
90
|
+
_ts_metadata("design:type", Object)
|
|
91
|
+
], UserToken.prototype, "metadata", void 0);
|
|
92
|
+
_ts_decorate([
|
|
93
|
+
CreateDateColumn({
|
|
94
|
+
type: 'timestamp',
|
|
95
|
+
name: 'created_at'
|
|
96
|
+
}),
|
|
97
|
+
_ts_metadata("design:type", typeof Date === "undefined" ? Object : Date)
|
|
98
|
+
], UserToken.prototype, "createdAt", void 0);
|
|
99
|
+
UserToken = _ts_decorate([
|
|
100
|
+
Entity('user_tokens'),
|
|
101
|
+
Index('IDX_user_token_token', [
|
|
102
|
+
'token'
|
|
103
|
+
], {
|
|
104
|
+
unique: true
|
|
105
|
+
}),
|
|
106
|
+
Index('IDX_user_token_user_type', [
|
|
107
|
+
'userId',
|
|
108
|
+
'type'
|
|
109
|
+
]),
|
|
110
|
+
Index('IDX_user_token_expires', [
|
|
111
|
+
'expiresAt'
|
|
112
|
+
])
|
|
113
|
+
], UserToken);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
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;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
import { Injectable } from '@nestjs/common';
|
|
8
|
+
import { ThrottlerException, ThrottlerGuard } from '@nestjs/throttler';
|
|
9
|
+
export class AuthThrottlerGuard extends ThrottlerGuard {
|
|
10
|
+
/**
|
|
11
|
+
* Generate tracker key combining IP and email for login attempts.
|
|
12
|
+
* This prevents attackers from brute-forcing multiple accounts from the same IP.
|
|
13
|
+
*/ async getTracker(req) {
|
|
14
|
+
const ip = req.ip || req.headers?.['x-forwarded-for'] || 'unknown';
|
|
15
|
+
const body = req.body;
|
|
16
|
+
const email = body?.email || '';
|
|
17
|
+
return `auth:${ip}:${email}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Custom error message for rate limit exceeded.
|
|
21
|
+
*/ throwThrottlingException() {
|
|
22
|
+
throw new ThrottlerException('Too many authentication attempts. Please try again later.');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Skip rate limiting for authenticated users on certain endpoints.
|
|
26
|
+
*/ async shouldSkip(context) {
|
|
27
|
+
const request = context.switchToHttp().getRequest();
|
|
28
|
+
const path = request.path || request.url || '';
|
|
29
|
+
// Remove query string for comparison
|
|
30
|
+
const pathOnly = path.split('?')[0];
|
|
31
|
+
// Skip rate limiting for authenticated endpoints (they have their own protection)
|
|
32
|
+
// Use exact match or prefix match to prevent bypass via path manipulation
|
|
33
|
+
const authenticatedEndpoints = [
|
|
34
|
+
'/auth/me',
|
|
35
|
+
'/auth/logout',
|
|
36
|
+
'/auth/change-password'
|
|
37
|
+
];
|
|
38
|
+
return authenticatedEndpoints.some((endpoint)=>pathOnly === endpoint || pathOnly.startsWith(`${endpoint}/`));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
AuthThrottlerGuard = _ts_decorate([
|
|
42
|
+
Injectable()
|
|
43
|
+
], AuthThrottlerGuard);
|
|
@@ -27,8 +27,8 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { Injectable, Optional } from '@nestjs/common';
|
|
29
29
|
import { map } from 'rxjs';
|
|
30
|
+
import { DEFAULT_COOKIE_NAME } from '../config';
|
|
30
31
|
import { AuthConfigService } from '../services/auth-config.service';
|
|
31
|
-
const DEFAULT_COOKIE_NAME = 'fsn_refresh_token';
|
|
32
32
|
export class ClearToken {
|
|
33
33
|
get cookieName() {
|
|
34
34
|
return this.authConfig?.getRefreshTokenCookieName() ?? DEFAULT_COOKIE_NAME;
|
|
@@ -37,14 +37,11 @@ export class ClearToken {
|
|
|
37
37
|
const req = context.switchToHttp().getRequest();
|
|
38
38
|
const res = context.switchToHttp().getResponse();
|
|
39
39
|
return next.handle().pipe(map((value)=>{
|
|
40
|
-
// Clear the refresh token cookie
|
|
41
40
|
this.clearRefreshTokenCookie(req, res);
|
|
42
41
|
return value;
|
|
43
42
|
}));
|
|
44
43
|
}
|
|
45
|
-
|
|
46
|
-
* Clear refresh token cookie
|
|
47
|
-
*/ clearRefreshTokenCookie(req, res) {
|
|
44
|
+
clearRefreshTokenCookie(req, res) {
|
|
48
45
|
const origin = req.headers.origin || '';
|
|
49
46
|
const hostname = req.hostname || '';
|
|
50
47
|
const isHttps = origin.startsWith('https://') || req.secure;
|
|
@@ -25,84 +25,90 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import { Injectable, Optional, PayloadTooLargeException } from '@nestjs/common';
|
|
28
|
+
import { Inject, Injectable, Optional, PayloadTooLargeException } from '@nestjs/common';
|
|
29
29
|
import { Buffer } from 'buffer';
|
|
30
30
|
import { catchError, map, throwError } from 'rxjs';
|
|
31
|
+
import { DEFAULT_COOKIE_NAME, MAX_COOKIE_BYTES } from '../config';
|
|
31
32
|
import { AuthConfigService } from '../services/auth-config.service';
|
|
32
|
-
const DEFAULT_COOKIE_NAME = 'fsn_refresh_token';
|
|
33
|
-
const MAX_COOKIE_BYTES = 3500;
|
|
34
33
|
export class SetToken {
|
|
35
34
|
get cookieName() {
|
|
36
35
|
return this.authConfig?.getRefreshTokenCookieName() ?? DEFAULT_COOKIE_NAME;
|
|
37
36
|
}
|
|
37
|
+
/** Convert expiration string (e.g., '7d', '24h') to milliseconds */ getMaxAgeMs() {
|
|
38
|
+
const expiry = this.authConfig?.getRefreshTokenExpiration() ?? '7d';
|
|
39
|
+
const match = expiry.match(/^(\d+)(s|m|h|d|w)$/);
|
|
40
|
+
if (!match) {
|
|
41
|
+
return 7 * 24 * 60 * 60 * 1000; // Default 7 days
|
|
42
|
+
}
|
|
43
|
+
const value = parseInt(match[1], 10);
|
|
44
|
+
const unit = match[2];
|
|
45
|
+
const multipliers = {
|
|
46
|
+
s: 1000,
|
|
47
|
+
m: 60 * 1000,
|
|
48
|
+
h: 60 * 60 * 1000,
|
|
49
|
+
d: 24 * 60 * 60 * 1000,
|
|
50
|
+
w: 7 * 24 * 60 * 60 * 1000
|
|
51
|
+
};
|
|
52
|
+
return value * (multipliers[unit] || multipliers['d']);
|
|
53
|
+
}
|
|
38
54
|
intercept(context, next) {
|
|
39
55
|
const req = context.switchToHttp().getRequest();
|
|
40
56
|
const res = context.switchToHttp().getResponse();
|
|
41
57
|
return next.handle().pipe(map((value)=>{
|
|
42
|
-
// Skip if no refreshToken in response
|
|
43
58
|
if (!value || !value.refreshToken) {
|
|
44
59
|
return value;
|
|
45
60
|
}
|
|
46
61
|
const isBrowser = this.isBrowserRequest(req);
|
|
47
62
|
const tokenValue = value.refreshToken;
|
|
48
63
|
if (isBrowser) {
|
|
49
|
-
// Browser: Set cookie and remove from response body
|
|
50
64
|
this.setRefreshTokenCookie(req, res, tokenValue);
|
|
51
|
-
// Return response without refreshToken for browsers
|
|
52
65
|
const { refreshToken, ...responseWithoutToken } = value;
|
|
53
66
|
return responseWithoutToken;
|
|
54
67
|
}
|
|
55
|
-
// API/Mobile: Keep refreshToken in response body
|
|
56
68
|
return value;
|
|
57
69
|
}), catchError((err)=>throwError(()=>err)));
|
|
58
70
|
}
|
|
59
|
-
|
|
60
|
-
* Detect if request is from a browser
|
|
61
|
-
* Checks Accept header and custom X-Client-Type header
|
|
62
|
-
*/ isBrowserRequest(req) {
|
|
63
|
-
// Check custom header first (allows explicit client identification)
|
|
71
|
+
isBrowserRequest(req) {
|
|
64
72
|
const clientType = req.headers['x-client-type'];
|
|
65
73
|
if (clientType) {
|
|
66
74
|
return clientType.toLowerCase() === 'browser' || clientType.toLowerCase() === 'web';
|
|
67
75
|
}
|
|
68
|
-
// Check Accept header - browsers typically include text/html
|
|
69
76
|
const accept = req.headers['accept'] || '';
|
|
70
77
|
if (accept.includes('text/html')) {
|
|
71
78
|
return true;
|
|
72
79
|
}
|
|
73
|
-
// Check User-Agent for common browsers
|
|
74
80
|
const userAgent = req.headers['user-agent'] || '';
|
|
75
81
|
const browserPatterns = /mozilla|chrome|safari|firefox|edge|opera|msie/i;
|
|
76
|
-
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
return false;
|
|
82
|
+
return browserPatterns.test(userAgent) && !userAgent.includes('Postman');
|
|
80
83
|
}
|
|
81
|
-
|
|
82
|
-
* Set refresh token in HTTP-only cookie
|
|
83
|
-
*/ setRefreshTokenCookie(req, res, tokenValue) {
|
|
84
|
-
const origin = req.headers.origin || '';
|
|
84
|
+
setRefreshTokenCookie(req, res, tokenValue) {
|
|
85
85
|
const hostname = req.hostname || '';
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
// Determine if we're in production - always use secure cookies in production
|
|
87
|
+
// This handles TLS-terminating proxies correctly (they set x-forwarded-proto)
|
|
88
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
89
|
+
const forwardedProto = req.headers['x-forwarded-proto'];
|
|
90
|
+
const isHttps = isProduction || forwardedProto === 'https' || req.secure;
|
|
91
|
+
// Set cookie domain - only for subdomains with 3+ parts
|
|
92
|
+
// For security, limit to same domain unless explicitly configured
|
|
93
|
+
let cookieDomain;
|
|
88
94
|
const domainParts = hostname.split('.');
|
|
89
|
-
if (domainParts.length > 2) {
|
|
95
|
+
if (domainParts.length > 2 && !hostname.includes('localhost')) {
|
|
90
96
|
cookieDomain = '.' + domainParts.slice(-2).join('.');
|
|
91
97
|
}
|
|
92
|
-
|
|
93
|
-
const rawCookieString = `${this.cookieName}=${tokenValue}; HttpOnly; Domain=${cookieDomain}`;
|
|
98
|
+
const rawCookieString = `${this.cookieName}=${tokenValue}; HttpOnly`;
|
|
94
99
|
const estimatedBytes = Buffer.byteLength(rawCookieString, 'utf8');
|
|
95
100
|
if (estimatedBytes > MAX_COOKIE_BYTES) {
|
|
96
101
|
throw new PayloadTooLargeException(`Refresh token too large (≈${estimatedBytes} bytes)`);
|
|
97
102
|
}
|
|
98
|
-
// Set HTTP-only secure cookie
|
|
99
103
|
res.cookie(this.cookieName, tokenValue, {
|
|
100
104
|
httpOnly: true,
|
|
101
105
|
secure: isHttps,
|
|
102
106
|
sameSite: isHttps ? 'strict' : 'lax',
|
|
103
|
-
|
|
107
|
+
...cookieDomain && {
|
|
108
|
+
domain: cookieDomain
|
|
109
|
+
},
|
|
104
110
|
path: '/',
|
|
105
|
-
maxAge:
|
|
111
|
+
maxAge: this.getMaxAgeMs()
|
|
106
112
|
});
|
|
107
113
|
}
|
|
108
114
|
constructor(authConfig){
|
|
@@ -113,6 +119,7 @@ export class SetToken {
|
|
|
113
119
|
SetToken = _ts_decorate([
|
|
114
120
|
Injectable(),
|
|
115
121
|
_ts_param(0, Optional()),
|
|
122
|
+
_ts_param(0, Inject(AuthConfigService)),
|
|
116
123
|
_ts_metadata("design:type", Function),
|
|
117
124
|
_ts_metadata("design:paramtypes", [
|
|
118
125
|
typeof AuthConfigService === "undefined" ? Object : AuthConfigService
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Email Provider Interface
|
|
3
|
+
*
|
|
4
|
+
* This interface defines the contract for email sending in the auth module.
|
|
5
|
+
* It allows auth to work independently of the email module.
|
|
6
|
+
*
|
|
7
|
+
* To use email features in auth:
|
|
8
|
+
* 1. Provide AUTH_EMAIL_PROVIDER in your app module
|
|
9
|
+
* 2. Implement IAuthEmailProvider using EmailSendService from nestjs-email
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // In app.module.ts or a provider file
|
|
14
|
+
* {
|
|
15
|
+
* provide: AUTH_EMAIL_PROVIDER,
|
|
16
|
+
* useFactory: (emailSendService: EmailSendService) => ({
|
|
17
|
+
* sendPasswordResetEmail: async (email, token, resetUrl) => {
|
|
18
|
+
* await emailSendService.sendTemplateEmail({
|
|
19
|
+
* templateSlug: 'password-reset',
|
|
20
|
+
* to: email,
|
|
21
|
+
* variables: { resetUrl, token },
|
|
22
|
+
* });
|
|
23
|
+
* },
|
|
24
|
+
* sendVerificationEmail: async (email, token, verifyUrl) => {
|
|
25
|
+
* await emailSendService.sendTemplateEmail({
|
|
26
|
+
* templateSlug: 'email-verification',
|
|
27
|
+
* to: email,
|
|
28
|
+
* variables: { verifyUrl, token },
|
|
29
|
+
* });
|
|
30
|
+
* },
|
|
31
|
+
* }),
|
|
32
|
+
* inject: [EmailSendService],
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/ /**
|
|
36
|
+
* Injection token for auth email provider
|
|
37
|
+
* Use this to provide the email implementation
|
|
38
|
+
*/ export const AUTH_EMAIL_PROVIDER = 'AUTH_EMAIL_PROVIDER';
|
|
@@ -1,7 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// TOKEN INTERFACES
|
|
3
|
-
// =============================================================================
|
|
4
|
-
/**
|
|
5
|
-
* Login Response when company/branch selection is required
|
|
6
|
-
* Frontend shows selection UI and calls /select endpoint with chosen company + branch
|
|
7
|
-
*/ export { };
|
|
1
|
+
/** Login Response when email verification is required */ export { };
|
package/fesm/interfaces/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export * from './authentication.interface';
|
|
2
|
+
export * from './auth-email-provider.interface';
|
|
2
3
|
export * from './company-branch.interface';
|
|
3
4
|
export * from './company.interface';
|
|
4
5
|
export * from './user.interface';
|
|
6
|
+
export * from './user-enricher.interface';
|
|
5
7
|
export * from './auth-module-options.interface';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Enricher Provider Interface
|
|
3
|
+
*
|
|
4
|
+
* Allows consuming applications to extend user list and profile
|
|
5
|
+
* without modifying the base nestjs-auth package.
|
|
6
|
+
*
|
|
7
|
+
* This follows the Provider Interface Pattern:
|
|
8
|
+
* - nestjs-auth defines the interface (this file)
|
|
9
|
+
* - Consuming apps implement the interface
|
|
10
|
+
* - nestjs-auth optionally injects the implementation
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // In your app module
|
|
14
|
+
* providers: [
|
|
15
|
+
* { provide: USER_ENRICHER, useClass: MyUserEnricher },
|
|
16
|
+
* ]
|
|
17
|
+
*
|
|
18
|
+
* // Your enricher implementation
|
|
19
|
+
* @Injectable()
|
|
20
|
+
* export class MyUserEnricher implements IUserEnricher {
|
|
21
|
+
* async enrichListQuery(query, user) {
|
|
22
|
+
* query.leftJoinAndSelect('user.department', 'department');
|
|
23
|
+
* return { extraFields: ['department.name'] };
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
*/ export { };
|
|
@@ -5,10 +5,9 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
}
|
|
7
7
|
import { CacheModule, UtilsModule } from '@flusys/nestjs-shared/modules';
|
|
8
|
-
import { Module
|
|
8
|
+
import { Module } from '@nestjs/common';
|
|
9
9
|
import { JwtModule } from '@nestjs/jwt';
|
|
10
10
|
import { PassportModule } from '@nestjs/passport';
|
|
11
|
-
import { getRepositoryToken } from '@nestjs/typeorm';
|
|
12
11
|
import { AuthConfigService } from '../services/auth-config.service';
|
|
13
12
|
import { AuthDataSourceProvider } from '../services/auth-datasource.provider';
|
|
14
13
|
import { AUTH_MODULE_OPTIONS } from '../config/auth.constants';
|
|
@@ -17,14 +16,14 @@ import { Company, CompanyBranch, User, UserCompanyPermission } from '../entities
|
|
|
17
16
|
import { JwtAuthGuard } from '@flusys/nestjs-shared/guards';
|
|
18
17
|
import { ClearToken } from '../interceptors/clear-token.interceptor';
|
|
19
18
|
import { SetToken } from '../interceptors/set-token.interceptor';
|
|
20
|
-
import { AuthenticationService, BranchService, CompanySelectionSessionService, CompanyService, UserPermissionService, UserService } from '../services';
|
|
19
|
+
import { AuthEmailService, AuthenticationService, BranchService, CompanySelectionSessionService, CompanyService, UserPermissionService, UserService } from '../services';
|
|
21
20
|
import { JwtStrategy } from '../strategies/jwt.strategy';
|
|
22
21
|
export class AuthModule {
|
|
23
22
|
static forRoot(options) {
|
|
24
23
|
return this.buildDynamicModule(options);
|
|
25
24
|
}
|
|
26
25
|
static forRootAsync(asyncOptions) {
|
|
27
|
-
const { global = false, includeController = true, bootstrapAppConfig: { enableCompanyFeature } = {}, imports: externalImports = [] } = asyncOptions;
|
|
26
|
+
const { global = false, includeController = true, bootstrapAppConfig: { enableCompanyFeature } = {}, imports: externalImports = [], providers: additionalProviders = [] } = asyncOptions;
|
|
28
27
|
const asyncProviders = this.createAsyncProviders(asyncOptions);
|
|
29
28
|
// Build imports
|
|
30
29
|
const imports = [
|
|
@@ -51,6 +50,7 @@ export class AuthModule {
|
|
|
51
50
|
// Build providers
|
|
52
51
|
const providers = [
|
|
53
52
|
...asyncProviders,
|
|
53
|
+
...additionalProviders,
|
|
54
54
|
// Config & Infrastructure
|
|
55
55
|
AuthConfigService,
|
|
56
56
|
AuthDataSourceProvider,
|
|
@@ -58,6 +58,7 @@ export class AuthModule {
|
|
|
58
58
|
JwtStrategy,
|
|
59
59
|
JwtAuthGuard,
|
|
60
60
|
AuthenticationService,
|
|
61
|
+
AuthEmailService,
|
|
61
62
|
// Interceptors
|
|
62
63
|
SetToken,
|
|
63
64
|
ClearToken,
|
|
@@ -77,6 +78,7 @@ export class AuthModule {
|
|
|
77
78
|
JwtStrategy,
|
|
78
79
|
JwtAuthGuard,
|
|
79
80
|
AuthenticationService,
|
|
81
|
+
AuthEmailService,
|
|
80
82
|
UserService
|
|
81
83
|
];
|
|
82
84
|
if (enableCompanyFeature) {
|
|
@@ -92,7 +94,7 @@ export class AuthModule {
|
|
|
92
94
|
};
|
|
93
95
|
}
|
|
94
96
|
static buildDynamicModule(options) {
|
|
95
|
-
const { bootstrapAppConfig: { enableCompanyFeature } = {}, includeController = true, global = false } = options;
|
|
97
|
+
const { bootstrapAppConfig: { enableCompanyFeature } = {}, includeController = true, global = false, providers: additionalProviders = [] } = options;
|
|
96
98
|
const imports = [
|
|
97
99
|
PassportModule.register({
|
|
98
100
|
defaultStrategy: 'jwt'
|
|
@@ -118,6 +120,7 @@ export class AuthModule {
|
|
|
118
120
|
provide: AUTH_MODULE_OPTIONS,
|
|
119
121
|
useValue: options
|
|
120
122
|
},
|
|
123
|
+
...additionalProviders,
|
|
121
124
|
// Config & Infrastructure
|
|
122
125
|
AuthConfigService,
|
|
123
126
|
AuthDataSourceProvider,
|
|
@@ -125,6 +128,7 @@ export class AuthModule {
|
|
|
125
128
|
JwtStrategy,
|
|
126
129
|
JwtAuthGuard,
|
|
127
130
|
AuthenticationService,
|
|
131
|
+
AuthEmailService,
|
|
128
132
|
// Interceptors
|
|
129
133
|
SetToken,
|
|
130
134
|
ClearToken,
|
|
@@ -143,6 +147,7 @@ export class AuthModule {
|
|
|
143
147
|
JwtStrategy,
|
|
144
148
|
JwtAuthGuard,
|
|
145
149
|
AuthenticationService,
|
|
150
|
+
AuthEmailService,
|
|
146
151
|
UserService
|
|
147
152
|
];
|
|
148
153
|
if (enableCompanyFeature) {
|
|
@@ -223,23 +228,6 @@ export class AuthModule {
|
|
|
223
228
|
}
|
|
224
229
|
];
|
|
225
230
|
}
|
|
226
|
-
/**
|
|
227
|
-
* Create repository providers that use AuthDataSourceProvider
|
|
228
|
-
* This replaces TypeOrmModule.forFeature() functionality
|
|
229
|
-
* Note: Must be REQUEST-scoped since AuthDataSourceProvider is REQUEST-scoped
|
|
230
|
-
*/ static getRepositoryProviders(enableCompanyFeature) {
|
|
231
|
-
const entities = this.getEntities(enableCompanyFeature);
|
|
232
|
-
return entities.map((entity)=>({
|
|
233
|
-
provide: getRepositoryToken(entity),
|
|
234
|
-
scope: Scope.REQUEST,
|
|
235
|
-
useFactory: async (dataSourceProvider)=>{
|
|
236
|
-
return await dataSourceProvider.getRepository(entity);
|
|
237
|
-
},
|
|
238
|
-
inject: [
|
|
239
|
-
AuthDataSourceProvider
|
|
240
|
-
]
|
|
241
|
-
}));
|
|
242
|
-
}
|
|
243
231
|
static getEntities(enableCompanyFeature = false) {
|
|
244
232
|
const coreEntities = [
|
|
245
233
|
User
|
|
@@ -25,87 +25,82 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import { Inject, Injectable, Optional } from '@nestjs/common';
|
|
28
|
+
import { Inject, Injectable, InternalServerErrorException, Logger, Optional } from '@nestjs/common';
|
|
29
29
|
import { AuthModuleOptions } from '../interfaces/auth-module-options.interface';
|
|
30
30
|
import { AUTH_MODULE_OPTIONS } from '../config/auth.constants';
|
|
31
31
|
export class AuthConfigService {
|
|
32
|
-
//
|
|
33
|
-
/**
|
|
34
|
-
* Get database mode (single or multi-tenant)
|
|
35
|
-
*/ getDatabaseMode() {
|
|
32
|
+
// Database Mode
|
|
33
|
+
/** Get database mode (single or multi-tenant) */ getDatabaseMode() {
|
|
36
34
|
return this.options.bootstrapAppConfig?.databaseMode ?? 'single';
|
|
37
35
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Check if running in multi-tenant mode
|
|
40
|
-
*/ isMultiTenant() {
|
|
36
|
+
/** Check if running in multi-tenant mode */ isMultiTenant() {
|
|
41
37
|
return this.getDatabaseMode() === 'multi-tenant';
|
|
42
38
|
}
|
|
43
|
-
//
|
|
44
|
-
/**
|
|
45
|
-
* Check if company feature is enabled
|
|
46
|
-
*/ isCompanyFeatureEnabled() {
|
|
39
|
+
// Feature Flags
|
|
40
|
+
/** Check if company feature is enabled */ isCompanyFeatureEnabled() {
|
|
47
41
|
return this.options.bootstrapAppConfig?.enableCompanyFeature ?? false;
|
|
48
42
|
}
|
|
49
|
-
//
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
43
|
+
// JWT Configuration
|
|
44
|
+
/** Get JWT secret key */ getJwtSecret() {
|
|
45
|
+
if (!this.config.jwtSecret) {
|
|
46
|
+
throw new InternalServerErrorException('[AuthModule] JWT_SECRET is required - set via environment or module config');
|
|
47
|
+
}
|
|
48
|
+
return this.config.jwtSecret;
|
|
54
49
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Get JWT expiration time
|
|
57
|
-
*/ getJwtExpiration() {
|
|
50
|
+
/** Get JWT expiration time */ getJwtExpiration() {
|
|
58
51
|
return this.config.jwtExpiration ?? '1h';
|
|
59
52
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
/** Get refresh token secret key */ getRefreshTokenSecret() {
|
|
54
|
+
if (this.config.refreshTokenSecret) {
|
|
55
|
+
return this.config.refreshTokenSecret;
|
|
56
|
+
}
|
|
57
|
+
// Production: require separate refresh token secret for security
|
|
58
|
+
if (process.env.NODE_ENV === 'production') {
|
|
59
|
+
throw new InternalServerErrorException('[AuthModule] REFRESH_TOKEN_SECRET is required in production - ' + 'using the same secret for access and refresh tokens is a security risk');
|
|
60
|
+
}
|
|
61
|
+
// Development: warn once about fallback to JWT secret
|
|
62
|
+
if (!this.refreshTokenSecretWarned) {
|
|
63
|
+
this.refreshTokenSecretWarned = true;
|
|
64
|
+
this.logger.warn('REFRESH_TOKEN_SECRET not configured - using JWT_SECRET as fallback. ' + 'For better security, configure a separate refresh token secret.');
|
|
65
|
+
}
|
|
66
|
+
return this.config.jwtSecret ?? this.getJwtSecret();
|
|
64
67
|
}
|
|
65
|
-
/**
|
|
66
|
-
* Get refresh token expiration time
|
|
67
|
-
*/ getRefreshTokenExpiration() {
|
|
68
|
+
/** Get refresh token expiration time */ getRefreshTokenExpiration() {
|
|
68
69
|
return this.config.refreshTokenExpiration ?? '7d';
|
|
69
70
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Get refresh token cookie name
|
|
72
|
-
*/ getRefreshTokenCookieName() {
|
|
71
|
+
/** Get refresh token cookie name */ getRefreshTokenCookieName() {
|
|
73
72
|
return this.config.refreshTokenCookieName ?? 'fsn_refresh_token';
|
|
74
73
|
}
|
|
75
|
-
//
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
// Frontend Configuration
|
|
75
|
+
/** Get frontend URL for email links */ getFrontendUrl() {
|
|
76
|
+
return this.config.frontendUrl;
|
|
77
|
+
}
|
|
78
|
+
// Database Configuration
|
|
79
|
+
/** Get default database config */ getDefaultDatabaseConfig() {
|
|
79
80
|
return this.config.defaultDatabaseConfig ?? this.config.tenantDefaultDatabaseConfig;
|
|
80
81
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Get tenant default database config (for multi-tenant)
|
|
83
|
-
*/ getTenantDefaultDatabaseConfig() {
|
|
82
|
+
/** Get tenant default database config (for multi-tenant) */ getTenantDefaultDatabaseConfig() {
|
|
84
83
|
return this.config.tenantDefaultDatabaseConfig;
|
|
85
84
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Get registered tenants
|
|
88
|
-
*/ getTenants() {
|
|
85
|
+
/** Get registered tenants */ getTenants() {
|
|
89
86
|
return this.config.tenants ?? [];
|
|
90
87
|
}
|
|
91
|
-
//
|
|
92
|
-
/**
|
|
93
|
-
* Get full module options
|
|
94
|
-
*/ getOptions() {
|
|
88
|
+
// Raw Options
|
|
89
|
+
/** Get full module options */ getOptions() {
|
|
95
90
|
return {
|
|
96
91
|
...this.options
|
|
97
92
|
};
|
|
98
93
|
}
|
|
99
|
-
/**
|
|
100
|
-
* Get runtime config
|
|
101
|
-
*/ getConfig() {
|
|
94
|
+
/** Get runtime config */ getConfig() {
|
|
102
95
|
return {
|
|
103
96
|
...this.config
|
|
104
97
|
};
|
|
105
98
|
}
|
|
106
99
|
constructor(injectedOptions){
|
|
100
|
+
_define_property(this, "logger", new Logger(AuthConfigService.name));
|
|
107
101
|
_define_property(this, "options", void 0);
|
|
108
102
|
_define_property(this, "config", void 0);
|
|
103
|
+
_define_property(this, "refreshTokenSecretWarned", false);
|
|
109
104
|
this.options = injectedOptions ?? {
|
|
110
105
|
global: false,
|
|
111
106
|
includeController: true
|