Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit c1ec019

Browse files
authored
Merge pull request #88 from flutter-news-app-full-source-code/refactor/implement-the-required-user-management-capabilities-for-administrators
Refactor/implement the required user management capabilities for administrators
2 parents 6134024 + 6f81b7f commit c1ec019

File tree

6 files changed

+115
-23
lines changed

6 files changed

+115
-23
lines changed

‎lib/src/rbac/permissions.dart‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ abstract class Permissions {
4444
// Allows deleting the authenticated user's own account
4545
static const String userDeleteOwned = 'user.delete_owned';
4646

47+
// Allows creating a new user (admin-only).
48+
static const String userCreate = 'user.create';
49+
// Allows updating any user's profile (admin-only).
50+
static const String userUpdate = 'user.update';
51+
// Allows deleting any user's account (admin-only).
52+
static const String userDelete = 'user.delete';
53+
4754
// User App Settings Permissions (User-owned)
4855
static const String userAppSettingsReadOwned = 'user_app_settings.read_owned';
4956
static const String userAppSettingsUpdateOwned =

‎lib/src/rbac/role_permissions.dart‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ final Set<String> _dashboardAdminPermissions = {
6868
Permissions.languageCreate,
6969
Permissions.languageUpdate,
7070
Permissions.languageDelete,
71-
Permissions.userRead, // Allows reading any user's profile
71+
Permissions.userRead, // Allows reading any user's profile.
72+
// Allow full user account management for admins.
73+
Permissions.userCreate,
74+
Permissions.userUpdate,
75+
Permissions.userDelete,
7276
Permissions.remoteConfigCreate,
7377
Permissions.remoteConfigUpdate,
7478
Permissions.remoteConfigDelete,

‎lib/src/registry/data_operation_registry.dart‎

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ class DataOperationRegistry {
188188
item: item as Language,
189189
userId: uid,
190190
),
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+
),
191196
'remote_config': (c, item, uid) => c
192197
.read<DataRepository<RemoteConfig>>()
193198
.create(item: item as RemoteConfig, userId: uid),
@@ -220,13 +225,56 @@ class DataOperationRegistry {
220225
'language': (c, id, item, uid) => c
221226
.read<DataRepository<Language>>()
222227
.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.
223234
'user': (c, id, item, uid) {
224235
final repo = c.read<DataRepository<User>>();
225236
final existingUser = c.read<FetchedItem<dynamic>>().data as User;
226-
final updatedUser = existingUser.copyWith(
227-
feedDecoratorStatus: (item as User).feedDecoratorStatus,
237+
final requestBody = item as Map<String, dynamic>;
238+
239+
AppUserRole? newAppRole;
240+
if (requestBody.containsKey('appRole')) {
241+
try {
242+
newAppRole = AppUserRole.values.byName(
243+
requestBody['appRole'] as String,
244+
);
245+
} on ArgumentError {
246+
throw BadRequestException(
247+
'Invalid value for "appRole": "${requestBody['appRole']}".',
248+
);
249+
}
250+
}
251+
252+
DashboardUserRole? newDashboardRole;
253+
if (requestBody.containsKey('dashboardRole')) {
254+
try {
255+
newDashboardRole = DashboardUserRole.values.byName(
256+
requestBody['dashboardRole'] as String,
257+
);
258+
} on ArgumentError {
259+
throw BadRequestException(
260+
'Invalid value for "dashboardRole": "${requestBody['dashboardRole']}".',
261+
);
262+
}
263+
}
264+
265+
Map<FeedDecoratorType, UserFeedDecoratorStatus>? newStatus;
266+
if (requestBody.containsKey('feedDecoratorStatus')) {
267+
newStatus = User.fromJson(
268+
{'feedDecoratorStatus': requestBody['feedDecoratorStatus']},
269+
).feedDecoratorStatus;
270+
}
271+
272+
final userWithUpdates = existingUser.copyWith(
273+
appRole: newAppRole,
274+
dashboardRole: newDashboardRole,
275+
feedDecoratorStatus: newStatus,
228276
);
229-
return repo.update(id: id, item: updatedUser, userId: uid);
277+
return repo.update(id: id, item: userWithUpdates, userId: uid);
230278
},
231279
'user_app_settings': (c, id, item, uid) => c
232280
.read<DataRepository<UserAppSettings>>()

‎lib/src/registry/model_registry.dart‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,17 +281,28 @@ final modelRegistry = <String, ModelConfig<dynamic>>{
281281
requiresOwnershipCheck: true, // Must be the owner
282282
requiresAuthentication: true,
283283
),
284+
// Admins can create users via the data endpoint.
285+
// User creation via auth routes (e.g., sign-up) is separate.
284286
postPermission: const ModelActionPermission(
285-
type: RequiredPermissionType
286-
.unsupported, // User creation handled by auth routes
287+
type: RequiredPermissionType.specificPermission,
288+
permission:Permissions.userCreate,
287289
requiresAuthentication: true,
288290
),
291+
// An admin can update any user's roles.
292+
// A regular user can update specific fields on their own profile
293+
// (e.g., feedDecoratorStatus), which is handled by the updater logic
294+
// in DataOperationRegistry. The ownership check ensures they can only
295+
// access their own user object to begin with.
289296
putPermission: const ModelActionPermission(
290297
type: RequiredPermissionType.specificPermission,
291298
permission: Permissions.userUpdateOwned, // User can update their own
292299
requiresOwnershipCheck: true, // Must be the owner
293300
requiresAuthentication: true,
294301
),
302+
// An admin can delete any user.
303+
// A regular user can delete their own account.
304+
// The ownership check middleware is bypassed for admins, so this single
305+
// config works for both roles.
295306
deletePermission: const ModelActionPermission(
296307
type: RequiredPermissionType.specificPermission,
297308
permission: Permissions.userDeleteOwned, // User can delete their own

‎routes/api/v1/data/[id]/index.dart‎

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -68,27 +68,41 @@ Future<Response> _handlePut(RequestContext context, String id) async {
6868

6969
requestBody['updatedAt'] = DateTime.now().toUtc().toIso8601String();
7070

71+
// The item to be passed to the updater function.
72+
// For 'user' updates, this will be the raw request body map to allow for
73+
// secure, selective field merging in the DataOperationRegistry.
74+
// For all other models, it's the deserialized object.
7175
dynamic itemToUpdate;
72-
try {
73-
itemToUpdate = modelConfig.fromJson(requestBody);
74-
} on TypeError catch (e, s) {
75-
_logger.warning('Deserialization TypeError in PUT /data/[id]', e, s);
76-
throw const BadRequestException(
77-
'Invalid request body: Missing or invalid required field(s).',
78-
);
79-
}
8076

81-
try {
82-
final bodyItemId = modelConfig.getId(itemToUpdate);
83-
if (bodyItemId != id) {
84-
throw BadRequestException(
85-
'Bad Request: ID in request body ("$bodyItemId") does not match ID in path ("$id").',
77+
if (modelName == 'user') {
78+
// For user updates, we pass the raw map to the updater.
79+
// This allows the updater to selectively apply fields, preventing mass
80+
// assignment vulnerabilities. The ID check is also skipped as the request
81+
// body for a user role update will not contain an ID.
82+
_logger.finer('User model update: using raw request body for updater.');
83+
itemToUpdate = requestBody;
84+
} else {
85+
// For all other models, deserialize the body into a model instance.
86+
try {
87+
itemToUpdate = modelConfig.fromJson(requestBody);
88+
} on TypeError catch (e, s) {
89+
_logger.warning('Deserialization TypeError in PUT /data/[id]', e, s);
90+
throw const BadRequestException(
91+
'Invalid request body: Missing or invalid required field(s).',
8692
);
8793
}
88-
} catch (e) {
89-
// Ignore if getId throws, as the ID might not be in the body,
90-
// which can be acceptable for some models.
91-
_logger.info('Could not get ID from PUT body: $e');
94+
95+
// Validate that the ID in the body matches the ID in the path.
96+
try {
97+
final bodyItemId = modelConfig.getId(itemToUpdate);
98+
if (bodyItemId != id) {
99+
throw BadRequestException(
100+
'Bad Request: ID in request body ("$bodyItemId") does not match ID in path ("$id").',
101+
);
102+
}
103+
} catch (e) {
104+
_logger.info('Could not get ID from PUT body: $e');
105+
}
92106
}
93107

94108
if (modelName == 'user_content_preferences') {

‎routes/api/v1/data/index.dart‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ Future<Response> _handlePost(RequestContext context) async {
117117
throw const BadRequestException('Missing or invalid request body.');
118118
}
119119

120+
// For user creation, ensure the email field is present.
121+
if (modelName == 'user') {
122+
if (!requestBody.containsKey('email') ||
123+
(requestBody['email'] as String).isEmpty) {
124+
throw const BadRequestException('Missing required field: "email".');
125+
}
126+
}
127+
120128
final now = DateTime.now().toUtc().toIso8601String();
121129
requestBody['id'] = ObjectId().oid;
122130
requestBody['createdAt'] = now;

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /