@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.
Files changed (45) hide show
  1. package/dist/auth.controller.d.ts +43 -0
  2. package/dist/auth.controller.js +115 -0
  3. package/dist/auth.module.d.ts +2 -0
  4. package/dist/auth.module.js +45 -0
  5. package/dist/auth.service.d.ts +51 -0
  6. package/dist/auth.service.js +243 -0
  7. package/dist/auth.service.spec.d.ts +1 -0
  8. package/dist/auth.service.spec.js +105 -0
  9. package/dist/contracts.d.ts +59 -0
  10. package/dist/contracts.js +2 -0
  11. package/dist/dto/change-password.dto.d.ts +6 -0
  12. package/dist/dto/change-password.dto.js +38 -0
  13. package/dist/dto/forgot-password.dto.d.ts +3 -0
  14. package/dist/dto/forgot-password.dto.js +20 -0
  15. package/dist/dto/login.dto.d.ts +5 -0
  16. package/dist/dto/login.dto.js +31 -0
  17. package/dist/dto/reset-password.dto.d.ts +5 -0
  18. package/dist/dto/reset-password.dto.js +31 -0
  19. package/dist/dto/signup.dto.d.ts +11 -0
  20. package/dist/dto/signup.dto.js +56 -0
  21. package/dist/index.d.ts +12 -0
  22. package/dist/index.js +28 -0
  23. package/dist/jwt-optional.guard.d.ts +7 -0
  24. package/dist/jwt-optional.guard.js +31 -0
  25. package/dist/jwt.strategy.d.ts +13 -0
  26. package/dist/jwt.strategy.js +32 -0
  27. package/dist/tokens.d.ts +2 -0
  28. package/dist/tokens.js +5 -0
  29. package/jest.config.cjs +10 -0
  30. package/package.json +33 -0
  31. package/src/auth.controller.ts +77 -0
  32. package/src/auth.module.ts +35 -0
  33. package/src/auth.service.spec.ts +129 -0
  34. package/src/auth.service.ts +272 -0
  35. package/src/contracts.ts +63 -0
  36. package/src/dto/change-password.dto.ts +22 -0
  37. package/src/dto/forgot-password.dto.ts +7 -0
  38. package/src/dto/login.dto.ts +16 -0
  39. package/src/dto/reset-password.dto.ts +16 -0
  40. package/src/dto/signup.dto.ts +37 -0
  41. package/src/index.ts +13 -0
  42. package/src/jwt-optional.guard.ts +28 -0
  43. package/src/jwt.strategy.ts +19 -0
  44. package/src/tokens.ts +3 -0
  45. package/tsconfig.json +13 -0
@@ -0,0 +1,43 @@
1
+ import { Request } from 'express';
2
+ import { AuthService } from './auth.service';
3
+ import { SignupDto } from './dto/signup.dto';
4
+ import { LoginDto } from './dto/login.dto';
5
+ import { ChangePasswordDto } from './dto/change-password.dto';
6
+ import { ForgotPasswordDto } from './dto/forgot-password.dto';
7
+ import { ResetPasswordDto } from './dto/reset-password.dto';
8
+ interface JwtUser {
9
+ userId: string;
10
+ email: string;
11
+ }
12
+ export declare class AuthController {
13
+ private readonly authService;
14
+ constructor(authService: AuthService);
15
+ signup(dto: SignupDto): Promise<{
16
+ accessToken: string;
17
+ }>;
18
+ login(dto: LoginDto): Promise<{
19
+ accessToken: string;
20
+ }>;
21
+ getMe(req: Request & {
22
+ user: JwtUser;
23
+ }): Promise<import("./auth.service").ProfileResponse>;
24
+ patchMe(req: Request & {
25
+ user: JwtUser;
26
+ }, body: {
27
+ preferredLanguage?: string;
28
+ }): Promise<{
29
+ preferredLanguage: string;
30
+ } | undefined>;
31
+ changePassword(req: Request & {
32
+ user: JwtUser;
33
+ }, dto: ChangePasswordDto): Promise<{
34
+ accessToken: string;
35
+ }>;
36
+ forgotPassword(dto: ForgotPasswordDto, req: Request): Promise<{
37
+ message: string;
38
+ }>;
39
+ resetPassword(dto: ResetPasswordDto): Promise<{
40
+ accessToken: string;
41
+ }>;
42
+ }
43
+ export {};
@@ -0,0 +1,115 @@
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.AuthController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const passport_1 = require("@nestjs/passport");
18
+ const auth_service_1 = require("./auth.service");
19
+ const signup_dto_1 = require("./dto/signup.dto");
20
+ const login_dto_1 = require("./dto/login.dto");
21
+ const change_password_dto_1 = require("./dto/change-password.dto");
22
+ const forgot_password_dto_1 = require("./dto/forgot-password.dto");
23
+ const reset_password_dto_1 = require("./dto/reset-password.dto");
24
+ let AuthController = class AuthController {
25
+ constructor(authService) {
26
+ this.authService = authService;
27
+ }
28
+ signup(dto) {
29
+ return this.authService.signup(dto);
30
+ }
31
+ login(dto) {
32
+ return this.authService.login(dto);
33
+ }
34
+ getMe(req) {
35
+ return this.authService.getMe(req.user.userId);
36
+ }
37
+ async patchMe(req, body) {
38
+ if (body.preferredLanguage != null) {
39
+ await this.authService.updatePreferredLanguage(req.user.userId, body.preferredLanguage);
40
+ return { preferredLanguage: body.preferredLanguage };
41
+ }
42
+ }
43
+ changePassword(req, dto) {
44
+ return this.authService.changePassword(req.user.userId, dto);
45
+ }
46
+ forgotPassword(dto, req) {
47
+ // The caller should set X-Frontend-Base-Url if necessary; otherwise this can be overridden at service level.
48
+ const headerBase = req.headers['x-frontend-base-url'] ?? '';
49
+ const base = headerBase || '';
50
+ return this.authService.forgotPassword(dto.email, base);
51
+ }
52
+ resetPassword(dto) {
53
+ return this.authService.resetPassword(dto);
54
+ }
55
+ };
56
+ exports.AuthController = AuthController;
57
+ __decorate([
58
+ (0, common_1.Post)('signup'),
59
+ __param(0, (0, common_1.Body)()),
60
+ __metadata("design:type", Function),
61
+ __metadata("design:paramtypes", [signup_dto_1.SignupDto]),
62
+ __metadata("design:returntype", void 0)
63
+ ], AuthController.prototype, "signup", null);
64
+ __decorate([
65
+ (0, common_1.Post)('login'),
66
+ __param(0, (0, common_1.Body)()),
67
+ __metadata("design:type", Function),
68
+ __metadata("design:paramtypes", [login_dto_1.LoginDto]),
69
+ __metadata("design:returntype", void 0)
70
+ ], AuthController.prototype, "login", null);
71
+ __decorate([
72
+ (0, common_1.Get)('me'),
73
+ (0, common_1.UseGuards)((0, passport_1.AuthGuard)('jwt')),
74
+ __param(0, (0, common_1.Req)()),
75
+ __metadata("design:type", Function),
76
+ __metadata("design:paramtypes", [Object]),
77
+ __metadata("design:returntype", void 0)
78
+ ], AuthController.prototype, "getMe", null);
79
+ __decorate([
80
+ (0, common_1.Patch)('me'),
81
+ (0, common_1.UseGuards)((0, passport_1.AuthGuard)('jwt')),
82
+ __param(0, (0, common_1.Req)()),
83
+ __param(1, (0, common_1.Body)()),
84
+ __metadata("design:type", Function),
85
+ __metadata("design:paramtypes", [Object, Object]),
86
+ __metadata("design:returntype", Promise)
87
+ ], AuthController.prototype, "patchMe", null);
88
+ __decorate([
89
+ (0, common_1.Patch)('change-password'),
90
+ (0, common_1.UseGuards)((0, passport_1.AuthGuard)('jwt')),
91
+ __param(0, (0, common_1.Req)()),
92
+ __param(1, (0, common_1.Body)()),
93
+ __metadata("design:type", Function),
94
+ __metadata("design:paramtypes", [Object, change_password_dto_1.ChangePasswordDto]),
95
+ __metadata("design:returntype", void 0)
96
+ ], AuthController.prototype, "changePassword", null);
97
+ __decorate([
98
+ (0, common_1.Post)('forgot-password'),
99
+ __param(0, (0, common_1.Body)()),
100
+ __param(1, (0, common_1.Req)()),
101
+ __metadata("design:type", Function),
102
+ __metadata("design:paramtypes", [forgot_password_dto_1.ForgotPasswordDto, Object]),
103
+ __metadata("design:returntype", void 0)
104
+ ], AuthController.prototype, "forgotPassword", null);
105
+ __decorate([
106
+ (0, common_1.Post)('reset-password'),
107
+ __param(0, (0, common_1.Body)()),
108
+ __metadata("design:type", Function),
109
+ __metadata("design:paramtypes", [reset_password_dto_1.ResetPasswordDto]),
110
+ __metadata("design:returntype", void 0)
111
+ ], AuthController.prototype, "resetPassword", null);
112
+ exports.AuthController = AuthController = __decorate([
113
+ (0, common_1.Controller)('auth'),
114
+ __metadata("design:paramtypes", [auth_service_1.AuthService])
115
+ ], AuthController);
@@ -0,0 +1,2 @@
1
+ export declare class AuthModule {
2
+ }
@@ -0,0 +1,45 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.AuthModule = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const jwt_1 = require("@nestjs/jwt");
12
+ const passport_1 = require("@nestjs/passport");
13
+ const auth_service_1 = require("./auth.service");
14
+ const auth_controller_1 = require("./auth.controller");
15
+ const jwt_strategy_1 = require("./jwt.strategy");
16
+ const tokens_1 = require("./tokens");
17
+ let AuthModule = class AuthModule {
18
+ };
19
+ exports.AuthModule = AuthModule;
20
+ exports.AuthModule = AuthModule = __decorate([
21
+ (0, common_1.Module)({
22
+ imports: [
23
+ passport_1.PassportModule,
24
+ jwt_1.JwtModule.register({
25
+ global: true,
26
+ secret: process.env.JWT_SECRET || 'dev-secret',
27
+ signOptions: { expiresIn: '1h' }
28
+ })
29
+ ],
30
+ providers: [
31
+ auth_service_1.AuthService,
32
+ jwt_strategy_1.JwtStrategy,
33
+ {
34
+ provide: tokens_1.AUTH_USERS_SERVICE,
35
+ useValue: null
36
+ },
37
+ {
38
+ provide: tokens_1.AUTH_EMAIL_SERVICE,
39
+ useValue: null
40
+ }
41
+ ],
42
+ controllers: [auth_controller_1.AuthController],
43
+ exports: [auth_service_1.AuthService, tokens_1.AUTH_USERS_SERVICE, tokens_1.AUTH_EMAIL_SERVICE]
44
+ })
45
+ ], AuthModule);
@@ -0,0 +1,51 @@
1
+ import { JwtService } from '@nestjs/jwt';
2
+ import { AuthEmailServiceContract, AuthUsersServiceContract } from './contracts';
3
+ import { SignupDto } from './dto/signup.dto';
4
+ import { LoginDto } from './dto/login.dto';
5
+ import { ChangePasswordDto } from './dto/change-password.dto';
6
+ import { ResetPasswordDto } from './dto/reset-password.dto';
7
+ export type ProfileResponse = {
8
+ id: string;
9
+ email: string;
10
+ firstName: string | null;
11
+ lastName: string | null;
12
+ phoneCountryCode: string | null;
13
+ phoneNumber: string | null;
14
+ preferredLanguage: string | null;
15
+ isAdmin: boolean;
16
+ isCustomer: boolean;
17
+ isCompany: boolean;
18
+ };
19
+ export declare class ApiError extends Error {
20
+ readonly status: number;
21
+ readonly code?: string | undefined;
22
+ constructor(status: number, message: string, code?: string | undefined);
23
+ }
24
+ export declare class AuthService {
25
+ private readonly usersService;
26
+ private readonly emailService;
27
+ private readonly jwtService;
28
+ constructor(usersService: AuthUsersServiceContract, emailService: AuthEmailServiceContract, jwtService: JwtService);
29
+ signup(dto: SignupDto): Promise<{
30
+ accessToken: string;
31
+ }>;
32
+ login(dto: LoginDto): Promise<{
33
+ accessToken: string;
34
+ }>;
35
+ tokenForUser(userId: string, email: string): Promise<{
36
+ accessToken: string;
37
+ }>;
38
+ getMe(userId: string): Promise<ProfileResponse>;
39
+ private profileWithRoles;
40
+ updatePreferredLanguage(userId: string, preferredLanguage: string): Promise<void>;
41
+ changePassword(userId: string, dto: ChangePasswordDto): Promise<{
42
+ accessToken: string;
43
+ }>;
44
+ forgotPassword(email: string, frontendBaseUrl: string): Promise<{
45
+ message: string;
46
+ }>;
47
+ resetPassword(dto: ResetPasswordDto): Promise<{
48
+ accessToken: string;
49
+ }>;
50
+ private buildToken;
51
+ }
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ 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;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __metadata = (this && this.__metadata) || function (k, v) {
42
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
43
+ };
44
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
45
+ return function (target, key) { decorator(target, key, paramIndex); }
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.AuthService = exports.ApiError = void 0;
49
+ const common_1 = require("@nestjs/common");
50
+ const jwt_1 = require("@nestjs/jwt");
51
+ const crypto = __importStar(require("crypto"));
52
+ const tokens_1 = require("./tokens");
53
+ class ApiError extends Error {
54
+ constructor(status, message, code) {
55
+ super(message);
56
+ this.status = status;
57
+ this.code = code;
58
+ }
59
+ }
60
+ exports.ApiError = ApiError;
61
+ function badRequest(message, code) {
62
+ return new ApiError(400, message, code);
63
+ }
64
+ function unauthorized(message, code) {
65
+ return new ApiError(401, message, code);
66
+ }
67
+ function toBaseProfile(user) {
68
+ return {
69
+ id: user.id,
70
+ email: user.email,
71
+ firstName: user.firstName ?? null,
72
+ lastName: user.lastName ?? null,
73
+ phoneCountryCode: user.phoneCountryCode ?? null,
74
+ phoneNumber: user.phoneNumber ?? null,
75
+ preferredLanguage: user.preferredLanguage ?? null
76
+ };
77
+ }
78
+ function isAdminFromEnv(email) {
79
+ const adminEmails = (process.env.ADMIN_EMAILS ?? '')
80
+ .split(',')
81
+ .map((e) => e.trim().toLowerCase())
82
+ .filter(Boolean);
83
+ return adminEmails.length > 0 && adminEmails.includes(email.toLowerCase());
84
+ }
85
+ let AuthService = class AuthService {
86
+ constructor(usersService, emailService, jwtService) {
87
+ this.usersService = usersService;
88
+ this.emailService = emailService;
89
+ this.jwtService = jwtService;
90
+ }
91
+ async signup(dto) {
92
+ const raw = dto.clientHash ?? dto.password;
93
+ if (!raw) {
94
+ throw badRequest('Send clientHash (SHA-256 of password) or password', 'SEND_CLIENT_HASH_OR_PASSWORD');
95
+ }
96
+ const existing = await this.usersService.findByEmail(dto.email);
97
+ if (existing) {
98
+ throw unauthorized('Email is already in use', 'EMAIL_ALREADY_IN_USE');
99
+ }
100
+ const passwordScheme = dto.clientHash ? 'sha256' : 'plain';
101
+ const user = await this.usersService.create({
102
+ email: dto.email,
103
+ password: raw,
104
+ passwordScheme,
105
+ firstName: dto.firstName,
106
+ lastName: dto.lastName,
107
+ phoneCountryCode: dto.phoneCountryCode,
108
+ phoneNumber: dto.phoneNumber
109
+ });
110
+ if (this.usersService.onSignupComplete) {
111
+ await this.usersService.onSignupComplete(user.id, {
112
+ userType: dto.userType,
113
+ firstName: dto.firstName,
114
+ lastName: dto.lastName
115
+ });
116
+ }
117
+ return this.buildToken(user.id, user.email);
118
+ }
119
+ async login(dto) {
120
+ if (!dto.clientHash && !dto.password) {
121
+ throw badRequest('Send clientHash or password', 'SEND_CLIENT_HASH_OR_PASSWORD');
122
+ }
123
+ const user = await this.usersService.findByEmail(dto.email);
124
+ if (!user) {
125
+ throw unauthorized('Invalid email or password', 'INVALID_EMAIL_OR_PASSWORD');
126
+ }
127
+ const scheme = user.passwordScheme ?? 'plain';
128
+ const valueToCompare = scheme === 'sha256' ? dto.clientHash : dto.password;
129
+ if (!valueToCompare) {
130
+ throw unauthorized(scheme === 'sha256'
131
+ ? 'Login requires clientHash (password hashed on client).'
132
+ : 'Resend your password for this device.', 'LOGIN_REQUIRES_CLIENT_HASH_OR_PASSWORD');
133
+ }
134
+ await this.usersService.verifyCredentials(dto.email, valueToCompare);
135
+ if (scheme === 'plain' && dto.clientHash && dto.password) {
136
+ await this.usersService.upgradeToClientHash(user.id, dto.clientHash);
137
+ }
138
+ return this.buildToken(user.id, user.email);
139
+ }
140
+ async tokenForUser(userId, email) {
141
+ return this.buildToken(userId, email);
142
+ }
143
+ async getMe(userId) {
144
+ const user = await this.usersService.findById(userId);
145
+ if (!user) {
146
+ throw unauthorized('User not found', 'USER_NOT_FOUND');
147
+ }
148
+ return this.profileWithRoles(user);
149
+ }
150
+ async profileWithRoles(user) {
151
+ const base = toBaseProfile(user);
152
+ let isCustomer = false;
153
+ let isCompany = false;
154
+ if (this.usersService.getProfileRoles) {
155
+ const roles = await this.usersService.getProfileRoles(user.id);
156
+ isCustomer = roles.isCustomer;
157
+ isCompany = roles.isCompany;
158
+ }
159
+ return {
160
+ ...base,
161
+ isAdmin: isAdminFromEnv(user.email),
162
+ isCustomer,
163
+ isCompany
164
+ };
165
+ }
166
+ async updatePreferredLanguage(userId, preferredLanguage) {
167
+ await this.usersService.updatePreferredLanguage(userId, preferredLanguage);
168
+ }
169
+ async changePassword(userId, dto) {
170
+ const user = await this.usersService.findById(userId);
171
+ if (!user) {
172
+ throw unauthorized('User not found', 'USER_NOT_FOUND');
173
+ }
174
+ const scheme = user.passwordScheme ?? 'plain';
175
+ const currentRaw = scheme === 'sha256' ? dto.currentClientHash : dto.currentPassword;
176
+ const newRaw = dto.newClientHash ?? dto.newPassword;
177
+ if (!currentRaw || !newRaw) {
178
+ throw badRequest(scheme === 'sha256'
179
+ ? 'Send currentClientHash and newClientHash'
180
+ : 'Send currentPassword and newPassword (or use clientHash)', 'SEND_CURRENT_AND_NEW_PASSWORD');
181
+ }
182
+ await this.usersService.verifyCredentials(user.email, currentRaw);
183
+ if (scheme === 'sha256') {
184
+ await this.usersService.upgradeToClientHash(userId, newRaw);
185
+ }
186
+ else {
187
+ await this.usersService.setPasswordPlain(userId, newRaw);
188
+ }
189
+ return this.buildToken(userId, user.email);
190
+ }
191
+ async forgotPassword(email, frontendBaseUrl) {
192
+ const user = await this.usersService.findByEmail(email);
193
+ if (!user) {
194
+ return {
195
+ message: 'If this email exists in our system, you will receive instructions to reset your password.'
196
+ };
197
+ }
198
+ const token = crypto.randomBytes(32).toString('hex');
199
+ const expiresAt = new Date(Date.now() + 60 * 60 * 1000);
200
+ await this.usersService.createPasswordReset(user.id, token, expiresAt);
201
+ const baseUrl = frontendBaseUrl.replace(/\/+$/, '');
202
+ const resetLink = `${baseUrl}/reset-password?token=${encodeURIComponent(token)}`;
203
+ await this.emailService.sendPasswordResetEmail(user.email, resetLink);
204
+ return {
205
+ message: 'If this email exists in our system, you will receive instructions to reset your password.'
206
+ };
207
+ }
208
+ async resetPassword(dto) {
209
+ const record = await this.usersService.findValidPasswordReset(dto.token);
210
+ if (!record) {
211
+ throw badRequest('Invalid or expired reset token.', 'INVALID_OR_EXPIRED_RESET_TOKEN');
212
+ }
213
+ const user = await this.usersService.findById(record.userId);
214
+ if (!user) {
215
+ throw badRequest('User not found for this token.', 'USER_NOT_FOUND_FOR_TOKEN');
216
+ }
217
+ const raw = dto.clientHash ?? dto.password;
218
+ if (!raw) {
219
+ throw badRequest('Send clientHash (SHA-256 of password) or password.', 'SEND_CLIENT_HASH_OR_PASSWORD');
220
+ }
221
+ const passwordScheme = dto.clientHash ? 'sha256' : 'plain';
222
+ if (passwordScheme === 'sha256') {
223
+ await this.usersService.upgradeToClientHash(user.id, raw);
224
+ }
225
+ else {
226
+ await this.usersService.setPasswordPlain(user.id, raw);
227
+ }
228
+ await this.usersService.markPasswordResetUsed(record.id);
229
+ return this.buildToken(user.id, user.email);
230
+ }
231
+ async buildToken(userId, email) {
232
+ const payload = { sub: userId, email };
233
+ const accessToken = await this.jwtService.signAsync(payload);
234
+ return { accessToken };
235
+ }
236
+ };
237
+ exports.AuthService = AuthService;
238
+ exports.AuthService = AuthService = __decorate([
239
+ (0, common_1.Injectable)(),
240
+ __param(0, (0, common_1.Inject)(tokens_1.AUTH_USERS_SERVICE)),
241
+ __param(1, (0, common_1.Inject)(tokens_1.AUTH_EMAIL_SERVICE)),
242
+ __metadata("design:paramtypes", [Object, Object, jwt_1.JwtService])
243
+ ], AuthService);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const jwt_1 = require("@nestjs/jwt");
4
+ const auth_service_1 = require("./auth.service");
5
+ class FakeUsersService {
6
+ constructor() {
7
+ this.users = new Map();
8
+ }
9
+ async findByEmail(email) {
10
+ for (const u of this.users.values()) {
11
+ if (u.email === email) {
12
+ return {
13
+ id: u.id,
14
+ email: u.email,
15
+ passwordScheme: u.passwordScheme
16
+ };
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+ async findById(id) {
22
+ const u = this.users.get(id);
23
+ return u ? { id: u.id, email: u.email, passwordScheme: u.passwordScheme } : null;
24
+ }
25
+ async create(input) {
26
+ const id = (this.users.size + 1).toString();
27
+ this.users.set(id, { id, email: input.email, passwordScheme: input.passwordScheme, password: input.password });
28
+ return { id, email: input.email, passwordScheme: input.passwordScheme };
29
+ }
30
+ async verifyCredentials(email, rawPasswordOrHash) {
31
+ const user = await this.findByEmail(email);
32
+ if (!user)
33
+ throw new Error('not found');
34
+ const stored = Array.from(this.users.values()).find((u) => u.id === user.id);
35
+ if (!stored || stored.password !== rawPasswordOrHash) {
36
+ throw new Error('invalid credentials');
37
+ }
38
+ }
39
+ async upgradeToClientHash(userId, clientHash) {
40
+ const u = this.users.get(userId);
41
+ if (u) {
42
+ u.password = clientHash;
43
+ u.passwordScheme = 'sha256';
44
+ }
45
+ }
46
+ async setPasswordPlain(userId, plainPassword) {
47
+ const u = this.users.get(userId);
48
+ if (u) {
49
+ u.password = plainPassword;
50
+ u.passwordScheme = 'plain';
51
+ }
52
+ }
53
+ async updatePreferredLanguage() {
54
+ // no-op for tests
55
+ }
56
+ async createPasswordReset() {
57
+ return {
58
+ id: '1',
59
+ userId: '1',
60
+ token: 'token',
61
+ expiresAt: new Date(),
62
+ usedAt: null
63
+ };
64
+ }
65
+ async findValidPasswordReset() {
66
+ return null;
67
+ }
68
+ async markPasswordResetUsed() {
69
+ // no-op
70
+ }
71
+ }
72
+ class FakeEmailService {
73
+ constructor() {
74
+ this.sent = [];
75
+ }
76
+ async sendPasswordResetEmail(email, resetLink) {
77
+ this.sent.push({ email, link: resetLink });
78
+ }
79
+ }
80
+ describe('AuthService', () => {
81
+ const users = new FakeUsersService();
82
+ const email = new FakeEmailService();
83
+ const jwt = new jwt_1.JwtService({ secret: 'test-secret' });
84
+ const service = new auth_service_1.AuthService(
85
+ // Inject via tokens in real Nest app; here we pass directly
86
+ users, email, jwt);
87
+ it('signs up new user with clientHash', async () => {
88
+ const token = await service.signup({
89
+ email: 'john@example.com',
90
+ clientHash: 'hash',
91
+ password: undefined
92
+ });
93
+ expect(typeof token.accessToken).toBe('string');
94
+ });
95
+ it('prevents duplicate email signup', async () => {
96
+ await expect(service.signup({
97
+ email: 'john@example.com',
98
+ clientHash: 'hash2',
99
+ password: undefined
100
+ })).rejects.toBeInstanceOf(auth_service_1.ApiError);
101
+ });
102
+ it('requires credentials on login', async () => {
103
+ await expect(service.login({ email: 'unknown@example.com' })).rejects.toBeInstanceOf(auth_service_1.ApiError);
104
+ });
105
+ });
@@ -0,0 +1,59 @@
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
+ export type PasswordResetRecord = {
12
+ id: string;
13
+ userId: string;
14
+ token: string;
15
+ expiresAt: Date;
16
+ usedAt: Date | null;
17
+ };
18
+ /** Payload passed to {@link AuthUsersServiceContract.onSignupComplete} after the user row exists. */
19
+ export type SignupFollowUpPayload = {
20
+ userType?: 'customer' | 'company';
21
+ firstName?: string;
22
+ lastName?: string;
23
+ };
24
+ export interface AuthUsersServiceContract {
25
+ findByEmail(email: string): Promise<AuthUserProfile | null>;
26
+ findById(id: string): Promise<AuthUserProfile | null>;
27
+ create(input: {
28
+ email: string;
29
+ password: string;
30
+ passwordScheme: 'sha256' | 'plain';
31
+ firstName?: string | null;
32
+ lastName?: string | null;
33
+ phoneCountryCode?: string | null;
34
+ phoneNumber?: string | null;
35
+ }): Promise<AuthUserProfile>;
36
+ verifyCredentials(email: string, rawPasswordOrHash: string): Promise<void>;
37
+ upgradeToClientHash(userId: string, clientHash: string): Promise<void>;
38
+ setPasswordPlain(userId: string, plainPassword: string): Promise<void>;
39
+ updatePreferredLanguage(userId: string, preferredLanguage: string): Promise<void>;
40
+ createPasswordReset(userId: string, token: string, expiresAt: Date): Promise<PasswordResetRecord>;
41
+ findValidPasswordReset(token: string): Promise<PasswordResetRecord | null>;
42
+ markPasswordResetUsed(resetId: string): Promise<void>;
43
+ /**
44
+ * Optional: create domain rows after signup (e.g. CustomerUser / CompanyUser).
45
+ * Implement in the host app when using `userType` on signup.
46
+ */
47
+ onSignupComplete?(userId: string, payload: SignupFollowUpPayload): Promise<void>;
48
+ /**
49
+ * Optional: resolve customer vs company flags for `/auth/me`.
50
+ * If omitted, both default to `false` (admin still uses `ADMIN_EMAILS`).
51
+ */
52
+ getProfileRoles?(userId: string): Promise<{
53
+ isCustomer: boolean;
54
+ isCompany: boolean;
55
+ }>;
56
+ }
57
+ export interface AuthEmailServiceContract {
58
+ sendPasswordResetEmail(email: string, resetLink: string): Promise<void>;
59
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ export declare class ChangePasswordDto {
2
+ currentPassword?: string;
3
+ currentClientHash?: string;
4
+ newPassword?: string;
5
+ newClientHash?: string;
6
+ }