@isi-ui7/bos7-shared 0.2.11 → 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.
- package/dist/bff/auth-logout.d.ts +2 -0
- package/dist/bff/auth-me.d.ts +6 -0
- package/dist/bff/auth-preferences.d.ts +2 -0
- package/dist/bff/auth-refresh.d.ts +6 -0
- package/dist/bff/health.d.ts +1 -0
- package/package.json +81 -27
- package/src/bff/auth-logout.ts +106 -0
- package/src/bff/auth-me.ts +74 -0
- package/src/bff/auth-preferences.ts +80 -0
- package/src/bff/auth-refresh.ts +106 -0
- package/src/bff/health.ts +12 -0
|
@@ -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,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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isi-ui7/bos7-shared",
|
|
3
|
-
"version": "0.2.
|
|
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": "
|
|
13
|
-
"module": "
|
|
14
|
-
"types": "
|
|
12
|
+
"main": "src/index.ts",
|
|
13
|
+
"module": "src/index.ts",
|
|
14
|
+
"types": "src/index.ts",
|
|
15
15
|
"exports": {
|
|
16
|
-
".": "./
|
|
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/
|
|
104
|
-
"@isi-ui7/
|
|
105
|
-
"@isi-ui7/
|
|
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/lookup-input": "0.2.3",
|
|
125
|
-
"@isi-ui7/editable-table": "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
|
+
}
|