| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490 |
- 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"));
- */
- 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
- // =========================================================================
- private string? _success_message = null;
- private string? _error_message = null;
- private bool _access_denied = 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: 4rem 2rem;
- }
- .access-denied h2 { color: #dc3545; }
- /* Modal */
- .modal-overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0,0,0,0.5);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
- }
- .modal-content {
- background: white;
- padding: 2rem;
- border-radius: 8px;
- max-width: 500px;
- width: 90%;
- max-height: 90vh;
- overflow-y: auto;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .modal-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1.5rem;
- padding-bottom: 1rem;
- border-bottom: 1px solid #e0e0e0;
- }
- .modal-header h3 { margin: 0; }
- .close-btn {
- background: none;
- border: none;
- font-size: 1.5rem;
- cursor: pointer;
- color: #666;
- }
- .close-btn:hover { color: #333; }
- /* Form */
- .form-group { margin-bottom: 1rem; }
- .form-group label {
- display: block;
- margin-bottom: 0.25rem;
- font-weight: 500;
- }
- .form-input {
- width: 100%;
- padding: 0.5rem;
- border: 1px solid #ccc;
- border-radius: 4px;
- font-size: 1rem;
- }
- .form-input:focus { outline: none; border-color: #007bff; }
- .form-hint { font-size: 0.75rem; color: #666; margin-top: 0.25rem; }
- .form-actions {
- display: flex;
- gap: 0.5rem;
- margin-top: 1.5rem;
- padding-top: 1rem;
- border-top: 1px solid #e0e0e0;
- }
- /* Badges */
- .badge {
- display: inline-block;
- padding: 0.25rem 0.5rem;
- border-radius: 4px;
- font-size: 0.75rem;
- margin-right: 0.25rem;
- margin-bottom: 0.25rem;
- }
- .badge-active { background: #d4edda; color: #155724; }
- .badge-inactive { background: #f8d7da; color: #721c24; }
- .badge-permission { background: #e9ecef; color: #495057; }
- /* Table */
- .table-container { overflow-x: auto; }
- .user-table {
- width: 100%;
- border-collapse: collapse;
- background: white;
- border-radius: 4px;
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
- }
- .user-table th,
- .user-table td {
- padding: 0.75rem;
- text-align: left;
- border-bottom: 1px solid #e0e0e0;
- }
- .user-table th {
- background: #f8f9fa;
- font-weight: 600;
- }
- .user-table tr:hover { background: #f8f9fa; }
- /* Search */
- .search-bar { margin-bottom: 1rem; }
- .search-form { display: flex; gap: 0.5rem; }
- .search-input {
- flex: 1;
- padding: 0.5rem;
- border: 1px solid #ccc;
- border-radius: 4px;
- }
- .btn-search { background: #28a745; color: white; }
- .btn-clear { background: #6c757d; color: white; }
- /* Pagination */
- .pagination {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 1rem;
- margin-top: 1rem;
- }
- .page-info { color: #666; }
- /* Empty State */
- .empty-state {
- text-align: center;
- padding: 3rem;
- background: white;
- border-radius: 4px;
- }
- .user-count {
- text-align: center;
- color: #666;
- margin-top: 1rem;
- font-size: 0.875rem;
- }
- /* Permission Editor */
- .permission-checkbox {
- display: inline-flex;
- align-items: center;
- margin-right: 1rem;
- margin-bottom: 0.5rem;
- }
- .permission-checkbox input { margin-right: 0.5rem; }
- .permission-tag {
- display: inline-flex;
- align-items: center;
- background: #e9ecef;
- padding: 0.25rem 0.5rem;
- border-radius: 4px;
- margin: 0.25rem;
- }
- .permission-tag .remove-tag {
- background: none;
- border: none;
- margin-left: 0.5rem;
- cursor: pointer;
- color: #666;
- font-size: 1rem;
- line-height: 1;
- }
- .permission-tag .remove-tag:hover { color: #dc3545; }
- .permission-input {
- padding: 0.5rem;
- border: 1px solid #ccc;
- border-radius: 4px;
- }
- .btn-add { background: #28a745; color: white; }
- .help-text { color: #666; font-size: 0.75rem; margin-top: 0.5rem; display: block; }
- .permission-group { margin-bottom: 1rem; }
- .permission-group h4 { margin-bottom: 0.5rem; font-size: 0.875rem; }
- </style>
- </head>
- <body>
- <div class="admin-container" sid="admin-container">
- <!-- Access Denied Message -->
- <div spry-if="this._access_denied" class="access-denied" sid="access-denied">
- <h2>Access Denied</h2>
- <p>You do not have permission to access the user management page.</p>
- <p>Please contact an administrator if you believe this is an error.</p>
- </div>
- <!-- Main Content (shown when authorized) -->
- <div spry-if="!this._access_denied">
- <header class="admin-header">
- <h1>User Management</h1>
- <div class="header-actions">
- <button sid="create-btn"
- spry-action=":CreateUser"
- hx-target="#user-form-container"
- hx-swap="innerHTML"
- class="btn btn-primary">
- + Create User
- </button>
- </div>
- </header>
- <!-- Success Message -->
- <div spry-if="this._success_message != null"
- class="alert alert-success"
- sid="success-alert"
- spry-global="success-alert">
- <span content-expr="this._success_message"></span>
- </div>
- <!-- Error Message -->
- <div spry-if="this._error_message != null"
- class="alert alert-error"
- sid="error-alert"
- spry-global="error-alert">
- <span content-expr="this._error_message"></span>
- </div>
- <!-- User List Component -->
- <spry-component name="UserListComponent" sid="user-list"/>
- <!-- User Form Container (for modal) -->
- <div id="user-form-container" sid="user-form-container">
- <spry-component name="UserFormComponent" sid="user-form"/>
- </div>
- </div>
- </div>
- </body>
- </html>
- """;
- }}
- public override async void prepare() throws Error {
- // Check permission
- var auth_result = session_service.authenticate_request(http_context, user_service);
- if (!auth_result.is_authenticated) {
- _access_denied = true;
- return;
- }
- var current_user = auth_result.user;
- if (current_user == null) {
- _access_denied = true;
- return;
- }
- // Check for user-management permission
- if (!permission_service.has_permission(current_user, PermissionService.USER_MANAGEMENT)) {
- _access_denied = true;
- return;
- }
- _access_denied = false;
- // Ensure form is hidden initially
- var user_form = get_component_child<UserFormComponent>("user-form");
- user_form.hide();
- }
- public async override void handle_action(string action) throws Error {
- // Don't process actions if access is denied
- if (_access_denied) {
- return;
- }
- var query = http_context.request.query_params;
- switch (action) {
- case "CreateUser":
- handle_create_user();
- break;
- case "EditUser":
- var user_id = get_query_value(query, "user_id");
- handle_edit_user(user_id);
- break;
- case "ToggleActive":
- var user_id = get_query_value(query, "user_id");
- yield handle_toggle_active(user_id);
- break;
- case "DeleteUser":
- var user_id = get_query_value(query, "user_id");
- yield handle_delete_user(user_id);
- break;
- }
- }
- // =========================================================================
- // Action Handlers
- // =========================================================================
- private void handle_create_user() throws Error {
- var user_form = get_component_child<UserFormComponent>("user-form");
- user_form.show_create();
- _success_message = null;
- _error_message = null;
- }
- private void handle_edit_user(string user_id) throws Error {
- if (user_id.length == 0) {
- _error_message = "Invalid user ID";
- return;
- }
- var user = user_service.get_user(user_id);
- if (user == null) {
- _error_message = "User not found";
- return;
- }
- var user_form = get_component_child<UserFormComponent>("user-form");
- user_form.set_user(user);
- _success_message = null;
- _error_message = null;
- }
- private async void handle_toggle_active(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");
- add_globals_from(user_list);
- }
- private async void handle_delete_user(string user_id) throws Error {
- if (user_id.length == 0) {
- _error_message = "Invalid user ID";
- return;
- }
- // Prevent self-deletion
- var auth_result = session_service.authenticate_request(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");
- add_globals_from(user_list);
- return;
- }
- }
- // Delete the user
- string? delete_error;
- if (!user_service.delete_user(user_id, out delete_error)) {
- _error_message = delete_error ?? "Failed to delete user";
- _success_message = null;
- } else {
- _success_message = "User deleted successfully";
- _error_message = null;
- }
- // Refresh the list
- var user_list = get_component_child<UserListComponent>("user-list");
- 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() : "";
- }
- }
- }
|