@isi-ui7/bos7-shared 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export declare function POST(request: Request): Promise<Response>;
2
+ export declare function GET(request: Request): Promise<Response>;
@@ -0,0 +1,6 @@
1
+ export interface AuthMeRouteOptions {
2
+ /** Error code returned in the 401 body when no access_token cookie is present. */
3
+ errorCode?: string;
4
+ }
5
+ export declare function makeAuthMeRoute(options?: AuthMeRouteOptions): (request: Request) => Promise<Response>;
6
+ export declare const GET: (request: Request) => Promise<Response>;
@@ -0,0 +1,2 @@
1
+ export declare function GET(request: Request): Promise<Response>;
2
+ export declare function POST(request: Request): Promise<Response>;
@@ -0,0 +1,6 @@
1
+ export interface AuthRefreshRouteOptions {
2
+ /** Fallback client_id used when AUTH7_CLIENT_ID env var is unset. */
3
+ clientIdFallback?: string;
4
+ }
5
+ export declare function makeAuthRefreshRoute(options?: AuthRefreshRouteOptions): (request: Request) => Promise<Response>;
6
+ export declare const POST: (request: Request) => Promise<Response>;
@@ -0,0 +1 @@
1
+ export declare function GET(): Promise<Response>;
package/dist/index.js CHANGED
@@ -20205,8 +20205,8 @@ function lx() {
20205
20205
  const e = (navigator.language || "").toLowerCase();
20206
20206
  return e.startsWith("en") ? "en" : e.startsWith("id") ? "id" : null;
20207
20207
  }
20208
- function ux() {
20209
- return fx() ?? lx() ?? Ka;
20208
+ function ux(e) {
20209
+ return fx() ?? e ?? lx() ?? Ka;
20210
20210
  }
20211
20211
  function As(e) {
20212
20212
  typeof document > "u" || (document.cookie = `${cc}=${e}; path=/; max-age=31536000; samesite=lax`);
@@ -20221,36 +20221,39 @@ const fc = bs({
20221
20221
  function hx({
20222
20222
  children: e,
20223
20223
  locale: t,
20224
- dictionaries: r
20224
+ defaultLocale: r,
20225
+ dictionaries: n
20225
20226
  }) {
20226
- const [n, a] = qt(t ?? ux()), i = ka(() => ({
20227
- id: { ...za.id, ...(r == null ? void 0 : r.id) ?? {} },
20228
- en: { ...za.en, ...(r == null ? void 0 : r.en) ?? {} }
20229
- }), [r]), s = ka(() => {
20230
- const o = i[n];
20227
+ const [a, i] = qt(
20228
+ t ?? ux(r)
20229
+ ), s = ka(() => ({
20230
+ id: { ...za.id, ...(n == null ? void 0 : n.id) ?? {} },
20231
+ en: { ...za.en, ...(n == null ? void 0 : n.en) ?? {} }
20232
+ }), [n]), o = ka(() => {
20233
+ const l = s[a];
20231
20234
  return {
20232
- locale: n,
20233
- setLocale(l) {
20234
- a(l), As(l);
20235
+ locale: a,
20236
+ setLocale(f) {
20237
+ i(f), As(f);
20235
20238
  },
20236
- t(l, f) {
20237
- return o[l] ?? f ?? l;
20239
+ t(f, c) {
20240
+ return l[f] ?? c ?? f;
20238
20241
  },
20239
- dictionary: o
20242
+ dictionary: l
20240
20243
  };
20241
- }, [i, n]);
20244
+ }, [s, a]);
20242
20245
  return Vt(() => {
20243
20246
  if (t) return;
20244
- let o = !1;
20245
- return fetch("/api/auth/preferences", { cache: "no-store" }).then(async (l) => l.ok ? await l.json() : null).then((l) => {
20246
- if (o) return;
20247
- const f = l == null ? void 0 : l.locale;
20248
- (f === "id" || f === "en") && (a(f), As(f));
20247
+ let l = !1;
20248
+ return fetch("/api/auth/preferences", { cache: "no-store" }).then(async (f) => f.ok ? await f.json() : null).then((f) => {
20249
+ if (l) return;
20250
+ const c = f == null ? void 0 : f.locale;
20251
+ (c === "id" || c === "en") && (i(c), As(c));
20249
20252
  }).catch(() => {
20250
20253
  }), () => {
20251
- o = !0;
20254
+ l = !0;
20252
20255
  };
20253
- }, [t]), /* @__PURE__ */ Ee(fc.Provider, { value: s, children: e });
20256
+ }, [t]), /* @__PURE__ */ Ee(fc.Provider, { value: o, children: e });
20254
20257
  }
20255
20258
  function Ii() {
20256
20259
  return Cs(fc);
@@ -20301,10 +20304,10 @@ function dx() {
20301
20304
  };
20302
20305
  }
20303
20306
  function C_(e) {
20304
- const t = (e == null ? void 0 : e.withAuth) ?? !0, r = e == null ? void 0 : e.dictionaries;
20305
- return function({ children: a, Layout: i }) {
20306
- const s = /* @__PURE__ */ Ee(hx, { dictionaries: r, children: /* @__PURE__ */ Ee(i, { children: a }) });
20307
- return t ? /* @__PURE__ */ Ee(Mc, { children: s }) : s;
20307
+ const t = (e == null ? void 0 : e.withAuth) ?? !0, r = e == null ? void 0 : e.dictionaries, n = (e == null ? void 0 : e.defaultLocale) ?? "en";
20308
+ return function({ children: i, Layout: s }) {
20309
+ const o = /* @__PURE__ */ Ee(hx, { defaultLocale: n, dictionaries: r, children: /* @__PURE__ */ Ee(s, { children: i }) });
20310
+ return t ? /* @__PURE__ */ Ee(Mc, { children: o }) : o;
20308
20311
  };
20309
20312
  }
20310
20313
  function px(e, t, r) {
@@ -10,6 +10,7 @@ type SharedShellProviderProps = {
10
10
  type SharedShellProviderOptions = {
11
11
  withAuth?: boolean;
12
12
  dictionaries?: Partial<Record<"id" | "en", Record<string, string>>>;
13
+ defaultLocale?: "id" | "en";
13
14
  };
14
15
  export declare function createShellProvider(options?: SharedShellProviderOptions): ({ children, Layout }: SharedShellProviderProps) => import("react/jsx-runtime").JSX.Element;
15
16
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isi-ui7/bos7-shared",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Shared auth7 and layout primitives for bos7 applications.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -9,11 +9,11 @@
9
9
  "directory": "packages/bos7-shared"
10
10
  },
11
11
  "type": "module",
12
- "main": "dist/index.js",
13
- "module": "dist/index.js",
14
- "types": "dist/index.d.ts",
12
+ "main": "src/index.ts",
13
+ "module": "src/index.ts",
14
+ "types": "src/index.ts",
15
15
  "exports": {
16
- ".": "./dist/index.js",
16
+ ".": "./src/index.ts",
17
17
  "./auth7": "./src/auth7/index.ts",
18
18
  "./page-layout": "./src/page-layout.tsx",
19
19
  "./modal-layout": "./src/modal-layout.tsx",
@@ -42,25 +42,85 @@
42
42
  "./shell/provider": "./src/shell/provider.tsx",
43
43
  "./shell/app-shell-layout": "./src/shell/app-shell-layout.tsx",
44
44
  "./notifications-bff": "./src/notifications-bff.ts",
45
+ "./bff/auth-me": "./src/bff/auth-me.ts",
46
+ "./bff/auth-refresh": "./src/bff/auth-refresh.ts",
47
+ "./bff/auth-logout": "./src/bff/auth-logout.ts",
48
+ "./bff/auth-preferences": "./src/bff/auth-preferences.ts",
49
+ "./bff/health": "./src/bff/health.ts",
45
50
  "./observability/otel-init": "./src/observability/otel-init.ts",
46
51
  "./form-types": "./src/form-types.ts",
52
+ "./form-value-schema": "./src/form-value-schema.ts",
47
53
  "./form-numeric": "./src/form-numeric.ts",
48
54
  "./form-renderer": "./src/form-renderer.tsx",
49
55
  "./form-renderer-utils": "./src/form-renderer-utils.ts",
50
56
  "./form-rules": "./src/form-rules.ts",
51
57
  "./i18n": "./src/i18n.tsx",
52
58
  "./workflow/use-crud-form": "./src/workflow/use-crud-form.ts",
53
- "./styles": "./src/form-contract.css",
54
- "./form-value-schema": "./src/form-value-schema.ts"
59
+ "./styles": "./src/form-contract.css"
55
60
  },
56
61
  "publishConfig": {
57
62
  "registry": "https://registry.npmjs.org",
58
- "access": "public"
63
+ "access": "public",
64
+ "main": "dist/index.js",
65
+ "module": "dist/index.js",
66
+ "types": "dist/index.d.ts",
67
+ "exports": {
68
+ ".": "./dist/index.js",
69
+ "./auth7": "./src/auth7/index.ts",
70
+ "./page-layout": "./src/page-layout.tsx",
71
+ "./modal-layout": "./src/modal-layout.tsx",
72
+ "./form-layout": "./src/form-layout.tsx",
73
+ "./backend": "./src/backend.ts",
74
+ "./export": "./src/export.ts",
75
+ "./types/notification": "./src/types/notification.ts",
76
+ "./nats/client": "./src/nats/client.ts",
77
+ "./event-bus": "./src/event-bus/index.ts",
78
+ "./event-bus/useEventBus": "./src/event-bus/useEventBus.ts",
79
+ "./api": "./src/api.ts",
80
+ "./types/core": "./src/types/core.ts",
81
+ "./style-contract": "./src/style-contract.ts",
82
+ "./crud-components": "./src/crud-components.tsx",
83
+ "./workflow/backend": "./src/workflow/backend.ts",
84
+ "./workflow/notif7": "./src/workflow/notif7.ts",
85
+ "./workflow/api-client": "./src/workflow/api-client.ts",
86
+ "./workflow/starter": "./src/workflow/starter.ts",
87
+ "./workflow/use-workflow-tracker": "./src/workflow/use-workflow-tracker.ts",
88
+ "./workflow/crud-client": "./src/workflow/crud-client.ts",
89
+ "./data-table/cursor-adapter": "./src/data-table/cursor-adapter.ts",
90
+ "./notification/routing": "./src/notification/routing.ts",
91
+ "./shell/interaction": "./src/shell/interaction.ts",
92
+ "./shell/branch": "./src/shell/branch.ts",
93
+ "./notif7": "./src/notif7.ts",
94
+ "./shell/provider": "./src/shell/provider.tsx",
95
+ "./shell/app-shell-layout": "./src/shell/app-shell-layout.tsx",
96
+ "./notifications-bff": "./src/notifications-bff.ts",
97
+ "./bff/auth-me": "./src/bff/auth-me.ts",
98
+ "./bff/auth-refresh": "./src/bff/auth-refresh.ts",
99
+ "./bff/auth-logout": "./src/bff/auth-logout.ts",
100
+ "./bff/auth-preferences": "./src/bff/auth-preferences.ts",
101
+ "./bff/health": "./src/bff/health.ts",
102
+ "./observability/otel-init": "./src/observability/otel-init.ts",
103
+ "./form-types": "./src/form-types.ts",
104
+ "./form-numeric": "./src/form-numeric.ts",
105
+ "./form-renderer": "./src/form-renderer.tsx",
106
+ "./form-renderer-utils": "./src/form-renderer-utils.ts",
107
+ "./form-rules": "./src/form-rules.ts",
108
+ "./i18n": "./src/i18n.tsx",
109
+ "./workflow/use-crud-form": "./src/workflow/use-crud-form.ts",
110
+ "./styles": "./src/form-contract.css",
111
+ "./form-value-schema": "./src/form-value-schema.ts"
112
+ }
59
113
  },
60
114
  "files": [
61
115
  "dist",
62
116
  "src"
63
117
  ],
118
+ "scripts": {
119
+ "build": "vite build",
120
+ "lint": "eslint \"src/**/*.{ts,tsx}\"",
121
+ "test": "vitest run --passWithNoTests",
122
+ "typecheck": "tsc --noEmit"
123
+ },
64
124
  "peerDependencies": {
65
125
  "react": ">=18",
66
126
  "react-dom": ">=18",
@@ -98,20 +158,23 @@
98
158
  "vite-plugin-dts": "^3.6.0",
99
159
  "vitest": "^1.0.0",
100
160
  "next": "16.1.1",
161
+ "@isi-ui7/corporate-themes": "workspace:*",
162
+ "@isi-ui7/i18n": "workspace:*",
163
+ "@isi-ui7/ui-shell": "workspace:*",
164
+ "@isi-ui7/modal-manager": "workspace:*",
165
+ "@isi-ui7/realtime": "workspace:*",
101
166
  "@carbon/react": "^1.97.0",
102
167
  "@carbon/icons-react": "^11.71.0",
103
- "@isi-ui7/corporate-themes": "0.2.4",
104
- "@isi-ui7/i18n": "0.2.1",
105
- "@isi-ui7/ui-shell": "0.2.4",
106
- "@isi-ui7/modal-manager": "0.2.2",
107
- "@isi-ui7/realtime": "0.2.3",
108
- "@isi-ui7/data-table": "0.2.3",
109
- "@isi-ui7/lookup-input": "0.2.3",
110
- "@isi-ui7/editable-table": "0.2.3"
168
+ "@isi-ui7/data-table": "workspace:*",
169
+ "@isi-ui7/lookup-input": "workspace:*",
170
+ "@isi-ui7/editable-table": "workspace:*"
111
171
  },
112
172
  "dependencies": {
113
173
  "@carbon/icons-react": "^11.71.0",
114
174
  "@carbon/react": "^1.97.0",
175
+ "@isi-ui7/data-table": "workspace:*",
176
+ "@isi-ui7/lookup-input": "workspace:*",
177
+ "@isi-ui7/editable-table": "workspace:*",
115
178
  "jose": "^5.9.6",
116
179
  "xlsx": "^0.18.5",
117
180
  "jspdf": "^4.2.1",
@@ -119,15 +182,6 @@
119
182
  "axios": "^1.7.7",
120
183
  "@opentelemetry/sdk-node": "^0.57.0",
121
184
  "@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
122
- "@opentelemetry/auto-instrumentations-node": "^0.57.0",
123
- "@isi-ui7/data-table": "0.2.3",
124
- "@isi-ui7/editable-table": "0.2.3",
125
- "@isi-ui7/lookup-input": "0.2.3"
126
- },
127
- "scripts": {
128
- "build": "vite build",
129
- "lint": "eslint \"src/**/*.{ts,tsx}\"",
130
- "test": "vitest run --passWithNoTests",
131
- "typecheck": "tsc --noEmit"
185
+ "@opentelemetry/auto-instrumentations-node": "^0.57.0"
132
186
  }
133
- }
187
+ }
@@ -0,0 +1,106 @@
1
+ // bff/auth-logout.ts — Shared BFF handlers for /api/auth/logout
2
+ // Logout endpoint for auth7. Canonical source: bos7-template.
3
+ // Byte-identical across bos7-template, bos7-enterprise, bos7-portal.
4
+ //
5
+ // Config is fully env-driven (no per-app hardcoded values). Module-level env
6
+ // assertions throw at import time, matching the original per-app behavior.
7
+
8
+ import { NextRequest, NextResponse } from 'next/server';
9
+
10
+ const AUTH7_API_URL = process.env.AUTH7_API_URL;
11
+ if (!AUTH7_API_URL) throw new Error('[bos7] AUTH7_API_URL env var is required');
12
+ const CLIENT_ID = process.env.AUTH7_CLIENT_ID || process.env.NEXT_PUBLIC_AUTH7_CLIENT_ID;
13
+ if (!CLIENT_ID) throw new Error('[bos7] AUTH7_CLIENT_ID env var is required');
14
+ const AUTH7_UI_URL = process.env.AUTH7_UI_URL || process.env.NEXT_PUBLIC_AUTH7_URL;
15
+ const APP_URL = process.env.APP_URL || process.env.NEXT_PUBLIC_APP_URL || '';
16
+ const LOGOUT_MARKER_COOKIE = 'bos7_logout_at';
17
+ const LOGOUT_MARKER_DOMAIN = process.env.BOS7_COOKIE_DOMAIN || '.bos7.local';
18
+
19
+ function getPostLogoutRedirectUri(request: NextRequest): string {
20
+ return new URL('/', APP_URL || request.url).toString();
21
+ }
22
+
23
+ function getAuth7LogoutUrl(request: NextRequest): string {
24
+ const url = new URL('/oauth2/logout', AUTH7_UI_URL);
25
+ url.searchParams.set('client_id', CLIENT_ID!);
26
+ url.searchParams.set('post_logout_redirect_uri', getPostLogoutRedirectUri(request));
27
+ return url.toString();
28
+ }
29
+
30
+ function clearAuthCookies(response: NextResponse): void {
31
+ const cookies = [
32
+ 'access_token',
33
+ 'refresh_token',
34
+ 'id_token',
35
+ 'token_expires_at',
36
+ 'token_scope',
37
+ 'auth7_pkce_verifier',
38
+ 'auth7_oauth_state',
39
+ 'auth7_return_url',
40
+ ];
41
+
42
+ cookies.forEach((name) => {
43
+ response.cookies.set(name, '', {
44
+ path: '/',
45
+ maxAge: 0,
46
+ sameSite: 'lax',
47
+ });
48
+ });
49
+ }
50
+
51
+ function setGlobalLogoutMarker(response: NextResponse): void {
52
+ response.cookies.set(LOGOUT_MARKER_COOKIE, Date.now().toString(), {
53
+ path: '/',
54
+ sameSite: 'lax',
55
+ secure: process.env.NODE_ENV === 'production',
56
+ domain: LOGOUT_MARKER_DOMAIN,
57
+ maxAge: 60 * 60 * 24 * 30,
58
+ });
59
+ }
60
+
61
+ // Public signatures use Web `Request`/`Response` (see auth-me.ts for why); the
62
+ // runtime object is always a real NextRequest.
63
+
64
+ // POST: clear httpOnly cookies (called from JS before navigating to auth7 SSO logout)
65
+ export async function POST(request: Request): Promise<Response> {
66
+ const req = request as unknown as NextRequest;
67
+ const accessToken = req.cookies.get('access_token')?.value;
68
+ if (accessToken) {
69
+ try {
70
+ await fetch(`${AUTH7_API_URL}/v1/auth/logout`, {
71
+ method: 'POST',
72
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}` },
73
+ });
74
+ } catch { /* best-effort */ }
75
+ }
76
+ const res = NextResponse.json({ ok: true });
77
+ clearAuthCookies(res);
78
+ setGlobalLogoutMarker(res);
79
+ return res;
80
+ }
81
+
82
+ export async function GET(request: Request): Promise<Response> {
83
+ const req = request as unknown as NextRequest;
84
+ const accessToken = req.cookies.get('access_token')?.value;
85
+
86
+ // Invalidate session on auth7 backend (best-effort)
87
+ if (accessToken) {
88
+ try {
89
+ await fetch(`${AUTH7_API_URL}/v1/auth/logout`, {
90
+ method: 'POST',
91
+ headers: {
92
+ 'Content-Type': 'application/json',
93
+ Authorization: `Bearer ${accessToken}`,
94
+ },
95
+ });
96
+ } catch {
97
+ // Ignore backend errors — we still clear local cookies
98
+ }
99
+ }
100
+
101
+ const response = NextResponse.redirect(getAuth7LogoutUrl(req));
102
+ clearAuthCookies(response);
103
+ setGlobalLogoutMarker(response);
104
+
105
+ return response;
106
+ }
@@ -0,0 +1,74 @@
1
+ // bff/auth-me.ts — Shared BFF handler for /api/auth/me
2
+ // Proxy to auth7 /oauth2/userinfo, augmenting userinfo with JWT claims
3
+ // (branch_id, roles) for shell display. Canonical source: bos7-template.
4
+ //
5
+ // Apps re-export the ready GET (default error code 'unauthorized'), or build a
6
+ // variant via makeAuthMeRoute({ errorCode }) — e.g. bos7-enterprise uses
7
+ // 'unauthenticated'.
8
+
9
+ import { NextRequest, NextResponse } from 'next/server';
10
+
11
+ const AUTH7_API_URL = process.env.AUTH7_API_URL || 'http://localhost:8090';
12
+
13
+ export interface AuthMeRouteOptions {
14
+ /** Error code returned in the 401 body when no access_token cookie is present. */
15
+ errorCode?: string;
16
+ }
17
+
18
+ function decodeJWTPayload(token: string): Record<string, unknown> | null {
19
+ try {
20
+ const parts = token.split('.');
21
+ if (parts.length < 2) return null;
22
+ const payload = parts[1];
23
+ const decoded = Buffer.from(payload, 'base64url').toString('utf-8');
24
+ return JSON.parse(decoded);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ export function makeAuthMeRoute(options: AuthMeRouteOptions = {}) {
31
+ const errorCode = options.errorCode ?? 'unauthorized';
32
+
33
+ // NOTE: The public signature uses the Web `Request`/`Response` types (not
34
+ // NextRequest/NextResponse) so the exported route handler stays assignable to
35
+ // Next's RouteHandlerConfig regardless of which `next` install resolves the
36
+ // type. At runtime Next always passes a real NextRequest, so the internal
37
+ // cast is safe.
38
+ return async function GET(request: Request): Promise<Response> {
39
+ const req = request as unknown as NextRequest;
40
+ const accessToken = req.cookies.get('access_token')?.value;
41
+
42
+ if (!accessToken) {
43
+ return NextResponse.json({ error: errorCode }, { status: 401 });
44
+ }
45
+
46
+ try {
47
+ // Fetch userinfo from auth7 OAuth2 endpoint
48
+ const response = await fetch(`${AUTH7_API_URL}/oauth2/userinfo`, {
49
+ headers: {
50
+ Authorization: `Bearer ${accessToken}`,
51
+ },
52
+ });
53
+
54
+ if (!response.ok) {
55
+ return NextResponse.json({ error: 'userinfo_failed' }, { status: response.status });
56
+ }
57
+
58
+ const userInfo = await response.json();
59
+
60
+ // Augment with JWT claims for shell display
61
+ const jwtClaims = decodeJWTPayload(accessToken);
62
+ if (jwtClaims) {
63
+ userInfo.roles = jwtClaims.roles || userInfo.roles || [];
64
+ userInfo.branch_id = jwtClaims.branch_id || null;
65
+ }
66
+
67
+ return NextResponse.json(userInfo);
68
+ } catch {
69
+ return NextResponse.json({ error: 'server_error' }, { status: 500 });
70
+ }
71
+ };
72
+ }
73
+
74
+ export const GET = makeAuthMeRoute();
@@ -0,0 +1,80 @@
1
+ // bff/auth-preferences.ts — Shared BFF handlers for /api/auth/preferences
2
+ // Locale preference (GET/POST) backed by auth7 profile + core7_locale cookie.
3
+ // Canonical source: bos7-template. Byte-identical across bos7-template,
4
+ // bos7-portal, bos7-workflow, auth7-ui.
5
+
6
+ import { NextRequest, NextResponse } from "next/server";
7
+
8
+ const COOKIE_KEY = "core7_locale";
9
+ const VALID = new Set(["id", "en"]);
10
+ const AUTH7_API_URL = process.env.AUTH7_API_URL || "http://localhost:8090";
11
+
12
+ // Public signatures use Web `Request`/`Response` (see bff/auth-me.ts for why);
13
+ // the runtime object is always a real NextRequest.
14
+ export async function GET(request: Request): Promise<Response> {
15
+ const req = request as unknown as NextRequest;
16
+ const cookieLocale = req.cookies.get(COOKIE_KEY)?.value ?? "id";
17
+ const accessToken = req.cookies.get("access_token")?.value;
18
+ if (!accessToken) {
19
+ return NextResponse.json({ locale: VALID.has(cookieLocale) ? cookieLocale : "id" });
20
+ }
21
+
22
+ try {
23
+ const res = await fetch(`${AUTH7_API_URL}/v1/auth/me`, {
24
+ headers: { Authorization: `Bearer ${accessToken}` },
25
+ cache: "no-store",
26
+ });
27
+ if (!res.ok) {
28
+ return NextResponse.json({ locale: VALID.has(cookieLocale) ? cookieLocale : "id" });
29
+ }
30
+ const data = (await res.json()) as { preferred_locale?: string };
31
+ const locale = data?.preferred_locale && VALID.has(data.preferred_locale) ? data.preferred_locale : (VALID.has(cookieLocale) ? cookieLocale : "id");
32
+ const response = NextResponse.json({ locale });
33
+ response.cookies.set(COOKIE_KEY, locale, {
34
+ httpOnly: false,
35
+ secure: process.env.NODE_ENV === "production",
36
+ sameSite: "lax",
37
+ maxAge: 31536000,
38
+ path: "/",
39
+ });
40
+ return response;
41
+ } catch {
42
+ return NextResponse.json({ locale: VALID.has(cookieLocale) ? cookieLocale : "id" });
43
+ }
44
+ }
45
+
46
+ export async function POST(request: Request): Promise<Response> {
47
+ const req = request as unknown as NextRequest;
48
+ const accessToken = req.cookies.get("access_token")?.value;
49
+ let locale = "id";
50
+ try {
51
+ const body = (await req.json()) as { locale?: string };
52
+ if (body?.locale && VALID.has(body.locale)) locale = body.locale;
53
+ } catch {
54
+ return NextResponse.json({ error: "invalid_request" }, { status: 400 });
55
+ }
56
+
57
+ const res = NextResponse.json({ locale });
58
+ if (accessToken) {
59
+ try {
60
+ await fetch(`${AUTH7_API_URL}/v1/auth/profile`, {
61
+ method: "PUT",
62
+ headers: {
63
+ "Content-Type": "application/json",
64
+ Authorization: `Bearer ${accessToken}`,
65
+ },
66
+ body: JSON.stringify({ preferred_locale: locale }),
67
+ });
68
+ } catch {
69
+ // keep local cookie fallback even if backend sync fails
70
+ }
71
+ }
72
+ res.cookies.set(COOKIE_KEY, locale, {
73
+ httpOnly: false,
74
+ secure: process.env.NODE_ENV === "production",
75
+ sameSite: "lax",
76
+ maxAge: 31536000,
77
+ path: "/",
78
+ });
79
+ return res;
80
+ }
@@ -0,0 +1,106 @@
1
+ // bff/auth-refresh.ts — Shared BFF handler for /api/auth/refresh
2
+ // Silent token refresh endpoint. Canonical source: bos7-template.
3
+ //
4
+ // The only per-app difference is the CLIENT_ID fallback used when
5
+ // AUTH7_CLIENT_ID is unset. Apps re-export the ready POST (fallback
6
+ // 'bos7-template') when byte-identical, or build a variant via
7
+ // makeAuthRefreshRoute({ clientIdFallback }).
8
+
9
+ import { NextRequest, NextResponse } from 'next/server';
10
+
11
+ const AUTH7_API_URL = process.env.AUTH7_API_URL || 'http://localhost:8090';
12
+
13
+ export interface AuthRefreshRouteOptions {
14
+ /** Fallback client_id used when AUTH7_CLIENT_ID env var is unset. */
15
+ clientIdFallback?: string;
16
+ }
17
+
18
+ function getRefreshToken(request: NextRequest): string | null {
19
+ return request.cookies.get('refresh_token')?.value ?? null;
20
+ }
21
+
22
+ export function makeAuthRefreshRoute(options: AuthRefreshRouteOptions = {}) {
23
+ const CLIENT_ID = process.env.AUTH7_CLIENT_ID || options.clientIdFallback || 'bos7-template';
24
+
25
+ // Public signature uses Web `Request`/`Response` (see auth-me.ts for why); the
26
+ // runtime object is always a real NextRequest.
27
+ return async function POST(request: Request): Promise<Response> {
28
+ const refreshToken = getRefreshToken(request as unknown as NextRequest);
29
+
30
+ if (!refreshToken) {
31
+ return NextResponse.json({ error: 'No refresh token' }, { status: 401 });
32
+ }
33
+
34
+ try {
35
+ const body = new URLSearchParams({
36
+ grant_type: 'refresh_token',
37
+ refresh_token: refreshToken,
38
+ client_id: CLIENT_ID,
39
+ });
40
+
41
+ const response = await fetch(`${AUTH7_API_URL}/oauth2/token`, {
42
+ method: 'POST',
43
+ headers: {
44
+ 'Content-Type': 'application/x-www-form-urlencoded',
45
+ },
46
+ body: body.toString(),
47
+ });
48
+
49
+ if (!response.ok) {
50
+ const errorData = await response.json().catch(() => ({}));
51
+ throw new Error(errorData.error_description || errorData.error || 'Token refresh failed');
52
+ }
53
+
54
+ const tokens = await response.json();
55
+ const expiresAt = Date.now() + tokens.expires_in * 1000;
56
+
57
+ const jsonResponse = NextResponse.json({ success: true });
58
+
59
+ jsonResponse.cookies.set('access_token', tokens.access_token, {
60
+ httpOnly: true,
61
+ secure: process.env.NODE_ENV === 'production',
62
+ sameSite: 'lax',
63
+ maxAge: tokens.expires_in,
64
+ path: '/',
65
+ });
66
+
67
+ jsonResponse.cookies.set('token_expires_at', expiresAt.toString(), {
68
+ httpOnly: true,
69
+ secure: process.env.NODE_ENV === 'production',
70
+ sameSite: 'lax',
71
+ maxAge: tokens.expires_in,
72
+ path: '/',
73
+ });
74
+
75
+ jsonResponse.cookies.set('token_scope', tokens.scope, {
76
+ httpOnly: true,
77
+ secure: process.env.NODE_ENV === 'production',
78
+ sameSite: 'lax',
79
+ maxAge: tokens.expires_in,
80
+ path: '/',
81
+ });
82
+
83
+ if (tokens.refresh_token) {
84
+ jsonResponse.cookies.set('refresh_token', tokens.refresh_token, {
85
+ httpOnly: true,
86
+ secure: process.env.NODE_ENV === 'production',
87
+ sameSite: 'lax',
88
+ maxAge: 8 * 3600,
89
+ path: '/',
90
+ });
91
+ }
92
+
93
+ return jsonResponse;
94
+ } catch (err) {
95
+ const response = NextResponse.json({ error: (err as Error).message }, { status: 401 });
96
+ response.cookies.set('access_token', '', { path: '/', maxAge: 0, sameSite: 'lax' });
97
+ response.cookies.set('refresh_token', '', { path: '/', maxAge: 0, sameSite: 'lax' });
98
+ response.cookies.set('id_token', '', { path: '/', maxAge: 0, sameSite: 'lax' });
99
+ response.cookies.set('token_expires_at', '', { path: '/', maxAge: 0, sameSite: 'lax' });
100
+ response.cookies.set('token_scope', '', { path: '/', maxAge: 0, sameSite: 'lax' });
101
+ return response;
102
+ }
103
+ };
104
+ }
105
+
106
+ export const POST = makeAuthRefreshRoute();
@@ -0,0 +1,12 @@
1
+ // bff/health.ts — Shared BFF handler for /api/health
2
+ // Liveness probe. Canonical source: bos7-template.
3
+ // Byte-identical across bos7-template and bos7-enterprise.
4
+
5
+ export async function GET(): Promise<Response> {
6
+ return Response.json({
7
+ status: 'ok',
8
+ version: process.env.APP_VERSION ?? 'unknown',
9
+ timestamp: new Date().toISOString(),
10
+ env: process.env.NODE_ENV ?? 'unknown',
11
+ });
12
+ }
@@ -16,15 +16,19 @@ type SharedShellProviderProps = {
16
16
  type SharedShellProviderOptions = {
17
17
  withAuth?: boolean;
18
18
  dictionaries?: Partial<Record<"id" | "en", Record<string, string>>>;
19
+ // Default bahasa untuk pengunjung baru; pilihan user (cookie) tetap menang.
20
+ // Standar platform: English.
21
+ defaultLocale?: "id" | "en";
19
22
  };
20
23
 
21
24
  export function createShellProvider(options?: SharedShellProviderOptions) {
22
25
  const withAuth = options?.withAuth ?? true;
23
26
  const dictionaries = options?.dictionaries;
27
+ const defaultLocale = options?.defaultLocale ?? "en";
24
28
 
25
29
  return function SharedShellProvider({ children, Layout }: SharedShellProviderProps) {
26
30
  const content = (
27
- <I18nProvider dictionaries={dictionaries}>
31
+ <I18nProvider defaultLocale={defaultLocale} dictionaries={dictionaries}>
28
32
  <Layout>{children}</Layout>
29
33
  </I18nProvider>
30
34
  );