@@ -2,8 +2,10 @@ import 'package:core/core.dart';
22import 'package:dart_frog/dart_frog.dart' ;
33import 'package:data_repository/data_repository.dart' ;
44import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/ownership_check_middleware.dart' ;
5+ import 'package:flutter_news_app_api_server_full_source_code/src/rbac/permission_service.dart' ;
56import 'package:flutter_news_app_api_server_full_source_code/src/services/country_query_service.dart' ;
67import 'package:flutter_news_app_api_server_full_source_code/src/services/dashboard_summary_service.dart' ;
8+ import 'package:logging/logging.dart' ;
79
810// --- Typedefs for Data Operations ---
911
@@ -54,6 +56,9 @@ typedef ItemDeleter =
5456/// data operations are performed for each model, improving consistency across
5557/// the API.
5658/// {@endtemplate}
59+
60+ final _log = Logger ('DataOperationRegistry' );
61+ 5762class DataOperationRegistry {
5863 /// {@macro data_operation_registry}
5964 DataOperationRegistry () {
@@ -188,11 +193,6 @@ class DataOperationRegistry {
188193 item: item as Language ,
189194 userId: uid,
190195 ),
191- // Handler for creating a new user.
192- 'user' : (c, item, uid) => c.read <DataRepository <User >>().create (
193- item: item as User ,
194- userId: uid,
195- ),
196196 'remote_config' : (c, item, uid) => c
197197 .read <DataRepository <RemoteConfig >>()
198198 .create (item: item as RemoteConfig , userId: uid),
@@ -225,56 +225,90 @@ class DataOperationRegistry {
225225 'language' : (c, id, item, uid) => c
226226 .read <DataRepository <Language >>()
227227 .update (id: id, item: item as Language , userId: uid),
228- // Custom updater for the 'user' model.
229- // This updater handles two distinct use cases:
230- // 1. Admins updating user roles (`appRole`, `dashboardRole`).
231- // 2. Regular users updating their own `feedDecoratorStatus`.
232- // It accepts a raw Map<String, dynamic> as the `item` to prevent
233- // mass assignment vulnerabilities, only applying allowed fields.
234- 'user' : (c, id, item, uid) {
235- final repo = c.read <DataRepository <User >>();
236- final existingUser = c.read <FetchedItem <dynamic >>().data as User ;
228+ // Custom updater for the 'user' model. This logic is critical for
229+ // security and architectural consistency.
230+ //
231+ // It enforces the following rules:
232+ // 1. Admins can ONLY update a user's `appRole` and `dashboardRole`.
233+ // 2. Regular users can ONLY update their own `feedDecoratorStatus`.
234+ // 3. Email updates are handled by the `AuthService`, not this generic
235+ // endpoint.
236+ //
237+ // The updater receives a raw `Map<String, dynamic>` from the request
238+ // body to prevent mass assignment vulnerabilities. It then reads the
239+ // pre-fetched user object (guaranteed by `dataFetchMiddleware`) and
240+ // selectively applies only the allowed fields using `copyWith`. This
241+ // creates a complete, valid `User` object that is then passed to the
242+ // repository's `update` method, satisfying the `DataRepository<T>`
243+ // contract.
244+ 'user' : (context, id, item, uid) async {
245+ _log.info ('Executing custom updater for user ID: $id .' );
246+ final permissionService = context.read <PermissionService >();
247+ final authenticatedUser = context.read <User >();
248+ final userToUpdate = context.read <FetchedItem <dynamic >>().data as User ;
237249 final requestBody = item as Map <String , dynamic >;
238250
239- AppUserRole ? newAppRole;
240- if (requestBody.containsKey ('appRole' )) {
241- try {
242- newAppRole = AppUserRole .values.byName (
243- requestBody['appRole' ] as String ,
251+ var userWithUpdates = userToUpdate;
252+ 253+ if (permissionService.isAdmin (authenticatedUser)) {
254+ _log.finer (
255+ 'Admin user ${authenticatedUser .id } is updating user $id .' ,
256+ );
257+ // Admin is only allowed to update roles.
258+ if (requestBody.keys.any (
259+ (k) => k != 'appRole' && k != 'dashboardRole' ,
260+ )) {
261+ _log.warning (
262+ 'Admin ${authenticatedUser .id } attempted to update invalid fields for user $id .' ,
244263 );
245- } on ArgumentError {
246- throw BadRequestException (
247- 'Invalid value for "appRole": "${requestBody ['appRole' ]}".' ,
264+ throw const ForbiddenException (
265+ 'Administrators can only update "appRole" and "dashboardRole" via this endpoint.' ,
248266 );
249267 }
250- }
251268
252- DashboardUserRole ? newDashboardRole;
253- if (requestBody.containsKey ('dashboardRole' )) {
254- try {
255- newDashboardRole = DashboardUserRole .values.byName (
256- requestBody['dashboardRole' ] as String ,
269+ final newAppRole = requestBody.containsKey ('appRole' )
270+ ? AppUserRole .values.byName (requestBody['appRole' ] as String )
271+ : null ;
272+ final newDashboardRole = requestBody.containsKey ('dashboardRole' )
273+ ? DashboardUserRole .values.byName (
274+ requestBody['dashboardRole' ] as String ,
275+ )
276+ : null ;
277+ 278+ userWithUpdates = userWithUpdates.copyWith (
279+ appRole: newAppRole,
280+ dashboardRole: newDashboardRole,
281+ );
282+ } else {
283+ _log.finer (
284+ 'Regular user ${authenticatedUser .id } is updating their own profile.' ,
285+ );
286+ // Regular user is only allowed to update feed decorator status.
287+ if (requestBody.keys.any ((k) => k != 'feedDecoratorStatus' )) {
288+ _log.warning (
289+ 'User ${authenticatedUser .id } attempted to update invalid fields.' ,
257290 );
258- } on ArgumentError {
259- throw BadRequestException (
260- 'Invalid value for "dashboardRole": "${requestBody ['dashboardRole' ]}".' ,
291+ throw const ForbiddenException (
292+ 'You can only update "feedDecoratorStatus" via this endpoint.' ,
261293 );
262294 }
263- }
264295
265- Map <FeedDecoratorType , UserFeedDecoratorStatus >? newStatus;
266- if (requestBody.containsKey ('feedDecoratorStatus' )) {
267- newStatus = User .fromJson (
268- {'feedDecoratorStatus' : requestBody['feedDecoratorStatus' ]},
269- ).feedDecoratorStatus;
296+ if (requestBody.containsKey ('feedDecoratorStatus' )) {
297+ final newStatus = User .fromJson (
298+ {'feedDecoratorStatus' : requestBody['feedDecoratorStatus' ]},
299+ ).feedDecoratorStatus;
300+ userWithUpdates = userWithUpdates.copyWith (
301+ feedDecoratorStatus: newStatus,
302+ );
303+ }
270304 }
271305
272- final userWithUpdates = existingUser.copyWith (
273- appRole: newAppRole,
274- dashboardRole: newDashboardRole,
275- feedDecoratorStatus: newStatus,
306+ _log.info ('User update validation passed. Calling repository.' );
307+ return context.read <DataRepository <User >>().update (
308+ id: id,
309+ item: userWithUpdates,
310+ userId: uid,
276311 );
277- return repo.update (id: id, item: userWithUpdates, userId: uid);
278312 },
279313 'user_app_settings' : (c, id, item, uid) => c
280314 .read <DataRepository <UserAppSettings >>()
@@ -302,8 +336,6 @@ class DataOperationRegistry {
302336 c.read <DataRepository <Country >>().delete (id: id, userId: uid),
303337 'language' : (c, id, uid) =>
304338 c.read <DataRepository <Language >>().delete (id: id, userId: uid),
305- 'user' : (c, id, uid) =>
306- c.read <DataRepository <User >>().delete (id: id, userId: uid),
307339 'user_app_settings' : (c, id, uid) =>
308340 c.read <DataRepository <UserAppSettings >>().delete (id: id, userId: uid),
309341 'user_content_preferences' : (c, id, uid) => c
0 commit comments