Skip to content

Commit a861015

Browse files
authored
Merge pull request #29 from flutter-news-app-full-source-code/enhance_intial_admin_seeding
Enhance intial admin seeding
2 parents 2dca561 + 71a9e05 commit a861015

File tree

3 files changed

+132
-0
lines changed

3 files changed

+132
-0
lines changed

.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,13 @@
3333
# Defaults to "https://api.sendgrid.com" if not set.
3434
# Use "https://api.eu.sendgrid.com" for EU-based accounts.
3535
# SENDGRID_API_URL="https://api.sendgrid.com"
36+
37+
# ADMIN OVERRIDE: Sets the single administrator account for the application.
38+
# On server startup, the system ensures that the user with this email is the
39+
# one and only administrator.
40+
# - If no admin exists, one will be created with this email.
41+
# - If an admin with a DIFFERENT email exists, they will be REMOVED and
42+
# replaced by a new admin with this email.
43+
# - If an admin with this email already exists, nothing changes.
44+
# This provides a secure way to set or recover the admin account.
45+
# OVERRIDE_ADMIN_EMAIL="[email protected]"

lib/src/config/environment_config.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,10 @@ abstract final class EnvironmentConfig {
133133
///
134134
/// Returns `null` if the `SENDGRID_API_URL` is not set.
135135
static String? get sendGridApiUrl => _env['SENDGRID_API_URL'];
136+
137+
/// Retrieves the override admin email from the environment, if provided.
138+
///
139+
/// This is used to set or replace the single administrator account on startup.
140+
/// Returns `null` if the `OVERRIDE_ADMIN_EMAIL` is not set.
141+
static String? get overrideAdminEmail => _env['OVERRIDE_ADMIN_EMAIL'];
136142
}

lib/src/services/database_seeding_service.dart

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:core/core.dart';
2+
import 'package:flutter_news_app_api_server_full_source_code/src/config/environment_config.dart';
23
import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_token_blacklist_service.dart';
34
import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_verification_code_storage_service.dart';
45
import 'package:logging/logging.dart';
@@ -25,6 +26,7 @@ class DatabaseSeedingService {
2526
_log.info('Starting database seeding process...');
2627

2728
await _ensureIndexes();
29+
await _seedOverrideAdminUser();
2830

2931
await _seedCollection<Country>(
3032
collectionName: 'countries',
@@ -73,6 +75,120 @@ class DatabaseSeedingService {
7375
_log.info('Database seeding process completed.');
7476
}
7577

78+
/// Ensures the single administrator account is correctly configured based on
79+
/// the `OVERRIDE_ADMIN_EMAIL` environment variable.
80+
Future<void> _seedOverrideAdminUser() async {
81+
_log.info('Checking for admin user override...');
82+
final overrideEmail = EnvironmentConfig.overrideAdminEmail;
83+
84+
if (overrideEmail == null || overrideEmail.isEmpty) {
85+
_log.info(
86+
'OVERRIDE_ADMIN_EMAIL not set. Skipping admin user override.',
87+
);
88+
return;
89+
}
90+
91+
final usersCollection = _db.collection('users');
92+
final existingAdmin = await usersCollection.findOne(
93+
where.eq('dashboardRole', DashboardUserRole.admin.name),
94+
);
95+
96+
// Case 1: An admin exists.
97+
if (existingAdmin != null) {
98+
final existingAdminEmail = existingAdmin['email'] as String;
99+
// If the existing admin's email is the same as the override, do nothing.
100+
if (existingAdminEmail == overrideEmail) {
101+
_log.info(
102+
'Admin user with email $overrideEmail already exists and matches '
103+
'override. No action needed.',
104+
);
105+
return;
106+
}
107+
108+
// If emails differ, delete the old admin and their data.
109+
_log.warning(
110+
'Found existing admin with email "$existingAdminEmail". It will be '
111+
'replaced by the override email "$overrideEmail".',
112+
);
113+
final oldAdminId = existingAdmin['_id'] as ObjectId;
114+
await _deleteUserAndData(oldAdminId);
115+
}
116+
117+
// Case 2: No admin exists, or the old one was just deleted.
118+
// Create the new admin.
119+
_log.info('Creating admin user for email: $overrideEmail');
120+
final newAdminId = ObjectId();
121+
final newAdminUser = User(
122+
id: newAdminId.oid,
123+
email: overrideEmail,
124+
appRole: AppUserRole.standardUser,
125+
dashboardRole: DashboardUserRole.admin,
126+
createdAt: DateTime.now(),
127+
feedActionStatus: Map.fromEntries(
128+
FeedActionType.values.map(
129+
(type) =>
130+
MapEntry(type, const UserFeedActionStatus(isCompleted: false)),
131+
),
132+
),
133+
);
134+
135+
await usersCollection.insertOne(
136+
{'_id': newAdminId, ...newAdminUser.toJson()..remove('id')},
137+
);
138+
139+
// Create default settings and preferences for the new admin.
140+
await _createUserSubDocuments(newAdminId);
141+
142+
_log.info('Successfully created admin user for $overrideEmail.');
143+
}
144+
145+
/// Deletes a user and their associated sub-documents.
146+
Future<void> _deleteUserAndData(ObjectId userId) async {
147+
await _db.collection('users').deleteOne(where.eq('_id', userId));
148+
await _db
149+
.collection('user_app_settings')
150+
.deleteOne(where.eq('_id', userId));
151+
await _db
152+
.collection('user_content_preferences')
153+
.deleteOne(where.eq('_id', userId));
154+
_log.info('Deleted user and associated data for ID: ${userId.oid}');
155+
}
156+
157+
/// Creates the default sub-documents (settings, preferences) for a new user.
158+
Future<void> _createUserSubDocuments(ObjectId userId) async {
159+
final defaultAppSettings = UserAppSettings(
160+
id: userId.oid,
161+
displaySettings: const DisplaySettings(
162+
baseTheme: AppBaseTheme.system,
163+
accentTheme: AppAccentTheme.defaultBlue,
164+
fontFamily: 'SystemDefault',
165+
textScaleFactor: AppTextScaleFactor.medium,
166+
fontWeight: AppFontWeight.regular,
167+
),
168+
language: 'en',
169+
feedPreferences: const FeedDisplayPreferences(
170+
headlineDensity: HeadlineDensity.standard,
171+
headlineImageStyle: HeadlineImageStyle.largeThumbnail,
172+
showSourceInHeadlineFeed: true,
173+
showPublishDateInHeadlineFeed: true,
174+
),
175+
);
176+
await _db.collection('user_app_settings').insertOne(
177+
{'_id': userId, ...defaultAppSettings.toJson()..remove('id')},
178+
);
179+
180+
final defaultUserPreferences = UserContentPreferences(
181+
id: userId.oid,
182+
followedCountries: const [],
183+
followedSources: const [],
184+
followedTopics: const [],
185+
savedHeadlines: const [],
186+
);
187+
await _db.collection('user_content_preferences').insertOne(
188+
{'_id': userId, ...defaultUserPreferences.toJson()..remove('id')},
189+
);
190+
}
191+
76192
/// Seeds a specific collection from a given list of fixture data.
77193
Future<void> _seedCollection<T>({
78194
required String collectionName,

0 commit comments

Comments
 (0)