web permissions2

This commit is contained in:
Priec
2026-08-11 14:33:24 +02:00
parent 5602140d05
commit 077d69d756
47 changed files with 2348 additions and 674 deletions

View File

@@ -17,7 +17,6 @@
{% if page.can_manage_scripts %}<a href="/admin/logic/new">Add logic</a>{% endif %}
{% if page.can_manage_validations %}<a href="/admin/validation/new">Add validation</a>{% endif %}
{% if page.can_manage_validations %}<a href="/admin/validation/sets/new">Add rule</a>{% endif %}
{% if page.can_manage_permissions %}<a href="/admin/permissions">Permissions</a>{% endif %}
{% if page.can_export %}<a href="/admin/export">Export</a>{% endif %}
</div>
</section>

View File

@@ -1,107 +0,0 @@
{% extends "ui/base.html" %}
{% block title %}Roles and permissions{% endblock %}
{% block content %}
<main>
<section class="heading">
<div>
<p class="eyebrow">Authorization</p>
<h1>Roles and permissions</h1>
<p>Manage data roles, their direct grants, inheritance, and user assignments.</p>
</div>
<div class="actions"><a href="/admin">← Admin panel</a></div>
</section>
{% if page.updated %}<p class="notice">Administration updated. The current session remains valid and all subsequent requests use the new state.</p>{% endif %}
{% if page.can_manage_roles %}
<section class="panel">
<h2>Data roles</h2>
<form method="get" action="/admin/permissions" class="form-grid">
<label>Role
<select name="role" onchange="this.form.submit()">
{% for role in page.editable_roles() %}
<option value="{{ role.name }}" {% if page.selected(role.name.as_str()) %}selected{% endif %}>{{ role.name }}{% if !role.parent.is_empty() %} → {{ role.parent }}{% endif %}</option>
{% endfor %}
</select>
</label>
</form>
<form hx-post="/admin/permissions/roles" hx-target="#permission-status" class="form-grid">
<label>New role<input name="name" required maxlength="50" placeholder="sales"></label>
<label>Inherits from
<select name="parent"><option value="">No parent</option>{% for role in page.editable_roles() %}<option value="{{ role.name }}">{{ role.name }}</option>{% endfor %}</select>
</label>
<button type="submit">Create role</button>
</form>
{% if page.selected_role_is_removable() %}
<form hx-post="/admin/permissions/roles/remove" hx-target="#permission-status">
<input type="hidden" name="role" value="{{ page.selected_role }}">
<button type="submit" class="danger">Remove {{ page.selected_role }}</button>
</form>
{% endif %}
</section>
<section class="panel">
<h2>Grants for {{ page.selected_role }}</h2>
<p class="hint">Direct grants can be revoked here. “Inherited” permissions come from the roles parent.</p>
<div class="table-scroll">
<table class="builder-table">
<thead><tr><th>Object</th><th>Scope</th><th>Actions</th></tr></thead>
<tbody>
{% for object in page.grantable_objects %}
<tr>
<td><code>{{ object.object }}</code></td>
<td>{{ object.kind }}{% if !object.profile.is_empty() %} · {{ object.profile }}{% endif %}{% if !object.table.is_empty() %} / {{ object.table }}{% endif %}</td>
<td>
<div class="actions">
{% for action in object.allowed_actions %}
{% if page.direct(object.object.as_str(), action.as_str()) %}
<form hx-post="/admin/permissions/revoke" hx-target="#permission-status">
<input type="hidden" name="role" value="{{ page.selected_role }}"><input type="hidden" name="object" value="{{ object.object }}"><input type="hidden" name="action" value="{{ action }}">
<button type="submit" class="danger">{{ action }} ✓</button>
</form>
{% else if page.effective(object.object.as_str(), action.as_str()) %}
<span class="tag">{{ action }} · inherited</span>
{% else %}
<form hx-post="/admin/permissions/grant" hx-target="#permission-status">
<input type="hidden" name="role" value="{{ page.selected_role }}"><input type="hidden" name="object" value="{{ object.object }}"><input type="hidden" name="action" value="{{ action }}">
<button type="submit" class="secondary">Grant {{ action }}</button>
</form>
{% endif %}
{% endfor %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
{% endif %}
{% if page.can_manage_users %}
<section class="panel">
<h2>Users</h2>
<table class="builder-table">
<thead><tr><th>Username</th><th>Email</th><th>Current role</th><th>Assign role</th><th>Reset password</th></tr></thead>
<tbody>{% for user in page.users %}<tr>
<td>{{ user.username }}</td><td>{{ user.email }}</td><td>{{ user.role }}</td>
<td><form hx-post="/admin/permissions/users/role" hx-target="#permission-status" class="actions">
<input type="hidden" name="username" value="{{ user.username }}">
<select name="role">{% for role in page.assignable_roles() %}<option value="{{ role.name }}" {% if user.role == role.name %}selected{% endif %}>{{ role.name }}</option>{% endfor %}</select>
<button type="submit">Assign</button>
</form></td>
<td>{% if page.can_reset_password(user) %}<form hx-post="/admin/permissions/users/password" hx-target="#permission-status" class="actions">
<input type="hidden" name="username" value="{{ user.username }}">
<input name="new_password" type="password" autocomplete="new-password" placeholder="New password" required>
<input name="new_password_confirmation" type="password" autocomplete="new-password" placeholder="Confirm password" required>
<button type="submit">Reset</button>
</form>{% endif %}</td>
</tr>{% endfor %}</tbody>
</table>
</section>
{% endif %}
<div id="permission-status" aria-live="polite"></div>
</main>
{% endblock %}

View File

@@ -154,22 +154,26 @@
{% if !page.permission_object.is_empty() %}
<section class="panel">
<h2>Data permissions for <code>{{ page.selection.table }}</code></h2>
<p class="hint">These grants cover this table family and take effect on subsequent requests without replacing the current session.</p>
<p class="hint">
These grants cover this table family and take effect on subsequent requests without replacing the
current session. Wider access — a whole profile, or every table at once — is on the
<a href="/permissions/grants">Permissions page</a>.
</p>
<table class="builder-table">
<thead><tr><th>Role</th><th>Actions</th></tr></thead>
<tbody>{% for role in page.role_permissions %}<tr>
<td>{{ role.role }}</td>
<td><div class="actions">{% for permission in role.actions %}
{% if permission.direct %}
<form hx-post="/admin/permissions/revoke" hx-target="#permission-status">
<input type="hidden" name="role" value="{{ role.role }}"><input type="hidden" name="object" value="{{ page.permission_object }}"><input type="hidden" name="action" value="{{ permission.action }}">
<form hx-post="/permissions/grants/apply" hx-target="#permission-status">
<input type="hidden" name="role" value="{{ role.role }}"><input type="hidden" name="mode" value="revoke"><input type="hidden" name="pair" value="{{ page.permission_object }}|{{ permission.action }}"><input type="hidden" name="return_to" value="/admin/table-definition{{ page.selection.query() }}">
<button type="submit" class="danger">{{ permission.action }} ✓</button>
</form>
{% else if permission.effective %}
<span class="tag">{{ permission.action }} · inherited</span>
{% else %}
<form hx-post="/admin/permissions/grant" hx-target="#permission-status">
<input type="hidden" name="role" value="{{ role.role }}"><input type="hidden" name="object" value="{{ page.permission_object }}"><input type="hidden" name="action" value="{{ permission.action }}">
<form hx-post="/permissions/grants/apply" hx-target="#permission-status">
<input type="hidden" name="role" value="{{ role.role }}"><input type="hidden" name="mode" value="grant"><input type="hidden" name="pair" value="{{ page.permission_object }}|{{ permission.action }}"><input type="hidden" name="return_to" value="/admin/table-definition{{ page.selection.query() }}">
<button type="submit" class="secondary">Grant {{ permission.action }}</button>
</form>
{% endif %}