@@ -38,8 +38,9 @@ class AuthService {
3838 /// Throws [OperationFailedException] if code generation/storage/email fails.
3939 Future <void > initiateEmailSignIn (String email) async {
4040 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 (
4344 email,
4445 );
4546
@@ -71,20 +72,30 @@ class AuthService {
7172 Future <({User user, String token})> completeEmailSignIn (
7273 String email,
7374 String code,
75+ // User? currentAuthUser, // Parameter for potential future linking logic
7476 ) 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 (
7780 email,
7881 code,
7982 );
8083 if (! isValidCode) {
81- // Consider distinguishing between expired and simply incorrect codes
82- // For now, treat both as invalid input.
8384 throw const InvalidInputException (
8485 'Invalid or expired verification code.' ,
8586 );
8687 }
8788
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+
8899 // 2. Find or create the user
89100 User user;
90101 try {
@@ -213,4 +224,153 @@ class AuthService {
213224 );
214225 // No specific exceptions are thrown in this placeholder implementation.
215226 }
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+ }
216376}
0 commit comments