chore: bootstrap gscMy on @gsc/web-kit + PAM/JIT request flow
Initial commit for gscMy carved out as its own repo (was tracked
loosely under the monorepo's web/ which is gitignored).
What this contains:
- Auth: next-auth v5 via @gsc/web-kit createAuth (Keycloak only,
identity sourced from claims, no admin.users writes)
- Chrome: @gsc/web-kit AdminShell — replaces the legacy MyShell.
Sidebar JSON config carried over and mapped to DbMenuItem.
- Middleware: createAuthMiddleware. Public: /access-denied,
/auth/keycloak, /signed-out, /api/health, /api/pam/approve.
- RP-initiated signout at /api/auth/signout → Keycloak end_session →
/signed-out (mirrors gscAdmin).
- Phosphor-iconned access-denied + signed-out landing pages.
PAM/JIT request flow (ported from gscAdmin's pre-strip git history):
- /access page (Active + Eligible tables, request modal with
duration slider + justification + optional MFA)
- API: /api/pam/{eligible, active, audit, request, approve/:token,
revoke/:id}
- src/lib/{authz, pam, pam-mail, pam-mfa}.ts — same files as
gscAdmin had before the strip. PAM tables (admin.privilege_*)
are shared with gscAdmin; gscMy uses the same Prisma model defs.
- Top-bar widget shows active grants with countdown + revoke.
Build/Deploy: Dockerfile (monorepo-root context), k8s manifests for
my.gosec.internal, self-signed TLS placeholder, DNS A record.
Keycloak gsc-my client extended to include my.gosec.internal/* in
redirect_uris + web_origins.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
73
src/app/(my)/layout.tsx
Normal file
73
src/app/(my)/layout.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { AdminShell, type DbMenuItem } from "@gsc/web-kit/chrome";
|
||||
import sidebarMenuJson from "@/config/sidebar-menu.json";
|
||||
import { getAuthenticatedUser, requireAuth } from "@/auth";
|
||||
import { brand } from "@/config/brand";
|
||||
import ActiveGrantsWidget from "@/components/pam/ActiveGrantsWidget";
|
||||
|
||||
type LegacySidebarItem = {
|
||||
id: number;
|
||||
icon?: string;
|
||||
name: string;
|
||||
url: string;
|
||||
key: string;
|
||||
submenulvl1?: { name: string; url: string; key: string; icon?: string }[];
|
||||
};
|
||||
|
||||
// Map the legacy JSON-driven sidebar onto the kit's DbMenuItem shape.
|
||||
// (When gscMy gets its own admin.menu_items rows we can drop the JSON
|
||||
// and load from DB the same way gscAdmin does.)
|
||||
function toDbMenuItem(item: LegacySidebarItem, order: number): DbMenuItem {
|
||||
return {
|
||||
id: String(item.id),
|
||||
key: item.key,
|
||||
translationKey: item.key,
|
||||
url: item.url,
|
||||
icon: item.icon ?? null,
|
||||
sortOrder: order,
|
||||
isActive: true,
|
||||
isSystemRequired: false,
|
||||
children:
|
||||
item.submenulvl1?.map((c, i) => ({
|
||||
id: `${item.id}.${i + 1}`,
|
||||
key: c.key,
|
||||
translationKey: c.key,
|
||||
url: c.url,
|
||||
icon: c.icon ?? null,
|
||||
sortOrder: i,
|
||||
isActive: true,
|
||||
isSystemRequired: false,
|
||||
})) ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
const sidebar = (sidebarMenuJson as LegacySidebarItem[]).map(toDbMenuItem);
|
||||
|
||||
export default async function MyGroupLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
await requireAuth();
|
||||
const user = await getAuthenticatedUser();
|
||||
|
||||
return (
|
||||
<AdminShell
|
||||
menus={{ sidebar, topbar: [], subbar: [] }}
|
||||
apps={[]}
|
||||
user={{
|
||||
displayName: user?.displayName || user?.email || "",
|
||||
email: user?.email ?? "",
|
||||
}}
|
||||
brand={brand}
|
||||
features={{
|
||||
chat: false,
|
||||
activityPanel: false,
|
||||
}}
|
||||
slots={{
|
||||
navbarExtras: <ActiveGrantsWidget />,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user