using Invercargill; using Invercargill.DataStructures; using Spry; using Spry.Authentication; using Statum; namespace ClankerSaddle { /** One row of the users table, with its row actions. */ public class ManagedUserRowView : Object { public int64 id { get; set; } public string username { get; set; default = ""; } public string email { get; set; default = ""; } public string forename { get; set; default = ""; } public string surname { get; set; default = ""; } public string name_display { get; set; default = ""; } public string date_of_birth { get; set; default = ""; } public bool enabled { get; set; } public bool is_admin { get; set; } public bool is_me { get; set; } public string created_display { get; set; default = ""; } public Statum.Model.ActionDto? alter { get; set; } public Statum.Model.ActionDto? set_enabled { get; set; } public Statum.Model.ActionDto? grant_admin { get; set; } public Statum.Model.ActionDto? revoke_admin { get; set; } public Statum.Model.ActionDto? delete { get; set; } } /** Public data of the `users` slot. */ public class UsersPublic : Object { public Statum.Model.ActionDto create { get; set; } } /** * Entrypoint for /users: the account table every row of which * carries its admin role badge, enabled state and row actions (edit * expander, enable/disable, grant/revoke admin, delete). Admin-only * (the authoritative guard; the nav link is hidden for non-admins * but the guard is what keeps the page closed). * * Rows are newest-first: the goal brief's list convention. The * enable/disable action seals the row's NEXT state so the button * never races the table, and the edit expander is prefilled from the * same row (one round trip, no separate fetch). */ public class UsersEntrypoint : StatumEntrypoint { private UserService users_service = Inversion.inject(); public override async DirectiveBuilder handle() throws GLib.Error { var guard = yield SpryAuth.require_permission(directives(), request, users_service, SpryAuth.ADMIN_PERMISSION, "/login"); if (guard != null) { return (!)guard; } var me = SpryAuth.current(request); var rows = new Vector(); foreach (var user in yield users_service.list_users(0, 1000)) { bool is_admin = SpryAuth.permission_matches(user.permissions.to_array(), SpryAuth.ADMIN_PERMISSION); var sealed_row = new ManagedUserPrivate() { user_id = user.id, enabled = !user.enabled }; var view = new ManagedUserRowView() { id = user.id, username = user.username, email = user.email, forename = user.forename, surname = user.surname, name_display = @"$(user.forename) $(user.surname)".strip(), date_of_birth = user.date_of_birth.format("%Y-%m-%d"), enabled = user.enabled, is_admin = is_admin, is_me = me.user_id == user.id, created_display = format_stamp(user.created), alter = action_registry.author_private( new ManagedUserPrivate() { user_id = user.id }), set_enabled = action_registry.author_private(sealed_row), delete = action_registry.author_private( new ManagedUserPrivate() { user_id = user.id }) }; if (is_admin) { view.revoke_admin = action_registry.author_private( new ManagedUserPrivate() { user_id = user.id }); } else { view.grant_admin = action_registry.author_private( new ManagedUserPrivate() { user_id = user.id }); } rows.add(view); } // Newest first. The ids are autoincrement, so id-descending // IS created-descending — including the same-minute ties the // display stamp (minute resolution) cannot distinguish. var sorted = rows.to_array(); for (int i = 1; i < sorted.length; i++) { var row = sorted[i]; int j = i - 1; while (j >= 0 && sorted[j].id < row.id) { sorted[j + 1] = sorted[j]; j--; } sorted[j + 1] = row; } var pub = new UsersPublic(); pub.create = action_registry.author(); var props = (PropertyDictionary) GObjectMapping.to_properties(pub); props["users"] = StateJson.list_array(sorted); var slot = state_service.new_slot(Scope.PAGE, new State() { type_name = "users", public_data = props, private_data = new PropertyDictionary() }); return directives().set(slot); } } }