Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e261ef0
feat(routing): add routes for user management feature
fulleni Oct 31, 2025
18c4b8a
chore: boilerplate
fulleni Oct 31, 2025
6a5f637
feat(user_management): instantiate user data repository
fulleni Oct 31, 2025
fe622bd
feat(user_management): create user filter state
fulleni Oct 31, 2025
21f0a3c
feat(user_management): create user filter events
fulleni Oct 31, 2025
c3d3d88
feat(user_management): create user filter bloc
fulleni Oct 31, 2025
f9731e4
feat(user_management): create user management state
fulleni Oct 31, 2025
5eacf77
feat(user_management): create user management events
fulleni Oct 31, 2025
c6fefe1
feat(user_management): create user management bloc
fulleni Oct 31, 2025
bd55313
feat(user_management): create user management page
fulleni Oct 31, 2025
1fcf378
feat(user_management): create users page with data table
fulleni Oct 31, 2025
94b995c
feat(user_management): create user action buttons widget
fulleni Oct 31, 2025
1d26d61
feat(user_management): create user filter dialog state
fulleni Oct 31, 2025
17ba844
feat(user_management): create user filter dialog events
fulleni Oct 31, 2025
9ab157a
feat(user_management): create user filter dialog bloc
fulleni Oct 31, 2025
bb5d788
feat(user_management): create user filter dialog widget
fulleni Oct 31, 2025
8f40ff3
feat(user_management): provide user management blocs in app
fulleni Oct 31, 2025
f102018
refactor(user_management): reorganize user_filter bloc directory stru…
fulleni Oct 31, 2025
5c66e10
feat(user_management): add user management to navigation shell
fulleni Oct 31, 2025
3dfd1cd
feat(user_management): add user management routes to router
fulleni Oct 31, 2025
cb82c6d
feat(l10n): add user management translations
fulleni Oct 31, 2025
f6d1c6b
feat(l10n): add user management localization
fulleni Oct 31, 2025
21b185f
docs(README): add user and role management section
fulleni Oct 31, 2025
214ef81
refactor(user_management): improve code maintainability
fulleni Oct 31, 2025
84aa764
fix(user_management): update localization import path
fulleni Oct 31, 2025
5b40c85
fix(user_management): update import path for AppLocalizations
fulleni Oct 31, 2025
246e975
feat(extensions): add new localization exports
fulleni Oct 31, 2025
1540211
fix(user_management): resolve ambiguous extension errors
fulleni Oct 31, 2025
16e84e8
lint: misc
fulleni Oct 31, 2025
8279722
stye: format
fulleni Oct 31, 2025
bbcf16e
fix(navigation): correct router branch order
fulleni Oct 31, 2025
3621a6c
fix(user_management): refine user table UI and add derived columns
fulleni Oct 31, 2025
ae959c8
refactor(user_management): Align user action buttons with content act…
fulleni Oct 31, 2025
8b14f39
feat(l10n): add authentication and subscription status translations
fulleni Oct 31, 2025
57c774b
build: l10n
fulleni Oct 31, 2025
244d92c
style: format
fulleni Oct 31, 2025
5a27a6f
style(user_management): adjust created at column size
fulleni Oct 31, 2025
3d0e8f1
feat(user_management): add logging and improve error handling
fulleni Oct 31, 2025
7556d2f
feat(user_management): optimize users table for mobile view
fulleni Nov 1, 2025
59f855c
feat(user_management): conditionally display authentication column ba…
fulleni Nov 1, 2025
a8592a4
refactor(user-management): add robust error handling to app role changes
fulleni Nov 1, 2025
3f6980d
refactor(bootstrap): remove redundant client initializations
fulleni Nov 1, 2025
550ab57
refactor(user-filter): remove unused status property from dialog state
fulleni Nov 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ Manage the entire lifecycle of your content from a single, intuitive interface.

</details>

<details>
<summary><strong>👥 User & Role Management</strong></summary>

### 👥 Granular User & Role Management
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.
- **Full User Roster:** See a comprehensive list of all users, including their email, app subscription level, and current dashboard role.
- **Dynamic Role Promotion:** Promote trusted users to a "Publisher" role, granting them content management capabilities without full administrative access.
- **Powerful Filtering:** Quickly locate specific users or user segments with multi-faceted filtering by email, app role, and dashboard role.
> **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.

</details>


<details>
<summary><strong>⚙️ App Monetization & Remote Control</strong></summary>

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

---

Expand Down
15 changes: 15 additions & 0 deletions lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/overview/bloc/ov
import 'package:flutter_news_app_web_dashboard_full_source_code/router/router.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/bloc/user_filter/user_filter_bloc.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/bloc/user_management_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:kv_storage_service/kv_storage_service.dart';
import 'package:logging/logging.dart';
Expand All @@ -41,6 +43,7 @@ class App extends StatelessWidget {
required DataRepository<Country> countriesRepository,
required DataRepository<Language> languagesRepository,
required DataRepository<LocalAd> localAdsRepository,
required DataRepository<User> usersRepository,
required KVStorageService storageService,
required AppEnvironment environment,
required PendingDeletionsService pendingDeletionsService,
Expand All @@ -57,6 +60,7 @@ class App extends StatelessWidget {
_countriesRepository = countriesRepository,
_languagesRepository = languagesRepository,
_localAdsRepository = localAdsRepository,
_usersRepository = usersRepository,
_environment = environment,
_pendingDeletionsService = pendingDeletionsService;

Expand All @@ -72,6 +76,7 @@ class App extends StatelessWidget {
final DataRepository<Country> _countriesRepository;
final DataRepository<Language> _languagesRepository;
final DataRepository<LocalAd> _localAdsRepository;
final DataRepository<User> _usersRepository;
final KVStorageService _kvStorageService;
final AppEnvironment _environment;

Expand All @@ -93,6 +98,7 @@ class App extends StatelessWidget {
RepositoryProvider.value(value: _countriesRepository),
RepositoryProvider.value(value: _languagesRepository),
RepositoryProvider.value(value: _localAdsRepository),
RepositoryProvider.value(value: _usersRepository),
RepositoryProvider.value(value: _kvStorageService),
RepositoryProvider(
create: (context) => const ThrottledFetchingService(),
Expand Down Expand Up @@ -163,6 +169,15 @@ class App extends StatelessWidget {
sourcesRepository: context.read<DataRepository<Source>>(),
),
),
// The UserFilterBloc is provided here to be available for both the
// UserManagementBloc and the UI components.
BlocProvider(create: (_) => UserFilterBloc()),
BlocProvider(
create: (context) => UserManagementBloc(
usersRepository: context.read<DataRepository<User>>(),
userFilterBloc: context.read<UserFilterBloc>(),
),
),
],
child: _AppView(
authenticationRepository: _authenticationRepository,
Expand Down
5 changes: 5 additions & 0 deletions lib/app/view/app_shell.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ class AppShell extends StatelessWidget {
selectedIcon: const Icon(Icons.folder),
label: l10n.contentManagement,
),
NavigationDestination(
icon: const Icon(Icons.people_outline),
selectedIcon: const Icon(Icons.people),
label: l10n.userManagement,
),
NavigationDestination(
icon: const Icon(Icons.settings_applications_outlined),
selectedIcon: const Icon(Icons.settings_applications),
Expand Down
85 changes: 15 additions & 70 deletions lib/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Future<Widget> bootstrap(
DataClient<Country> countriesClient;
DataClient<Language> languagesClient;
DataClient<LocalAd> localAdsClient;
DataClient<User> usersClient;

if (appConfig.environment == app_config.AppEnvironment.demo) {
headlinesClient = DataInMemory<Headline>(
Expand Down Expand Up @@ -125,76 +126,11 @@ Future<Widget> bootstrap(
initialData: localAdsFixturesData,
logger: Logger('DataInMemory<LocalAd>'),
);
} else if (appConfig.environment == app_config.AppEnvironment.development) {
headlinesClient = DataApi<Headline>(
httpClient: httpClient!,
modelName: 'headline',
fromJson: Headline.fromJson,
toJson: (headline) => headline.toJson(),
logger: Logger('DataApi<Headline>'),
);
topicsClient = DataApi<Topic>(
httpClient: httpClient,
modelName: 'topic',
fromJson: Topic.fromJson,
toJson: (topic) => topic.toJson(),
logger: Logger('DataApi<Topic>'),
);
sourcesClient = DataApi<Source>(
httpClient: httpClient,
modelName: 'source',
fromJson: Source.fromJson,
toJson: (source) => source.toJson(),
logger: Logger('DataApi<Source>'),
);
userContentPreferencesClient = DataApi<UserContentPreferences>(
httpClient: httpClient,
modelName: 'user_content_preferences',
fromJson: UserContentPreferences.fromJson,
toJson: (prefs) => prefs.toJson(),
logger: Logger('DataApi<UserContentPreferences>'),
);
userAppSettingsClient = DataApi<UserAppSettings>(
httpClient: httpClient,
modelName: 'user_app_settings',
fromJson: UserAppSettings.fromJson,
toJson: (settings) => settings.toJson(),
logger: Logger('DataApi<UserAppSettings>'),
);
remoteConfigClient = DataApi<RemoteConfig>(
httpClient: httpClient,
modelName: 'remote_config',
fromJson: RemoteConfig.fromJson,
toJson: (config) => config.toJson(),
logger: Logger('DataApi<RemoteConfig>'),
);
dashboardSummaryClient = DataApi<DashboardSummary>(
httpClient: httpClient,
modelName: 'dashboard_summary',
fromJson: DashboardSummary.fromJson,
toJson: (summary) => summary.toJson(),
logger: Logger('DataApi<DashboardSummary>'),
);
countriesClient = DataApi<Country>(
httpClient: httpClient,
modelName: 'country',
fromJson: Country.fromJson,
toJson: (country) => country.toJson(),
logger: Logger('DataApi<Country>'),
);
languagesClient = DataApi<Language>(
httpClient: httpClient,
modelName: 'language',
fromJson: Language.fromJson,
toJson: (language) => language.toJson(),
logger: Logger('DataApi<Language>'),
);
localAdsClient = DataApi<LocalAd>(
httpClient: httpClient,
modelName: 'local_ad',
fromJson: LocalAd.fromJson,
toJson: LocalAd.toJson,
logger: Logger('DataApi<LocalAd>'),
usersClient = DataInMemory<User>(
toJson: (i) => i.toJson(),
getId: (i) => i.id,
// No initial data for users in demo mode.
logger: Logger('DataInMemory<User>'),
);
} else {
headlinesClient = DataApi<Headline>(
Expand Down Expand Up @@ -267,6 +203,13 @@ Future<Widget> bootstrap(
toJson: FeedItem.toJson,
logger: Logger('DataApi<LocalAd>'),
);
usersClient = DataApi<User>(
httpClient: httpClient,
modelName: 'user',
fromJson: User.fromJson,
toJson: (user) => user.toJson(),
logger: Logger('DataApi<User>'),
);
}

pendingDeletionsService = PendingDeletionsServiceImpl(
Expand Down Expand Up @@ -300,6 +243,7 @@ Future<Widget> bootstrap(
final localAdsRepository = DataRepository<LocalAd>(
dataClient: localAdsClient,
);
final usersRepository = DataRepository<User>(dataClient: usersClient);

return App(
authenticationRepository: authenticationRepository,
Expand All @@ -313,6 +257,7 @@ Future<Widget> bootstrap(
countriesRepository: countriesRepository,
languagesRepository: languagesRepository,
localAdsRepository: localAdsRepository,
usersRepository: usersRepository,
storageService: kvStorage,
environment: environment,
pendingDeletionsService: pendingDeletionsService,
Expand Down
132 changes: 132 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2845,6 +2845,138 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Logo URL'**
String get logoUrl;

/// Label for the user management navigation item
///
/// In en, this message translates to:
/// **'User Management'**
String get userManagement;

/// Description for the User Management page
///
/// In en, this message translates to:
/// **'Manage system users, including their roles and permissions.'**
String get userManagementPageDescription;

/// Headline for loading state of users
///
/// In en, this message translates to:
/// **'Loading Users'**
String get loadingUsers;

/// Message when no users are found
///
/// In en, this message translates to:
/// **'No users found.'**
String get noUsersFound;

/// Column header for user email
///
/// In en, this message translates to:
/// **'Email'**
String get email;

/// Column header for user app role
///
/// In en, this message translates to:
/// **'App Role'**
String get appRole;

/// Column header for user dashboard role
///
/// In en, this message translates to:
/// **'Dashboard Role'**
String get dashboardRole;

/// Column header for creation date
///
/// In en, this message translates to:
/// **'Created At'**
String get createdAt;

/// Action to promote a user to a publisher role.
///
/// In en, this message translates to:
/// **'Promote to Publisher'**
String get promoteToPublisher;

/// Action to demote a publisher back to a standard user role.
///
/// In en, this message translates to:
/// **'Demote to User'**
String get demoteToUser;

/// Localized name for DashboardUserRole.admin
///
/// In en, this message translates to:
/// **'Admin'**
String get adminRole;

/// Localized name for DashboardUserRole.publisher
///
/// In en, this message translates to:
/// **'Publisher'**
String get publisherRole;

/// Title for the filter dialog when filtering users.
///
/// In en, this message translates to:
/// **'Filter Users'**
String get filterUsers;

/// Hint text for the user search field.
///
/// In en, this message translates to:
/// **'Search by user email...'**
String get searchByUserEmail;

/// Hint text for selecting app roles in a filter dialog.
///
/// In en, this message translates to:
/// **'Select App Roles'**
String get selectAppRoles;

/// Hint text for selecting dashboard roles in a filter dialog.
///
/// In en, this message translates to:
/// **'Select Dashboard Roles'**
String get selectDashboardRoles;

/// Column header for authentication status
///
/// In en, this message translates to:
/// **'Authentication'**
String get authentication;

/// Column header for subscription status
///
/// In en, this message translates to:
/// **'Subscription'**
String get subscription;

/// Authentication status for a guest user
///
/// In en, this message translates to:
/// **'Anonymous'**
String get authenticationAnonymous;

/// Authentication status for a signed-in user
///
/// In en, this message translates to:
/// **'Authenticated'**
String get authenticationAuthenticated;

/// Subscription status for a free user
///
/// In en, this message translates to:
/// **'Free'**
String get subscriptionFree;

/// Subscription status for a premium user
///
/// In en, this message translates to:
/// **'Premium'**
String get subscriptionPremium;
}

class _AppLocalizationsDelegate
Expand Down
Loading
Loading