|
| 1 | +import 'package:dart_frog/dart_frog.dart'; |
| 2 | +import 'package:ht_api/src/rbac/permission_service.dart'; |
| 3 | +import 'package:ht_api/src/registry/model_registry.dart'; |
| 4 | +import 'package:ht_data_repository/ht_data_repository.dart'; |
| 5 | +import 'package:ht_shared/ht_shared.dart'; |
| 6 | + |
| 7 | +/// A wrapper class to provide a fetched item into the request context. |
| 8 | +/// |
| 9 | +/// This ensures type safety and avoids providing a raw `dynamic` object, |
| 10 | +/// which could lead to ambiguity if other dynamic objects are in the context. |
| 11 | +class FetchedItem<T> { |
| 12 | + /// Creates a wrapper for the fetched item. |
| 13 | + const FetchedItem(this.data); |
| 14 | + |
| 15 | + /// The fetched item data. |
| 16 | + final T data; |
| 17 | +} |
| 18 | + |
| 19 | +/// Middleware to check if the authenticated user is the owner of the requested |
| 20 | +/// item. |
| 21 | +/// |
| 22 | +/// This middleware is designed to run on item-specific routes (e.g., `/[id]`). |
| 23 | +/// It performs the following steps: |
| 24 | +/// |
| 25 | +/// 1. Determines if an ownership check is required for the current action |
| 26 | +/// (GET, PUT, DELETE) based on the `ModelConfig`. |
| 27 | +/// 2. If a check is required and the user is not an admin, it fetches the |
| 28 | +/// item from the database. |
| 29 | +/// 3. It then compares the item's owner ID with the authenticated user's ID. |
| 30 | +/// 4. If the check fails, it throws a [ForbiddenException]. |
| 31 | +/// 5. If the check passes, it provides the fetched item into the request |
| 32 | +/// context via `context.provide<FetchedItem<dynamic>>`. This prevents the |
| 33 | +/// downstream route handler from needing to fetch the item again. |
| 34 | +Middleware ownershipCheckMiddleware() { |
| 35 | + return (handler) { |
| 36 | + return (context) async { |
| 37 | + final modelName = context.read<String>(); |
| 38 | + final modelConfig = context.read<ModelConfig<dynamic>>(); |
| 39 | + final user = context.read<User>(); |
| 40 | + final permissionService = context.read<PermissionService>(); |
| 41 | + final method = context.request.method; |
| 42 | + final id = context.request.uri.pathSegments.last; |
| 43 | + |
| 44 | + ModelActionPermission permission; |
| 45 | + switch (method) { |
| 46 | + case HttpMethod.get: |
| 47 | + permission = modelConfig.getItemPermission; |
| 48 | + case HttpMethod.put: |
| 49 | + permission = modelConfig.putPermission; |
| 50 | + case HttpMethod.delete: |
| 51 | + permission = modelConfig.deletePermission; |
| 52 | + default: |
| 53 | + // For other methods, no ownership check is performed here. |
| 54 | + return handler(context); |
| 55 | + } |
| 56 | + |
| 57 | + // If no ownership check is required or if the user is an admin, |
| 58 | + // proceed to the next handler without fetching the item. |
| 59 | + if (!permission.requiresOwnershipCheck || |
| 60 | + permissionService.isAdmin(user)) { |
| 61 | + return handler(context); |
| 62 | + } |
| 63 | + |
| 64 | + if (modelConfig.getOwnerId == null) { |
| 65 | + throw const OperationFailedException( |
| 66 | + 'Internal Server Error: Model configuration error for ownership check.', |
| 67 | + ); |
| 68 | + } |
| 69 | + |
| 70 | + final userIdForRepoCall = user.id; |
| 71 | + dynamic item; |
| 72 | + |
| 73 | + switch (modelName) { |
| 74 | + case 'user': |
| 75 | + final repo = context.read<HtDataRepository<User>>(); |
| 76 | + item = await repo.read(id: id, userId: userIdForRepoCall); |
| 77 | + case 'user_app_settings': |
| 78 | + final repo = context.read<HtDataRepository<UserAppSettings>>(); |
| 79 | + item = await repo.read(id: id, userId: userIdForRepoCall); |
| 80 | + case 'user_content_preferences': |
| 81 | + final repo = context.read<HtDataRepository<UserContentPreferences>>(); |
| 82 | + item = await repo.read(id: id, userId: userIdForRepoCall); |
| 83 | + default: |
| 84 | + throw OperationFailedException( |
| 85 | + 'Ownership check not implemented for model "$modelName".', |
| 86 | + ); |
| 87 | + } |
| 88 | + |
| 89 | + final itemOwnerId = modelConfig.getOwnerId!(item); |
| 90 | + if (itemOwnerId != user.id) { |
| 91 | + throw const ForbiddenException( |
| 92 | + 'You do not have permission to access this item.', |
| 93 | + ); |
| 94 | + } |
| 95 | + |
| 96 | + final updatedContext = context.provide<FetchedItem<dynamic>>( |
| 97 | + () => FetchedItem(item), |
| 98 | + ); |
| 99 | + |
| 100 | + return handler(updatedContext); |
| 101 | + }; |
| 102 | + }; |
| 103 | +} |
0 commit comments