|
@@ -1,546 +0,0 @@
|
|
|
-using Spry;
|
|
|
|
|
-using Inversion;
|
|
|
|
|
-using Astralis;
|
|
|
|
|
-using Invercargill;
|
|
|
|
|
-using Invercargill.DataStructures;
|
|
|
|
|
-using Spry.Authorisation;
|
|
|
|
|
-
|
|
|
|
|
-namespace Spry.Authentication.Components {
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * UserDetailsComponent - Display and edit component for a single user.
|
|
|
|
|
- *
|
|
|
|
|
- * This component provides:
|
|
|
|
|
- * - HTML5 `<details>` element with `<summary>` showing key user info
|
|
|
|
|
- * - Two-column table displaying all user fields when expanded
|
|
|
|
|
- * - View mode with Edit/Delete buttons
|
|
|
|
|
- * - Edit mode with form fields and integrated permission editing
|
|
|
|
|
- * - Permission badges display
|
|
|
|
|
- *
|
|
|
|
|
- * The component uses an `is_editing` state to toggle between view and edit modes.
|
|
|
|
|
- *
|
|
|
|
|
- * HTMX Target: `user-{user_id}` (the details element itself)
|
|
|
|
|
- *
|
|
|
|
|
- * Usage:
|
|
|
|
|
- * var component = factory.create<UserDetailsComponent>();
|
|
|
|
|
- * component.set_user(user);
|
|
|
|
|
- * component.available_permissions = {"user-management", "user.create", ...};
|
|
|
|
|
- *
|
|
|
|
|
- * This component uses the inject<> pattern for dependency injection.
|
|
|
|
|
- */
|
|
|
|
|
- public class UserDetailsComponent : Component {
|
|
|
|
|
-
|
|
|
|
|
- private UserProjection _user;
|
|
|
|
|
- private AuthorisationContext _auth_context = inject<AuthorisationContext>();
|
|
|
|
|
- private UserService _user_service = inject<UserService>();
|
|
|
|
|
- private ComponentFactory _factory = inject<ComponentFactory>();
|
|
|
|
|
- private HttpContext _http_context = inject<HttpContext>();
|
|
|
|
|
-
|
|
|
|
|
- // Editing state
|
|
|
|
|
- private Vector<string> _editing_permissions;
|
|
|
|
|
- private string _editing_username;
|
|
|
|
|
- private string _editing_email;
|
|
|
|
|
- private string _editing_password;
|
|
|
|
|
-
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
- // Configuration Properties
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * List of permissions that the application supports.
|
|
|
|
|
- * Must be set by the application before the component is rendered.
|
|
|
|
|
- * The component does NOT hardcode any permissions.
|
|
|
|
|
- */
|
|
|
|
|
- public Vector<string> available_permissions { get; set; default = new Vector<string>(); }
|
|
|
|
|
-
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
- // State Properties (must be public for template expression access)
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
-
|
|
|
|
|
- public int64 user_id { get; private set; default = 0; }
|
|
|
|
|
- public string username { get; private set; default = ""; }
|
|
|
|
|
- public string email { get; private set; default = ""; }
|
|
|
|
|
- public string created_at { get; private set; default = ""; }
|
|
|
|
|
- public string[] permissions { get; private set; default = {}; }
|
|
|
|
|
- public int permission_count { get; private set; default = 0; }
|
|
|
|
|
- public bool is_editing { get; private set; default = false; }
|
|
|
|
|
- public string? error_message { get; private set; default = null; }
|
|
|
|
|
-
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
- // Public API
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Sets the user to display.
|
|
|
|
|
- *
|
|
|
|
|
- * @param user The user to display
|
|
|
|
|
- */
|
|
|
|
|
- public void set_user(UserProjection user) {
|
|
|
|
|
- _user = user;
|
|
|
|
|
- user_id = user.id;
|
|
|
|
|
- username = user.username;
|
|
|
|
|
- email = user.email;
|
|
|
|
|
- created_at = user.created.format("%Y-%m-%d %H:%M:%S UTC");
|
|
|
|
|
- // Convert ImmutableLot<string> to string[]
|
|
|
|
|
- var perm_list = new List<string>();
|
|
|
|
|
- foreach (var perm in user.permissions) {
|
|
|
|
|
- perm_list.append(perm);
|
|
|
|
|
- }
|
|
|
|
|
- var perm_array = new string[perm_list.length()];
|
|
|
|
|
- int i = 0;
|
|
|
|
|
- foreach (var perm in perm_list) {
|
|
|
|
|
- perm_array[i++] = perm;
|
|
|
|
|
- }
|
|
|
|
|
- permissions = perm_array;
|
|
|
|
|
- permission_count = permissions.length;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Gets the user being displayed.
|
|
|
|
|
- *
|
|
|
|
|
- * @return The user being displayed
|
|
|
|
|
- */
|
|
|
|
|
- public UserProjection get_user() {
|
|
|
|
|
- return _user;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
- // Component Implementation
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
-
|
|
|
|
|
- public override string markup { get {
|
|
|
|
|
- return """
|
|
|
|
|
- <details sid="user-details"
|
|
|
|
|
- id-expr='"user-" + this.user_id'
|
|
|
|
|
- hx-swap="outerHTML">
|
|
|
|
|
- <!-- Summary: Always visible, shows key info -->
|
|
|
|
|
- <summary style="display: flex; align-items: center; gap: 1rem; padding: 0.75rem; background: #f8f9fa; border-radius: 4px; cursor: pointer; list-style: none;">
|
|
|
|
|
- <span content-expr="this.username" style="font-weight: 500; min-width: 120px;"></span>
|
|
|
|
|
- <span spry-if="this.is_editing" style="color: #856404; font-style: italic;">Editing...</span>
|
|
|
|
|
- <span spry-if="!this.is_editing" content-expr="this.email" style="color: #6c757d;"></span>
|
|
|
|
|
- <span spry-if="!this.is_editing && this.permission_count > 0" style="margin-left: auto; font-size: 0.85rem; color: #495057;">
|
|
|
|
|
- <span content-expr="this.permission_count"></span> permission(s)
|
|
|
|
|
- </span>
|
|
|
|
|
- <span spry-if="!this.is_editing && this.permission_count == 0" style="margin-left: auto; font-size: 0.85rem; color: #999;">
|
|
|
|
|
- No permissions
|
|
|
|
|
- </span>
|
|
|
|
|
- </summary>
|
|
|
|
|
-
|
|
|
|
|
- <!-- VIEW MODE -->
|
|
|
|
|
- <div spry-if="!this.is_editing" style="padding: 1rem; border: 1px solid #e9ecef; border-top: none; border-radius: 0 0 4px 4px;">
|
|
|
|
|
- <table style="width: 100%; border-collapse: collapse;">
|
|
|
|
|
- <tbody>
|
|
|
|
|
- <tr>
|
|
|
|
|
- <td style="padding: 0.5rem 0; font-weight: 500; width: 150px; vertical-align: top;">User ID</td>
|
|
|
|
|
- <td style="padding: 0.5rem 0;"><code content-expr="stringify(this.user_id)" style="font-size: 0.85rem; background: #f5f5f5; padding: 0.125rem 0.25rem; border-radius: 3px;"></code></td>
|
|
|
|
|
- </tr>
|
|
|
|
|
- <tr>
|
|
|
|
|
- <td style="padding: 0.5rem 0; font-weight: 500;">Username</td>
|
|
|
|
|
- <td style="padding: 0.5rem 0;"><span content-expr="this.username"></span></td>
|
|
|
|
|
- </tr>
|
|
|
|
|
- <tr>
|
|
|
|
|
- <td style="padding: 0.5rem 0; font-weight: 500;">Email</td>
|
|
|
|
|
- <td style="padding: 0.5rem 0;"><span content-expr="this.email"></span></td>
|
|
|
|
|
- </tr>
|
|
|
|
|
- <tr>
|
|
|
|
|
- <td style="padding: 0.5rem 0; font-weight: 500;">Created</td>
|
|
|
|
|
- <td style="padding: 0.5rem 0;"><span content-expr="this.created_at"></span></td>
|
|
|
|
|
- </tr>
|
|
|
|
|
- <tr>
|
|
|
|
|
- <td style="padding: 0.5rem 0; font-weight: 500; vertical-align: top;">Permissions</td>
|
|
|
|
|
- <td style="padding: 0.5rem 0;">
|
|
|
|
|
- <div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
|
|
|
|
|
- <spry-outlet sid="permission-badges"/>
|
|
|
|
|
- </div>
|
|
|
|
|
- </td>
|
|
|
|
|
- </tr>
|
|
|
|
|
- </tbody>
|
|
|
|
|
- </table>
|
|
|
|
|
-
|
|
|
|
|
- <!-- View Mode Actions -->
|
|
|
|
|
- <div style="display: flex; gap: 0.5rem; margin-top: 1rem;">
|
|
|
|
|
- <button sid="edit-btn"
|
|
|
|
|
- spry-action=":StartEdit"
|
|
|
|
|
- spry-target="user-details"
|
|
|
|
|
- style="padding: 0.25rem 0.75rem; cursor: pointer; font-size: 0.875rem;">
|
|
|
|
|
- Edit
|
|
|
|
|
- </button>
|
|
|
|
|
- <button sid="delete-btn"
|
|
|
|
|
- spry-action=":DeleteUser"
|
|
|
|
|
- hx-confirm="Are you sure you want to delete this user?"
|
|
|
|
|
- style="padding: 0.25rem 0.75rem; cursor: pointer; font-size: 0.875rem; background: #dc3545; color: white; border: none; border-radius: 4px;">
|
|
|
|
|
- Delete
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- EDIT MODE -->
|
|
|
|
|
- <div spry-if="this.is_editing" style="padding: 1rem; border: 2px solid #ffc107; border-top: none; border-radius: 0 0 4px 4px; background: #fffdf5;">
|
|
|
|
|
- <form sid="edit-form"
|
|
|
|
|
- spry-action=":SaveEdit"
|
|
|
|
|
- spry-target="user-details">
|
|
|
|
|
-
|
|
|
|
|
- <table style="width: 100%; border-collapse: collapse;">
|
|
|
|
|
- <tbody>
|
|
|
|
|
- <tr>
|
|
|
|
|
- <td style="padding: 0.5rem 0; font-weight: 500; width: 150px;">User ID</td>
|
|
|
|
|
- <td style="padding: 0.5rem 0;"><code content-expr="this.user_id.to_string()" style="font-size: 0.85rem; background: #f5f5f5; padding: 0.125rem 0.25rem; border-radius: 3px;"></code></td>
|
|
|
|
|
- </tr>
|
|
|
|
|
- <tr>
|
|
|
|
|
- <td style="padding: 0.5rem 0; font-weight: 500;">Username *</td>
|
|
|
|
|
- <td style="padding: 0.5rem 0;">
|
|
|
|
|
- <input type="text"
|
|
|
|
|
- name="username"
|
|
|
|
|
- sid="username-input"
|
|
|
|
|
- required
|
|
|
|
|
- minlength="3"
|
|
|
|
|
- pattern="[a-zA-Z0-9_]+"
|
|
|
|
|
- style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; box-sizing: border-box;"
|
|
|
|
|
- value-expr="this.username"/>
|
|
|
|
|
- </td>
|
|
|
|
|
- </tr>
|
|
|
|
|
- <tr>
|
|
|
|
|
- <td style="padding: 0.5rem 0; font-weight: 500;">Email *</td>
|
|
|
|
|
- <td style="padding: 0.5rem 0;">
|
|
|
|
|
- <input type="email"
|
|
|
|
|
- name="email"
|
|
|
|
|
- sid="email-input"
|
|
|
|
|
- required
|
|
|
|
|
- style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; box-sizing: border-box;"
|
|
|
|
|
- value-expr="this.email"/>
|
|
|
|
|
- </td>
|
|
|
|
|
- </tr>
|
|
|
|
|
- <tr>
|
|
|
|
|
- <td style="padding: 0.5rem 0; font-weight: 500;">New Password</td>
|
|
|
|
|
- <td style="padding: 0.5rem 0;">
|
|
|
|
|
- <input type="password"
|
|
|
|
|
- name="new_password"
|
|
|
|
|
- sid="password-input"
|
|
|
|
|
- minlength="8"
|
|
|
|
|
- placeholder="Leave blank to keep current"
|
|
|
|
|
- style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; box-sizing: border-box;"/>
|
|
|
|
|
- <small style="color: #6c757d;">Minimum 8 characters if changing</small>
|
|
|
|
|
- </td>
|
|
|
|
|
- </tr>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Permission Checkboxes (dynamically generated from available_permissions) -->
|
|
|
|
|
- <tr>
|
|
|
|
|
- <td style="padding: 0.5rem 0; font-weight: 500; vertical-align: top;">Permissions</td>
|
|
|
|
|
- <td style="padding: 0.5rem 0;">
|
|
|
|
|
- <div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
|
|
|
|
- <spry-outlet sid="permission-checkboxes"/>
|
|
|
|
|
- </div>
|
|
|
|
|
- </td>
|
|
|
|
|
- </tr>
|
|
|
|
|
- </tbody>
|
|
|
|
|
- </table>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Error Message -->
|
|
|
|
|
- <div spry-if="this.error_message != null"
|
|
|
|
|
- style="padding: 0.5rem; margin: 0.5rem 0; background: #f8d7da; color: #721c24; border-radius: 4px;">
|
|
|
|
|
- <span content-expr="this.error_message"></span>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Edit Actions -->
|
|
|
|
|
- <div style="display: flex; gap: 0.5rem; margin-top: 1rem;">
|
|
|
|
|
- <button type="submit" style="padding: 0.5rem 1rem; cursor: pointer;">Save Changes</button>
|
|
|
|
|
- <button type="button"
|
|
|
|
|
- sid="cancel-btn"
|
|
|
|
|
- spry-action=":CancelEdit"
|
|
|
|
|
- spry-target="user-details"
|
|
|
|
|
- style="padding: 0.5rem 1rem; cursor: pointer; background: #6c757d; color: white; border: none; border-radius: 4px;">
|
|
|
|
|
- Cancel
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </form>
|
|
|
|
|
- </div>
|
|
|
|
|
- </details>
|
|
|
|
|
- """;
|
|
|
|
|
- }}
|
|
|
|
|
-
|
|
|
|
|
- public async override void prepare() throws Error {
|
|
|
|
|
- if (_user == null) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (is_editing) {
|
|
|
|
|
- yield prepare_edit_mode();
|
|
|
|
|
- } else {
|
|
|
|
|
- yield prepare_view_mode();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private async void prepare_view_mode() throws Error {
|
|
|
|
|
- // Create permission badges
|
|
|
|
|
- var badges = new Series<Renderable>();
|
|
|
|
|
- foreach (var permission in permissions) {
|
|
|
|
|
- var badge = create_permission_badge(permission);
|
|
|
|
|
- badges.add(badge);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Show "No permissions" message if none
|
|
|
|
|
- if (permissions.length == 0) {
|
|
|
|
|
- var no_perms = create_text_renderable("No permissions assigned");
|
|
|
|
|
- badges.add(no_perms);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- set_outlet_children("permission-badges", badges);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private async void prepare_edit_mode() throws Error {
|
|
|
|
|
- // Initialize editing state if not already done
|
|
|
|
|
- if (_editing_permissions == null) {
|
|
|
|
|
- _editing_permissions = new Vector<string>();
|
|
|
|
|
- foreach (var perm in permissions) {
|
|
|
|
|
- _editing_permissions.add(perm);
|
|
|
|
|
- }
|
|
|
|
|
- _editing_username = username;
|
|
|
|
|
- _editing_email = email;
|
|
|
|
|
- _editing_password = "";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Generate permission checkboxes from available_permissions
|
|
|
|
|
- var checkboxes = new Series<Renderable>();
|
|
|
|
|
- foreach (var perm in available_permissions) {
|
|
|
|
|
- bool is_checked = has_editing_permission(perm);
|
|
|
|
|
- var checkbox = create_permission_checkbox(perm, is_checked);
|
|
|
|
|
- checkboxes.add(checkbox);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (available_permissions.length == 0) {
|
|
|
|
|
- var no_perms = create_text_renderable("No permissions available");
|
|
|
|
|
- checkboxes.add(no_perms);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- set_outlet_children("permission-checkboxes", checkboxes);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- public async override void handle_action(string action) throws Error {
|
|
|
|
|
- switch (action) {
|
|
|
|
|
- case "StartEdit":
|
|
|
|
|
- yield handle_start_edit_async();
|
|
|
|
|
- break;
|
|
|
|
|
-
|
|
|
|
|
- case "SaveEdit":
|
|
|
|
|
- yield handle_save_edit_async();
|
|
|
|
|
- break;
|
|
|
|
|
-
|
|
|
|
|
- case "CancelEdit":
|
|
|
|
|
- handle_cancel_edit();
|
|
|
|
|
- break;
|
|
|
|
|
-
|
|
|
|
|
- case "DeleteUser":
|
|
|
|
|
- yield handle_delete_user_async();
|
|
|
|
|
- break;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
- // Action Handlers
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
-
|
|
|
|
|
- private async void handle_start_edit_async() throws Error {
|
|
|
|
|
- is_editing = true;
|
|
|
|
|
- error_message = null;
|
|
|
|
|
- _editing_permissions = null; // Reset to trigger re-initialization
|
|
|
|
|
- yield prepare();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private async void handle_save_edit_async() throws Error {
|
|
|
|
|
- var query = _http_context.request.query_params;
|
|
|
|
|
-
|
|
|
|
|
- string new_username = query.get_any_or_default("username") ?? _editing_username;
|
|
|
|
|
- string new_email = query.get_any_or_default("email") ?? _editing_email;
|
|
|
|
|
- string new_password = query.get_any_or_default("new_password") ?? "";
|
|
|
|
|
-
|
|
|
|
|
- // Validate
|
|
|
|
|
- if (new_username.length < 3) {
|
|
|
|
|
- error_message = "Username must be at least 3 characters";
|
|
|
|
|
- yield prepare();
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!new_email.contains("@")) {
|
|
|
|
|
- error_message = "Invalid email address";
|
|
|
|
|
- yield prepare();
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (new_password.length > 0 && new_password.length < 8) {
|
|
|
|
|
- error_message = "Password must be at least 8 characters";
|
|
|
|
|
- yield prepare();
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Collect permissions from checkboxes
|
|
|
|
|
- _editing_permissions = new Vector<string>();
|
|
|
|
|
- foreach (var perm in available_permissions) {
|
|
|
|
|
- string field_name = get_permission_field_name(perm);
|
|
|
|
|
- var field_value = query.get_any_or_default(field_name);
|
|
|
|
|
- if (field_value != null) {
|
|
|
|
|
- _editing_permissions.add(perm);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- // Update user using alter_user
|
|
|
|
|
- yield _user_service.alter_user(
|
|
|
|
|
- _user.id,
|
|
|
|
|
- new_username,
|
|
|
|
|
- new_email,
|
|
|
|
|
- _user.forename ?? "",
|
|
|
|
|
- _user.surname ?? "",
|
|
|
|
|
- _user.date_of_birth,
|
|
|
|
|
- _user.enabled
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- // Update password if provided
|
|
|
|
|
- if (new_password.length > 0) {
|
|
|
|
|
- yield _user_service.set_password(_user.id, new_password);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Update permissions - clear all and re-add
|
|
|
|
|
- yield _user_service.clear_user_permissions(_user.id);
|
|
|
|
|
- foreach (var perm in _editing_permissions) {
|
|
|
|
|
- yield _user_service.set_user_permission(_user.id, perm);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Fetch updated user projection to refresh local state
|
|
|
|
|
- var updated_permissions = yield _user_service.get_user_permissions(_user.id);
|
|
|
|
|
- // Update local state with new values
|
|
|
|
|
- username = new_username;
|
|
|
|
|
- email = new_email;
|
|
|
|
|
- var perm_list = new List<string>();
|
|
|
|
|
- foreach (var perm in updated_permissions) {
|
|
|
|
|
- perm_list.append(perm);
|
|
|
|
|
- }
|
|
|
|
|
- var perm_array = new string[perm_list.length()];
|
|
|
|
|
- int i = 0;
|
|
|
|
|
- foreach (var perm in perm_list) {
|
|
|
|
|
- perm_array[i++] = perm;
|
|
|
|
|
- }
|
|
|
|
|
- permissions = perm_array;
|
|
|
|
|
- permission_count = permissions.length;
|
|
|
|
|
-
|
|
|
|
|
- is_editing = false;
|
|
|
|
|
- error_message = null;
|
|
|
|
|
- _editing_permissions = null;
|
|
|
|
|
-
|
|
|
|
|
- yield prepare();
|
|
|
|
|
- } catch (Error e) {
|
|
|
|
|
- error_message = @"Failed to save: $(e.message)";
|
|
|
|
|
- yield prepare();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private void handle_cancel_edit() {
|
|
|
|
|
- is_editing = false;
|
|
|
|
|
- error_message = null;
|
|
|
|
|
- _editing_permissions = null;
|
|
|
|
|
- _editing_password = "";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private async void handle_delete_user_async() throws Error {
|
|
|
|
|
- // Check authorization using AuthorisationContext
|
|
|
|
|
- if (_auth_context.is_anonymous()) {
|
|
|
|
|
- error_message = "You must be logged in to delete users";
|
|
|
|
|
- yield prepare();
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!_auth_context.has_permission("user-management")) {
|
|
|
|
|
- error_message = "You do not have permission to delete users";
|
|
|
|
|
- yield prepare();
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Prevent self-deletion - compare int64 user ids
|
|
|
|
|
- int64? current_user_id = _auth_context.token.user_identifier.as_int64_or_null();
|
|
|
|
|
- if (current_user_id != null && current_user_id == user_id) {
|
|
|
|
|
- error_message = "Cannot delete your own account";
|
|
|
|
|
- yield prepare();
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Delete the user
|
|
|
|
|
- try {
|
|
|
|
|
- yield _user_service.delete_user(user_id);
|
|
|
|
|
-
|
|
|
|
|
- // Set hx-refresh header to cause page reload
|
|
|
|
|
- // This ensures the user list is refreshed without needing parent references
|
|
|
|
|
- set_refresh_response();
|
|
|
|
|
-
|
|
|
|
|
- } catch (Error e) {
|
|
|
|
|
- error_message = e.message;
|
|
|
|
|
- yield prepare();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
- // Private Helpers
|
|
|
|
|
- // =========================================================================
|
|
|
|
|
-
|
|
|
|
|
- private bool has_editing_permission(string permission) {
|
|
|
|
|
- if (_editing_permissions == null) {
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
- foreach (var perm in _editing_permissions) {
|
|
|
|
|
- if (perm == permission) {
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private string get_permission_field_name(string permission) {
|
|
|
|
|
- return @"perm_$(permission.replace(".", "-"))";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private Renderable create_permission_badge(string permission) {
|
|
|
|
|
- var doc = new MarkupDocument();
|
|
|
|
|
- var escaped = GLib.Markup.escape_text(permission);
|
|
|
|
|
- doc.body.inner_html = @"<span style=\"background: #e9ecef; color: #495057; padding: 0.125rem 0.5rem; border-radius: 12px; font-size: 0.8rem; white-space: nowrap;\">$escaped</span>";
|
|
|
|
|
- return new InlineRenderable(doc);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private Renderable create_text_renderable(string text) {
|
|
|
|
|
- var doc = new MarkupDocument();
|
|
|
|
|
- var escaped = GLib.Markup.escape_text(text);
|
|
|
|
|
- doc.body.inner_html = @"<span style=\"color: #999; font-style: italic; font-size: 0.85rem;\">$escaped</span>";
|
|
|
|
|
- return new InlineRenderable(doc);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private Renderable create_permission_checkbox(string permission, bool is_checked) {
|
|
|
|
|
- var doc = new MarkupDocument();
|
|
|
|
|
- var escaped = GLib.Markup.escape_text(permission);
|
|
|
|
|
- var field_name = get_permission_field_name(permission);
|
|
|
|
|
- string checked_attr = is_checked ? "checked" : "";
|
|
|
|
|
- doc.body.inner_html = @"<label style=\"display: flex; align-items: center; gap: 0.25rem; font-size: 0.875rem; cursor: pointer;\"><input type=\"checkbox\" name=\"$field_name\" value=\"$permission\" $checked_attr/>$escaped</label>";
|
|
|
|
|
- return new InlineRenderable(doc);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Sets the HX-Refresh header on the response to trigger a full page refresh.
|
|
|
|
|
- * This is used instead of parent component references to update the user list.
|
|
|
|
|
- */
|
|
|
|
|
- private void set_refresh_response() {
|
|
|
|
|
- response.set_header("HX-Refresh", "true");
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Helper class for inline HTML rendering.
|
|
|
|
|
- * Used for dynamically generated permission badges and checkboxes.
|
|
|
|
|
- */
|
|
|
|
|
- internal class InlineRenderable : GLib.Object, Renderable {
|
|
|
|
|
- private MarkupDocument _doc;
|
|
|
|
|
-
|
|
|
|
|
- public InlineRenderable(MarkupDocument doc) {
|
|
|
|
|
- _doc = doc;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- public async MarkupDocument to_document() throws Error {
|
|
|
|
|
- return _doc;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|