From bb9ee3785c4a8d7c0db581fbfb123dcab98896f6 Mon Sep 17 00:00:00 2001 From: JB Date: Sat, 25 Jul 2026 03:36:22 -0400 Subject: [PATCH] Add ISAC role management --- docs/DESIGN_SYSTEM.md | 7 ++ src/app.css | 1 + src/routes/admin/+layout.svelte | 14 ++-- src/routes/admin/+page.server.ts | 1 + src/routes/admin/roles/+page.server.ts | 97 ++++++++++++++++++++++++++ src/routes/admin/roles/+page.svelte | 52 ++++++++++++++ 6 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 src/routes/admin/roles/+page.server.ts create mode 100644 src/routes/admin/roles/+page.svelte diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index d705824..7e933f8 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -209,6 +209,13 @@ Use the spacing tokens for new components. Prefer `8px`, `12px`, and `20px` gaps unrestricted administrative role; `Moderator` is reserved for community content moderation and does not imply ISAC access. Operational ISAC access is delegated separately through scoped roles and permissions. +- Roles & Permissions is a separate User Operations registry. Role definitions + compose discrete capabilities through a dense permission catalogue with an + explicit selected count and textual `GRANTED` / `NOT GRANTED` states. The + built-in Admin role is visible but system-protected. Delegated access managers + must never be able to grant `system.admin`; both option visibility and + server-side validation enforce this boundary. Role mutations are recorded in + the audit log with their previous and resulting permission sets. - Account deletion uses a collapsed danger zone, explicit identity confirmation, and plain-language retention guidance. Soft-deleted accounts retain authored content under a deleted-agent identity while releasing credentials, usernames, diff --git a/src/app.css b/src/app.css index 30765d4..a3de51c 100644 --- a/src/app.css +++ b/src/app.css @@ -104,4 +104,5 @@ @media(max-width:1200px){.weapon-identity-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.weapon-fixed-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.weapon-tier-grid{grid-template-columns:1fr}.weapon-talent-pools{grid-template-columns:1fr}}@media(max-width:700px){.weapon-identity-grid,.weapon-fixed-grid{grid-template-columns:1fr 1fr}.compact-selection{grid-template-columns:1fr}.base-bonus-row,.augment-effect,.augment-effect:not(:has(>button)){grid-template-columns:30px 1fr}.base-bonus-row .field:nth-of-type(2),.augment-effect .field:nth-of-type(2){grid-column:2}.base-bonus-row>button,.augment-effect>button{grid-column:2}.augment-sequence{grid-template-columns:1fr 1fr}}@media(max-width:480px){.weapon-identity-grid,.weapon-fixed-grid{grid-template-columns:1fr}.weapon-range-row{grid-template-columns:45px 1fr 1fr}} .legal-table-wrap{overflow-x:auto;margin:18px 0}.legal-table{width:100%;min-width:720px;border-collapse:collapse;border:1px solid var(--shd-border);background:#0b0e0f}.legal-table th,.legal-table td{padding:12px 14px;border:1px solid var(--shd-border);text-align:left;vertical-align:top}.legal-table th{color:var(--shd-orange);background:#111516;font-size:7px;letter-spacing:1px}.legal-table td{color:#a1a7a8;font:12px/1.55 Georgia,serif}.legal-table code,.legal-document code{color:#d9dedf;font:11px Consolas,monospace}.cookie-notice{position:fixed;z-index:1000;right:24px;bottom:24px;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:22px;width:min(620px,calc(100vw - 48px));padding:18px 20px;border:1px solid var(--shd-orange);background:rgba(10,13,14,.98);box-shadow:0 14px 48px #000c,4px 0 0 var(--shd-orange)}.cookie-notice .eyebrow{margin:0 0 8px}.cookie-notice div>p:not(.eyebrow){margin:0;color:var(--shd-text-muted);font:12px/1.5 Georgia,serif}.cookie-notice a{display:inline-block;margin-top:10px;color:var(--shd-orange);font-size:7px;font-weight:900;letter-spacing:1px}.cookie-notice button{min-width:84px}@media(max-width:600px){.cookie-notice{right:12px;bottom:12px;grid-template-columns:1fr;width:calc(100vw - 24px)}.cookie-notice button{width:100%}} .kpi-filters{height:auto}.kpi-filters>a{position:relative;min-height:70px;padding:11px 13px;background:#111415;border:1px solid #2d3233;border-top:2px solid #3a4041;display:grid;color:inherit;cursor:pointer}.kpi-filters>a:hover,.kpi-filters>a:focus-visible{border-color:#8a672b;border-top-color:var(--shd-orange);background:#171a1b}.kpi-filters>a.active{border-color:#8a672b;border-top-color:var(--shd-orange);background:#211a0e;box-shadow:inset 0 -2px #f6a821}.kpi-filters>a:active{transform:translateY(1px);filter:brightness(.88)}.kpi-filters>a span{font-size:7px;letter-spacing:1.3px;color:#737a7c}.kpi-filters>a strong{font-size:23px;line-height:1.1;margin:3px 0}.kpi-filters>a small{font-size:7px;color:#555d5f}.kpi-filters>a.active span,.kpi-filters>a.active strong{color:var(--shd-orange)} +.role-identity{grid-template-columns:minmax(220px,.7fr) minmax(320px,1.3fr)!important}.permission-catalogue{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;margin-top:8px}.permission-catalogue label{min-height:54px;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:9px;padding:8px 10px;border:1px solid #303536;background:#0d1011;cursor:pointer}.permission-catalogue label:hover,.permission-catalogue label:focus-within{border-color:#8a672b;background:#151819}.permission-catalogue label:active{transform:translateY(1px);filter:brightness(.88)}.permission-catalogue input{width:auto;margin:0;accent-color:var(--shd-orange)}.permission-catalogue span{display:grid;gap:4px}.permission-catalogue b{color:#d7d9d5;font-size:9px}.permission-catalogue small{color:#626a6c;font-size:7px;line-height:1.35}.permission-catalogue em{color:#596164;font-size:6px;font-style:normal;font-weight:900;letter-spacing:.7px}.permission-catalogue label:has(input:checked){border-color:#6e5226;background:#211a0e}.permission-catalogue label:has(input:checked) em{color:var(--shd-orange)}.role-row{min-width:960px;grid-template-columns:1.1fr .55fr 2fr .65fr .3fr}.role-permission-summary{display:flex!important;flex-wrap:wrap;gap:3px}.role-permission-summary b{padding:3px 5px;border:1px solid #3a4840;color:#75c08c;font-size:6px;letter-spacing:.5px}@media(max-width:700px){.permission-catalogue,.role-identity{grid-template-columns:1fr!important}} diff --git a/src/routes/admin/+layout.svelte b/src/routes/admin/+layout.svelte index 4fd03c1..b38f502 100644 --- a/src/routes/admin/+layout.svelte +++ b/src/routes/admin/+layout.svelte @@ -9,7 +9,11 @@ { href: '/admin/gear', label: 'Gear', code: 'GEAR' }, { href: '/admin/weapons', label: 'Weapons', code: 'WPN' } ]; - const userSections = [{ href: '/admin/users', label: 'User operations', code: 'USER' }]; + const userSections = [ + { href: '/admin/users', label: 'User operations', code: 'USER', permission: 'users.view' }, + { href: '/admin/roles', label: 'Roles & permissions', code: 'ROLE', permission: 'users.roles' } + ]; + const visibleUserSections = $derived(userSections.filter((section) => can(section.permission)));
@@ -19,12 +23,12 @@