Skip to content

Commit 3b116f8

Browse files
authored
Merge pull request #5 from headlines-toolkit/fix_data_migration_for_account_linking
Fix data migration for account linking
2 parents 0820116 + ecc8203 commit 3b116f8

File tree

2 files changed

+137
-40
lines changed

2 files changed

+137
-40
lines changed

lib/src/services/auth_service.dart

Lines changed: 127 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ class AuthService {
7474
/// Throws [OperationFailedException] for user lookup/creation or token errors.
7575
Future<({User user, String token})> completeEmailSignIn(
7676
String email,
77-
String code,
78-
// User? currentAuthUser, // Parameter for potential future linking logic
79-
) async {
77+
String code, {
78+
User? currentAuthUser, // Parameter for potential future linking logic
79+
}) async {
8080
// 1. Validate the code for standard sign-in
8181
final isValidCode = await _verificationCodeStorageService
8282
.validateSignInCode(email, code);
@@ -96,53 +96,141 @@ class AuthService {
9696
);
9797
}
9898

99-
// 2. Find or create the user
99+
// 2. Find or create the user, and migrate data if anonymous
100100
User user;
101101
try {
102-
// Attempt to find user by email (assuming a query method exists)
103-
// NOTE: HtDataRepository<User> currently lacks findByEmail.
104-
// We'll simulate this by querying all and filtering for now.
105-
// Replace with a proper query when available.
106-
final query = {'email': email}; // Hypothetical query
107-
final paginatedResponse = await _userRepository.readAllByQuery(query);
108-
109-
if (paginatedResponse.items.isNotEmpty) {
110-
user = paginatedResponse.items.first;
111-
print('Found existing user: ${user.id} for email $email');
112-
} else {
113-
// User not found, create a new one
114-
print('User not found for $email, creating new user.');
115-
user = User(
116-
id: _uuid.v4(), // Generate new ID
102+
if (currentAuthUser != null &&
103+
currentAuthUser.role == UserRole.guestUser) {
104+
// This is an anonymous user linking their account.
105+
// Migrate their existing data to the new permanent user.
106+
print(
107+
'Anonymous user ${currentAuthUser.id} is linking email $email. '
108+
'Migrating data...',
109+
);
110+
111+
// Fetch existing settings and preferences for the anonymous user
112+
UserAppSettings? existingAppSettings;
113+
UserContentPreferences? existingUserPreferences;
114+
try {
115+
existingAppSettings = await _userAppSettingsRepository.read(
116+
id: currentAuthUser.id,
117+
userId: currentAuthUser.id,
118+
);
119+
existingUserPreferences = await _userContentPreferencesRepository
120+
.read(id: currentAuthUser.id, userId: currentAuthUser.id);
121+
print(
122+
'Fetched existing settings and preferences for anonymous user '
123+
'${currentAuthUser.id}.',
124+
);
125+
} on NotFoundException {
126+
print(
127+
'No existing settings/preferences found for anonymous user '
128+
'${currentAuthUser.id}. Creating new ones.',
129+
);
130+
// If not found, proceed to create new ones later.
131+
} catch (e) {
132+
print(
133+
'Error fetching existing settings/preferences for anonymous user '
134+
'${currentAuthUser.id}: $e',
135+
);
136+
// Log and continue, new defaults will be created.
137+
}
138+
139+
// Update the existing anonymous user to be permanent
140+
user = currentAuthUser.copyWith(
117141
email: email,
118-
role: UserRole.standardUser, // Email verified user is standard user
142+
role: UserRole.standardUser,
119143
);
120-
user = await _userRepository.create(item: user); // Save the new user
121-
print('Created new user: ${user.id}');
122-
123-
// Create default UserAppSettings for the new user
124-
final defaultAppSettings = UserAppSettings(id: user.id);
125-
await _userAppSettingsRepository.create(
126-
item: defaultAppSettings,
127-
userId: user.id, // Pass user ID for scoping
144+
user = await _userRepository.update(id: user.id, item: user);
145+
print(
146+
'Updated anonymous user ${user.id} to permanent with email $email.',
128147
);
129-
print('Created default UserAppSettings for user: ${user.id}');
130148

131-
// Create default UserContentPreferences for the new user
132-
final defaultUserPreferences = UserContentPreferences(id: user.id);
133-
await _userContentPreferencesRepository.create(
134-
item: defaultUserPreferences,
135-
userId: user.id, // Pass user ID for scoping
136-
);
137-
print('Created default UserContentPreferences for user: ${user.id}');
149+
// Update or create UserAppSettings for the now-permanent user
150+
if (existingAppSettings != null) {
151+
// Update existing settings with the new user ID (though it's the same)
152+
// and persist.
153+
await _userAppSettingsRepository.update(
154+
id: existingAppSettings.id,
155+
item: existingAppSettings.copyWith(id: user.id),
156+
userId: user.id,
157+
);
158+
print('Migrated UserAppSettings for user: ${user.id}');
159+
} else {
160+
// Create default settings if none existed for the anonymous user
161+
final defaultAppSettings = UserAppSettings(id: user.id);
162+
await _userAppSettingsRepository.create(
163+
item: defaultAppSettings,
164+
userId: user.id,
165+
);
166+
print('Created default UserAppSettings for user: ${user.id}');
167+
}
168+
169+
// Update or create UserContentPreferences for the now-permanent user
170+
if (existingUserPreferences != null) {
171+
// Update existing preferences with the new user ID (though it's the same)
172+
// and persist.
173+
await _userContentPreferencesRepository.update(
174+
id: existingUserPreferences.id,
175+
item: existingUserPreferences.copyWith(id: user.id),
176+
userId: user.id,
177+
);
178+
print('Migrated UserContentPreferences for user: ${user.id}');
179+
} else {
180+
// Create default preferences if none existed for the anonymous user
181+
final defaultUserPreferences = UserContentPreferences(id: user.id);
182+
await _userContentPreferencesRepository.create(
183+
item: defaultUserPreferences,
184+
userId: user.id,
185+
);
186+
print('Created default UserContentPreferences for user: ${user.id}');
187+
}
188+
} else {
189+
// Standard sign-in/sign-up flow (not anonymous linking)
190+
// Attempt to find user by email
191+
final query = {'email': email};
192+
final paginatedResponse = await _userRepository.readAllByQuery(query);
193+
194+
if (paginatedResponse.items.isNotEmpty) {
195+
user = paginatedResponse.items.first;
196+
print('Found existing user: ${user.id} for email $email');
197+
} else {
198+
// User not found, create a new one
199+
print('User not found for $email, creating new user.');
200+
user = User(
201+
id: _uuid.v4(), // Generate new ID
202+
email: email,
203+
role: UserRole.standardUser, // Email verified user is standard user
204+
);
205+
user = await _userRepository.create(item: user); // Save the new user
206+
print('Created new user: ${user.id}');
207+
208+
// Create default UserAppSettings for the new user
209+
final defaultAppSettings = UserAppSettings(id: user.id);
210+
await _userAppSettingsRepository.create(
211+
item: defaultAppSettings,
212+
userId: user.id, // Pass user ID for scoping
213+
);
214+
print('Created default UserAppSettings for user: ${user.id}');
215+
216+
// Create default UserContentPreferences for the new user
217+
final defaultUserPreferences = UserContentPreferences(id: user.id);
218+
await _userContentPreferencesRepository.create(
219+
item: defaultUserPreferences,
220+
userId: user.id, // Pass user ID for scoping
221+
);
222+
print('Created default UserContentPreferences for user: ${user.id}');
223+
}
138224
}
139225
} on HtHttpException catch (e) {
140-
print('Error finding/creating user for $email: $e');
226+
print('Error finding/creating/migrating user for $email: $e');
141227
throw const OperationFailedException(
142-
'Failed to find or create user account.',
228+
'Failed to find, create, or migrate user account.',
143229
);
144230
} catch (e) {
145-
print('Unexpected error during user lookup/creation for $email: $e');
231+
print(
232+
'Unexpected error during user lookup/creation/migration for $email: $e',
233+
);
146234
throw const OperationFailedException('Failed to process user account.');
147235
}
148236

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ Future<Response> onRequest(RequestContext context) async {
1818
// Read the AuthService provided by middleware
1919
final authService = context.read<AuthService>();
2020

21+
// Read the authenticated User from context (provided by authentication middleware)
22+
// This user might be null (if not authenticated) or an anonymous user.
23+
final authenticatedUser = context.read<User?>();
24+
2125
// Parse the request body
2226
final dynamic body;
2327
try {
@@ -63,7 +67,12 @@ Future<Response> onRequest(RequestContext context) async {
6367

6468
try {
6569
// Call the AuthService to handle the verification and sign-in logic
66-
final result = await authService.completeEmailSignIn(email, code);
70+
// Pass the current authenticated user for potential data migration.
71+
final result = await authService.completeEmailSignIn(
72+
email,
73+
code,
74+
currentAuthUser: authenticatedUser,
75+
);
6776

6877
// Create the specific payload containing user and token
6978
final authPayload = AuthSuccessResponse(

0 commit comments

Comments
 (0)