@@ -41,11 +41,65 @@ class AuthService {
41
41
42
42
/// Initiates the email sign-in process.
43
43
///
44
- /// Generates a verification code, stores it, and sends it via email.
45
- /// Throws [InvalidInputException] for invalid email format (via email client).
46
- /// Throws [OperationFailedException] if code generation/storage/email fails.
47
- Future <void > initiateEmailSignIn (String email) async {
44
+ /// This method is context-aware based on the [isDashboardLogin] flag.
45
+ ///
46
+ /// - For the user-facing app (`isDashboardLogin: false` ), it generates and
47
+ /// sends a verification code to the given [email] without pre-validation,
48
+ /// supporting a unified sign-in/sign-up flow.
49
+ /// - For the dashboard (`isDashboardLogin: true` ), it performs a strict
50
+ /// login-only check. It verifies that a user with the given [email] exists
51
+ /// and has either the 'admin' or 'publisher' role *before* sending a code.
52
+ ///
53
+ /// - [email] : The email address to send the code to.
54
+ /// - [isDashboardLogin] : A flag to indicate if this is a login attempt from
55
+ /// the dashboard, which enforces stricter checks.
56
+ ///
57
+ /// Throws [UnauthorizedException] if `isDashboardLogin` is true and the user
58
+ /// does not exist.
59
+ /// Throws [ForbiddenException] if `isDashboardLogin` is true and the user
60
+ /// exists but lacks the required roles.
61
+ Future <void > initiateEmailSignIn (
62
+ String email, {
63
+ bool isDashboardLogin = false ,
64
+ }) async {
48
65
try {
66
+ // For dashboard login, first validate the user exists and has permissions.
67
+ if (isDashboardLogin) {
68
+ print ('Dashboard login initiated for $email . Verifying user...' );
69
+ User ? user;
70
+ try {
71
+ final query = {'email' : email};
72
+ final response = await _userRepository.readAllByQuery (query);
73
+ if (response.items.isNotEmpty) {
74
+ user = response.items.first;
75
+ }
76
+ } on HtHttpException catch (e) {
77
+ print ('Repository error while verifying dashboard user $email : $e ' );
78
+ rethrow ;
79
+ }
80
+
81
+ if (user == null ) {
82
+ print ('Dashboard login failed: User $email not found.' );
83
+ throw const UnauthorizedException (
84
+ 'This email address is not registered for dashboard access.' ,
85
+ );
86
+ }
87
+
88
+ final hasRequiredRole =
89
+ user.roles.contains (UserRoles .admin) ||
90
+ user.roles.contains (UserRoles .publisher);
91
+
92
+ if (! hasRequiredRole) {
93
+ print (
94
+ 'Dashboard login failed: User ${user .id } lacks required roles.' ,
95
+ );
96
+ throw const ForbiddenException (
97
+ 'Your account does not have the required permissions to sign in.' ,
98
+ );
99
+ }
100
+ print ('Dashboard user ${user .id } verified successfully.' );
101
+ }
102
+
49
103
// Generate and store the code for standard sign-in
50
104
final code = await _verificationCodeStorageService
51
105
.generateAndStoreSignInCode (email);
@@ -67,16 +121,23 @@ class AuthService {
67
121
68
122
/// Completes the email sign-in process by verifying the code.
69
123
///
70
- /// If the code is valid, finds or creates the user, generates an auth token.
71
- /// Returns the authenticated User and the generated token.
124
+ /// This method is context-aware based on the [isDashboardLogin] flag.
125
+ ///
126
+ /// - For the dashboard (`isDashboardLogin: true` ), it validates the code and
127
+ /// logs in the existing user. It will not create a new user in this flow.
128
+ /// - For the user-facing app (`isDashboardLogin: false` ), it validates the
129
+ /// code and either logs in the existing user or creates a new one with a
130
+ /// 'standardUser' role if they don't exist.
131
+ ///
132
+ /// Returns the authenticated [User] and a new authentication token.
133
+ ///
72
134
/// Throws [InvalidInputException] if the code is invalid or expired.
73
- /// Throws [AuthenticationException] for specific code mismatch.
74
- /// Throws [OperationFailedException] for user lookup/creation or token errors.
75
135
Future <({User user, String token})> completeEmailSignIn (
76
136
String email,
77
137
String code, {
78
- User ? currentAuthUser, // Parameter for potential future linking logic
79
- String ? clientType, // e.g., 'dashboard', 'mobile_app'
138
+ // Flag to indicate if this is a login attempt from the dashboard,
139
+ // which enforces stricter checks.
140
+ bool isDashboardLogin = false ,
80
141
}) async {
81
142
// 1. Validate the code for standard sign-in
82
143
final isValidCode = await _verificationCodeStorageService
@@ -97,146 +158,63 @@ class AuthService {
97
158
);
98
159
}
99
160
100
- // 2. Find or create the user, and migrate data if anonymous
161
+ // 2. Find or create the user based on the context
101
162
User user;
102
163
try {
103
- if (currentAuthUser != null &&
104
- currentAuthUser.roles.contains (UserRoles .guestUser)) {
105
- // This is an anonymous user linking their account.
106
- // Migrate their existing data to the new permanent user.
107
- print (
108
- 'Anonymous user ${currentAuthUser .id } is linking email $email . '
109
- 'Migrating data...' ,
110
- );
164
+ // Attempt to find user by email
165
+ final query = {'email' : email};
166
+ final paginatedResponse = await _userRepository.readAllByQuery (query);
111
167
112
- // Fetch existing settings and preferences for the anonymous user
113
- UserAppSettings ? existingAppSettings;
114
- UserContentPreferences ? existingUserPreferences;
115
- try {
116
- existingAppSettings = await _userAppSettingsRepository.read (
117
- id: currentAuthUser.id,
118
- userId: currentAuthUser.id,
119
- );
120
- existingUserPreferences = await _userContentPreferencesRepository
121
- .read (id: currentAuthUser.id, userId: currentAuthUser.id);
122
- print (
123
- 'Fetched existing settings and preferences for anonymous user '
124
- '${currentAuthUser .id }.' ,
125
- );
126
- } on NotFoundException {
127
- print (
128
- 'No existing settings/preferences found for anonymous user '
129
- '${currentAuthUser .id }. Creating new ones.' ,
130
- );
131
- // If not found, proceed to create new ones later.
132
- } catch (e) {
168
+ if (paginatedResponse.items.isNotEmpty) {
169
+ user = paginatedResponse.items.first;
170
+ print ('Found existing user: ${user .id } for email $email ' );
171
+ } else {
172
+ // User not found.
173
+ if (isDashboardLogin) {
174
+ // This should not happen if the request-code flow is correct.
175
+ // It's a safeguard.
133
176
print (
134
- 'Error fetching existing settings/preferences for anonymous user '
135
- '${currentAuthUser .id }: $e ' ,
177
+ 'Error: Dashboard login verification failed for non-existent user $email .' ,
136
178
);
137
- // Log and continue, new defaults will be created.
179
+ throw const UnauthorizedException ( 'User account does not exist.' );
138
180
}
139
181
140
- // Update the existing anonymous user to be permanent
141
- user = currentAuthUser.copyWith (
182
+ // Create a new user for the standard app flow.
183
+ print ('User not found for $email , creating new user.' );
184
+
185
+ // All new users created via the public API get the standard role.
186
+ // Admin users must be provisioned out-of-band (e.g., via fixtures).
187
+ final roles = [UserRoles .standardUser];
188
+
189
+ user = User (
190
+ id: _uuid.v4 (),
142
191
email: email,
143
- roles: [ UserRoles .standardUser] ,
192
+ roles: roles ,
144
193
);
145
- user = await _userRepository.update (id: user.id, item: user);
146
- print (
147
- 'Updated anonymous user ${user .id } to permanent with email $email .' ,
194
+ user = await _userRepository.create (item: user);
195
+ print ('Created new user: ${user .id } with roles: ${user .roles }' );
196
+
197
+ // Create default UserAppSettings for the new user
198
+ final defaultAppSettings = UserAppSettings (id: user.id);
199
+ await _userAppSettingsRepository.create (
200
+ item: defaultAppSettings,
201
+ userId: user.id,
148
202
);
203
+ print ('Created default UserAppSettings for user: ${user .id }' );
149
204
150
- // Update or create UserAppSettings for the now-permanent user
151
- if (existingAppSettings != null ) {
152
- // Update existing settings with the new user ID (though it's the same)
153
- // and persist.
154
- await _userAppSettingsRepository.update (
155
- id: existingAppSettings.id,
156
- item: existingAppSettings.copyWith (id: user.id),
157
- userId: user.id,
158
- );
159
- print ('Migrated UserAppSettings for user: ${user .id }' );
160
- } else {
161
- // Create default settings if none existed for the anonymous user
162
- final defaultAppSettings = UserAppSettings (id: user.id);
163
- await _userAppSettingsRepository.create (
164
- item: defaultAppSettings,
165
- userId: user.id,
166
- );
167
- print ('Created default UserAppSettings for user: ${user .id }' );
168
- }
169
-
170
- // Update or create UserContentPreferences for the now-permanent user
171
- if (existingUserPreferences != null ) {
172
- // Update existing preferences with the new user ID (though it's the same)
173
- // and persist.
174
- await _userContentPreferencesRepository.update (
175
- id: existingUserPreferences.id,
176
- item: existingUserPreferences.copyWith (id: user.id),
177
- userId: user.id,
178
- );
179
- print ('Migrated UserContentPreferences for user: ${user .id }' );
180
- } else {
181
- // Create default preferences if none existed for the anonymous user
182
- final defaultUserPreferences = UserContentPreferences (id: user.id);
183
- await _userContentPreferencesRepository.create (
184
- item: defaultUserPreferences,
185
- userId: user.id,
186
- );
187
- print ('Created default UserContentPreferences for user: ${user .id }' );
188
- }
189
- } else {
190
- // Standard sign-in/sign-up flow (not anonymous linking)
191
- // Attempt to find user by email
192
- final query = {'email' : email};
193
- final paginatedResponse = await _userRepository.readAllByQuery (query);
194
-
195
- if (paginatedResponse.items.isNotEmpty) {
196
- user = paginatedResponse.items.first;
197
- print ('Found existing user: ${user .id } for email $email ' );
198
- } else {
199
- // User not found, create a new one
200
- print ('User not found for $email , creating new user.' );
201
- // Assign roles based on client type. New users from the dashboard
202
- // could be granted publisher rights, for example.
203
- final roles = (clientType == 'dashboard' )
204
- ? [UserRoles .standardUser, UserRoles .publisher]
205
- : [UserRoles .standardUser];
206
- user = User (
207
- id: _uuid.v4 (), // Generate new ID
208
- email: email,
209
- roles: roles,
210
- );
211
- user = await _userRepository.create (item: user); // Save the new user
212
- print ('Created new user: ${user .id }' );
213
-
214
- // Create default UserAppSettings for the new user
215
- final defaultAppSettings = UserAppSettings (id: user.id);
216
- await _userAppSettingsRepository.create (
217
- item: defaultAppSettings,
218
- userId: user.id, // Pass user ID for scoping
219
- );
220
- print ('Created default UserAppSettings for user: ${user .id }' );
221
-
222
- // Create default UserContentPreferences for the new user
223
- final defaultUserPreferences = UserContentPreferences (id: user.id);
224
- await _userContentPreferencesRepository.create (
225
- item: defaultUserPreferences,
226
- userId: user.id, // Pass user ID for scoping
227
- );
228
- print ('Created default UserContentPreferences for user: ${user .id }' );
229
- }
205
+ // Create default UserContentPreferences for the new user
206
+ final defaultUserPreferences = UserContentPreferences (id: user.id);
207
+ await _userContentPreferencesRepository.create (
208
+ item: defaultUserPreferences,
209
+ userId: user.id,
210
+ );
211
+ print ('Created default UserContentPreferences for user: ${user .id }' );
230
212
}
231
213
} on HtHttpException catch (e) {
232
- print ('Error finding/creating/migrating user for $email : $e ' );
233
- throw const OperationFailedException (
234
- 'Failed to find, create, or migrate user account.' ,
235
- );
214
+ print ('Error finding/creating user for $email : $e ' );
215
+ throw const OperationFailedException ('Failed to find or create user account.' );
236
216
} catch (e) {
237
- print (
238
- 'Unexpected error during user lookup/creation/migration for $email : $e ' ,
239
- );
217
+ print ('Unexpected error during user lookup/creation for $email : $e ' );
240
218
throw const OperationFailedException ('Failed to process user account.' );
241
219
}
242
220
@@ -250,7 +228,7 @@ class AuthService {
250
228
throw const OperationFailedException (
251
229
'Failed to generate authentication token.' ,
252
230
);
253
- }
231
+ }
254
232
}
255
233
256
234
/// Performs anonymous sign-in.
0 commit comments