Skip to content

Commit 10ab766

Browse files
committed
refactor(api)!: re-implement DefaultUserPreferenceLimitService
BREAKING CHANGE: The DefaultUserPreferenceLimitService has been completely rewritten to implement the new UserPreferenceLimitService interface. The obsolete checkUpdatePreferences method has been removed. The unused _permissionService field has been removed. A new checkInterestLimits method is implemented to enforce all InterestConfig limits (total, pinned, and notification subscriptions) for a given user role. A new checkUserContentPreferencesLimits method is implemented to enforce all UserPreferenceConfig limits (followed items and saved headlines).
1 parent b282423 commit 10ab766

File tree

2 files changed

+200
-43
lines changed

2 files changed

+200
-43
lines changed
Lines changed: 170 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,197 @@
11
import 'package:core/core.dart';
22
import 'package:data_repository/data_repository.dart';
3-
import 'package:flutter_news_app_api_server_full_source_code/src/rbac/permission_service.dart';
43
import 'package:flutter_news_app_api_server_full_source_code/src/services/user_preference_limit_service.dart';
54
import 'package:logging/logging.dart';
65

76
/// {@template default_user_preference_limit_service}
87
/// 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].
1010
/// {@endtemplate}
1111
class DefaultUserPreferenceLimitService implements UserPreferenceLimitService {
1212
/// {@macro default_user_preference_limit_service}
1313
const DefaultUserPreferenceLimitService({
1414
required DataRepository<RemoteConfig> remoteConfigRepository,
15-
required PermissionService permissionService,
1615
required Logger log,
1716
}) : _remoteConfigRepository = remoteConfigRepository,
18-
_permissionService = permissionService,
1917
_log = log;
2018

2119
final DataRepository<RemoteConfig> _remoteConfigRepository;
22-
final PermissionService _permissionService;
2320
final Logger _log;
2421

2522
// Assuming a fixed ID for the RemoteConfig document
2623
static const String _remoteConfigId = kRemoteConfigId;
2724

2825
@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+
38172
_log.info(
39-
'checkUpdatePreferences is a placeholder and performs no validation.',
173+
'User content preferences limits check passed for user ${user.id}.',
40174
);
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+
};
42196
}
43197
}
Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,45 @@
11
import 'package:core/core.dart';
22

33
/// {@template user_preference_limit_service}
4-
/// Service responsible for enforcing user preference limits based on user role.
4+
/// A service responsible for enforcing all user preference limits based on
5+
/// the user's role and the application's remote configuration.
6+
///
7+
/// This service centralizes validation for both the `Interest` model and
8+
/// the `UserContentPreferences` model (e.g., followed items, saved headlines).
59
/// {@endtemplate}
610
abstract class UserPreferenceLimitService {
711
/// {@macro user_preference_limit_service}
812
const UserPreferenceLimitService();
913

10-
/// Checks if the user is allowed to add a *single* item of the given type,
11-
/// considering their current count of that item type and their role.
14+
/// Validates a new or updated [Interest] against the user's role-based
15+
/// limits defined in `InterestConfig`.
1216
///
13-
/// This method is typically used when a user performs an action that adds
14-
/// one item, such as saving a single headline or following a single source.
17+
/// This method checks multiple limits:
18+
/// - The total number of interests.
19+
/// - The number of interests marked as pinned feed filters.
20+
/// - The number of subscriptions for each notification delivery type across
21+
/// all of the user's interests.
1522
///
1623
/// - [user]: The authenticated user.
17-
/// - [itemType]: The type of item being added (e.g., 'country', 'source',
18-
/// 'category', 'headline').
19-
/// - [currentCount]: The current number of items of this type the user has.
24+
/// - [interest]: The `Interest` object being created or updated.
25+
/// - [existingInterests]: A list of the user's other existing interests,
26+
/// used to calculate total counts.
2027
///
21-
/// Throws [ForbiddenException] if adding the item would exceed the user's
22-
/// limit for their role.
23-
Future<void> checkAddItem(User user, String itemType, int currentCount);
28+
/// Throws a [ForbiddenException] if any limit is exceeded.
29+
Future<void> checkInterestLimits({
30+
required User user,
31+
required Interest interest,
32+
required List<Interest> existingInterests,
33+
});
2434

25-
/// Checks if the proposed *entire state* of the user's preferences,
26-
/// represented by [updatedPreferences], exceeds the limits based on their role.
35+
/// Validates an updated [UserContentPreferences] object against the limits
36+
/// defined in `UserPreferenceConfig`.
2737
///
28-
/// This method is typically used when the full [UserContentPreferences] object
29-
/// is being updated, such as when a user saves changes from a preferences screen.
30-
/// It validates the total counts across all relevant lists (followed countries,
31-
/// sources, categories, and saved headlines).
32-
///
33-
/// - [user]: The authenticated user.
34-
/// - [updatedPreferences]: The proposed [UserContentPreferences] object.
35-
///
36-
/// Throws [ForbiddenException] if any list within the preferences exceeds
37-
/// the user's limit for their role.
38-
Future<void> checkUpdatePreferences(
39-
User user,
40-
UserContentPreferences updatedPreferences,
41-
);
38+
/// This method checks the total counts for followed items (countries,
39+
/// sources, topics) and saved headlines.
40+
/// Throws a [ForbiddenException] if any limit is exceeded.
41+
Future<void> checkUserContentPreferencesLimits({
42+
required User user,
43+
required UserContentPreferences updatedPreferences,
44+
});
4245
}

0 commit comments

Comments
 (0)