@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,38 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ChangePasswordDto = void 0;
13
+ const class_validator_1 = require("class-validator");
14
+ class ChangePasswordDto {
15
+ }
16
+ exports.ChangePasswordDto = ChangePasswordDto;
17
+ __decorate([
18
+ (0, class_validator_1.IsOptional)(),
19
+ (0, class_validator_1.IsString)(),
20
+ (0, class_validator_1.MinLength)(6),
21
+ __metadata("design:type", String)
22
+ ], ChangePasswordDto.prototype, "currentPassword", void 0);
23
+ __decorate([
24
+ (0, class_validator_1.IsOptional)(),
25
+ (0, class_validator_1.IsString)(),
26
+ __metadata("design:type", String)
27
+ ], ChangePasswordDto.prototype, "currentClientHash", void 0);
28
+ __decorate([
29
+ (0, class_validator_1.IsOptional)(),
30
+ (0, class_validator_1.IsString)(),
31
+ (0, class_validator_1.MinLength)(6),
32
+ __metadata("design:type", String)
33
+ ], ChangePasswordDto.prototype, "newPassword", void 0);
34
+ __decorate([
35
+ (0, class_validator_1.IsOptional)(),
36
+ (0, class_validator_1.IsString)(),
37
+ __metadata("design:type", String)
38
+ ], ChangePasswordDto.prototype, "newClientHash", void 0);
@@ -0,0 +1,3 @@
1
+ export declare class ForgotPasswordDto {
2
+ email: string;
3
+ }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ForgotPasswordDto = void 0;
13
+ const class_validator_1 = require("class-validator");
14
+ class ForgotPasswordDto {
15
+ }
16
+ exports.ForgotPasswordDto = ForgotPasswordDto;
17
+ __decorate([
18
+ (0, class_validator_1.IsEmail)(),
19
+ __metadata("design:type", String)
20
+ ], ForgotPasswordDto.prototype, "email", void 0);
@@ -0,0 +1,5 @@
1
+ export declare class LoginDto {
2
+ email: string;
3
+ password?: string;
4
+ clientHash?: string;
5
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.LoginDto = void 0;
13
+ const class_validator_1 = require("class-validator");
14
+ class LoginDto {
15
+ }
16
+ exports.LoginDto = LoginDto;
17
+ __decorate([
18
+ (0, class_validator_1.IsEmail)(),
19
+ __metadata("design:type", String)
20
+ ], LoginDto.prototype, "email", void 0);
21
+ __decorate([
22
+ (0, class_validator_1.IsOptional)(),
23
+ (0, class_validator_1.IsString)(),
24
+ (0, class_validator_1.MinLength)(6),
25
+ __metadata("design:type", String)
26
+ ], LoginDto.prototype, "password", void 0);
27
+ __decorate([
28
+ (0, class_validator_1.IsOptional)(),
29
+ (0, class_validator_1.IsString)(),
30
+ __metadata("design:type", String)
31
+ ], LoginDto.prototype, "clientHash", void 0);
@@ -0,0 +1,5 @@
1
+ export declare class ResetPasswordDto {
2
+ token: string;
3
+ password?: string;
4
+ clientHash?: string;
5
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ResetPasswordDto = void 0;
13
+ const class_validator_1 = require("class-validator");
14
+ class ResetPasswordDto {
15
+ }
16
+ exports.ResetPasswordDto = ResetPasswordDto;
17
+ __decorate([
18
+ (0, class_validator_1.IsString)(),
19
+ __metadata("design:type", String)
20
+ ], ResetPasswordDto.prototype, "token", void 0);
21
+ __decorate([
22
+ (0, class_validator_1.IsOptional)(),
23
+ (0, class_validator_1.IsString)(),
24
+ (0, class_validator_1.MinLength)(6),
25
+ __metadata("design:type", String)
26
+ ], ResetPasswordDto.prototype, "password", void 0);
27
+ __decorate([
28
+ (0, class_validator_1.IsOptional)(),
29
+ (0, class_validator_1.IsString)(),
30
+ __metadata("design:type", String)
31
+ ], ResetPasswordDto.prototype, "clientHash", void 0);
@@ -0,0 +1,11 @@
1
+ export declare class SignupDto {
2
+ email: string;
3
+ password?: string;
4
+ clientHash?: string;
5
+ firstName?: string;
6
+ lastName?: string;
7
+ phoneCountryCode?: string;
8
+ phoneNumber?: string;
9
+ /** When set, the host app should link customer/company profile rows via `AuthUsersServiceContract.onSignupComplete`. */
10
+ userType?: 'customer' | 'company';
11
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.SignupDto = void 0;
13
+ const class_validator_1 = require("class-validator");
14
+ class SignupDto {
15
+ }
16
+ exports.SignupDto = SignupDto;
17
+ __decorate([
18
+ (0, class_validator_1.IsEmail)(),
19
+ __metadata("design:type", String)
20
+ ], SignupDto.prototype, "email", void 0);
21
+ __decorate([
22
+ (0, class_validator_1.IsOptional)(),
23
+ (0, class_validator_1.IsString)(),
24
+ (0, class_validator_1.MinLength)(6),
25
+ __metadata("design:type", String)
26
+ ], SignupDto.prototype, "password", void 0);
27
+ __decorate([
28
+ (0, class_validator_1.IsOptional)(),
29
+ (0, class_validator_1.IsString)(),
30
+ __metadata("design:type", String)
31
+ ], SignupDto.prototype, "clientHash", void 0);
32
+ __decorate([
33
+ (0, class_validator_1.IsOptional)(),
34
+ (0, class_validator_1.IsString)(),
35
+ __metadata("design:type", String)
36
+ ], SignupDto.prototype, "firstName", void 0);
37
+ __decorate([
38
+ (0, class_validator_1.IsOptional)(),
39
+ (0, class_validator_1.IsString)(),
40
+ __metadata("design:type", String)
41
+ ], SignupDto.prototype, "lastName", void 0);
42
+ __decorate([
43
+ (0, class_validator_1.IsOptional)(),
44
+ (0, class_validator_1.IsString)(),
45
+ __metadata("design:type", String)
46
+ ], SignupDto.prototype, "phoneCountryCode", void 0);
47
+ __decorate([
48
+ (0, class_validator_1.IsOptional)(),
49
+ (0, class_validator_1.IsString)(),
50
+ __metadata("design:type", String)
51
+ ], SignupDto.prototype, "phoneNumber", void 0);
52
+ __decorate([
53
+ (0, class_validator_1.IsOptional)(),
54
+ (0, class_validator_1.IsIn)(['customer', 'company']),
55
+ __metadata("design:type", String)
56
+ ], SignupDto.prototype, "userType", void 0);
@@ -0,0 +1,12 @@
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';
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./auth.module"), exports);
18
+ __exportStar(require("./auth.service"), exports);
19
+ __exportStar(require("./auth.controller"), exports);
20
+ __exportStar(require("./jwt.strategy"), exports);
21
+ __exportStar(require("./jwt-optional.guard"), exports);
22
+ __exportStar(require("./contracts"), exports);
23
+ __exportStar(require("./tokens"), exports);
24
+ __exportStar(require("./dto/signup.dto"), exports);
25
+ __exportStar(require("./dto/login.dto"), exports);
26
+ __exportStar(require("./dto/forgot-password.dto"), exports);
27
+ __exportStar(require("./dto/reset-password.dto"), exports);
28
+ __exportStar(require("./dto/change-password.dto"), exports);
@@ -0,0 +1,7 @@
1
+ import { ExecutionContext } from '@nestjs/common';
2
+ declare const JwtOptionalGuard_base: import("@nestjs/passport").Type<import("@nestjs/passport").IAuthGuard>;
3
+ export declare class JwtOptionalGuard extends JwtOptionalGuard_base {
4
+ handleRequest<TUser = unknown>(err: unknown, user: unknown, _info: unknown, _context: ExecutionContext, _status?: unknown): TUser;
5
+ canActivate(context: ExecutionContext): boolean | Promise<boolean> | import("rxjs").Observable<boolean>;
6
+ }
7
+ export {};
@@ -0,0 +1,31 @@
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.JwtOptionalGuard = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const passport_1 = require("@nestjs/passport");
12
+ let JwtOptionalGuard = class JwtOptionalGuard extends (0, passport_1.AuthGuard)('jwt') {
13
+ handleRequest(err, user, _info, _context, _status) {
14
+ if (err) {
15
+ return null;
16
+ }
17
+ return (user ?? null);
18
+ }
19
+ canActivate(context) {
20
+ const request = context.switchToHttp().getRequest();
21
+ const authHeader = request.headers['authorization'];
22
+ if (!authHeader) {
23
+ return true;
24
+ }
25
+ return super.canActivate(context);
26
+ }
27
+ };
28
+ exports.JwtOptionalGuard = JwtOptionalGuard;
29
+ exports.JwtOptionalGuard = JwtOptionalGuard = __decorate([
30
+ (0, common_1.Injectable)()
31
+ ], JwtOptionalGuard);
@@ -0,0 +1,13 @@
1
+ import { Strategy } from 'passport-jwt';
2
+ declare const JwtStrategy_base: new (...args: any[]) => Strategy;
3
+ export declare class JwtStrategy extends JwtStrategy_base {
4
+ constructor();
5
+ validate(payload: {
6
+ sub: string;
7
+ email: string;
8
+ }): Promise<{
9
+ userId: string;
10
+ email: string;
11
+ }>;
12
+ }
13
+ export {};
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.JwtStrategy = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const passport_1 = require("@nestjs/passport");
15
+ const passport_jwt_1 = require("passport-jwt");
16
+ let JwtStrategy = class JwtStrategy extends (0, passport_1.PassportStrategy)(passport_jwt_1.Strategy) {
17
+ constructor() {
18
+ super({
19
+ jwtFromRequest: passport_jwt_1.ExtractJwt.fromAuthHeaderAsBearerToken(),
20
+ ignoreExpiration: false,
21
+ secretOrKey: process.env.JWT_SECRET || 'dev-secret'
22
+ });
23
+ }
24
+ async validate(payload) {
25
+ return { userId: payload.sub, email: payload.email };
26
+ }
27
+ };
28
+ exports.JwtStrategy = JwtStrategy;
29
+ exports.JwtStrategy = JwtStrategy = __decorate([
30
+ (0, common_1.Injectable)(),
31
+ __metadata("design:paramtypes", [])
32
+ ], JwtStrategy);
@@ -0,0 +1,2 @@
1
+ export declare const AUTH_USERS_SERVICE: unique symbol;
2
+ export declare const AUTH_EMAIL_SERVICE: unique symbol;
package/dist/tokens.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AUTH_EMAIL_SERVICE = exports.AUTH_USERS_SERVICE = void 0;
4
+ exports.AUTH_USERS_SERVICE = Symbol('AUTH_USERS_SERVICE');
5
+ exports.AUTH_EMAIL_SERVICE = Symbol('AUTH_EMAIL_SERVICE');
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/src'],
5
+ moduleFileExtensions: ['ts', 'js', 'json'],
6
+ transform: {
7
+ '^.+\\.ts$': 'ts-jest'
8
+ }
9
+ };
10
+
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@saulo.martins/backend-auth",
3
+ "version": "1.0.2",
4
+ "private": false,
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.json",
9
+ "test": "jest"
10
+ },
11
+ "dependencies": {
12
+ "@nestjs/common": "^10.4.15",
13
+ "@nestjs/jwt": "^10.2.0",
14
+ "@nestjs/passport": "^10.0.3",
15
+ "passport": "^0.7.0",
16
+ "passport-jwt": "^4.0.1",
17
+ "reflect-metadata": "^0.2.2",
18
+ "rxjs": "^7.8.1"
19
+ },
20
+ "devDependencies": {
21
+ "@nestjs/testing": "^10.4.15",
22
+ "@types/jest": "^29.5.14",
23
+ "@types/node": "^22.10.5",
24
+ "@types/passport-jwt": "^4.0.1",
25
+ "jest": "^29.7.0",
26
+ "ts-jest": "^29.2.5",
27
+ "typescript": "^5.7.3"
28
+ },
29
+ "peerDependencies": {
30
+ "@nestjs/core": "^10.4.15"
31
+ }
32
+ }
33
+
@@ -0,0 +1,77 @@
1
+ import { Body, Controller, Get, Patch, Post, Req, UseGuards } from '@nestjs/common';
2
+ import { AuthGuard } from '@nestjs/passport';
3
+ import { Request } from 'express';
4
+ import { AuthService } from './auth.service';
5
+ import { SignupDto } from './dto/signup.dto';
6
+ import { LoginDto } from './dto/login.dto';
7
+ import { ChangePasswordDto } from './dto/change-password.dto';
8
+ import { ForgotPasswordDto } from './dto/forgot-password.dto';
9
+ import { ResetPasswordDto } from './dto/reset-password.dto';
10
+
11
+ interface JwtUser {
12
+ userId: string;
13
+ email: string;
14
+ }
15
+
16
+ @Controller('auth')
17
+ export class AuthController {
18
+ constructor(private readonly authService: AuthService) {}
19
+
20
+ @Post('signup')
21
+ signup(@Body() dto: SignupDto) {
22
+ return this.authService.signup(dto);
23
+ }
24
+
25
+ @Post('login')
26
+ login(@Body() dto: LoginDto) {
27
+ return this.authService.login(dto);
28
+ }
29
+
30
+ @Get('me')
31
+ @UseGuards(AuthGuard('jwt'))
32
+ getMe(@Req() req: Request & { user: JwtUser }) {
33
+ return this.authService.getMe(req.user.userId);
34
+ }
35
+
36
+ @Patch('me')
37
+ @UseGuards(AuthGuard('jwt'))
38
+ async patchMe(
39
+ @Req() req: Request & { user: JwtUser },
40
+ @Body() body: { preferredLanguage?: string }
41
+ ) {
42
+ if (body.preferredLanguage != null) {
43
+ await this.authService.updatePreferredLanguage(
44
+ req.user.userId,
45
+ body.preferredLanguage
46
+ );
47
+ return { preferredLanguage: body.preferredLanguage };
48
+ }
49
+ }
50
+
51
+ @Patch('change-password')
52
+ @UseGuards(AuthGuard('jwt'))
53
+ changePassword(
54
+ @Req() req: Request & { user: JwtUser },
55
+ @Body() dto: ChangePasswordDto
56
+ ) {
57
+ return this.authService.changePassword(req.user.userId, dto);
58
+ }
59
+
60
+ @Post('forgot-password')
61
+ forgotPassword(
62
+ @Body() dto: ForgotPasswordDto,
63
+ @Req() req: Request
64
+ ) {
65
+ // The caller should set X-Frontend-Base-Url if necessary; otherwise this can be overridden at service level.
66
+ const headerBase =
67
+ (req.headers['x-frontend-base-url'] as string | undefined) ?? '';
68
+ const base = headerBase || '';
69
+ return this.authService.forgotPassword(dto.email, base);
70
+ }
71
+
72
+ @Post('reset-password')
73
+ resetPassword(@Body() dto: ResetPasswordDto) {
74
+ return this.authService.resetPassword(dto);
75
+ }
76
+ }
77
+
@@ -0,0 +1,35 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { JwtModule } from '@nestjs/jwt';
3
+ import { PassportModule } from '@nestjs/passport';
4
+ import { AuthService } from './auth.service';
5
+ import { AuthController } from './auth.controller';
6
+ import { JwtStrategy } from './jwt.strategy';
7
+ import { AUTH_EMAIL_SERVICE, AUTH_USERS_SERVICE } from './tokens';
8
+ import { AuthEmailServiceContract, AuthUsersServiceContract } from './contracts';
9
+
10
+ @Module({
11
+ imports: [
12
+ PassportModule,
13
+ JwtModule.register({
14
+ global: true,
15
+ secret: process.env.JWT_SECRET || 'dev-secret',
16
+ signOptions: { expiresIn: '1h' }
17
+ })
18
+ ],
19
+ providers: [
20
+ AuthService,
21
+ JwtStrategy,
22
+ {
23
+ provide: AUTH_USERS_SERVICE,
24
+ useValue: null as unknown as AuthUsersServiceContract
25
+ },
26
+ {
27
+ provide: AUTH_EMAIL_SERVICE,
28
+ useValue: null as unknown as AuthEmailServiceContract
29
+ }
30
+ ],
31
+ controllers: [AuthController],
32
+ exports: [AuthService, AUTH_USERS_SERVICE, AUTH_EMAIL_SERVICE]
33
+ })
34
+ export class AuthModule {}
35
+
@@ -0,0 +1,129 @@
1
+ import { JwtService } from '@nestjs/jwt';
2
+ import { AuthService, ApiError } from './auth.service';
3
+ import { AUTH_EMAIL_SERVICE, AUTH_USERS_SERVICE } from './tokens';
4
+ import { AuthEmailServiceContract, AuthUsersServiceContract } from './contracts';
5
+
6
+ class FakeUsersService implements AuthUsersServiceContract {
7
+ private users = new Map<string, { id: string; email: string; passwordScheme: 'sha256' | 'plain'; password: string }>();
8
+
9
+ async findByEmail(email: string) {
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
+
22
+ async findById(id: string) {
23
+ const u = this.users.get(id);
24
+ return u ? { id: u.id, email: u.email, passwordScheme: u.passwordScheme } : null;
25
+ }
26
+
27
+ async create(input: {
28
+ email: string;
29
+ password: string;
30
+ passwordScheme: 'sha256' | 'plain';
31
+ }) {
32
+ const id = (this.users.size + 1).toString();
33
+ this.users.set(id, { id, email: input.email, passwordScheme: input.passwordScheme, password: input.password });
34
+ return { id, email: input.email, passwordScheme: input.passwordScheme };
35
+ }
36
+
37
+ async verifyCredentials(email: string, rawPasswordOrHash: string): Promise<void> {
38
+ const user = await this.findByEmail(email);
39
+ if (!user) throw new Error('not found');
40
+ const stored = Array.from(this.users.values()).find((u) => u.id === user.id);
41
+ if (!stored || stored.password !== rawPasswordOrHash) {
42
+ throw new Error('invalid credentials');
43
+ }
44
+ }
45
+
46
+ async upgradeToClientHash(userId: string, clientHash: string): Promise<void> {
47
+ const u = this.users.get(userId);
48
+ if (u) {
49
+ u.password = clientHash;
50
+ u.passwordScheme = 'sha256';
51
+ }
52
+ }
53
+
54
+ async setPasswordPlain(userId: string, plainPassword: string): Promise<void> {
55
+ const u = this.users.get(userId);
56
+ if (u) {
57
+ u.password = plainPassword;
58
+ u.passwordScheme = 'plain';
59
+ }
60
+ }
61
+
62
+ async updatePreferredLanguage(): Promise<void> {
63
+ // no-op for tests
64
+ }
65
+
66
+ async createPasswordReset() {
67
+ return {
68
+ id: '1',
69
+ userId: '1',
70
+ token: 'token',
71
+ expiresAt: new Date(),
72
+ usedAt: null
73
+ };
74
+ }
75
+
76
+ async findValidPasswordReset() {
77
+ return null;
78
+ }
79
+
80
+ async markPasswordResetUsed(): Promise<void> {
81
+ // no-op
82
+ }
83
+ }
84
+
85
+ class FakeEmailService implements AuthEmailServiceContract {
86
+ public sent: { email: string; link: string }[] = [];
87
+ async sendPasswordResetEmail(email: string, resetLink: string): Promise<void> {
88
+ this.sent.push({ email, link: resetLink });
89
+ }
90
+ }
91
+
92
+ describe('AuthService', () => {
93
+ const users = new FakeUsersService();
94
+ const email = new FakeEmailService();
95
+ const jwt = new JwtService({ secret: 'test-secret' });
96
+ const service = new AuthService(
97
+ // Inject via tokens in real Nest app; here we pass directly
98
+ (users as unknown) as any,
99
+ (email as unknown) as any,
100
+ jwt
101
+ );
102
+
103
+ it('signs up new user with clientHash', async () => {
104
+ const token = await service.signup({
105
+ email: 'john@example.com',
106
+ clientHash: 'hash',
107
+ password: undefined
108
+ } as any);
109
+
110
+ expect(typeof token.accessToken).toBe('string');
111
+ });
112
+
113
+ it('prevents duplicate email signup', async () => {
114
+ await expect(
115
+ service.signup({
116
+ email: 'john@example.com',
117
+ clientHash: 'hash2',
118
+ password: undefined
119
+ } as any)
120
+ ).rejects.toBeInstanceOf(ApiError);
121
+ });
122
+
123
+ it('requires credentials on login', async () => {
124
+ await expect(
125
+ service.login({ email: 'unknown@example.com' } as any)
126
+ ).rejects.toBeInstanceOf(ApiError);
127
+ });
128
+ });
129
+