@@ -41,11 +41,65 @@ class AuthService {
4141
4242 /// Initiates the email sign-in process.
4343 ///
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 {
4865 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+
49103 // Generate and store the code for standard sign-in
50104 final code = await _verificationCodeStorageService
51105 .generateAndStoreSignInCode (email);
@@ -67,16 +121,23 @@ class AuthService {
67121
68122 /// Completes the email sign-in process by verifying the code.
69123 ///
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+ ///
72134 /// 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.
75135 Future <({User user, String token})> completeEmailSignIn (
76136 String email,
77137 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 ,
80141 }) async {
81142 // 1. Validate the code for standard sign-in
82143 final isValidCode = await _verificationCodeStorageService
@@ -97,146 +158,63 @@ class AuthService {
97158 );
98159 }
99160
100- // 2. Find or create the user, and migrate data if anonymous
161+ // 2. Find or create the user based on the context
101162 User user;
102163 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);
111167
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.
133176 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 .' ,
136178 );
137- // Log and continue, new defaults will be created.
179+ throw const UnauthorizedException ( 'User account does not exist.' );
138180 }
139181
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 (),
142191 email: email,
143- roles: [ UserRoles .standardUser] ,
192+ roles: roles ,
144193 );
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,
148202 );
203+ print ('Created default UserAppSettings for user: ${user .id }' );
149204
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 }' );
230212 }
231213 } 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.' );
236216 } 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 ' );
240218 throw const OperationFailedException ('Failed to process user account.' );
241219 }
242220
@@ -250,7 +228,7 @@ class AuthService {
250228 throw const OperationFailedException (
251229 'Failed to generate authentication token.' ,
252230 );
253- }
231+ }
254232 }
255233
256234 /// Performs anonymous sign-in.
0 commit comments