diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e448ae7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# 1.0.0 - 2025-10-13 + +* chore!: initial release under semantic versioning. \ No newline at end of file diff --git a/lib/src/database/migrations/20251013000056_add_saved_filters_to_user_preferences.dart b/lib/src/database/migrations/20251013000056_add_saved_filters_to_user_preferences.dart new file mode 100644 index 0000000..919df74 --- /dev/null +++ b/lib/src/database/migrations/20251013000056_add_saved_filters_to_user_preferences.dart @@ -0,0 +1,39 @@ +import 'package:flutter_news_app_api_server_full_source_code/src/database/migration.dart'; +import 'package:logging/logging.dart'; +import 'package:mongo_dart/mongo_dart.dart'; + +/// Migration to add the `savedFilters` field to existing +/// `user_content_preferences` documents. +class AddSavedFiltersToUserPreferences extends Migration { + /// {@macro add_saved_filters_to_user_preferences} + AddSavedFiltersToUserPreferences() + : super( + prDate: '20251013000056', + prId: '56', + prSummary: 'Add savedFilters field to user_content_preferences', + ); + + @override + Future up(Db db, Logger log) async { + final collection = db.collection('user_content_preferences'); + final result = await collection.updateMany( + // Filter for documents where 'savedFilters' does not exist. + where.notExists('savedFilters'), + // Set 'savedFilters' to an empty array. + modify.set('savedFilters', []), + ); + log.info( + 'Updated ${result.nModified} documents in user_content_preferences.', + ); + } + + @override + Future down(Db db, Logger log) async { + final collection = db.collection('user_content_preferences'); + await collection.updateMany( + where.exists('savedFilters'), + modify.unset('savedFilters'), + ); + log.info('Removed "savedFilters" field from user_content_preferences.'); + } +} diff --git a/lib/src/database/migrations/all_migrations.dart b/lib/src/database/migrations/all_migrations.dart index 4c5a728..596767a 100644 --- a/lib/src/database/migrations/all_migrations.dart +++ b/lib/src/database/migrations/all_migrations.dart @@ -1,13 +1,15 @@ import 'package:flutter_news_app_api_server_full_source_code/src/database/migration.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/database/migrations/20250924084800__refactor_ad_config_to_role_based.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/database/migrations/20251013000056_add_saved_filters_to_user_preferences.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/database_migration_service.dart' show DatabaseMigrationService; /// A central list of all database migrations to be applied. /// /// New migration classes should be added to this list. The -/// [DatabaseMigrationService] will automatically sort and apply them based on -/// their `prDate` property. +/// [DatabaseMigrationService] will automatically sort and apply them based +/// on their `prDate` property. final List allMigrations = [ RefactorAdConfigToRoleBased(), + AddSavedFiltersToUserPreferences(), ]; diff --git a/lib/src/services/auth_service.dart b/lib/src/services/auth_service.dart index 1dfe650..eb0a751 100644 --- a/lib/src/services/auth_service.dart +++ b/lib/src/services/auth_service.dart @@ -564,6 +564,7 @@ class AuthService { followedSources: const [], followedTopics: const [], savedHeadlines: const [], + savedFilters: const [], ); await _userContentPreferencesRepository.create( item: defaultUserPreferences, diff --git a/lib/src/services/database_seeding_service.dart b/lib/src/services/database_seeding_service.dart index dd139da..fc89de8 100644 --- a/lib/src/services/database_seeding_service.dart +++ b/lib/src/services/database_seeding_service.dart @@ -362,6 +362,7 @@ class DatabaseSeedingService { followedSources: const [], followedTopics: const [], savedHeadlines: const [], + savedFilters: const [], ); await _db.collection('user_content_preferences').insertOne({ '_id': userId, diff --git a/lib/src/services/default_user_preference_limit_service.dart b/lib/src/services/default_user_preference_limit_service.dart index b57d0c2..b299cb5 100644 --- a/lib/src/services/default_user_preference_limit_service.dart +++ b/lib/src/services/default_user_preference_limit_service.dart @@ -50,18 +50,28 @@ class DefaultUserPreferenceLimitService implements UserPreferenceLimitService { // 2. Determine the limit based on the user's app role. int limit; String accountType; + final isFollowedItem = + itemType == 'country' || itemType == 'source' || itemType == 'topic'; switch (user.appRole) { case AppUserRole.premiumUser: accountType = 'premium'; - limit = (itemType == 'headline') - ? limits.premiumSavedHeadlinesLimit - : limits.premiumFollowedItemsLimit; + if (isFollowedItem) { + limit = limits.premiumFollowedItemsLimit; + } else if (itemType == 'headline') { + limit = limits.premiumSavedHeadlinesLimit; + } else { + limit = limits.premiumSavedFiltersLimit; + } case AppUserRole.standardUser: accountType = 'standard'; - limit = (itemType == 'headline') - ? limits.authenticatedSavedHeadlinesLimit - : limits.authenticatedFollowedItemsLimit; + if (isFollowedItem) { + limit = limits.authenticatedFollowedItemsLimit; + } else if (itemType == 'headline') { + limit = limits.authenticatedSavedHeadlinesLimit; + } else { + limit = limits.authenticatedSavedFiltersLimit; + } case AppUserRole.guestUser: accountType = 'guest'; limit = (itemType == 'headline') @@ -113,6 +123,7 @@ class DefaultUserPreferenceLimitService implements UserPreferenceLimitService { // 2. Determine limits based on the user's app role. int followedItemsLimit; int savedHeadlinesLimit; + int savedFiltersLimit; String accountType; switch (user.appRole) { @@ -120,14 +131,17 @@ class DefaultUserPreferenceLimitService implements UserPreferenceLimitService { accountType = 'premium'; followedItemsLimit = limits.premiumFollowedItemsLimit; savedHeadlinesLimit = limits.premiumSavedHeadlinesLimit; + savedFiltersLimit = limits.premiumSavedFiltersLimit; case AppUserRole.standardUser: accountType = 'standard'; followedItemsLimit = limits.authenticatedFollowedItemsLimit; savedHeadlinesLimit = limits.authenticatedSavedHeadlinesLimit; + savedFiltersLimit = limits.authenticatedSavedFiltersLimit; case AppUserRole.guestUser: accountType = 'guest'; followedItemsLimit = limits.guestFollowedItemsLimit; savedHeadlinesLimit = limits.guestSavedHeadlinesLimit; + savedFiltersLimit = limits.guestSavedFiltersLimit; } // 3. Check if proposed preferences exceed limits @@ -155,6 +169,12 @@ class DefaultUserPreferenceLimitService implements UserPreferenceLimitService { 'for your account type ($accountType).', ); } + if (updatedPreferences.savedFilters.length > savedFiltersLimit) { + throw ForbiddenException( + 'You have reached the maximum number of saved filters allowed ' + 'for your account type ($accountType).', + ); + } } on HttpException { // Propagate known exceptions from repositories rethrow; diff --git a/pubspec.lock b/pubspec.lock index 21a7955..243c0c3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -117,11 +117,11 @@ packages: dependency: "direct main" description: path: "." - ref: "828b984517edec069266579c624c7af9fbe0e2ac" - resolved-ref: "828b984517edec069266579c624c7af9fbe0e2ac" + ref: "v1.2.0" + resolved-ref: d052799c1ebb7bcdd1a725a2a7919948c14fa001 url: "https://github.com/flutter-news-app-full-source-code/core.git" source: git - version: "0.0.0" + version: "1.2.0" coverage: dependency: transitive description: @@ -158,8 +158,8 @@ packages: dependency: "direct main" description: path: "." - ref: "960a00882045ee252274d9d57bfca606c1fe5d64" - resolved-ref: "960a00882045ee252274d9d57bfca606c1fe5d64" + ref: "v1.0.0" + resolved-ref: b9a8a8c2c660928c22f2d38d657000bcae4c74d6 url: "https://github.com/flutter-news-app-full-source-code/data-client.git" source: git version: "0.0.0" @@ -167,8 +167,8 @@ packages: dependency: "direct main" description: path: "." - ref: ee2ace6243f1ad4710c192c36215d3ce9e91c7a3 - resolved-ref: ee2ace6243f1ad4710c192c36215d3ce9e91c7a3 + ref: "v1.0.0" + resolved-ref: f55b686acdbc0af9c617df4833733a07f9208b57 url: "https://github.com/flutter-news-app-full-source-code/data-mongodb.git" source: git version: "0.0.0" @@ -176,8 +176,8 @@ packages: dependency: "direct main" description: path: "." - ref: f8f01f1191286efbba41fa2bb369fb16eb652ccf - resolved-ref: f8f01f1191286efbba41fa2bb369fb16eb652ccf + ref: "v1.0.0" + resolved-ref: "7f9242d810d60fefd2f883b19e1650e8e4eb41a3" url: "https://github.com/flutter-news-app-full-source-code/data-repository.git" source: git version: "0.0.0" @@ -225,8 +225,8 @@ packages: dependency: "direct main" description: path: "." - ref: c5e36d06b58918056dde4a58d177a39fb66bfe24 - resolved-ref: c5e36d06b58918056dde4a58d177a39fb66bfe24 + ref: "v1.0.0" + resolved-ref: "31faf0e429aeea519204dfdd26afdba36ea1d8dc" url: "https://github.com/flutter-news-app-full-source-code/email-client.git" source: git version: "0.0.0" @@ -234,8 +234,8 @@ packages: dependency: "direct main" description: path: "." - ref: a68acd99c37b844af4eb3ed9f2eb9fd494c57f73 - resolved-ref: a68acd99c37b844af4eb3ed9f2eb9fd494c57f73 + ref: "v1.0.0" + resolved-ref: "2be152f3c48acd35d0a79a01d7258a4e8e8ab399" url: "https://github.com/flutter-news-app-full-source-code/email-repository.git" source: git version: "0.0.0" @@ -243,8 +243,8 @@ packages: dependency: "direct main" description: path: "." - ref: "4cd256b1a65a648db2519d8b26b4e9174d4d3a0c" - resolved-ref: "4cd256b1a65a648db2519d8b26b4e9174d4d3a0c" + ref: "v1.0.0" + resolved-ref: f28572a0c0598348391df7701a205249f2454031 url: "https://github.com/flutter-news-app-full-source-code/email-sendgrid.git" source: git version: "0.0.0" @@ -316,8 +316,8 @@ packages: dependency: "direct main" description: path: "." - ref: "57f6bcfc0f209ecc135fdcb688194045ec87e6e8" - resolved-ref: "57f6bcfc0f209ecc135fdcb688194045ec87e6e8" + ref: "v1.0.1" + resolved-ref: ce550196f78ee2e95aa9e985759179265983689d url: "https://github.com/flutter-news-app-full-source-code/http-client.git" source: git version: "0.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index b29a480..93e93ed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,6 +2,7 @@ name: flutter_news_app_api_server_full_source_code description: The complete backend API server for the Flutter News App Toolkit, built with Dart Frog. Powers authentication, data management, user settings, and more. repository: https://github.com/flutter-news-app-full-source-code/flutter-news-app-api-server-full-source-code publish_to: none +version: 1.0.0 environment: sdk: ^3.9.0 @@ -11,38 +12,38 @@ dependencies: core: git: url: https://github.com/flutter-news-app-full-source-code/core.git - ref: 828b984517edec069266579c624c7af9fbe0e2ac + ref: v1.2.0 dart_frog: ^1.1.0 dart_jsonwebtoken: ^3.2.0 data_client: git: url: https://github.com/flutter-news-app-full-source-code/data-client.git - ref: 960a00882045ee252274d9d57bfca606c1fe5d64 + ref: v1.0.0 data_mongodb: git: url: https://github.com/flutter-news-app-full-source-code/data-mongodb.git - ref: ee2ace6243f1ad4710c192c36215d3ce9e91c7a3 + ref: v1.0.0 data_repository: git: url: https://github.com/flutter-news-app-full-source-code/data-repository.git - ref: f8f01f1191286efbba41fa2bb369fb16eb652ccf + ref: v1.0.0 dotenv: ^4.2.0 email_client: git: url: https://github.com/flutter-news-app-full-source-code/email-client.git - ref: c5e36d06b58918056dde4a58d177a39fb66bfe24 + ref: v1.0.0 email_repository: git: url: https://github.com/flutter-news-app-full-source-code/email-repository.git - ref: a68acd99c37b844af4eb3ed9f2eb9fd494c57f73 + ref: v1.0.0 email_sendgrid: git: url: https://github.com/flutter-news-app-full-source-code/email-sendgrid.git - ref: 4cd256b1a65a648db2519d8b26b4e9174d4d3a0c + ref: v1.0.0 http_client: git: url: https://github.com/flutter-news-app-full-source-code/http-client.git - ref: 57f6bcfc0f209ecc135fdcb688194045ec87e6e8 + ref: v1.0.1 json_annotation: ^4.9.0 logging: ^1.3.0 meta: ^1.16.0