@vynelix/nestjs-multi-auth 0.3.2 → 1.0.0
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/CHANGELOG.md +10 -0
- package/README.md +309 -18
- package/dist/auth/auth.controller.d.ts +3 -2
- package/dist/auth/auth.controller.js +32 -5
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.module.js +116 -68
- package/dist/auth/auth.module.js.map +1 -1
- package/dist/auth/auth.service.d.ts +52 -25
- package/dist/auth/auth.service.js +238 -327
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/auth/core/auth-mapper.d.ts +1 -1
- package/dist/auth/core/database-otp.provider.d.ts +12 -0
- package/dist/auth/core/database-otp.provider.js +122 -0
- package/dist/auth/core/database-otp.provider.js.map +1 -0
- package/dist/auth/dto/requests/magic-link.dto.d.ts +1 -0
- package/dist/auth/dto/requests/magic-link.dto.js +6 -0
- package/dist/auth/dto/requests/magic-link.dto.js.map +1 -1
- package/dist/auth/dto/requests/mfa.dto.d.ts +5 -1
- package/dist/auth/dto/requests/mfa.dto.js +29 -8
- package/dist/auth/dto/requests/mfa.dto.js.map +1 -1
- package/dist/auth/enums/identifier-type.enum.d.ts +11 -0
- package/dist/auth/enums/identifier-type.enum.js +17 -0
- package/dist/auth/enums/identifier-type.enum.js.map +1 -0
- package/dist/auth/enums/mfa-type.enum.d.ts +5 -0
- package/dist/auth/enums/mfa-type.enum.js +10 -0
- package/dist/auth/enums/mfa-type.enum.js.map +1 -0
- package/dist/auth/enums/otp-purpose.enum.d.ts +8 -0
- package/dist/auth/enums/otp-purpose.enum.js +13 -0
- package/dist/auth/enums/otp-purpose.enum.js.map +1 -0
- package/dist/auth/enums/session-event.enum.d.ts +7 -0
- package/dist/auth/enums/session-event.enum.js +12 -0
- package/dist/auth/enums/session-event.enum.js.map +1 -0
- package/dist/auth/interfaces/auth-module-options.interface.d.ts +36 -1
- package/dist/auth/interfaces/auth-notification-provider.interface.d.ts +7 -1
- package/dist/auth/interfaces/auth-otp-provider.interface.d.ts +60 -0
- package/dist/auth/interfaces/auth-otp-provider.interface.js +7 -0
- package/dist/auth/interfaces/auth-otp-provider.interface.js.map +1 -0
- package/dist/auth/interfaces/models.interface.d.ts +91 -0
- package/dist/auth/interfaces/models.interface.js +3 -0
- package/dist/auth/interfaces/models.interface.js.map +1 -0
- package/dist/auth/interfaces/oauth-strategy.interface.d.ts +2 -2
- package/dist/auth/interfaces/repositories.interface.d.ts +70 -0
- package/dist/auth/interfaces/repositories.interface.js +3 -0
- package/dist/auth/interfaces/repositories.interface.js.map +1 -0
- package/dist/auth/interfaces/repository-tokens.d.ts +7 -0
- package/dist/auth/interfaces/repository-tokens.js +11 -0
- package/dist/auth/interfaces/repository-tokens.js.map +1 -0
- package/dist/auth/interfaces/signup-event-body.interface.d.ts +1 -1
- package/dist/auth/strategies/local-auth.strategy.d.ts +3 -5
- package/dist/auth/strategies/local-auth.strategy.js +64 -99
- package/dist/auth/strategies/local-auth.strategy.js.map +1 -1
- package/dist/auth/strategies/oauth/apple.strategy.d.ts +4 -6
- package/dist/auth/strategies/oauth/apple.strategy.js +57 -70
- package/dist/auth/strategies/oauth/apple.strategy.js.map +1 -1
- package/dist/auth/strategies/oauth/facebook.strategy.d.ts +4 -6
- package/dist/auth/strategies/oauth/facebook.strategy.js +58 -71
- package/dist/auth/strategies/oauth/facebook.strategy.js.map +1 -1
- package/dist/auth/strategies/oauth/google.strategy.d.ts +4 -6
- package/dist/auth/strategies/oauth/google.strategy.js +84 -155
- package/dist/auth/strategies/oauth/google.strategy.js.map +1 -1
- package/dist/auth/strategies/oauth/oauth.strategy.d.ts +2 -2
- package/dist/database/prisma/prisma-auth.adapter.d.ts +11 -0
- package/dist/database/prisma/prisma-auth.adapter.js +64 -0
- package/dist/database/prisma/prisma-auth.adapter.js.map +1 -0
- package/dist/database/prisma/repositories/auth-identifier.repository.d.ts +16 -0
- package/dist/database/prisma/repositories/auth-identifier.repository.js +41 -0
- package/dist/database/prisma/repositories/auth-identifier.repository.js.map +1 -0
- package/dist/database/prisma/repositories/auth.repository.d.ts +19 -0
- package/dist/database/prisma/repositories/auth.repository.js +49 -0
- package/dist/database/prisma/repositories/auth.repository.js.map +1 -0
- package/dist/database/prisma/repositories/index.d.ts +7 -0
- package/dist/database/prisma/repositories/index.js +24 -0
- package/dist/database/prisma/repositories/index.js.map +1 -0
- package/dist/database/prisma/repositories/mfa-method.repository.d.ts +11 -0
- package/dist/database/prisma/repositories/mfa-method.repository.js +33 -0
- package/dist/database/prisma/repositories/mfa-method.repository.js.map +1 -0
- package/dist/database/prisma/repositories/oauth-provider.repository.d.ts +14 -0
- package/dist/database/prisma/repositories/oauth-provider.repository.js +39 -0
- package/dist/database/prisma/repositories/oauth-provider.repository.js.map +1 -0
- package/dist/database/prisma/repositories/otp-token.repository.d.ts +13 -0
- package/dist/database/prisma/repositories/otp-token.repository.js +52 -0
- package/dist/database/prisma/repositories/otp-token.repository.js.map +1 -0
- package/dist/database/prisma/repositories/session-log.repository.d.ts +12 -0
- package/dist/database/prisma/repositories/session-log.repository.js +42 -0
- package/dist/database/prisma/repositories/session-log.repository.js.map +1 -0
- package/dist/database/prisma/repositories/session.repository.d.ts +16 -0
- package/dist/database/prisma/repositories/session.repository.js +41 -0
- package/dist/database/prisma/repositories/session.repository.js.map +1 -0
- package/dist/database/typeorm/entities/auth-identify.entity.d.ts +23 -0
- package/dist/database/typeorm/entities/auth-identify.entity.js +61 -0
- package/dist/database/typeorm/entities/auth-identify.entity.js.map +1 -0
- package/dist/database/typeorm/entities/auth.entity.d.ts +45 -0
- package/dist/database/typeorm/entities/auth.entity.js +129 -0
- package/dist/database/typeorm/entities/auth.entity.js.map +1 -0
- package/dist/database/typeorm/entities/base.entity.d.ts +7 -0
- package/dist/database/typeorm/entities/base.entity.js +44 -0
- package/dist/database/typeorm/entities/base.entity.js.map +1 -0
- package/dist/database/typeorm/entities/entities.d.ts +7 -0
- package/dist/database/typeorm/entities/entities.js +18 -0
- package/dist/database/typeorm/entities/entities.js.map +1 -0
- package/dist/database/typeorm/entities/mfa-method.entity.d.ts +10 -0
- package/dist/database/typeorm/entities/mfa-method.entity.js +47 -0
- package/dist/database/typeorm/entities/mfa-method.entity.js.map +1 -0
- package/dist/database/typeorm/entities/oauth-provider.entity.d.ts +38 -0
- package/dist/database/typeorm/entities/oauth-provider.entity.js +88 -0
- package/dist/database/typeorm/entities/oauth-provider.entity.js.map +1 -0
- package/dist/database/typeorm/entities/otp-token.entity.d.ts +15 -0
- package/dist/database/typeorm/entities/otp-token.entity.js +53 -0
- package/dist/database/typeorm/entities/otp-token.entity.js.map +1 -0
- package/dist/database/typeorm/entities/session.entity.d.ts +21 -0
- package/dist/database/typeorm/entities/session.entity.js +71 -0
- package/dist/database/typeorm/entities/session.entity.js.map +1 -0
- package/dist/database/typeorm/entities/session_log.entity.d.ts +25 -0
- package/dist/database/typeorm/entities/session_log.entity.js +78 -0
- package/dist/database/typeorm/entities/session_log.entity.js.map +1 -0
- package/dist/database/typeorm/migrations/auth-entities.d.ts +7 -0
- package/dist/database/typeorm/migrations/auth-entities.js +18 -0
- package/dist/database/typeorm/migrations/auth-entities.js.map +1 -0
- package/dist/database/typeorm/migrations/auth-schema.initializer.d.ts +9 -0
- package/dist/database/typeorm/migrations/auth-schema.initializer.js +36 -0
- package/dist/database/typeorm/migrations/auth-schema.initializer.js.map +1 -0
- package/dist/database/typeorm/migrations/auth.migration.v1.d.ts +12 -0
- package/dist/database/typeorm/migrations/auth.migration.v1.js +157 -0
- package/dist/database/typeorm/migrations/auth.migration.v1.js.map +1 -0
- package/dist/database/typeorm/migrations/auth.migration.v2.d.ts +8 -0
- package/dist/database/typeorm/migrations/auth.migration.v2.js +23 -0
- package/dist/database/typeorm/migrations/auth.migration.v2.js.map +1 -0
- package/dist/database/typeorm/migrations/auth.migrations.d.ts +2 -0
- package/dist/database/typeorm/migrations/auth.migrations.js +10 -0
- package/dist/database/typeorm/migrations/auth.migrations.js.map +1 -0
- package/dist/database/typeorm/migrations/migration.service.d.ts +9 -0
- package/dist/database/typeorm/migrations/migration.service.js +75 -0
- package/dist/database/typeorm/migrations/migration.service.js.map +1 -0
- package/dist/database/typeorm/repositories/auth-identifier.repository.d.ts +18 -0
- package/dist/database/typeorm/repositories/auth-identifier.repository.js +64 -0
- package/dist/database/typeorm/repositories/auth-identifier.repository.js.map +1 -0
- package/dist/database/typeorm/repositories/auth.repository.d.ts +21 -0
- package/dist/database/typeorm/repositories/auth.repository.js +71 -0
- package/dist/database/typeorm/repositories/auth.repository.js.map +1 -0
- package/dist/database/typeorm/repositories/index.d.ts +7 -0
- package/dist/database/typeorm/repositories/index.js +24 -0
- package/dist/database/typeorm/repositories/index.js.map +1 -0
- package/dist/database/typeorm/repositories/mfa-method.repository.d.ts +13 -0
- package/dist/database/typeorm/repositories/mfa-method.repository.js +46 -0
- package/dist/database/typeorm/repositories/mfa-method.repository.js.map +1 -0
- package/dist/database/typeorm/repositories/oauth-provider.repository.d.ts +16 -0
- package/dist/database/typeorm/repositories/oauth-provider.repository.js +52 -0
- package/dist/database/typeorm/repositories/oauth-provider.repository.js.map +1 -0
- package/dist/database/typeorm/repositories/otp-token.repository.d.ts +13 -0
- package/dist/database/typeorm/repositories/otp-token.repository.js +52 -0
- package/dist/database/typeorm/repositories/otp-token.repository.js.map +1 -0
- package/dist/database/typeorm/repositories/session-log.repository.d.ts +12 -0
- package/dist/database/typeorm/repositories/session-log.repository.js +47 -0
- package/dist/database/typeorm/repositories/session-log.repository.js.map +1 -0
- package/dist/database/typeorm/repositories/session.repository.d.ts +18 -0
- package/dist/database/typeorm/repositories/session.repository.js +84 -0
- package/dist/database/typeorm/repositories/session.repository.js.map +1 -0
- package/dist/database/typeorm/typeorm-auth.adapter.d.ts +2 -0
- package/dist/database/typeorm/typeorm-auth.adapter.js +50 -0
- package/dist/database/typeorm/typeorm-auth.adapter.js.map +1 -0
- package/dist/index.d.ts +11 -9
- package/dist/index.js +18 -10
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/fix-imports.js +46 -0
- package/fix-syntax.js +66 -0
- package/package.json +22 -20
- package/patch-local-specs.js +52 -0
- package/patch-oauth-specs.js +41 -0
- package/patch-specs.js +36 -0
- package/prompts/database_adapter.md +157 -0
- package/prompts/otp_provider.md +292 -0
- package/scratch/create-prisma-repos-final.js +139 -0
- package/scratch/create-prisma-repos.js +280 -0
- package/scratch/fix-events-spec.js +12 -0
- package/scratch/fix-imports.js +53 -0
- package/scratch/fix-prisma-interfaces.js +41 -0
- package/scratch/fix-prisma-interfaces2.js +36 -0
- package/scratch/fix-prisma-interfaces3.js +26 -0
- package/scratch/fix-prisma.js +28 -0
- package/scratch/fix-test-imports.js +51 -0
- package/scratch/fix-tests-advanced.js +68 -0
- package/scratch/fix-tests-advanced2.js +23 -0
- package/scratch/fix-tests-concrete.js +40 -0
- package/scratch/fix-tests-final.js +35 -0
- package/scratch/fix-tests-final2.js +28 -0
- package/scratch/fix-tests-final3.js +26 -0
- package/scratch/fix-tests-methods.js +40 -0
- package/scratch/fix-tests-proxy.js +67 -0
- package/scratch/fix-tests-types.js +24 -0
- package/scratch/rewrite-tests.js +110 -0
- package/tsc_errors.txt +60 -0
- package/tsc_errors2.txt +93 -0
- package/tmp-cv-test/package-lock.json +0 -250
- package/tmp-cv-test/package.json +0 -17
- package/tmp-cv-test/test.ts +0 -11
|
@@ -17,16 +17,14 @@ exports.AuthService = void 0;
|
|
|
17
17
|
const common_1 = require("@nestjs/common");
|
|
18
18
|
const event_emitter_1 = require("@nestjs/event-emitter");
|
|
19
19
|
const jwt_1 = require("@nestjs/jwt");
|
|
20
|
-
const
|
|
21
|
-
const
|
|
20
|
+
const repository_tokens_1 = require("./interfaces/repository-tokens");
|
|
21
|
+
const auth_otp_provider_interface_1 = require("./interfaces/auth-otp-provider.interface");
|
|
22
22
|
const bcrypt = require("bcrypt");
|
|
23
23
|
const local_auth_strategy_1 = require("./strategies/local-auth.strategy");
|
|
24
24
|
const oauth_strategy_1 = require("./strategies/oauth/oauth.strategy");
|
|
25
25
|
const auth_type_enum_1 = require("./enums/auth-type.enum");
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const otp_token_entity_1 = require("./entities/otp-token.entity");
|
|
29
|
-
const mfa_method_entity_1 = require("./entities/mfa-method.entity");
|
|
26
|
+
const otp_purpose_enum_1 = require("./enums/otp-purpose.enum");
|
|
27
|
+
const mfa_type_enum_1 = require("./enums/mfa-type.enum");
|
|
30
28
|
const otplib_1 = require("otplib");
|
|
31
29
|
const auth_module_options_interface_1 = require("./interfaces/auth-module-options.interface");
|
|
32
30
|
const auth_notification_provider_interface_1 = require("./interfaces/auth-notification-provider.interface");
|
|
@@ -34,16 +32,19 @@ const crypto = require("crypto");
|
|
|
34
32
|
const duration_util_1 = require("./utils/duration.util");
|
|
35
33
|
const auth_mapper_1 = require("./core/auth-mapper");
|
|
36
34
|
const auth_events_1 = require("./enums/auth.events");
|
|
37
|
-
const
|
|
35
|
+
const session_event_enum_1 = require("./enums/session-event.enum");
|
|
38
36
|
let AuthService = AuthService_1 = class AuthService {
|
|
39
|
-
constructor(jwtService, passwordStrategy, oauthStrategy, sessionRepository, sessionLogRepo, authRepo,
|
|
37
|
+
constructor(jwtService, passwordStrategy, oauthStrategy, sessionRepository, sessionLogRepo, authRepo, authIdentifierRepo, otpProvider, otpProviderEmail, otpProviderPhone, mfaRepo, options, notificationProvider, eventEmitter) {
|
|
40
38
|
this.jwtService = jwtService;
|
|
41
39
|
this.passwordStrategy = passwordStrategy;
|
|
42
40
|
this.oauthStrategy = oauthStrategy;
|
|
43
41
|
this.sessionRepository = sessionRepository;
|
|
44
42
|
this.sessionLogRepo = sessionLogRepo;
|
|
45
43
|
this.authRepo = authRepo;
|
|
46
|
-
this.
|
|
44
|
+
this.authIdentifierRepo = authIdentifierRepo;
|
|
45
|
+
this.otpProvider = otpProvider;
|
|
46
|
+
this.otpProviderEmail = otpProviderEmail;
|
|
47
|
+
this.otpProviderPhone = otpProviderPhone;
|
|
47
48
|
this.mfaRepo = mfaRepo;
|
|
48
49
|
this.options = options;
|
|
49
50
|
this.notificationProvider = notificationProvider;
|
|
@@ -51,6 +52,14 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
51
52
|
this.logger = new common_1.Logger(AuthService_1.name);
|
|
52
53
|
this.createSessionLog = this.options.createSessionLogOnInvalid ?? false;
|
|
53
54
|
}
|
|
55
|
+
/** Returns the correct OTP provider for the given identifier type. */
|
|
56
|
+
resolveOtpProvider(identifierType) {
|
|
57
|
+
if (identifierType === 'email')
|
|
58
|
+
return this.otpProviderEmail;
|
|
59
|
+
if (identifierType === 'phone')
|
|
60
|
+
return this.otpProviderPhone;
|
|
61
|
+
return this.otpProvider;
|
|
62
|
+
}
|
|
54
63
|
// --- INTERNAL HELPER: Generate Token Pair ---
|
|
55
64
|
async generateTokens(uid, sessionId, namespace) {
|
|
56
65
|
const refreshJti = crypto.randomUUID();
|
|
@@ -76,7 +85,7 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
76
85
|
expiresAt.setSeconds(expiresAt.getSeconds() + durationSeconds);
|
|
77
86
|
const deviceFingerprint = this.fingerprint(userAgent);
|
|
78
87
|
let session;
|
|
79
|
-
const _createSession = () => {
|
|
88
|
+
const _createSession = async () => {
|
|
80
89
|
return this.sessionRepository.create({
|
|
81
90
|
id: crypto.randomUUID(),
|
|
82
91
|
uid,
|
|
@@ -89,13 +98,7 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
89
98
|
});
|
|
90
99
|
};
|
|
91
100
|
if (this.options.sessionCreationPolicy === auth_module_options_interface_1.SessionCreationPolicy.REUSE_DEVICE) {
|
|
92
|
-
session = await this.sessionRepository.
|
|
93
|
-
where: {
|
|
94
|
-
uid,
|
|
95
|
-
namespace,
|
|
96
|
-
deviceFingerprint,
|
|
97
|
-
},
|
|
98
|
-
});
|
|
101
|
+
session = await this.sessionRepository.findDeviceSession(uid, namespace, deviceFingerprint);
|
|
99
102
|
if (session) {
|
|
100
103
|
session.expiresAt = expiresAt;
|
|
101
104
|
session.ipAddress = ip;
|
|
@@ -103,11 +106,11 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
103
106
|
session.deviceFingerprint = deviceFingerprint;
|
|
104
107
|
}
|
|
105
108
|
else {
|
|
106
|
-
session = _createSession();
|
|
109
|
+
session = await _createSession();
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
else {
|
|
110
|
-
session = _createSession();
|
|
113
|
+
session = await _createSession();
|
|
111
114
|
}
|
|
112
115
|
const tokens = await this.generateTokens(uid, session.id, namespace);
|
|
113
116
|
session.refreshTokenHash = await bcrypt.hash(tokens.refreshToken, 10);
|
|
@@ -117,7 +120,7 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
117
120
|
async createSessionLogFromSessionIfEnabled({ event, reason, session, }) {
|
|
118
121
|
if (!this.createSessionLog)
|
|
119
122
|
return;
|
|
120
|
-
const sessionLog = this.sessionLogRepo.create({
|
|
123
|
+
const sessionLog = await this.sessionLogRepo.create({
|
|
121
124
|
...session,
|
|
122
125
|
sessionId: session.id,
|
|
123
126
|
event,
|
|
@@ -130,7 +133,10 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
130
133
|
throw new Error("Either session or sessionId must be provided.");
|
|
131
134
|
}
|
|
132
135
|
if (!session) {
|
|
133
|
-
|
|
136
|
+
const foundSession = await this.sessionRepository.findById(sessionId);
|
|
137
|
+
if (!foundSession)
|
|
138
|
+
throw new Error("Session not found");
|
|
139
|
+
session = foundSession;
|
|
134
140
|
}
|
|
135
141
|
await this.createSessionLogFromSessionIfEnabled({
|
|
136
142
|
session,
|
|
@@ -140,26 +146,24 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
140
146
|
await this.sessionRepository.delete(session.id);
|
|
141
147
|
}
|
|
142
148
|
async invalidateSessions({ uid, event, reason, }) {
|
|
143
|
-
await this.sessionRepository.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
await
|
|
149
|
+
const sessions = await this.sessionRepository.findByUid(uid);
|
|
150
|
+
if (sessions.length === 0) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (this.createSessionLog) {
|
|
154
|
+
const logs = sessions.map((session) => ({
|
|
155
|
+
...session,
|
|
156
|
+
id: undefined,
|
|
157
|
+
sessionId: session.id,
|
|
158
|
+
event,
|
|
159
|
+
reason,
|
|
160
|
+
timestamp: new Date()
|
|
161
|
+
}));
|
|
162
|
+
for (const log of logs) {
|
|
163
|
+
await this.sessionLogRepo.save(log);
|
|
158
164
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
162
|
-
});
|
|
165
|
+
}
|
|
166
|
+
await this.sessionRepository.deleteByUid(uid);
|
|
163
167
|
}
|
|
164
168
|
async signup({ dto, uid, userAgent, ip, namespace }) {
|
|
165
169
|
if (!dto.method)
|
|
@@ -197,7 +201,7 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
197
201
|
// Force verification if no password was provided for local strategies (passwordless signup)
|
|
198
202
|
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;
|
|
199
203
|
// Check if user has 2FA enabled
|
|
200
|
-
const mfaMethod = await this.mfaRepo.
|
|
204
|
+
const mfaMethod = await this.mfaRepo.findByUidAndEnabled(auth.uid);
|
|
201
205
|
const has2FA = !!mfaMethod;
|
|
202
206
|
const triggerVerification = isPasswordless ||
|
|
203
207
|
(this.options.verificationRequired && !identifier?.isVerified) ||
|
|
@@ -266,7 +270,7 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
266
270
|
// Force verification if no password was provided for local strategies (passwordless login)
|
|
267
271
|
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;
|
|
268
272
|
// Check if user has 2FA enabled
|
|
269
|
-
const mfaMethod = await this.mfaRepo.
|
|
273
|
+
const mfaMethod = await this.mfaRepo.findByUidAndEnabled(auth.uid);
|
|
270
274
|
const has2FA = !!mfaMethod;
|
|
271
275
|
// Trigger email/phone verification only if required and identifier not verified.
|
|
272
276
|
// MFA (2FA) is handled separately via dedicated endpoints.
|
|
@@ -307,26 +311,14 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
307
311
|
}
|
|
308
312
|
// --- VERIFICATION LOGIC ---
|
|
309
313
|
async sendVerification(auth, currentIdentifier) {
|
|
310
|
-
if (!this.notificationProvider)
|
|
314
|
+
if (!this.notificationProvider && !this.otpProvider && !this.otpProviderEmail && !this.otpProviderPhone)
|
|
311
315
|
return;
|
|
312
|
-
// 1. Determine primary identifier (email or phone) to send the code to
|
|
313
|
-
// If we have a verified email/phone on the same UID, use that.
|
|
314
|
-
// Otherwise use the current identifier if it's verifiable.
|
|
315
316
|
let primaryIdentifier = currentIdentifier?.type !== 'USERNAME' ? currentIdentifier : null;
|
|
316
317
|
if (!primaryIdentifier) {
|
|
317
|
-
|
|
318
|
-
const allIdentifiers = await this.authRepo.query(`SELECT ai.* FROM auth_identifiers ai
|
|
319
|
-
JOIN auth a ON ai."authId" = a.id
|
|
320
|
-
WHERE a.uid = $1 AND ai.type IN ('EMAIL', 'PHONE')
|
|
321
|
-
ORDER BY ai."isVerified" DESC, ai."createdAt" ASC LIMIT 1`, [auth.uid]);
|
|
322
|
-
primaryIdentifier = allIdentifiers[0];
|
|
318
|
+
primaryIdentifier = await this.authIdentifierRepo.findByUidAndTypes(auth.uid, ['EMAIL', 'PHONE']);
|
|
323
319
|
}
|
|
324
320
|
if (!primaryIdentifier) {
|
|
325
|
-
|
|
326
|
-
const fullAuth = await this.authRepo.findOne({
|
|
327
|
-
where: { id: auth.id },
|
|
328
|
-
relations: ['identifiers']
|
|
329
|
-
});
|
|
321
|
+
const fullAuth = await this.authRepo.findWithIdentifiers(auth.id);
|
|
330
322
|
primaryIdentifier = fullAuth?.identifiers?.find(id => id.type === 'EMAIL' || id.type === 'PHONE');
|
|
331
323
|
}
|
|
332
324
|
if (!primaryIdentifier) {
|
|
@@ -335,64 +327,45 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
335
327
|
}
|
|
336
328
|
return;
|
|
337
329
|
}
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
:
|
|
342
|
-
|
|
343
|
-
// 3. Save OTP Token
|
|
344
|
-
const expiresAt = new Date();
|
|
345
|
-
const otpExpMins = this.options.otpExpiresIn || 15;
|
|
346
|
-
expiresAt.setMinutes(expiresAt.getMinutes() + otpExpMins);
|
|
347
|
-
const otpToken = this.otpRepo.create({
|
|
330
|
+
const purpose = primaryIdentifier.type === 'EMAIL' ? otp_purpose_enum_1.OtpPurpose.VERIFY_EMAIL : otp_purpose_enum_1.OtpPurpose.VERIFY_PHONE;
|
|
331
|
+
const idType = primaryIdentifier.type === 'EMAIL' ? 'email' : 'phone';
|
|
332
|
+
const result = await this.resolveOtpProvider(idType).issue({
|
|
333
|
+
uid: auth.uid,
|
|
334
|
+
authId: auth.id,
|
|
348
335
|
identifier: primaryIdentifier.value,
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
requestUserId: auth.uid,
|
|
353
|
-
requestAuthId: auth.id,
|
|
336
|
+
identifierType: idType,
|
|
337
|
+
purpose,
|
|
338
|
+
expiresIn: this.options.otpExpiresIn,
|
|
354
339
|
});
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
340
|
+
if (!result.handledDelivery && this.notificationProvider && result.code) {
|
|
341
|
+
try {
|
|
342
|
+
await this.notificationProvider.sendVerificationCode({
|
|
343
|
+
to: primaryIdentifier.value,
|
|
344
|
+
code: result.code,
|
|
345
|
+
type: primaryIdentifier.type === 'EMAIL' ? 'email' : 'phone',
|
|
346
|
+
purpose,
|
|
347
|
+
expiresAt: result.expiresAt
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
this.logger.error(`Failed to send verification code to ${primaryIdentifier.value}`, e);
|
|
352
|
+
throw new common_1.BadRequestException('Failed to send verification code');
|
|
353
|
+
}
|
|
363
354
|
}
|
|
364
355
|
}
|
|
365
356
|
async verifyCode({ uid, code, userAgent, ip, namespace }) {
|
|
366
|
-
const auth = await this.authRepo.
|
|
357
|
+
const auth = await this.authRepo.findByUid(uid);
|
|
367
358
|
if (!auth)
|
|
368
359
|
throw new common_1.BadRequestException('Identity not found');
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
if (new Date() > otp.expiresAt) {
|
|
380
|
-
throw new common_1.BadRequestException('Verification code expired');
|
|
381
|
-
}
|
|
382
|
-
const isMatch = await bcrypt.compare(code, otp.codeHash);
|
|
383
|
-
if (!isMatch)
|
|
384
|
-
throw new common_1.BadRequestException('Invalid verification code');
|
|
385
|
-
// Success!
|
|
386
|
-
otp.isUsed = true;
|
|
387
|
-
await this.otpRepo.save(otp);
|
|
388
|
-
auth.isVerified = true;
|
|
389
|
-
await this.authRepo.save(auth);
|
|
390
|
-
// Also mark all identifiers for this Auth as verified
|
|
391
|
-
await this.authRepo.query(`UPDATE auth_identifiers SET "isVerified" = true WHERE "authId" = $1`, [auth.id]);
|
|
392
|
-
// If there's a requestAuthId on the OTP (which there should be), update that one too if different
|
|
393
|
-
if (otp.requestAuthId && otp.requestAuthId !== auth.id) {
|
|
394
|
-
await this.authRepo.update(otp.requestAuthId, { isVerified: true });
|
|
395
|
-
await this.authRepo.query(`UPDATE auth_identifiers SET "isVerified" = true WHERE "authId" = $1`, [otp.requestAuthId]);
|
|
360
|
+
const result = await this.resolveOtpProvider().verify({ uid, code });
|
|
361
|
+
if (result.success) {
|
|
362
|
+
auth.isVerified = true;
|
|
363
|
+
await this.authRepo.save(auth);
|
|
364
|
+
await this.authIdentifierRepo.markVerifiedByAuthId(auth.id);
|
|
365
|
+
if (result.authId && result.authId !== auth.id) {
|
|
366
|
+
await this.authRepo.update(result.authId, { isVerified: true });
|
|
367
|
+
await this.authIdentifierRepo.markVerifiedByAuthId(result.authId);
|
|
368
|
+
}
|
|
396
369
|
}
|
|
397
370
|
const tokens = await this.createSession(auth.uid, userAgent, ip, namespace);
|
|
398
371
|
if (this.eventEmitter) {
|
|
@@ -401,72 +374,54 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
401
374
|
return { message: 'Identity verified successfully', tokens, auth };
|
|
402
375
|
}
|
|
403
376
|
async resendVerification(uid) {
|
|
404
|
-
const auth = await this.authRepo.
|
|
377
|
+
const auth = await this.authRepo.findByUid(uid);
|
|
405
378
|
if (!auth)
|
|
406
379
|
throw new common_1.BadRequestException('Identity not found');
|
|
407
|
-
if (!this.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
throw new common_1.BadRequestException(`Please wait ${wait} seconds before requesting a new code.`);
|
|
421
|
-
}
|
|
380
|
+
if (!this.resolveOtpProvider().resend) {
|
|
381
|
+
await this.sendVerification(auth);
|
|
382
|
+
return { message: 'Verification code resent' };
|
|
383
|
+
}
|
|
384
|
+
const result = await this.resolveOtpProvider().resend({ uid });
|
|
385
|
+
if (!result.handledDelivery && this.notificationProvider && result.code && result.metadata) {
|
|
386
|
+
await this.notificationProvider.sendVerificationCode({
|
|
387
|
+
to: result.metadata.identifier,
|
|
388
|
+
code: result.code,
|
|
389
|
+
type: result.metadata.identifierType,
|
|
390
|
+
purpose: result.metadata.purpose,
|
|
391
|
+
expiresAt: result.expiresAt
|
|
392
|
+
});
|
|
422
393
|
}
|
|
423
|
-
await this.sendVerification(auth);
|
|
424
394
|
return { message: 'Verification code resent' };
|
|
425
395
|
}
|
|
426
396
|
async refreshTokens({ refreshToken, currentUserAgent, currentIp, namespace }) {
|
|
427
397
|
try {
|
|
428
398
|
const payload = await this.jwtService.verifyAsync(refreshToken, { secret: this.options.jwtRefreshSecret || process.env.JWT_REFRESH_SECRET });
|
|
429
399
|
const resolvedNamespace = namespace ?? payload.namespace;
|
|
430
|
-
const
|
|
431
|
-
if (resolvedNamespace) {
|
|
432
|
-
whereClause.namespace = resolvedNamespace;
|
|
433
|
-
}
|
|
434
|
-
const session = await this.sessionRepository.findOne({
|
|
435
|
-
where: whereClause,
|
|
436
|
-
select: [
|
|
437
|
-
'id',
|
|
438
|
-
'uid',
|
|
439
|
-
'refreshTokenHash',
|
|
440
|
-
'expiresAt',
|
|
441
|
-
'deviceFingerprint',
|
|
442
|
-
'ipAddress',
|
|
443
|
-
'namespace',
|
|
444
|
-
],
|
|
445
|
-
});
|
|
400
|
+
const session = await this.sessionRepository.findByIdWithDetails(payload.sessionId, resolvedNamespace);
|
|
446
401
|
if (this.options.debug) {
|
|
447
|
-
this.logger.debug(`Namespace from JWT: ${payload.namespace}, Namespace passed to refresh method: ${namespace}
|
|
402
|
+
this.logger.debug(`Namespace from JWT: ${payload.namespace}, Namespace passed to refresh method: ${namespace}`);
|
|
448
403
|
}
|
|
449
404
|
if (!session)
|
|
450
405
|
throw new common_1.ForbiddenException('Session not found');
|
|
451
406
|
const incomingFingerprint = this.fingerprint(currentUserAgent);
|
|
452
407
|
if (session.deviceFingerprint !== incomingFingerprint) {
|
|
453
|
-
this.invalidateSession({ session, event:
|
|
408
|
+
this.invalidateSession({ session, event: session_event_enum_1.SessionEvent.REVOKE, reason: 'Device mismatch during refresh' });
|
|
454
409
|
throw new common_1.ForbiddenException('Device mismatch');
|
|
455
410
|
}
|
|
456
411
|
if (session.namespace !== namespace && session.namespace !== resolvedNamespace) {
|
|
457
412
|
if (this.options.debug) {
|
|
458
413
|
this.logger.debug(`Namespace provided: ${namespace}, Session namespace: ${session.namespace}.\nNamespace mismatch: ${session.namespace !== namespace}`);
|
|
459
414
|
}
|
|
460
|
-
this.invalidateSession({ session, event:
|
|
415
|
+
this.invalidateSession({ session, event: session_event_enum_1.SessionEvent.REVOKE, reason: 'Namespace mismatch during refresh' });
|
|
461
416
|
throw new common_1.ForbiddenException("Namespace mismatch");
|
|
462
417
|
}
|
|
463
418
|
if (new Date() > session.expiresAt) {
|
|
464
|
-
this.invalidateSession({ session, event:
|
|
419
|
+
this.invalidateSession({ session, event: session_event_enum_1.SessionEvent.EXPIRE, reason: 'Session expired during refresh' });
|
|
465
420
|
throw new common_1.ForbiddenException('Session expired');
|
|
466
421
|
}
|
|
467
422
|
const isMatch = await bcrypt.compare(refreshToken, session.refreshTokenHash);
|
|
468
423
|
if (!isMatch) {
|
|
469
|
-
this.invalidateSession({ session, event:
|
|
424
|
+
this.invalidateSession({ session, event: session_event_enum_1.SessionEvent.REVOKE, reason: 'Invalid refresh token during refresh' });
|
|
470
425
|
throw new common_1.ForbiddenException('Invalid refresh token');
|
|
471
426
|
}
|
|
472
427
|
const tokens = await this.generateTokens(session.uid, session.id, namespace);
|
|
@@ -497,8 +452,8 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
497
452
|
try {
|
|
498
453
|
const payload = this.jwtService.decode(refreshToken);
|
|
499
454
|
if (payload?.sessionId) {
|
|
500
|
-
const session = await this.sessionRepository.
|
|
501
|
-
this.invalidateSession({ session, event:
|
|
455
|
+
const session = await this.sessionRepository.findById(payload.sessionId);
|
|
456
|
+
this.invalidateSession({ session, event: session_event_enum_1.SessionEvent.LOGOUT, reason: 'User logout' });
|
|
502
457
|
if (session && this.eventEmitter) {
|
|
503
458
|
this.eventEmitter.emit(auth_events_1.AuthEvents.LOGOUT, { uid: session.uid });
|
|
504
459
|
}
|
|
@@ -515,35 +470,28 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
515
470
|
const value = dto.email || dto.phone || dto.username;
|
|
516
471
|
if (!value)
|
|
517
472
|
throw new common_1.BadRequestException('Identifier is required');
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
JOIN auth a ON ai."authId" = a.id
|
|
521
|
-
WHERE ai.value = $1 LIMIT 1`, [value.toLowerCase()]);
|
|
522
|
-
if (!identifier[0]) {
|
|
523
|
-
// Security: Don't reveal if user exists.
|
|
524
|
-
// But typically for forgot-password, users expect an error if email is wrong.
|
|
525
|
-
// We'll return success anyway to prevent enumeration.
|
|
473
|
+
const result = await this.authIdentifierRepo.findWithAuthByValue(value);
|
|
474
|
+
if (!result) {
|
|
526
475
|
return { message: 'If an account exists, a reset code has been sent.' };
|
|
527
476
|
}
|
|
528
|
-
const primaryAuth =
|
|
529
|
-
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
:
|
|
533
|
-
const hash = await bcrypt.hash(code, 10);
|
|
534
|
-
const expiresAt = new Date();
|
|
535
|
-
expiresAt.setMinutes(expiresAt.getMinutes() + (this.options.otpExpiresIn || 15));
|
|
536
|
-
await this.otpRepo.save(this.otpRepo.create({
|
|
477
|
+
const { identifier: primaryAuth, auth: primaryAuthObj } = result;
|
|
478
|
+
const resetIdType = primaryAuth.type === 'PHONE' ? 'phone' : 'email';
|
|
479
|
+
const issueResult = await this.resolveOtpProvider(resetIdType).issue({
|
|
480
|
+
uid: primaryAuthObj.uid,
|
|
481
|
+
authId: primaryAuthObj.id,
|
|
537
482
|
identifier: primaryAuth.value,
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
483
|
+
identifierType: resetIdType,
|
|
484
|
+
purpose: otp_purpose_enum_1.OtpPurpose.PASSWORD_RESET,
|
|
485
|
+
expiresIn: this.options.otpExpiresIn || 15
|
|
486
|
+
});
|
|
487
|
+
if (!issueResult.handledDelivery && this.notificationProvider && issueResult.code) {
|
|
488
|
+
await this.notificationProvider.sendVerificationCode({
|
|
489
|
+
to: primaryAuth.value,
|
|
490
|
+
code: issueResult.code,
|
|
491
|
+
type: primaryAuth.type === 'PHONE' ? 'phone' : 'email',
|
|
492
|
+
purpose: otp_purpose_enum_1.OtpPurpose.PASSWORD_RESET,
|
|
493
|
+
expiresAt: issueResult.expiresAt
|
|
494
|
+
});
|
|
547
495
|
}
|
|
548
496
|
if (this.eventEmitter) {
|
|
549
497
|
this.eventEmitter.emit(auth_events_1.AuthEvents.PASSWORD_RESET, { auth: primaryAuth });
|
|
@@ -551,39 +499,26 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
551
499
|
return { message: 'If an account exists, a reset code has been sent.' };
|
|
552
500
|
}
|
|
553
501
|
async resetPassword(dto) {
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
order: { createdAt: 'DESC' },
|
|
557
|
-
});
|
|
558
|
-
if (!otp || new Date() > otp.expiresAt) {
|
|
502
|
+
const result = await this.resolveOtpProvider().verify({ uid: dto.uid, code: dto.code, purpose: otp_purpose_enum_1.OtpPurpose.PASSWORD_RESET });
|
|
503
|
+
if (!result.success || !result.authId) {
|
|
559
504
|
throw new common_1.BadRequestException('Invalid or expired reset code');
|
|
560
505
|
}
|
|
561
|
-
const isMatch = await bcrypt.compare(dto.code, otp.codeHash);
|
|
562
|
-
if (!isMatch)
|
|
563
|
-
throw new common_1.BadRequestException('Invalid reset code');
|
|
564
|
-
// Update password
|
|
565
506
|
const hash = await bcrypt.hash(dto.newPassword, 10);
|
|
566
|
-
await this.authRepo.update(
|
|
507
|
+
await this.authRepo.update(result.authId, {
|
|
567
508
|
secretHash: hash,
|
|
568
509
|
isActive: true // Unlock account on successful reset
|
|
569
510
|
});
|
|
570
|
-
// Mark OTP as used
|
|
571
|
-
otp.isUsed = true;
|
|
572
|
-
await this.otpRepo.save(otp);
|
|
573
511
|
// Security: Invalidate all sessions
|
|
574
|
-
await this.invalidateSessions({ uid: dto.uid, event:
|
|
512
|
+
await this.invalidateSessions({ uid: dto.uid, event: session_event_enum_1.SessionEvent.REVOKE, reason: 'User reset password' });
|
|
575
513
|
if (this.eventEmitter) {
|
|
576
|
-
const auth = await this.authRepo.
|
|
514
|
+
const auth = await this.authRepo.findByUid(dto.uid);
|
|
577
515
|
this.eventEmitter.emit(auth_events_1.AuthEvents.PASSWORD_RESET, { auth });
|
|
578
516
|
}
|
|
579
517
|
return { message: 'Password reset successful. All active sessions have been logged out.' };
|
|
580
518
|
}
|
|
581
519
|
async updatePassword(uid, dto, userAgent, ip) {
|
|
582
520
|
// 1. Get primary LOCAL auth
|
|
583
|
-
const auth = await this.authRepo.
|
|
584
|
-
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]) },
|
|
585
|
-
select: ['id', 'secretHash', 'uid']
|
|
586
|
-
});
|
|
521
|
+
const auth = await this.authRepo.findByUidAndStrategies(uid, [auth_type_enum_1.AuthStrategy.LOCAL, auth_type_enum_1.AuthStrategy.EMAIL, auth_type_enum_1.AuthStrategy.PHONE, auth_type_enum_1.AuthStrategy.USERNAME]);
|
|
587
522
|
if (!auth || !auth.secretHash) {
|
|
588
523
|
throw new common_1.BadRequestException('Password update only available for local accounts');
|
|
589
524
|
}
|
|
@@ -596,27 +531,24 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
596
531
|
await this.authRepo.save(auth);
|
|
597
532
|
// 4. Notification with Security Link
|
|
598
533
|
if (this.notificationProvider?.sendPasswordChangedNotification) {
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
purpose: otp_token_entity_1.OtpPurpose.SECURE_ACCOUNT,
|
|
609
|
-
codeHash: tokenHash,
|
|
610
|
-
expiresAt,
|
|
611
|
-
requestUserId: auth.uid,
|
|
612
|
-
requestAuthId: auth.id,
|
|
613
|
-
}));
|
|
614
|
-
const secureLink = `${this.options.frontendUrl || ''}/auth/secure?token=${secureToken}&uid=${auth.uid}`;
|
|
615
|
-
await this.notificationProvider.sendPasswordChangedNotification(identifier[0].value, {
|
|
616
|
-
ip: ip || 'Unknown',
|
|
617
|
-
userAgent: userAgent || 'Unknown',
|
|
618
|
-
secureAccountLink: secureLink,
|
|
534
|
+
const identifier = await this.authIdentifierRepo.findByUidAndTypes(auth.uid, ['EMAIL']);
|
|
535
|
+
if (identifier) {
|
|
536
|
+
const issueResult = await this.resolveOtpProvider('email').issue({
|
|
537
|
+
uid: auth.uid,
|
|
538
|
+
authId: auth.id,
|
|
539
|
+
identifier: identifier.value,
|
|
540
|
+
identifierType: 'email',
|
|
541
|
+
purpose: otp_purpose_enum_1.OtpPurpose.SECURE_ACCOUNT,
|
|
542
|
+
expiresIn: 24 * 60
|
|
619
543
|
});
|
|
544
|
+
if (issueResult.code) {
|
|
545
|
+
const secureLink = `${this.options.frontendUrl || ''}/auth/secure?token=${issueResult.code}&uid=${auth.uid}`;
|
|
546
|
+
await this.notificationProvider.sendPasswordChangedNotification(identifier.value, {
|
|
547
|
+
ip: ip || 'Unknown',
|
|
548
|
+
userAgent: userAgent || 'Unknown',
|
|
549
|
+
secureAccountLink: secureLink,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
620
552
|
}
|
|
621
553
|
}
|
|
622
554
|
if (this.eventEmitter) {
|
|
@@ -625,23 +557,21 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
625
557
|
return { message: 'Password updated successfully' };
|
|
626
558
|
}
|
|
627
559
|
async secureAccount(dto) {
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
560
|
+
const result = await this.resolveOtpProvider('email').verify({
|
|
561
|
+
uid: dto.uid,
|
|
562
|
+
code: dto.token,
|
|
563
|
+
purpose: otp_purpose_enum_1.OtpPurpose.SECURE_ACCOUNT,
|
|
631
564
|
});
|
|
632
|
-
if (!
|
|
565
|
+
if (!result.success) {
|
|
633
566
|
throw new common_1.BadRequestException('Invalid or expired security token');
|
|
634
567
|
}
|
|
635
|
-
const isMatch = await bcrypt.compare(dto.token, otp.codeHash);
|
|
636
|
-
if (!isMatch)
|
|
637
|
-
throw new common_1.BadRequestException('Invalid security token');
|
|
638
568
|
// 1. Lock all auth methods
|
|
639
|
-
await this.authRepo.
|
|
569
|
+
const auths = await this.authRepo.findAllByUid(dto.uid);
|
|
570
|
+
for (const auth of auths) {
|
|
571
|
+
await this.authRepo.update(auth.id, { isActive: false });
|
|
572
|
+
}
|
|
640
573
|
// 2. Invalidate sessions
|
|
641
|
-
this.invalidateSessions({ uid: dto.uid, event:
|
|
642
|
-
// 3. Mark OTP as used
|
|
643
|
-
otp.isUsed = true;
|
|
644
|
-
await this.otpRepo.save(otp);
|
|
574
|
+
this.invalidateSessions({ uid: dto.uid, event: session_event_enum_1.SessionEvent.REVOKE, reason: 'User secured account by invalidating all sessions' });
|
|
645
575
|
if (this.eventEmitter) {
|
|
646
576
|
this.eventEmitter.emit(auth_events_1.AuthEvents.ACCOUNT_SECURED, { uid: dto.uid });
|
|
647
577
|
}
|
|
@@ -649,33 +579,22 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
649
579
|
}
|
|
650
580
|
// --- MAGIC LINK ---
|
|
651
581
|
async requestMagicLink(dto) {
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
WHERE ai.value = $1 LIMIT 1`, [dto.email.toLowerCase()]);
|
|
655
|
-
let authId;
|
|
656
|
-
let uid;
|
|
657
|
-
if (!identifier[0]) {
|
|
582
|
+
const result = await this.authIdentifierRepo.findWithAuthByValue(dto.email.toLowerCase());
|
|
583
|
+
if (!result) {
|
|
658
584
|
// Optional: Auto-signup if not exists, but let's stick to existing for now
|
|
659
585
|
throw new common_1.BadRequestException('No account found with this email');
|
|
660
586
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
uid
|
|
664
|
-
|
|
665
|
-
const token = crypto.randomBytes(32).toString('hex');
|
|
666
|
-
const hash = await bcrypt.hash(token, 10);
|
|
667
|
-
const expiresAt = new Date();
|
|
668
|
-
expiresAt.setMinutes(expiresAt.getMinutes() + 15);
|
|
669
|
-
await this.otpRepo.save(this.otpRepo.create({
|
|
587
|
+
const { auth: primaryAuthObj } = result;
|
|
588
|
+
const issueResult = await this.resolveOtpProvider('email').issue({
|
|
589
|
+
uid: primaryAuthObj.uid,
|
|
590
|
+
authId: primaryAuthObj.id,
|
|
670
591
|
identifier: dto.email.toLowerCase(),
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
if (this.notificationProvider?.sendMagicLink) {
|
|
678
|
-
const link = `${this.options.frontendUrl || ''}/auth/magic-callback?token=${token}&email=${dto.email}`;
|
|
592
|
+
identifierType: 'email',
|
|
593
|
+
purpose: otp_purpose_enum_1.OtpPurpose.MAGIC_LINK,
|
|
594
|
+
expiresIn: 15
|
|
595
|
+
});
|
|
596
|
+
if (this.notificationProvider?.sendMagicLink && issueResult.code) {
|
|
597
|
+
const link = `${this.options.frontendUrl || ''}/auth/magic-callback?token=${issueResult.code}&email=${dto.email}`;
|
|
679
598
|
await this.notificationProvider.sendMagicLink(dto.email, link);
|
|
680
599
|
}
|
|
681
600
|
if (this.eventEmitter) {
|
|
@@ -684,19 +603,18 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
684
603
|
return { message: 'Magic link sent to your email.' };
|
|
685
604
|
}
|
|
686
605
|
async verifyMagicLink({ dto, userAgent, ip, namespace }) {
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
606
|
+
const identifier = await this.authIdentifierRepo.findWithAuthByValue(dto.email.toLowerCase());
|
|
607
|
+
if (!identifier)
|
|
608
|
+
throw new common_1.BadRequestException('Identity not found');
|
|
609
|
+
const result = await this.resolveOtpProvider('email').verify({
|
|
610
|
+
uid: identifier.auth.uid,
|
|
611
|
+
code: dto.token,
|
|
612
|
+
purpose: otp_purpose_enum_1.OtpPurpose.MAGIC_LINK
|
|
690
613
|
});
|
|
691
|
-
if (!
|
|
614
|
+
if (!result.success || !result.authId) {
|
|
692
615
|
throw new common_1.BadRequestException('Invalid or expired magic link');
|
|
693
616
|
}
|
|
694
|
-
const
|
|
695
|
-
if (!isMatch)
|
|
696
|
-
throw new common_1.BadRequestException('Invalid magic link');
|
|
697
|
-
otp.isUsed = true;
|
|
698
|
-
await this.otpRepo.save(otp);
|
|
699
|
-
const auth = await this.authRepo.findOne({ where: { id: otp.requestAuthId } });
|
|
617
|
+
const auth = await this.authRepo.findById(result.authId);
|
|
700
618
|
if (!auth)
|
|
701
619
|
throw new common_1.BadRequestException('Identity not found');
|
|
702
620
|
const tokens = await this.createSession(auth.uid, userAgent, ip, namespace);
|
|
@@ -707,10 +625,10 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
707
625
|
}
|
|
708
626
|
// --- MFA (2FA) LOGIC ---
|
|
709
627
|
async enrollMfa(uid, type) {
|
|
710
|
-
if (type !==
|
|
628
|
+
if (type !== mfa_type_enum_1.MfaType.TOTP) {
|
|
711
629
|
throw new common_1.BadRequestException('Currently only TOTP MFA is supported');
|
|
712
630
|
}
|
|
713
|
-
let mfa = await this.mfaRepo.
|
|
631
|
+
let mfa = await this.mfaRepo.findByUidAndType(uid, type);
|
|
714
632
|
if (mfa?.isEnabled) {
|
|
715
633
|
throw new common_1.BadRequestException('MFA is already enabled for this account');
|
|
716
634
|
}
|
|
@@ -718,7 +636,7 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
718
636
|
const appName = this.options.appName || 'NestJS Auth';
|
|
719
637
|
const otpauth = otplib_1.authenticator.keyuri(uid, appName, secret);
|
|
720
638
|
if (!mfa) {
|
|
721
|
-
mfa = this.mfaRepo.create({
|
|
639
|
+
mfa = await this.mfaRepo.create({
|
|
722
640
|
uid,
|
|
723
641
|
type,
|
|
724
642
|
secret,
|
|
@@ -732,16 +650,10 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
732
650
|
if (this.eventEmitter) {
|
|
733
651
|
this.eventEmitter.emit(auth_events_1.AuthEvents.MFA_ENROLLED, { uid, type });
|
|
734
652
|
}
|
|
735
|
-
return {
|
|
736
|
-
secret,
|
|
737
|
-
otpauth,
|
|
738
|
-
};
|
|
653
|
+
return { secret, otpauth };
|
|
739
654
|
}
|
|
740
655
|
async activateMfa(uid, type, code) {
|
|
741
|
-
const mfa = await this.mfaRepo.
|
|
742
|
-
where: { uid, type },
|
|
743
|
-
select: ['id', 'secret', 'isEnabled']
|
|
744
|
-
});
|
|
656
|
+
const mfa = await this.mfaRepo.findByUidAndType(uid, type);
|
|
745
657
|
if (!mfa) {
|
|
746
658
|
throw new common_1.BadRequestException('No MFA enrollment found');
|
|
747
659
|
}
|
|
@@ -763,80 +675,81 @@ let AuthService = AuthService_1 = class AuthService {
|
|
|
763
675
|
}
|
|
764
676
|
return { message: 'MFA activated successfully' };
|
|
765
677
|
}
|
|
678
|
+
async mfaLogin(uid, code, userAgent, ip, namespace) {
|
|
679
|
+
const auth = await this.authRepo.findByUid(uid);
|
|
680
|
+
if (!auth)
|
|
681
|
+
throw new common_1.BadRequestException('Identity not found');
|
|
682
|
+
const mfa = await this.mfaRepo.findByUidAndEnabled(uid);
|
|
683
|
+
if (!mfa)
|
|
684
|
+
throw new common_1.BadRequestException('MFA is not enabled for this account');
|
|
685
|
+
const isValid = otplib_1.authenticator.verify({
|
|
686
|
+
token: code,
|
|
687
|
+
secret: mfa.secret,
|
|
688
|
+
});
|
|
689
|
+
if (!isValid)
|
|
690
|
+
throw new common_1.BadRequestException('Invalid MFA code');
|
|
691
|
+
const { secretHash, ...filteredAuth } = auth;
|
|
692
|
+
const tokens = await this.createSession(auth.uid, userAgent, ip, namespace);
|
|
693
|
+
if (this.eventEmitter) {
|
|
694
|
+
this.eventEmitter.emit(auth_events_1.AuthEvents.LOGIN, { auth: filteredAuth, tokens });
|
|
695
|
+
}
|
|
696
|
+
return { message: 'Login successful', tokens, auth: filteredAuth };
|
|
697
|
+
}
|
|
766
698
|
async viewAll() {
|
|
767
|
-
const auths = await this.authRepo.
|
|
699
|
+
const auths = await this.authRepo.findAll();
|
|
768
700
|
return auth_mapper_1.AuthMapper.toDtoList(auths);
|
|
769
701
|
}
|
|
770
702
|
async me(uid) {
|
|
771
|
-
const
|
|
772
|
-
return auth_mapper_1.AuthMapper.toDto(
|
|
703
|
+
const auths = await this.authRepo.findAllByUid(uid);
|
|
704
|
+
return auth_mapper_1.AuthMapper.toDto(auths[0]);
|
|
773
705
|
}
|
|
774
706
|
async viewAllMyAuthMethods(uid) {
|
|
775
|
-
const auths = await this.authRepo.
|
|
707
|
+
const auths = await this.authRepo.findAllByUid(uid);
|
|
776
708
|
return auth_mapper_1.AuthMapper.toDtoList(auths);
|
|
777
709
|
}
|
|
778
710
|
async deleteAccount(uid) {
|
|
779
711
|
// 1. Delete all sessions for this UID
|
|
780
|
-
await this.invalidateSessions({ uid, event:
|
|
712
|
+
await this.invalidateSessions({ uid, event: session_event_enum_1.SessionEvent.DELETE, reason: 'User deleted account' });
|
|
781
713
|
// 2. Delete all MFA methods for this UID
|
|
782
|
-
await this.mfaRepo.
|
|
783
|
-
// 3. Delete all
|
|
784
|
-
await this.
|
|
785
|
-
// 4. Delete all Auth methods for this UID.
|
|
786
|
-
// TypeORM should handle cascading deletion of AuthIdentifier and OAuthProvider if configured.
|
|
787
|
-
// However, if not configured with ON DELETE CASCADE in the DB, it's safer to use repo.remove or repo.delete.
|
|
788
|
-
// Using repo.delete with uid will delete all matching Auth records.
|
|
789
|
-
await this.authRepo.delete({ uid });
|
|
714
|
+
await this.mfaRepo.deleteByUid(uid);
|
|
715
|
+
// 3. Delete all Auth methods for this UID.
|
|
716
|
+
await this.authRepo.deleteByUid(uid);
|
|
790
717
|
}
|
|
791
718
|
async deleteAuthMethod(uid, authId) {
|
|
792
|
-
const auth = await this.authRepo.
|
|
793
|
-
if (!auth) {
|
|
719
|
+
const auth = await this.authRepo.findById(authId);
|
|
720
|
+
if (!auth || auth.uid !== uid) {
|
|
794
721
|
throw new common_1.BadRequestException('Authentication method not found or does not belong to user');
|
|
795
722
|
}
|
|
796
723
|
// Check if this is the last auth method
|
|
797
|
-
const
|
|
798
|
-
if (
|
|
724
|
+
const allAuths = await this.authRepo.findAllByUid(uid);
|
|
725
|
+
if (allAuths.length <= 1) {
|
|
799
726
|
throw new common_1.BadRequestException('Cannot delete the last authentication method. Delete account instead.');
|
|
800
727
|
}
|
|
801
|
-
// If deleting the primary auth method, we might need to assign a new one
|
|
802
|
-
if (auth.isPrimary) {
|
|
803
|
-
const nextAuth = await this.authRepo.findOne({ where: { uid, id: authId } }); // This is wrong, should be NOT authId
|
|
804
|
-
}
|
|
805
|
-
// Actually, let's keep it simple for now: just delete it.
|
|
806
|
-
// If it was primary, the next available one should ideally become primary.
|
|
807
728
|
await this.authRepo.delete(authId);
|
|
808
729
|
// After deletion, find if there's any primary left.
|
|
809
730
|
// If not, assign the first available one as primary.
|
|
810
|
-
const
|
|
811
|
-
if (!
|
|
812
|
-
const remainingAuth =
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
await this.authRepo.save(remainingAuth);
|
|
816
|
-
}
|
|
731
|
+
const remainingAuths = await this.authRepo.findAllByUid(uid);
|
|
732
|
+
if (remainingAuths.length > 0 && !remainingAuths.some(a => a.isPrimary)) {
|
|
733
|
+
const remainingAuth = remainingAuths[0];
|
|
734
|
+
remainingAuth.isPrimary = true;
|
|
735
|
+
await this.authRepo.save(remainingAuth);
|
|
817
736
|
}
|
|
818
737
|
}
|
|
819
738
|
async getSessions({ uid, namespace }) {
|
|
820
|
-
const
|
|
821
|
-
if (
|
|
822
|
-
|
|
739
|
+
const sessions = await this.sessionRepository.findByUid(uid);
|
|
740
|
+
if (namespace !== undefined) {
|
|
741
|
+
return sessions.filter(s => s.namespace === namespace);
|
|
823
742
|
}
|
|
824
|
-
|
|
825
|
-
return (await sessions).map((d) => Object.assign(new session_entity_1.Session(), d.toMap()));
|
|
743
|
+
return sessions;
|
|
826
744
|
}
|
|
827
745
|
async getSession(id) {
|
|
828
|
-
return this.sessionRepository.
|
|
746
|
+
return this.sessionRepository.findById(id);
|
|
829
747
|
}
|
|
830
748
|
async revokeSession({ sessionId }) {
|
|
831
|
-
await this.invalidateSession({ sessionId, event:
|
|
749
|
+
await this.invalidateSession({ sessionId, event: session_event_enum_1.SessionEvent.REVOKE, reason: 'User requested session revoke' });
|
|
832
750
|
}
|
|
833
751
|
async getSessionLogs({ uid, namespace }) {
|
|
834
|
-
|
|
835
|
-
if (typeof namespace === "string") {
|
|
836
|
-
whereClause.namespace = namespace;
|
|
837
|
-
}
|
|
838
|
-
const sessions = this.sessionLogRepo.find({ where: whereClause, relations: [] });
|
|
839
|
-
return (await sessions).map((d) => Object.assign(new session_log_entity_1.SessionLog(), d.toMap()));
|
|
752
|
+
return this.sessionLogRepo.findByUidAndNamespace(uid, namespace);
|
|
840
753
|
}
|
|
841
754
|
};
|
|
842
755
|
exports.AuthService = AuthService;
|
|
@@ -844,22 +757,20 @@ exports.AuthService = AuthService = AuthService_1 = __decorate([
|
|
|
844
757
|
(0, common_1.Injectable)(),
|
|
845
758
|
__param(1, (0, common_1.Optional)()),
|
|
846
759
|
__param(2, (0, common_1.Optional)()),
|
|
847
|
-
__param(3, (0,
|
|
848
|
-
__param(4, (0,
|
|
849
|
-
__param(5, (0,
|
|
850
|
-
__param(6, (0,
|
|
851
|
-
__param(7, (0,
|
|
852
|
-
__param(8, (0, common_1.Inject)(
|
|
853
|
-
__param(9, (0, common_1.
|
|
854
|
-
__param(
|
|
855
|
-
__param(
|
|
760
|
+
__param(3, (0, common_1.Inject)(repository_tokens_1.SESSION_REPOSITORY_TOKEN)),
|
|
761
|
+
__param(4, (0, common_1.Inject)(repository_tokens_1.SESSION_LOG_REPOSITORY_TOKEN)),
|
|
762
|
+
__param(5, (0, common_1.Inject)(repository_tokens_1.AUTH_REPOSITORY_TOKEN)),
|
|
763
|
+
__param(6, (0, common_1.Inject)(repository_tokens_1.AUTH_IDENTIFIER_REPOSITORY_TOKEN)),
|
|
764
|
+
__param(7, (0, common_1.Inject)(auth_otp_provider_interface_1.AUTH_OTP_PROVIDER)),
|
|
765
|
+
__param(8, (0, common_1.Inject)(auth_otp_provider_interface_1.AUTH_OTP_PROVIDER_EMAIL)),
|
|
766
|
+
__param(9, (0, common_1.Inject)(auth_otp_provider_interface_1.AUTH_OTP_PROVIDER_PHONE)),
|
|
767
|
+
__param(10, (0, common_1.Inject)(repository_tokens_1.MFA_METHOD_REPOSITORY_TOKEN)),
|
|
768
|
+
__param(11, (0, common_1.Inject)(auth_module_options_interface_1.AUTH_MODULE_OPTIONS)),
|
|
769
|
+
__param(12, (0, common_1.Optional)()),
|
|
770
|
+
__param(12, (0, common_1.Inject)(auth_notification_provider_interface_1.AUTH_NOTIFICATION_PROVIDER)),
|
|
771
|
+
__param(13, (0, common_1.Optional)()),
|
|
856
772
|
__metadata("design:paramtypes", [jwt_1.JwtService,
|
|
857
773
|
local_auth_strategy_1.LocalAuthStrategy,
|
|
858
|
-
oauth_strategy_1.OAuthAuthStrategy,
|
|
859
|
-
typeorm_2.Repository,
|
|
860
|
-
typeorm_2.Repository,
|
|
861
|
-
typeorm_2.Repository,
|
|
862
|
-
typeorm_2.Repository,
|
|
863
|
-
typeorm_2.Repository, Object, Object, event_emitter_1.EventEmitter2])
|
|
774
|
+
oauth_strategy_1.OAuthAuthStrategy, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, event_emitter_1.EventEmitter2])
|
|
864
775
|
], AuthService);
|
|
865
776
|
//# sourceMappingURL=auth.service.js.map
|