Claude 08a62d550c feat(auth): refresh-token rotation, per-app role gate, custom error page
- jwt callback stores refresh_token + expires_at on initial sign-in and
  exchanges via Keycloak's token endpoint at expiresAt-30s. On refresh
  failure, marks session.user.error so consumers can force re-auth.
- New `requireClientRole(roles): boolean` option runs in the signIn
  callback against the access_token's realm_access.roles; false bounces
  the user to the configured error page with ?error=AccessDenied.
- New `pages` passthrough so consumers can route AccessDenied to a
  branded full-page message instead of NextAuth's default error UI.
- Realm roles are read by decoding the access_token, NOT from the OIDC
  `profile` (the userinfo endpoint never carries realm_access). Earlier
  code that read profile.realm_access.roles produced empty role arrays
  in both jwt and signIn — both callsites fixed via a shared
  rolesFromAccessToken helper.

Consumers: gscCRM v2.6.11..v2.6.19 (CRM enforces gsccrm_* groups via
requireClientRole + a /access-denied public route).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:25:50 +02:00

@gsc/web-kit

App skeleton for GSC Next.js frontends. Curates @limitless/ui primitives behind a pre-configured layout + auth + data/forms/feedback/navigation stack so apps just write their domain pages.

See the implementation plan in the parent repo for the full module map. This is a file: dep consumed by every GSC frontend.

Install (in a consumer app)

// package.json
{
  "dependencies": {
    "@gsc/web-kit": "file:../../../templates/gsc-web-kit"
  }
}

Layered architecture

your app
  └── @gsc/web-kit         ← this package (layout, auth, data, forms…)
        └── @limitless/ui   ← Bootstrap-flavoured primitives
              └── bootstrap

Sub-exports

import "@gsc/web-kit/css";              // CSS bundle (layout-3 + JetBrains Mono)

// Chrome — unified header/footer/sidebar shell
import { AdminShell } from "@gsc/web-kit/chrome";

// Lower-level layout primitives
import { AppLayout } from "@gsc/web-kit/layout";
import { useShell } from "@gsc/web-kit/shell";
import { fetchShellConfig } from "@gsc/web-kit/shell/server";

// Auth (NextAuth v5 + Keycloak)
import { createAuth } from "@gsc/web-kit/auth/server";
import { createAuthMiddleware } from "@gsc/web-kit/auth/middleware";
import { signInRedirect } from "@gsc/web-kit/auth";

// Building blocks — curated re-exports from @limitless/ui
import {
  Table, DataTable, Pagination, TreeView, Timeline,
  Calendar, Gallery, Sortable, ListGroup,
  StatWidget, ProgressWidget, ChartWidget, ContentWidget,
} from "@gsc/web-kit/data";

import {
  FormGroup, FormControl, FormCheck, Select, InputGroup,
  SelectSingle, MultiSelect, TagsSelect, AsyncSelect,
  DatePicker, ColorPicker, TagInput, FileUpload,
  Slider, Rating, DualListBox, ImageCropper, Wizard, Stepper,
  // validation
  useValidation, useFieldValidation, required, email, password,
  noInjection, europeanAddress, /* …etc */
} from "@gsc/web-kit/forms";

import {
  Alert, Toast, Notification, Modal, Offcanvas,
  Popover, Tooltip, SweetAlert, Spinner,
  Progress, ProgressStacked, IdleTimeout, FAB,
} from "@gsc/web-kit/feedback";

import {
  Breadcrumbs, Nav, Tabs, Pills, Dropdown, ContextMenu,
  Scrollspy, PageHeader, Accordion, Collapse, Carousel,
  Embed, SyntaxHighlighter, Card, Badge, Button, Media,
} from "@gsc/web-kit/navigation";

import { useDisclosure } from "@gsc/web-kit/utils";

The /api sub-export is reserved for a future HTTP client helper; it currently re-exports nothing.

Phases

Phase Scope Status
1 Package scaffold + CSS bundle + sub-export stubs done
2 layout · auth · shell — usable end-to-end with shell-api done
3 data · forms — curated re-exports from limitless + validation done (v0.3.0)
4 feedback · navigation · utils — curated re-exports from limitless done (v0.3.0)
5 chrome · AdminShell + headers + LogoutButton + nav migrations done (v0.4.0)
5a Roll out chrome to gscCRM / gscChronos / gscAdmin / gscPortal in progress
6 api · HTTP client helper (Bearer injection, 401 → signInRedirect) planned

Chrome (/chrome) — v0.4.0

Unified AdminShell providing navbar, subbar, page-header, sidebar, footer, optional chat overlay and activity panel. Every app receives the same UI chrome and toggles features it doesn't use via features props.

Adopting chrome in a new app

  1. Add the dep (already a file: resolve to this kit) and import:

    // app/[locale]/layout.tsx
    import { AdminShell } from "@gsc/web-kit/chrome";
    import "@gsc/web-kit/css";
    
  2. Apply nav migrations to your app's database:

    # In your app's migrations directory, copy the two kit-canonical files verbatim
    cp node_modules/@gsc/web-kit/migrations/nav-schema.up.sql    apps/<your-app>/migrations/00X_nav_schema.up.sql
    cp node_modules/@gsc/web-kit/migrations/nav-apps-seed.up.sql apps/<your-app>/migrations/00Y_nav_apps_seed.up.sql
    # Then copy the menu-items template once and adapt to your app's menu
    cp node_modules/@gsc/web-kit/migrations/nav-menu-items-template.sql apps/<your-app>/migrations/00Z_nav_menu_items.up.sql
    # edit 00Z_… to replace example rows with your app's sidebar/topbar entries
    psql "$DATABASE_URL" -f apps/<your-app>/migrations/00X_nav_schema.up.sql
    psql "$DATABASE_URL" -f apps/<your-app>/migrations/00Y_nav_apps_seed.up.sql
    psql "$DATABASE_URL" -f apps/<your-app>/migrations/00Z_nav_menu_items.up.sql
    

    Re-copy nav-schema + nav-apps-seed on every kit upgrade. The menu-items file is yours after the first copy — the kit never touches it again.

  3. Add Prisma to read the data:

    // prisma/schema.prisma
    datasource db {
      provider = "postgresql"
      url      = env("DATABASE_URL")
      schemas  = ["nav"]
    }
    generator client {
      provider        = "prisma-client-js"
      previewFeatures = ["multiSchema"]
    }
    

    Add "postinstall": "prisma generate" to package.json. Copy prisma/ into the Docker image before npm install so the generate step sees it.

  4. Wire the layout server component:

    const [sidebar, topbar, subbar, apps] = await Promise.all([
      getMenuItemsByType("sidebar"),
      getMenuItemsByType("topbar"),
      getMenuItemsByType("subbar"),
      getApps(),
    ]);
    
    return (
      <AdminShell
        menus={{ sidebar, topbar, subbar }}
        apps={apps}
        user={{ displayName, email, avatarUrl }}
        brand={{
          name: "MyApp",
          product: "GoSec MyApp",
          logoUrl: "https://assets.gosec.cloud/logos/logo.svg",
          websiteUrl: "https://gosec.cloud",
          supportUrl: "https://support.gosec.cloud/",
          docsUrl: "https://support.gosec.cloud/docs",
          copyrightStartYear: 2024,
        }}
        features={{ chat: false, activityPanel: false }}
      >
        {children}
      </AdminShell>
    );
    

Props reference

<AdminShell> is the kit's contract — see src/chrome/types.ts for the authoritative TypeScript definition. Summary:

  • Data: menus, apps, user, notificationCount?, activity?
  • Brand (required): name, product, logoUrl, websiteUrl, supportUrl, docsUrl, copyrightStartYear (+ optional logoSmallUrl)
  • features (all optional booleans, sensible defaults): search, searchHistory, searchOptions, browseApps, messages, notifications, subbar, subbarSupport, subbarSettings, pageHeader, pageHeaderCustomers, pageHeaderContacts, activityPanel, chat, footer
  • slots (ReactNode overrides): pageTitle, pageHeaderExtras, subbarExtras, activityPanel, navbarExtras, footerExtras
  • Behavior: onSignOut? (default next-auth signOut), labels?: Partial<ChromeLabels> (override individual chrome strings)

i18n

Chrome's own strings come from next-intl namespace chrome with English fallbacks. Add to your app's common.json:

{
  "chrome": {
    "navigation": "Navigation",
    "logout": "Logout",
    "support": "Support",
    ...
  }
}

Menu item labels (menu.dashboard, menu.accounts, …) live in your app's existing translation namespace.

Semver

Public surface = <AdminShell> props + exported types + migration files + chrome CSS class names.

  • Major — removed/renamed prop, changed prop shape, removed feature flag, renamed CSS class, changed migration DDL.
  • Minor — new optional prop, new feature flag (default off), new slot, new export, new migration file.
  • Patch — bugfix, internal refactor.

Deprecations land for one minor release with console.warn and a CHANGELOG.md note before removal in the next major. Pin "@gsc/web-kit": "^X.Y.Z" to receive minors automatically.


Notes

  • AppLayout is a thin wrapper around <AppShell> from @limitless/ui — they share the ShellConfig DTO, so the kit owns the consumer-facing surface without duplicating chrome code.
  • All form, data, feedback, and navigation modules are curated re-exports: apps should never need to reach into @limitless/ui directly.
  • The Widget family in /data is intentionally narrow (StatWidget, ProgressWidget, ChartWidget, ContentWidget) — the installed limitless dist has a duplicate Widget file/folder collision, so only names exported by both are passed through.
Description
App skeleton for GSC Next.js frontends. Layout, auth, data, forms, feedback, navigation on top of @limitless/ui.
Readme 454 KiB
Languages
TypeScript 92%
CSS 5.4%
JavaScript 2.6%