diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 5e160deb..bbdedced 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -137,6 +137,10 @@ func main() { admin := api.Group("/admin") admin.Post("/users", adminHandler.CreateUser) admin.Get("/check", adminHandler.CheckAuth) + admin.Get("/users", adminHandler.ListUsers) + admin.Patch("/users/:loginId", adminHandler.UpdateUser) + admin.Delete("/users/:loginId", adminHandler.DeleteUser) + admin.Patch("/users/:loginId/status", adminHandler.UpdateUserStatus) // Webhook for Descope Generic SMS Gateway auth.Post("/webhooks/descope-sms", authHandler.HandleDescopeSmsRelay) diff --git a/backend/internal/handler/admin_handler.go b/backend/internal/handler/admin_handler.go index 95871850..d54de25a 100644 --- a/backend/internal/handler/admin_handler.go +++ b/backend/internal/handler/admin_handler.go @@ -5,6 +5,7 @@ import ( "log" "os" "strings" + "net/url" "github.com/descope/go-sdk/descope" "github.com/descope/go-sdk/descope/client" @@ -43,17 +44,8 @@ func boolPtr(b bool) *bool { return &b } -type CreateUserRequest struct { - LoginID string `json:"loginId"` - Email string `json:"email"` - Phone string `json:"phone"` - DisplayName string `json:"displayName"` - Roles []string `json:"roles"` - Tenants map[string][]string `json:"tenants"` // tenantId -> roles -} - -func (h *AdminHandler) CreateUser(c *fiber.Ctx) error { - // 1. Simple Password Check +// checkAuth Helper +func (h *AdminHandler) checkAuth(c *fiber.Ctx) error { adminPass := os.Getenv("ADMIN_PASSWORD") if adminPass == "" { adminPass = "admin" // Default fallback @@ -63,6 +55,165 @@ func (h *AdminHandler) CreateUser(c *fiber.Ctx) error { if reqPass != adminPass { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid Admin Password"}) } + return nil +} + +type CreateUserRequest struct { + LoginID string `json:"loginId"` + Email string `json:"email"` + Phone string `json:"phone"` + DisplayName string `json:"displayName"` + Roles []string `json:"roles"` + Tenants map[string][]string `json:"tenants"` // tenantId -> roles +} + +func (h *AdminHandler) CheckAuth(c *fiber.Ctx) error { + if err := h.checkAuth(c); err != nil { + return err + } + return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "ok"}) +} + +// ListUsers - GET /api/v1/admin/users +func (h *AdminHandler) ListUsers(c *fiber.Ctx) error { + if err := h.checkAuth(c); err != nil { return err } + + text := c.Query("text") + // Limit is not directly supported in SearchAll options as a simple int in all SDK versions, + // but let's check the options struct. + // Based on previous inspection: SearchAll takes UserSearchOptions. + + var users []*descope.UserResponse + var err error + + if text != "" { + options := &descope.UserSearchOptions{ Text: text, Limit: 50 } + users, _, err = h.DescopeClient.Management.User().SearchAll(context.Background(), options) + } else { + // Nil options means default search (usually returns all or default page) + users, _, err = h.DescopeClient.Management.User().SearchAll(context.Background(), nil) + } + + if err != nil { + log.Printf("[Admin] ListUsers failed: %v", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + return c.JSON(fiber.Map{"users": users}) +} + +// DeleteUser - DELETE /api/v1/admin/users/:loginId +func (h *AdminHandler) DeleteUser(c *fiber.Ctx) error { + if err := h.checkAuth(c); err != nil { return err } + + loginID := c.Params("loginId") + // Decode if necessary (Fiber usually decodes params, but let's be safe if it's double encoded) + if decoded, err := url.QueryUnescape(loginID); err == nil { + loginID = decoded + } + + log.Printf("[Admin] Deleting user: %s", loginID) + if err := h.DescopeClient.Management.User().Delete(context.Background(), loginID); err != nil { + log.Printf("[Admin] DeleteUser failed: %v", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + return c.JSON(fiber.Map{"message": "User deleted successfully"}) +} + +// UpdateUserStatus - PATCH /api/v1/admin/users/:loginId/status +func (h *AdminHandler) UpdateUserStatus(c *fiber.Ctx) error { + if err := h.checkAuth(c); err != nil { return err } + + loginID := c.Params("loginId") + if decoded, err := url.QueryUnescape(loginID); err == nil { + loginID = decoded + } + + var req struct { + Status string `json:"status"` // "enabled" or "disabled" + } + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid body"}) + } + + var user *descope.UserResponse + var err error + + log.Printf("[Admin] Updating status for %s to %s", loginID, req.Status) + + if req.Status == "enabled" || req.Status == "active" { + user, err = h.DescopeClient.Management.User().Activate(context.Background(), loginID) + } else { + user, err = h.DescopeClient.Management.User().Deactivate(context.Background(), loginID) + } + + if err != nil { + log.Printf("[Admin] Status update failed: %v", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + return c.JSON(fiber.Map{ + "message": "Status updated", + "user": user, + }) +} + +// UpdateUser - PATCH /api/v1/admin/users/:loginId +func (h *AdminHandler) UpdateUser(c *fiber.Ctx) error { + if err := h.checkAuth(c); err != nil { return err } + + loginID := c.Params("loginId") + if decoded, err := url.QueryUnescape(loginID); err == nil { + loginID = decoded + } + + var req struct { + Email *string `json:"email"` + Phone *string `json:"phone"` + DisplayName *string `json:"displayName"` + } + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid body"}) + } + + ctx := context.Background() + var err error + + // Update Display Name + if req.DisplayName != nil { + _, err = h.DescopeClient.Management.User().UpdateDisplayName(ctx, loginID, *req.DisplayName) + if err != nil { + return c.Status(500).JSON(fiber.Map{"error": "Failed to update name: " + err.Error()}) + } + } + + // Update Email + if req.Email != nil { + _, err = h.DescopeClient.Management.User().UpdateEmail(ctx, loginID, *req.Email, true, false) // verified=true, addToLoginIDs=false + if err != nil { + return c.Status(500).JSON(fiber.Map{"error": "Failed to update email: " + err.Error()}) + } + } + + // Update Phone + if req.Phone != nil { + phone := *req.Phone + // Normalize + if strings.HasPrefix(phone, "010") { + phone = "+82" + phone[1:] + } + _, err = h.DescopeClient.Management.User().UpdatePhone(ctx, loginID, phone, true, false) // verified=true, addToLoginIDs=false + if err != nil { + return c.Status(500).JSON(fiber.Map{"error": "Failed to update phone: " + err.Error()}) + } + } + + return c.JSON(fiber.Map{"message": "User updated successfully"}) +} + +func (h *AdminHandler) CreateUser(c *fiber.Ctx) error { + if err := h.checkAuth(c); err != nil { return err } if h.DescopeClient == nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Descope Client not configured"}) @@ -127,18 +278,4 @@ func (h *AdminHandler) CreateUser(c *fiber.Ctx) error { "message": "User created successfully", "user": res, }) -} - -func (h *AdminHandler) CheckAuth(c *fiber.Ctx) error { - adminPass := os.Getenv("ADMIN_PASSWORD") - if adminPass == "" { - adminPass = "admin" - } - - reqPass := c.Get("X-Admin-Password") - if reqPass != adminPass { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid Admin Password"}) - } - - return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "ok"}) -} +} \ No newline at end of file diff --git a/frontend/lib/core/services/auth_proxy_service.dart b/frontend/lib/core/services/auth_proxy_service.dart index 27be3335..0fa208d8 100644 --- a/frontend/lib/core/services/auth_proxy_service.dart +++ b/frontend/lib/core/services/auth_proxy_service.dart @@ -143,6 +143,92 @@ class AuthProxyService { } } + static Future> listUsers(String adminPassword, {String? query}) async { + var uri = Uri.parse('$_baseUrl/api/v1/admin/users'); + if (query != null && query.isNotEmpty) { + uri = uri.replace(queryParameters: {'text': query}); + } + + final response = await http.get( + uri, + headers: { + 'Content-Type': 'application/json', + 'X-Admin-Password': adminPassword, + }, + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return data['users'] ?? []; + } else { + throw Exception('Failed to list users: ${response.body}'); + } + } + + static Future deleteUser(String adminPassword, String loginId) async { + final encodedId = Uri.encodeComponent(loginId); + final url = Uri.parse('$_baseUrl/api/v1/admin/users/$encodedId'); + + final response = await http.delete( + url, + headers: { + 'Content-Type': 'application/json', + 'X-Admin-Password': adminPassword, + }, + ); + + if (response.statusCode != 200) { + throw Exception('Failed to delete user: ${response.body}'); + } + } + + static Future updateUserStatus(String adminPassword, String loginId, String status) async { + final encodedId = Uri.encodeComponent(loginId); + final url = Uri.parse('$_baseUrl/api/v1/admin/users/$encodedId/status'); + + final response = await http.patch( + url, + headers: { + 'Content-Type': 'application/json', + 'X-Admin-Password': adminPassword, + }, + body: jsonEncode({'status': status}), + ); + + if (response.statusCode != 200) { + throw Exception('Failed to update status: ${response.body}'); + } + } + + static Future updateUserDetails({ + required String adminPassword, + required String loginId, + String? email, + String? phone, + String? displayName, + }) async { + final encodedId = Uri.encodeComponent(loginId); + final url = Uri.parse('$_baseUrl/api/v1/admin/users/$encodedId'); + + final body = {}; + if (email != null) body['email'] = email; + if (phone != null) body['phone'] = phone; + if (displayName != null) body['displayName'] = displayName; + + final response = await http.patch( + url, + headers: { + 'Content-Type': 'application/json', + 'X-Admin-Password': adminPassword, + }, + body: jsonEncode(body), + ); + + if (response.statusCode != 200) { + throw Exception('Failed to update user: ${response.body}'); + } + } + static Future sendLog(String level, String message, {Map? data}) async { final url = Uri.parse('$_baseUrl/api/v1/client-log'); try { diff --git a/frontend/lib/features/admin/presentation/user_management_screen.dart b/frontend/lib/features/admin/presentation/user_management_screen.dart new file mode 100644 index 00000000..07e8ed4b --- /dev/null +++ b/frontend/lib/features/admin/presentation/user_management_screen.dart @@ -0,0 +1,426 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'dart:async'; +import '../../../../core/services/auth_proxy_service.dart'; + +class UserManagementScreen extends StatefulWidget { + const UserManagementScreen({super.key}); + + @override + State createState() => _UserManagementScreenState(); +} + +class _UserManagementScreenState extends State with SingleTickerProviderStateMixin { + late TabController _tabController; + bool _isAuthorized = false; + String? _verifiedAdminPassword; + bool _isLoading = false; + + // --- List Tab Variables --- + List _users = []; + final TextEditingController _searchController = TextEditingController(); + Timer? _debounce; + + // --- Create Tab Controllers --- + final _formKey = GlobalKey(); + final TextEditingController _createLoginIdController = TextEditingController(); + final TextEditingController _createEmailController = TextEditingController(); + final TextEditingController _createPhoneController = TextEditingController(); + final TextEditingController _createNameController = TextEditingController(); + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + WidgetsBinding.instance.addPostFrameCallback((_) => _verifyAccess()); + } + + @override + void dispose() { + _tabController.dispose(); + _searchController.dispose(); + _debounce?.cancel(); + _createLoginIdController.dispose(); + _createEmailController.dispose(); + _createPhoneController.dispose(); + _createNameController.dispose(); + super.dispose(); + } + + // --- Authentication --- + Future _verifyAccess() async { + final passwordController = TextEditingController(); + + final String? inputPassword = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text("Admin Authentication Required"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("Please enter the admin password."), + const SizedBox(height: 16), + TextField( + controller: passwordController, + obscureText: true, + decoration: const InputDecoration(labelText: "Password", border: OutlineInputBorder()), + autofocus: true, + onSubmitted: (value) => Navigator.pop(context, value), + ), + ], + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(context, null), child: const Text("Cancel")), + FilledButton(onPressed: () => Navigator.pop(context, passwordController.text), child: const Text("Enter")), + ], + ), + ); + + if (inputPassword == null || inputPassword.isEmpty) { + if (mounted) context.go('/dashboard'); + return; + } + + setState(() => _isLoading = true); + final isValid = await AuthProxyService.checkAdminAuth(inputPassword); + setState(() => _isLoading = false); + + if (isValid) { + if (mounted) { + setState(() { + _isAuthorized = true; + _verifiedAdminPassword = inputPassword; + }); + _loadUsers(); + } + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Invalid Password'), backgroundColor: Colors.red)); + context.go('/dashboard'); + } + } + } + + // --- User List Logic --- + Future _loadUsers({String? query}) async { + if (_verifiedAdminPassword == null) return; + setState(() => _isLoading = true); + try { + final users = await AuthProxyService.listUsers(_verifiedAdminPassword!, query: query); + setState(() => _users = users); + } catch (e) { + _showError("Failed to load users: $e"); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + void _onSearchChanged(String query) { + if (_debounce?.isActive ?? false) _debounce!.cancel(); + _debounce = Timer(const Duration(milliseconds: 500), () { + _loadUsers(query: query); + }); + } + + Future _deleteUser(String loginId) async { + if (_verifiedAdminPassword == null) return; + + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Delete User"), + content: Text("Are you sure you want to delete $loginId? This cannot be undone."), + actions: [ + TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("Cancel")), + FilledButton( + style: FilledButton.styleFrom(backgroundColor: Colors.red), + onPressed: () => Navigator.pop(context, true), + child: const Text("Delete") + ), + ], + ), + ); + + if (confirm != true) return; + + setState(() => _isLoading = true); + try { + await AuthProxyService.deleteUser(_verifiedAdminPassword!, loginId); + _showSuccess("User deleted"); + _loadUsers(query: _searchController.text); + } catch (e) { + _showError("Failed to delete: $e"); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + Future _toggleStatus(String loginId, String currentStatus) async { + if (_verifiedAdminPassword == null) return; + final newStatus = (currentStatus == "enabled" || currentStatus == "active") ? "disabled" : "enabled"; + + setState(() => _isLoading = true); + try { + await AuthProxyService.updateUserStatus(_verifiedAdminPassword!, loginId, newStatus); + _showSuccess("User status updated to $newStatus"); + _loadUsers(query: _searchController.text); + } catch (e) { + _showError("Failed to update status: $e"); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + Future _editUser(Map user) async { + if (_verifiedAdminPassword == null) return; + + final loginIDs = (user['loginIds'] as List?) ?? []; + final loginId = loginIDs.isNotEmpty ? loginIDs.first.toString() : ""; + if (loginId.isEmpty) return; + + final nameController = TextEditingController(text: user['name'] ?? user['user']?['name'] ?? ""); + final emailController = TextEditingController(text: user['user']?['email'] ?? ""); + final phoneController = TextEditingController(text: user['user']?['phone'] ?? ""); + + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text("Edit User: $loginId"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField(controller: nameController, decoration: const InputDecoration(labelText: "Name")), + TextField(controller: emailController, decoration: const InputDecoration(labelText: "Email")), + TextField(controller: phoneController, decoration: const InputDecoration(labelText: "Phone")), + ], + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("Cancel")), + FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text("Save")), + ], + ), + ); + + if (confirm != true) return; + + setState(() => _isLoading = true); + try { + await AuthProxyService.updateUserDetails( + adminPassword: _verifiedAdminPassword!, + loginId: loginId, + displayName: nameController.text.trim(), + email: emailController.text.trim(), + phone: phoneController.text.trim(), + ); + _showSuccess("User updated successfully"); + _loadUsers(query: _searchController.text); + } catch (e) { + _showError("Update failed: $e"); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + // --- Create User Logic --- + Future _createUserSubmit() async { + if (!_formKey.currentState!.validate()) return; + if (_verifiedAdminPassword == null) return; + + setState(() => _isLoading = true); + try { + await AuthProxyService.createUser( + loginId: _createLoginIdController.text.trim(), + adminPassword: _verifiedAdminPassword!, + email: _createEmailController.text.trim().isEmpty ? null : _createEmailController.text.trim(), + phone: _createPhoneController.text.trim().isEmpty ? null : _createPhoneController.text.trim(), + displayName: _createNameController.text.trim().isEmpty ? null : _createNameController.text.trim(), + ); + + _showSuccess("User created successfully"); + _formKey.currentState!.reset(); + _createLoginIdController.clear(); + _createEmailController.clear(); + _createPhoneController.clear(); + _createNameController.clear(); + + // Switch to list tab and reload + _tabController.animateTo(0); + _loadUsers(); + + } catch (e) { + _showError("Error: $e"); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + // --- UI Helpers --- + void _showError(String msg) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg), backgroundColor: Colors.red)); + } + + void _showSuccess(String msg) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg), backgroundColor: Colors.green)); + } + + @override + Widget build(BuildContext context) { + if (!_isAuthorized) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + + return Scaffold( + appBar: AppBar( + title: const Text('User Management'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/dashboard'), + ), + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab(icon: Icon(Icons.list), text: "User List"), + Tab(icon: Icon(Icons.person_add), text: "Create User"), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _buildUserListTab(), + _buildCreateUserTab(), + ], + ), + ); + } + + Widget _buildUserListTab() { + return Column( + children: [ + if (_isLoading) const LinearProgressIndicator(), + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + controller: _searchController, + decoration: const InputDecoration( + labelText: "Search Users", + prefixIcon: Icon(Icons.search), + border: OutlineInputBorder(), + hintText: "Email, Phone, or Name", + ), + onChanged: _onSearchChanged, + ), + ), + Expanded( + child: _users.isEmpty + ? const Center(child: Text("No users found.")) + : ListView.separated( + itemCount: _users.length, + separatorBuilder: (_, __) => const Divider(), + itemBuilder: (context, index) { + final user = _users[index]; + final userObj = user['user'] ?? {}; // Descope struct structure might vary + // Based on Descope API, user root might have fields directly or inside 'user' + // Go SDK SearchAll returns UserResponse struct. + + final loginIDs = (user['loginIds'] as List?) ?? []; + final loginId = loginIDs.isNotEmpty ? loginIDs.first.toString() : "Unknown ID"; + final name = user['name'] ?? user['user']?['name'] ?? "No Name"; + final status = user['status'] ?? "unknown"; + final isEnabled = status == "enabled" || status == "active"; + + return ListTile( + leading: CircleAvatar( + backgroundColor: isEnabled ? Colors.green.shade100 : Colors.grey.shade300, + child: Icon(Icons.person, color: isEnabled ? Colors.green : Colors.grey), + ), + title: Text(name), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(loginId, style: const TextStyle(fontWeight: FontWeight.bold)), + Text("Status: $status", style: TextStyle(color: isEnabled ? Colors.green : Colors.red, fontSize: 12)), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit, color: Colors.blue), + tooltip: "Edit User", + onPressed: () => _editUser(user), + ), + IconButton( + icon: Icon(isEnabled ? Icons.block : Icons.check_circle, color: isEnabled ? Colors.orange : Colors.green), + tooltip: isEnabled ? "Disable User" : "Enable User", + onPressed: () => _toggleStatus(loginId, status), + ), + IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + tooltip: "Delete User", + onPressed: () => _deleteUser(loginId), + ), + ], + ), + ); + }, + ), + ), + ], + ); + } + + Widget _buildCreateUserTab() { + return SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 600), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (_isLoading) const LinearProgressIndicator(), + const SizedBox(height: 20), + TextFormField( + controller: _createLoginIdController, + decoration: const InputDecoration( + labelText: "Login ID (Required)", + border: OutlineInputBorder(), + helperText: "Unique identifier (Email or Phone)", + ), + validator: (value) => value == null || value.isEmpty ? 'Please enter Login ID' : null, + ), + const SizedBox(height: 16), + TextFormField( + controller: _createNameController, + decoration: const InputDecoration(labelText: "Display Name", border: OutlineInputBorder(), prefixIcon: Icon(Icons.person)), + ), + const SizedBox(height: 16), + TextFormField( + controller: _createEmailController, + decoration: const InputDecoration(labelText: "Email", border: OutlineInputBorder(), prefixIcon: Icon(Icons.email)), + ), + const SizedBox(height: 16), + TextFormField( + controller: _createPhoneController, + decoration: const InputDecoration(labelText: "Phone Number", border: OutlineInputBorder(), prefixIcon: Icon(Icons.phone), helperText: "010-xxxx-xxxx"), + ), + const SizedBox(height: 32), + FilledButton( + onPressed: _isLoading ? null : _createUserSubmit, + style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)), + child: const Text("Create User"), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 21073a99..89ece320 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -8,7 +8,7 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; import 'features/auth/presentation/login_screen.dart'; import 'features/dashboard/presentation/dashboard_screen.dart'; -import 'features/admin/presentation/create_user_screen.dart'; +import 'features/admin/presentation/user_management_screen.dart'; import 'core/services/auth_proxy_service.dart'; import 'core/services/logger_service.dart'; import 'package:logging/logging.dart'; @@ -90,7 +90,7 @@ final _router = GoRouter( path: '/admin/users', builder: (context, state) { _routerLogger.info("Navigating to /admin/users"); - return const CreateUserScreen(); + return const UserManagementScreen(); }, ), ],