@vynelix/nestjs-multi-auth 0.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/README.md +760 -0
- package/bin/cli.js +152 -0
- package/dist/auth/auth-type.enum.d.ts +22 -0
- package/dist/auth/auth-type.enum.js +32 -0
- package/dist/auth/auth-type.enum.js.map +1 -0
- package/dist/auth/auth.controller.d.ts +70 -0
- package/dist/auth/auth.controller.js +434 -0
- package/dist/auth/auth.controller.js.map +1 -0
- package/dist/auth/auth.module.d.ts +9 -0
- package/dist/auth/auth.module.js +193 -0
- package/dist/auth/auth.module.js.map +1 -0
- package/dist/auth/auth.service.d.ts +180 -0
- package/dist/auth/auth.service.js +695 -0
- package/dist/auth/auth.service.js.map +1 -0
- package/dist/auth/core/auth-mapper.d.ts +6 -0
- package/dist/auth/core/auth-mapper.js +17 -0
- package/dist/auth/core/auth-mapper.js.map +1 -0
- package/dist/auth/core/jwt.strategy.d.ts +13 -0
- package/dist/auth/core/jwt.strategy.js +59 -0
- package/dist/auth/core/jwt.strategy.js.map +1 -0
- package/dist/auth/core/registration.d.ts +3 -0
- package/dist/auth/core/registration.js +60 -0
- package/dist/auth/core/registration.js.map +1 -0
- package/dist/auth/current-user-interface.d.ts +4 -0
- package/dist/auth/current-user-interface.js +4 -0
- package/dist/auth/current-user-interface.js.map +1 -0
- package/dist/auth/current-user.decorator.d.ts +2 -0
- package/dist/auth/current-user.decorator.js +16 -0
- package/dist/auth/current-user.decorator.js.map +1 -0
- package/dist/auth/decorator/current-user.decorator.d.ts +2 -0
- package/dist/auth/decorator/current-user.decorator.js +13 -0
- package/dist/auth/decorator/current-user.decorator.js.map +1 -0
- package/dist/auth/decorator/email-or-phone.decorator.d.ts +2 -0
- package/dist/auth/decorator/email-or-phone.decorator.js +26 -0
- package/dist/auth/decorator/email-or-phone.decorator.js.map +1 -0
- package/dist/auth/decorator/optional.decorator.d.ts +2 -0
- package/dist/auth/decorator/optional.decorator.js +9 -0
- package/dist/auth/decorator/optional.decorator.js.map +1 -0
- package/dist/auth/decorator/public.decorator.d.ts +2 -0
- package/dist/auth/decorator/public.decorator.js +9 -0
- package/dist/auth/decorator/public.decorator.js.map +1 -0
- package/dist/auth/dto/auth-response.dto.d.ts +17 -0
- package/dist/auth/dto/auth-response.dto.js +83 -0
- package/dist/auth/dto/auth-response.dto.js.map +1 -0
- package/dist/auth/dto/identifier-response.dto.d.ts +8 -0
- package/dist/auth/dto/identifier-response.dto.js +46 -0
- package/dist/auth/dto/identifier-response.dto.js.map +1 -0
- package/dist/auth/dto/login.dto.d.ts +11 -0
- package/dist/auth/dto/login.dto.js +68 -0
- package/dist/auth/dto/login.dto.js.map +1 -0
- package/dist/auth/dto/mfa.dto.d.ts +8 -0
- package/dist/auth/dto/mfa.dto.js +51 -0
- package/dist/auth/dto/mfa.dto.js.map +1 -0
- package/dist/auth/dto/oauth-provider-response.dto.d.ts +5 -0
- package/dist/auth/dto/oauth-provider-response.dto.js +30 -0
- package/dist/auth/dto/oauth-provider-response.dto.js.map +1 -0
- package/dist/auth/dto/refresh-token.dto.d.ts +3 -0
- package/dist/auth/dto/refresh-token.dto.js +28 -0
- package/dist/auth/dto/refresh-token.dto.js.map +1 -0
- package/dist/auth/dto/requests/forgot-password.dto.d.ts +5 -0
- package/dist/auth/dto/requests/forgot-password.dto.js +36 -0
- package/dist/auth/dto/requests/forgot-password.dto.js.map +1 -0
- package/dist/auth/dto/requests/login.dto.d.ts +11 -0
- package/dist/auth/dto/requests/login.dto.js +68 -0
- package/dist/auth/dto/requests/login.dto.js.map +1 -0
- package/dist/auth/dto/requests/magic-link.dto.d.ts +6 -0
- package/dist/auth/dto/requests/magic-link.dto.js +33 -0
- package/dist/auth/dto/requests/magic-link.dto.js.map +1 -0
- package/dist/auth/dto/requests/mfa.dto.d.ts +8 -0
- package/dist/auth/dto/requests/mfa.dto.js +51 -0
- package/dist/auth/dto/requests/mfa.dto.js.map +1 -0
- package/dist/auth/dto/requests/refresh-token.dto.d.ts +3 -0
- package/dist/auth/dto/requests/refresh-token.dto.js +29 -0
- package/dist/auth/dto/requests/refresh-token.dto.js.map +1 -0
- package/dist/auth/dto/requests/reset-password.dto.d.ts +5 -0
- package/dist/auth/dto/requests/reset-password.dto.js +36 -0
- package/dist/auth/dto/requests/reset-password.dto.js.map +1 -0
- package/dist/auth/dto/requests/secure-account.dto.d.ts +3 -0
- package/dist/auth/dto/requests/secure-account.dto.js +24 -0
- package/dist/auth/dto/requests/secure-account.dto.js.map +1 -0
- package/dist/auth/dto/requests/signup.dto.d.ts +13 -0
- package/dist/auth/dto/requests/signup.dto.js +70 -0
- package/dist/auth/dto/requests/signup.dto.js.map +1 -0
- package/dist/auth/dto/requests/update-password.dto.d.ts +4 -0
- package/dist/auth/dto/requests/update-password.dto.js +30 -0
- package/dist/auth/dto/requests/update-password.dto.js.map +1 -0
- package/dist/auth/dto/requests/verify.dto.d.ts +7 -0
- package/dist/auth/dto/requests/verify.dto.js +48 -0
- package/dist/auth/dto/requests/verify.dto.js.map +1 -0
- package/dist/auth/dto/responses/auth-response.dto.d.ts +17 -0
- package/dist/auth/dto/responses/auth-response.dto.js +83 -0
- package/dist/auth/dto/responses/auth-response.dto.js.map +1 -0
- package/dist/auth/dto/responses/identifier-response.dto.d.ts +10 -0
- package/dist/auth/dto/responses/identifier-response.dto.js +56 -0
- package/dist/auth/dto/responses/identifier-response.dto.js.map +1 -0
- package/dist/auth/dto/responses/oauth-provider-response.dto.d.ts +10 -0
- package/dist/auth/dto/responses/oauth-provider-response.dto.js +55 -0
- package/dist/auth/dto/responses/oauth-provider-response.dto.js.map +1 -0
- package/dist/auth/dto/signup.dto.d.ts +13 -0
- package/dist/auth/dto/signup.dto.js +70 -0
- package/dist/auth/dto/signup.dto.js.map +1 -0
- package/dist/auth/dto/verify.dto.d.ts +7 -0
- package/dist/auth/dto/verify.dto.js +48 -0
- package/dist/auth/dto/verify.dto.js.map +1 -0
- package/dist/auth/entities/auth-identify.entity.d.ts +33 -0
- package/dist/auth/entities/auth-identify.entity.js +73 -0
- package/dist/auth/entities/auth-identify.entity.js.map +1 -0
- package/dist/auth/entities/auth.entity.d.ts +45 -0
- package/dist/auth/entities/auth.entity.js +129 -0
- package/dist/auth/entities/auth.entity.js.map +1 -0
- package/dist/auth/entities/base.entity.d.ts +6 -0
- package/dist/auth/entities/base.entity.js +41 -0
- package/dist/auth/entities/base.entity.js.map +1 -0
- package/dist/auth/entities/mfa-method.entity.d.ts +23 -0
- package/dist/auth/entities/mfa-method.entity.js +69 -0
- package/dist/auth/entities/mfa-method.entity.js.map +1 -0
- package/dist/auth/entities/oauth-provider.entity.d.ts +38 -0
- package/dist/auth/entities/oauth-provider.entity.js +88 -0
- package/dist/auth/entities/oauth-provider.entity.js.map +1 -0
- package/dist/auth/entities/otp-token.entity.d.ts +22 -0
- package/dist/auth/entities/otp-token.entity.js +61 -0
- package/dist/auth/entities/otp-token.entity.js.map +1 -0
- package/dist/auth/entities/session.entity.d.ts +19 -0
- package/dist/auth/entities/session.entity.js +65 -0
- package/dist/auth/entities/session.entity.js.map +1 -0
- package/dist/auth/enums/auth-type.enum.d.ts +23 -0
- package/dist/auth/enums/auth-type.enum.js +34 -0
- package/dist/auth/enums/auth-type.enum.js.map +1 -0
- package/dist/auth/guards/jwt-auth.guard.d.ts +10 -0
- package/dist/auth/guards/jwt-auth.guard.js +68 -0
- package/dist/auth/guards/jwt-auth.guard.js.map +1 -0
- package/dist/auth/guards/optional-auth.guard.d.ts +8 -0
- package/dist/auth/guards/optional-auth.guard.js +45 -0
- package/dist/auth/guards/optional-auth.guard.js.map +1 -0
- package/dist/auth/guards/ws-jwt-auth.guard.d.ts +0 -0
- package/dist/auth/guards/ws-jwt-auth.guard.js +31 -0
- package/dist/auth/guards/ws-jwt-auth.guard.js.map +1 -0
- package/dist/auth/interfaces/auth-module-async-options.interface.d.ts +7 -0
- package/dist/auth/interfaces/auth-module-async-options.interface.js +3 -0
- package/dist/auth/interfaces/auth-module-async-options.interface.js.map +1 -0
- package/dist/auth/interfaces/auth-module-options.interface.d.ts +132 -0
- package/dist/auth/interfaces/auth-module-options.interface.js +5 -0
- package/dist/auth/interfaces/auth-module-options.interface.js.map +1 -0
- package/dist/auth/interfaces/auth-notification-provider.interface.d.ts +29 -0
- package/dist/auth/interfaces/auth-notification-provider.interface.js +5 -0
- package/dist/auth/interfaces/auth-notification-provider.interface.js.map +1 -0
- package/dist/auth/interfaces/auth-user-service.interface.d.ts +13 -0
- package/dist/auth/interfaces/auth-user-service.interface.js +7 -0
- package/dist/auth/interfaces/auth-user-service.interface.js.map +1 -0
- package/dist/auth/interfaces/current-user-interface.d.ts +4 -0
- package/dist/auth/interfaces/current-user-interface.js +4 -0
- package/dist/auth/interfaces/current-user-interface.js.map +1 -0
- package/dist/auth/interfaces/current-user.decorator.d.ts +2 -0
- package/dist/auth/interfaces/current-user.decorator.js +16 -0
- package/dist/auth/interfaces/current-user.decorator.js.map +1 -0
- package/dist/auth/interfaces/jwt-payload-interface.d.ts +4 -0
- package/dist/auth/interfaces/jwt-payload-interface.js +3 -0
- package/dist/auth/interfaces/jwt-payload-interface.js.map +1 -0
- package/dist/auth/interfaces/oauth-strategy.interface.d.ts +14 -0
- package/dist/auth/interfaces/oauth-strategy.interface.js +3 -0
- package/dist/auth/interfaces/oauth-strategy.interface.js.map +1 -0
- package/dist/auth/jwt-payload-interface.d.ts +4 -0
- package/dist/auth/jwt-payload-interface.js +3 -0
- package/dist/auth/jwt-payload-interface.js.map +1 -0
- package/dist/auth/jwt.strategy.d.ts +13 -0
- package/dist/auth/jwt.strategy.js +60 -0
- package/dist/auth/jwt.strategy.js.map +1 -0
- package/dist/auth/strategies/company.strategy.d.ts +0 -0
- package/dist/auth/strategies/company.strategy.js +1 -0
- package/dist/auth/strategies/company.strategy.js.map +1 -0
- package/dist/auth/strategies/google.strategy.d.ts +7 -0
- package/dist/auth/strategies/google.strategy.js +23 -0
- package/dist/auth/strategies/google.strategy.js.map +1 -0
- package/dist/auth/strategies/local-auth.strategy.d.ts +24 -0
- package/dist/auth/strategies/local-auth.strategy.js +244 -0
- package/dist/auth/strategies/local-auth.strategy.js.map +1 -0
- package/dist/auth/strategies/oauth/apple.strategy.d.ts +27 -0
- package/dist/auth/strategies/oauth/apple.strategy.js +176 -0
- package/dist/auth/strategies/oauth/apple.strategy.js.map +1 -0
- package/dist/auth/strategies/oauth/facebook.strategy.d.ts +24 -0
- package/dist/auth/strategies/oauth/facebook.strategy.js +150 -0
- package/dist/auth/strategies/oauth/facebook.strategy.js.map +1 -0
- package/dist/auth/strategies/oauth/google.strategy.d.ts +25 -0
- package/dist/auth/strategies/oauth/google.strategy.js +225 -0
- package/dist/auth/strategies/oauth/google.strategy.js.map +1 -0
- package/dist/auth/strategies/oauth/oauth-strategy.interface.d.ts +14 -0
- package/dist/auth/strategies/oauth/oauth-strategy.interface.js +3 -0
- package/dist/auth/strategies/oauth/oauth-strategy.interface.js.map +1 -0
- package/dist/auth/strategies/oauth/oauth.strategy.d.ts +25 -0
- package/dist/auth/strategies/oauth/oauth.strategy.js +70 -0
- package/dist/auth/strategies/oauth/oauth.strategy.js.map +1 -0
- package/dist/auth/strategies/otp.strategy.d.ts +7 -0
- package/dist/auth/strategies/otp.strategy.js +23 -0
- package/dist/auth/strategies/otp.strategy.js.map +1 -0
- package/dist/auth/strategies/password.strategy.d.ts +14 -0
- package/dist/auth/strategies/password.strategy.js +130 -0
- package/dist/auth/strategies/password.strategy.js.map +1 -0
- package/dist/auth/utils/duration.util.d.ts +8 -0
- package/dist/auth/utils/duration.util.js +29 -0
- package/dist/auth/utils/duration.util.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/auth-entities.d.ts +7 -0
- package/dist/migrations/auth-entities.js +18 -0
- package/dist/migrations/auth-entities.js.map +1 -0
- package/dist/migrations/auth-schema.initializer.d.ts +9 -0
- package/dist/migrations/auth-schema.initializer.js +36 -0
- package/dist/migrations/auth-schema.initializer.js.map +1 -0
- package/dist/migrations/auth.migration.v1.d.ts +12 -0
- package/dist/migrations/auth.migration.v1.js +157 -0
- package/dist/migrations/auth.migration.v1.js.map +1 -0
- package/dist/migrations/auth.migration.v2.d.ts +8 -0
- package/dist/migrations/auth.migration.v2.js +39 -0
- package/dist/migrations/auth.migration.v2.js.map +1 -0
- package/dist/migrations/auth.migrations.d.ts +2 -0
- package/dist/migrations/auth.migrations.js +8 -0
- package/dist/migrations/auth.migrations.js.map +1 -0
- package/dist/migrations/migration.runner.d.ts +9 -0
- package/dist/migrations/migration.runner.js +67 -0
- package/dist/migrations/migration.runner.js.map +1 -0
- package/dist/migrations/migration.service.d.ts +9 -0
- package/dist/migrations/migration.service.js +71 -0
- package/dist/migrations/migration.service.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +82 -0
|
@@ -0,0 +1,695 @@
|
|
|
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
|
+
var AuthService_1;
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.AuthService = void 0;
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
const jwt_1 = require("@nestjs/jwt");
|
|
19
|
+
const typeorm_1 = require("@nestjs/typeorm");
|
|
20
|
+
const typeorm_2 = require("typeorm");
|
|
21
|
+
const bcrypt = require("bcrypt");
|
|
22
|
+
const local_auth_strategy_1 = require("./strategies/local-auth.strategy");
|
|
23
|
+
const oauth_strategy_1 = require("./strategies/oauth/oauth.strategy");
|
|
24
|
+
const auth_type_enum_1 = require("./enums/auth-type.enum");
|
|
25
|
+
const auth_entity_1 = require("./entities/auth.entity");
|
|
26
|
+
const session_entity_1 = require("./entities/session.entity");
|
|
27
|
+
const otp_token_entity_1 = require("./entities/otp-token.entity");
|
|
28
|
+
const mfa_method_entity_1 = require("./entities/mfa-method.entity");
|
|
29
|
+
const otplib_1 = require("otplib");
|
|
30
|
+
const auth_module_options_interface_1 = require("./interfaces/auth-module-options.interface");
|
|
31
|
+
const auth_notification_provider_interface_1 = require("./interfaces/auth-notification-provider.interface");
|
|
32
|
+
const crypto = require("crypto");
|
|
33
|
+
const duration_util_1 = require("./utils/duration.util");
|
|
34
|
+
const auth_mapper_1 = require("./core/auth-mapper");
|
|
35
|
+
let AuthService = AuthService_1 = class AuthService {
|
|
36
|
+
constructor(jwtService, passwordStrategy, oauthStrategy, sessionRepository, authRepo, otpRepo, mfaRepo, options, notificationProvider) {
|
|
37
|
+
this.jwtService = jwtService;
|
|
38
|
+
this.passwordStrategy = passwordStrategy;
|
|
39
|
+
this.oauthStrategy = oauthStrategy;
|
|
40
|
+
this.sessionRepository = sessionRepository;
|
|
41
|
+
this.authRepo = authRepo;
|
|
42
|
+
this.otpRepo = otpRepo;
|
|
43
|
+
this.mfaRepo = mfaRepo;
|
|
44
|
+
this.options = options;
|
|
45
|
+
this.notificationProvider = notificationProvider;
|
|
46
|
+
this.logger = new common_1.Logger(AuthService_1.name);
|
|
47
|
+
}
|
|
48
|
+
// --- INTERNAL HELPER: Generate Token Pair ---
|
|
49
|
+
async generateTokens(uid, sessionId) {
|
|
50
|
+
const refreshJti = crypto.randomUUID();
|
|
51
|
+
const [accessToken, refreshToken] = await Promise.all([
|
|
52
|
+
this.jwtService.signAsync({ sub: uid, sessionId }, {
|
|
53
|
+
secret: this.options.jwtSecret || process.env.JWT_SECRET,
|
|
54
|
+
expiresIn: (this.options.accessTokenExpiresIn || '15m'),
|
|
55
|
+
}),
|
|
56
|
+
this.jwtService.signAsync({ sub: uid, sessionId, jti: refreshJti }, {
|
|
57
|
+
secret: this.options.jwtRefreshSecret || process.env.JWT_REFRESH_SECRET,
|
|
58
|
+
expiresIn: (this.options.refreshTokenExpiresIn || '7d'),
|
|
59
|
+
}),
|
|
60
|
+
]);
|
|
61
|
+
return { accessToken, refreshToken };
|
|
62
|
+
}
|
|
63
|
+
fingerprint(userAgent) {
|
|
64
|
+
return crypto.createHash('sha256').update(userAgent).digest('hex');
|
|
65
|
+
}
|
|
66
|
+
// --- INTERNAL HELPER: Create/Update Session in DB ---
|
|
67
|
+
async createSession(uid, userAgent = 'Unknown', ip = 'Unknown') {
|
|
68
|
+
const expiresAt = new Date();
|
|
69
|
+
const durationSeconds = (0, duration_util_1.parseDuration)(this.options.refreshTokenExpiresIn || '7d', 7 * 24 * 60 * 60);
|
|
70
|
+
expiresAt.setSeconds(expiresAt.getSeconds() + durationSeconds);
|
|
71
|
+
const deviceFingerprint = this.fingerprint(userAgent);
|
|
72
|
+
const session = this.sessionRepository.create({
|
|
73
|
+
uid,
|
|
74
|
+
deviceFingerprint,
|
|
75
|
+
ipAddress: ip,
|
|
76
|
+
expiresAt,
|
|
77
|
+
refreshTokenHash: '',
|
|
78
|
+
userAgent,
|
|
79
|
+
});
|
|
80
|
+
await this.sessionRepository.save(session);
|
|
81
|
+
const tokens = await this.generateTokens(uid, session.id);
|
|
82
|
+
session.refreshTokenHash = await bcrypt.hash(tokens.refreshToken, 10);
|
|
83
|
+
await this.sessionRepository.save(session);
|
|
84
|
+
return tokens;
|
|
85
|
+
}
|
|
86
|
+
async signup(dto, uid, userAgent, ip) {
|
|
87
|
+
if (!dto.method)
|
|
88
|
+
throw new common_1.BadRequestException('Method is required');
|
|
89
|
+
const enabledStrategies = this.options.enabledStrategies || Object.values(auth_type_enum_1.AuthStrategy);
|
|
90
|
+
if (!enabledStrategies.includes(dto.method)) {
|
|
91
|
+
throw new common_1.BadRequestException(`Authentication method ${dto.method} is currently disabled.`);
|
|
92
|
+
}
|
|
93
|
+
let auth;
|
|
94
|
+
let identifier;
|
|
95
|
+
switch (dto.method) {
|
|
96
|
+
case auth_type_enum_1.AuthStrategy.EMAIL:
|
|
97
|
+
case auth_type_enum_1.AuthStrategy.PHONE:
|
|
98
|
+
case auth_type_enum_1.AuthStrategy.USERNAME:
|
|
99
|
+
case auth_type_enum_1.AuthStrategy.LOCAL:
|
|
100
|
+
if (!this.passwordStrategy)
|
|
101
|
+
throw new common_1.BadRequestException('Local authentication is not configured.');
|
|
102
|
+
const localResult = await this.passwordStrategy.registerCredentials(dto, uid);
|
|
103
|
+
auth = localResult.auth;
|
|
104
|
+
identifier = localResult.identifier;
|
|
105
|
+
break;
|
|
106
|
+
case auth_type_enum_1.AuthStrategy.GOOGLE:
|
|
107
|
+
case auth_type_enum_1.AuthStrategy.FACEBOOK:
|
|
108
|
+
case auth_type_enum_1.AuthStrategy.APPLE:
|
|
109
|
+
case auth_type_enum_1.AuthStrategy.OAUTH:
|
|
110
|
+
if (!this.oauthStrategy)
|
|
111
|
+
throw new common_1.BadRequestException('OAuth authentication is not configured.');
|
|
112
|
+
const oauthResult = await this.oauthStrategy.registerCredentials(dto, uid);
|
|
113
|
+
auth = oauthResult.auth;
|
|
114
|
+
identifier = oauthResult.identifier;
|
|
115
|
+
break;
|
|
116
|
+
default:
|
|
117
|
+
throw new Error('Unsupported signup provider');
|
|
118
|
+
}
|
|
119
|
+
// Force verification if no password was provided for local strategies (passwordless signup)
|
|
120
|
+
const isPasswordless = [auth_type_enum_1.AuthStrategy.EMAIL, auth_type_enum_1.AuthStrategy.PHONE, auth_type_enum_1.AuthStrategy.USERNAME, auth_type_enum_1.AuthStrategy.LOCAL].includes(dto.method) && !dto.password;
|
|
121
|
+
// Check if user has 2FA enabled
|
|
122
|
+
const mfaMethod = await this.mfaRepo.findOne({ where: { uid: auth.uid, isEnabled: true } });
|
|
123
|
+
const has2FA = !!mfaMethod;
|
|
124
|
+
const triggerVerification = isPasswordless ||
|
|
125
|
+
(this.options.verificationRequired && !identifier?.isVerified) ||
|
|
126
|
+
has2FA;
|
|
127
|
+
const { secretHash, ...filteredAuth } = auth;
|
|
128
|
+
if (triggerVerification) {
|
|
129
|
+
if (this.notificationProvider) {
|
|
130
|
+
if (!identifier?.isVerified || has2FA || isPasswordless) {
|
|
131
|
+
await this.sendVerification(auth, identifier);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
message: isPasswordless ? 'Passwordless signup: Verification code sent.' : 'Signup successful. Please verify your identity.',
|
|
135
|
+
auth: filteredAuth,
|
|
136
|
+
verificationRequired: true
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
else if (isPasswordless) {
|
|
140
|
+
throw new common_1.BadRequestException('A notification provider is required for passwordless signup.');
|
|
141
|
+
}
|
|
142
|
+
else if (this.options.verificationRequired) {
|
|
143
|
+
throw new common_1.BadRequestException('Verification is required but no notification provider is configured.');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const tokens = await this.createSession(auth.uid, userAgent, ip);
|
|
147
|
+
return { ...tokens, auth: filteredAuth };
|
|
148
|
+
}
|
|
149
|
+
async login(dto, userAgent, ip) {
|
|
150
|
+
if (!dto.method)
|
|
151
|
+
throw new common_1.BadRequestException('Method is required');
|
|
152
|
+
const enabledStrategies = this.options.enabledStrategies || Object.values(auth_type_enum_1.AuthStrategy);
|
|
153
|
+
if (!enabledStrategies.includes(dto.method)) {
|
|
154
|
+
throw new common_1.BadRequestException(`Authentication method ${dto.method} is currently disabled.`);
|
|
155
|
+
}
|
|
156
|
+
let auth;
|
|
157
|
+
let identifier;
|
|
158
|
+
switch (dto.method) {
|
|
159
|
+
case auth_type_enum_1.AuthStrategy.EMAIL:
|
|
160
|
+
case auth_type_enum_1.AuthStrategy.PHONE:
|
|
161
|
+
case auth_type_enum_1.AuthStrategy.USERNAME:
|
|
162
|
+
case auth_type_enum_1.AuthStrategy.LOCAL:
|
|
163
|
+
if (!this.passwordStrategy)
|
|
164
|
+
throw new common_1.BadRequestException('Local authentication is not configured.');
|
|
165
|
+
const localResult = await this.passwordStrategy.login(dto);
|
|
166
|
+
auth = localResult.auth;
|
|
167
|
+
identifier = localResult.identifier;
|
|
168
|
+
break;
|
|
169
|
+
case auth_type_enum_1.AuthStrategy.GOOGLE:
|
|
170
|
+
case auth_type_enum_1.AuthStrategy.FACEBOOK:
|
|
171
|
+
case auth_type_enum_1.AuthStrategy.APPLE:
|
|
172
|
+
case auth_type_enum_1.AuthStrategy.OAUTH:
|
|
173
|
+
if (!this.oauthStrategy)
|
|
174
|
+
throw new common_1.BadRequestException('OAuth authentication is not configured.');
|
|
175
|
+
const oauthResult = await this.oauthStrategy.login(dto);
|
|
176
|
+
auth = oauthResult.auth;
|
|
177
|
+
identifier = oauthResult.identifier;
|
|
178
|
+
break;
|
|
179
|
+
default:
|
|
180
|
+
throw new Error('Unsupported login provider');
|
|
181
|
+
}
|
|
182
|
+
if (!auth.isActive) {
|
|
183
|
+
throw new common_1.ForbiddenException('This account is currently locked or disabled. Please contact support or reset your password.');
|
|
184
|
+
}
|
|
185
|
+
// Force verification if no password was provided for local strategies (passwordless login)
|
|
186
|
+
const isPasswordless = [auth_type_enum_1.AuthStrategy.EMAIL, auth_type_enum_1.AuthStrategy.PHONE, auth_type_enum_1.AuthStrategy.USERNAME, auth_type_enum_1.AuthStrategy.LOCAL].includes(dto.method) && !dto.password;
|
|
187
|
+
// Check if user has 2FA enabled
|
|
188
|
+
const mfaMethod = await this.mfaRepo.findOne({ where: { uid: auth.uid, isEnabled: true } });
|
|
189
|
+
const has2FA = !!mfaMethod;
|
|
190
|
+
// Trigger email/phone verification only if required and identifier not verified.
|
|
191
|
+
// MFA (2FA) is handled separately via dedicated endpoints.
|
|
192
|
+
const triggerVerification = isPasswordless ||
|
|
193
|
+
(this.options.verificationRequired && !identifier?.isVerified);
|
|
194
|
+
if (triggerVerification) {
|
|
195
|
+
if (this.notificationProvider) {
|
|
196
|
+
await this.sendVerification(auth, identifier);
|
|
197
|
+
return {
|
|
198
|
+
message: isPasswordless ? 'Passwordless login: Verification code sent.' : 'Identity verification required.',
|
|
199
|
+
auth,
|
|
200
|
+
verificationRequired: true,
|
|
201
|
+
tokens: undefined,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
else if (isPasswordless) {
|
|
205
|
+
throw new common_1.BadRequestException('A notification provider is required for passwordless login.');
|
|
206
|
+
}
|
|
207
|
+
else if (this.options.verificationRequired) {
|
|
208
|
+
throw new common_1.BadRequestException('Verification is required but no notification provider is configured.');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const { secretHash, ...filteredAuth } = auth;
|
|
212
|
+
// If MFA is enabled, inform client that additional MFA verification is required.
|
|
213
|
+
if (has2FA) {
|
|
214
|
+
return {
|
|
215
|
+
message: 'MFA required',
|
|
216
|
+
auth: filteredAuth,
|
|
217
|
+
mfaRequired: true,
|
|
218
|
+
tokens: undefined,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const tokens = await this.createSession(auth.uid, userAgent, ip);
|
|
222
|
+
return { ...tokens, auth: filteredAuth };
|
|
223
|
+
}
|
|
224
|
+
// --- VERIFICATION LOGIC ---
|
|
225
|
+
async sendVerification(auth, currentIdentifier) {
|
|
226
|
+
if (!this.notificationProvider)
|
|
227
|
+
return;
|
|
228
|
+
// 1. Determine primary identifier (email or phone) to send the code to
|
|
229
|
+
// If we have a verified email/phone on the same UID, use that.
|
|
230
|
+
// Otherwise use the current identifier if it's verifiable.
|
|
231
|
+
let primaryIdentifier = currentIdentifier?.type !== 'USERNAME' ? currentIdentifier : null;
|
|
232
|
+
if (!primaryIdentifier) {
|
|
233
|
+
// Look for any EMAIL or PHONE linked to this UID
|
|
234
|
+
const allIdentifiers = await this.authRepo.query(`SELECT ai.* FROM auth_identifiers ai
|
|
235
|
+
JOIN auth a ON ai."authId" = a.id
|
|
236
|
+
WHERE a.uid = $1 AND ai.type IN ('EMAIL', 'PHONE')
|
|
237
|
+
ORDER BY ai."isVerified" DESC, ai."createdAt" ASC LIMIT 1`, [auth.uid]);
|
|
238
|
+
primaryIdentifier = allIdentifiers[0];
|
|
239
|
+
}
|
|
240
|
+
if (!primaryIdentifier) {
|
|
241
|
+
// As a last resort, reload auth with identifiers and find first EMAIL/PHONE
|
|
242
|
+
const fullAuth = await this.authRepo.findOne({
|
|
243
|
+
where: { id: auth.id },
|
|
244
|
+
relations: ['identifiers']
|
|
245
|
+
});
|
|
246
|
+
primaryIdentifier = fullAuth?.identifiers?.find(id => id.type === 'EMAIL' || id.type === 'PHONE');
|
|
247
|
+
}
|
|
248
|
+
if (!primaryIdentifier) {
|
|
249
|
+
this.logger.warn(`No email or phone found for Auth UID: ${auth.uid}. Verification skipped.`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
// 2. Generate 6-digit code
|
|
253
|
+
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
|
254
|
+
const hash = await bcrypt.hash(code, 10);
|
|
255
|
+
// 3. Save OTP Token
|
|
256
|
+
const expiresAt = new Date();
|
|
257
|
+
const otpExpMins = this.options.otpExpiresIn || 15;
|
|
258
|
+
expiresAt.setMinutes(expiresAt.getMinutes() + otpExpMins);
|
|
259
|
+
const otpToken = this.otpRepo.create({
|
|
260
|
+
identifier: primaryIdentifier.value,
|
|
261
|
+
purpose: primaryIdentifier.type === 'EMAIL' ? otp_token_entity_1.OtpPurpose.VERIFY_EMAIL : otp_token_entity_1.OtpPurpose.VERIFY_PHONE,
|
|
262
|
+
codeHash: hash,
|
|
263
|
+
expiresAt,
|
|
264
|
+
requestUserId: auth.uid,
|
|
265
|
+
requestAuthId: auth.id,
|
|
266
|
+
});
|
|
267
|
+
await this.otpRepo.save(otpToken);
|
|
268
|
+
// 4. Send via Provider
|
|
269
|
+
try {
|
|
270
|
+
await this.notificationProvider.sendVerificationCode(primaryIdentifier.value, code, primaryIdentifier.type === 'EMAIL' ? 'email' : 'phone');
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
this.logger.error(`Failed to send verification code to ${primaryIdentifier.value}`, e);
|
|
274
|
+
throw new common_1.BadRequestException('Failed to send verification code');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async verifyCode(uid, code, userAgent, ip) {
|
|
278
|
+
const auth = await this.authRepo.findOne({ where: { uid } });
|
|
279
|
+
if (!auth)
|
|
280
|
+
throw new common_1.BadRequestException('Identity not found');
|
|
281
|
+
// Find the latest unused OTP for this UID
|
|
282
|
+
const otp = await this.otpRepo.findOne({
|
|
283
|
+
where: { requestUserId: uid, isUsed: false },
|
|
284
|
+
order: { createdAt: 'DESC' },
|
|
285
|
+
});
|
|
286
|
+
if (!otp) {
|
|
287
|
+
if (auth.isVerified)
|
|
288
|
+
return { message: 'Identity already verified' };
|
|
289
|
+
throw new common_1.BadRequestException('No verification code found');
|
|
290
|
+
}
|
|
291
|
+
if (new Date() > otp.expiresAt) {
|
|
292
|
+
throw new common_1.BadRequestException('Verification code expired');
|
|
293
|
+
}
|
|
294
|
+
const isMatch = await bcrypt.compare(code, otp.codeHash);
|
|
295
|
+
if (!isMatch)
|
|
296
|
+
throw new common_1.BadRequestException('Invalid verification code');
|
|
297
|
+
// Success!
|
|
298
|
+
otp.isUsed = true;
|
|
299
|
+
await this.otpRepo.save(otp);
|
|
300
|
+
auth.isVerified = true;
|
|
301
|
+
await this.authRepo.save(auth);
|
|
302
|
+
// Also mark all identifiers for this Auth as verified
|
|
303
|
+
await this.authRepo.query(`UPDATE auth_identifiers SET "isVerified" = true WHERE "authId" = $1`, [auth.id]);
|
|
304
|
+
// If there's a requestAuthId on the OTP (which there should be), update that one too if different
|
|
305
|
+
if (otp.requestAuthId && otp.requestAuthId !== auth.id) {
|
|
306
|
+
await this.authRepo.update(otp.requestAuthId, { isVerified: true });
|
|
307
|
+
await this.authRepo.query(`UPDATE auth_identifiers SET "isVerified" = true WHERE "authId" = $1`, [otp.requestAuthId]);
|
|
308
|
+
}
|
|
309
|
+
const tokens = await this.createSession(auth.uid, userAgent, ip);
|
|
310
|
+
return { message: 'Identity verified successfully', tokens, auth };
|
|
311
|
+
}
|
|
312
|
+
async resendVerification(uid) {
|
|
313
|
+
const auth = await this.authRepo.findOne({ where: { uid } });
|
|
314
|
+
if (!auth)
|
|
315
|
+
throw new common_1.BadRequestException('Identity not found');
|
|
316
|
+
if (!this.notificationProvider) {
|
|
317
|
+
throw new common_1.BadRequestException('Verification is not configured');
|
|
318
|
+
}
|
|
319
|
+
// Check resend interval
|
|
320
|
+
const latestOtp = await this.otpRepo.findOne({
|
|
321
|
+
where: { requestUserId: uid },
|
|
322
|
+
order: { createdAt: 'DESC' },
|
|
323
|
+
});
|
|
324
|
+
if (latestOtp) {
|
|
325
|
+
const intervalSeconds = this.options.otpResendInterval || 60;
|
|
326
|
+
const diffMs = Date.now() - latestOtp.createdAt.getTime();
|
|
327
|
+
if (diffMs < intervalSeconds * 1000) {
|
|
328
|
+
const wait = Math.ceil(intervalSeconds - diffMs / 1000);
|
|
329
|
+
throw new common_1.BadRequestException(`Please wait ${wait} seconds before requesting a new code.`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
await this.sendVerification(auth);
|
|
333
|
+
return { message: 'Verification code resent' };
|
|
334
|
+
}
|
|
335
|
+
async refreshTokens(refreshToken, currentUserAgent, currentIp) {
|
|
336
|
+
try {
|
|
337
|
+
const payload = await this.jwtService.verifyAsync(refreshToken, { secret: this.options.jwtRefreshSecret || process.env.JWT_REFRESH_SECRET });
|
|
338
|
+
const session = await this.sessionRepository.findOne({
|
|
339
|
+
where: { id: payload.sessionId },
|
|
340
|
+
select: [
|
|
341
|
+
'id',
|
|
342
|
+
'uid',
|
|
343
|
+
'refreshTokenHash',
|
|
344
|
+
'expiresAt',
|
|
345
|
+
'deviceFingerprint',
|
|
346
|
+
'ipAddress',
|
|
347
|
+
],
|
|
348
|
+
});
|
|
349
|
+
if (!session)
|
|
350
|
+
throw new common_1.ForbiddenException('Session not found');
|
|
351
|
+
const incomingFingerprint = this.fingerprint(currentUserAgent);
|
|
352
|
+
if (session.deviceFingerprint !== incomingFingerprint) {
|
|
353
|
+
await this.sessionRepository.delete(session.id);
|
|
354
|
+
throw new common_1.ForbiddenException('Device mismatch');
|
|
355
|
+
}
|
|
356
|
+
if (new Date() > session.expiresAt) {
|
|
357
|
+
await this.sessionRepository.delete(session.id);
|
|
358
|
+
throw new common_1.ForbiddenException('Session expired');
|
|
359
|
+
}
|
|
360
|
+
const isMatch = await bcrypt.compare(refreshToken, session.refreshTokenHash);
|
|
361
|
+
if (!isMatch) {
|
|
362
|
+
await this.sessionRepository.delete(session.id);
|
|
363
|
+
throw new common_1.ForbiddenException('Invalid refresh token');
|
|
364
|
+
}
|
|
365
|
+
const tokens = await this.generateTokens(session.uid, session.id);
|
|
366
|
+
const newHash = await bcrypt.hash(tokens.refreshToken, 10);
|
|
367
|
+
const newExpiry = new Date();
|
|
368
|
+
const durationSeconds = (0, duration_util_1.parseDuration)(this.options.refreshTokenExpiresIn || '7d', 7 * 24 * 60 * 60);
|
|
369
|
+
newExpiry.setSeconds(newExpiry.getSeconds() + durationSeconds);
|
|
370
|
+
await this.sessionRepository.update(session.id, {
|
|
371
|
+
refreshTokenHash: newHash,
|
|
372
|
+
expiresAt: newExpiry,
|
|
373
|
+
ipAddress: currentIp ?? session.ipAddress,
|
|
374
|
+
});
|
|
375
|
+
return tokens;
|
|
376
|
+
}
|
|
377
|
+
catch (e) {
|
|
378
|
+
this.logger.error('Error refreshing tokens', e);
|
|
379
|
+
throw new common_1.ForbiddenException('Invalid request');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async logout(refreshToken) {
|
|
383
|
+
if (!refreshToken)
|
|
384
|
+
return;
|
|
385
|
+
try {
|
|
386
|
+
const payload = this.jwtService.decode(refreshToken);
|
|
387
|
+
if (payload?.sessionId) {
|
|
388
|
+
await this.sessionRepository.delete(payload.sessionId);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch (e) {
|
|
392
|
+
this.logger.error('Error logging out', e);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// --- PASSWORD MANAGEMENT & SECURITY ---
|
|
396
|
+
async forgotPassword(dto) {
|
|
397
|
+
const value = dto.email || dto.phone || dto.username;
|
|
398
|
+
if (!value)
|
|
399
|
+
throw new common_1.BadRequestException('Identifier is required');
|
|
400
|
+
// 1. Find identifier
|
|
401
|
+
const identifier = await this.authRepo.query(`SELECT ai.*, a.uid, a.id as "authId" FROM auth_identifiers ai
|
|
402
|
+
JOIN auth a ON ai."authId" = a.id
|
|
403
|
+
WHERE ai.value = $1 LIMIT 1`, [value.toLowerCase()]);
|
|
404
|
+
if (!identifier[0]) {
|
|
405
|
+
// Security: Don't reveal if user exists.
|
|
406
|
+
// But typically for forgot-password, users expect an error if email is wrong.
|
|
407
|
+
// We'll return success anyway to prevent enumeration.
|
|
408
|
+
return { message: 'If an account exists, a reset code has been sent.' };
|
|
409
|
+
}
|
|
410
|
+
const primaryAuth = identifier[0];
|
|
411
|
+
// 2. Generate OTP
|
|
412
|
+
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
|
413
|
+
const hash = await bcrypt.hash(code, 10);
|
|
414
|
+
const expiresAt = new Date();
|
|
415
|
+
expiresAt.setMinutes(expiresAt.getMinutes() + (this.options.otpExpiresIn || 15));
|
|
416
|
+
await this.otpRepo.save(this.otpRepo.create({
|
|
417
|
+
identifier: primaryAuth.value,
|
|
418
|
+
purpose: otp_token_entity_1.OtpPurpose.PASSWORD_RESET,
|
|
419
|
+
codeHash: hash,
|
|
420
|
+
expiresAt,
|
|
421
|
+
requestUserId: primaryAuth.uid,
|
|
422
|
+
requestAuthId: primaryAuth.authId,
|
|
423
|
+
}));
|
|
424
|
+
// 3. Send notification
|
|
425
|
+
if (this.notificationProvider) {
|
|
426
|
+
await this.notificationProvider.sendVerificationCode(primaryAuth.value, code, 'email');
|
|
427
|
+
}
|
|
428
|
+
return { message: 'If an account exists, a reset code has been sent.' };
|
|
429
|
+
}
|
|
430
|
+
async resetPassword(dto) {
|
|
431
|
+
const otp = await this.otpRepo.findOne({
|
|
432
|
+
where: { requestUserId: dto.uid, purpose: otp_token_entity_1.OtpPurpose.PASSWORD_RESET, isUsed: false },
|
|
433
|
+
order: { createdAt: 'DESC' },
|
|
434
|
+
});
|
|
435
|
+
if (!otp || new Date() > otp.expiresAt) {
|
|
436
|
+
throw new common_1.BadRequestException('Invalid or expired reset code');
|
|
437
|
+
}
|
|
438
|
+
const isMatch = await bcrypt.compare(dto.code, otp.codeHash);
|
|
439
|
+
if (!isMatch)
|
|
440
|
+
throw new common_1.BadRequestException('Invalid reset code');
|
|
441
|
+
// Update password
|
|
442
|
+
const hash = await bcrypt.hash(dto.newPassword, 10);
|
|
443
|
+
await this.authRepo.update(otp.requestAuthId, {
|
|
444
|
+
secretHash: hash,
|
|
445
|
+
isActive: true // Unlock account on successful reset
|
|
446
|
+
});
|
|
447
|
+
// Mark OTP as used
|
|
448
|
+
otp.isUsed = true;
|
|
449
|
+
await this.otpRepo.save(otp);
|
|
450
|
+
// Security: Invalidate all sessions
|
|
451
|
+
await this.sessionRepository.delete({ uid: dto.uid });
|
|
452
|
+
return { message: 'Password reset successful. All active sessions have been logged out.' };
|
|
453
|
+
}
|
|
454
|
+
async updatePassword(uid, dto, userAgent, ip) {
|
|
455
|
+
// 1. Get primary LOCAL auth
|
|
456
|
+
const auth = await this.authRepo.findOne({
|
|
457
|
+
where: { uid, strategy: (0, typeorm_2.In)([auth_type_enum_1.AuthStrategy.LOCAL, auth_type_enum_1.AuthStrategy.EMAIL, auth_type_enum_1.AuthStrategy.PHONE, auth_type_enum_1.AuthStrategy.USERNAME]) },
|
|
458
|
+
select: ['id', 'secretHash', 'uid']
|
|
459
|
+
});
|
|
460
|
+
if (!auth || !auth.secretHash) {
|
|
461
|
+
throw new common_1.BadRequestException('Password update only available for local accounts');
|
|
462
|
+
}
|
|
463
|
+
// 2. Verify current password
|
|
464
|
+
const isMatch = await bcrypt.compare(dto.currentPassword, auth.secretHash);
|
|
465
|
+
if (!isMatch)
|
|
466
|
+
throw new common_1.BadRequestException('Incorrect current password');
|
|
467
|
+
// 3. Hash and save new password
|
|
468
|
+
auth.secretHash = await bcrypt.hash(dto.newPassword, 10);
|
|
469
|
+
await this.authRepo.save(auth);
|
|
470
|
+
// 4. Notification with Security Link
|
|
471
|
+
if (this.notificationProvider?.sendPasswordChangedNotification) {
|
|
472
|
+
const secureToken = crypto.randomBytes(32).toString('hex');
|
|
473
|
+
const tokenHash = await bcrypt.hash(secureToken, 10);
|
|
474
|
+
const expiresAt = new Date();
|
|
475
|
+
expiresAt.setHours(expiresAt.getHours() + 24);
|
|
476
|
+
// Get user email
|
|
477
|
+
const identifier = await this.authRepo.query(`SELECT value FROM auth_identifiers WHERE "authId" = $1 AND type = 'EMAIL' LIMIT 1`, [auth.id]);
|
|
478
|
+
if (identifier[0]) {
|
|
479
|
+
await this.otpRepo.save(this.otpRepo.create({
|
|
480
|
+
identifier: identifier[0].value,
|
|
481
|
+
purpose: otp_token_entity_1.OtpPurpose.SECURE_ACCOUNT,
|
|
482
|
+
codeHash: tokenHash,
|
|
483
|
+
expiresAt,
|
|
484
|
+
requestUserId: auth.uid,
|
|
485
|
+
requestAuthId: auth.id,
|
|
486
|
+
}));
|
|
487
|
+
const secureLink = `${this.options.frontendUrl || ''}/auth/secure?token=${secureToken}&uid=${auth.uid}`;
|
|
488
|
+
await this.notificationProvider.sendPasswordChangedNotification(identifier[0].value, {
|
|
489
|
+
ip: ip || 'Unknown',
|
|
490
|
+
userAgent: userAgent || 'Unknown',
|
|
491
|
+
secureAccountLink: secureLink,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return { message: 'Password updated successfully' };
|
|
496
|
+
}
|
|
497
|
+
async secureAccount(dto) {
|
|
498
|
+
const otp = await this.otpRepo.findOne({
|
|
499
|
+
where: { requestUserId: dto.uid, purpose: otp_token_entity_1.OtpPurpose.SECURE_ACCOUNT, isUsed: false },
|
|
500
|
+
order: { createdAt: 'DESC' },
|
|
501
|
+
});
|
|
502
|
+
if (!otp || new Date() > otp.expiresAt) {
|
|
503
|
+
throw new common_1.BadRequestException('Invalid or expired security token');
|
|
504
|
+
}
|
|
505
|
+
const isMatch = await bcrypt.compare(dto.token, otp.codeHash);
|
|
506
|
+
if (!isMatch)
|
|
507
|
+
throw new common_1.BadRequestException('Invalid security token');
|
|
508
|
+
// 1. Lock all auth methods
|
|
509
|
+
await this.authRepo.update({ uid: dto.uid }, { isActive: false });
|
|
510
|
+
// 2. Invalidate sessions
|
|
511
|
+
await this.sessionRepository.delete({ uid: dto.uid });
|
|
512
|
+
// 3. Mark OTP as used
|
|
513
|
+
otp.isUsed = true;
|
|
514
|
+
await this.otpRepo.save(otp);
|
|
515
|
+
return { message: 'Account secured and locked. Please reset your password to regain access.' };
|
|
516
|
+
}
|
|
517
|
+
// --- MAGIC LINK ---
|
|
518
|
+
async requestMagicLink(dto) {
|
|
519
|
+
const identifier = await this.authRepo.query(`SELECT ai.*, a.id as "authId", a.uid FROM auth_identifiers ai
|
|
520
|
+
JOIN auth a ON ai."authId" = a.id
|
|
521
|
+
WHERE ai.value = $1 LIMIT 1`, [dto.email.toLowerCase()]);
|
|
522
|
+
let authId;
|
|
523
|
+
let uid;
|
|
524
|
+
if (!identifier[0]) {
|
|
525
|
+
// Optional: Auto-signup if not exists, but let's stick to existing for now
|
|
526
|
+
throw new common_1.BadRequestException('No account found with this email');
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
authId = identifier[0].authId;
|
|
530
|
+
uid = identifier[0].uid;
|
|
531
|
+
}
|
|
532
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
533
|
+
const hash = await bcrypt.hash(token, 10);
|
|
534
|
+
const expiresAt = new Date();
|
|
535
|
+
expiresAt.setMinutes(expiresAt.getMinutes() + 15);
|
|
536
|
+
await this.otpRepo.save(this.otpRepo.create({
|
|
537
|
+
identifier: dto.email.toLowerCase(),
|
|
538
|
+
purpose: otp_token_entity_1.OtpPurpose.MAGIC_LINK,
|
|
539
|
+
codeHash: hash,
|
|
540
|
+
expiresAt,
|
|
541
|
+
requestUserId: uid,
|
|
542
|
+
requestAuthId: authId,
|
|
543
|
+
}));
|
|
544
|
+
if (this.notificationProvider?.sendMagicLink) {
|
|
545
|
+
const link = `${this.options.frontendUrl || ''}/auth/magic-callback?token=${token}&email=${dto.email}`;
|
|
546
|
+
await this.notificationProvider.sendMagicLink(dto.email, link);
|
|
547
|
+
}
|
|
548
|
+
return { message: 'Magic link sent to your email.' };
|
|
549
|
+
}
|
|
550
|
+
async verifyMagicLink(dto, userAgent, ip) {
|
|
551
|
+
const otp = await this.otpRepo.findOne({
|
|
552
|
+
where: { purpose: otp_token_entity_1.OtpPurpose.MAGIC_LINK, isUsed: false },
|
|
553
|
+
order: { createdAt: 'DESC' },
|
|
554
|
+
});
|
|
555
|
+
if (!otp || new Date() > otp.expiresAt) {
|
|
556
|
+
throw new common_1.BadRequestException('Invalid or expired magic link');
|
|
557
|
+
}
|
|
558
|
+
const isMatch = await bcrypt.compare(dto.token, otp.codeHash);
|
|
559
|
+
if (!isMatch)
|
|
560
|
+
throw new common_1.BadRequestException('Invalid magic link');
|
|
561
|
+
otp.isUsed = true;
|
|
562
|
+
await this.otpRepo.save(otp);
|
|
563
|
+
const auth = await this.authRepo.findOne({ where: { id: otp.requestAuthId } });
|
|
564
|
+
if (!auth)
|
|
565
|
+
throw new common_1.BadRequestException('Identity not found');
|
|
566
|
+
const tokens = await this.createSession(auth.uid, userAgent, ip);
|
|
567
|
+
return { tokens, auth };
|
|
568
|
+
}
|
|
569
|
+
// --- MFA (2FA) LOGIC ---
|
|
570
|
+
async enrollMfa(uid, type) {
|
|
571
|
+
if (type !== mfa_method_entity_1.MfaType.TOTP) {
|
|
572
|
+
throw new common_1.BadRequestException('Currently only TOTP MFA is supported');
|
|
573
|
+
}
|
|
574
|
+
let mfa = await this.mfaRepo.findOne({ where: { uid, type }, select: ['id', 'secret', 'isEnabled', 'type'] });
|
|
575
|
+
if (mfa?.isEnabled) {
|
|
576
|
+
throw new common_1.BadRequestException('MFA is already enabled for this account');
|
|
577
|
+
}
|
|
578
|
+
const secret = otplib_1.authenticator.generateSecret();
|
|
579
|
+
const appName = this.options.appName || 'NestJS Auth';
|
|
580
|
+
const otpauth = otplib_1.authenticator.keyuri(uid, appName, secret);
|
|
581
|
+
if (!mfa) {
|
|
582
|
+
mfa = this.mfaRepo.create({
|
|
583
|
+
uid,
|
|
584
|
+
type,
|
|
585
|
+
secret,
|
|
586
|
+
isEnabled: false,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
mfa.secret = secret;
|
|
591
|
+
}
|
|
592
|
+
await this.mfaRepo.save(mfa);
|
|
593
|
+
return {
|
|
594
|
+
secret,
|
|
595
|
+
otpauth,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
async activateMfa(uid, type, code) {
|
|
599
|
+
const mfa = await this.mfaRepo.findOne({
|
|
600
|
+
where: { uid, type },
|
|
601
|
+
select: ['id', 'secret', 'isEnabled']
|
|
602
|
+
});
|
|
603
|
+
if (!mfa) {
|
|
604
|
+
throw new common_1.BadRequestException('No MFA enrollment found');
|
|
605
|
+
}
|
|
606
|
+
if (mfa.isEnabled) {
|
|
607
|
+
throw new common_1.BadRequestException('MFA is already enabled');
|
|
608
|
+
}
|
|
609
|
+
const isValid = otplib_1.authenticator.verify({
|
|
610
|
+
token: code,
|
|
611
|
+
secret: mfa.secret,
|
|
612
|
+
});
|
|
613
|
+
if (!isValid) {
|
|
614
|
+
throw new common_1.BadRequestException('Invalid MFA code');
|
|
615
|
+
}
|
|
616
|
+
mfa.isEnabled = true;
|
|
617
|
+
mfa.isDefault = true;
|
|
618
|
+
await this.mfaRepo.save(mfa);
|
|
619
|
+
return { message: 'MFA activated successfully' };
|
|
620
|
+
}
|
|
621
|
+
async viewAll() {
|
|
622
|
+
const auths = await this.authRepo.find({ relations: ['identifiers', 'oauthProviders'] });
|
|
623
|
+
return auth_mapper_1.AuthMapper.toDtoList(auths);
|
|
624
|
+
}
|
|
625
|
+
async me(uid) {
|
|
626
|
+
const auth = await this.authRepo.findOne({ where: { uid }, relations: ['identifiers', 'oauthProviders'] });
|
|
627
|
+
return auth_mapper_1.AuthMapper.toDto(auth);
|
|
628
|
+
}
|
|
629
|
+
async viewAllMyAuthMethods(uid) {
|
|
630
|
+
const auths = await this.authRepo.find({ where: { uid }, relations: ['identifiers', 'oauthProviders'] });
|
|
631
|
+
return auth_mapper_1.AuthMapper.toDtoList(auths);
|
|
632
|
+
}
|
|
633
|
+
async deleteAccount(uid) {
|
|
634
|
+
// 1. Delete all sessions for this UID
|
|
635
|
+
await this.sessionRepository.delete({ uid });
|
|
636
|
+
// 2. Delete all MFA methods for this UID
|
|
637
|
+
await this.mfaRepo.delete({ uid });
|
|
638
|
+
// 3. Delete all OTP tokens requested by this UID
|
|
639
|
+
await this.otpRepo.delete({ requestUserId: uid });
|
|
640
|
+
// 4. Delete all Auth methods for this UID.
|
|
641
|
+
// TypeORM should handle cascading deletion of AuthIdentifier and OAuthProvider if configured.
|
|
642
|
+
// However, if not configured with ON DELETE CASCADE in the DB, it's safer to use repo.remove or repo.delete.
|
|
643
|
+
// Using repo.delete with uid will delete all matching Auth records.
|
|
644
|
+
await this.authRepo.delete({ uid });
|
|
645
|
+
}
|
|
646
|
+
async deleteAuthMethod(uid, authId) {
|
|
647
|
+
const auth = await this.authRepo.findOne({ where: { id: authId, uid } });
|
|
648
|
+
if (!auth) {
|
|
649
|
+
throw new common_1.BadRequestException('Authentication method not found or does not belong to user');
|
|
650
|
+
}
|
|
651
|
+
// Check if this is the last auth method
|
|
652
|
+
const count = await this.authRepo.count({ where: { uid } });
|
|
653
|
+
if (count <= 1) {
|
|
654
|
+
throw new common_1.BadRequestException('Cannot delete the last authentication method. Delete account instead.');
|
|
655
|
+
}
|
|
656
|
+
// If deleting the primary auth method, we might need to assign a new one
|
|
657
|
+
if (auth.isPrimary) {
|
|
658
|
+
const nextAuth = await this.authRepo.findOne({ where: { uid, id: authId } }); // This is wrong, should be NOT authId
|
|
659
|
+
}
|
|
660
|
+
// Actually, let's keep it simple for now: just delete it.
|
|
661
|
+
// If it was primary, the next available one should ideally become primary.
|
|
662
|
+
await this.authRepo.delete(authId);
|
|
663
|
+
// After deletion, find if there's any primary left.
|
|
664
|
+
// If not, assign the first available one as primary.
|
|
665
|
+
const hasPrimary = await this.authRepo.findOne({ where: { uid, isPrimary: true } });
|
|
666
|
+
if (!hasPrimary) {
|
|
667
|
+
const remainingAuth = await this.authRepo.findOne({ where: { uid } });
|
|
668
|
+
if (remainingAuth) {
|
|
669
|
+
remainingAuth.isPrimary = true;
|
|
670
|
+
await this.authRepo.save(remainingAuth);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
exports.AuthService = AuthService;
|
|
676
|
+
exports.AuthService = AuthService = AuthService_1 = __decorate([
|
|
677
|
+
(0, common_1.Injectable)(),
|
|
678
|
+
__param(1, (0, common_1.Optional)()),
|
|
679
|
+
__param(2, (0, common_1.Optional)()),
|
|
680
|
+
__param(3, (0, typeorm_1.InjectRepository)(session_entity_1.Session)),
|
|
681
|
+
__param(4, (0, typeorm_1.InjectRepository)(auth_entity_1.Auth)),
|
|
682
|
+
__param(5, (0, typeorm_1.InjectRepository)(otp_token_entity_1.OtpToken)),
|
|
683
|
+
__param(6, (0, typeorm_1.InjectRepository)(mfa_method_entity_1.MfaMethod)),
|
|
684
|
+
__param(7, (0, common_1.Inject)(auth_module_options_interface_1.AUTH_MODULE_OPTIONS)),
|
|
685
|
+
__param(8, (0, common_1.Optional)()),
|
|
686
|
+
__param(8, (0, common_1.Inject)(auth_notification_provider_interface_1.AUTH_NOTIFICATION_PROVIDER)),
|
|
687
|
+
__metadata("design:paramtypes", [jwt_1.JwtService,
|
|
688
|
+
local_auth_strategy_1.LocalAuthStrategy,
|
|
689
|
+
oauth_strategy_1.OAuthAuthStrategy,
|
|
690
|
+
typeorm_2.Repository,
|
|
691
|
+
typeorm_2.Repository,
|
|
692
|
+
typeorm_2.Repository,
|
|
693
|
+
typeorm_2.Repository, Object, Object])
|
|
694
|
+
], AuthService);
|
|
695
|
+
//# sourceMappingURL=auth.service.js.map
|