Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/src/rbac/permissions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ abstract class Permissions {
'push_notification_device.create_owned';
static const String pushNotificationDeviceDeleteOwned =
'push_notification_device.delete_owned';
static const String pushNotificationDeviceReadOwned =
'push_notification_device.read_owned';

// In-App Notification Permissions (User-owned)
/// Allows reading the user's own in-app notifications.
Expand Down
1 change: 1 addition & 0 deletions lib/src/rbac/role_permissions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ final Set<String> _appGuestUserPermissions = {
// notifications.
Permissions.pushNotificationDeviceCreateOwned,
Permissions.pushNotificationDeviceDeleteOwned,
Permissions.pushNotificationDeviceReadOwned,
// Allow all app users to manage their own in-app notifications.
Permissions.inAppNotificationReadOwned,
Permissions.inAppNotificationUpdateOwned,
Expand Down
13 changes: 10 additions & 3 deletions lib/src/registry/data_operation_registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ class DataOperationRegistry {
sort: s,
pagination: p,
),
'push_notification_device': (c, uid, f, s, p) =>
c.read<DataRepository<PushNotificationDevice>>().readAll(
userId: uid,
filter: f,
sort: s,
pagination: p,
),
});

// --- Register Item Creators ---
Expand Down Expand Up @@ -430,9 +437,9 @@ class DataOperationRegistry {
.update(id: id, item: item as RemoteConfig, userId: uid),
'in_app_notification': (c, id, item, uid) =>
c.read<DataRepository<InAppNotification>>().update(
id: id,
item: item as InAppNotification,
),
id: id,
item: item as InAppNotification,
),
});

// --- Register Item Deleters ---
Expand Down
13 changes: 10 additions & 3 deletions lib/src/registry/model_registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -429,12 +429,19 @@ final modelRegistry = <String, ModelConfig<dynamic>>{
fromJson: PushNotificationDevice.fromJson,
getId: (d) => d.id,
getOwnerId: (dynamic item) => (item as PushNotificationDevice).userId,
// Collection GET is allowed for a user to fetch their own notification devices.
// The generic route handler will automatically scope the query to the
// authenticated user's ID because `getOwnerId` is defined.
getCollectionPermission: const ModelActionPermission(
type: RequiredPermissionType.unsupported,
type: RequiredPermissionType.specificPermission,
permission: Permissions.pushNotificationDeviceReadOwned,
),
// Required by the ownership check middelware
// Item GET is allowed for a user to fetch a single one of their devices.
// The ownership check middleware will verify they own this specific item.
getItemPermission: const ModelActionPermission(
type: RequiredPermissionType.adminOnly,
type: RequiredPermissionType.specificPermission,
permission: Permissions.pushNotificationDeviceReadOwned,
requiresOwnershipCheck: true,
),
// POST is allowed for any authenticated user to register their own device.
// A custom check within the DataOperationRegistry's creator function will
Expand Down
6 changes: 6 additions & 0 deletions lib/src/services/database_seeding_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ class DatabaseSeedingService {
'unique': true,
'sparse': true,
},
{
// Optimizes fetching all devices for a specific user, which is
// needed for the device cleanup flow on the client.
'key': {'userId': 1},
'name': 'userId_index',
},
],
});
_log.info('Ensured indexes for "push_notification_devices".');
Expand Down
63 changes: 32 additions & 31 deletions lib/src/services/push_notification_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,26 @@ class DefaultPushNotificationService implements IPushNotificationService {
/// {@macro default_push_notification_service}
DefaultPushNotificationService({
required DataRepository<PushNotificationDevice>
pushNotificationDeviceRepository,
pushNotificationDeviceRepository,
required DataRepository<UserContentPreferences>
userContentPreferencesRepository,
userContentPreferencesRepository,
required DataRepository<RemoteConfig> remoteConfigRepository,
required DataRepository<InAppNotification> inAppNotificationRepository,
required IPushNotificationClient? firebaseClient,
required IPushNotificationClient? oneSignalClient,
required Logger log,
}) : _pushNotificationDeviceRepository = pushNotificationDeviceRepository,
_userContentPreferencesRepository = userContentPreferencesRepository,
_remoteConfigRepository = remoteConfigRepository,
_inAppNotificationRepository = inAppNotificationRepository,
_firebaseClient = firebaseClient,
_oneSignalClient = oneSignalClient,
_log = log;
}) : _pushNotificationDeviceRepository = pushNotificationDeviceRepository,
_userContentPreferencesRepository = userContentPreferencesRepository,
_remoteConfigRepository = remoteConfigRepository,
_inAppNotificationRepository = inAppNotificationRepository,
_firebaseClient = firebaseClient,
_oneSignalClient = oneSignalClient,
_log = log;

final DataRepository<PushNotificationDevice>
_pushNotificationDeviceRepository;
_pushNotificationDeviceRepository;
final DataRepository<UserContentPreferences>
_userContentPreferencesRepository;
_userContentPreferencesRepository;
final DataRepository<RemoteConfig> _remoteConfigRepository;
final DataRepository<InAppNotification> _inAppNotificationRepository;
final IPushNotificationClient? _firebaseClient;
Expand Down Expand Up @@ -113,8 +113,8 @@ class DefaultPushNotificationService implements IPushNotificationService {
// Check if breaking news notifications are enabled.
final isBreakingNewsEnabled =
pushConfig.deliveryConfigs[PushNotificationSubscriptionDeliveryType
.breakingOnly] ??
false;
.breakingOnly] ??
false;

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

// 2. Find all user preferences that contain a saved headline filter
// subscribed to breaking news. This query targets the embedded 'savedHeadlineFilters' array.
final subscribedUserPreferences =
await _userContentPreferencesRepository.readAll(
filter: {
'savedHeadlineFilters.deliveryTypes': {
r'$in': [
PushNotificationSubscriptionDeliveryType.breakingOnly.name,
],
},
},
);
final subscribedUserPreferences = await _userContentPreferencesRepository
.readAll(
filter: {
'savedHeadlineFilters.deliveryTypes': {
r'$in': [
PushNotificationSubscriptionDeliveryType.breakingOnly.name,
],
},
},
);

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

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

// 4. Fetch all devices for all subscribed users in a single bulk query.
final allDevicesResponse =
await _pushNotificationDeviceRepository.readAll(
filter: {
'userId': {r'$in': userIds.toList()},
},
);
final allDevicesResponse = await _pushNotificationDeviceRepository
.readAll(
filter: {
'userId': {r'$in': userIds.toList()},
},
);

final allDevices = allDevicesResponse.items;
if (allDevices.isEmpty) {
Expand Down
Loading