| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- using Spry;
- using Inversion;
- using Astralis;
- using Invercargill;
- using Invercargill.DataStructures;
- namespace Spry.Users.Components {
- /**
- * UserManagementPage - PageComponent orchestrating all user management components.
- *
- * This page provides a complete user management interface that includes:
- * - Permission check: Only accessible with "user-management" permission
- * - User list with search, filter, and pagination
- * - User form modal for create/edit operations
- * - Status messages (success/error alerts)
- *
- * Cross-Component Communication:
- * - Child components trigger actions via "UserManagementPage:ActionName" pattern
- * - The page handles these actions and updates child components accordingly
- * - Uses add_globals_from() to share globals with child components
- *
- * Required Permission: "user-management"
- *
- * Usage:
- * // Register as a page:
- * spry_cfg.add_page<UserManagementPage>(new EndpointRoute("/admin/users"));
- *
- * This component uses the inject<> pattern for dependency injection.
- */
- public class UserManagementPage : PageComponent {
- private PermissionService _permission_service = inject<PermissionService>();
- private UserService _user_service = inject<UserService>();
- private SessionService _session_service = inject<SessionService>();
- private ComponentFactory _factory = inject<ComponentFactory>();
- private HttpContext _http_context = inject<HttpContext>();
- // =========================================================================
- // State Properties (must be public for template expression access)
- // =========================================================================
- public string? success_message { get; private set; default = null; }
- public string? error_message { get; private set; default = null; }
- public bool access_denied { get; private set; default = false; }
- // =========================================================================
- // Component Implementation
- // =========================================================================
- public override string markup { get {
- return """
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8"/>
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
- <title>User Management - Admin</title>
- <script spry-res="htmx.js"></script>
- <style>
- /* Basic admin styles */
- * { box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- margin: 0;
- padding: 0;
- background: #f5f5f5;
- color: #333;
- }
- .admin-container {
- max-width: 1200px;
- margin: 0 auto;
- padding: 2rem;
- }
- .admin-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 2rem;
- padding-bottom: 1rem;
- border-bottom: 1px solid #e0e0e0;
- }
- .admin-header h1 {
- margin: 0;
- font-size: 1.75rem;
- color: #333;
- }
- .btn {
- display: inline-block;
- padding: 0.5rem 1rem;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 0.875rem;
- font-weight: 500;
- text-decoration: none;
- transition: background-color 0.2s;
- }
- .btn-primary { background: #007bff; color: white; }
- .btn-primary:hover { background: #0056b3; }
- .btn-secondary { background: #6c757d; color: white; }
- .btn-secondary:hover { background: #545b62; }
- .btn-edit { background: #17a2b8; color: white; }
- .btn-delete { background: #dc3545; color: white; }
- .btn-sm { padding: 0.25rem 0.5rem; font-size: 0.75rem; }
- .btn:disabled { opacity: 0.5; cursor: not-allowed; }
- /* Alerts */
- .alert {
- padding: 1rem;
- border-radius: 4px;
- margin-bottom: 1rem;
- }
- .alert-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
- .alert-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
- /* Access Denied */
- .access-denied {
- text-align: center;
- padding: 3rem;
- }
- .access-denied h2 { color: #dc3545; }
- </style>
- </head>
- <body>
- <div class="admin-container">
- <div class="admin-header">
- <h1>User Management</h1>
- <button sid="create-btn"
- spry-action=":CreateUser"
- class="btn btn-primary">
- + Create User
- </button>
- </div>
- <!-- Access Denied Message -->
- <div spry-if="this.access_denied" class="access-denied" sid="accessDeniedMsg">
- <h2>Access Denied</h2>
- <p>You do not have permission to access this page.</p>
- </div>
- <!-- Success Message -->
- <div spry-if="this.success_message != null" class="alert alert-success" sid="successAlert">
- <span content-expr="this.success_message"></span>
- </div>
- <!-- Error Message -->
- <div spry-if="this.error_message != null" class="alert alert-error" sid="errorAlert">
- <span content-expr="this.error_message"></span>
- </div>
- <!-- Main Content (hidden if access denied) -->
- <div spry-if="!this.access_denied" sid="mainContent">
- <!-- User List Component -->
- <spry-component name="UserListComponent" sid="user-list"/>
- <!-- User Form Modal -->
- <div id="user-form-container" sid="user-form-container">
- <spry-component name="UserFormComponent" sid="user-form"/>
- </div>
- </div>
- </div>
- </body>
- </html>
- """;
- }}
- public async override void prepare() throws Error {
- // Check permission
- var auth_result = yield _session_service.authenticate_request_async(_http_context, _user_service);
- if (!auth_result.is_authenticated || auth_result.user == null) {
- access_denied = true;
- return;
- }
- var has_permission = yield _permission_service.has_permission_by_id_async(
- auth_result.user.id,
- PermissionService.USER_MANAGEMENT
- );
- if (!has_permission) {
- access_denied = true;
- return;
- }
- // Share globals with child components
- var user_list = get_component_child<UserListComponent>("user-list");
- if (user_list != null) {
- add_globals_from(user_list);
- }
- var user_form = get_component_child<UserFormComponent>("user-form");
- if (user_form != null) {
- add_globals_from(user_form);
- }
- }
- public async override void handle_action(string action) throws Error {
- // Check permission for all actions
- if (access_denied) {
- return;
- }
- var query = _http_context.request.query_params;
- switch (action) {
- case "CreateUser":
- yield handle_create_user_async();
- break;
- case "EditUser":
- var user_id = get_query_value(query, "user_id");
- yield handle_edit_user_async(user_id);
- break;
- case "ToggleActive":
- var user_id = get_query_value(query, "user_id");
- yield handle_toggle_active_async(user_id);
- break;
- case "DeleteUser":
- var user_id = get_query_value(query, "user_id");
- yield handle_delete_user_async(user_id);
- break;
- }
- }
- // =========================================================================
- // Action Handlers
- // =========================================================================
- private async void handle_create_user_async() throws Error {
- var user_form = get_component_child<UserFormComponent>("user-form");
- if (user_form != null) {
- user_form.show_create();
- }
- success_message = null;
- error_message = null;
- }
- private async void handle_edit_user_async(string user_id) throws Error {
- if (user_id.length == 0) {
- error_message = "Invalid user ID";
- return;
- }
- var user = yield _user_service.get_user_async(user_id);
- if (user == null) {
- error_message = "User not found";
- return;
- }
- var user_form = get_component_child<UserFormComponent>("user-form");
- if (user_form != null) {
- user_form.set_user(user);
- }
- success_message = null;
- error_message = null;
- }
- private async void handle_toggle_active_async(string user_id) throws Error {
- // Note: Current User model doesn't have is_active field
- // This is a placeholder for future implementation
- error_message = "Toggle active functionality not yet implemented";
- success_message = null;
- // Refresh the list
- var user_list = get_component_child<UserListComponent>("user-list");
- if (user_list != null) {
- add_globals_from(user_list);
- }
- }
- private async void handle_delete_user_async(string user_id) throws Error {
- if (user_id.length == 0) {
- error_message = "Invalid user ID";
- return;
- }
- // Prevent self-deletion
- var auth_result = yield _session_service.authenticate_request_async(_http_context, _user_service);
- if (auth_result.is_authenticated && auth_result.user != null) {
- if (auth_result.user.id == user_id) {
- error_message = "Cannot delete your own account";
- success_message = null;
- // Refresh the list
- var user_list = get_component_child<UserListComponent>("user-list");
- if (user_list != null) {
- add_globals_from(user_list);
- }
- return;
- }
- }
- // Delete the user
- try {
- yield _user_service.delete_user_async(user_id);
- success_message = "User deleted successfully";
- error_message = null;
- } catch (Error e) {
- error_message = e.message;
- success_message = null;
- }
- // Refresh the list
- var user_list = get_component_child<UserListComponent>("user-list");
- if (user_list != null) {
- add_globals_from(user_list);
- }
- }
- // =========================================================================
- // Private Helpers
- // =========================================================================
- private string get_query_value(Catalogue<string, string> query, string key) {
- var value = query.get_any_or_default(key);
- return value != null ? ((!)value).strip() : "";
- }
- }
- }
|