@flusys/nestjs-auth 1.0.0-beta → 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 -291
- package/cjs/config/auth.constants.js +12 -0
- package/cjs/controllers/authentication.controller.js +219 -10
- package/cjs/controllers/branch.controller.js +44 -1
- package/cjs/controllers/company-selection.controller.js +27 -20
- package/cjs/controllers/company.controller.js +44 -1
- package/cjs/controllers/user-permission.controller.js +40 -25
- package/cjs/controllers/user.controller.js +211 -7
- 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/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 +23 -2
- package/cjs/services/auth-datasource.provider.js +5 -3
- package/cjs/services/auth-email.service.js +471 -0
- package/cjs/services/authentication.service.js +169 -77
- package/cjs/services/branch.service.js +6 -5
- package/cjs/services/company-selection-session.service.js +1 -1
- package/cjs/services/index.js +1 -0
- package/cjs/services/user-permission.service.js +0 -2
- package/cjs/services/user.service.js +213 -34
- package/cjs/validators/password.validator.js +45 -0
- package/config/auth.constants.d.ts +3 -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 +6 -2
- package/fesm/controllers/authentication.controller.js +221 -12
- package/fesm/controllers/branch.controller.js +45 -2
- package/fesm/controllers/company-selection.controller.js +28 -21
- package/fesm/controllers/company.controller.js +45 -2
- package/fesm/controllers/user-permission.controller.js +41 -26
- package/fesm/controllers/user.controller.js +214 -10
- package/fesm/dtos/authentication.dto.js +106 -14
- 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 -1
- 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 +24 -3
- package/fesm/services/auth-datasource.provider.js +6 -4
- package/fesm/services/auth-email.service.js +420 -0
- package/fesm/services/authentication.service.js +169 -77
- package/fesm/services/branch.service.js +6 -5
- package/fesm/services/company-selection-session.service.js +1 -1
- package/fesm/services/index.js +1 -0
- package/fesm/services/user-permission.service.js +1 -3
- package/fesm/services/user.service.js +215 -36
- 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 +16 -11
- package/services/company-selection-session.service.d.ts +1 -1
- package/services/index.d.ts +1 -0
- package/services/user-permission.service.d.ts +0 -1
- package/services/user.service.d.ts +19 -3
- package/validators/password.validator.d.ts +10 -0
package/fesm/dtos/user.dto.js
CHANGED
|
@@ -21,7 +21,8 @@ function _ts_metadata(k, v) {
|
|
|
21
21
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
22
|
}
|
|
23
23
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
24
|
-
import { IsBoolean, IsEmail, IsObject, IsOptional, IsString } from 'class-validator';
|
|
24
|
+
import { IsBoolean, IsEmail, IsNotEmpty, IsObject, IsOptional, IsString, IsUUID, MaxLength } from 'class-validator';
|
|
25
|
+
import { IsStrongPassword } from '../validators/password.validator';
|
|
25
26
|
export class CreateUserDto {
|
|
26
27
|
constructor(){
|
|
27
28
|
_define_property(this, "name", void 0);
|
|
@@ -62,10 +63,13 @@ _ts_decorate([
|
|
|
62
63
|
], CreateUserDto.prototype, "email", void 0);
|
|
63
64
|
_ts_decorate([
|
|
64
65
|
ApiProperty({
|
|
65
|
-
example: '
|
|
66
|
-
description: 'Password'
|
|
66
|
+
example: 'SecureP@ss123',
|
|
67
|
+
description: 'Password (must meet complexity requirements)'
|
|
67
68
|
}),
|
|
68
69
|
IsString(),
|
|
70
|
+
IsNotEmpty(),
|
|
71
|
+
IsStrongPassword(),
|
|
72
|
+
MaxLength(128),
|
|
69
73
|
_ts_metadata("design:type", String)
|
|
70
74
|
], CreateUserDto.prototype, "password", void 0);
|
|
71
75
|
_ts_decorate([
|
|
@@ -118,6 +122,7 @@ export class UpdateUserDto {
|
|
|
118
122
|
_define_property(this, "phone", void 0);
|
|
119
123
|
_define_property(this, "email", void 0);
|
|
120
124
|
_define_property(this, "password", void 0);
|
|
125
|
+
_define_property(this, "oldPassword", void 0);
|
|
121
126
|
_define_property(this, "profilePictureId", void 0);
|
|
122
127
|
_define_property(this, "isActive", void 0);
|
|
123
128
|
_define_property(this, "emailVerified", void 0);
|
|
@@ -129,7 +134,7 @@ _ts_decorate([
|
|
|
129
134
|
ApiProperty({
|
|
130
135
|
description: 'User ID'
|
|
131
136
|
}),
|
|
132
|
-
|
|
137
|
+
IsUUID(),
|
|
133
138
|
_ts_metadata("design:type", String)
|
|
134
139
|
], UpdateUserDto.prototype, "id", void 0);
|
|
135
140
|
_ts_decorate([
|
|
@@ -161,12 +166,22 @@ _ts_decorate([
|
|
|
161
166
|
], UpdateUserDto.prototype, "email", void 0);
|
|
162
167
|
_ts_decorate([
|
|
163
168
|
ApiPropertyOptional({
|
|
164
|
-
description: 'New password'
|
|
169
|
+
description: 'New password (must meet complexity requirements)'
|
|
165
170
|
}),
|
|
166
171
|
IsOptional(),
|
|
167
172
|
IsString(),
|
|
173
|
+
IsStrongPassword(),
|
|
174
|
+
MaxLength(128),
|
|
168
175
|
_ts_metadata("design:type", String)
|
|
169
176
|
], UpdateUserDto.prototype, "password", void 0);
|
|
177
|
+
_ts_decorate([
|
|
178
|
+
ApiPropertyOptional({
|
|
179
|
+
description: 'Current password (required when changing password on own profile)'
|
|
180
|
+
}),
|
|
181
|
+
IsOptional(),
|
|
182
|
+
IsString(),
|
|
183
|
+
_ts_metadata("design:type", String)
|
|
184
|
+
], UpdateUserDto.prototype, "oldPassword", void 0);
|
|
170
185
|
_ts_decorate([
|
|
171
186
|
ApiPropertyOptional({
|
|
172
187
|
description: 'Profile picture file ID'
|
package/fesm/entities/index.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
export * from './company-branch.entity';
|
|
2
2
|
export * from './company.entity';
|
|
3
|
+
export * from './pending-registration.entity';
|
|
3
4
|
export { PermissionType, UserCompanyPermission } from './user-company-permission.entity';
|
|
4
5
|
export * from './user.entity';
|
|
6
|
+
export * from './user-token.entity';
|
|
5
7
|
import { CompanyBranch } from './company-branch.entity';
|
|
6
8
|
import { Company } from './company.entity';
|
|
9
|
+
import { PendingRegistration } from './pending-registration.entity';
|
|
7
10
|
import { UserCompanyPermission } from './user-company-permission.entity';
|
|
8
11
|
import { User } from './user.entity';
|
|
12
|
+
import { UserToken } from './user-token.entity';
|
|
9
13
|
export const CoreEntities = [
|
|
10
|
-
User
|
|
14
|
+
User,
|
|
15
|
+
UserToken,
|
|
16
|
+
PendingRegistration
|
|
11
17
|
];
|
|
12
18
|
export const CompanyEntities = [
|
|
13
19
|
Company,
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
export class PendingRegistration {
|
|
25
|
+
constructor(){
|
|
26
|
+
_define_property(this, "id", void 0);
|
|
27
|
+
_define_property(this, "email", void 0);
|
|
28
|
+
_define_property(this, "name", void 0);
|
|
29
|
+
_define_property(this, "phone", void 0);
|
|
30
|
+
_define_property(this, "passwordHash", void 0);
|
|
31
|
+
_define_property(this, "token", void 0);
|
|
32
|
+
_define_property(this, "expiresAt", void 0);
|
|
33
|
+
_define_property(this, "companyData", void 0);
|
|
34
|
+
_define_property(this, "createdAt", void 0);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
_ts_decorate([
|
|
38
|
+
PrimaryGeneratedColumn('uuid'),
|
|
39
|
+
_ts_metadata("design:type", String)
|
|
40
|
+
], PendingRegistration.prototype, "id", void 0);
|
|
41
|
+
_ts_decorate([
|
|
42
|
+
Column({
|
|
43
|
+
type: 'varchar',
|
|
44
|
+
length: 255
|
|
45
|
+
}),
|
|
46
|
+
_ts_metadata("design:type", String)
|
|
47
|
+
], PendingRegistration.prototype, "email", void 0);
|
|
48
|
+
_ts_decorate([
|
|
49
|
+
Column({
|
|
50
|
+
type: 'varchar',
|
|
51
|
+
length: 255,
|
|
52
|
+
nullable: true
|
|
53
|
+
}),
|
|
54
|
+
_ts_metadata("design:type", Object)
|
|
55
|
+
], PendingRegistration.prototype, "name", void 0);
|
|
56
|
+
_ts_decorate([
|
|
57
|
+
Column({
|
|
58
|
+
type: 'varchar',
|
|
59
|
+
length: 255,
|
|
60
|
+
nullable: true
|
|
61
|
+
}),
|
|
62
|
+
_ts_metadata("design:type", Object)
|
|
63
|
+
], PendingRegistration.prototype, "phone", void 0);
|
|
64
|
+
_ts_decorate([
|
|
65
|
+
Column({
|
|
66
|
+
type: 'text',
|
|
67
|
+
name: 'passwordHash'
|
|
68
|
+
}),
|
|
69
|
+
_ts_metadata("design:type", String)
|
|
70
|
+
], PendingRegistration.prototype, "passwordHash", void 0);
|
|
71
|
+
_ts_decorate([
|
|
72
|
+
Column({
|
|
73
|
+
type: 'varchar',
|
|
74
|
+
length: 64
|
|
75
|
+
}),
|
|
76
|
+
_ts_metadata("design:type", String)
|
|
77
|
+
], PendingRegistration.prototype, "token", void 0);
|
|
78
|
+
_ts_decorate([
|
|
79
|
+
Column({
|
|
80
|
+
type: 'timestamp',
|
|
81
|
+
name: 'expires_at'
|
|
82
|
+
}),
|
|
83
|
+
_ts_metadata("design:type", typeof Date === "undefined" ? Object : Date)
|
|
84
|
+
], PendingRegistration.prototype, "expiresAt", void 0);
|
|
85
|
+
_ts_decorate([
|
|
86
|
+
Column({
|
|
87
|
+
type: 'json',
|
|
88
|
+
nullable: true,
|
|
89
|
+
name: 'company_data'
|
|
90
|
+
}),
|
|
91
|
+
_ts_metadata("design:type", Object)
|
|
92
|
+
], PendingRegistration.prototype, "companyData", void 0);
|
|
93
|
+
_ts_decorate([
|
|
94
|
+
CreateDateColumn({
|
|
95
|
+
type: 'timestamp',
|
|
96
|
+
name: 'created_at'
|
|
97
|
+
}),
|
|
98
|
+
_ts_metadata("design:type", typeof Date === "undefined" ? Object : Date)
|
|
99
|
+
], PendingRegistration.prototype, "createdAt", void 0);
|
|
100
|
+
PendingRegistration = _ts_decorate([
|
|
101
|
+
Entity('pending_registrations'),
|
|
102
|
+
Index('IDX_pending_reg_email', [
|
|
103
|
+
'email'
|
|
104
|
+
], {
|
|
105
|
+
unique: true
|
|
106
|
+
}),
|
|
107
|
+
Index('IDX_pending_reg_token', [
|
|
108
|
+
'token'
|
|
109
|
+
], {
|
|
110
|
+
unique: true
|
|
111
|
+
}),
|
|
112
|
+
Index('IDX_pending_reg_expires', [
|
|
113
|
+
'expiresAt'
|
|
114
|
+
])
|
|
115
|
+
], PendingRegistration);
|
|
@@ -27,12 +27,6 @@ import { Company } from './company.entity';
|
|
|
27
27
|
import { User } from './user.entity';
|
|
28
28
|
export { PermissionType };
|
|
29
29
|
export class UserCompanyPermission {
|
|
30
|
-
/* ---------- Helpers ---------- */ isCompanyPermission() {
|
|
31
|
-
return this.permissionType === PermissionType.COMPANY;
|
|
32
|
-
}
|
|
33
|
-
isBranchPermission() {
|
|
34
|
-
return this.permissionType === PermissionType.BRANCH;
|
|
35
|
-
}
|
|
36
30
|
constructor(){
|
|
37
31
|
_define_property(this, "id", void 0);
|
|
38
32
|
/* ---------- Ownership ---------- */ _define_property(this, "userId", void 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 +1 @@
|
|
|
1
|
-
/** Login Response when
|
|
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 { };
|