Tiny Go service that returns ShellConfig JSON for any registered app.
Backs the runtime-loaded <AppShell> component being added to
@limitless/ui (next).
Endpoints:
GET /api/v1/shell/{appKey} → app + branding + user + menus, ETag-cached
GET /api/v1/apps → registered app inventory
GET /healthz, /readyz → ops probes
Auth:
Keycloak Bearer JWT validated against the gosecCloud realm.
Discovery URL is overridable so pods can hit Keycloak via the
in-cluster service (https://keycloak.keycloak.svc.cluster.local:8443)
while still validating the canonical issuer (auth.gosec.cloud).
Lazy JWKS init — pod stays up if Keycloak is briefly unreachable.
Data model (gsc_core.shell):
apps · menu_items (zone enum: topbar/sidebar/footer/user-menu) ·
menu_role_grants (Keycloak realm roles, OR semantics, empty=all) ·
branding
Seed includes the 8 gsc-crm sidebar items + topbar search +
user-menu (settings/support/logout) + footer (docs).
K8s:
Namespace gsc-shell (ambient mesh).
Deployment 2 replicas, internal-only ingress shell-api.gosec.internal,
EJBCA SERVER cert.
ServiceEntry for auth.gosec.cloud (vestigial — discovery now uses
in-cluster path; keeping for ad-hoc curl from inside pods).
Added to keycloak/allow-keycloak-clients AuthorizationPolicy out of band.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
102 lines
4.0 KiB
PL/PgSQL
102 lines
4.0 KiB
PL/PgSQL
-- gsc-shell-api — initial schema.
|
|
--
|
|
-- Lives in `gsc_core` so it's reachable from every app that needs chrome data.
|
|
-- Owned by the `gsc_shell` role (created out of band).
|
|
--
|
|
-- Idempotent: every CREATE uses IF NOT EXISTS where possible.
|
|
|
|
CREATE SCHEMA IF NOT EXISTS shell;
|
|
|
|
-- Apps registry.
|
|
-- An "app" is a frontend that wants to render chrome from this service.
|
|
CREATE TABLE IF NOT EXISTS shell.apps (
|
|
app_key TEXT PRIMARY KEY CHECK (app_key ~ '^[a-z][a-z0-9-]*$'),
|
|
display_name TEXT NOT NULL,
|
|
base_url TEXT NOT NULL,
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Menu zones. Mirrors what the existing AdminShell expects.
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'menu_zone' AND typnamespace = 'shell'::regnamespace) THEN
|
|
CREATE TYPE shell.menu_zone AS ENUM ('topbar', 'sidebar', 'footer', 'user-menu');
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Menu items. One row per nav entry per app per zone.
|
|
-- Hierarchical via parent_id (sidebar submenus, etc.).
|
|
CREATE TABLE IF NOT EXISTS shell.menu_items (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
app_key TEXT NOT NULL REFERENCES shell.apps(app_key) ON DELETE CASCADE,
|
|
parent_id UUID REFERENCES shell.menu_items(id) ON DELETE CASCADE,
|
|
zone shell.menu_zone NOT NULL,
|
|
key TEXT NOT NULL CHECK (key ~ '^[a-z][a-z0-9_-]*$'),
|
|
translation_key TEXT NOT NULL, -- e.g. 'menu.dashboard'
|
|
href TEXT NOT NULL, -- relative within app, or absolute if is_external
|
|
icon TEXT, -- Phosphor class, e.g. 'ph-house'
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_external BOOLEAN NOT NULL DEFAULT false,
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE (app_key, zone, key)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_menu_items_app_zone
|
|
ON shell.menu_items (app_key, zone, sort_order)
|
|
WHERE is_active;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_menu_items_parent
|
|
ON shell.menu_items (parent_id)
|
|
WHERE parent_id IS NOT NULL;
|
|
|
|
-- Role grants. Empty grants = visible to anyone authenticated. OR semantics
|
|
-- across rows: user sees the item if they hold ANY listed role.
|
|
-- Roles are Keycloak realm roles; we store their names as plain text since
|
|
-- Keycloak is the source of truth.
|
|
CREATE TABLE IF NOT EXISTS shell.menu_role_grants (
|
|
menu_item_id UUID NOT NULL REFERENCES shell.menu_items(id) ON DELETE CASCADE,
|
|
role TEXT NOT NULL,
|
|
PRIMARY KEY (menu_item_id, role)
|
|
);
|
|
|
|
-- Per-app branding. Logo, product name, footer text. Optional for now.
|
|
CREATE TABLE IF NOT EXISTS shell.branding (
|
|
app_key TEXT PRIMARY KEY REFERENCES shell.apps(app_key) ON DELETE CASCADE,
|
|
logo_url TEXT NOT NULL,
|
|
product_name TEXT NOT NULL,
|
|
footer_html TEXT,
|
|
brand_color TEXT, -- optional CSS color override
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- updated_at maintenance.
|
|
CREATE OR REPLACE FUNCTION shell.touch_updated_at() RETURNS trigger AS $$
|
|
BEGIN
|
|
NEW.updated_at := now();
|
|
RETURN NEW;
|
|
END $$ LANGUAGE plpgsql;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'apps_touch_updated_at') THEN
|
|
CREATE TRIGGER apps_touch_updated_at
|
|
BEFORE UPDATE ON shell.apps
|
|
FOR EACH ROW EXECUTE FUNCTION shell.touch_updated_at();
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'menu_items_touch_updated_at') THEN
|
|
CREATE TRIGGER menu_items_touch_updated_at
|
|
BEFORE UPDATE ON shell.menu_items
|
|
FOR EACH ROW EXECUTE FUNCTION shell.touch_updated_at();
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'branding_touch_updated_at') THEN
|
|
CREATE TRIGGER branding_touch_updated_at
|
|
BEFORE UPDATE ON shell.branding
|
|
FOR EACH ROW EXECUTE FUNCTION shell.touch_updated_at();
|
|
END IF;
|
|
END $$;
|