Adopt @gsc/web-kit i18n factory + LogoutButton
i18n/request.ts: delegate to kit's createI18nConfig — locale resolution chain (cookie → access_token claim → Accept-Language → default) now lives in the kit instead of being reimplemented here. LogoutButton becomes a thin re-export of the kit version. Lockfile reflects updated dep graph. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
142
package-lock.json
generated
142
package-lock.json
generated
@@ -7,18 +7,24 @@
|
||||
"": {
|
||||
"name": "gsc-my",
|
||||
"version": "1.0.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.37.0",
|
||||
"@gsc/chat": "file:../../infra/gscAICoreSystem/frontends/gscBicameralFrontend",
|
||||
"@gsc/web-kit": "file:../../templates/gsc-web-kit",
|
||||
"@limitless/ui": "file:../../templates/limitless-ui",
|
||||
"@phosphor-icons/web": "2.1.1",
|
||||
"@prisma/client": "^6.1.0",
|
||||
"bootstrap": "^5.3.3",
|
||||
"clsx": "^2.1.0",
|
||||
"ldapts": "^8.0.36",
|
||||
"next": "^16.1.1",
|
||||
"next-auth": "^5.0.0-beta.25",
|
||||
"next-intl": "^4.6.1",
|
||||
"nodemailer": "^7.0.7",
|
||||
"pg": "^8.20.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"undici": "^6.25.0",
|
||||
"zod": "^3.23.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -37,7 +43,7 @@
|
||||
},
|
||||
"../../infra/gscAICoreSystem/frontends/gscBicameralFrontend": {
|
||||
"name": "@gsc/chat",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@limitless/ui": "file:./limitless-ui"
|
||||
@@ -55,6 +61,41 @@
|
||||
"react-dom": "^18.2.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"../../templates/gsc-web-kit": {
|
||||
"name": "@gsc/web-kit",
|
||||
"version": "0.5.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@limitless/ui": "file:../limitless-ui",
|
||||
"zod": "^3.23.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"next": "16.1.1",
|
||||
"next-auth": "^5.0.0-beta.25",
|
||||
"next-intl": "^4.6.1",
|
||||
"typescript": "^5.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@gsc/chat": "*",
|
||||
"bootstrap": "^5.3.3",
|
||||
"next": ">=15.0.0",
|
||||
"next-auth": "^5.0.0-beta.25",
|
||||
"next-intl": "^4.6.0",
|
||||
"react": "^18.2.0 || ^19.0.0",
|
||||
"react-dom": "^18.2.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@gsc/chat": {
|
||||
"optional": true
|
||||
},
|
||||
"bootstrap": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"../../templates/limitless-ui": {
|
||||
"name": "@limitless/ui",
|
||||
"version": "0.1.0",
|
||||
@@ -70,8 +111,8 @@
|
||||
"devDependencies": {
|
||||
"@types/google.maps": "^3.55.0",
|
||||
"@types/nodemailer": "^6.4.0",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"typescript": "^5.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -106,35 +147,6 @@
|
||||
"react-dom": "^18.2.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@auth/core": {
|
||||
"version": "0.37.4",
|
||||
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.4.tgz",
|
||||
"integrity": "sha512-HOXJwXWXQRhbBDHlMU0K/6FT1v+wjtzdKhsNg0ZN7/gne6XPsIrjZ4daMcFnbq0Z/vsAbYBinQhhua0d77v7qw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@panva/hkdf": "^1.2.1",
|
||||
"jose": "^5.9.6",
|
||||
"oauth4webapi": "^3.1.1",
|
||||
"preact": "10.24.3",
|
||||
"preact-render-to-string": "6.5.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@simplewebauthn/browser": "^9.0.1",
|
||||
"@simplewebauthn/server": "^9.0.2",
|
||||
"nodemailer": "^6.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@simplewebauthn/browser": {
|
||||
"optional": true
|
||||
},
|
||||
"@simplewebauthn/server": {
|
||||
"optional": true
|
||||
},
|
||||
"nodemailer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||
@@ -647,6 +659,10 @@
|
||||
"resolved": "../../infra/gscAICoreSystem/frontends/gscBicameralFrontend",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@gsc/web-kit": {
|
||||
"resolved": "../../templates/gsc-web-kit",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -1728,6 +1744,12 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@phosphor-icons/web": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@phosphor-icons/web/-/web-2.1.1.tgz",
|
||||
"integrity": "sha512-QjrfbItu5Rb2i37GzsKxmrRHfZPTVk3oXSPBnQ2+oACDbQRWGAeB0AsvZw263n1nFouQuff+khOCtRbrc6+k+A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
@@ -3267,6 +3289,15 @@
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -5240,15 +5271,6 @@
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
|
||||
"integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -5362,6 +5384,18 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/ldapts": {
|
||||
"version": "8.1.7",
|
||||
"resolved": "https://registry.npmjs.org/ldapts/-/ldapts-8.1.7.tgz",
|
||||
"integrity": "sha512-TJl6T92eIwMf/OJ0hDfKVa6ISwzo+lqCWCI5Mf//ARlKa3LKQZaSrme/H2rCRBhW0DZCQlrsV+fgoW5YHRNLUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"strict-event-emitter-types": "2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
@@ -5763,6 +5797,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "7.0.13",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz",
|
||||
"integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nypm": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
|
||||
@@ -6845,6 +6888,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/strict-event-emitter-types": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz",
|
||||
"integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/string.prototype.includes": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
||||
@@ -7250,6 +7299,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.25.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz",
|
||||
"integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { signOut } from "next-auth/react";
|
||||
|
||||
export function LogoutButton({ label }: { label: string }) {
|
||||
const handleLogout = async () => {
|
||||
const res = await fetch("/api/auth/logout");
|
||||
const { logoutUrl } = await res.json();
|
||||
await signOut({ redirect: false });
|
||||
window.location.href = logoutUrl;
|
||||
};
|
||||
|
||||
// Plain anchor → /api/auth/signout. The route handler runs
|
||||
// RP-initiated logout in a single redirect: kills the NextAuth cookie,
|
||||
// ends the Keycloak SSO session, lands on /signed-out. No client-side
|
||||
// signOut() dance, no two-step fetch.
|
||||
return (
|
||||
<button type="button" onClick={handleLogout} className="dropdown-item">
|
||||
<a href="/api/auth/signout" className="dropdown-item">
|
||||
<i className="ph-sign-out me-2"></i>
|
||||
{label}
|
||||
</button>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,78 +1,17 @@
|
||||
import { getRequestConfig } from "next-intl/server";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { createI18nConfig } from "@gsc/web-kit/i18n/server";
|
||||
import { auth } from "@/auth";
|
||||
|
||||
export const locales = ["en", "de", "fr"] as const;
|
||||
export type Locale = (typeof locales)[number];
|
||||
export const defaultLocale: Locale = "en";
|
||||
|
||||
function isLocale(v: unknown): v is Locale {
|
||||
return typeof v === "string" && (locales as readonly string[]).includes(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the `preferred_language` claim from the user's Keycloak access
|
||||
* token. The claim is sourced from FreeIPA's `preferredLanguage` user
|
||||
* attribute via the gosecCloud realm's LDAP attribute mapper and the
|
||||
* per-client OIDC protocol mapper. Returns `undefined` if no session,
|
||||
* no claim, or the value isn't in our supported `locales`.
|
||||
*/
|
||||
async function localeFromKeycloak(): Promise<Locale | undefined> {
|
||||
try {
|
||||
export default createI18nConfig({
|
||||
locales,
|
||||
defaultLocale,
|
||||
getAccessToken: async () => {
|
||||
const session = await auth();
|
||||
const accessToken = (session?.user as { accessToken?: string } | undefined)?.accessToken;
|
||||
if (!accessToken) return undefined;
|
||||
const payload = accessToken.split(".")[1];
|
||||
if (!payload) return undefined;
|
||||
const padded = payload + "=".repeat((4 - (payload.length % 4)) % 4);
|
||||
const decoded = Buffer.from(padded, "base64").toString("utf8");
|
||||
const json = JSON.parse(decoded) as { preferred_language?: string };
|
||||
return isLocale(json.preferred_language) ? json.preferred_language : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default getRequestConfig(async () => {
|
||||
// Resolution order:
|
||||
// 1. NEXT_LOCALE cookie — recent /settings change, beats the
|
||||
// stale token claim until the next refresh.
|
||||
// 2. Keycloak `preferred_language` claim — FreeIPA-backed
|
||||
// durable preference.
|
||||
// 3. Accept-Language header.
|
||||
// 4. defaultLocale.
|
||||
const cookieStore = await cookies();
|
||||
const cookieLocale = cookieStore.get("NEXT_LOCALE")?.value;
|
||||
|
||||
let locale: Locale | undefined = isLocale(cookieLocale) ? cookieLocale : undefined;
|
||||
|
||||
if (!locale) {
|
||||
locale = await localeFromKeycloak();
|
||||
}
|
||||
|
||||
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 = 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({ key }) {
|
||||
return key;
|
||||
},
|
||||
};
|
||||
return (session?.user as { accessToken?: string } | undefined)?.accessToken;
|
||||
},
|
||||
loadMessages: async (locale) =>
|
||||
(await import(`../../public/locales/${locale}/common.json`)).default,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user