|
1 | 1 | import 'package:core/core.dart'; |
2 | 2 | import 'package:data_repository/data_repository.dart'; |
3 | | -import 'package:flutter_news_app_api_server_full_source_code/src/rbac/permission_service.dart'; |
4 | 3 | import 'package:flutter_news_app_api_server_full_source_code/src/services/user_preference_limit_service.dart'; |
5 | 4 | import 'package:logging/logging.dart'; |
6 | 5 |
|
7 | 6 | /// {@template default_user_preference_limit_service} |
8 | 7 | /// Default implementation of [UserPreferenceLimitService] that enforces limits |
9 | | -/// based on user role and the new `InterestConfig` within [RemoteConfig]. |
| 8 | +/// based on user role and the `InterestConfig` and `UserPreferenceConfig` |
| 9 | +/// sections within the application's [RemoteConfig]. |
10 | 10 | /// {@endtemplate} |
11 | 11 | class DefaultUserPreferenceLimitService implements UserPreferenceLimitService { |
12 | 12 | /// {@macro default_user_preference_limit_service} |
13 | 13 | const DefaultUserPreferenceLimitService({ |
14 | 14 | required DataRepository<RemoteConfig> remoteConfigRepository, |
15 | | - required PermissionService permissionService, |
16 | 15 | required Logger log, |
17 | 16 | }) : _remoteConfigRepository = remoteConfigRepository, |
18 | | - _permissionService = permissionService, |
19 | 17 | _log = log; |
20 | 18 |
|
21 | 19 | final DataRepository<RemoteConfig> _remoteConfigRepository; |
22 | | - final PermissionService _permissionService; |
23 | 20 | final Logger _log; |
24 | 21 |
|
25 | 22 | // Assuming a fixed ID for the RemoteConfig document |
26 | 23 | static const String _remoteConfigId = kRemoteConfigId; |
27 | 24 |
|
28 | 25 | @override |
29 | | - Future<void> checkUpdatePreferences( |
30 | | - User user, |
31 | | - UserContentPreferences updatedPreferences, |
32 | | - ) async { |
33 | | - // This method is now a placeholder. The new, granular limit checking |
34 | | - // is handled by custom creators/updaters in the DataOperationRegistry |
35 | | - // for the 'interest' model, which will call a more specific method. |
36 | | - // For now, this method does nothing to avoid incorrect validation |
37 | | - // on the full UserContentPreferences object. |
| 26 | + Future<void> checkInterestLimits({ |
| 27 | + required User user, |
| 28 | + required Interest interest, |
| 29 | + required List<Interest> existingInterests, |
| 30 | + }) async { |
| 31 | + _log.info('Checking interest limits for user ${user.id}.'); |
| 32 | + final remoteConfig = await _remoteConfigRepository.read( |
| 33 | + id: _remoteConfigId, |
| 34 | + ); |
| 35 | + final limits = remoteConfig.interestConfig.limits[user.appRole]; |
| 36 | + |
| 37 | + if (limits == null) { |
| 38 | + _log.severe( |
| 39 | + 'Interest limits not found for role ${user.appRole}. ' |
| 40 | + 'Denying request by default.', |
| 41 | + ); |
| 42 | + throw const ForbiddenException('Interest limits are not configured.'); |
| 43 | + } |
| 44 | + |
| 45 | + // 1. Check total number of interests. |
| 46 | + final newTotal = existingInterests.length + 1; |
| 47 | + if (newTotal > limits.total) { |
| 48 | + _log.warning( |
| 49 | + 'User ${user.id} exceeded total interest limit: ' |
| 50 | + '${limits.total} (attempted $newTotal).', |
| 51 | + ); |
| 52 | + throw ForbiddenException( |
| 53 | + 'You have reached your limit of ${limits.total} saved interests.', |
| 54 | + ); |
| 55 | + } |
| 56 | + |
| 57 | + // 2. Check total number of pinned feed filters. |
| 58 | + if (interest.isPinnedFeedFilter) { |
| 59 | + final pinnedCount = |
| 60 | + existingInterests.where((i) => i.isPinnedFeedFilter).length + 1; |
| 61 | + if (pinnedCount > limits.pinnedFeedFilters) { |
| 62 | + _log.warning( |
| 63 | + 'User ${user.id} exceeded pinned feed filter limit: ' |
| 64 | + '${limits.pinnedFeedFilters} (attempted $pinnedCount).', |
| 65 | + ); |
| 66 | + throw ForbiddenException( |
| 67 | + 'You have reached your limit of ${limits.pinnedFeedFilters} ' |
| 68 | + 'pinned feed filters.', |
| 69 | + ); |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + // 3. Check notification subscription limits for each type. |
| 74 | + for (final deliveryType in interest.deliveryTypes) { |
| 75 | + final notificationLimit = limits.notifications[deliveryType]; |
| 76 | + if (notificationLimit == null) { |
| 77 | + _log.severe( |
| 78 | + 'Notification limit for type ${deliveryType.name} not found for ' |
| 79 | + 'role ${user.appRole}. Denying request by default.', |
| 80 | + ); |
| 81 | + throw ForbiddenException( |
| 82 | + 'Notification limits for ${deliveryType.name} are not configured.', |
| 83 | + ); |
| 84 | + } |
| 85 | + |
| 86 | + final subscriptionCount = |
| 87 | + existingInterests |
| 88 | + .where((i) => i.deliveryTypes.contains(deliveryType)) |
| 89 | + .length + |
| 90 | + 1; |
| 91 | + |
| 92 | + if (subscriptionCount > notificationLimit) { |
| 93 | + _log.warning( |
| 94 | + 'User ${user.id} exceeded notification limit for ' |
| 95 | + '${deliveryType.name}: $notificationLimit ' |
| 96 | + '(attempted $subscriptionCount).', |
| 97 | + ); |
| 98 | + throw ForbiddenException( |
| 99 | + 'You have reached your limit of $notificationLimit ' |
| 100 | + '${deliveryType.name} notification subscriptions.', |
| 101 | + ); |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + _log.info('Interest limits check passed for user ${user.id}.'); |
| 106 | + } |
| 107 | + |
| 108 | + @override |
| 109 | + Future<void> checkUserContentPreferencesLimits({ |
| 110 | + required User user, |
| 111 | + required UserContentPreferences updatedPreferences, |
| 112 | + }) async { |
| 113 | + _log.info('Checking user content preferences limits for user ${user.id}.'); |
| 114 | + final remoteConfig = await _remoteConfigRepository.read( |
| 115 | + id: _remoteConfigId, |
| 116 | + ); |
| 117 | + final limits = remoteConfig.userPreferenceConfig; |
| 118 | + |
| 119 | + final (followedItemsLimit, savedHeadlinesLimit) = _getLimitsForRole( |
| 120 | + user.appRole, |
| 121 | + limits, |
| 122 | + ); |
| 123 | + |
| 124 | + // Check followed countries |
| 125 | + if (updatedPreferences.followedCountries.length > followedItemsLimit) { |
| 126 | + _log.warning( |
| 127 | + 'User ${user.id} exceeded followed countries limit: ' |
| 128 | + '$followedItemsLimit (attempted ' |
| 129 | + '${updatedPreferences.followedCountries.length}).', |
| 130 | + ); |
| 131 | + throw ForbiddenException( |
| 132 | + 'You have reached your limit of $followedItemsLimit followed countries.', |
| 133 | + ); |
| 134 | + } |
| 135 | + |
| 136 | + // Check followed sources |
| 137 | + if (updatedPreferences.followedSources.length > followedItemsLimit) { |
| 138 | + _log.warning( |
| 139 | + 'User ${user.id} exceeded followed sources limit: ' |
| 140 | + '$followedItemsLimit (attempted ' |
| 141 | + '${updatedPreferences.followedSources.length}).', |
| 142 | + ); |
| 143 | + throw ForbiddenException( |
| 144 | + 'You have reached your limit of $followedItemsLimit followed sources.', |
| 145 | + ); |
| 146 | + } |
| 147 | + |
| 148 | + // Check followed topics |
| 149 | + if (updatedPreferences.followedTopics.length > followedItemsLimit) { |
| 150 | + _log.warning( |
| 151 | + 'User ${user.id} exceeded followed topics limit: ' |
| 152 | + '$followedItemsLimit (attempted ' |
| 153 | + '${updatedPreferences.followedTopics.length}).', |
| 154 | + ); |
| 155 | + throw ForbiddenException( |
| 156 | + 'You have reached your limit of $followedItemsLimit followed topics.', |
| 157 | + ); |
| 158 | + } |
| 159 | + |
| 160 | + // Check saved headlines |
| 161 | + if (updatedPreferences.savedHeadlines.length > savedHeadlinesLimit) { |
| 162 | + _log.warning( |
| 163 | + 'User ${user.id} exceeded saved headlines limit: ' |
| 164 | + '$savedHeadlinesLimit (attempted ' |
| 165 | + '${updatedPreferences.savedHeadlines.length}).', |
| 166 | + ); |
| 167 | + throw ForbiddenException( |
| 168 | + 'You have reached your limit of $savedHeadlinesLimit saved headlines.', |
| 169 | + ); |
| 170 | + } |
| 171 | + |
38 | 172 | _log.info( |
39 | | - 'checkUpdatePreferences is a placeholder and performs no validation.', |
| 173 | + 'User content preferences limits check passed for user ${user.id}.', |
40 | 174 | ); |
41 | | - return Future.value(); |
| 175 | + } |
| 176 | + |
| 177 | + /// Helper to get the correct limits based on the user's role. |
| 178 | + (int, int) _getLimitsForRole( |
| 179 | + AppUserRole role, |
| 180 | + UserPreferenceConfig limits, |
| 181 | + ) { |
| 182 | + return switch (role) { |
| 183 | + AppUserRole.guestUser => ( |
| 184 | + limits.guestFollowedItemsLimit, |
| 185 | + limits.guestSavedHeadlinesLimit, |
| 186 | + ), |
| 187 | + AppUserRole.standardUser => ( |
| 188 | + limits.authenticatedFollowedItemsLimit, |
| 189 | + limits.authenticatedSavedHeadlinesLimit, |
| 190 | + ), |
| 191 | + AppUserRole.premiumUser => ( |
| 192 | + limits.premiumFollowedItemsLimit, |
| 193 | + limits.premiumSavedHeadlinesLimit, |
| 194 | + ), |
| 195 | + }; |
42 | 196 | } |
43 | 197 | } |
0 commit comments