1
+ import 'dart:async' ;
2
+
1
3
import 'package:ht_api/src/rbac/permission_service.dart' ;
2
4
import 'package:ht_api/src/rbac/permissions.dart' ;
3
5
import 'package:ht_api/src/services/auth_token_service.dart' ;
@@ -116,29 +118,21 @@ class AuthService {
116
118
}
117
119
}
118
120
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.
132
121
/// Completes the email sign-in process by verifying the code.
133
122
///
134
123
/// This method is context-aware and handles multiple scenarios:
135
124
///
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
+ ///
140
133
/// - **Dashboard Login:** If [isDashboardLogin] is true, it performs a
141
134
/// strict login for an existing user with dashboard permissions.
135
+ ///
142
136
/// - **Standard Sign-In/Sign-Up:** If no authenticated user is present, it
143
137
/// either logs in an existing user with the given [email] or creates a
144
138
/// new `standardUser` .
@@ -151,6 +145,7 @@ class AuthService {
151
145
String code, {
152
146
required bool isDashboardLogin,
153
147
User ? authenticatedUser,
148
+ String ? currentToken,
154
149
}) async {
155
150
// 1. Validate the verification code.
156
151
final isValidCode =
@@ -168,21 +163,73 @@ class AuthService {
168
163
);
169
164
}
170
165
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.
172
184
if (authenticatedUser != null &&
173
185
authenticatedUser.appRole == AppUserRole .guestUser) {
174
186
_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 .' ,
180
188
);
181
- }
182
189
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
+ );
184
201
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.
186
233
User user;
187
234
try {
188
235
// Attempt to find user by email
@@ -258,7 +305,7 @@ class AuthService {
258
305
throw const OperationFailedException ('Failed to process user account.' );
259
306
}
260
307
261
- // 3 . Generate authentication token
308
+ // 4 . Generate authentication token
262
309
try {
263
310
final token = await _authTokenService.generateToken (user);
264
311
_log.info ('Generated token for user ${user .id }' );
@@ -507,29 +554,20 @@ class AuthService {
507
554
}
508
555
}
509
556
510
- /// Converts a guest user to a permanent standard user.
557
+ /// Converts a guest user to a new permanent standard user.
511
558
///
512
559
/// 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.
519
562
Future <({User user, String token})> _convertGuestUserToPermanent ({
520
563
required User guestUser,
521
564
required String verifiedEmail,
522
565
}) 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.
531
569
532
- // 2 . Update the guest user's details to make them permanent.
570
+ // 1 . Update the guest user's details to make them permanent.
533
571
final updatedUser = guestUser.copyWith (
534
572
email: verifiedEmail,
535
573
appRole: AppUserRole .standardUser,
@@ -543,7 +581,7 @@ class AuthService {
543
581
'User ${permanentUser .id } successfully converted to permanent account with email $verifiedEmail .' ,
544
582
);
545
583
546
- // 3 . Generate a new token for the now-permanent user.
584
+ // 2 . Generate a new token for the now-permanent user.
547
585
final newToken = await _authTokenService.generateToken (permanentUser);
548
586
_log.info ('Generated new token for converted user ${permanentUser .id }' );
549
587
0 commit comments