Skip to content

Commit 6709962

Browse files
authored
Merge pull request #101 from flutter-news-app-full-source-code/refactor/Notification-Device-Management
Refactor/notification device management
2 parents 4c2c706 + 2bed469 commit 6709962

File tree

6 files changed

+61
-37
lines changed

6 files changed

+61
-37
lines changed

lib/src/rbac/permissions.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ abstract class Permissions {
8989
'push_notification_device.create_owned';
9090
static const String pushNotificationDeviceDeleteOwned =
9191
'push_notification_device.delete_owned';
92+
static const String pushNotificationDeviceReadOwned =
93+
'push_notification_device.read_owned';
9294

9395
// In-App Notification Permissions (User-owned)
9496
/// Allows reading the user's own in-app notifications.

lib/src/rbac/role_permissions.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ final Set<String> _appGuestUserPermissions = {
2424
// notifications.
2525
Permissions.pushNotificationDeviceCreateOwned,
2626
Permissions.pushNotificationDeviceDeleteOwned,
27+
Permissions.pushNotificationDeviceReadOwned,
2728
// Allow all app users to manage their own in-app notifications.
2829
Permissions.inAppNotificationReadOwned,
2930
Permissions.inAppNotificationUpdateOwned,

lib/src/registry/data_operation_registry.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@ class DataOperationRegistry {
184184
sort: s,
185185
pagination: p,
186186
),
187+
'push_notification_device': (c, uid, f, s, p) =>
188+
c.read<DataRepository<PushNotificationDevice>>().readAll(
189+
userId: uid,
190+
filter: f,
191+
sort: s,
192+
pagination: p,
193+
),
187194
});
188195

189196
// --- Register Item Creators ---
@@ -430,9 +437,9 @@ class DataOperationRegistry {
430437
.update(id: id, item: item as RemoteConfig, userId: uid),
431438
'in_app_notification': (c, id, item, uid) =>
432439
c.read<DataRepository<InAppNotification>>().update(
433-
id: id,
434-
item: item as InAppNotification,
435-
),
440+
id: id,
441+
item: item as InAppNotification,
442+
),
436443
});
437444

438445
// --- Register Item Deleters ---

lib/src/registry/model_registry.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -429,12 +429,19 @@ final modelRegistry = <String, ModelConfig<dynamic>>{
429429
fromJson: PushNotificationDevice.fromJson,
430430
getId: (d) => d.id,
431431
getOwnerId: (dynamic item) => (item as PushNotificationDevice).userId,
432+
// Collection GET is allowed for a user to fetch their own notification devices.
433+
// The generic route handler will automatically scope the query to the
434+
// authenticated user's ID because `getOwnerId` is defined.
432435
getCollectionPermission: const ModelActionPermission(
433-
type: RequiredPermissionType.unsupported,
436+
type: RequiredPermissionType.specificPermission,
437+
permission: Permissions.pushNotificationDeviceReadOwned,
434438
),
435-
// Required by the ownership check middelware
439+
// Item GET is allowed for a user to fetch a single one of their devices.
440+
// The ownership check middleware will verify they own this specific item.
436441
getItemPermission: const ModelActionPermission(
437-
type: RequiredPermissionType.adminOnly,
442+
type: RequiredPermissionType.specificPermission,
443+
permission: Permissions.pushNotificationDeviceReadOwned,
444+
requiresOwnershipCheck: true,
438445
),
439446
// POST is allowed for any authenticated user to register their own device.
440447
// A custom check within the DataOperationRegistry's creator function will

lib/src/services/database_seeding_service.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,12 @@ class DatabaseSeedingService {
248248
'unique': true,
249249
'sparse': true,
250250
},
251+
{
252+
// Optimizes fetching all devices for a specific user, which is
253+
// needed for the device cleanup flow on the client.
254+
'key': {'userId': 1},
255+
'name': 'userId_index',
256+
},
251257
],
252258
});
253259
_log.info('Ensured indexes for "push_notification_devices".');

lib/src/services/push_notification_service.dart

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,26 @@ class DefaultPushNotificationService implements IPushNotificationService {
3232
/// {@macro default_push_notification_service}
3333
DefaultPushNotificationService({
3434
required DataRepository<PushNotificationDevice>
35-
pushNotificationDeviceRepository,
35+
pushNotificationDeviceRepository,
3636
required DataRepository<UserContentPreferences>
37-
userContentPreferencesRepository,
37+
userContentPreferencesRepository,
3838
required DataRepository<RemoteConfig> remoteConfigRepository,
3939
required DataRepository<InAppNotification> inAppNotificationRepository,
4040
required IPushNotificationClient? firebaseClient,
4141
required IPushNotificationClient? oneSignalClient,
4242
required Logger log,
43-
}) : _pushNotificationDeviceRepository = pushNotificationDeviceRepository,
44-
_userContentPreferencesRepository = userContentPreferencesRepository,
45-
_remoteConfigRepository = remoteConfigRepository,
46-
_inAppNotificationRepository = inAppNotificationRepository,
47-
_firebaseClient = firebaseClient,
48-
_oneSignalClient = oneSignalClient,
49-
_log = log;
43+
}) : _pushNotificationDeviceRepository = pushNotificationDeviceRepository,
44+
_userContentPreferencesRepository = userContentPreferencesRepository,
45+
_remoteConfigRepository = remoteConfigRepository,
46+
_inAppNotificationRepository = inAppNotificationRepository,
47+
_firebaseClient = firebaseClient,
48+
_oneSignalClient = oneSignalClient,
49+
_log = log;
5050

5151
final DataRepository<PushNotificationDevice>
52-
_pushNotificationDeviceRepository;
52+
_pushNotificationDeviceRepository;
5353
final DataRepository<UserContentPreferences>
54-
_userContentPreferencesRepository;
54+
_userContentPreferencesRepository;
5555
final DataRepository<RemoteConfig> _remoteConfigRepository;
5656
final DataRepository<InAppNotification> _inAppNotificationRepository;
5757
final IPushNotificationClient? _firebaseClient;
@@ -113,8 +113,8 @@ class DefaultPushNotificationService implements IPushNotificationService {
113113
// Check if breaking news notifications are enabled.
114114
final isBreakingNewsEnabled =
115115
pushConfig.deliveryConfigs[PushNotificationSubscriptionDeliveryType
116-
.breakingOnly] ??
117-
false;
116+
.breakingOnly] ??
117+
false;
118118

119119
if (!isBreakingNewsEnabled) {
120120
_log.info('Breaking news notifications are disabled. Aborting.');
@@ -123,16 +123,16 @@ class DefaultPushNotificationService implements IPushNotificationService {
123123

124124
// 2. Find all user preferences that contain a saved headline filter
125125
// subscribed to breaking news. This query targets the embedded 'savedHeadlineFilters' array.
126-
final subscribedUserPreferences =
127-
await _userContentPreferencesRepository.readAll(
128-
filter: {
129-
'savedHeadlineFilters.deliveryTypes': {
130-
r'$in': [
131-
PushNotificationSubscriptionDeliveryType.breakingOnly.name,
132-
],
133-
},
134-
},
135-
);
126+
final subscribedUserPreferences = await _userContentPreferencesRepository
127+
.readAll(
128+
filter: {
129+
'savedHeadlineFilters.deliveryTypes': {
130+
r'$in': [
131+
PushNotificationSubscriptionDeliveryType.breakingOnly.name,
132+
],
133+
},
134+
},
135+
);
136136

137137
if (subscribedUserPreferences.items.isEmpty) {
138138
_log.info('No users subscribed to breaking news. Aborting.');
@@ -142,21 +142,22 @@ class DefaultPushNotificationService implements IPushNotificationService {
142142
// 3. Collect all unique user IDs from the preference documents.
143143
// Using a Set automatically handles deduplication.
144144
// The ID of the UserContentPreferences document is the user's ID.
145-
final userIds =
146-
subscribedUserPreferences.items.map((preference) => preference.id).toSet();
145+
final userIds = subscribedUserPreferences.items
146+
.map((preference) => preference.id)
147+
.toSet();
147148

148149
_log.info(
149150
'Found ${subscribedUserPreferences.items.length} users with '
150151
'subscriptions to breaking news.',
151152
);
152153

153154
// 4. Fetch all devices for all subscribed users in a single bulk query.
154-
final allDevicesResponse =
155-
await _pushNotificationDeviceRepository.readAll(
156-
filter: {
157-
'userId': {r'$in': userIds.toList()},
158-
},
159-
);
155+
final allDevicesResponse = await _pushNotificationDeviceRepository
156+
.readAll(
157+
filter: {
158+
'userId': {r'$in': userIds.toList()},
159+
},
160+
);
160161

161162
final allDevices = allDevicesResponse.items;
162163
if (allDevices.isEmpty) {

0 commit comments

Comments
 (0)