Skip to content

Commit 074c3ab

Browse files
authored
Merge pull request #119 from flutter-news-app-full-source-code/feat/build-full-user-management
Feat/build full user management
2 parents 6e5b7c3 + 550ab57 commit 074c3ab

26 files changed

+1922
-71
lines changed

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@ Manage the entire lifecycle of your content from a single, intuitive interface.
3333
3434
</details>
3535

36+
<details>
37+
<summary><strong>👥 User & Role Management</strong></summary>
38+
39+
### 👥 Granular User & Role Management
40+
Effortlessly manage your entire user base with a dedicated user management system. View all registered users, filter them by email or role, and dynamically adjust their dashboard permissions.
41+
- **Full User Roster:** See a comprehensive list of all users, including their email, app subscription level, and current dashboard role.
42+
- **Dynamic Role Promotion:** Promote trusted users to a "Publisher" role, granting them content management capabilities without full administrative access.
43+
- **Powerful Filtering:** Quickly locate specific users or user segments with multi-faceted filtering by email, app role, and dashboard role.
44+
> **Your Advantage:** Delegate content creation responsibilities securely, build out your editorial team, and maintain a clear overview of all system users and their permissions, all from a single, centralized interface.
45+
46+
</details>
47+
48+
3649
<details>
3750
<summary><strong>⚙️ App Monetization & Remote Control</strong></summary>
3851

@@ -60,7 +73,7 @@ Dynamically control the mobile app's behavior and operational state directly fro
6073
### 🔐 Secure Administrative Access
6174
A complete and secure user authentication system is built-in for your editorial and administrative teams.
6275
- **Modern, Passwordless Sign-In:** Ensures that only authorized personnel can access the dashboard using a secure and easy-to-use email-based verification system.
63-
> **Your Advantage:** The security and user management for your administrative team is already handled, providing peace of mind from day one.
76+
> **Your Advantage:** The security and access control for your administrative team is already handled, providing peace of mind from day one.
6477
6578
---
6679

lib/app/view/app.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/overview/bloc/ov
2222
import 'package:flutter_news_app_web_dashboard_full_source_code/router/router.dart';
2323
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart';
2424
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart';
25+
import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/bloc/user_filter/user_filter_bloc.dart';
26+
import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/bloc/user_management_bloc.dart';
2527
import 'package:go_router/go_router.dart';
2628
import 'package:kv_storage_service/kv_storage_service.dart';
2729
import 'package:logging/logging.dart';
@@ -41,6 +43,7 @@ class App extends StatelessWidget {
4143
required DataRepository<Country> countriesRepository,
4244
required DataRepository<Language> languagesRepository,
4345
required DataRepository<LocalAd> localAdsRepository,
46+
required DataRepository<User> usersRepository,
4447
required KVStorageService storageService,
4548
required AppEnvironment environment,
4649
required PendingDeletionsService pendingDeletionsService,
@@ -57,6 +60,7 @@ class App extends StatelessWidget {
5760
_countriesRepository = countriesRepository,
5861
_languagesRepository = languagesRepository,
5962
_localAdsRepository = localAdsRepository,
63+
_usersRepository = usersRepository,
6064
_environment = environment,
6165
_pendingDeletionsService = pendingDeletionsService;
6266

@@ -72,6 +76,7 @@ class App extends StatelessWidget {
7276
final DataRepository<Country> _countriesRepository;
7377
final DataRepository<Language> _languagesRepository;
7478
final DataRepository<LocalAd> _localAdsRepository;
79+
final DataRepository<User> _usersRepository;
7580
final KVStorageService _kvStorageService;
7681
final AppEnvironment _environment;
7782

@@ -93,6 +98,7 @@ class App extends StatelessWidget {
9398
RepositoryProvider.value(value: _countriesRepository),
9499
RepositoryProvider.value(value: _languagesRepository),
95100
RepositoryProvider.value(value: _localAdsRepository),
101+
RepositoryProvider.value(value: _usersRepository),
96102
RepositoryProvider.value(value: _kvStorageService),
97103
RepositoryProvider(
98104
create: (context) => const ThrottledFetchingService(),
@@ -163,6 +169,15 @@ class App extends StatelessWidget {
163169
sourcesRepository: context.read<DataRepository<Source>>(),
164170
),
165171
),
172+
// The UserFilterBloc is provided here to be available for both the
173+
// UserManagementBloc and the UI components.
174+
BlocProvider(create: (_) => UserFilterBloc()),
175+
BlocProvider(
176+
create: (context) => UserManagementBloc(
177+
usersRepository: context.read<DataRepository<User>>(),
178+
userFilterBloc: context.read<UserFilterBloc>(),
179+
),
180+
),
166181
],
167182
child: _AppView(
168183
authenticationRepository: _authenticationRepository,

lib/app/view/app_shell.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ class AppShell extends StatelessWidget {
5050
selectedIcon: const Icon(Icons.folder),
5151
label: l10n.contentManagement,
5252
),
53+
NavigationDestination(
54+
icon: const Icon(Icons.people_outline),
55+
selectedIcon: const Icon(Icons.people),
56+
label: l10n.userManagement,
57+
),
5358
NavigationDestination(
5459
icon: const Icon(Icons.settings_applications_outlined),
5560
selectedIcon: const Icon(Icons.settings_applications),

lib/bootstrap.dart

Lines changed: 15 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Future<Widget> bootstrap(
6565
DataClient<Country> countriesClient;
6666
DataClient<Language> languagesClient;
6767
DataClient<LocalAd> localAdsClient;
68+
DataClient<User> usersClient;
6869

6970
if (appConfig.environment == app_config.AppEnvironment.demo) {
7071
headlinesClient = DataInMemory<Headline>(
@@ -125,76 +126,11 @@ Future<Widget> bootstrap(
125126
initialData: localAdsFixturesData,
126127
logger: Logger('DataInMemory<LocalAd>'),
127128
);
128-
} else if (appConfig.environment == app_config.AppEnvironment.development) {
129-
headlinesClient = DataApi<Headline>(
130-
httpClient: httpClient!,
131-
modelName: 'headline',
132-
fromJson: Headline.fromJson,
133-
toJson: (headline) => headline.toJson(),
134-
logger: Logger('DataApi<Headline>'),
135-
);
136-
topicsClient = DataApi<Topic>(
137-
httpClient: httpClient,
138-
modelName: 'topic',
139-
fromJson: Topic.fromJson,
140-
toJson: (topic) => topic.toJson(),
141-
logger: Logger('DataApi<Topic>'),
142-
);
143-
sourcesClient = DataApi<Source>(
144-
httpClient: httpClient,
145-
modelName: 'source',
146-
fromJson: Source.fromJson,
147-
toJson: (source) => source.toJson(),
148-
logger: Logger('DataApi<Source>'),
149-
);
150-
userContentPreferencesClient = DataApi<UserContentPreferences>(
151-
httpClient: httpClient,
152-
modelName: 'user_content_preferences',
153-
fromJson: UserContentPreferences.fromJson,
154-
toJson: (prefs) => prefs.toJson(),
155-
logger: Logger('DataApi<UserContentPreferences>'),
156-
);
157-
userAppSettingsClient = DataApi<UserAppSettings>(
158-
httpClient: httpClient,
159-
modelName: 'user_app_settings',
160-
fromJson: UserAppSettings.fromJson,
161-
toJson: (settings) => settings.toJson(),
162-
logger: Logger('DataApi<UserAppSettings>'),
163-
);
164-
remoteConfigClient = DataApi<RemoteConfig>(
165-
httpClient: httpClient,
166-
modelName: 'remote_config',
167-
fromJson: RemoteConfig.fromJson,
168-
toJson: (config) => config.toJson(),
169-
logger: Logger('DataApi<RemoteConfig>'),
170-
);
171-
dashboardSummaryClient = DataApi<DashboardSummary>(
172-
httpClient: httpClient,
173-
modelName: 'dashboard_summary',
174-
fromJson: DashboardSummary.fromJson,
175-
toJson: (summary) => summary.toJson(),
176-
logger: Logger('DataApi<DashboardSummary>'),
177-
);
178-
countriesClient = DataApi<Country>(
179-
httpClient: httpClient,
180-
modelName: 'country',
181-
fromJson: Country.fromJson,
182-
toJson: (country) => country.toJson(),
183-
logger: Logger('DataApi<Country>'),
184-
);
185-
languagesClient = DataApi<Language>(
186-
httpClient: httpClient,
187-
modelName: 'language',
188-
fromJson: Language.fromJson,
189-
toJson: (language) => language.toJson(),
190-
logger: Logger('DataApi<Language>'),
191-
);
192-
localAdsClient = DataApi<LocalAd>(
193-
httpClient: httpClient,
194-
modelName: 'local_ad',
195-
fromJson: LocalAd.fromJson,
196-
toJson: LocalAd.toJson,
197-
logger: Logger('DataApi<LocalAd>'),
129+
usersClient = DataInMemory<User>(
130+
toJson: (i) => i.toJson(),
131+
getId: (i) => i.id,
132+
// No initial data for users in demo mode.
133+
logger: Logger('DataInMemory<User>'),
198134
);
199135
} else {
200136
headlinesClient = DataApi<Headline>(
@@ -267,6 +203,13 @@ Future<Widget> bootstrap(
267203
toJson: FeedItem.toJson,
268204
logger: Logger('DataApi<LocalAd>'),
269205
);
206+
usersClient = DataApi<User>(
207+
httpClient: httpClient,
208+
modelName: 'user',
209+
fromJson: User.fromJson,
210+
toJson: (user) => user.toJson(),
211+
logger: Logger('DataApi<User>'),
212+
);
270213
}
271214

272215
pendingDeletionsService = PendingDeletionsServiceImpl(
@@ -300,6 +243,7 @@ Future<Widget> bootstrap(
300243
final localAdsRepository = DataRepository<LocalAd>(
301244
dataClient: localAdsClient,
302245
);
246+
final usersRepository = DataRepository<User>(dataClient: usersClient);
303247

304248
return App(
305249
authenticationRepository: authenticationRepository,
@@ -313,6 +257,7 @@ Future<Widget> bootstrap(
313257
countriesRepository: countriesRepository,
314258
languagesRepository: languagesRepository,
315259
localAdsRepository: localAdsRepository,
260+
usersRepository: usersRepository,
316261
storageService: kvStorage,
317262
environment: environment,
318263
pendingDeletionsService: pendingDeletionsService,

lib/l10n/app_localizations.dart

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2845,6 +2845,138 @@ abstract class AppLocalizations {
28452845
/// In en, this message translates to:
28462846
/// **'Logo URL'**
28472847
String get logoUrl;
2848+
2849+
/// Label for the user management navigation item
2850+
///
2851+
/// In en, this message translates to:
2852+
/// **'User Management'**
2853+
String get userManagement;
2854+
2855+
/// Description for the User Management page
2856+
///
2857+
/// In en, this message translates to:
2858+
/// **'Manage system users, including their roles and permissions.'**
2859+
String get userManagementPageDescription;
2860+
2861+
/// Headline for loading state of users
2862+
///
2863+
/// In en, this message translates to:
2864+
/// **'Loading Users'**
2865+
String get loadingUsers;
2866+
2867+
/// Message when no users are found
2868+
///
2869+
/// In en, this message translates to:
2870+
/// **'No users found.'**
2871+
String get noUsersFound;
2872+
2873+
/// Column header for user email
2874+
///
2875+
/// In en, this message translates to:
2876+
/// **'Email'**
2877+
String get email;
2878+
2879+
/// Column header for user app role
2880+
///
2881+
/// In en, this message translates to:
2882+
/// **'App Role'**
2883+
String get appRole;
2884+
2885+
/// Column header for user dashboard role
2886+
///
2887+
/// In en, this message translates to:
2888+
/// **'Dashboard Role'**
2889+
String get dashboardRole;
2890+
2891+
/// Column header for creation date
2892+
///
2893+
/// In en, this message translates to:
2894+
/// **'Created At'**
2895+
String get createdAt;
2896+
2897+
/// Action to promote a user to a publisher role.
2898+
///
2899+
/// In en, this message translates to:
2900+
/// **'Promote to Publisher'**
2901+
String get promoteToPublisher;
2902+
2903+
/// Action to demote a publisher back to a standard user role.
2904+
///
2905+
/// In en, this message translates to:
2906+
/// **'Demote to User'**
2907+
String get demoteToUser;
2908+
2909+
/// Localized name for DashboardUserRole.admin
2910+
///
2911+
/// In en, this message translates to:
2912+
/// **'Admin'**
2913+
String get adminRole;
2914+
2915+
/// Localized name for DashboardUserRole.publisher
2916+
///
2917+
/// In en, this message translates to:
2918+
/// **'Publisher'**
2919+
String get publisherRole;
2920+
2921+
/// Title for the filter dialog when filtering users.
2922+
///
2923+
/// In en, this message translates to:
2924+
/// **'Filter Users'**
2925+
String get filterUsers;
2926+
2927+
/// Hint text for the user search field.
2928+
///
2929+
/// In en, this message translates to:
2930+
/// **'Search by user email...'**
2931+
String get searchByUserEmail;
2932+
2933+
/// Hint text for selecting app roles in a filter dialog.
2934+
///
2935+
/// In en, this message translates to:
2936+
/// **'Select App Roles'**
2937+
String get selectAppRoles;
2938+
2939+
/// Hint text for selecting dashboard roles in a filter dialog.
2940+
///
2941+
/// In en, this message translates to:
2942+
/// **'Select Dashboard Roles'**
2943+
String get selectDashboardRoles;
2944+
2945+
/// Column header for authentication status
2946+
///
2947+
/// In en, this message translates to:
2948+
/// **'Authentication'**
2949+
String get authentication;
2950+
2951+
/// Column header for subscription status
2952+
///
2953+
/// In en, this message translates to:
2954+
/// **'Subscription'**
2955+
String get subscription;
2956+
2957+
/// Authentication status for a guest user
2958+
///
2959+
/// In en, this message translates to:
2960+
/// **'Anonymous'**
2961+
String get authenticationAnonymous;
2962+
2963+
/// Authentication status for a signed-in user
2964+
///
2965+
/// In en, this message translates to:
2966+
/// **'Authenticated'**
2967+
String get authenticationAuthenticated;
2968+
2969+
/// Subscription status for a free user
2970+
///
2971+
/// In en, this message translates to:
2972+
/// **'Free'**
2973+
String get subscriptionFree;
2974+
2975+
/// Subscription status for a premium user
2976+
///
2977+
/// In en, this message translates to:
2978+
/// **'Premium'**
2979+
String get subscriptionPremium;
28482980
}
28492981

28502982
class _AppLocalizationsDelegate

0 commit comments

Comments
 (0)