1+ import 'dart:async' ;
2+
13import 'package:ht_api/src/rbac/permission_service.dart' ;
24import 'package:ht_api/src/rbac/permissions.dart' ;
35import 'package:ht_api/src/services/auth_token_service.dart' ;
@@ -116,29 +118,21 @@ class AuthService {
116118 }
117119 }
118120
119- /// Completes the email sign-in process by verifying the code.
120- ///
121- /// This method is context-aware based on the [isDashboardLogin] flag.
122- ///
123- /// - For the dashboard (`isDashboardLogin: true` ), it validates the code and
124- /// logs in the existing user. It will not create a new user in this flow.
125- /// - For the user-facing app (`isDashboardLogin: false` ), it validates the
126- /// code and either logs in the existing user or creates a new one with a
127- /// 'standardUser' role if they don't exist.
128- ///
129- /// Returns the authenticated [User] and a new authentication token.
130- ///
131- /// Throws [InvalidInputException] if the code is invalid or expired.
132121 /// Completes the email sign-in process by verifying the code.
133122 ///
134123 /// This method is context-aware and handles multiple scenarios:
135124 ///
136- /// - **Guest to Permanent Conversion:** If an authenticated `guestUser`
137- /// (from [authenticatedUser] ) performs this action, their account is
138- /// upgraded to a permanent `standardUser` with the verified [email] .
139- /// Their existing data is preserved.
125+ /// - **Guest Sign-In:** If an authenticated `guestUser` (from
126+ /// [authenticatedUser] ) performs this action, the service checks if a
127+ /// permanent account with the verified [email] already exists.
128+ /// - If it exists, the user is signed into that account, the old guest
129+ /// token is invalidated, and the temporary guest account is deleted.
130+ /// - If it does not exist, the guest account is converted into a new
131+ /// permanent `standardUser`, and the old guest token is invalidated.
132+ ///
140133 /// - **Dashboard Login:** If [isDashboardLogin] is true, it performs a
141134 /// strict login for an existing user with dashboard permissions.
135+ ///
142136 /// - **Standard Sign-In/Sign-Up:** If no authenticated user is present, it
143137 /// either logs in an existing user with the given [email] or creates a
144138 /// new `standardUser` .
@@ -151,6 +145,7 @@ class AuthService {
151145 String code, {
152146 required bool isDashboardLogin,
153147 User ? authenticatedUser,
148+ String ? currentToken,
154149 }) async {
155150 // 1. Validate the verification code.
156151 final isValidCode =
@@ -168,21 +163,73 @@ class AuthService {
168163 );
169164 }
170165
171- // 2. Check for Guest-to-Permanent user conversion flow.
166+ // 2. If this is a guest flow, invalidate the old anonymous token.
167+ // This is a fire-and-forget operation; we don't want to block the
168+ // login if invalidation fails, but we should log any errors.
169+ if (authenticatedUser != null &&
170+ authenticatedUser.appRole == AppUserRole .guestUser &&
171+ currentToken != null ) {
172+ unawaited (
173+ _authTokenService.invalidateToken (currentToken).catchError ((e, s) {
174+ _log.warning (
175+ 'Failed to invalidate old anonymous token for user ${authenticatedUser .id }.' ,
176+ e,
177+ s is StackTrace ? s : null ,
178+ );
179+ }),
180+ );
181+ }
182+
183+ // 3. Check if the sign-in is initiated from an authenticated guest session.
172184 if (authenticatedUser != null &&
173185 authenticatedUser.appRole == AppUserRole .guestUser) {
174186 _log.info (
175- 'Starting account conversion for guest user ${authenticatedUser .id } to email $email .' ,
176- );
177- return _convertGuestUserToPermanent (
178- guestUser: authenticatedUser,
179- verifiedEmail: email,
187+ 'Guest user ${authenticatedUser .id } is attempting to sign in with email $email .' ,
180188 );
181- }
182189
183- // 3. If not a conversion, proceed with standard or dashboard login.
190+ // Check if an account with the target email already exists.
191+ final existingUser = await _findUserByEmail (email);
192+
193+ if (existingUser != null ) {
194+ // --- Scenario A: Sign-in to an existing account ---
195+ // The user wants to log into their existing account, abandoning the
196+ // guest session.
197+ _log.info (
198+ 'Existing account found for email $email (ID: ${existingUser .id }). '
199+ 'Signing in and abandoning guest session ${authenticatedUser .id }.' ,
200+ );
184201
185- // Find or create the user based on the context.
202+ // Delete the now-orphaned anonymous user account and its data.
203+ // This is a fire-and-forget operation; we don't want to block the
204+ // login if cleanup fails, but we should log any errors.
205+ unawaited (
206+ deleteAccount (userId: authenticatedUser.id).catchError ((e, s) {
207+ _log.severe (
208+ 'Failed to clean up orphaned anonymous user ${authenticatedUser .id } after sign-in.' ,
209+ e,
210+ s is StackTrace ? s : null ,
211+ );
212+ }),
213+ );
214+
215+ // Generate a new token for the existing permanent user.
216+ final token = await _authTokenService.generateToken (existingUser);
217+ _log.info ('Generated new token for existing user ${existingUser .id }.' );
218+ return (user: existingUser, token: token);
219+ } else {
220+ // --- Scenario B: Convert guest to a new permanent account ---
221+ // No account exists with this email, so proceed with conversion.
222+ _log.info (
223+ 'No existing account for $email . Converting guest user ${authenticatedUser .id } to a new permanent account.' ,
224+ );
225+ return _convertGuestUserToPermanent (
226+ guestUser: authenticatedUser,
227+ verifiedEmail: email,
228+ );
229+ }
230+ }
231+
232+ // 4. If not a guest flow, proceed with standard or dashboard login.
186233 User user;
187234 try {
188235 // Attempt to find user by email
@@ -258,7 +305,7 @@ class AuthService {
258305 throw const OperationFailedException ('Failed to process user account.' );
259306 }
260307
261- // 3 . Generate authentication token
308+ // 4 . Generate authentication token
262309 try {
263310 final token = await _authTokenService.generateToken (user);
264311 _log.info ('Generated token for user ${user .id }' );
@@ -507,29 +554,20 @@ class AuthService {
507554 }
508555 }
509556
510- /// Converts a guest user to a permanent standard user.
557+ /// Converts a guest user to a new permanent standard user.
511558 ///
512559 /// This helper method encapsulates the logic for updating the user's
513- /// record with a verified email, upgrading their role, and generating a new
514- /// authentication token. It ensures that all associated user data is
515- /// preserved during the conversion.
516- ///
517- /// Throws [ConflictException] if the target email is already in use by
518- /// another permanent account.
560+ /// record with a verified email and upgrading their role. It assumes that
561+ /// the target email is not already in use by another account.
519562 Future <({User user, String token})> _convertGuestUserToPermanent ({
520563 required User guestUser,
521564 required String verifiedEmail,
522565 }) async {
523- // 1. Check if the target email is already in use by another permanent user.
524- final existingUser = await _findUserByEmail (verifiedEmail);
525- if (existingUser != null && existingUser.id != guestUser.id) {
526- // If a different user already exists with this email, throw an error.
527- throw ConflictException (
528- 'This email address is already associated with another account.' ,
529- );
530- }
566+ // The check for an existing user with the verifiedEmail is now handled
567+ // by the calling method, `completeEmailSignIn`. This method now only
568+ // handles the conversion itself.
531569
532- // 2 . Update the guest user's details to make them permanent.
570+ // 1 . Update the guest user's details to make them permanent.
533571 final updatedUser = guestUser.copyWith (
534572 email: verifiedEmail,
535573 appRole: AppUserRole .standardUser,
@@ -543,7 +581,7 @@ class AuthService {
543581 'User ${permanentUser .id } successfully converted to permanent account with email $verifiedEmail .' ,
544582 );
545583
546- // 3 . Generate a new token for the now-permanent user.
584+ // 2 . Generate a new token for the now-permanent user.
547585 final newToken = await _authTokenService.generateToken (permanentUser);
548586 _log.info ('Generated new token for converted user ${permanentUser .id }' );
549587
0 commit comments