From 73bd0aac9f3ca533b89f8a0fc1db06d784f2b9ef Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 31 Jul 2025 13:44:21 +0100 Subject: [PATCH 1/6] feat(dependencies): add language repository and data client - Add late final DataRepository languageRepository - Initialize languageClient and languageRepository in initialize method - Configure DataMongodb for Language with 'languages' collection --- lib/src/config/app_dependencies.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index 75dca6b..73827ce 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -52,6 +52,7 @@ class AppDependencies { late final DataRepository topicRepository; late final DataRepository sourceRepository; late final DataRepository countryRepository; + late final DataRepository languageRepository; late final DataRepository userRepository; late final DataRepository userAppSettingsRepository; late final DataRepository @@ -128,6 +129,13 @@ class AppDependencies { toJson: (item) => item.toJson(), logger: Logger('DataMongodb'), ); + final languageClient = DataMongodb( + connectionManager: _mongoDbConnectionManager, + modelName: 'languages', + fromJson: Language.fromJson, + toJson: (item) => item.toJson(), + logger: Logger('DataMongodb'), + ); final userClient = DataMongodb( connectionManager: _mongoDbConnectionManager, modelName: 'users', @@ -162,6 +170,7 @@ class AppDependencies { topicRepository = DataRepository(dataClient: topicClient); sourceRepository = DataRepository(dataClient: sourceClient); countryRepository = DataRepository(dataClient: countryClient); + languageRepository = DataRepository(dataClient: languageClient); userRepository = DataRepository(dataClient: userClient); userAppSettingsRepository = DataRepository( dataClient: userAppSettingsClient, From f54ebec3a4e7161b7ebf3ffdc3158adf7fb5d8d3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 31 Jul 2025 13:44:28 +0100 Subject: [PATCH 2/6] feat(routes): inject language repository into dependency chain - Add language repository to the list of provided dependencies in the middleware function - This change enables access to the language repository throughout the application, facilitating language-related operations and improving modularity --- routes/_middleware.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/routes/_middleware.dart b/routes/_middleware.dart index 98f813a..609462d 100644 --- a/routes/_middleware.dart +++ b/routes/_middleware.dart @@ -90,6 +90,11 @@ Handler middleware(Handler handler) { (_) => deps.countryRepository, ), ) // + .use( + provider>( + (_) => deps.languageRepository, + ), + ) // .use( provider>((_) => deps.userRepository), ) // From 347c5c75c833a2134ac61e5eebb3f008404f94e4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 31 Jul 2025 13:51:07 +0100 Subject: [PATCH 3/6] feat(rbac): add language permissions - Add create, read, update, and delete permissions for languages - Maintain consistent naming convention with existing permissions --- lib/src/rbac/permissions.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/rbac/permissions.dart b/lib/src/rbac/permissions.dart index c278f54..33821ee 100644 --- a/lib/src/rbac/permissions.dart +++ b/lib/src/rbac/permissions.dart @@ -28,6 +28,12 @@ abstract class Permissions { static const String countryUpdate = 'country.update'; static const String countryDelete = 'country.delete'; + // Language Permissions + static const String languageCreate = 'language.create'; + static const String languageRead = 'language.read'; + static const String languageUpdate = 'language.update'; + static const String languageDelete = 'language.delete'; + // User Permissions // Allows reading any user profile (e.g., for admin or public profiles) static const String userRead = 'user.read'; From e6fb40d73f36952996563cc769c952dea7b8b82b Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 31 Jul 2025 13:57:13 +0100 Subject: [PATCH 4/6] feat(rbac): expand role permissions for guest users and dashboard publishers - Add languageRead permission to guest users - Extend dashboard publisher permissions to include: - Additional content type read permissions (headline, topic, source, country, language, remoteConfig) - Create, update, and delete language permissions - Improve code readability with better commenting --- lib/src/rbac/role_permissions.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/src/rbac/role_permissions.dart b/lib/src/rbac/role_permissions.dart index 5d3c730..973dcbf 100644 --- a/lib/src/rbac/role_permissions.dart +++ b/lib/src/rbac/role_permissions.dart @@ -8,6 +8,7 @@ final Set _appGuestUserPermissions = { Permissions.topicRead, Permissions.sourceRead, Permissions.countryRead, + Permissions.languageRead, Permissions.userAppSettingsReadOwned, Permissions.userAppSettingsUpdateOwned, Permissions.userContentPreferencesReadOwned, @@ -30,9 +31,20 @@ final Set _appPremiumUserPermissions = { // --- Dashboard Role Permissions --- final Set _dashboardPublisherPermissions = { + // Publishers need to read all content types to manage them effectively. + Permissions.headlineRead, + Permissions.topicRead, + Permissions.sourceRead, + Permissions.countryRead, + Permissions.languageRead, + Permissions.remoteConfigRead, + + // Publishers can manage headlines. Permissions.headlineCreate, Permissions.headlineUpdate, Permissions.headlineDelete, + + // Core dashboard access and quality-of-life permissions. Permissions.dashboardLogin, Permissions.rateLimitingBypass, }; @@ -48,6 +60,9 @@ final Set _dashboardAdminPermissions = { Permissions.countryCreate, Permissions.countryUpdate, Permissions.countryDelete, + Permissions.languageCreate, + Permissions.languageUpdate, + Permissions.languageDelete, Permissions.userRead, // Allows reading any user's profile Permissions.remoteConfigCreate, Permissions.remoteConfigUpdate, From e5d68490e981f1e1ec07dcb2e6347c245d63dd67 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 31 Jul 2025 14:00:25 +0100 Subject: [PATCH 5/6] feat(api): add language endpoints to data route - Implement GET and POST endpoints for language data - Add readAll and create functionality for Language repository - Maintain consistent structure with existing endpoints --- routes/api/v1/data/index.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/routes/api/v1/data/index.dart b/routes/api/v1/data/index.dart index 664c2d8..47bba1c 100644 --- a/routes/api/v1/data/index.dart +++ b/routes/api/v1/data/index.dart @@ -117,6 +117,14 @@ Future _handleGet(RequestContext context) async { sort: sort, pagination: pagination, ); + case 'language': + final repo = context.read>(); + responseData = await repo.readAll( + userId: userIdForRepoCall, + filter: filter, + sort: sort, + pagination: pagination, + ); case 'user': final repo = context.read>(); responseData = await repo.readAll( @@ -201,6 +209,12 @@ Future _handlePost(RequestContext context) async { item: itemToCreate as Country, userId: userIdForRepoCall, ); + case 'language': + final repo = context.read>(); + createdItem = await repo.create( + item: itemToCreate as Language, + userId: userIdForRepoCall, + ); case 'remote_config': final repo = context.read>(); createdItem = await repo.create( From 719103c2f197bc10c508e6e5217ff8f68249ac37 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 31 Jul 2025 14:16:41 +0100 Subject: [PATCH 6/6] feat(database): implement collection seeding and indexing - Add _seedCollection method to seed specific collections from fixture data - Implement indexing for various collections to enable efficient searches - Remove unused import of mongodb_rate_limit_service.dart - Refactor code to improve readability and maintainability --- .../services/database_seeding_service.dart | 256 +++++++++++------- 1 file changed, 151 insertions(+), 105 deletions(-) diff --git a/lib/src/services/database_seeding_service.dart b/lib/src/services/database_seeding_service.dart index 6ba1e75..f17963b 100644 --- a/lib/src/services/database_seeding_service.dart +++ b/lib/src/services/database_seeding_service.dart @@ -1,6 +1,5 @@ import 'package:core/core.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/config/environment_config.dart'; -import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_rate_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_token_blacklist_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_verification_code_storage_service.dart'; import 'package:logging/logging.dart'; @@ -28,10 +27,148 @@ class DatabaseSeedingService { await _ensureIndexes(); await _seedOverrideAdminUser(); + await _seedCollection( + collectionName: 'countries', + fixtureData: countriesFixturesData, + getId: (item) => item.id, + toJson: (item) => item.toJson(), + ); + await _seedCollection( + collectionName: 'languages', + fixtureData: languagesFixturesData, + getId: (item) => item.id, + toJson: (item) => item.toJson(), + ); + await _seedCollection( + collectionName: 'remote_configs', + fixtureData: remoteConfigsFixturesData, + getId: (item) => item.id, + toJson: (item) => item.toJson(), + ); _log.info('Database seeding process completed.'); } + /// Seeds a specific collection from a given list of fixture data. + Future _seedCollection({ + required String collectionName, + required List fixtureData, + required String Function(T) getId, + required Map Function(T) toJson, + }) async { + _log.info('Seeding collection: "$collectionName"...'); + try { + if (fixtureData.isEmpty) { + _log.info('No documents to seed for "$collectionName".'); + return; + } + + final collection = _db.collection(collectionName); + final operations = >[]; + + for (final item in fixtureData) { + // Use the predefined hex string ID from the fixture to create a + // deterministic ObjectId. This is crucial for maintaining relationships + // between documents (e.g., a headline and its source). + final objectId = ObjectId.fromHexString(getId(item)); + final document = toJson(item)..remove('id'); + + operations.add({ + // Use updateOne with $set to be less destructive than replaceOne. + 'updateOne': { + // Filter by the specific, deterministic _id. + 'filter': {'_id': objectId}, + // Set the fields of the document. + 'update': {r'$set': document}, + 'upsert': true, + }, + }); + } + + final result = await collection.bulkWrite(operations); + _log.info( + 'Seeding for "$collectionName" complete. ' + 'Upserted: ${result.nUpserted}, Modified: ${result.nModified}.', + ); + } on Exception catch (e, s) { + _log.severe('Failed to seed collection "$collectionName".', e, s); + rethrow; + } + } + + /// Ensures that the necessary indexes exist on the collections. + /// + /// This method is idempotent; it will only create indexes if they do not + /// already exist. It's crucial for enabling efficient text searches. + Future _ensureIndexes() async { + _log.info('Ensuring database indexes exist...'); + try { + // Text index for searching headlines by title + await _db + .collection('headlines') + .createIndex(keys: {'title': 'text'}, name: 'headlines_text_index'); + + // Text index for searching topics by name + await _db + .collection('topics') + .createIndex(keys: {'name': 'text'}, name: 'topics_text_index'); + + // Text index for searching sources by name + await _db + .collection('sources') + .createIndex(keys: {'name': 'text'}, name: 'sources_text_index'); + + // --- TTL and Unique Indexes via runCommand --- + // The following indexes are created using the generic `runCommand` because + // they require specific options not exposed by the simpler `createIndex` + // helper method in the `mongo_dart` library. + // Specifically, `expireAfterSeconds` is needed for TTL indexes. + + // Indexes for the verification codes collection + await _db.runCommand({ + 'createIndexes': kVerificationCodesCollection, + 'indexes': [ + { + // This is a TTL (Time-To-Live) index. MongoDB will automatically + // delete documents from this collection when the `expiresAt` field's + // value is older than the specified number of seconds (0). + 'key': {'expiresAt': 1}, + 'name': 'expiresAt_ttl_index', + 'expireAfterSeconds': 0, + }, + { + // This ensures that each email can only have one pending + // verification code at a time, preventing duplicates. + 'key': {'email': 1}, + 'name': 'email_unique_index', + 'unique': true, + }, + ], + }); + + // Index for the token blacklist collection + await _db.runCommand({ + 'createIndexes': kBlacklistedTokensCollection, + 'indexes': [ + { + // This is a TTL index. MongoDB will automatically delete documents + // (blacklisted tokens) when the `expiry` field's value is past. + 'key': {'expiry': 1}, + 'name': 'expiry_ttl_index', + 'expireAfterSeconds': 0, + }, + ], + }); + + _log.info('Database indexes are set up correctly.'); + } on Exception catch (e, s) { + _log.severe('Failed to create database indexes.', e, s); + // We rethrow here because if indexes can't be created, + // critical features like search will fail. + rethrow; + } + } + /// Ensures the single administrator account is correctly configured based on /// the `OVERRIDE_ADMIN_EMAIL` environment variable. Future _seedOverrideAdminUser() async { @@ -39,9 +176,7 @@ class DatabaseSeedingService { final overrideEmail = EnvironmentConfig.overrideAdminEmail; if (overrideEmail == null || overrideEmail.isEmpty) { - _log.info( - 'OVERRIDE_ADMIN_EMAIL not set. Skipping admin user override.', - ); + _log.info('OVERRIDE_ADMIN_EMAIL not set. Skipping admin user override.'); return; } @@ -89,9 +224,10 @@ class DatabaseSeedingService { ), ); - await usersCollection.insertOne( - {'_id': newAdminId, ...newAdminUser.toJson()..remove('id')}, - ); + await usersCollection.insertOne({ + '_id': newAdminId, + ...newAdminUser.toJson()..remove('id'), + }); // Create default settings and preferences for the new admin. await _createUserSubDocuments(newAdminId); @@ -130,9 +266,10 @@ class DatabaseSeedingService { showPublishDateInHeadlineFeed: true, ), ); - await _db.collection('user_app_settings').insertOne( - {'_id': userId, ...defaultAppSettings.toJson()..remove('id')}, - ); + await _db.collection('user_app_settings').insertOne({ + '_id': userId, + ...defaultAppSettings.toJson()..remove('id'), + }); final defaultUserPreferences = UserContentPreferences( id: userId.oid, @@ -141,100 +278,9 @@ class DatabaseSeedingService { followedTopics: const [], savedHeadlines: const [], ); - await _db.collection('user_content_preferences').insertOne( - {'_id': userId, ...defaultUserPreferences.toJson()..remove('id')}, - ); - } - - /// Ensures that the necessary indexes exist on the collections. - /// - /// This method is idempotent; it will only create indexes if they do not - /// already exist. It's crucial for enabling efficient text searches. - Future _ensureIndexes() async { - _log.info('Ensuring database indexes exist...'); - try { - // Text index for searching headlines by title - await _db - .collection('headlines') - .createIndex(keys: {'title': 'text'}, name: 'headlines_text_index'); - - // Text index for searching topics by name - await _db - .collection('topics') - .createIndex(keys: {'name': 'text'}, name: 'topics_text_index'); - - // Text index for searching sources by name - await _db - .collection('sources') - .createIndex(keys: {'name': 'text'}, name: 'sources_text_index'); - - // --- TTL and Unique Indexes via runCommand --- - // The following indexes are created using the generic `runCommand` because - // they require specific options not exposed by the simpler `createIndex` - // helper method in the `mongo_dart` library. - // Specifically, `expireAfterSeconds` is needed for TTL indexes. - - // Indexes for the verification codes collection - await _db.runCommand({ - 'createIndexes': kVerificationCodesCollection, - 'indexes': [ - { - // This is a TTL (Time-To-Live) index. MongoDB will automatically - // delete documents from this collection when the `expiresAt` field's - // value is older than the specified number of seconds (0). - 'key': {'expiresAt': 1}, - 'name': 'expiresAt_ttl_index', - 'expireAfterSeconds': 0, - }, - { - // This ensures that each email can only have one pending - // verification code at a time, preventing duplicates. - 'key': {'email': 1}, - 'name': 'email_unique_index', - 'unique': true, - }, - ], - }); - - // Index for the token blacklist collection - await _db.runCommand({ - 'createIndexes': kBlacklistedTokensCollection, - 'indexes': [ - { - // This is a TTL index. MongoDB will automatically delete documents - // (blacklisted tokens) when the `expiry` field's value is past. - 'key': {'expiry': 1}, - 'name': 'expiry_ttl_index', - 'expireAfterSeconds': 0, - }, - ], - }); - - // Index for the rate limit attempts collection - await _db.runCommand({ - 'createIndexes': kRateLimitAttemptsCollection, - 'indexes': [ - { - // This is a TTL index. MongoDB will automatically delete request - // attempt documents 24 hours after they are created. - 'key': {'createdAt': 1}, - 'name': 'createdAt_ttl_index', - 'expireAfterSeconds': 86400, // 24 hours - }, - { - // Index on the key field for faster lookups. - 'key': {'key': 1}, - 'name': 'key_index', - }, - ], - }); - - _log.info('Database indexes are set up correctly.'); - } on Exception catch (e, s) { - _log.severe('Failed to create database indexes.', e, s); - // We rethrow here because if indexes can't be created, - // critical features like search will fail. - rethrow; - } + await _db.collection('user_content_preferences').insertOne({ + '_id': userId, + ...defaultUserPreferences.toJson()..remove('id'), + }); } }