@apptimate/core-lib 1.1.0 → 1.3.0
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/package.json +1 -1
- package/src/constants/menus.ts +241 -593
- package/src/utils/apiProxy.ts +155 -0
- package/src/utils/httpClient.ts +34 -27
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { cookie } from "../constants/storageKeys";
|
|
2
|
+
import { decode } from "./commonService";
|
|
3
|
+
|
|
4
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
5
|
+
"connection",
|
|
6
|
+
"content-length",
|
|
7
|
+
"content-encoding",
|
|
8
|
+
"keep-alive",
|
|
9
|
+
"proxy-authenticate",
|
|
10
|
+
"proxy-authorization",
|
|
11
|
+
"te",
|
|
12
|
+
"trailer",
|
|
13
|
+
"transfer-encoding",
|
|
14
|
+
"upgrade",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function parseCookieHeader(header: string | null): Record<string, string> {
|
|
18
|
+
if (!header) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return header.split(";").reduce<Record<string, string>>((acc, part) => {
|
|
23
|
+
const [rawName, ...valueParts] = part.trim().split("=");
|
|
24
|
+
if (!rawName) {
|
|
25
|
+
return acc;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
acc[rawName] = decodeURIComponent(valueParts.join("=") || "");
|
|
29
|
+
return acc;
|
|
30
|
+
}, {});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getCookieValue(cookies: Record<string, string>, name: string): string | null {
|
|
34
|
+
return cookies[name] ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getAccessToken(cookies: Record<string, string>): string | null {
|
|
38
|
+
const cookieNames = [
|
|
39
|
+
cookie.access_token.encrypted ? cookie.access_token.secretName : cookie.access_token.name,
|
|
40
|
+
cookie.access_token.name,
|
|
41
|
+
cookie.access_token.secretName,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
for (const name of cookieNames) {
|
|
45
|
+
const value = getCookieValue(cookies, name);
|
|
46
|
+
if (!value) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return cookie.access_token.encrypted ? decode(value) : value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildCookieHeader(name: string, options?: { httpOnly?: boolean }): string {
|
|
57
|
+
const parts = [
|
|
58
|
+
`${name}=`,
|
|
59
|
+
"Path=/",
|
|
60
|
+
"Max-Age=0",
|
|
61
|
+
"Expires=Thu, 01 Jan 1970 00:00:00 GMT",
|
|
62
|
+
"SameSite=Lax",
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
if (options?.httpOnly) {
|
|
66
|
+
parts.push("HttpOnly");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (process.env.NODE_ENV === "production") {
|
|
70
|
+
parts.push("Secure");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return parts.join("; ");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createSessionLogoutResponse(): Response {
|
|
77
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
78
|
+
headers.append("set-cookie", buildCookieHeader(cookie.access_token.name, { httpOnly: true }));
|
|
79
|
+
headers.append("set-cookie", buildCookieHeader(cookie.access_token.secretName, { httpOnly: true }));
|
|
80
|
+
headers.append("set-cookie", buildCookieHeader("selected_organization_id"));
|
|
81
|
+
|
|
82
|
+
return new Response(JSON.stringify({ cleared: true }), {
|
|
83
|
+
status: 200,
|
|
84
|
+
headers,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function forwardApiProxyRequest(request: Request, pathSegments: string[]): Promise<Response> {
|
|
89
|
+
const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL;
|
|
90
|
+
|
|
91
|
+
if (!apiBaseUrl) {
|
|
92
|
+
return new Response(JSON.stringify({
|
|
93
|
+
is_success: false,
|
|
94
|
+
message: "NEXT_PUBLIC_API_URL is not configured.",
|
|
95
|
+
result: null,
|
|
96
|
+
system_code: "api_proxy_not_configured",
|
|
97
|
+
}), {
|
|
98
|
+
status: 500,
|
|
99
|
+
headers: { "content-type": "application/json" },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const incomingUrl = new URL(request.url);
|
|
104
|
+
const upstreamPath = pathSegments.join("/");
|
|
105
|
+
const upstreamUrl = new URL(upstreamPath, `${apiBaseUrl.replace(/\/$/, "")}/`);
|
|
106
|
+
upstreamUrl.search = incomingUrl.search;
|
|
107
|
+
|
|
108
|
+
const cookies = parseCookieHeader(request.headers.get("cookie"));
|
|
109
|
+
const token = getAccessToken(cookies);
|
|
110
|
+
const selectedOrganizationId = getCookieValue(cookies, "selected_organization_id");
|
|
111
|
+
|
|
112
|
+
const headers = new Headers();
|
|
113
|
+
const accept = request.headers.get("accept");
|
|
114
|
+
const contentType = request.headers.get("content-type");
|
|
115
|
+
const organizationId = request.headers.get("x-organization-id") ?? selectedOrganizationId;
|
|
116
|
+
|
|
117
|
+
if (accept) {
|
|
118
|
+
headers.set("accept", accept);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (contentType) {
|
|
122
|
+
headers.set("content-type", contentType);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (organizationId) {
|
|
126
|
+
headers.set("x-organization-id", organizationId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (token) {
|
|
130
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const hasBody = !["GET", "HEAD"].includes(request.method.toUpperCase());
|
|
134
|
+
const body = hasBody ? await request.arrayBuffer() : undefined;
|
|
135
|
+
|
|
136
|
+
const upstreamResponse = await fetch(upstreamUrl.toString(), {
|
|
137
|
+
method: request.method,
|
|
138
|
+
headers,
|
|
139
|
+
body,
|
|
140
|
+
cache: "no-store",
|
|
141
|
+
redirect: "manual",
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const responseHeaders = new Headers();
|
|
145
|
+
upstreamResponse.headers.forEach((value, key) => {
|
|
146
|
+
if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) {
|
|
147
|
+
responseHeaders.set(key, value);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return new Response(upstreamResponse.body, {
|
|
152
|
+
status: upstreamResponse.status,
|
|
153
|
+
headers: responseHeaders,
|
|
154
|
+
});
|
|
155
|
+
}
|
package/src/utils/httpClient.ts
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { IApiResponse } from "../common/interfaces/ICommon";
|
|
2
|
-
import Cookies from 'js-cookie';
|
|
3
|
-
import { cookie } from '../constants/storageKeys';
|
|
4
|
-
import { decrypt, replacePlaceholders } from './commonService';
|
|
5
2
|
|
|
6
3
|
/**
|
|
7
4
|
* Handles 401 Unauthorized responses globally.
|
|
8
|
-
* Clears the
|
|
5
|
+
* Clears the server-managed session and app-owned local state, then redirects
|
|
6
|
+
* to the login page.
|
|
9
7
|
*/
|
|
10
8
|
export function handleUnauthorized(): void {
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
// Best-effort server-side cookie clear for HttpOnly auth cookies.
|
|
10
|
+
if (typeof window !== 'undefined') {
|
|
11
|
+
fetch('/api/session/logout', {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
credentials: 'same-origin',
|
|
14
|
+
keepalive: true,
|
|
15
|
+
}).catch(() => {
|
|
16
|
+
// Ignore logout cleanup failures and proceed with client-side reset.
|
|
17
|
+
});
|
|
18
|
+
}
|
|
17
19
|
|
|
18
20
|
// Clear only app-owned localStorage keys instead of wiping the entire origin
|
|
19
21
|
if (typeof window !== 'undefined') {
|
|
@@ -49,6 +51,25 @@ export interface ClientResponse<T = any> {
|
|
|
49
51
|
responseData: IApiResponse<T>;
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
function buildBrowserProxyUrl(rawUrl: string): string {
|
|
55
|
+
if (typeof window === 'undefined') {
|
|
56
|
+
return rawUrl;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const parsedUrl = new URL(rawUrl, window.location.origin);
|
|
61
|
+
|
|
62
|
+
if (parsedUrl.origin === window.location.origin) {
|
|
63
|
+
return parsedUrl.toString();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const proxyPath = parsedUrl.pathname.replace(/^\/+/, '');
|
|
67
|
+
return `/api/proxy/${proxyPath}${parsedUrl.search}`;
|
|
68
|
+
} catch {
|
|
69
|
+
return rawUrl;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
export async function sendRequest<T = any>({ url, method, data, params, headers, signal }: RequestOptions): Promise<ClientResponse<T>> {
|
|
53
74
|
const defaultHeaders: Record<string, string> = {
|
|
54
75
|
'Accept': 'application/json',
|
|
@@ -69,24 +90,10 @@ export async function sendRequest<T = any>({ url, method, data, params, headers,
|
|
|
69
90
|
}
|
|
70
91
|
}
|
|
71
92
|
|
|
72
|
-
//
|
|
93
|
+
// Client-side requests are routed through a same-origin proxy so the
|
|
94
|
+
// browser never needs direct access to the bearer token.
|
|
73
95
|
if (typeof window !== 'undefined') {
|
|
74
|
-
|
|
75
|
-
cookie.access_token.encrypted ? cookie.access_token.secretName : cookie.access_token.name,
|
|
76
|
-
{}
|
|
77
|
-
);
|
|
78
|
-
const tokenValue = Cookies.get(name);
|
|
79
|
-
|
|
80
|
-
if (tokenValue) {
|
|
81
|
-
try {
|
|
82
|
-
const token = cookie.access_token.encrypted ? decrypt(tokenValue) : tokenValue;
|
|
83
|
-
if (token) {
|
|
84
|
-
defaultHeaders['Authorization'] = `Bearer ${token}`;
|
|
85
|
-
}
|
|
86
|
-
} catch (error) {
|
|
87
|
-
console.error("Failed to process auth token:", error);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
96
|
+
finalUrl = buildBrowserProxyUrl(finalUrl);
|
|
90
97
|
|
|
91
98
|
// Automatically inject selected organization ID into every request
|
|
92
99
|
try {
|