Compare commits
7 Commits
387e10b2fb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a11353577d | ||
|
|
71bce1bd56 | ||
|
|
85a31eb3d6 | ||
|
|
ec33b7bcb8 | ||
|
|
08a62d550c | ||
|
|
960dfeba7c | ||
|
|
440f815df7 |
150
README.md
150
README.md
@@ -29,7 +29,10 @@ your app
|
|||||||
```ts
|
```ts
|
||||||
import "@gsc/web-kit/css"; // CSS bundle (layout-3 + JetBrains Mono)
|
import "@gsc/web-kit/css"; // CSS bundle (layout-3 + JetBrains Mono)
|
||||||
|
|
||||||
// Chrome
|
// 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 { AppLayout } from "@gsc/web-kit/layout";
|
||||||
import { useShell } from "@gsc/web-kit/shell";
|
import { useShell } from "@gsc/web-kit/shell";
|
||||||
import { fetchShellConfig } from "@gsc/web-kit/shell/server";
|
import { fetchShellConfig } from "@gsc/web-kit/shell/server";
|
||||||
@@ -81,8 +84,149 @@ The `/api` sub-export is reserved for a future HTTP client helper; it currently
|
|||||||
| 2 | layout · auth · shell — usable end-to-end with shell-api | **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)** |
|
| 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)** |
|
| 4 | feedback · navigation · utils — curated re-exports from limitless | **done (v0.3.0)** |
|
||||||
| 4a | api · HTTP client helper (Bearer injection, 401 → signInRedirect) | planned |
|
| 5 | chrome · AdminShell + headers + LogoutButton + nav migrations | **done (v0.4.0)** |
|
||||||
| 5 | Roll out to gscCRM / gscChronos / gscAdmin / gscPortal | planned |
|
| 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:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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
|
||||||
|
// 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**:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```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
|
## Notes
|
||||||
|
|
||||||
|
|||||||
30
migrations/nav-apps-seed.up.sql
Normal file
30
migrations/nav-apps-seed.up.sql
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
-- @gsc/web-kit nav.apps canonical seed (apply verbatim, never edit)
|
||||||
|
--
|
||||||
|
-- Replicated into every app's nav.apps table so the "browse apps" panel
|
||||||
|
-- shows the same canonical list everywhere. Updates flow via kit version
|
||||||
|
-- bumps: re-copy this file into each app's migrations directory and re-apply.
|
||||||
|
--
|
||||||
|
-- ON CONFLICT (key) DO UPDATE keeps the canonical list in sync — any local
|
||||||
|
-- edits to these rows will be overwritten on next apply. Add new apps by
|
||||||
|
-- editing this file in the kit and bumping the kit's minor version.
|
||||||
|
|
||||||
|
INSERT INTO nav.apps (key, name, description, url, icon_class, icon_url, icon_bg, sort_order, enabled)
|
||||||
|
VALUES
|
||||||
|
('gsc-crm', 'CRM', 'Customer relationship management', 'https://crm.gosec.internal', NULL, 'https://assets.gosec.cloud/logos/crm.svg', NULL, 10, TRUE),
|
||||||
|
('gsc-chronos', 'Chronos', 'Time tracking and timesheets', 'https://chronos.gosec.internal', NULL, 'https://assets.gosec.cloud/logos/chronos.svg', NULL, 20, TRUE),
|
||||||
|
('gsc-meet', 'GSC Meet', 'Video conferencing with AI features', '/apps/gsc-meet', 'ph-video-camera', NULL, 'bg-success-lt', 30, TRUE),
|
||||||
|
('gsc-voice', 'Voice', 'PBX and telephony management', '/apps/gsc-voice', 'ph-phone', NULL, 'bg-primary-lt', 40, TRUE),
|
||||||
|
('gsc-ai-hub', 'AI Hub', 'AI models and services management', '/apps/gsc-ai-hub', 'ph-brain', NULL, 'bg-warning-lt', 50, TRUE),
|
||||||
|
('gsc-surveillance', 'Surveillance', 'Video surveillance and security', '/apps/surveillance', 'ph-shield-check', NULL, 'bg-danger-lt', 60, TRUE),
|
||||||
|
('gsc-archive', 'Archive', 'Email archiving and eDiscovery', '/apps/gsc-archive', 'ph-archive-box', NULL, 'bg-info-lt', 70, TRUE),
|
||||||
|
('gsc-dam', 'Digital Asset Manager', 'Media and asset management platform', '/apps/gsc-dam', 'ph-images-square', NULL, 'bg-secondary-lt', 80, TRUE)
|
||||||
|
ON CONFLICT (key) DO UPDATE
|
||||||
|
SET name = EXCLUDED.name,
|
||||||
|
description = EXCLUDED.description,
|
||||||
|
url = EXCLUDED.url,
|
||||||
|
icon_class = EXCLUDED.icon_class,
|
||||||
|
icon_url = EXCLUDED.icon_url,
|
||||||
|
icon_bg = EXCLUDED.icon_bg,
|
||||||
|
sort_order = EXCLUDED.sort_order,
|
||||||
|
enabled = EXCLUDED.enabled,
|
||||||
|
updated_at = NOW();
|
||||||
70
migrations/nav-menu-items-template.sql
Normal file
70
migrations/nav-menu-items-template.sql
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
-- @gsc/web-kit nav.menu_items TEMPLATE — copy once, then adapt per app
|
||||||
|
--
|
||||||
|
-- Each app curates its own sidebar/topbar/subbar items. Copy this file into
|
||||||
|
-- your app's migrations directory (e.g. apps/gscFoo/migrations/00X_nav_menu_items.up.sql),
|
||||||
|
-- replace the example rows below with your app's real menu, and apply.
|
||||||
|
--
|
||||||
|
-- The kit will NOT update this file in subsequent versions — once you've
|
||||||
|
-- copied and adapted it, it's yours. The DDL (nav-schema.up.sql) and the
|
||||||
|
-- canonical apps list (nav-apps-seed.up.sql) are kit-owned and re-copied
|
||||||
|
-- on every kit upgrade; menu_items rows belong to the app.
|
||||||
|
--
|
||||||
|
-- Notes on columns:
|
||||||
|
-- - key: unique stable identifier (used by ON CONFLICT)
|
||||||
|
-- - translation_key: dotted i18n key, e.g. "menu.dashboard"
|
||||||
|
-- (resolved via next-intl namespace "menu")
|
||||||
|
-- - url: "/dashboard" — pass "#" for parent items with children
|
||||||
|
-- - icon: Phosphor CSS class, e.g. "ph-house"
|
||||||
|
-- - menu_type: 'sidebar' (left nav), 'topbar' (user dropdown), 'subbar' (settings dropdown)
|
||||||
|
-- - parent_id: UUID of parent menu_item for nested items (or NULL)
|
||||||
|
-- - sort_order: ascending; render order within type/parent
|
||||||
|
-- - is_system_required: cannot be hidden/removed by user customization
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- SIDEBAR (left navigation)
|
||||||
|
-- ============================================================================
|
||||||
|
INSERT INTO nav.menu_items (menu_type, key, translation_key, url, icon, sort_order, is_active, is_system_required)
|
||||||
|
VALUES
|
||||||
|
-- Replace these example rows with your app's sidebar items.
|
||||||
|
('sidebar', 'dashboard', 'menu.dashboard', '/dashboard', 'ph-house', 10, TRUE, TRUE),
|
||||||
|
('sidebar', 'example-1', 'menu.example1', '/example-1', 'ph-folder-open', 20, TRUE, FALSE),
|
||||||
|
('sidebar', 'example-2', 'menu.example2', '/example-2', 'ph-list', 30, TRUE, FALSE)
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- TOPBAR (user dropdown in navbar)
|
||||||
|
-- ============================================================================
|
||||||
|
INSERT INTO nav.menu_items (menu_type, key, translation_key, url, icon, sort_order, is_active, is_system_required)
|
||||||
|
VALUES
|
||||||
|
('topbar', 'profile', 'menu.profile', '/profile', 'ph-user', 10, TRUE, TRUE),
|
||||||
|
('topbar', 'settings', 'menu.settings', '/settings', 'ph-gear', 20, TRUE, TRUE)
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- SUBBAR (Settings dropdown items)
|
||||||
|
-- ============================================================================
|
||||||
|
-- Uncomment / customize if your app exposes a Settings dropdown in the subbar.
|
||||||
|
--
|
||||||
|
-- INSERT INTO nav.menu_items (menu_type, key, translation_key, url, icon, sort_order, is_active, is_system_required)
|
||||||
|
-- VALUES
|
||||||
|
-- ('subbar', 'general-settings', 'menu.generalSettings', '/settings/general', 'ph-sliders', 10, TRUE, FALSE),
|
||||||
|
-- ('subbar', 'notifications', 'menu.notifications', '/settings/notifications', 'ph-bell', 20, TRUE, FALSE)
|
||||||
|
-- ON CONFLICT (key) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- NESTED EXAMPLES (parent + children)
|
||||||
|
-- ============================================================================
|
||||||
|
-- For nested sidebar items, set url='#' on the parent and reference its id
|
||||||
|
-- via parent_id on children. Example:
|
||||||
|
--
|
||||||
|
-- WITH parent AS (
|
||||||
|
-- INSERT INTO nav.menu_items (menu_type, key, translation_key, url, icon, sort_order)
|
||||||
|
-- VALUES ('sidebar', 'reports', 'menu.reports', '#', 'ph-chart-bar', 90)
|
||||||
|
-- ON CONFLICT (key) DO UPDATE SET sort_order = EXCLUDED.sort_order
|
||||||
|
-- RETURNING id
|
||||||
|
-- )
|
||||||
|
-- INSERT INTO nav.menu_items (menu_type, parent_id, key, translation_key, url, icon, sort_order)
|
||||||
|
-- SELECT 'sidebar', parent.id, 'reports-sales', 'menu.reportsSales', '/reports/sales', 'ph-chart-line', 10 FROM parent
|
||||||
|
-- UNION ALL
|
||||||
|
-- SELECT 'sidebar', parent.id, 'reports-leads', 'menu.reportsLeads', '/reports/leads', 'ph-chart-pie', 20 FROM parent
|
||||||
|
-- ON CONFLICT (key) DO NOTHING;
|
||||||
117
migrations/nav-schema.up.sql
Normal file
117
migrations/nav-schema.up.sql
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
-- @gsc/web-kit nav schema (canonical, apply verbatim, never edit)
|
||||||
|
--
|
||||||
|
-- Creates the navigation tables consumed by @gsc/web-kit/chrome AdminShell:
|
||||||
|
-- - nav.menu_items — per-app sidebar/topbar/subbar items
|
||||||
|
-- - nav.menu_role_requirements — role gating (enforcement TBD)
|
||||||
|
-- - nav.menu_permission_requirements
|
||||||
|
-- - nav.menu_product_requirements
|
||||||
|
-- - nav.apps — cross-app "browse apps" panel data
|
||||||
|
--
|
||||||
|
-- Per-app philosophy: each app applies this schema to its OWN database
|
||||||
|
-- (gsc_crm, gsc_chronos, gsc_admin, ...). Menu items are app-specific;
|
||||||
|
-- the apps list is replicated from nav-apps-seed.up.sql so every app's
|
||||||
|
-- "browse apps" panel shows the same canonical list.
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
CREATE SCHEMA IF NOT EXISTS nav;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Enums
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_type t
|
||||||
|
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||||
|
WHERE t.typname = 'menu_type' AND n.nspname = 'nav'
|
||||||
|
) THEN
|
||||||
|
CREATE TYPE nav.menu_type AS ENUM ('sidebar', 'topbar', 'subbar');
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_type t
|
||||||
|
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||||
|
WHERE t.typname = 'requirement_logic' AND n.nspname = 'nav'
|
||||||
|
) THEN
|
||||||
|
CREATE TYPE nav.requirement_logic AS ENUM ('OR', 'AND');
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- menu_items
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS nav.menu_items (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
menu_type nav.menu_type NOT NULL,
|
||||||
|
parent_id UUID NULL REFERENCES nav.menu_items(id) ON DELETE CASCADE,
|
||||||
|
key VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
translation_key VARCHAR(128) NOT NULL,
|
||||||
|
url VARCHAR(256) NOT NULL,
|
||||||
|
icon VARCHAR(64) NULL,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
is_system_required BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
role_logic nav.requirement_logic NOT NULL DEFAULT 'OR',
|
||||||
|
permission_logic nav.requirement_logic NOT NULL DEFAULT 'OR',
|
||||||
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_menu_items_type ON nav.menu_items (menu_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_menu_items_parent ON nav.menu_items (parent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_menu_items_sort ON nav.menu_items (sort_order);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- menu requirement tables (role / permission / product)
|
||||||
|
-- Cross-DB IDs stored as plain strings; no cross-DB FKs.
|
||||||
|
-- Enforcement is not yet wired in v0.4.0 chrome.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS nav.menu_role_requirements (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
menu_item_id UUID NOT NULL REFERENCES nav.menu_items(id) ON DELETE CASCADE,
|
||||||
|
role_id VARCHAR(128) NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE (menu_item_id, role_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS nav.menu_permission_requirements (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
menu_item_id UUID NOT NULL REFERENCES nav.menu_items(id) ON DELETE CASCADE,
|
||||||
|
permission_id VARCHAR(128) NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE (menu_item_id, permission_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS nav.menu_product_requirements (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
menu_item_id UUID NOT NULL REFERENCES nav.menu_items(id) ON DELETE CASCADE,
|
||||||
|
product_id VARCHAR(128) NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE (menu_item_id, product_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- apps — cross-app browse-apps panel data
|
||||||
|
-- Either icon_class (Phosphor / FA class) or icon_url (logo image) may be set.
|
||||||
|
-- icon_bg styles the colored square behind icon_class (e.g. 'bg-primary-lt').
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS nav.apps (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
key VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
name VARCHAR(128) NOT NULL,
|
||||||
|
description VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
url VARCHAR(512) NOT NULL,
|
||||||
|
icon_class VARCHAR(64) NULL,
|
||||||
|
icon_url VARCHAR(512) NULL,
|
||||||
|
icon_bg VARCHAR(32) NULL,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_apps_sort_order ON nav.apps (sort_order);
|
||||||
116
package-lock.json
generated
116
package-lock.json
generated
@@ -1,17 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "@gsc/web-kit",
|
"name": "@gsc/web-kit",
|
||||||
"version": "0.2.0",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@gsc/web-kit",
|
"name": "@gsc/web-kit",
|
||||||
"version": "0.2.0",
|
"version": "0.4.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@limitless/ui": "file:../limitless-ui",
|
"@limitless/ui": "file:../limitless-ui",
|
||||||
"next-auth": "^5.0.0-beta.25",
|
|
||||||
"next-intl": "^4.6.1",
|
|
||||||
"zod": "^3.23.0"
|
"zod": "^3.23.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -19,15 +17,23 @@
|
|||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"next": "16.1.1",
|
"next": "16.1.1",
|
||||||
|
"next-auth": "^5.0.0-beta.25",
|
||||||
|
"next-intl": "^4.6.1",
|
||||||
"typescript": "^5.4.0"
|
"typescript": "^5.4.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
"@gsc/chat": "*",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"next": ">=15.0.0",
|
"next": ">=15.0.0",
|
||||||
|
"next-auth": "^5.0.0-beta.25",
|
||||||
|
"next-intl": "^4.6.0",
|
||||||
"react": "^18.2.0 || ^19.0.0",
|
"react": "^18.2.0 || ^19.0.0",
|
||||||
"react-dom": "^18.2.0 || ^19.0.0"
|
"react-dom": "^18.2.0 || ^19.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
|
"@gsc/chat": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"bootstrap": {
|
"bootstrap": {
|
||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
@@ -62,6 +68,7 @@
|
|||||||
"version": "0.41.2",
|
"version": "0.41.2",
|
||||||
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.2.tgz",
|
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.2.tgz",
|
||||||
"integrity": "sha512-Hx5MNBxN2fJTbJKGUKAA0wca43D0Akl3TvufY54Gn8lop7F+34vU1zA1pn0vQfIoVuLIrpfc2nkyjwIaPJMW7w==",
|
"integrity": "sha512-Hx5MNBxN2fJTbJKGUKAA0wca43D0Akl3TvufY54Gn8lop7F+34vU1zA1pn0vQfIoVuLIrpfc2nkyjwIaPJMW7w==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@panva/hkdf": "^1.2.1",
|
"@panva/hkdf": "^1.2.1",
|
||||||
@@ -91,6 +98,7 @@
|
|||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -101,12 +109,14 @@
|
|||||||
"version": "3.1.4",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.4.tgz",
|
||||||
"integrity": "sha512-Lbke1aOrsygKKR09Ux0NrZgbTqpDmiwXOgzyDOJ8Owr1zd5qOKTauf62hH+Seeku3ju77rHWH9I5SfX2CN0vuA==",
|
"integrity": "sha512-Lbke1aOrsygKKR09Ux0NrZgbTqpDmiwXOgzyDOJ8Owr1zd5qOKTauf62hH+Seeku3ju77rHWH9I5SfX2CN0vuA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@formatjs/icu-messageformat-parser": {
|
"node_modules/@formatjs/icu-messageformat-parser": {
|
||||||
"version": "3.5.7",
|
"version": "3.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.7.tgz",
|
||||||
"integrity": "sha512-wJxRZ+SiUCIMTL86bQlZU9bEKDQqqvgk2ezQ1BySUdWRfHqOzj4IKUVFeUZKS9w58M4e7wMSG0Sl86LAPb7Qww==",
|
"integrity": "sha512-wJxRZ+SiUCIMTL86bQlZU9bEKDQqqvgk2ezQ1BySUdWRfHqOzj4IKUVFeUZKS9w58M4e7wMSG0Sl86LAPb7Qww==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/icu-skeleton-parser": "2.1.7"
|
"@formatjs/icu-skeleton-parser": "2.1.7"
|
||||||
@@ -116,12 +126,14 @@
|
|||||||
"version": "2.1.7",
|
"version": "2.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.7.tgz",
|
||||||
"integrity": "sha512-cIw1SFP0bi0CUBiJ2jzp99ws3OJNQDfStcHq9Z0iHWzItmiIikihFO+npR8C80yDlp7ZuBCLXCcKjgWjHicksA==",
|
"integrity": "sha512-cIw1SFP0bi0CUBiJ2jzp99ws3OJNQDfStcHq9Z0iHWzItmiIikihFO+npR8C80yDlp7ZuBCLXCcKjgWjHicksA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@formatjs/intl-localematcher": {
|
"node_modules/@formatjs/intl-localematcher": {
|
||||||
"version": "0.8.6",
|
"version": "0.8.6",
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.6.tgz",
|
||||||
"integrity": "sha512-AZRgUxj0q93lyF7Z5lFS85bLINXuBLX4R3tCKicO6fSWo6cvh9GQfoR3B1WlsqQwefZ1QORTivhInx7gM6HUzQ==",
|
"integrity": "sha512-AZRgUxj0q93lyF7Z5lFS85bLINXuBLX4R3tCKicO6fSWo6cvh9GQfoR3B1WlsqQwefZ1QORTivhInx7gM6HUzQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/fast-memoize": "3.1.4"
|
"@formatjs/fast-memoize": "3.1.4"
|
||||||
@@ -131,6 +143,7 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||||
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -144,6 +157,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -166,6 +180,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -188,6 +203,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -204,6 +220,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -220,6 +237,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -236,6 +254,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -252,6 +271,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -268,6 +288,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -284,6 +305,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -300,6 +322,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -316,6 +339,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -332,6 +356,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -348,6 +373,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -370,6 +396,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -392,6 +419,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -414,6 +442,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -436,6 +465,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -458,6 +488,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -480,6 +511,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -502,6 +534,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -524,6 +557,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -543,6 +577,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -562,6 +597,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -581,6 +617,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -601,6 +638,7 @@
|
|||||||
"version": "16.1.1",
|
"version": "16.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz",
|
||||||
"integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==",
|
"integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-arm64": {
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
@@ -610,6 +648,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -626,6 +665,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -642,6 +682,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -658,6 +699,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -674,6 +716,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -690,6 +733,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -706,6 +750,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -722,6 +767,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -735,6 +781,7 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
|
||||||
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
|
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/panva"
|
"url": "https://github.com/sponsors/panva"
|
||||||
@@ -744,6 +791,7 @@
|
|||||||
"version": "2.5.6",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
|
||||||
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
|
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
|
||||||
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -782,6 +830,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -802,6 +851,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -822,6 +872,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -842,6 +893,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -862,6 +914,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -882,6 +935,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -902,6 +956,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -922,6 +977,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -942,6 +998,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -962,6 +1019,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -982,6 +1040,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1002,6 +1061,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1022,6 +1082,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1051,6 +1112,7 @@
|
|||||||
"version": "1.21.5",
|
"version": "1.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz",
|
||||||
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
|
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core-darwin-arm64": {
|
"node_modules/@swc/core-darwin-arm64": {
|
||||||
@@ -1060,6 +1122,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0 AND MIT",
|
"license": "Apache-2.0 AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1076,6 +1139,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0 AND MIT",
|
"license": "Apache-2.0 AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1092,6 +1156,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1108,6 +1173,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0 AND MIT",
|
"license": "Apache-2.0 AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1124,6 +1190,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0 AND MIT",
|
"license": "Apache-2.0 AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1140,6 +1207,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0 AND MIT",
|
"license": "Apache-2.0 AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1156,6 +1224,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0 AND MIT",
|
"license": "Apache-2.0 AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1172,6 +1241,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0 AND MIT",
|
"license": "Apache-2.0 AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1188,6 +1258,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0 AND MIT",
|
"license": "Apache-2.0 AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1204,6 +1275,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0 AND MIT",
|
"license": "Apache-2.0 AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1220,6 +1292,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0 AND MIT",
|
"license": "Apache-2.0 AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1236,6 +1309,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0 AND MIT",
|
"license": "Apache-2.0 AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1249,12 +1323,14 @@
|
|||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.15",
|
"version": "0.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||||
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
|
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
@@ -1264,6 +1340,7 @@
|
|||||||
"version": "0.1.26",
|
"version": "0.1.26",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz",
|
||||||
"integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==",
|
"integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/counter": "^0.1.3"
|
"@swc/counter": "^0.1.3"
|
||||||
@@ -1303,6 +1380,7 @@
|
|||||||
"version": "2.10.29",
|
"version": "2.10.29",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz",
|
||||||
"integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==",
|
"integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"baseline-browser-mapping": "dist/cli.cjs"
|
"baseline-browser-mapping": "dist/cli.cjs"
|
||||||
@@ -1336,6 +1414,7 @@
|
|||||||
"version": "1.0.30001792",
|
"version": "1.0.30001792",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz",
|
||||||
"integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==",
|
"integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1356,6 +1435,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
@@ -1369,6 +1449,7 @@
|
|||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -1378,6 +1459,7 @@
|
|||||||
"version": "4.11.1",
|
"version": "4.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.11.1.tgz",
|
||||||
"integrity": "sha512-C0tsPVuvyNp+++qWJP+mty/KLLStjerOZqu3W1xWLJkChEDbDi9Taoj6blK7L/onxbuVzwgH6k9Sf+rOV6lOvw==",
|
"integrity": "sha512-C0tsPVuvyNp+++qWJP+mty/KLLStjerOZqu3W1xWLJkChEDbDi9Taoj6blK7L/onxbuVzwgH6k9Sf+rOV6lOvw==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -1393,6 +1475,7 @@
|
|||||||
"version": "11.2.4",
|
"version": "11.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.4.tgz",
|
||||||
"integrity": "sha512-iKP6+uJXn+XcfRgYfGPE3+mqCoODV2vATrXDLo/YkYgIdelJHJPBEbc0GZThipAYPuk+8QJFiPgOfblU085ABg==",
|
"integrity": "sha512-iKP6+uJXn+XcfRgYfGPE3+mqCoODV2vATrXDLo/YkYgIdelJHJPBEbc0GZThipAYPuk+8QJFiPgOfblU085ABg==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/fast-memoize": "3.1.4",
|
"@formatjs/fast-memoize": "3.1.4",
|
||||||
@@ -1403,6 +1486,7 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -1412,6 +1496,7 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-extglob": "^2.1.1"
|
"is-extglob": "^2.1.1"
|
||||||
@@ -1424,6 +1509,7 @@
|
|||||||
"version": "6.2.3",
|
"version": "6.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
|
||||||
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
|
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/panva"
|
"url": "https://github.com/sponsors/panva"
|
||||||
@@ -1433,6 +1519,7 @@
|
|||||||
"version": "3.3.12",
|
"version": "3.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -1451,6 +1538,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@@ -1460,6 +1548,7 @@
|
|||||||
"version": "16.1.1",
|
"version": "16.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz",
|
||||||
"integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==",
|
"integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "16.1.1",
|
"@next/env": "16.1.1",
|
||||||
@@ -1513,6 +1602,7 @@
|
|||||||
"version": "5.0.0-beta.31",
|
"version": "5.0.0-beta.31",
|
||||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.31.tgz",
|
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.31.tgz",
|
||||||
"integrity": "sha512-1OBgCKPzo+S7UWWMp3xgvGvIJ0OpV7B3vR4ZDRqD9a4Ch+OT6dakLXG9ivhtmIWVa71nTSXattOHyCg8sNi8/Q==",
|
"integrity": "sha512-1OBgCKPzo+S7UWWMp3xgvGvIJ0OpV7B3vR4ZDRqD9a4Ch+OT6dakLXG9ivhtmIWVa71nTSXattOHyCg8sNi8/Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "0.41.2"
|
"@auth/core": "0.41.2"
|
||||||
@@ -1540,6 +1630,7 @@
|
|||||||
"version": "4.11.1",
|
"version": "4.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.11.1.tgz",
|
||||||
"integrity": "sha512-s32lFFLXkxrW6fy+4IVaGD5J8xPpbEDFLfBbXV73CTbHAGhOGMjYN4/rftdsKOQ44AnPhnZ5Et+ZNMr5tRpsqA==",
|
"integrity": "sha512-s32lFFLXkxrW6fy+4IVaGD5J8xPpbEDFLfBbXV73CTbHAGhOGMjYN4/rftdsKOQ44AnPhnZ5Et+ZNMr5tRpsqA==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -1571,12 +1662,14 @@
|
|||||||
"version": "4.11.1",
|
"version": "4.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.11.1.tgz",
|
||||||
"integrity": "sha512-jHKGij7NoYccy2y54+e/wHVMoRgNt4h/Kn0XS9c4GbKu3KgJyANLUN8sFcDixv6sqz4V2kh6CTWgrkIidQksUg==",
|
"integrity": "sha512-jHKGij7NoYccy2y54+e/wHVMoRgNt4h/Kn0XS9c4GbKu3KgJyANLUN8sFcDixv6sqz4V2kh6CTWgrkIidQksUg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/next-intl/node_modules/@swc/core": {
|
"node_modules/next-intl/node_modules/@swc/core": {
|
||||||
"version": "1.15.33",
|
"version": "1.15.33",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.33.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.33.tgz",
|
||||||
"integrity": "sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==",
|
"integrity": "sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==",
|
||||||
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1617,6 +1710,7 @@
|
|||||||
"version": "0.5.21",
|
"version": "0.5.21",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz",
|
||||||
"integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==",
|
"integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
@@ -1628,12 +1722,14 @@
|
|||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/oauth4webapi": {
|
"node_modules/oauth4webapi": {
|
||||||
"version": "3.8.6",
|
"version": "3.8.6",
|
||||||
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.6.tgz",
|
||||||
"integrity": "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==",
|
"integrity": "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/panva"
|
"url": "https://github.com/sponsors/panva"
|
||||||
@@ -1643,12 +1739,14 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -1661,12 +1759,14 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz",
|
||||||
"integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==",
|
"integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.31",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1695,6 +1795,7 @@
|
|||||||
"version": "10.24.3",
|
"version": "10.24.3",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
|
||||||
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
|
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1705,6 +1806,7 @@
|
|||||||
"version": "6.5.11",
|
"version": "6.5.11",
|
||||||
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz",
|
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz",
|
||||||
"integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==",
|
"integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"preact": ">=10"
|
"preact": ">=10"
|
||||||
@@ -1744,6 +1846,7 @@
|
|||||||
"version": "7.8.0",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
|
||||||
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
|
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1757,6 +1860,7 @@
|
|||||||
"version": "0.34.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||||
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||||
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -1802,6 +1906,7 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -1811,6 +1916,7 @@
|
|||||||
"version": "5.1.6",
|
"version": "5.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
||||||
"integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
|
"integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"client-only": "0.0.1"
|
"client-only": "0.0.1"
|
||||||
@@ -1834,6 +1940,7 @@
|
|||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"dev": true,
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
@@ -1861,6 +1968,7 @@
|
|||||||
"version": "4.11.1",
|
"version": "4.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.11.1.tgz",
|
||||||
"integrity": "sha512-/dqWSqUSbVMzC+fdy7io8enhGYHeGeHK1bFhTLrp0ZblqdzY4FkE+tkffW6IfCauqaIA2/z4DQae4XEn93+raw==",
|
"integrity": "sha512-/dqWSqUSbVMzC+fdy7io8enhGYHeGeHK1bFhTLrp0ZblqdzY4FkE+tkffW6IfCauqaIA2/z4DQae4XEn93+raw==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
|
|||||||
35
package.json
35
package.json
@@ -1,12 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "@gsc/web-kit",
|
"name": "@gsc/web-kit",
|
||||||
"version": "0.3.0",
|
"version": "0.5.1",
|
||||||
"description": "GSC web app skeleton \u2014 layout, auth, data, forms, feedback, navigation. Built on @limitless/ui. Drop into a Next.js app and just write pages.",
|
"description": "GSC web app skeleton — layout, auth, data, forms, feedback, navigation, chrome. Built on @limitless/ui. Drop into a Next.js app and just write pages.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"migrations"
|
||||||
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@@ -37,6 +41,10 @@
|
|||||||
"types": "./dist/shell/server.d.ts",
|
"types": "./dist/shell/server.d.ts",
|
||||||
"import": "./dist/shell/server.js"
|
"import": "./dist/shell/server.js"
|
||||||
},
|
},
|
||||||
|
"./chrome": {
|
||||||
|
"types": "./dist/chrome/index.d.ts",
|
||||||
|
"import": "./dist/chrome/index.js"
|
||||||
|
},
|
||||||
"./data": {
|
"./data": {
|
||||||
"types": "./dist/data/index.d.ts",
|
"types": "./dist/data/index.d.ts",
|
||||||
"import": "./dist/data/index.js"
|
"import": "./dist/data/index.js"
|
||||||
@@ -57,10 +65,21 @@
|
|||||||
"types": "./dist/api/index.d.ts",
|
"types": "./dist/api/index.d.ts",
|
||||||
"import": "./dist/api/index.js"
|
"import": "./dist/api/index.js"
|
||||||
},
|
},
|
||||||
|
"./i18n": {
|
||||||
|
"types": "./dist/i18n/index.d.ts",
|
||||||
|
"import": "./dist/i18n/index.js"
|
||||||
|
},
|
||||||
|
"./i18n/server": {
|
||||||
|
"types": "./dist/i18n/server.d.ts",
|
||||||
|
"import": "./dist/i18n/server.js"
|
||||||
|
},
|
||||||
"./utils": {
|
"./utils": {
|
||||||
"types": "./dist/utils/index.d.ts",
|
"types": "./dist/utils/index.d.ts",
|
||||||
"import": "./dist/utils/index.js"
|
"import": "./dist/utils/index.js"
|
||||||
}
|
},
|
||||||
|
"./migrations/nav-schema.up.sql": "./migrations/nav-schema.up.sql",
|
||||||
|
"./migrations/nav-apps-seed.up.sql": "./migrations/nav-apps-seed.up.sql",
|
||||||
|
"./migrations/nav-menu-items-template.sql": "./migrations/nav-menu-items-template.sql"
|
||||||
},
|
},
|
||||||
"sideEffects": [
|
"sideEffects": [
|
||||||
"**/*.css"
|
"**/*.css"
|
||||||
@@ -72,24 +91,28 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@limitless/ui": "file:../limitless-ui",
|
"@limitless/ui": "file:../limitless-ui",
|
||||||
"next-auth": "^5.0.0-beta.25",
|
|
||||||
"next-intl": "^4.6.1",
|
|
||||||
"zod": "^3.23.0"
|
"zod": "^3.23.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
"@gsc/chat": "*",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"next": ">=15.0.0",
|
"next": ">=15.0.0",
|
||||||
|
"next-auth": "^5.0.0-beta.25",
|
||||||
|
"next-intl": "^4.6.0",
|
||||||
"react": "^18.2.0 || ^19.0.0",
|
"react": "^18.2.0 || ^19.0.0",
|
||||||
"react-dom": "^18.2.0 || ^19.0.0"
|
"react-dom": "^18.2.0 || ^19.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"bootstrap": { "optional": true }
|
"bootstrap": { "optional": true },
|
||||||
|
"@gsc/chat": { "optional": true }
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"next": "16.1.1",
|
"next": "16.1.1",
|
||||||
|
"next-auth": "^5.0.0-beta.25",
|
||||||
|
"next-intl": "^4.6.1",
|
||||||
"typescript": "^5.4.0"
|
"typescript": "^5.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
50
src/auth/SessionExpirationGuard.tsx
Normal file
50
src/auth/SessionExpirationGuard.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useSession, signIn } from "next-auth/react";
|
||||||
|
|
||||||
|
import type { SessionUser } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop-in client component that re-triggers Keycloak sign-in when the
|
||||||
|
* kit's server-side refresh flow gives up (`session.user.error` set to
|
||||||
|
* `RefreshAccessTokenError` or `RefreshTokenMissing`).
|
||||||
|
*
|
||||||
|
* Without this, a UI tab left open past the Keycloak SSO session lifetime
|
||||||
|
* holds a stale `accessToken`: the middleware sees a cookie, `requireAuth`
|
||||||
|
* sees an `accessToken` string, and the user only finds out things are
|
||||||
|
* broken when a downstream API returns 401. Mount this inside a
|
||||||
|
* `<SessionProvider>` (typically alongside other providers in the locale
|
||||||
|
* root) so the session refetch interval surfaces the error within minutes.
|
||||||
|
*
|
||||||
|
* Renders nothing.
|
||||||
|
*/
|
||||||
|
export function SessionExpirationGuard() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const error = (session?.user as Partial<SessionUser> | undefined)?.error;
|
||||||
|
// Guard against React's StrictMode double-invoke + the brief window
|
||||||
|
// between calling signIn and the page navigating away.
|
||||||
|
const firedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!error || firedRef.current) return;
|
||||||
|
if (
|
||||||
|
error !== "RefreshAccessTokenError" &&
|
||||||
|
error !== "RefreshTokenMissing"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
firedRef.current = true;
|
||||||
|
const callbackUrl =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? window.location.pathname + window.location.search
|
||||||
|
: "/";
|
||||||
|
// signIn() POSTs the CSRF-protected provider endpoint, which then
|
||||||
|
// initiates a fresh OIDC flow; the stale session cookie is replaced
|
||||||
|
// atomically when the flow completes. Keycloak's own SSO cookie may
|
||||||
|
// still log the user in transparently if it hasn't also expired.
|
||||||
|
void signIn("keycloak", { callbackUrl });
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export type { SessionUser } from "./types";
|
export type { SessionUser } from "./types";
|
||||||
|
export { SessionExpirationGuard } from "./SessionExpirationGuard";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client-side hard navigation to the sign-in endpoint. Use when a
|
* Client-side hard navigation to the sign-in endpoint. Use when a
|
||||||
|
|||||||
@@ -1,9 +1,75 @@
|
|||||||
import NextAuth, { type NextAuthResult } from "next-auth";
|
import NextAuth, { type NextAuthResult } from "next-auth";
|
||||||
import Keycloak from "next-auth/providers/keycloak";
|
import Keycloak from "next-auth/providers/keycloak";
|
||||||
|
import type { JWT } from "next-auth/jwt";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import type { CreateAuthOptions, SessionUser } from "./types";
|
import type { CreateAuthOptions, SessionUser } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the Keycloak access token using the stored refresh_token. Returns
|
||||||
|
* an updated JWT with new accessToken/expiresAt/refreshToken/idToken, or
|
||||||
|
* the original JWT marked with `error: "RefreshAccessTokenError"` if the
|
||||||
|
* refresh failed (caller surfaces this through the session so the client
|
||||||
|
* can re-trigger sign-in).
|
||||||
|
*
|
||||||
|
* Pre-refresh skew is handled by the caller; this function unconditionally
|
||||||
|
* exchanges the refresh_token.
|
||||||
|
*/
|
||||||
|
async function refreshAccessToken(
|
||||||
|
token: JWT,
|
||||||
|
keycloak: CreateAuthOptions["keycloak"],
|
||||||
|
): Promise<JWT> {
|
||||||
|
const refreshToken = (token as { refreshToken?: string }).refreshToken;
|
||||||
|
if (!refreshToken) {
|
||||||
|
return { ...token, error: "RefreshTokenMissing" };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${keycloak.issuer}/protocol/openid-connect/token`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
client_id: keycloak.clientId,
|
||||||
|
client_secret: keycloak.clientSecret,
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const body = (await res.json()) as {
|
||||||
|
access_token?: string;
|
||||||
|
expires_in?: number;
|
||||||
|
refresh_token?: string;
|
||||||
|
id_token?: string;
|
||||||
|
error?: string;
|
||||||
|
error_description?: string;
|
||||||
|
};
|
||||||
|
if (!res.ok || !body.access_token) {
|
||||||
|
console.error(
|
||||||
|
"@gsc/web-kit: refresh_token exchange failed",
|
||||||
|
res.status,
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
return { ...token, error: "RefreshAccessTokenError" };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...token,
|
||||||
|
accessToken: body.access_token,
|
||||||
|
expiresAt:
|
||||||
|
Math.floor(Date.now() / 1000) + (body.expires_in ?? 300),
|
||||||
|
// Keycloak rotates refresh tokens by default; fall back to the
|
||||||
|
// existing one when rotation is disabled at the realm.
|
||||||
|
refreshToken: body.refresh_token ?? refreshToken,
|
||||||
|
idToken: body.id_token ?? token.idToken,
|
||||||
|
error: undefined,
|
||||||
|
} as JWT;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("@gsc/web-kit: refresh_token exchange threw", err);
|
||||||
|
return { ...token, error: "RefreshAccessTokenError" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type { SessionUser, CreateAuthOptions };
|
export type { SessionUser, CreateAuthOptions };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,6 +128,35 @@ export interface AuthBundle {
|
|||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Decode the Keycloak access_token (JWS) and return its `realm_access.roles`.
|
||||||
|
* Realm roles are only present in the access_token, never in the OIDC
|
||||||
|
* userinfo `profile` — so callbacks that need to gate on roles must reach
|
||||||
|
* into `account.access_token`, not `profile`. We don't verify the signature
|
||||||
|
* here: NextAuth has already verified the ID token via the OIDC flow, the
|
||||||
|
* access_token came back over the same TLS exchange, and the only use of
|
||||||
|
* the decoded roles is local sign-in policy.
|
||||||
|
*/
|
||||||
|
function rolesFromAccessToken(accessToken: string | undefined): string[] {
|
||||||
|
if (!accessToken) return [];
|
||||||
|
const parts = accessToken.split(".");
|
||||||
|
if (parts.length < 2) return [];
|
||||||
|
try {
|
||||||
|
const payload = parts[1];
|
||||||
|
const padded = payload + "=".repeat((4 - (payload.length % 4)) % 4);
|
||||||
|
const decoded = Buffer.from(
|
||||||
|
padded.replace(/-/g, "+").replace(/_/g, "/"),
|
||||||
|
"base64",
|
||||||
|
).toString("utf-8");
|
||||||
|
const claims = JSON.parse(decoded) as {
|
||||||
|
realm_access?: { roles?: string[] };
|
||||||
|
};
|
||||||
|
return claims.realm_access?.roles ?? [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createAuth(opts: CreateAuthOptions): AuthBundle {
|
export function createAuth(opts: CreateAuthOptions): AuthBundle {
|
||||||
// NextAuth v5: provider-specific paths like /api/auth/signin/keycloak
|
// NextAuth v5: provider-specific paths like /api/auth/signin/keycloak
|
||||||
// are POST-only (CSRF-protected form submit). A GET redirect there
|
// are POST-only (CSRF-protected form submit). A GET redirect there
|
||||||
@@ -70,8 +165,6 @@ export function createAuth(opts: CreateAuthOptions): AuthBundle {
|
|||||||
// lists configured providers. Apps wanting one-click Keycloak can
|
// lists configured providers. Apps wanting one-click Keycloak can
|
||||||
// override signInPath with a custom page that calls signIn('keycloak').
|
// override signInPath with a custom page that calls signIn('keycloak').
|
||||||
const signInPath = opts.signInPath ?? "/api/auth/signin";
|
const signInPath = opts.signInPath ?? "/api/auth/signin";
|
||||||
const defaultTenantId =
|
|
||||||
opts.defaultTenantId ?? "00000000-0000-0000-0000-000000000000";
|
|
||||||
|
|
||||||
const na = NextAuth({
|
const na = NextAuth({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -82,35 +175,76 @@ export function createAuth(opts: CreateAuthOptions): AuthBundle {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
trustHost: true,
|
trustHost: true,
|
||||||
|
pages: opts.pages,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
|
async signIn({ account }) {
|
||||||
|
// Per-client access gate. The kit's consumers pass a predicate
|
||||||
|
// (e.g. "must be in gsccrm_* group") via `requireClientRole`; we
|
||||||
|
// run it against the access_token's `realm_access.roles`. Realm
|
||||||
|
// roles are NOT present in the OIDC userinfo `profile`, only in
|
||||||
|
// the access_token — so we decode it. Return false → NextAuth
|
||||||
|
// refuses to mint a session and bounces the user to the configured
|
||||||
|
// error page with `?error=AccessDenied`.
|
||||||
|
if (!opts.requireClientRole) return true;
|
||||||
|
const roles = rolesFromAccessToken(account?.access_token);
|
||||||
|
return opts.requireClientRole(roles);
|
||||||
|
},
|
||||||
async jwt({ token, account, profile }) {
|
async jwt({ token, account, profile }) {
|
||||||
// `account` + `profile` are only set on the initial sign-in.
|
// Initial sign-in: capture identity claims + refresh material.
|
||||||
// On subsequent calls the token is read from the encrypted JWT
|
|
||||||
// cookie, so the fields we set here persist for the session.
|
|
||||||
if (account && profile) {
|
if (account && profile) {
|
||||||
|
// Realm convention: GSC identity ships as three claims sourced
|
||||||
|
// from FreeIPA user attributes via the gosecCloud realm's LDAP
|
||||||
|
// federation + per-client OIDC protocol mappers.
|
||||||
|
const p = profile as {
|
||||||
|
gscTenantId?: string;
|
||||||
|
gscCustomerId?: string;
|
||||||
|
gscSID?: string;
|
||||||
|
};
|
||||||
|
if (!p.gscTenantId || !p.gscCustomerId || !p.gscSID) {
|
||||||
|
throw new Error(
|
||||||
|
`Keycloak token missing required GSC identity claims (gscTenantId=${!!p.gscTenantId}, gscCustomerId=${!!p.gscCustomerId}, gscSID=${!!p.gscSID})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
token.keycloakId = profile.sub;
|
token.keycloakId = profile.sub;
|
||||||
token.tenantId =
|
token.tenantId = p.gscTenantId;
|
||||||
(profile as { tenant_id?: string }).tenant_id ?? defaultTenantId;
|
token.customerId = p.gscCustomerId;
|
||||||
|
token.gscSid = p.gscSID;
|
||||||
token.displayName =
|
token.displayName =
|
||||||
profile.name ??
|
profile.name ??
|
||||||
(profile as { preferred_username?: string }).preferred_username ??
|
(profile as { preferred_username?: string }).preferred_username ??
|
||||||
"";
|
"";
|
||||||
token.givenName = profile.given_name ?? "";
|
token.givenName = profile.given_name ?? "";
|
||||||
token.familyName = profile.family_name ?? "";
|
token.familyName = profile.family_name ?? "";
|
||||||
token.roles =
|
// Realm roles are in the access_token, not the OIDC profile.
|
||||||
(profile as { realm_access?: { roles?: string[] } }).realm_access
|
token.roles = rolesFromAccessToken(account.access_token);
|
||||||
?.roles ?? [];
|
|
||||||
token.accessToken = account.access_token;
|
token.accessToken = account.access_token;
|
||||||
token.idToken = account.id_token;
|
token.idToken = account.id_token;
|
||||||
|
token.refreshToken = account.refresh_token;
|
||||||
|
// `expires_at` is Unix seconds (NextAuth's normalized form);
|
||||||
|
// fall back to `expires_in` (relative) if the provider omitted it.
|
||||||
|
token.expiresAt =
|
||||||
|
(account.expires_at as number | undefined) ??
|
||||||
|
Math.floor(Date.now() / 1000) +
|
||||||
|
((account.expires_in as number | undefined) ?? 300);
|
||||||
|
return token;
|
||||||
}
|
}
|
||||||
return token;
|
|
||||||
|
// Subsequent calls: refresh if the access token is about to expire.
|
||||||
|
// Skew of 30s avoids handing out a token that will time out mid-flight.
|
||||||
|
const expiresAt = (token as { expiresAt?: number }).expiresAt ?? 0;
|
||||||
|
if (Math.floor(Date.now() / 1000) < expiresAt - 30) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
return refreshAccessToken(token, opts.keycloak);
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
if (!token) return session;
|
if (!token) return session;
|
||||||
const user: SessionUser = {
|
const user: SessionUser = {
|
||||||
id: token.sub as string,
|
id: token.sub as string,
|
||||||
keycloakId: (token.keycloakId as string) ?? (token.sub as string),
|
keycloakId: (token.keycloakId as string) ?? (token.sub as string),
|
||||||
tenantId: (token.tenantId as string) ?? defaultTenantId,
|
tenantId: token.tenantId as string,
|
||||||
|
customerId: token.customerId as string,
|
||||||
|
gscSid: token.gscSid as string,
|
||||||
email: session.user?.email ?? "",
|
email: session.user?.email ?? "",
|
||||||
displayName: (token.displayName as string) ?? "",
|
displayName: (token.displayName as string) ?? "",
|
||||||
givenName: (token.givenName as string) ?? "",
|
givenName: (token.givenName as string) ?? "",
|
||||||
@@ -118,6 +252,7 @@ export function createAuth(opts: CreateAuthOptions): AuthBundle {
|
|||||||
roles: (token.roles as string[]) ?? [],
|
roles: (token.roles as string[]) ?? [],
|
||||||
accessToken: (token.accessToken as string) ?? "",
|
accessToken: (token.accessToken as string) ?? "",
|
||||||
idToken: token.idToken as string | undefined,
|
idToken: token.idToken as string | undefined,
|
||||||
|
error: (token as { error?: string }).error,
|
||||||
};
|
};
|
||||||
// NextAuth's default `session.user` type is narrow; we replace
|
// NextAuth's default `session.user` type is narrow; we replace
|
||||||
// it wholesale with our canonical shape. The cast keeps TS quiet
|
// it wholesale with our canonical shape. The cast keeps TS quiet
|
||||||
@@ -131,7 +266,11 @@ export function createAuth(opts: CreateAuthOptions): AuthBundle {
|
|||||||
async function requireAuth(): Promise<SessionUser> {
|
async function requireAuth(): Promise<SessionUser> {
|
||||||
const session = await na.auth();
|
const session = await na.auth();
|
||||||
const user = (session as unknown as { user?: SessionUser } | null)?.user;
|
const user = (session as unknown as { user?: SessionUser } | null)?.user;
|
||||||
if (!user || !user.accessToken) {
|
// `user.error` is set when the kit's silent refresh failed (refresh
|
||||||
|
// token expired/revoked or Keycloak rejected the exchange). The
|
||||||
|
// session cookie is still present, so the middleware won't catch
|
||||||
|
// this — bouncing here is what forces a fresh sign-in flow.
|
||||||
|
if (!user || !user.accessToken || user.error) {
|
||||||
redirect(signInPath);
|
redirect(signInPath);
|
||||||
}
|
}
|
||||||
return user;
|
return user;
|
||||||
|
|||||||
@@ -7,8 +7,16 @@ export interface SessionUser {
|
|||||||
id: string;
|
id: string;
|
||||||
/** Same as `id`. Exposed separately so callers can name-distinguish. */
|
/** Same as `id`. Exposed separately so callers can name-distinguish. */
|
||||||
keycloakId: string;
|
keycloakId: string;
|
||||||
/** Tenant the user is operating in. May fall back to defaultTenantId. */
|
/** Tenant the user is operating in. From the Keycloak `gscTenantId` claim. */
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
|
/** Customer the user belongs to. From the Keycloak `gscCustomerId` claim. */
|
||||||
|
customerId: string;
|
||||||
|
/**
|
||||||
|
* GSC composite security identifier (tenant + customer + user parts).
|
||||||
|
* From the Keycloak `gscSID` claim. Distinct from any Keycloak/OIDC
|
||||||
|
* session `sid`.
|
||||||
|
*/
|
||||||
|
gscSid: string;
|
||||||
email: string;
|
email: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
givenName: string;
|
givenName: string;
|
||||||
@@ -19,6 +27,13 @@ export interface SessionUser {
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
/** Keycloak ID token. Optional; only present if the IdP returns it. */
|
/** Keycloak ID token. Optional; only present if the IdP returns it. */
|
||||||
idToken?: string;
|
idToken?: string;
|
||||||
|
/**
|
||||||
|
* Set when the kit's silent refresh failed (refresh_token expired or
|
||||||
|
* revoked). Clients should treat this as a forced re-auth signal —
|
||||||
|
* any value here means the accessToken is stale and won't be refreshed
|
||||||
|
* by the kit on its own.
|
||||||
|
*/
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,12 +45,34 @@ export interface CreateAuthOptions {
|
|||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
issuer: string;
|
issuer: string;
|
||||||
};
|
};
|
||||||
/** Used when Keycloak claims don't carry a `tenant_id`. */
|
|
||||||
defaultTenantId?: string;
|
|
||||||
/**
|
/**
|
||||||
* Where `requireAuth()` / the middleware redirects unauthenticated
|
* Where `requireAuth()` / the middleware redirects unauthenticated
|
||||||
* users. Default: `/api/auth/signin/keycloak` (NextAuth's auto-redirect
|
* users. Default: `/api/auth/signin/keycloak` (NextAuth's auto-redirect
|
||||||
* endpoint). Override if the app has a custom landing page.
|
* endpoint). Override if the app has a custom landing page.
|
||||||
*/
|
*/
|
||||||
signInPath?: string;
|
signInPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional gate: predicate that runs on initial sign-in against the
|
||||||
|
* Keycloak `realm_access.roles` array. Return `true` to allow the
|
||||||
|
* sign-in, `false` to deny — denial bounces the user to NextAuth's
|
||||||
|
* error route with `?error=AccessDenied` and no session is created.
|
||||||
|
*
|
||||||
|
* Use this to enforce "must be in <app>_* group" semantics without
|
||||||
|
* relying solely on per-endpoint role gates in downstream services.
|
||||||
|
* Example for gscCRM:
|
||||||
|
* requireClientRole: (roles) => roles.some((r) => r.startsWith("gsccrm_"))
|
||||||
|
*/
|
||||||
|
requireClientRole?: (roles: string[]) => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NextAuth page overrides. Set `pages.error` to a custom branded
|
||||||
|
* full-page message route when `requireClientRole` may deny sign-in,
|
||||||
|
* so users see something meaningful instead of NextAuth's default
|
||||||
|
* error page. The route should be public (skipped by auth middleware).
|
||||||
|
*/
|
||||||
|
pages?: {
|
||||||
|
error?: string;
|
||||||
|
signIn?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
1152
src/chrome/AdminShell.tsx
Normal file
1152
src/chrome/AdminShell.tsx
Normal file
File diff suppressed because it is too large
Load Diff
45
src/chrome/LogoutButton.tsx
Normal file
45
src/chrome/LogoutButton.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default flow: a plain anchor to `/api/auth/signout` (overridable via
|
||||||
|
* the `signoutPath` prop). The route handler runs RP-initiated logout
|
||||||
|
* in a single redirect — kills the NextAuth cookie, ends the Keycloak
|
||||||
|
* SSO session, lands on the app's signed-out page.
|
||||||
|
*
|
||||||
|
* Apps with a different shape (no `/api/auth/signout`, need to fire
|
||||||
|
* custom telemetry, etc.) pass `onSignOut` to replace the navigation
|
||||||
|
* with a button + custom handler.
|
||||||
|
*/
|
||||||
|
type LogoutButtonProps = {
|
||||||
|
label: string;
|
||||||
|
signoutPath?: string;
|
||||||
|
onSignOut?: () => void | Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function LogoutButton({
|
||||||
|
label,
|
||||||
|
signoutPath = "/api/auth/signout",
|
||||||
|
onSignOut,
|
||||||
|
}: LogoutButtonProps) {
|
||||||
|
if (onSignOut) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
void onSignOut();
|
||||||
|
}}
|
||||||
|
className="dropdown-item"
|
||||||
|
>
|
||||||
|
<i className="ph-sign-out me-2"></i>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href={signoutPath} className="dropdown-item">
|
||||||
|
<i className="ph-sign-out me-2"></i>
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
src/chrome/_ambient.d.ts
vendored
Normal file
17
src/chrome/_ambient.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Ambient declarations for peer dependencies we don't install in the kit.
|
||||||
|
// `@gsc/chat` is provided by the consuming app — declared here so the kit
|
||||||
|
// can typecheck against the surface we use without owning a hard dep.
|
||||||
|
|
||||||
|
declare module "@gsc/chat" {
|
||||||
|
import type { FC } from "react";
|
||||||
|
|
||||||
|
export const ChatBubble: FC<{
|
||||||
|
isOpen: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const ChatOverlay: FC<{
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
37
src/chrome/brandIcons.ts
Normal file
37
src/chrome/brandIcons.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Returns a Next.js Metadata `icons` object derived from a Brand.
|
||||||
|
// Apps drop this into their root layout's `metadata` export to ship
|
||||||
|
// the brand logo as the favicon — fixes the "favicon 404" every
|
||||||
|
// consumer of the kit was shipping with.
|
||||||
|
//
|
||||||
|
// import type { Metadata } from "next";
|
||||||
|
// import { brandIcons } from "@gsc/web-kit/chrome";
|
||||||
|
// import { brand } from "@/config/brand";
|
||||||
|
//
|
||||||
|
// export const metadata: Metadata = {
|
||||||
|
// title: brand.product,
|
||||||
|
// icons: brandIcons(brand),
|
||||||
|
// };
|
||||||
|
|
||||||
|
import type { Brand } from "./types";
|
||||||
|
|
||||||
|
interface BrandMetaIcons {
|
||||||
|
icon: { url: string; type?: string }[];
|
||||||
|
shortcut?: { url: string; type?: string }[];
|
||||||
|
apple?: { url: string; type?: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function brandIcons(brand: Brand): BrandMetaIcons {
|
||||||
|
const url = brand.faviconUrl ?? brand.logoUrl;
|
||||||
|
// Heuristic: trust the file extension to set the MIME type. Most
|
||||||
|
// brand logos in the GoSec assets bucket are SVG.
|
||||||
|
const type =
|
||||||
|
/\.svg(\?|$)/i.test(url) ? "image/svg+xml" :
|
||||||
|
/\.png(\?|$)/i.test(url) ? "image/png" :
|
||||||
|
/\.ico(\?|$)/i.test(url) ? "image/x-icon" :
|
||||||
|
undefined;
|
||||||
|
return {
|
||||||
|
icon: [{ url, ...(type ? { type } : {}) }],
|
||||||
|
shortcut: [{ url }],
|
||||||
|
apple: [{ url }],
|
||||||
|
};
|
||||||
|
}
|
||||||
110
src/chrome/header/BrowseApps.tsx
Normal file
110
src/chrome/header/BrowseApps.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { AppListItem, ChromeLabels } from "../types";
|
||||||
|
|
||||||
|
type BrowseAppsProps = {
|
||||||
|
show?: boolean;
|
||||||
|
apps: AppListItem[];
|
||||||
|
viewAllUrl?: string; // default "/apps"
|
||||||
|
labels: Pick<ChromeLabels, "browseApps" | "viewAll">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function BrowseApps({ show = true, apps, viewAllUrl = "/apps", labels }: BrowseAppsProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (open) setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enabled = apps.filter((a) => a.enabled).sort((a, b) => a.sortOrder - b.sortOrder);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile search icon (always rendered alongside browse-apps trigger) */}
|
||||||
|
<li className="nav-item d-lg-none">
|
||||||
|
<Link
|
||||||
|
href="#navbar_search"
|
||||||
|
className="navbar-nav-link navbar-nav-link-icon rounded-pill"
|
||||||
|
>
|
||||||
|
<i className="ph ph-magnifying-glass"></i>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{show && (
|
||||||
|
<li
|
||||||
|
className="nav-item nav-item-dropdown-lg dropdown"
|
||||||
|
onMouseLeave={close}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="#"
|
||||||
|
className="navbar-nav-link navbar-nav-link-icon rounded-pill"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(!open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="ph ph-squares-four"></i>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
open
|
||||||
|
? "dropdown-menu dropdown-menu-scrollable-sm wmin-lg-600 p-0 show"
|
||||||
|
: "dropdown-menu dropdown-menu-scrollable-sm wmin-lg-600 p-0"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="d-flex align-items-center border-bottom p-3">
|
||||||
|
<h6 className="mb-0">{labels.browseApps}</h6>
|
||||||
|
<Link href={viewAllUrl} className="ms-auto">
|
||||||
|
{labels.viewAll}
|
||||||
|
<i className="ph ph-arrow-circle-right ms-1"></i>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row row-cols-1 row-cols-sm-2 g-0">
|
||||||
|
{enabled.map((app, idx) => {
|
||||||
|
const isLast = idx === enabled.length - 1;
|
||||||
|
const isEvenColumn = idx % 2 === 0;
|
||||||
|
const itemClasses = [
|
||||||
|
"dropdown-item",
|
||||||
|
"text-wrap",
|
||||||
|
"h-100",
|
||||||
|
"align-items-start",
|
||||||
|
isEvenColumn ? "border-end-sm" : "",
|
||||||
|
isLast ? "" : "border-bottom",
|
||||||
|
"p-3",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="col" key={app.key}>
|
||||||
|
<Link href={app.url} className={itemClasses}>
|
||||||
|
<div>
|
||||||
|
{app.iconUrl ? (
|
||||||
|
<img src={app.iconUrl} className="h-40px mb-2" alt="" />
|
||||||
|
) : app.iconClass ? (
|
||||||
|
<div
|
||||||
|
className={`d-flex align-items-center justify-content-center h-40px w-40px rounded mb-2 ${app.iconBg ?? "bg-secondary-lt"}`}
|
||||||
|
>
|
||||||
|
<i className={`${app.iconClass} fs-2`}></i>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="fw-semibold my-1">{app.name}</div>
|
||||||
|
{app.description && (
|
||||||
|
<div className="text-muted">{app.description}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/chrome/header/HeaderContacts.tsx
Normal file
12
src/chrome/header/HeaderContacts.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// Placeholder for parity with gscAdmin's contacts header zone. Renders nothing
|
||||||
|
// today; an app may pass slots.pageHeaderExtras to render its own contacts UI.
|
||||||
|
type HeaderContactsProps = {
|
||||||
|
show?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function HeaderContacts({ show = true }: HeaderContactsProps) {
|
||||||
|
if (!show) return null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
98
src/chrome/header/HeaderCustomers.tsx
Normal file
98
src/chrome/header/HeaderCustomers.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export type CustomerOption = {
|
||||||
|
id: number | string;
|
||||||
|
name: string;
|
||||||
|
logo: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type HeaderCustomersProps = {
|
||||||
|
show?: boolean;
|
||||||
|
customers?: CustomerOption[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function HeaderCustomers({ show = true, customers = [] }: HeaderCustomersProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [selectedName, setSelectedName] = useState("");
|
||||||
|
const [selectedLogo, setSelectedLogo] = useState("");
|
||||||
|
|
||||||
|
const toggle = () => setOpen((v) => !v);
|
||||||
|
|
||||||
|
const select = (c: CustomerOption) => {
|
||||||
|
setSelectedName(c.name);
|
||||||
|
setSelectedLogo(c.logo);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!show || customers.length === 0) return null;
|
||||||
|
|
||||||
|
const current = customers[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dropdown w-100 w-sm-auto">
|
||||||
|
<Link
|
||||||
|
href="#"
|
||||||
|
className="d-flex align-items-center text-body lh-1 dropdown-toggle py-sm-2"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
toggle();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={selectedLogo || current.logo}
|
||||||
|
className="w-32px h-32px me-2"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<div className="me-auto me-lg-1">
|
||||||
|
<div className="fs-sm text-muted mb-1">Customer</div>
|
||||||
|
<div className="fw-semibold">{selectedName || current.name}</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
open
|
||||||
|
? "dropdown-menu dropdown-menu-lg-end w-100 w-lg-auto wmin-300 wmin-sm-350 pt-0 show"
|
||||||
|
: "dropdown-menu dropdown-menu-lg-end w-100 w-lg-auto wmin-300 wmin-sm-350 pt-0"
|
||||||
|
}
|
||||||
|
onMouseLeave={toggle}
|
||||||
|
>
|
||||||
|
<div className="d-flex align-items-center p-3">
|
||||||
|
<h6 className="fw-semibold mb-0">Customers</h6>
|
||||||
|
<Link href="#" className="ms-auto">
|
||||||
|
View all
|
||||||
|
<i className="ph ph-arrow-circle-right ms-1"></i>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{customers.map((c) => (
|
||||||
|
<Link
|
||||||
|
href="#"
|
||||||
|
className={
|
||||||
|
selectedName === c.name
|
||||||
|
? "dropdown-item py-2 active"
|
||||||
|
: "dropdown-item py-2"
|
||||||
|
}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
select(c);
|
||||||
|
}}
|
||||||
|
key={c.id}
|
||||||
|
>
|
||||||
|
<img src={c.logo} className="w-32px h-32px me-2" alt="" />
|
||||||
|
<div>
|
||||||
|
<div className="fw-semibold">{c.name}</div>
|
||||||
|
{c.description && (
|
||||||
|
<div className="fs-sm text-muted">{c.description}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/chrome/header/Messages.tsx
Normal file
24
src/chrome/header/Messages.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
type MessagesProps = {
|
||||||
|
show?: boolean;
|
||||||
|
onOpenChat?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Messages({ show = true, onOpenChat }: MessagesProps) {
|
||||||
|
if (!show) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="nav-item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="navbar-nav-link navbar-nav-link-icon rounded-pill border-0 bg-transparent position-relative"
|
||||||
|
title="Open chat"
|
||||||
|
aria-label="Open chat"
|
||||||
|
onClick={onOpenChat}
|
||||||
|
>
|
||||||
|
<i className="ph ph-chat-circle"></i>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
src/chrome/header/Search.tsx
Normal file
26
src/chrome/header/Search.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { SearchHistory } from "./SearchHistory";
|
||||||
|
import { SearchOptions } from "./SearchOptions";
|
||||||
|
|
||||||
|
type SearchProps = {
|
||||||
|
show?: boolean;
|
||||||
|
showHistory?: boolean;
|
||||||
|
showOptions?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Search({ show = true, showHistory = true, showOptions = true }: SearchProps) {
|
||||||
|
if (!show) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="navbar-collapse justify-content-center flex-lg-1 order-2 order-lg-1 collapse"
|
||||||
|
id="navbar_search"
|
||||||
|
>
|
||||||
|
<div className="navbar-search flex-fill position-relative mt-2 mt-lg-0 mx-lg-3">
|
||||||
|
{showHistory && <SearchHistory />}
|
||||||
|
{showOptions && <SearchOptions />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
src/chrome/header/SearchHistory.tsx
Normal file
39
src/chrome/header/SearchHistory.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function SearchHistory() {
|
||||||
|
const [showSearchHistory, setShowSearchHistory] = useState(false);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (showSearchHistory) setShowSearchHistory(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="form-control-feedback form-control-feedback-start flex-grow-1"
|
||||||
|
data-color-theme="dark"
|
||||||
|
onMouseLeave={close}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control bg-transparent rounded-pill"
|
||||||
|
placeholder="Search"
|
||||||
|
onClick={() => setShowSearchHistory(!showSearchHistory)}
|
||||||
|
/>
|
||||||
|
<div className="form-control-feedback-icon">
|
||||||
|
<i className="ph ph-magnifying-glass"></i>
|
||||||
|
</div>
|
||||||
|
{showSearchHistory && (
|
||||||
|
<div className="dropdown-menu w-100 show" data-color-theme="light">
|
||||||
|
<div className="dropdown-item text-muted">
|
||||||
|
<div className="text-center w-32px me-3">
|
||||||
|
<i className="ph ph-magnifying-glass"></i>
|
||||||
|
</div>
|
||||||
|
<span>Type to search...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
103
src/chrome/header/SearchOptions.tsx
Normal file
103
src/chrome/header/SearchOptions.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function SearchOptions() {
|
||||||
|
const [showSearchOptions, setShowSearchOptions] = useState(false);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (showSearchOptions) setShowSearchOptions(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
href="#"
|
||||||
|
className="navbar-nav-link align-items-center justify-content-center w-40px h-32px rounded-pill position-absolute end-0 top-50 translate-middle-y p-0 me-1"
|
||||||
|
onClick={() => setShowSearchOptions(!showSearchOptions)}
|
||||||
|
>
|
||||||
|
<i className="ph ph-faders-horizontal"></i>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
showSearchOptions
|
||||||
|
? "dropdown-menu w-100 p-3 show"
|
||||||
|
: "dropdown-menu w-100 p-3"
|
||||||
|
}
|
||||||
|
onMouseLeave={close}
|
||||||
|
>
|
||||||
|
<div className="d-flex align-items-center mb-3">
|
||||||
|
<h6 className="mb-0">Search options</h6>
|
||||||
|
<Link href="#" className="text-body rounded-pill ms-auto">
|
||||||
|
<i className="ph ph-clock-counter-clockwise"></i>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<span className="d-block form-label">Category</span>
|
||||||
|
<label className="form-check form-check-inline">
|
||||||
|
<input type="checkbox" className="form-check-input" />
|
||||||
|
<span className="form-check-label">Invoices</span>
|
||||||
|
</label>
|
||||||
|
<label className="form-check form-check-inline">
|
||||||
|
<input type="checkbox" className="form-check-input" />
|
||||||
|
<span className="form-check-label">Files</span>
|
||||||
|
</label>
|
||||||
|
<label className="form-check form-check-inline">
|
||||||
|
<input type="checkbox" className="form-check-input" />
|
||||||
|
<span className="form-check-label">Users</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="form-label">Addition</label>
|
||||||
|
<div className="input-group">
|
||||||
|
<select className="form-select w-auto flex-grow-0">
|
||||||
|
<option value="1">has</option>
|
||||||
|
<option value="2">has not</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Enter the word(s)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="form-label">Status</label>
|
||||||
|
<div className="input-group">
|
||||||
|
<select className="form-select w-auto flex-grow-0">
|
||||||
|
<option value="1">is</option>
|
||||||
|
<option value="2">is not</option>
|
||||||
|
</select>
|
||||||
|
<select className="form-select">
|
||||||
|
<option value="1">Active</option>
|
||||||
|
<option value="2">Inactive</option>
|
||||||
|
<option value="3">New</option>
|
||||||
|
<option value="4">Expired</option>
|
||||||
|
<option value="5">Pending</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="d-flex">
|
||||||
|
<button type="button" className="btn btn-light">
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="ms-auto">
|
||||||
|
<button type="button" className="btn btn-light">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-primary ms-2">
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/chrome/header/index.ts
Normal file
7
src/chrome/header/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export { Search } from "./Search";
|
||||||
|
export { SearchHistory } from "./SearchHistory";
|
||||||
|
export { SearchOptions } from "./SearchOptions";
|
||||||
|
export { Messages } from "./Messages";
|
||||||
|
export { BrowseApps } from "./BrowseApps";
|
||||||
|
export { HeaderContacts } from "./HeaderContacts";
|
||||||
|
export { HeaderCustomers, type CustomerOption } from "./HeaderCustomers";
|
||||||
26
src/chrome/index.ts
Normal file
26
src/chrome/index.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export { AdminShell } from "./AdminShell";
|
||||||
|
export { LogoutButton } from "./LogoutButton";
|
||||||
|
export {
|
||||||
|
Search,
|
||||||
|
SearchHistory,
|
||||||
|
SearchOptions,
|
||||||
|
Messages,
|
||||||
|
BrowseApps,
|
||||||
|
HeaderContacts,
|
||||||
|
HeaderCustomers,
|
||||||
|
type CustomerOption,
|
||||||
|
} from "./header";
|
||||||
|
export { useChromeLabels, DEFAULT_CHROME_LABELS } from "./labels";
|
||||||
|
export { brandIcons } from "./brandIcons";
|
||||||
|
export type {
|
||||||
|
AdminShellProps,
|
||||||
|
ActivityFeedItem,
|
||||||
|
AppListItem,
|
||||||
|
Brand,
|
||||||
|
ChromeFeatures,
|
||||||
|
ChromeLabels,
|
||||||
|
ChromeSlots,
|
||||||
|
ChromeUser,
|
||||||
|
DbMenuItem,
|
||||||
|
MenuData,
|
||||||
|
} from "./types";
|
||||||
73
src/chrome/labels.ts
Normal file
73
src/chrome/labels.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import type { ChromeLabels } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default English chrome labels. Used as fallback when next-intl namespace
|
||||||
|
* "chrome" doesn't resolve a key and no per-key override is passed in props.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_CHROME_LABELS: ChromeLabels = {
|
||||||
|
navigation: "Navigation",
|
||||||
|
main: "MAIN",
|
||||||
|
dashboard: "Dashboard",
|
||||||
|
support: "Support",
|
||||||
|
settings: "Settings",
|
||||||
|
allSettings: "All Settings",
|
||||||
|
logout: "Logout",
|
||||||
|
myProfile: "My Profile",
|
||||||
|
docs: "Docs",
|
||||||
|
browseApps: "Browse apps",
|
||||||
|
viewAll: "View all",
|
||||||
|
activityTitle: "Activity",
|
||||||
|
newNotifications: "New notifications",
|
||||||
|
olderNotifications: "Older notifications",
|
||||||
|
noOlderNotifications: "No older notifications",
|
||||||
|
noNewNotifications: "No new notifications",
|
||||||
|
expandSidebar: "Expand sidebar",
|
||||||
|
collapseSidebar: "Collapse sidebar",
|
||||||
|
closeSidebar: "Close sidebar",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve chrome labels with three-level precedence:
|
||||||
|
* 1. `overrides` prop (per-key escape hatch)
|
||||||
|
* 2. next-intl namespace "chrome" message
|
||||||
|
* 3. hardcoded English default
|
||||||
|
*
|
||||||
|
* Apps add `"chrome": { ... }` to their next-intl messages to translate.
|
||||||
|
*/
|
||||||
|
export function useChromeLabels(overrides?: Partial<ChromeLabels>): ChromeLabels {
|
||||||
|
let t: ((key: string) => string) | null = null;
|
||||||
|
try {
|
||||||
|
t = useTranslations("chrome");
|
||||||
|
} catch {
|
||||||
|
t = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = { ...DEFAULT_CHROME_LABELS };
|
||||||
|
|
||||||
|
if (t) {
|
||||||
|
for (const key of Object.keys(resolved) as (keyof ChromeLabels)[]) {
|
||||||
|
try {
|
||||||
|
const v = t(key);
|
||||||
|
// next-intl returns the key path when the message is missing — treat
|
||||||
|
// that as "no translation" so we fall back to the English default.
|
||||||
|
if (v && v !== key && !v.startsWith("chrome.")) {
|
||||||
|
resolved[key] = v;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Missing key — keep the default.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides) {
|
||||||
|
for (const k of Object.keys(overrides) as (keyof ChromeLabels)[]) {
|
||||||
|
const v = overrides[k];
|
||||||
|
if (v != null) resolved[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
190
src/chrome/types.ts
Normal file
190
src/chrome/types.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Menu data (Prisma-shaped, returned by app's getMenuItemsByType())
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type DbMenuItem = {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
translationKey: string;
|
||||||
|
url: string;
|
||||||
|
icon: string | null;
|
||||||
|
sortOrder: number;
|
||||||
|
isActive: boolean;
|
||||||
|
isSystemRequired: boolean;
|
||||||
|
children?: DbMenuItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MenuData = {
|
||||||
|
sidebar: DbMenuItem[];
|
||||||
|
topbar: DbMenuItem[];
|
||||||
|
subbar: DbMenuItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Browse-apps panel data (returned by app's getApps())
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type AppListItem = {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
url: string;
|
||||||
|
iconClass?: string | null; // e.g. "ph-phone"
|
||||||
|
iconUrl?: string | null; // image URL (preferred when set)
|
||||||
|
iconBg?: string | null; // background utility class for iconClass square
|
||||||
|
sortOrder: number;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Activity panel data (when features.activityPanel:true)
|
||||||
|
// Optional — apps may pass `slots.activityPanel` to render their own body.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type ActivityFeedItem = {
|
||||||
|
id: string;
|
||||||
|
actorName: string;
|
||||||
|
actorAvatarUrl?: string;
|
||||||
|
message: ReactNode; // freeform body, may contain JSX
|
||||||
|
timestamp: string; // pre-formatted display string (e.g. "2 hours ago")
|
||||||
|
group?: "new" | "older"; // default "new"
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Brand (replaces site-informations.json)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type Brand = {
|
||||||
|
name: string; // "CRM"
|
||||||
|
product: string; // "GoSec CRM"
|
||||||
|
logoUrl: string; // full navbar logo
|
||||||
|
logoSmallUrl?: string; // optional compact logo
|
||||||
|
faviconUrl?: string; // optional favicon override (defaults to logoUrl)
|
||||||
|
websiteUrl: string; // footer brand link
|
||||||
|
supportUrl: string; // subbar Support + footer Support link
|
||||||
|
docsUrl: string; // footer docs link
|
||||||
|
copyrightStartYear: number; // e.g. 2023
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// User
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type ChromeUser = {
|
||||||
|
displayName: string;
|
||||||
|
email?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
roles?: string[]; // future: role-gated menu filtering
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Feature toggles
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type ChromeFeatures = {
|
||||||
|
// Navbar
|
||||||
|
search?: boolean; // default true
|
||||||
|
searchHistory?: boolean; // default true
|
||||||
|
searchOptions?: boolean; // default true
|
||||||
|
browseApps?: boolean; // default true
|
||||||
|
messages?: boolean; // default true
|
||||||
|
notifications?: boolean; // default true
|
||||||
|
// Subbar
|
||||||
|
subbar?: boolean; // default true
|
||||||
|
subbarSupport?: boolean; // default true
|
||||||
|
subbarSettings?: boolean; // default true
|
||||||
|
// Page header
|
||||||
|
pageHeader?: boolean; // default true
|
||||||
|
pageHeaderCustomers?: boolean; // default false (CRM-specific)
|
||||||
|
pageHeaderContacts?: boolean; // default false (CRM-specific)
|
||||||
|
// Overlays
|
||||||
|
activityPanel?: boolean; // default false
|
||||||
|
chat?: boolean; // default false
|
||||||
|
// Footer
|
||||||
|
footer?: boolean; // default true
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Slots (app-rendered content; overrides built-in renderers)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type ChromeSlots = {
|
||||||
|
pageTitle?: ReactNode;
|
||||||
|
pageHeaderExtras?: ReactNode;
|
||||||
|
subbarExtras?: ReactNode;
|
||||||
|
activityPanel?: ReactNode; // overrides built-in activity body
|
||||||
|
navbarExtras?: ReactNode;
|
||||||
|
footerExtras?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// i18n labels (chrome strings, overridable per-key)
|
||||||
|
// Defaults shipped in labels.ts; apps may also set keys under next-intl
|
||||||
|
// namespace "chrome".
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type ChromeLabels = {
|
||||||
|
navigation: string; // sidebar header "Navigation"
|
||||||
|
main: string; // sidebar group header "MAIN"
|
||||||
|
dashboard: string; // default page title for "/"
|
||||||
|
support: string; // subbar / footer "Support"
|
||||||
|
settings: string; // subbar "Settings"
|
||||||
|
allSettings: string; // subbar dropdown "All Settings"
|
||||||
|
logout: string; // user dropdown "Logout"
|
||||||
|
myProfile: string; // user dropdown "My Profile" (cross-app link to gscMy)
|
||||||
|
docs: string; // footer "Docs"
|
||||||
|
browseApps: string; // browse-apps "Browse apps"
|
||||||
|
viewAll: string; // browse-apps "View all"
|
||||||
|
activityTitle: string; // activity panel header
|
||||||
|
newNotifications: string; // activity panel "New notifications"
|
||||||
|
olderNotifications: string; // activity panel "Older notifications"
|
||||||
|
noOlderNotifications: string; // activity panel empty older state
|
||||||
|
noNewNotifications: string; // activity panel empty new state
|
||||||
|
expandSidebar: string; // aria-label
|
||||||
|
collapseSidebar: string; // aria-label
|
||||||
|
closeSidebar: string; // aria-label
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// AdminShell props
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type AdminShellProps = {
|
||||||
|
// Data
|
||||||
|
menus: MenuData;
|
||||||
|
apps: AppListItem[];
|
||||||
|
user: ChromeUser;
|
||||||
|
notificationCount?: number;
|
||||||
|
activity?: ActivityFeedItem[];
|
||||||
|
|
||||||
|
// Brand (required)
|
||||||
|
brand: Brand;
|
||||||
|
|
||||||
|
// Feature toggles
|
||||||
|
features?: ChromeFeatures;
|
||||||
|
|
||||||
|
// Slots
|
||||||
|
slots?: ChromeSlots;
|
||||||
|
|
||||||
|
// Behavior
|
||||||
|
onSignOut?: () => void | Promise<void>;
|
||||||
|
/**
|
||||||
|
* Override the path the LogoutButton anchor points at. Defaults to
|
||||||
|
* `/api/auth/signout` — the canonical RP-initiated logout endpoint
|
||||||
|
* (single redirect: kills NextAuth cookie + ends Keycloak SSO + lands
|
||||||
|
* on the app's signed-out page).
|
||||||
|
*/
|
||||||
|
signoutPath?: string;
|
||||||
|
/**
|
||||||
|
* URL the "My Profile" item in the user dropdown points at. Defaults
|
||||||
|
* to the canonical gscMy profile page (`https://my.gosec.internal/profile`).
|
||||||
|
* gscMy itself should override this to its own local `/profile` route.
|
||||||
|
* Pass `null` to omit the item entirely.
|
||||||
|
*/
|
||||||
|
myProfileUrl?: string | null;
|
||||||
|
labels?: Partial<ChromeLabels>;
|
||||||
|
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
@@ -59,7 +59,6 @@ export {
|
|||||||
useValidation,
|
useValidation,
|
||||||
useFieldValidation,
|
useFieldValidation,
|
||||||
useAddressAutocomplete,
|
useAddressAutocomplete,
|
||||||
loadGoogleMapsScript,
|
|
||||||
} from "@limitless/ui";
|
} from "@limitless/ui";
|
||||||
|
|
||||||
// Validation — format validators
|
// Validation — format validators
|
||||||
|
|||||||
7
src/i18n/index.ts
Normal file
7
src/i18n/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @gsc/web-kit/i18n — placeholder for future client-side i18n helpers
|
||||||
|
* (locale switcher component, etc.). The server-side factory lives in
|
||||||
|
* `@gsc/web-kit/i18n/server` so consumer client bundles don't pull in
|
||||||
|
* next/headers and next-intl/server.
|
||||||
|
*/
|
||||||
|
export {};
|
||||||
140
src/i18n/server.ts
Normal file
140
src/i18n/server.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* @gsc/web-kit/i18n/server — next-intl request config factory.
|
||||||
|
*
|
||||||
|
* Resolves the active locale from:
|
||||||
|
* 1. `NEXT_LOCALE` cookie (set by an in-app language switcher; beats
|
||||||
|
* the token claim until the next refresh).
|
||||||
|
* 2. Keycloak access-token `preferred_language` claim, sourced from
|
||||||
|
* FreeIPA's `preferredLanguage` user attribute via the gosecCloud
|
||||||
|
* realm's `preferred-language` LDAP mapper + per-client OIDC
|
||||||
|
* `oidc-usermodel-attribute-mapper`.
|
||||||
|
* 3. `Accept-Language` request header.
|
||||||
|
* 4. `defaultLocale`.
|
||||||
|
*
|
||||||
|
* Mirrors `createAuth()` from `@gsc/web-kit/auth/server`: one factory
|
||||||
|
* call, app supplies the locale list and message loader, kit owns the
|
||||||
|
* resolution chain.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // src/i18n/request.ts
|
||||||
|
* import { createI18nConfig } from "@gsc/web-kit/i18n/server";
|
||||||
|
* import { auth } from "@/auth";
|
||||||
|
*
|
||||||
|
* export const locales = ["en", "de", "fr"] as const;
|
||||||
|
* export const defaultLocale = "en" as const;
|
||||||
|
*
|
||||||
|
* export default createI18nConfig({
|
||||||
|
* locales,
|
||||||
|
* defaultLocale,
|
||||||
|
* getAccessToken: async () =>
|
||||||
|
* (await auth())?.user?.accessToken as string | undefined,
|
||||||
|
* loadMessages: (locale) =>
|
||||||
|
* import(`../../public/locales/${locale}/common.json`).then((m) => m.default),
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
import { getRequestConfig } from "next-intl/server";
|
||||||
|
import { cookies, headers } from "next/headers";
|
||||||
|
|
||||||
|
export interface CreateI18nOptions<L extends string> {
|
||||||
|
/** Supported locale codes. First entry is conventional UI fallback order. */
|
||||||
|
locales: readonly L[];
|
||||||
|
/** Locale used when nothing in the resolution chain matches. */
|
||||||
|
defaultLocale: L;
|
||||||
|
/**
|
||||||
|
* Return the current user's Keycloak access token, or undefined if no
|
||||||
|
* session. Typically `async () => (await auth())?.user?.accessToken`.
|
||||||
|
* The kit doesn't import the app's auth module — pass it explicitly so
|
||||||
|
* the kit stays decoupled from how the app wires NextAuth.
|
||||||
|
*/
|
||||||
|
getAccessToken?: () => Promise<string | undefined>;
|
||||||
|
/** Load the message bundle for a resolved locale. */
|
||||||
|
loadMessages: (locale: L) => Promise<Record<string, unknown>>;
|
||||||
|
/**
|
||||||
|
* JWT claim name carrying the user's preferred language. Default
|
||||||
|
* `preferred_language` — matches the gosecCloud per-client OIDC mapper
|
||||||
|
* named `preferredLanguage` (config `claim.name=preferred_language`).
|
||||||
|
*/
|
||||||
|
claimName?: string;
|
||||||
|
/** Cookie name carrying a recent in-app language switch. Default `NEXT_LOCALE`. */
|
||||||
|
cookieName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeAccessTokenClaim(
|
||||||
|
accessToken: string,
|
||||||
|
claimName: string,
|
||||||
|
): string | undefined {
|
||||||
|
// Plain base64url-decode of the JWT payload. We don't verify here:
|
||||||
|
// NextAuth already verified the ID token via OIDC, the access token
|
||||||
|
// came back over the same TLS exchange, and the only use is a local
|
||||||
|
// locale preference. Same approach as `rolesFromAccessToken` in
|
||||||
|
// auth/server.ts.
|
||||||
|
const parts = accessToken.split(".");
|
||||||
|
if (parts.length < 2) return undefined;
|
||||||
|
try {
|
||||||
|
const payload = parts[1];
|
||||||
|
const padded = payload + "=".repeat((4 - (payload.length % 4)) % 4);
|
||||||
|
const decoded = Buffer.from(
|
||||||
|
padded.replace(/-/g, "+").replace(/_/g, "/"),
|
||||||
|
"base64",
|
||||||
|
).toString("utf-8");
|
||||||
|
const claims = JSON.parse(decoded) as Record<string, unknown>;
|
||||||
|
const value = claims[claimName];
|
||||||
|
return typeof value === "string" ? value : undefined;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createI18nConfig<L extends string>(opts: CreateI18nOptions<L>) {
|
||||||
|
const claimName = opts.claimName ?? "preferred_language";
|
||||||
|
const cookieName = opts.cookieName ?? "NEXT_LOCALE";
|
||||||
|
const localeSet = new Set<string>(opts.locales);
|
||||||
|
const isLocale = (v: unknown): v is L =>
|
||||||
|
typeof v === "string" && localeSet.has(v);
|
||||||
|
|
||||||
|
return getRequestConfig(async () => {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const cookieLocale = cookieStore.get(cookieName)?.value;
|
||||||
|
|
||||||
|
let locale: L | undefined = isLocale(cookieLocale) ? cookieLocale : undefined;
|
||||||
|
|
||||||
|
if (!locale && opts.getAccessToken) {
|
||||||
|
try {
|
||||||
|
const token = await opts.getAccessToken();
|
||||||
|
if (token) {
|
||||||
|
const claim = decodeAccessTokenClaim(token, claimName);
|
||||||
|
if (isLocale(claim)) locale = claim;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Session read failed (e.g. missing cookie outside a request
|
||||||
|
// scope). Fall through to Accept-Language.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!locale) {
|
||||||
|
const headersList = await headers();
|
||||||
|
const acceptLanguage = headersList.get("accept-language");
|
||||||
|
if (acceptLanguage) {
|
||||||
|
const candidate = acceptLanguage.split(",")[0]?.split("-")[0];
|
||||||
|
if (isLocale(candidate)) locale = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!locale) locale = opts.defaultLocale;
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
messages: await opts.loadMessages(locale),
|
||||||
|
onError(error) {
|
||||||
|
if (error.code === "MISSING_MESSAGE") {
|
||||||
|
console.warn(error.message);
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getMessageFallback({ key }) {
|
||||||
|
return key;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
export * from "./layout/index";
|
export * from "./layout/index";
|
||||||
export * from "./shell/index";
|
export * from "./shell/index";
|
||||||
|
export * from "./chrome/index";
|
||||||
export * from "./data/index";
|
export * from "./data/index";
|
||||||
export * from "./forms/index";
|
export * from "./forms/index";
|
||||||
export * from "./feedback/index";
|
export * from "./feedback/index";
|
||||||
|
|||||||
Reference in New Issue
Block a user