From cb44a3e59073f5fa3f5c6b00ce2c1c9b704c75cb Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 08:51:09 +0100 Subject: [PATCH 1/6] feat(env): add INITIAL_ADMIN_EMAIL to .env.example - Include INITIAL_ADMIN_EMAIL in the environment variables example file - Explain its purpose and usage for setting up the initial administrator account - Clarify that it's only used on the first startup and won't change existing admin email --- .env.example | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.env.example b/.env.example index fb6d661..f3c00e2 100644 --- a/.env.example +++ b/.env.example @@ -33,3 +33,10 @@ # Defaults to "https://api.sendgrid.com" if not set. # Use "https://api.eu.sendgrid.com" for EU-based accounts. # SENDGRID_API_URL="https://api.sendgrid.com" + +# REQUIRED FOR FIRST RUN: The email address for the initial administrator account. +# This value is used ONLY on the first startup to create the first admin user. +# If a user with this email already exists, nothing will happen. +# Changing this value after the first run WILL NOT change the existing admin's email. +# To create a new admin, you must do so from the dashboard after logging in. +# INITIAL_ADMIN_EMAIL="admin@example.com" From e33686103c0e4062aa3dc53099d4db211505e886 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 08:51:36 +0100 Subject: [PATCH 2/6] feat(config): add support for initial admin email environment variable - Introduce new getter `initialAdminEmail` in EnvironmentConfig class - This allows for seeding the first administrator account on startup - Returns null if the `INITIAL_ADMIN_EMAIL` environment variable is not set --- lib/src/config/environment_config.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/config/environment_config.dart b/lib/src/config/environment_config.dart index 0f9be7d..4d70a64 100644 --- a/lib/src/config/environment_config.dart +++ b/lib/src/config/environment_config.dart @@ -133,4 +133,10 @@ abstract final class EnvironmentConfig { /// /// Returns `null` if the `SENDGRID_API_URL` is not set. static String? get sendGridApiUrl => _env['SENDGRID_API_URL']; + + /// Retrieves the initial admin email from the environment, if provided. + /// + /// This is used for seeding the first administrator account on startup. + /// Returns `null` if the `INITIAL_ADMIN_EMAIL` is not set. + static String? get initialAdminEmail => _env['INITIAL_ADMIN_EMAIL']; } From 163532bc69062aa0ea3bede505911b47251d04bd Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 08:56:00 +0100 Subject: [PATCH 3/6] feat(database): add initial admin user seeding - Implement _seedInitialAdminUser method to create an admin user from environment variables - Add necessary imports and update DatabaseSeedingService class - Ensure initial admin user is created with default settings and preferences --- .../services/database_seeding_service.dart | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/lib/src/services/database_seeding_service.dart b/lib/src/services/database_seeding_service.dart index 29b102a..b1dfdc9 100644 --- a/lib/src/services/database_seeding_service.dart +++ b/lib/src/services/database_seeding_service.dart @@ -1,4 +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_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'; @@ -25,6 +26,7 @@ class DatabaseSeedingService { _log.info('Starting database seeding process...'); await _ensureIndexes(); + await _seedInitialAdminUser(); await _seedCollection( collectionName: 'countries', @@ -73,6 +75,82 @@ class DatabaseSeedingService { _log.info('Database seeding process completed.'); } + /// Seeds the initial administrator user from the environment variable. + Future _seedInitialAdminUser() async { + _log.info('Checking for initial admin user...'); + final adminEmail = EnvironmentConfig.initialAdminEmail; + + if (adminEmail == null || adminEmail.isEmpty) { + _log.info('INITIAL_ADMIN_EMAIL not set. Skipping admin user seeding.'); + return; + } + + final usersCollection = _db.collection('users'); + final existingAdmin = await usersCollection.findOne({'email': adminEmail}); + + if (existingAdmin != null) { + _log.info('Admin user with email $adminEmail already exists.'); + return; + } + + _log.info('Creating initial admin user for email: $adminEmail'); + final adminId = ObjectId(); + final adminUser = User( + id: adminId.oid, + email: adminEmail, + appRole: AppUserRole.standardUser, // Admins are standard app users + dashboardRole: DashboardUserRole.admin, // With admin dashboard role + createdAt: DateTime.now(), + feedActionStatus: Map.fromEntries( + FeedActionType.values.map( + (type) => + MapEntry(type, const UserFeedActionStatus(isCompleted: false)), + ), + ), + ); + + await usersCollection.insertOne( + {'_id': adminId, ...adminUser.toJson()..remove('id')}, + ); + + // Also create their default settings and preferences documents + final defaultAppSettings = UserAppSettings( + id: adminId.oid, + displaySettings: const DisplaySettings( + baseTheme: AppBaseTheme.system, + accentTheme: AppAccentTheme.defaultBlue, + fontFamily: 'SystemDefault', + textScaleFactor: AppTextScaleFactor.medium, + fontWeight: AppFontWeight.regular, + ), + language: 'en', + feedPreferences: const FeedDisplayPreferences( + headlineDensity: HeadlineDensity.standard, + headlineImageStyle: HeadlineImageStyle.largeThumbnail, + showSourceInHeadlineFeed: true, + showPublishDateInHeadlineFeed: true, + ), + ); + + await _db.collection('user_app_settings').insertOne( + {'_id': adminId, ...defaultAppSettings.toJson()..remove('id')}, + ); + + final defaultUserPreferences = UserContentPreferences( + id: adminId.oid, + followedCountries: const [], + followedSources: const [], + followedTopics: const [], + savedHeadlines: const [], + ); + + await _db.collection('user_content_preferences').insertOne( + {'_id': adminId, ...defaultUserPreferences.toJson()..remove('id')}, + ); + + _log.info('Successfully created initial admin user for $adminEmail.'); + } + /// Seeds a specific collection from a given list of fixture data. Future _seedCollection({ required String collectionName, From ae50b418ac617bc268736c30d805cd38396b7790 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 09:24:45 +0100 Subject: [PATCH 4/6] docs(env): update OVERRIDE_ADMIN_EMAIL documentation - Clarify the behavior of the OVERRIDE_ADMIN_EMAIL environment variable - Emphasize the security benefits of this feature - Provide more detailed explanations for different scenarios --- .env.example | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index f3c00e2..6cbadc3 100644 --- a/.env.example +++ b/.env.example @@ -34,9 +34,12 @@ # Use "https://api.eu.sendgrid.com" for EU-based accounts. # SENDGRID_API_URL="https://api.sendgrid.com" -# REQUIRED FOR FIRST RUN: The email address for the initial administrator account. -# This value is used ONLY on the first startup to create the first admin user. -# If a user with this email already exists, nothing will happen. -# Changing this value after the first run WILL NOT change the existing admin's email. -# To create a new admin, you must do so from the dashboard after logging in. -# INITIAL_ADMIN_EMAIL="admin@example.com" +# ADMIN OVERRIDE: Sets the single administrator account for the application. +# On server startup, the system ensures that the user with this email is the +# one and only administrator. +# - If no admin exists, one will be created with this email. +# - If an admin with a DIFFERENT email exists, they will be REMOVED and +# replaced by a new admin with this email. +# - If an admin with this email already exists, nothing changes. +# This provides a secure way to set or recover the admin account. +# OVERRIDE_ADMIN_EMAIL="admin@example.com" From 43bf7dfa7ef7d91bcf5e62d6b1bb263faf90f3f6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 09:25:21 +0100 Subject: [PATCH 5/6] refactor(config): rename initial admin email to override admin email - Update environment variable name from INITIAL_ADMIN_EMAIL to OVERRIDE_ADMIN_EMAIL - Modify documentation to reflect that this variable is used to set or replace the single administrator account on startup --- lib/src/config/environment_config.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/config/environment_config.dart b/lib/src/config/environment_config.dart index 4d70a64..d65fc70 100644 --- a/lib/src/config/environment_config.dart +++ b/lib/src/config/environment_config.dart @@ -134,9 +134,9 @@ abstract final class EnvironmentConfig { /// Returns `null` if the `SENDGRID_API_URL` is not set. static String? get sendGridApiUrl => _env['SENDGRID_API_URL']; - /// Retrieves the initial admin email from the environment, if provided. + /// Retrieves the override admin email from the environment, if provided. /// - /// This is used for seeding the first administrator account on startup. - /// Returns `null` if the `INITIAL_ADMIN_EMAIL` is not set. - static String? get initialAdminEmail => _env['INITIAL_ADMIN_EMAIL']; + /// This is used to set or replace the single administrator account on startup. + /// Returns `null` if the `OVERRIDE_ADMIN_EMAIL` is not set. + static String? get overrideAdminEmail => _env['OVERRIDE_ADMIN_EMAIL']; } From 71a9e050179522c2ec8f0114d1c04fd39255c696 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 09:28:17 +0100 Subject: [PATCH 6/6] refactor(database): improve admin user seeding and override process - Rename `_seedInitialAdminUser` to `_seedOverrideAdminUser` - Update logic to handle admin user override based on OVERRIDE_ADMIN_EMAIL - Add functionality to delete existing admin user and their data if overridden - Separate user sub-document creation into a reusable method - Update method names and variables to reflect new functionality --- .../services/database_seeding_service.dart | 92 +++++++++++++------ 1 file changed, 65 insertions(+), 27 deletions(-) diff --git a/lib/src/services/database_seeding_service.dart b/lib/src/services/database_seeding_service.dart index b1dfdc9..7559967 100644 --- a/lib/src/services/database_seeding_service.dart +++ b/lib/src/services/database_seeding_service.dart @@ -26,7 +26,7 @@ class DatabaseSeedingService { _log.info('Starting database seeding process...'); await _ensureIndexes(); - await _seedInitialAdminUser(); + await _seedOverrideAdminUser(); await _seedCollection( collectionName: 'countries', @@ -75,31 +75,54 @@ class DatabaseSeedingService { _log.info('Database seeding process completed.'); } - /// Seeds the initial administrator user from the environment variable. - Future _seedInitialAdminUser() async { - _log.info('Checking for initial admin user...'); - final adminEmail = EnvironmentConfig.initialAdminEmail; + /// Ensures the single administrator account is correctly configured based on + /// the `OVERRIDE_ADMIN_EMAIL` environment variable. + Future _seedOverrideAdminUser() async { + _log.info('Checking for admin user override...'); + final overrideEmail = EnvironmentConfig.overrideAdminEmail; - if (adminEmail == null || adminEmail.isEmpty) { - _log.info('INITIAL_ADMIN_EMAIL not set. Skipping admin user seeding.'); + if (overrideEmail == null || overrideEmail.isEmpty) { + _log.info( + 'OVERRIDE_ADMIN_EMAIL not set. Skipping admin user override.', + ); return; } final usersCollection = _db.collection('users'); - final existingAdmin = await usersCollection.findOne({'email': adminEmail}); + final existingAdmin = await usersCollection.findOne( + where.eq('dashboardRole', DashboardUserRole.admin.name), + ); + // Case 1: An admin exists. if (existingAdmin != null) { - _log.info('Admin user with email $adminEmail already exists.'); - return; + final existingAdminEmail = existingAdmin['email'] as String; + // If the existing admin's email is the same as the override, do nothing. + if (existingAdminEmail == overrideEmail) { + _log.info( + 'Admin user with email $overrideEmail already exists and matches ' + 'override. No action needed.', + ); + return; + } + + // If emails differ, delete the old admin and their data. + _log.warning( + 'Found existing admin with email "$existingAdminEmail". It will be ' + 'replaced by the override email "$overrideEmail".', + ); + final oldAdminId = existingAdmin['_id'] as ObjectId; + await _deleteUserAndData(oldAdminId); } - _log.info('Creating initial admin user for email: $adminEmail'); - final adminId = ObjectId(); - final adminUser = User( - id: adminId.oid, - email: adminEmail, - appRole: AppUserRole.standardUser, // Admins are standard app users - dashboardRole: DashboardUserRole.admin, // With admin dashboard role + // Case 2: No admin exists, or the old one was just deleted. + // Create the new admin. + _log.info('Creating admin user for email: $overrideEmail'); + final newAdminId = ObjectId(); + final newAdminUser = User( + id: newAdminId.oid, + email: overrideEmail, + appRole: AppUserRole.standardUser, + dashboardRole: DashboardUserRole.admin, createdAt: DateTime.now(), feedActionStatus: Map.fromEntries( FeedActionType.values.map( @@ -110,12 +133,31 @@ class DatabaseSeedingService { ); await usersCollection.insertOne( - {'_id': adminId, ...adminUser.toJson()..remove('id')}, + {'_id': newAdminId, ...newAdminUser.toJson()..remove('id')}, ); - // Also create their default settings and preferences documents + // Create default settings and preferences for the new admin. + await _createUserSubDocuments(newAdminId); + + _log.info('Successfully created admin user for $overrideEmail.'); + } + + /// Deletes a user and their associated sub-documents. + Future _deleteUserAndData(ObjectId userId) async { + await _db.collection('users').deleteOne(where.eq('_id', userId)); + await _db + .collection('user_app_settings') + .deleteOne(where.eq('_id', userId)); + await _db + .collection('user_content_preferences') + .deleteOne(where.eq('_id', userId)); + _log.info('Deleted user and associated data for ID: ${userId.oid}'); + } + + /// Creates the default sub-documents (settings, preferences) for a new user. + Future _createUserSubDocuments(ObjectId userId) async { final defaultAppSettings = UserAppSettings( - id: adminId.oid, + id: userId.oid, displaySettings: const DisplaySettings( baseTheme: AppBaseTheme.system, accentTheme: AppAccentTheme.defaultBlue, @@ -131,24 +173,20 @@ class DatabaseSeedingService { showPublishDateInHeadlineFeed: true, ), ); - await _db.collection('user_app_settings').insertOne( - {'_id': adminId, ...defaultAppSettings.toJson()..remove('id')}, + {'_id': userId, ...defaultAppSettings.toJson()..remove('id')}, ); final defaultUserPreferences = UserContentPreferences( - id: adminId.oid, + id: userId.oid, followedCountries: const [], followedSources: const [], followedTopics: const [], savedHeadlines: const [], ); - await _db.collection('user_content_preferences').insertOne( - {'_id': adminId, ...defaultUserPreferences.toJson()..remove('id')}, + {'_id': userId, ...defaultUserPreferences.toJson()..remove('id')}, ); - - _log.info('Successfully created initial admin user for $adminEmail.'); } /// Seeds a specific collection from a given list of fixture data.