diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-22 16:37:08 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-22 16:37:08 +0300 |
| commit | 874a305bbc23e9a1e6428b4e87071c3567317545 (patch) | |
| tree | dd159396b6c907fd2b3bf65ad25169405d581787 | |
| parent | 34e0053b156396d5c7be0831cabb71c041deadb9 (diff) | |
Fix final review issues for AdminUsersScreen (task db)
- Add bounds check on optimistic create success path to mirror the error path guard
- Refactor _CreateUserDialogState.build() by extracting _buildUsernameField() and _buildPasswordField() helpers; remove stale line-count comment
- Extract _AdminSection ConsumerWidget from SettingsScreen.build() following the _ThemeToggle pattern
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | player-android/lib/screens/admin_users_screen.dart | 115 | ||||
| -rw-r--r-- | player-android/lib/screens/settings_screen.dart | 68 |
2 files changed, 109 insertions, 74 deletions
diff --git a/player-android/lib/screens/admin_users_screen.dart b/player-android/lib/screens/admin_users_screen.dart index 77c2db8..a51aabb 100644 --- a/player-android/lib/screens/admin_users_screen.dart +++ b/player-android/lib/screens/admin_users_screen.dart @@ -118,7 +118,13 @@ class _AdminUsersScreenState extends ConsumerState<AdminUsersScreen> { ); if (!mounted) return; // Replace the placeholder slot with the real user returned by the server. - setState(() { _users![placeholderIdx] = created; }); + // Guard bounds in case a concurrent refresh shrank the list while the + // create request was in flight (mirrors the bounds check in the error path). + setState(() { + if (placeholderIdx < (_users?.length ?? 0)) { + _users![placeholderIdx] = created; + } + }); } catch (e) { if (!mounted) return; // Revert optimistic insertion on error by removing the known slot. @@ -530,10 +536,64 @@ class _CreateUserDialogState extends State<_CreateUserDialog> { ); } + /// Builds the username TextFormField with non-empty validation. + Widget _buildUsernameField() { + return TextFormField( + key: const Key('admin_create_username'), + controller: _usernameController, + decoration: const InputDecoration( + labelText: 'Username', + border: OutlineInputBorder(), + ), + textInputAction: TextInputAction.next, + autocorrect: false, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Username is required.'; + } + return null; + }, + ); + } + + /// Builds the password TextFormField with a visibility toggle and length + /// validation (minimum 8 characters). + Widget _buildPasswordField() { + return TextFormField( + key: const Key('admin_create_password'), + controller: _passwordController, + decoration: InputDecoration( + labelText: 'Password', + border: const OutlineInputBorder(), + // Toggle visibility icon so the admin can verify the typed password. + suffixIcon: IconButton( + icon: Icon( + _passwordVisible + ? Icons.visibility_off_outlined + : Icons.visibility_outlined, + ), + tooltip: _passwordVisible ? 'Hide password' : 'Show password', + onPressed: () => + setState(() => _passwordVisible = !_passwordVisible), + ), + ), + obscureText: !_passwordVisible, + textInputAction: TextInputAction.done, + onFieldSubmitted: (_) => _submit(), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Password is required.'; + } + if (value.length < 8) { + return 'Password must be at least 8 characters.'; + } + return null; + }, + ); + } + @override Widget build(BuildContext context) { - // _buildForm and _buildActions were single-call-site helpers; inlined here - // to reduce indirection. The merged build() stays well under 50 lines. return AlertDialog( key: const Key('admin_create_user_dialog'), title: const Text('Create user'), @@ -542,54 +602,9 @@ class _CreateUserDialogState extends State<_CreateUserDialog> { child: Column( mainAxisSize: MainAxisSize.min, children: [ - TextFormField( - key: const Key('admin_create_username'), - controller: _usernameController, - decoration: const InputDecoration( - labelText: 'Username', - border: OutlineInputBorder(), - ), - textInputAction: TextInputAction.next, - autocorrect: false, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Username is required.'; - } - return null; - }, - ), + _buildUsernameField(), const SizedBox(height: 16), - TextFormField( - key: const Key('admin_create_password'), - controller: _passwordController, - decoration: InputDecoration( - labelText: 'Password', - border: const OutlineInputBorder(), - // Toggle visibility icon so the admin can verify the typed password. - suffixIcon: IconButton( - icon: Icon( - _passwordVisible - ? Icons.visibility_off_outlined - : Icons.visibility_outlined, - ), - tooltip: _passwordVisible ? 'Hide password' : 'Show password', - onPressed: () => - setState(() => _passwordVisible = !_passwordVisible), - ), - ), - obscureText: !_passwordVisible, - textInputAction: TextInputAction.done, - onFieldSubmitted: (_) => _submit(), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Password is required.'; - } - if (value.length < 8) { - return 'Password must be at least 8 characters.'; - } - return null; - }, - ), + _buildPasswordField(), const SizedBox(height: 8), CheckboxListTile( key: const Key('admin_create_is_admin'), diff --git a/player-android/lib/screens/settings_screen.dart b/player-android/lib/screens/settings_screen.dart index 1260273..388acbe 100644 --- a/player-android/lib/screens/settings_screen.dart +++ b/player-android/lib/screens/settings_screen.dart @@ -257,33 +257,10 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> { onTap: () => context.go(AppRoutes.shares), ), - // ---------------------------------------------------------------- // Admin section: only visible to admin users. // Non-admin users are gated out here; the server enforces this // independently via 403 responses, making this defence-in-depth. - // ---------------------------------------------------------------- - if (isAdmin) ...[ - const SizedBox(height: 32), - const Divider(), - const SizedBox(height: 24), - - Text( - 'Administration', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 12), - - // Manage Users tile — navigates to /admin/users. - ListTile( - key: const Key('settings_manage_users'), - contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.manage_accounts_outlined), - title: const Text('Manage Users'), - subtitle: const Text('Create and delete user accounts'), - trailing: const Icon(Icons.chevron_right), - onTap: () => context.go(AppRoutes.adminUsers), - ), - ], + if (isAdmin) const _AdminSection(), ], ), ), @@ -341,6 +318,49 @@ class _ThemeToggle extends ConsumerWidget { } // --------------------------------------------------------------------------- +// Admin section widget +// --------------------------------------------------------------------------- + +/// Administration section shown only to admin users in [SettingsScreen]. +/// +/// Extracted as a [ConsumerWidget] (following the [_ThemeToggle] pattern) so +/// [_SettingsScreenState.build] does not need to reference [AppRoutes.adminUsers] +/// directly and stays focused on layout concerns. The server independently +/// enforces admin-only access via 403, so this UI gate is defence-in-depth. +class _AdminSection extends ConsumerWidget { + const _AdminSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 32), + const Divider(), + const SizedBox(height: 24), + + Text( + 'Administration', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 12), + + // Manage Users tile — navigates to /admin/users. + ListTile( + key: const Key('settings_manage_users'), + contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.manage_accounts_outlined), + title: const Text('Manage Users'), + subtitle: const Text('Create and delete user accounts'), + trailing: const Icon(Icons.chevron_right), + onTap: () => context.go(AppRoutes.adminUsers), + ), + ], + ); + } +} + +// --------------------------------------------------------------------------- // File-level helpers // --------------------------------------------------------------------------- |
