From 100c51c865fc2621890b316bcc5368b0c2e4dd8f Mon Sep 17 00:00:00 2001 From: sBubshait Date: Wed, 23 Jul 2025 15:10:50 +0300 Subject: [PATCH] feat: creating users from the front end for admin users --- .../wesal/wesal/dto/CreateUserRequest.java | 2 +- .../wesal/wesal/dto/UpdateUserRequest.java | 2 +- .../java/online/wesal/wesal/entity/User.java | 2 +- frontend/lib/screens/pages/profile_page.dart | 294 ++++++++++++++++++ frontend/lib/services/user_service.dart | 35 +++ frontend/lib/utils/password_generator.dart | 16 + 6 files changed, 348 insertions(+), 3 deletions(-) create mode 100644 frontend/lib/utils/password_generator.dart diff --git a/backend/src/main/java/online/wesal/wesal/dto/CreateUserRequest.java b/backend/src/main/java/online/wesal/wesal/dto/CreateUserRequest.java index 579aa49..eada67c 100644 --- a/backend/src/main/java/online/wesal/wesal/dto/CreateUserRequest.java +++ b/backend/src/main/java/online/wesal/wesal/dto/CreateUserRequest.java @@ -11,7 +11,7 @@ public class CreateUserRequest { private String email; @NotBlank - @Size(min = 8) + @Size(min = 6) private String password; @NotBlank diff --git a/backend/src/main/java/online/wesal/wesal/dto/UpdateUserRequest.java b/backend/src/main/java/online/wesal/wesal/dto/UpdateUserRequest.java index 1f5b171..7556c3a 100644 --- a/backend/src/main/java/online/wesal/wesal/dto/UpdateUserRequest.java +++ b/backend/src/main/java/online/wesal/wesal/dto/UpdateUserRequest.java @@ -10,7 +10,7 @@ public class UpdateUserRequest { private String avatar; - @Size(min = 8, message = "Password must be at least 8 characters long") + @Size(min = 6, message = "Password must be at least 8 characters long") private String password; public UpdateUserRequest() {} diff --git a/backend/src/main/java/online/wesal/wesal/entity/User.java b/backend/src/main/java/online/wesal/wesal/entity/User.java index 75f8ee4..25e900e 100644 --- a/backend/src/main/java/online/wesal/wesal/entity/User.java +++ b/backend/src/main/java/online/wesal/wesal/entity/User.java @@ -23,7 +23,7 @@ public class User { @Column(nullable = false) @NotBlank - @Size(min = 8) + @Size(min = 6) private String password; @Column(nullable = false) diff --git a/frontend/lib/screens/pages/profile_page.dart b/frontend/lib/screens/pages/profile_page.dart index 2dcc633..360e4b3 100644 --- a/frontend/lib/screens/pages/profile_page.dart +++ b/frontend/lib/screens/pages/profile_page.dart @@ -6,6 +6,7 @@ import '../../services/auth_service.dart'; import '../../services/post_service.dart'; import '../../models/post_models.dart'; import '../../widgets/posts_list.dart'; +import '../../utils/password_generator.dart'; class ProfilePage extends StatefulWidget { @override @@ -288,15 +289,28 @@ class _SettingsPageState extends State { final TextEditingController _tokenController = TextEditingController(); bool isLoading = false; + // Admin user creation + Map? userData; + bool isLoadingUser = true; + bool isCreatingUser = false; + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _displayNameController = TextEditingController(); + @override void initState() { super.initState(); _loadFCMToken(); + _loadUserData(); + _generatePassword(); } @override void dispose() { _tokenController.dispose(); + _emailController.dispose(); + _passwordController.dispose(); + _displayNameController.dispose(); super.dispose(); } @@ -333,6 +347,68 @@ class _SettingsPageState extends State { } } + Future _loadUserData() async { + setState(() { + isLoadingUser = true; + }); + + final result = await UserService.getCurrentUser(); + + setState(() { + isLoadingUser = false; + if (result['success'] == true) { + userData = result['data']; + } else { + userData = null; + } + }); + } + + void _generatePassword() { + _passwordController.text = PasswordGenerator.generateReadablePassword(); + } + + bool get _isAdmin { + return userData?['role'] == 'ADMIN'; + } + + Future> _createUserAccount() async { + if (_emailController.text.trim().isEmpty || + _displayNameController.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Please fill in all fields'), + backgroundColor: Colors.red, + ), + ); + return {'success': false, 'message': 'Please fill in all fields'}; + } + + setState(() { + isCreatingUser = true; + }); + + try { + final result = await UserService.createUser( + email: _emailController.text.trim(), + password: _passwordController.text, + displayName: _displayNameController.text.trim(), + ); + + setState(() { + isCreatingUser = false; + }); + + return result; + } catch (e) { + setState(() { + isCreatingUser = false; + }); + + return {'success': false, 'message': 'Error creating user: $e'}; + } + } + void _signOut() async { showDialog( context: context, @@ -363,6 +439,193 @@ class _SettingsPageState extends State { ); } + void _showCredentialsDialog(String email, String password) { + final credentialsText = 'Email: $email\nPassword: $password'; + final TextEditingController credentialsController = TextEditingController( + text: credentialsText, + ); + + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: Text('Account Created Successfully!'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Share these credentials with the user:', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + ), + SizedBox(height: 16), + + TextField( + controller: credentialsController, + decoration: InputDecoration( + border: OutlineInputBorder(), + suffixIcon: IconButton( + icon: Icon(Icons.copy), + onPressed: () async { + await Clipboard.setData( + ClipboardData(text: credentialsText), + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Credentials copied to clipboard'), + backgroundColor: Color(0xFF6A4C93), + ), + ); + }, + tooltip: 'Copy credentials', + ), + ), + maxLines: 2, + readOnly: true, + style: TextStyle(fontFamily: 'monospace', fontSize: 14), + ), + ], + ), + actions: [ + ElevatedButton( + onPressed: () => Navigator.of(dialogContext).pop(), + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, + ), + child: Text('Done'), + ), + ], + ); + }, + ); + } + + void _showCreateAccountDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return StatefulBuilder( + builder: (context, setDialogState) { + return AlertDialog( + title: Text('Create Account'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _emailController, + decoration: InputDecoration( + labelText: 'Email', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.email), + ), + keyboardType: TextInputType.emailAddress, + ), + SizedBox(height: 16), + + TextField( + controller: _displayNameController, + decoration: InputDecoration( + labelText: 'Display Name', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.person), + ), + ), + SizedBox(height: 16), + + TextField( + controller: _passwordController, + decoration: InputDecoration( + labelText: 'Password', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.lock), + suffixIcon: IconButton( + icon: Icon(Icons.refresh), + onPressed: () { + setDialogState(() { + _generatePassword(); + }); + }, + tooltip: 'Generate new password', + ), + ), + readOnly: true, + style: TextStyle( + fontFamily: 'monospace', + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8), + + Text( + 'Password is auto-generated for security', + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text('Cancel'), + ), + ElevatedButton( + onPressed: isCreatingUser + ? null + : () async { + final email = _emailController.text.trim(); + final password = _passwordController.text; + + final result = await _createUserAccount(); + + if (mounted) { + Navigator.of(dialogContext).pop(); + + if (result['success']) { + _showCredentialsDialog(email, password); + // Clear form + _emailController.clear(); + _displayNameController.clear(); + _generatePassword(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + result['message'] ?? + 'Failed to create user', + ), + backgroundColor: Colors.red, + ), + ); + } + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, + ), + child: isCreatingUser + ? SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), + ), + ) + : Text('Create'), + ), + ], + ); + }, + ); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -382,6 +645,37 @@ class _SettingsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (!isLoadingUser && _isAdmin) ...[ + Text( + 'Admin Tools', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Color(0xFF6A4C93), + ), + ), + SizedBox(height: 16), + + Container( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () => _showCreateAccountDialog(context), + icon: Icon(Icons.person_add, size: 18), + label: Text('Create an Account'), + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), + ), + ), + ), + + SizedBox(height: 32), + ], + Text( 'Development Tools', style: TextStyle( diff --git a/frontend/lib/services/user_service.dart b/frontend/lib/services/user_service.dart index 6793c47..5ec856d 100644 --- a/frontend/lib/services/user_service.dart +++ b/frontend/lib/services/user_service.dart @@ -78,4 +78,39 @@ class UserService { }; } } + + static Future> createUser({ + required String email, + required String password, + required String displayName, + }) async { + try { + final response = await HttpService.post('/admin/createUser', { + 'email': email, + 'password': password, + 'displayName': displayName, + }); + + final responseData = jsonDecode(response.body); + + if (response.statusCode == 200 || response.statusCode == 201) { + return { + 'success': responseData['status'] ?? false, + 'message': responseData['message'] ?? 'User created successfully', + 'data': responseData['data'], + }; + } else { + return { + 'success': false, + 'message': responseData['message'] ?? 'Failed to create user', + }; + } + } catch (e) { + print('Error creating user: $e'); + return { + 'success': false, + 'message': 'Network error. Please check your connection.', + }; + } + } } diff --git a/frontend/lib/utils/password_generator.dart b/frontend/lib/utils/password_generator.dart new file mode 100644 index 0000000..4160124 --- /dev/null +++ b/frontend/lib/utils/password_generator.dart @@ -0,0 +1,16 @@ +import 'dart:math'; + +class PasswordGenerator { + static const String _readableChars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + static final Random _random = Random(); + + static String generateReadablePassword({int length = 6}) { + StringBuffer password = StringBuffer(); + + for (int i = 0; i < length; i++) { + password.write(_readableChars[_random.nextInt(_readableChars.length)]); + } + + return password.toString(); + } +} \ No newline at end of file