Skip to content

Commit 01da8ab

Browse files
committed
refactor(auth_service): improve email sign-in flow for guest users
- Expand import to include dart:async for unawaited operation - Enhance documentation of completeEmailSignIn method - Refactor guest user sign-in logic to handle existing account check - Simplify _convertGuestUserToPermanent method by removing email conflict check
1 parent 66a6d6f commit 01da8ab

File tree

1 file changed

+63
-43
lines changed

1 file changed

+63
-43
lines changed

lib/src/services/auth_service.dart

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:async';
2+
13
import 'package:ht_api/src/rbac/permission_service.dart';
24
import 'package:ht_api/src/rbac/permissions.dart';
35
import '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, and the temporary
129+
/// guest account is deleted.
130+
/// - If it does not exist, the guest account is converted into a new
131+
/// permanent `standardUser` with the verified [email].
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`.
@@ -168,21 +162,56 @@ class AuthService {
168162
);
169163
}
170164

171-
// 2. Check for Guest-to-Permanent user conversion flow.
165+
// 2. Check if the sign-in is initiated from an authenticated guest session.
172166
if (authenticatedUser != null &&
173167
authenticatedUser.appRole == AppUserRole.guestUser) {
174168
_log.info(
175-
'Starting account conversion for guest user ${authenticatedUser.id} to email $email.',
176-
);
177-
return _convertGuestUserToPermanent(
178-
guestUser: authenticatedUser,
179-
verifiedEmail: email,
169+
'Guest user ${authenticatedUser.id} is attempting to sign in with email $email.',
180170
);
181-
}
182171

183-
// 3. If not a conversion, proceed with standard or dashboard login.
172+
// Check if an account with the target email already exists.
173+
final existingUser = await _findUserByEmail(email);
184174

185-
// Find or create the user based on the context.
175+
if (existingUser != null) {
176+
// --- Scenario A: Sign-in to an existing account ---
177+
// The user wants to log into their existing account, abandoning the
178+
// guest session.
179+
_log.info(
180+
'Existing account found for email $email (ID: ${existingUser.id}). '
181+
'Signing in and abandoning guest session ${authenticatedUser.id}.',
182+
);
183+
184+
// Delete the now-orphaned anonymous user account and its data.
185+
// This is a fire-and-forget operation; we don't want to block the
186+
// login if cleanup fails, but we should log any errors.
187+
unawaited(
188+
deleteAccount(userId: authenticatedUser.id).catchError((e, s) {
189+
_log.severe(
190+
'Failed to clean up orphaned anonymous user ${authenticatedUser.id} after sign-in.',
191+
e,
192+
s is StackTrace ? s : null,
193+
);
194+
}),
195+
);
196+
197+
// Generate a new token for the existing permanent user.
198+
final token = await _authTokenService.generateToken(existingUser);
199+
_log.info('Generated new token for existing user ${existingUser.id}.');
200+
return (user: existingUser, token: token);
201+
} else {
202+
// --- Scenario B: Convert guest to a new permanent account ---
203+
// No account exists with this email, so proceed with conversion.
204+
_log.info(
205+
'No existing account for $email. Converting guest user ${authenticatedUser.id} to a new permanent account.',
206+
);
207+
return _convertGuestUserToPermanent(
208+
guestUser: authenticatedUser,
209+
verifiedEmail: email,
210+
);
211+
}
212+
}
213+
214+
// 3. If not a guest flow, proceed with standard or dashboard login.
186215
User user;
187216
try {
188217
// Attempt to find user by email
@@ -507,29 +536,20 @@ class AuthService {
507536
}
508537
}
509538

510-
/// Converts a guest user to a permanent standard user.
539+
/// Converts a guest user to a new permanent standard user.
511540
///
512541
/// 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.
542+
/// record with a verified email and upgrading their role. It assumes that
543+
/// the target email is not already in use by another account.
519544
Future<({User user, String token})> _convertGuestUserToPermanent({
520545
required User guestUser,
521546
required String verifiedEmail,
522547
}) 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-
}
548+
// The check for an existing user with the verifiedEmail is now handled
549+
// by the calling method, `completeEmailSignIn`. This method now only
550+
// handles the conversion itself.
531551

532-
// 2. Update the guest user's details to make them permanent.
552+
// 1. Update the guest user's details to make them permanent.
533553
final updatedUser = guestUser.copyWith(
534554
email: verifiedEmail,
535555
appRole: AppUserRole.standardUser,
@@ -543,7 +563,7 @@ class AuthService {
543563
'User ${permanentUser.id} successfully converted to permanent account with email $verifiedEmail.',
544564
);
545565

546-
// 3. Generate a new token for the now-permanent user.
566+
// 2. Generate a new token for the now-permanent user.
547567
final newToken = await _authTokenService.generateToken(permanentUser);
548568
_log.info('Generated new token for converted user ${permanentUser.id}');
549569

0 commit comments

Comments
 (0)