@saulo.martins/backend-auth 1.0.2
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/dist/auth.controller.d.ts +43 -0
- package/dist/auth.controller.js +115 -0
- package/dist/auth.module.d.ts +2 -0
- package/dist/auth.module.js +45 -0
- package/dist/auth.service.d.ts +51 -0
- package/dist/auth.service.js +243 -0
- package/dist/auth.service.spec.d.ts +1 -0
- package/dist/auth.service.spec.js +105 -0
- package/dist/contracts.d.ts +59 -0
- package/dist/contracts.js +2 -0
- package/dist/dto/change-password.dto.d.ts +6 -0
- package/dist/dto/change-password.dto.js +38 -0
- package/dist/dto/forgot-password.dto.d.ts +3 -0
- package/dist/dto/forgot-password.dto.js +20 -0
- package/dist/dto/login.dto.d.ts +5 -0
- package/dist/dto/login.dto.js +31 -0
- package/dist/dto/reset-password.dto.d.ts +5 -0
- package/dist/dto/reset-password.dto.js +31 -0
- package/dist/dto/signup.dto.d.ts +11 -0
- package/dist/dto/signup.dto.js +56 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +28 -0
- package/dist/jwt-optional.guard.d.ts +7 -0
- package/dist/jwt-optional.guard.js +31 -0
- package/dist/jwt.strategy.d.ts +13 -0
- package/dist/jwt.strategy.js +32 -0
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.js +5 -0
- package/jest.config.cjs +10 -0
- package/package.json +33 -0
- package/src/auth.controller.ts +77 -0
- package/src/auth.module.ts +35 -0
- package/src/auth.service.spec.ts +129 -0
- package/src/auth.service.ts +272 -0
- package/src/contracts.ts +63 -0
- package/src/dto/change-password.dto.ts +22 -0
- package/src/dto/forgot-password.dto.ts +7 -0
- package/src/dto/login.dto.ts +16 -0
- package/src/dto/reset-password.dto.ts +16 -0
- package/src/dto/signup.dto.ts +37 -0
- package/src/index.ts +13 -0
- package/src/jwt-optional.guard.ts +28 -0
- package/src/jwt.strategy.ts +19 -0
- package/src/tokens.ts +3 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
2
|
+
import { JwtService } from '@nestjs/jwt';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
import {
|
|
5
|
+
AuthEmailServiceContract,
|
|
6
|
+
AuthUserProfile,
|
|
7
|
+
AuthUsersServiceContract
|
|
8
|
+
} from './contracts';
|
|
9
|
+
import { AUTH_EMAIL_SERVICE, AUTH_USERS_SERVICE } from './tokens';
|
|
10
|
+
import { SignupDto } from './dto/signup.dto';
|
|
11
|
+
import { LoginDto } from './dto/login.dto';
|
|
12
|
+
import { ChangePasswordDto } from './dto/change-password.dto';
|
|
13
|
+
import { ResetPasswordDto } from './dto/reset-password.dto';
|
|
14
|
+
|
|
15
|
+
export type ProfileResponse = {
|
|
16
|
+
id: string;
|
|
17
|
+
email: string;
|
|
18
|
+
firstName: string | null;
|
|
19
|
+
lastName: string | null;
|
|
20
|
+
phoneCountryCode: string | null;
|
|
21
|
+
phoneNumber: string | null;
|
|
22
|
+
preferredLanguage: string | null;
|
|
23
|
+
isAdmin: boolean;
|
|
24
|
+
isCustomer: boolean;
|
|
25
|
+
isCompany: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export class ApiError extends Error {
|
|
29
|
+
constructor(
|
|
30
|
+
public readonly status: number,
|
|
31
|
+
message: string,
|
|
32
|
+
public readonly code?: string
|
|
33
|
+
) {
|
|
34
|
+
super(message);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function badRequest(message: string, code?: string): ApiError {
|
|
39
|
+
return new ApiError(400, message, code);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function unauthorized(message: string, code?: string): ApiError {
|
|
43
|
+
return new ApiError(401, message, code);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toBaseProfile(
|
|
47
|
+
user: AuthUserProfile
|
|
48
|
+
): Omit<ProfileResponse, 'isAdmin' | 'isCustomer' | 'isCompany'> {
|
|
49
|
+
return {
|
|
50
|
+
id: user.id,
|
|
51
|
+
email: user.email,
|
|
52
|
+
firstName: user.firstName ?? null,
|
|
53
|
+
lastName: user.lastName ?? null,
|
|
54
|
+
phoneCountryCode: user.phoneCountryCode ?? null,
|
|
55
|
+
phoneNumber: user.phoneNumber ?? null,
|
|
56
|
+
preferredLanguage: user.preferredLanguage ?? null
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isAdminFromEnv(email: string): boolean {
|
|
61
|
+
const adminEmails = (process.env.ADMIN_EMAILS ?? '')
|
|
62
|
+
.split(',')
|
|
63
|
+
.map((e) => e.trim().toLowerCase())
|
|
64
|
+
.filter(Boolean);
|
|
65
|
+
return adminEmails.length > 0 && adminEmails.includes(email.toLowerCase());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Injectable()
|
|
69
|
+
export class AuthService {
|
|
70
|
+
constructor(
|
|
71
|
+
@Inject(AUTH_USERS_SERVICE)
|
|
72
|
+
private readonly usersService: AuthUsersServiceContract,
|
|
73
|
+
@Inject(AUTH_EMAIL_SERVICE)
|
|
74
|
+
private readonly emailService: AuthEmailServiceContract,
|
|
75
|
+
private readonly jwtService: JwtService
|
|
76
|
+
) {}
|
|
77
|
+
|
|
78
|
+
async signup(dto: SignupDto) {
|
|
79
|
+
const raw = dto.clientHash ?? dto.password;
|
|
80
|
+
if (!raw) {
|
|
81
|
+
throw badRequest(
|
|
82
|
+
'Send clientHash (SHA-256 of password) or password',
|
|
83
|
+
'SEND_CLIENT_HASH_OR_PASSWORD'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const existing = await this.usersService.findByEmail(dto.email);
|
|
87
|
+
if (existing) {
|
|
88
|
+
throw unauthorized('Email is already in use', 'EMAIL_ALREADY_IN_USE');
|
|
89
|
+
}
|
|
90
|
+
const passwordScheme = dto.clientHash ? 'sha256' : 'plain';
|
|
91
|
+
const user = await this.usersService.create({
|
|
92
|
+
email: dto.email,
|
|
93
|
+
password: raw,
|
|
94
|
+
passwordScheme,
|
|
95
|
+
firstName: dto.firstName,
|
|
96
|
+
lastName: dto.lastName,
|
|
97
|
+
phoneCountryCode: dto.phoneCountryCode,
|
|
98
|
+
phoneNumber: dto.phoneNumber
|
|
99
|
+
});
|
|
100
|
+
if (this.usersService.onSignupComplete) {
|
|
101
|
+
await this.usersService.onSignupComplete(user.id, {
|
|
102
|
+
userType: dto.userType,
|
|
103
|
+
firstName: dto.firstName,
|
|
104
|
+
lastName: dto.lastName
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return this.buildToken(user.id, user.email);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async login(dto: LoginDto) {
|
|
111
|
+
if (!dto.clientHash && !dto.password) {
|
|
112
|
+
throw badRequest(
|
|
113
|
+
'Send clientHash or password',
|
|
114
|
+
'SEND_CLIENT_HASH_OR_PASSWORD'
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const user = await this.usersService.findByEmail(dto.email);
|
|
118
|
+
if (!user) {
|
|
119
|
+
throw unauthorized(
|
|
120
|
+
'Invalid email or password',
|
|
121
|
+
'INVALID_EMAIL_OR_PASSWORD'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const scheme = user.passwordScheme ?? 'plain';
|
|
125
|
+
const valueToCompare =
|
|
126
|
+
scheme === 'sha256' ? dto.clientHash : dto.password;
|
|
127
|
+
if (!valueToCompare) {
|
|
128
|
+
throw unauthorized(
|
|
129
|
+
scheme === 'sha256'
|
|
130
|
+
? 'Login requires clientHash (password hashed on client).'
|
|
131
|
+
: 'Resend your password for this device.',
|
|
132
|
+
'LOGIN_REQUIRES_CLIENT_HASH_OR_PASSWORD'
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
await this.usersService.verifyCredentials(dto.email, valueToCompare);
|
|
136
|
+
if (scheme === 'plain' && dto.clientHash && dto.password) {
|
|
137
|
+
await this.usersService.upgradeToClientHash(user.id, dto.clientHash);
|
|
138
|
+
}
|
|
139
|
+
return this.buildToken(user.id, user.email);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async tokenForUser(userId: string, email: string) {
|
|
143
|
+
return this.buildToken(userId, email);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getMe(userId: string): Promise<ProfileResponse> {
|
|
147
|
+
const user = await this.usersService.findById(userId);
|
|
148
|
+
if (!user) {
|
|
149
|
+
throw unauthorized('User not found', 'USER_NOT_FOUND');
|
|
150
|
+
}
|
|
151
|
+
return this.profileWithRoles(user);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async profileWithRoles(user: AuthUserProfile): Promise<ProfileResponse> {
|
|
155
|
+
const base = toBaseProfile(user);
|
|
156
|
+
let isCustomer = false;
|
|
157
|
+
let isCompany = false;
|
|
158
|
+
if (this.usersService.getProfileRoles) {
|
|
159
|
+
const roles = await this.usersService.getProfileRoles(user.id);
|
|
160
|
+
isCustomer = roles.isCustomer;
|
|
161
|
+
isCompany = roles.isCompany;
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
...base,
|
|
165
|
+
isAdmin: isAdminFromEnv(user.email),
|
|
166
|
+
isCustomer,
|
|
167
|
+
isCompany
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async updatePreferredLanguage(
|
|
172
|
+
userId: string,
|
|
173
|
+
preferredLanguage: string
|
|
174
|
+
): Promise<void> {
|
|
175
|
+
await this.usersService.updatePreferredLanguage(userId, preferredLanguage);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async changePassword(
|
|
179
|
+
userId: string,
|
|
180
|
+
dto: ChangePasswordDto
|
|
181
|
+
): Promise<{ accessToken: string }> {
|
|
182
|
+
const user = await this.usersService.findById(userId);
|
|
183
|
+
if (!user) {
|
|
184
|
+
throw unauthorized('User not found', 'USER_NOT_FOUND');
|
|
185
|
+
}
|
|
186
|
+
const scheme = user.passwordScheme ?? 'plain';
|
|
187
|
+
const currentRaw =
|
|
188
|
+
scheme === 'sha256' ? dto.currentClientHash : dto.currentPassword;
|
|
189
|
+
const newRaw = dto.newClientHash ?? dto.newPassword;
|
|
190
|
+
if (!currentRaw || !newRaw) {
|
|
191
|
+
throw badRequest(
|
|
192
|
+
scheme === 'sha256'
|
|
193
|
+
? 'Send currentClientHash and newClientHash'
|
|
194
|
+
: 'Send currentPassword and newPassword (or use clientHash)',
|
|
195
|
+
'SEND_CURRENT_AND_NEW_PASSWORD'
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
await this.usersService.verifyCredentials(user.email, currentRaw);
|
|
199
|
+
if (scheme === 'sha256') {
|
|
200
|
+
await this.usersService.upgradeToClientHash(userId, newRaw);
|
|
201
|
+
} else {
|
|
202
|
+
await this.usersService.setPasswordPlain(userId, newRaw);
|
|
203
|
+
}
|
|
204
|
+
return this.buildToken(userId, user.email);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async forgotPassword(email: string, frontendBaseUrl: string): Promise<{ message: string }> {
|
|
208
|
+
const user = await this.usersService.findByEmail(email);
|
|
209
|
+
if (!user) {
|
|
210
|
+
return {
|
|
211
|
+
message:
|
|
212
|
+
'If this email exists in our system, you will receive instructions to reset your password.'
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
216
|
+
const expiresAt = new Date(Date.now() + 60 * 60 * 1000);
|
|
217
|
+
await this.usersService.createPasswordReset(user.id, token, expiresAt);
|
|
218
|
+
|
|
219
|
+
const baseUrl = frontendBaseUrl.replace(/\/+$/, '');
|
|
220
|
+
const resetLink = `${baseUrl}/reset-password?token=${encodeURIComponent(
|
|
221
|
+
token
|
|
222
|
+
)}`;
|
|
223
|
+
await this.emailService.sendPasswordResetEmail(user.email, resetLink);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
message:
|
|
227
|
+
'If this email exists in our system, you will receive instructions to reset your password.'
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async resetPassword(dto: ResetPasswordDto): Promise<{ accessToken: string }> {
|
|
232
|
+
const record = await this.usersService.findValidPasswordReset(dto.token);
|
|
233
|
+
if (!record) {
|
|
234
|
+
throw badRequest(
|
|
235
|
+
'Invalid or expired reset token.',
|
|
236
|
+
'INVALID_OR_EXPIRED_RESET_TOKEN'
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const user = await this.usersService.findById(record.userId);
|
|
240
|
+
if (!user) {
|
|
241
|
+
throw badRequest(
|
|
242
|
+
'User not found for this token.',
|
|
243
|
+
'USER_NOT_FOUND_FOR_TOKEN'
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const raw = dto.clientHash ?? dto.password;
|
|
248
|
+
if (!raw) {
|
|
249
|
+
throw badRequest(
|
|
250
|
+
'Send clientHash (SHA-256 of password) or password.',
|
|
251
|
+
'SEND_CLIENT_HASH_OR_PASSWORD'
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const passwordScheme = dto.clientHash ? 'sha256' : 'plain';
|
|
256
|
+
if (passwordScheme === 'sha256') {
|
|
257
|
+
await this.usersService.upgradeToClientHash(user.id, raw);
|
|
258
|
+
} else {
|
|
259
|
+
await this.usersService.setPasswordPlain(user.id, raw);
|
|
260
|
+
}
|
|
261
|
+
await this.usersService.markPasswordResetUsed(record.id);
|
|
262
|
+
|
|
263
|
+
return this.buildToken(user.id, user.email);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private async buildToken(userId: string, email: string) {
|
|
267
|
+
const payload = { sub: userId, email };
|
|
268
|
+
const accessToken = await this.jwtService.signAsync(payload);
|
|
269
|
+
return { accessToken };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
package/src/contracts.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export type AuthUserProfile = {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
firstName?: string | null;
|
|
5
|
+
lastName?: string | null;
|
|
6
|
+
phoneCountryCode?: string | null;
|
|
7
|
+
phoneNumber?: string | null;
|
|
8
|
+
preferredLanguage?: string | null;
|
|
9
|
+
passwordScheme?: 'sha256' | 'plain' | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type PasswordResetRecord = {
|
|
13
|
+
id: string;
|
|
14
|
+
userId: string;
|
|
15
|
+
token: string;
|
|
16
|
+
expiresAt: Date;
|
|
17
|
+
usedAt: Date | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Payload passed to {@link AuthUsersServiceContract.onSignupComplete} after the user row exists. */
|
|
21
|
+
export type SignupFollowUpPayload = {
|
|
22
|
+
userType?: 'customer' | 'company';
|
|
23
|
+
firstName?: string;
|
|
24
|
+
lastName?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface AuthUsersServiceContract {
|
|
28
|
+
findByEmail(email: string): Promise<AuthUserProfile | null>;
|
|
29
|
+
findById(id: string): Promise<AuthUserProfile | null>;
|
|
30
|
+
create(input: {
|
|
31
|
+
email: string;
|
|
32
|
+
password: string;
|
|
33
|
+
passwordScheme: 'sha256' | 'plain';
|
|
34
|
+
firstName?: string | null;
|
|
35
|
+
lastName?: string | null;
|
|
36
|
+
phoneCountryCode?: string | null;
|
|
37
|
+
phoneNumber?: string | null;
|
|
38
|
+
}): Promise<AuthUserProfile>;
|
|
39
|
+
verifyCredentials(email: string, rawPasswordOrHash: string): Promise<void>;
|
|
40
|
+
upgradeToClientHash(userId: string, clientHash: string): Promise<void>;
|
|
41
|
+
setPasswordPlain(userId: string, plainPassword: string): Promise<void>;
|
|
42
|
+
updatePreferredLanguage(userId: string, preferredLanguage: string): Promise<void>;
|
|
43
|
+
createPasswordReset(userId: string, token: string, expiresAt: Date): Promise<PasswordResetRecord>;
|
|
44
|
+
findValidPasswordReset(token: string): Promise<PasswordResetRecord | null>;
|
|
45
|
+
markPasswordResetUsed(resetId: string): Promise<void>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Optional: create domain rows after signup (e.g. CustomerUser / CompanyUser).
|
|
49
|
+
* Implement in the host app when using `userType` on signup.
|
|
50
|
+
*/
|
|
51
|
+
onSignupComplete?(userId: string, payload: SignupFollowUpPayload): Promise<void>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Optional: resolve customer vs company flags for `/auth/me`.
|
|
55
|
+
* If omitted, both default to `false` (admin still uses `ADMIN_EMAILS`).
|
|
56
|
+
*/
|
|
57
|
+
getProfileRoles?(userId: string): Promise<{ isCustomer: boolean; isCompany: boolean }>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AuthEmailServiceContract {
|
|
61
|
+
sendPasswordResetEmail(email: string, resetLink: string): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { IsOptional, IsString, MinLength } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export class ChangePasswordDto {
|
|
4
|
+
@IsOptional()
|
|
5
|
+
@IsString()
|
|
6
|
+
@MinLength(6)
|
|
7
|
+
currentPassword?: string;
|
|
8
|
+
|
|
9
|
+
@IsOptional()
|
|
10
|
+
@IsString()
|
|
11
|
+
currentClientHash?: string;
|
|
12
|
+
|
|
13
|
+
@IsOptional()
|
|
14
|
+
@IsString()
|
|
15
|
+
@MinLength(6)
|
|
16
|
+
newPassword?: string;
|
|
17
|
+
|
|
18
|
+
@IsOptional()
|
|
19
|
+
@IsString()
|
|
20
|
+
newClientHash?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { IsEmail, IsOptional, IsString, MinLength } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export class LoginDto {
|
|
4
|
+
@IsEmail()
|
|
5
|
+
email!: string;
|
|
6
|
+
|
|
7
|
+
@IsOptional()
|
|
8
|
+
@IsString()
|
|
9
|
+
@MinLength(6)
|
|
10
|
+
password?: string;
|
|
11
|
+
|
|
12
|
+
@IsOptional()
|
|
13
|
+
@IsString()
|
|
14
|
+
clientHash?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { IsOptional, IsString, MinLength } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export class ResetPasswordDto {
|
|
4
|
+
@IsString()
|
|
5
|
+
token!: string;
|
|
6
|
+
|
|
7
|
+
@IsOptional()
|
|
8
|
+
@IsString()
|
|
9
|
+
@MinLength(6)
|
|
10
|
+
password?: string;
|
|
11
|
+
|
|
12
|
+
@IsOptional()
|
|
13
|
+
@IsString()
|
|
14
|
+
clientHash?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { IsEmail, IsIn, IsOptional, IsString, MinLength } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export class SignupDto {
|
|
4
|
+
@IsEmail()
|
|
5
|
+
email!: string;
|
|
6
|
+
|
|
7
|
+
@IsOptional()
|
|
8
|
+
@IsString()
|
|
9
|
+
@MinLength(6)
|
|
10
|
+
password?: string;
|
|
11
|
+
|
|
12
|
+
@IsOptional()
|
|
13
|
+
@IsString()
|
|
14
|
+
clientHash?: string;
|
|
15
|
+
|
|
16
|
+
@IsOptional()
|
|
17
|
+
@IsString()
|
|
18
|
+
firstName?: string;
|
|
19
|
+
|
|
20
|
+
@IsOptional()
|
|
21
|
+
@IsString()
|
|
22
|
+
lastName?: string;
|
|
23
|
+
|
|
24
|
+
@IsOptional()
|
|
25
|
+
@IsString()
|
|
26
|
+
phoneCountryCode?: string;
|
|
27
|
+
|
|
28
|
+
@IsOptional()
|
|
29
|
+
@IsString()
|
|
30
|
+
phoneNumber?: string;
|
|
31
|
+
|
|
32
|
+
/** When set, the host app should link customer/company profile rows via `AuthUsersServiceContract.onSignupComplete`. */
|
|
33
|
+
@IsOptional()
|
|
34
|
+
@IsIn(['customer', 'company'])
|
|
35
|
+
userType?: 'customer' | 'company';
|
|
36
|
+
}
|
|
37
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './auth.module';
|
|
2
|
+
export * from './auth.service';
|
|
3
|
+
export * from './auth.controller';
|
|
4
|
+
export * from './jwt.strategy';
|
|
5
|
+
export * from './jwt-optional.guard';
|
|
6
|
+
export * from './contracts';
|
|
7
|
+
export * from './tokens';
|
|
8
|
+
export * from './dto/signup.dto';
|
|
9
|
+
export * from './dto/login.dto';
|
|
10
|
+
export * from './dto/forgot-password.dto';
|
|
11
|
+
export * from './dto/reset-password.dto';
|
|
12
|
+
export * from './dto/change-password.dto';
|
|
13
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ExecutionContext, Injectable } from '@nestjs/common';
|
|
2
|
+
import { AuthGuard } from '@nestjs/passport';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class JwtOptionalGuard extends AuthGuard('jwt') {
|
|
6
|
+
handleRequest<TUser = unknown>(
|
|
7
|
+
err: unknown,
|
|
8
|
+
user: unknown,
|
|
9
|
+
_info: unknown,
|
|
10
|
+
_context: ExecutionContext,
|
|
11
|
+
_status?: unknown
|
|
12
|
+
): TUser {
|
|
13
|
+
if (err) {
|
|
14
|
+
return null as TUser;
|
|
15
|
+
}
|
|
16
|
+
return (user ?? null) as TUser;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
canActivate(context: ExecutionContext) {
|
|
20
|
+
const request = context.switchToHttp().getRequest();
|
|
21
|
+
const authHeader: string | undefined = request.headers['authorization'];
|
|
22
|
+
if (!authHeader) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return super.canActivate(context);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { PassportStrategy } from '@nestjs/passport';
|
|
3
|
+
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class JwtStrategy extends PassportStrategy(Strategy) {
|
|
7
|
+
constructor() {
|
|
8
|
+
super({
|
|
9
|
+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
10
|
+
ignoreExpiration: false,
|
|
11
|
+
secretOrKey: process.env.JWT_SECRET || 'dev-secret'
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async validate(payload: { sub: string; email: string }) {
|
|
16
|
+
return { userId: payload.sub, email: payload.email };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
package/src/tokens.ts
ADDED
package/tsconfig.json
ADDED