feat(group): 1st implementation of Groups

this implements first version (manageable only by admin right now)

    routes:

        GET /api/groups
        List subject groups (paginated). Admin-only.

        POST /api/groups
        Create a new ReBAC subject group. Admin-only. The name must match the RFC 5321 local-part shape and be globally unique (case-insensitive).

        GET /api/groups/search
        Search non-virtual groups by name substring. Authenticated only (no admin role required) — backs the share-dialog recipient autocomplete.

        GET /api/groups/{id}
        Fetch a single group's details. Admin-only.

        DELETE /api/groups/{id}
        Delete a group. Cascades to `subject_group_members` (FK) and to `access_grants` rows referencing this group as a subject. Admin-only.

        PATCH /api/groups/{id}
        Update a group's metadata. Admin-only. v1 only persists name renames.

        GET /api/groups/{id}/effective-members
        List every user transitively reached through this group (members of members of members, etc.). Used by admin / audit tooling. Admin-only.

        GET /api/groups/{id}/members
        List the *direct* members of a group (one level only). Admin-only.

        POST /api/groups/{id}/members
        Add a member to a group. Exactly one of `user_id` / `group_id` must be provided. Adding a group-member runs a write-time cycle check and a nesting-depth check (max 8). Admin-only.

        DELETE /api/groups/{id}/members/group/{gid}
        Remove a nested group-member from a group. Admin-only.

        DELETE /api/groups/{id}/members/user/{uid}
        Remove a user-member from a group. Admin-only.

fix hurl

groups

round

groups
This commit is contained in:
Edouard Vanbelle
2026-05-30 23:35:47 +02:00
parent 41356b6490
commit 09985f8a95
54 changed files with 6421 additions and 145 deletions
+15
View File
@@ -6,6 +6,7 @@ import { createUserVignette } from '../components/userVignette.js';
import { getCsrfHeaders } from '../core/csrf.js';
import { formatFileSize, formatQuotaSize } from '../core/formatters.js';
import { i18n } from '../core/i18n.js';
import { groupsView } from '../views/groups/groupsView.js';
function setupUserMenu() {
const wrapper = document.getElementById('user-menu-wrapper');
@@ -15,6 +16,7 @@ function setupUserMenu() {
const themeSegmented = document.getElementById('user-menu-theme-segmented');
const aboutBtn = document.getElementById('user-menu-about');
const adminBtn = document.getElementById('user-menu-admin');
const groupsBtn = document.getElementById('user-menu-groups');
const adminDivider = document.getElementById('user-menu-admin-divider');
const profileBtn = document.getElementById('user-menu-profile');
const roleBadge = document.getElementById('user-menu-role-badge');
@@ -42,6 +44,12 @@ function setupUserMenu() {
if (adminBtn) {
isAdmin ? adminBtn.classList.remove('hidden') : adminBtn.classList.add('hidden');
}
if (groupsBtn) {
// v1: admin-only. v2 will broaden to "has any manageable
// group" — change the right-hand side here without touching
// anything else.
isAdmin ? groupsBtn.classList.remove('hidden') : groupsBtn.classList.add('hidden');
}
if (adminDivider) {
isAdmin ? adminDivider.classList.remove('hidden') : adminDivider.classList.add('hidden');
}
@@ -117,6 +125,13 @@ function setupUserMenu() {
});
}
if (groupsBtn) {
groupsBtn.addEventListener('click', () => {
wrapper.classList.remove('open');
groupsView.open();
});
}
if (profileBtn) {
profileBtn.addEventListener('click', () => {
wrapper.classList.remove('open');