@@ -38,8 +38,9 @@ class AuthService {
38
38
/// Throws [OperationFailedException] if code generation/storage/email fails.
39
39
Future <void > initiateEmailSignIn (String email) async {
40
40
try {
41
- // Generate and store the code
42
- final code = await _verificationCodeStorageService.generateAndStoreCode (
41
+ // Generate and store the code for standard sign-in
42
+ final code =
43
+ await _verificationCodeStorageService.generateAndStoreSignInCode (
43
44
email,
44
45
);
45
46
@@ -71,20 +72,30 @@ class AuthService {
71
72
Future <({User user, String token})> completeEmailSignIn (
72
73
String email,
73
74
String code,
75
+ // User? currentAuthUser, // Parameter for potential future linking logic
74
76
) async {
75
- // 1. Validate the code
76
- final isValidCode = await _verificationCodeStorageService.validateCode (
77
+ // 1. Validate the code for standard sign-in
78
+ final isValidCode =
79
+ await _verificationCodeStorageService.validateSignInCode (
77
80
email,
78
81
code,
79
82
);
80
83
if (! isValidCode) {
81
- // Consider distinguishing between expired and simply incorrect codes
82
- // For now, treat both as invalid input.
83
84
throw const InvalidInputException (
84
85
'Invalid or expired verification code.' ,
85
86
);
86
87
}
87
88
89
+ // After successful code validation, clear the sign-in code
90
+ try {
91
+ await _verificationCodeStorageService.clearSignInCode (email);
92
+ } catch (e) {
93
+ // Log or handle if clearing fails, but don't let it block sign-in
94
+ print (
95
+ 'Warning: Failed to clear sign-in code for $email after validation: $e ' ,
96
+ );
97
+ }
98
+
88
99
// 2. Find or create the user
89
100
User user;
90
101
try {
@@ -213,4 +224,153 @@ class AuthService {
213
224
);
214
225
// No specific exceptions are thrown in this placeholder implementation.
215
226
}
227
+
228
+ /// Initiates the process of linking an [emailToLink] to an existing
229
+ /// authenticated [anonymousUser] 's account.
230
+ ///
231
+ /// Throws [ConflictException] if the [emailToLink] is already in use by
232
+ /// another permanent account, or if the [anonymousUser] is not actually
233
+ /// anonymous, or if the [emailToLink] is already pending verification for
234
+ /// another linking process.
235
+ /// Throws [OperationFailedException] for other errors.
236
+ Future <void > initiateLinkEmailProcess ({
237
+ required User anonymousUser,
238
+ required String emailToLink,
239
+ }) async {
240
+ if (! anonymousUser.isAnonymous) {
241
+ throw const BadRequestException (
242
+ 'Account is already permanent. Cannot link email.' ,
243
+ );
244
+ }
245
+
246
+ try {
247
+ // 1. Check if emailToLink is already used by another *permanent* user.
248
+ final query = {'email' : emailToLink, 'isAnonymous' : false };
249
+ final existingUsers = await _userRepository.readAllByQuery (query);
250
+ if (existingUsers.items.isNotEmpty) {
251
+ // Ensure it's not the same user if somehow an anonymous user had an email
252
+ // (though current logic prevents this for new anonymous users).
253
+ // This check is more for emails used by *other* permanent accounts.
254
+ if (existingUsers.items.any ((u) => u.id != anonymousUser.id)) {
255
+ throw ConflictException (
256
+ 'Email address "$emailToLink " is already in use by another account.' ,
257
+ );
258
+ }
259
+ }
260
+
261
+ // 2. Generate and store the link code.
262
+ // The storage service itself might throw ConflictException if emailToLink
263
+ // is pending for another user or if this user has a pending code.
264
+ final code =
265
+ await _verificationCodeStorageService.generateAndStoreLinkCode (
266
+ userId: anonymousUser.id,
267
+ emailToLink: emailToLink,
268
+ );
269
+
270
+ // 3. Send the code via email
271
+ await _emailRepository.sendOtpEmail (
272
+ recipientEmail: emailToLink,
273
+ otpCode: code,
274
+ );
275
+ print (
276
+ 'Initiated email link for user ${anonymousUser .id } to email $emailToLink , code sent.' ,
277
+ );
278
+ } on HtHttpException {
279
+ rethrow ;
280
+ } catch (e) {
281
+ print (
282
+ 'Error during initiateLinkEmailProcess for user ${anonymousUser .id }, email $emailToLink : $e ' ,
283
+ );
284
+ throw OperationFailedException (
285
+ 'Failed to initiate email linking process: ${e .toString ()}' ,
286
+ );
287
+ }
288
+ }
289
+
290
+ /// Completes the email linking process for an [anonymousUser] by verifying
291
+ /// the [codeFromUser] .
292
+ ///
293
+ /// If successful, updates the user to be permanent with the linked email
294
+ /// and returns the updated User and a new authentication token.
295
+ /// Throws [InvalidInputException] if the code is invalid or expired.
296
+ /// Throws [OperationFailedException] for other errors.
297
+ Future <({User user, String token})> completeLinkEmailProcess ({
298
+ required User anonymousUser,
299
+ required String codeFromUser,
300
+ required String oldAnonymousToken, // Needed to invalidate it
301
+ }) async {
302
+ if (! anonymousUser.isAnonymous) {
303
+ // Should ideally not happen if flow is correct, but good safeguard.
304
+ throw const BadRequestException (
305
+ 'Account is already permanent. Cannot complete email linking.' ,
306
+ );
307
+ }
308
+
309
+ try {
310
+ // 1. Validate the link code and retrieve the email that was being linked.
311
+ final linkedEmail =
312
+ await _verificationCodeStorageService.validateAndRetrieveLinkedEmail (
313
+ userId: anonymousUser.id,
314
+ linkCode: codeFromUser,
315
+ );
316
+
317
+ if (linkedEmail == null ) {
318
+ throw const InvalidInputException (
319
+ 'Invalid or expired verification code for email linking.' ,
320
+ );
321
+ }
322
+
323
+ // 2. Update the user to be permanent.
324
+ final updatedUser = User (
325
+ id: anonymousUser.id, // Preserve original ID
326
+ email: linkedEmail,
327
+ isAnonymous: false , // Now a permanent user
328
+ );
329
+ final permanentUser = await _userRepository.update (
330
+ updatedUser.id,
331
+ updatedUser,
332
+ );
333
+ print (
334
+ 'User ${permanentUser .id } successfully linked with email $linkedEmail .' ,
335
+ );
336
+
337
+ // 3. Generate a new authentication token for the now-permanent user.
338
+ final newToken = await _authTokenService.generateToken (permanentUser);
339
+ print ('Generated new token for linked user ${permanentUser .id }' );
340
+
341
+ // 4. Invalidate the old anonymous token.
342
+ try {
343
+ await _authTokenService.invalidateToken (oldAnonymousToken);
344
+ print (
345
+ 'Successfully invalidated old anonymous token for user ${permanentUser .id }.' ,
346
+ );
347
+ } catch (e) {
348
+ // Log error but don't fail the whole linking process if invalidation fails.
349
+ // The new token is more important.
350
+ print (
351
+ 'Warning: Failed to invalidate old anonymous token for user ${permanentUser .id }: $e ' ,
352
+ );
353
+ }
354
+
355
+ // 5. Clear the link code from storage.
356
+ try {
357
+ await _verificationCodeStorageService.clearLinkCode (anonymousUser.id);
358
+ } catch (e) {
359
+ print (
360
+ 'Warning: Failed to clear link code for user ${anonymousUser .id } after linking: $e ' ,
361
+ );
362
+ }
363
+
364
+ return (user: permanentUser, token: newToken);
365
+ } on HtHttpException {
366
+ rethrow ;
367
+ } catch (e) {
368
+ print (
369
+ 'Error during completeLinkEmailProcess for user ${anonymousUser .id }: $e ' ,
370
+ );
371
+ throw OperationFailedException (
372
+ 'Failed to complete email linking process: ${e .toString ()}' ,
373
+ );
374
+ }
375
+ }
216
376
}
0 commit comments