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>
47 lines
1.3 KiB
TypeScript
47 lines
1.3 KiB
TypeScript
import { getRequestConfig } from "next-intl/server";
|
|
import { cookies, headers } from "next/headers";
|
|
|
|
export const locales = ["en", "de", "fr"] as const;
|
|
export type Locale = (typeof locales)[number];
|
|
export const defaultLocale: Locale = "en";
|
|
|
|
export default getRequestConfig(async () => {
|
|
// Try to get locale from cookie
|
|
const cookieStore = await cookies();
|
|
let locale = cookieStore.get("NEXT_LOCALE")?.value as Locale | undefined;
|
|
|
|
// Fall back to Accept-Language header
|
|
if (!locale || !locales.includes(locale)) {
|
|
const headersList = await headers();
|
|
const acceptLanguage = headersList.get("accept-language");
|
|
if (acceptLanguage) {
|
|
const preferredLocale = acceptLanguage
|
|
.split(",")[0]
|
|
?.split("-")[0] as Locale;
|
|
if (locales.includes(preferredLocale)) {
|
|
locale = preferredLocale;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fall back to default
|
|
if (!locale || !locales.includes(locale)) {
|
|
locale = defaultLocale;
|
|
}
|
|
|
|
return {
|
|
locale,
|
|
messages: (await import(`../../public/locales/${locale}/common.json`)).default,
|
|
onError(error) {
|
|
if (error.code === "MISSING_MESSAGE") {
|
|
console.warn(error.message);
|
|
} else {
|
|
console.error(error);
|
|
}
|
|
},
|
|
getMessageFallback({ namespace, key }) {
|
|
return key;
|
|
},
|
|
};
|
|
});
|