Skip to content

Commit f046806

Browse files
authored
Merge pull request #23 from headlines-toolkit/refactor_enhance_auth_logic
Refactor enhance auth logic
2 parents 66a6d6f + af9d55f commit f046806

File tree

2 files changed

+92
-45
lines changed

2 files changed

+92
-45
lines changed

lib/src/services/auth_service.dart

Lines changed: 82 additions & 44 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, 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

routes/api/v1/auth/verify-code.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,24 @@ Future<Response> onRequest(RequestContext context) async {
6969
// Check for the optional dashboard login flag. Default to false.
7070
final isDashboardLogin = (body['isDashboardLogin'] as bool?) ?? false;
7171

72+
// Extract the current token from the Authorization header, if it exists.
73+
// This is needed for the guest-to-permanent flow to invalidate the old token.
74+
final authHeader = context.request.headers[HttpHeaders.authorizationHeader];
75+
String? currentToken;
76+
if (authHeader != null && authHeader.startsWith('Bearer ')) {
77+
currentToken = authHeader.substring(7);
78+
}
79+
7280
try {
7381
// Call the AuthService to handle the verification and sign-in logic.
7482
// Pass the authenticatedUser to allow for anonymous-to-permanent account
75-
// conversion.
83+
// conversion, and the currentToken for invalidation.
7684
final result = await authService.completeEmailSignIn(
7785
email,
7886
code,
7987
isDashboardLogin: isDashboardLogin,
8088
authenticatedUser: authenticatedUser,
89+
currentToken: currentToken,
8190
);
8291

8392
// Create the specific payload containing user and token

0 commit comments

Comments
 (0)