@jcoder-stack/abp-react 0.1.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/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/application-configuration-DhOZRqtz.d.ts +170 -0
- package/dist/auth.d.ts +56 -0
- package/dist/auth.js +58 -0
- package/dist/chunk-HWT6PBBS.js +155 -0
- package/dist/chunk-OK6PUP2E.js +21 -0
- package/dist/chunk-QQFH53SC.js +700 -0
- package/dist/chunk-UDMHSDXZ.js +161 -0
- package/dist/chunk-XXJLSSG3.js +116 -0
- package/dist/core.d.ts +65 -0
- package/dist/core.js +24 -0
- package/dist/i18n.d.ts +18 -0
- package/dist/i18n.js +12 -0
- package/dist/is-granted-C0-1wvoW.d.ts +10 -0
- package/dist/logger-BSnS65IC.d.ts +60 -0
- package/dist/logger.d.ts +26 -0
- package/dist/logger.js +26 -0
- package/dist/oidc-7fu5kKVF.d.ts +202 -0
- package/dist/permissions.d.ts +18 -0
- package/dist/permissions.js +8 -0
- package/dist/proxy.d.ts +201 -0
- package/dist/proxy.js +535 -0
- package/dist/react.d.ts +125 -0
- package/dist/react.js +236 -0
- package/dist/router.d.ts +39 -0
- package/dist/router.js +41 -0
- package/dist/translator-B3hyoZmK.d.ts +40 -0
- package/dist/types-Bj0MpXtI.d.ts +97 -0
- package/package.json +116 -0
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLogger,
|
|
3
|
+
resolveConfig
|
|
4
|
+
} from "./chunk-UDMHSDXZ.js";
|
|
5
|
+
import {
|
|
6
|
+
AuthError,
|
|
7
|
+
clearCookie,
|
|
8
|
+
createAuth,
|
|
9
|
+
createCodec,
|
|
10
|
+
createCookieSessionStore,
|
|
11
|
+
createTokenClient,
|
|
12
|
+
formatCultureCookie,
|
|
13
|
+
handshakeSchema,
|
|
14
|
+
oidcStrategy,
|
|
15
|
+
parseCookieHeader,
|
|
16
|
+
parseCultureCookie,
|
|
17
|
+
passwordStrategy,
|
|
18
|
+
sanitizeReturnUrl,
|
|
19
|
+
serializeCookie
|
|
20
|
+
} from "./chunk-QQFH53SC.js";
|
|
21
|
+
import {
|
|
22
|
+
parseApplicationConfiguration,
|
|
23
|
+
toHttpError
|
|
24
|
+
} from "./chunk-HWT6PBBS.js";
|
|
25
|
+
|
|
26
|
+
// src/proxy/abp-call.ts
|
|
27
|
+
var TENANT_COOKIE = "__tenant";
|
|
28
|
+
var CULTURE_COOKIE = ".AspNetCore.Culture";
|
|
29
|
+
function buildPolicyHeaders(session, cookieHeader) {
|
|
30
|
+
const cookies = parseCookieHeader(cookieHeader);
|
|
31
|
+
const headers = {};
|
|
32
|
+
const tenant = session?.tenant ?? cookies[TENANT_COOKIE];
|
|
33
|
+
if (tenant) headers.__tenant = tenant;
|
|
34
|
+
const culture = parseCultureCookie(cookies[CULTURE_COOKIE]) ?? session?.culture;
|
|
35
|
+
if (culture) headers["Accept-Language"] = culture;
|
|
36
|
+
return headers;
|
|
37
|
+
}
|
|
38
|
+
var POLICY_HEADERS = /* @__PURE__ */ new Set(["__tenant", "accept-language"]);
|
|
39
|
+
function callAbpWithSession(rt, session, cookieHeader, req) {
|
|
40
|
+
const policy = buildPolicyHeaders(session, cookieHeader);
|
|
41
|
+
const callerHeaders = Object.fromEntries(
|
|
42
|
+
Object.entries(req.headers ?? {}).filter(([key]) => !POLICY_HEADERS.has(key.toLowerCase()))
|
|
43
|
+
);
|
|
44
|
+
return rt.proxy.send(
|
|
45
|
+
{ ...req, headers: { ...callerHeaders, ...policy } },
|
|
46
|
+
{
|
|
47
|
+
session,
|
|
48
|
+
refresh: () => session === null ? Promise.resolve(null) : rt.auth.session.refresh(session, cookieHeader)
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/proxy/abp-identity.ts
|
|
54
|
+
var APP_CONFIG_PATH = "/api/abp/application-configuration";
|
|
55
|
+
function deriveIdentity(config, opts = {}) {
|
|
56
|
+
const user = config.currentUser;
|
|
57
|
+
const id = user.isAuthenticated ? user.id ?? null : null;
|
|
58
|
+
if (user.isAuthenticated && id === null) {
|
|
59
|
+
opts.logger?.warn("application-configuration reported an authenticated user without an id", {
|
|
60
|
+
userName: user.userName ?? void 0
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
isAuthenticated: id !== null,
|
|
65
|
+
user: id === null ? null : {
|
|
66
|
+
id,
|
|
67
|
+
userName: user.userName ?? "",
|
|
68
|
+
email: user.email ?? void 0,
|
|
69
|
+
roles: user.roles
|
|
70
|
+
},
|
|
71
|
+
grantedPolicies: config.auth.grantedPolicies,
|
|
72
|
+
tenant: config.currentTenant.id === null ? null : { id: config.currentTenant.id, name: config.currentTenant.name }
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function loadAppState(rt, session, cookieHeader) {
|
|
76
|
+
const res = await callAbpWithSession(rt, session, cookieHeader, {
|
|
77
|
+
path: APP_CONFIG_PATH,
|
|
78
|
+
method: "GET"
|
|
79
|
+
});
|
|
80
|
+
if (res.status >= 400) {
|
|
81
|
+
let payload2;
|
|
82
|
+
if (typeof res.body === "string") {
|
|
83
|
+
try {
|
|
84
|
+
payload2 = JSON.parse(res.body);
|
|
85
|
+
} catch {
|
|
86
|
+
payload2 = res.body;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
throw toHttpError(res.status, payload2);
|
|
90
|
+
}
|
|
91
|
+
let payload;
|
|
92
|
+
try {
|
|
93
|
+
if (typeof res.body !== "string")
|
|
94
|
+
throw new Error("non-text application-configuration response");
|
|
95
|
+
payload = JSON.parse(res.body);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
rt.logger.warn("application-configuration returned non-JSON body", {
|
|
98
|
+
status: res.status,
|
|
99
|
+
error: String(error)
|
|
100
|
+
});
|
|
101
|
+
throw toHttpError(res.status, typeof res.body === "string" ? res.body : void 0);
|
|
102
|
+
}
|
|
103
|
+
const config = parseApplicationConfiguration(payload, {
|
|
104
|
+
onError: (error) => rt.logger.warn("application-configuration shape drift", { error: String(error) })
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
config,
|
|
108
|
+
identity: deriveIdentity(config, { logger: rt.logger }),
|
|
109
|
+
setCookies: res.setCookies
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function createAbpIdentityResolver(rt) {
|
|
113
|
+
return async (session, ctx) => (await loadAppState(rt(), session, ctx.cookieHeader)).identity;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/proxy/auth-env.ts
|
|
117
|
+
import { z } from "zod";
|
|
118
|
+
var abpAuthEnvSchema = z.object({
|
|
119
|
+
issuer: z.string().url(),
|
|
120
|
+
clientId: z.string().min(1),
|
|
121
|
+
clientSecret: z.string().optional(),
|
|
122
|
+
scope: z.string().default("openid profile"),
|
|
123
|
+
redirectUri: z.string().url(),
|
|
124
|
+
postLogoutRedirectUri: z.string().url().optional(),
|
|
125
|
+
sessionSecret: z.string().min(32),
|
|
126
|
+
abpBaseUrl: z.string().url(),
|
|
127
|
+
debug: z.boolean().default(false)
|
|
128
|
+
});
|
|
129
|
+
function resolveAbpAuthEnv(env, opts = {}) {
|
|
130
|
+
const schema = opts.schema ?? abpAuthEnvSchema;
|
|
131
|
+
return schema.parse({
|
|
132
|
+
issuer: env.AUTH_ISSUER,
|
|
133
|
+
clientId: env.AUTH_CLIENT_ID,
|
|
134
|
+
clientSecret: env.AUTH_CLIENT_SECRET,
|
|
135
|
+
scope: env.AUTH_SCOPE,
|
|
136
|
+
redirectUri: env.AUTH_REDIRECT_URI,
|
|
137
|
+
postLogoutRedirectUri: env.AUTH_POST_LOGOUT_REDIRECT_URI,
|
|
138
|
+
sessionSecret: env.AUTH_SESSION_SECRET,
|
|
139
|
+
abpBaseUrl: env.AUTH_ABP_BASE_URL,
|
|
140
|
+
debug: env.AUTH_DEBUG === "true"
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/proxy/proxy.ts
|
|
145
|
+
var AbpProxyError = class extends Error {
|
|
146
|
+
constructor(message, setCookies, opts) {
|
|
147
|
+
super(message, opts);
|
|
148
|
+
this.setCookies = setCookies;
|
|
149
|
+
this.name = "AbpProxyError";
|
|
150
|
+
}
|
|
151
|
+
setCookies;
|
|
152
|
+
};
|
|
153
|
+
var IDEMPOTENT = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
154
|
+
var isRetryableStatus = (status) => status >= 500 || status === 429;
|
|
155
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
156
|
+
var discardBody = (res) => res.body?.cancel().catch(() => {
|
|
157
|
+
});
|
|
158
|
+
var FORWARDABLE = /* @__PURE__ */ new Set([
|
|
159
|
+
"content-type",
|
|
160
|
+
"accept",
|
|
161
|
+
"accept-language",
|
|
162
|
+
"if-match",
|
|
163
|
+
"if-none-match",
|
|
164
|
+
"x-requested-with",
|
|
165
|
+
"content-disposition",
|
|
166
|
+
"__tenant"
|
|
167
|
+
]);
|
|
168
|
+
var EXPOSED_RESPONSE_HEADERS = [
|
|
169
|
+
"content-type",
|
|
170
|
+
"content-disposition",
|
|
171
|
+
"content-length",
|
|
172
|
+
"etag",
|
|
173
|
+
"cache-control",
|
|
174
|
+
"last-modified"
|
|
175
|
+
];
|
|
176
|
+
function exposeHeaders(headers) {
|
|
177
|
+
const out = new Headers();
|
|
178
|
+
for (const name of EXPOSED_RESPONSE_HEADERS) {
|
|
179
|
+
const value = headers.get(name);
|
|
180
|
+
if (value !== null) out.set(name, value);
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
function sanitizeHeaders(headers) {
|
|
185
|
+
if (headers === void 0) return {};
|
|
186
|
+
return Object.fromEntries(
|
|
187
|
+
Object.entries(headers).filter(([key]) => FORWARDABLE.has(key.toLowerCase()))
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
var ABSOLUTE_OR_PROTOCOL_RELATIVE = /^([a-z][a-z0-9+.-]*:)?\/\//i;
|
|
191
|
+
function resolveTargetUrl(path, baseUrl) {
|
|
192
|
+
if (ABSOLUTE_OR_PROTOCOL_RELATIVE.test(path)) {
|
|
193
|
+
throw new Error("abp proxy: path escapes baseUrl");
|
|
194
|
+
}
|
|
195
|
+
const base = new URL(baseUrl);
|
|
196
|
+
const basePath = base.pathname.endsWith("/") ? base.pathname : `${base.pathname}/`;
|
|
197
|
+
const target = new URL(path.replace(/^\//, ""), new URL(basePath, base.origin));
|
|
198
|
+
if (target.origin !== base.origin || !`${target.pathname}/`.startsWith(basePath)) {
|
|
199
|
+
throw new Error("abp proxy: path escapes baseUrl");
|
|
200
|
+
}
|
|
201
|
+
return target.toString();
|
|
202
|
+
}
|
|
203
|
+
function createAbpProxy(opts) {
|
|
204
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
205
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
206
|
+
const retries = opts.retry?.retries ?? 2;
|
|
207
|
+
return {
|
|
208
|
+
async send(req, auth) {
|
|
209
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
210
|
+
const maxRetries = IDEMPOTENT.has(method) ? retries : 0;
|
|
211
|
+
const url = resolveTargetUrl(req.path, opts.baseUrl);
|
|
212
|
+
const budget = opts.totalTimeoutMs === void 0 ? void 0 : AbortSignal.timeout(opts.totalTimeoutMs);
|
|
213
|
+
const stops = [req.signal, budget].filter((signal) => signal !== void 0);
|
|
214
|
+
const stopped = () => stops.some((signal) => signal.aborted);
|
|
215
|
+
const stopReason = () => stops.find((signal) => signal.aborted)?.reason;
|
|
216
|
+
let session = auth.session;
|
|
217
|
+
let setCookies = [];
|
|
218
|
+
let refreshedOnce = false;
|
|
219
|
+
let attempt = 0;
|
|
220
|
+
const backoff = async () => {
|
|
221
|
+
await sleep(2 ** attempt * 100);
|
|
222
|
+
attempt++;
|
|
223
|
+
return !stopped();
|
|
224
|
+
};
|
|
225
|
+
for (; ; ) {
|
|
226
|
+
let res;
|
|
227
|
+
try {
|
|
228
|
+
res = await fetchFn(url, {
|
|
229
|
+
method,
|
|
230
|
+
headers: {
|
|
231
|
+
...sanitizeHeaders(req.headers),
|
|
232
|
+
...session === null ? {} : { Authorization: `Bearer ${session.tokens.accessToken}` }
|
|
233
|
+
},
|
|
234
|
+
body: req.body,
|
|
235
|
+
signal: AbortSignal.any([...stops, AbortSignal.timeout(timeoutMs)])
|
|
236
|
+
});
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (attempt < maxRetries && !stopped()) {
|
|
239
|
+
opts.logger?.debug("proxy retry after network error", { attempt, path: req.path });
|
|
240
|
+
if (await backoff()) continue;
|
|
241
|
+
}
|
|
242
|
+
if (setCookies.length > 0) {
|
|
243
|
+
throw new AbpProxyError("abp proxy request failed after refresh", setCookies, {
|
|
244
|
+
cause: error
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
if (res.status === 401 && !refreshedOnce && session?.tokens.refreshToken !== void 0) {
|
|
250
|
+
refreshedOnce = true;
|
|
251
|
+
const refreshed = await auth.refresh();
|
|
252
|
+
if (refreshed !== null) {
|
|
253
|
+
session = refreshed.session;
|
|
254
|
+
setCookies = refreshed.setCookies;
|
|
255
|
+
opts.logger?.debug("proxy replaying after refresh", { path: req.path });
|
|
256
|
+
await discardBody(res);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (isRetryableStatus(res.status) && attempt < maxRetries && !stopped()) {
|
|
261
|
+
opts.logger?.debug("proxy retry", { attempt, status: res.status, path: req.path });
|
|
262
|
+
await discardBody(res);
|
|
263
|
+
if (await backoff()) continue;
|
|
264
|
+
if (setCookies.length > 0) {
|
|
265
|
+
throw new AbpProxyError("abp proxy request aborted after refresh", setCookies, {
|
|
266
|
+
cause: stopReason()
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
throw stopReason();
|
|
270
|
+
}
|
|
271
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
272
|
+
const isText = /^text\/|[+/]json|[+/]xml|urlencoded/i.test(contentType) || contentType === "";
|
|
273
|
+
return {
|
|
274
|
+
status: res.status,
|
|
275
|
+
headers: exposeHeaders(res.headers),
|
|
276
|
+
body: isText ? await res.text() : await res.arrayBuffer(),
|
|
277
|
+
setCookies
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/proxy/auth-runtime.ts
|
|
285
|
+
var DEFAULT_SESSION_COOKIE = "auth_session";
|
|
286
|
+
var DEFAULT_LOGIN_COOKIE = "auth_login";
|
|
287
|
+
var DEFAULT_SESSION_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
|
288
|
+
var DEFAULT_LOGIN_COOKIE_MAX_AGE = 600;
|
|
289
|
+
var SWITCH_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
|
|
290
|
+
function cookieAttributesOf(settings) {
|
|
291
|
+
return { secure: settings.secure, sameSite: settings.sameSite };
|
|
292
|
+
}
|
|
293
|
+
function createAbpAuthRuntime(envRecord, opts = {}) {
|
|
294
|
+
const env = resolveAbpAuthEnv(envRecord, { schema: opts.envSchema });
|
|
295
|
+
const cookies = {
|
|
296
|
+
session: {
|
|
297
|
+
...opts.cookies?.session,
|
|
298
|
+
name: opts.cookies?.session?.name ?? DEFAULT_SESSION_COOKIE,
|
|
299
|
+
maxAge: opts.cookies?.session?.maxAge ?? DEFAULT_SESSION_COOKIE_MAX_AGE
|
|
300
|
+
},
|
|
301
|
+
login: {
|
|
302
|
+
...opts.cookies?.login,
|
|
303
|
+
name: opts.cookies?.login?.name ?? DEFAULT_LOGIN_COOKIE,
|
|
304
|
+
maxAge: opts.cookies?.login?.maxAge ?? DEFAULT_LOGIN_COOKIE_MAX_AGE
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const logger = opts.logger ?? createLogger({
|
|
308
|
+
scope: "auth",
|
|
309
|
+
config: resolveConfig({ LOG_LEVEL: env.debug ? "debug" : "info" })
|
|
310
|
+
});
|
|
311
|
+
const tokenClient = createTokenClient({
|
|
312
|
+
issuer: env.issuer,
|
|
313
|
+
clientId: env.clientId,
|
|
314
|
+
clientSecret: env.clientSecret,
|
|
315
|
+
scope: env.scope,
|
|
316
|
+
fetchFn: opts.fetchFn,
|
|
317
|
+
logger,
|
|
318
|
+
// `__tenant` 是 ABP 的多租户约定,不属于 OIDC 协议,由这层注入,协议客户端保持后端无关。
|
|
319
|
+
tenantPropagation: (tenant) => ({
|
|
320
|
+
headers: { [TENANT_COOKIE]: tenant },
|
|
321
|
+
query: { [TENANT_COOKIE]: tenant }
|
|
322
|
+
})
|
|
323
|
+
});
|
|
324
|
+
const oidc = oidcStrategy({
|
|
325
|
+
tokenClient,
|
|
326
|
+
redirectUri: env.redirectUri,
|
|
327
|
+
now: opts.now,
|
|
328
|
+
logger,
|
|
329
|
+
// 服务端寿命跟着握手 cookie 的 maxAge 走,两者错开会让延长 cookie 寿命变成静默的 handshake_expired。
|
|
330
|
+
handshakeMaxAgeSeconds: cookies.login.maxAge
|
|
331
|
+
});
|
|
332
|
+
const strategies = [];
|
|
333
|
+
if (opts.strategies?.oidc !== false) strategies.push(oidc);
|
|
334
|
+
if (opts.strategies?.password !== false)
|
|
335
|
+
strategies.push(passwordStrategy({ tokenClient, now: opts.now, logger }));
|
|
336
|
+
const proxy = createAbpProxy({
|
|
337
|
+
baseUrl: env.abpBaseUrl,
|
|
338
|
+
fetchFn: opts.fetchFn,
|
|
339
|
+
timeoutMs: opts.proxy?.timeoutMs,
|
|
340
|
+
retry: opts.proxy?.retries === void 0 ? void 0 : { retries: opts.proxy.retries },
|
|
341
|
+
totalTimeoutMs: opts.proxy?.totalTimeoutMs,
|
|
342
|
+
logger
|
|
343
|
+
});
|
|
344
|
+
const callRuntime = {
|
|
345
|
+
proxy,
|
|
346
|
+
logger,
|
|
347
|
+
get auth() {
|
|
348
|
+
return auth;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const auth = createAuth({
|
|
352
|
+
strategies,
|
|
353
|
+
store: createCookieSessionStore({
|
|
354
|
+
secret: env.sessionSecret,
|
|
355
|
+
cookieName: cookies.session.name,
|
|
356
|
+
maxAge: cookies.session.maxAge,
|
|
357
|
+
cookieOptions: cookieAttributesOf(cookies.session),
|
|
358
|
+
logger
|
|
359
|
+
}),
|
|
360
|
+
resolveIdentity: opts.resolveIdentity ?? createAbpIdentityResolver(() => callRuntime),
|
|
361
|
+
refreshGrant: tokenClient.refreshGrant,
|
|
362
|
+
revoke: tokenClient.revoke,
|
|
363
|
+
logger,
|
|
364
|
+
now: opts.now,
|
|
365
|
+
skewSeconds: opts.session?.skewSeconds,
|
|
366
|
+
coalesceTtlMs: opts.session?.coalesceTtlMs,
|
|
367
|
+
revokeTimeoutMs: opts.session?.revokeTimeoutMs
|
|
368
|
+
});
|
|
369
|
+
return {
|
|
370
|
+
env,
|
|
371
|
+
auth,
|
|
372
|
+
oidc,
|
|
373
|
+
proxy,
|
|
374
|
+
handshakeCodec: createCodec(env.sessionSecret, handshakeSchema, {
|
|
375
|
+
usage: "handshake",
|
|
376
|
+
onError: (error) => logger.debug("handshake cookie open failed", { error: String(error) })
|
|
377
|
+
}),
|
|
378
|
+
logger,
|
|
379
|
+
cookies,
|
|
380
|
+
postLogoutRedirectUri: opts.postLogoutRedirectUri ?? env.postLogoutRedirectUri
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/proxy/auth-handlers.ts
|
|
385
|
+
function originOf(value) {
|
|
386
|
+
if (value === null || value === "null") return null;
|
|
387
|
+
try {
|
|
388
|
+
return new URL(value).origin;
|
|
389
|
+
} catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function isSameSiteRequest(request) {
|
|
394
|
+
const fetchSite = request.headers.get("sec-fetch-site");
|
|
395
|
+
if (fetchSite !== null) return fetchSite === "same-origin" || fetchSite === "none";
|
|
396
|
+
const selfOrigin = new URL(request.url).origin;
|
|
397
|
+
const origin = originOf(request.headers.get("origin"));
|
|
398
|
+
if (origin !== null) return origin === selfOrigin;
|
|
399
|
+
const referer = originOf(request.headers.get("referer"));
|
|
400
|
+
if (referer !== null) return referer === selfOrigin;
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
var CROSS_SITE_RESPONSE = () => new Response("cross-site request rejected", { status: 403 });
|
|
404
|
+
async function handleLogin(request, rt) {
|
|
405
|
+
const url = new URL(request.url);
|
|
406
|
+
const cookies = parseCookieHeader(request.headers.get("cookie"));
|
|
407
|
+
const { redirectUrl, handshake } = await rt.oidc.begin({
|
|
408
|
+
returnUrl: sanitizeReturnUrl(url.searchParams.get("returnUrl")),
|
|
409
|
+
tenant: cookies[TENANT_COOKIE] ?? null
|
|
410
|
+
});
|
|
411
|
+
const headers = new Headers({ Location: redirectUrl });
|
|
412
|
+
headers.append(
|
|
413
|
+
"Set-Cookie",
|
|
414
|
+
serializeCookie(rt.cookies.login.name, await rt.handshakeCodec.seal(handshake), {
|
|
415
|
+
...cookieAttributesOf(rt.cookies.login),
|
|
416
|
+
maxAge: rt.cookies.login.maxAge
|
|
417
|
+
})
|
|
418
|
+
);
|
|
419
|
+
return new Response(null, { status: 302, headers });
|
|
420
|
+
}
|
|
421
|
+
function callbackFailure(rt, code) {
|
|
422
|
+
const headers = new Headers({ Location: `/login?error=${code}` });
|
|
423
|
+
headers.append(
|
|
424
|
+
"Set-Cookie",
|
|
425
|
+
clearCookie(rt.cookies.login.name, cookieAttributesOf(rt.cookies.login))
|
|
426
|
+
);
|
|
427
|
+
return new Response(null, { status: 302, headers });
|
|
428
|
+
}
|
|
429
|
+
async function handleCallback(request, rt) {
|
|
430
|
+
const url = new URL(request.url);
|
|
431
|
+
const cookies = parseCookieHeader(request.headers.get("cookie"));
|
|
432
|
+
const sealed = cookies[rt.cookies.login.name];
|
|
433
|
+
const handshake = sealed === void 0 ? null : await rt.handshakeCodec.open(sealed);
|
|
434
|
+
if (handshake === null) return callbackFailure(rt, "session_open_failed");
|
|
435
|
+
try {
|
|
436
|
+
const result = await rt.oidc.complete({
|
|
437
|
+
kind: "callback",
|
|
438
|
+
params: url.searchParams,
|
|
439
|
+
handshake
|
|
440
|
+
});
|
|
441
|
+
const setCookies = await rt.auth.session.establish(result, {
|
|
442
|
+
tenant: cookies[TENANT_COOKIE] ?? null,
|
|
443
|
+
culture: parseCultureCookie(cookies[CULTURE_COOKIE]),
|
|
444
|
+
cookieHeader: request.headers.get("cookie")
|
|
445
|
+
});
|
|
446
|
+
const headers = new Headers({ Location: sanitizeReturnUrl(handshake.returnUrl) });
|
|
447
|
+
for (const cookie of setCookies) headers.append("Set-Cookie", cookie);
|
|
448
|
+
headers.append(
|
|
449
|
+
"Set-Cookie",
|
|
450
|
+
clearCookie(rt.cookies.login.name, cookieAttributesOf(rt.cookies.login))
|
|
451
|
+
);
|
|
452
|
+
return new Response(null, { status: 302, headers });
|
|
453
|
+
} catch (error) {
|
|
454
|
+
const code = error instanceof AuthError ? error.code : "exchange_failed";
|
|
455
|
+
rt.logger.warn("login callback failed", { code });
|
|
456
|
+
return callbackFailure(rt, code);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async function handleLogout(request, rt) {
|
|
460
|
+
if (!isSameSiteRequest(request)) return CROSS_SITE_RESPONSE();
|
|
461
|
+
const cookieHeader = request.headers.get("cookie");
|
|
462
|
+
const session = await rt.auth.session.current(cookieHeader);
|
|
463
|
+
let location = rt.postLogoutRedirectUri ?? "/";
|
|
464
|
+
try {
|
|
465
|
+
location = await rt.oidc.logoutUrl({
|
|
466
|
+
idToken: session?.tokens.idToken,
|
|
467
|
+
postLogoutRedirectUri: rt.postLogoutRedirectUri
|
|
468
|
+
}) ?? location;
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
const headers = new Headers({ Location: location });
|
|
472
|
+
for (const cookie of await rt.auth.session.destroy(cookieHeader)) {
|
|
473
|
+
headers.append("Set-Cookie", cookie);
|
|
474
|
+
}
|
|
475
|
+
return new Response(null, { status: 302, headers });
|
|
476
|
+
}
|
|
477
|
+
var TENANT_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
478
|
+
var CULTURE_PATTERN = /^[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8}){0,4}$/;
|
|
479
|
+
function handleSetCulture(request) {
|
|
480
|
+
if (!isSameSiteRequest(request)) return CROSS_SITE_RESPONSE();
|
|
481
|
+
const url = new URL(request.url);
|
|
482
|
+
const culture = url.searchParams.get("culture");
|
|
483
|
+
if (!culture) return new Response("culture is required", { status: 400 });
|
|
484
|
+
if (!CULTURE_PATTERN.test(culture)) {
|
|
485
|
+
return new Response("culture is not a valid BCP-47 tag", { status: 400 });
|
|
486
|
+
}
|
|
487
|
+
const headers = new Headers({ Location: sanitizeReturnUrl(url.searchParams.get("returnUrl")) });
|
|
488
|
+
headers.append(
|
|
489
|
+
"Set-Cookie",
|
|
490
|
+
serializeCookie(CULTURE_COOKIE, formatCultureCookie(culture), {
|
|
491
|
+
httpOnly: false,
|
|
492
|
+
maxAge: SWITCH_COOKIE_MAX_AGE
|
|
493
|
+
})
|
|
494
|
+
);
|
|
495
|
+
return new Response(null, { status: 302, headers });
|
|
496
|
+
}
|
|
497
|
+
function handleSetTenant(request) {
|
|
498
|
+
if (!isSameSiteRequest(request)) return CROSS_SITE_RESPONSE();
|
|
499
|
+
const url = new URL(request.url);
|
|
500
|
+
const tenant = url.searchParams.get("tenant");
|
|
501
|
+
if (tenant !== null && tenant !== "" && !TENANT_PATTERN.test(tenant)) {
|
|
502
|
+
return new Response("tenant is not a valid identifier", { status: 400 });
|
|
503
|
+
}
|
|
504
|
+
const headers = new Headers({ Location: sanitizeReturnUrl(url.searchParams.get("returnUrl")) });
|
|
505
|
+
headers.append(
|
|
506
|
+
"Set-Cookie",
|
|
507
|
+
tenant ? serializeCookie(TENANT_COOKIE, tenant, { httpOnly: false, maxAge: SWITCH_COOKIE_MAX_AGE }) : clearCookie(TENANT_COOKIE, { httpOnly: false })
|
|
508
|
+
);
|
|
509
|
+
return new Response(null, { status: 302, headers });
|
|
510
|
+
}
|
|
511
|
+
export {
|
|
512
|
+
AbpProxyError,
|
|
513
|
+
CULTURE_COOKIE,
|
|
514
|
+
DEFAULT_LOGIN_COOKIE,
|
|
515
|
+
DEFAULT_LOGIN_COOKIE_MAX_AGE,
|
|
516
|
+
DEFAULT_SESSION_COOKIE,
|
|
517
|
+
DEFAULT_SESSION_COOKIE_MAX_AGE,
|
|
518
|
+
SWITCH_COOKIE_MAX_AGE,
|
|
519
|
+
TENANT_COOKIE,
|
|
520
|
+
abpAuthEnvSchema,
|
|
521
|
+
buildPolicyHeaders,
|
|
522
|
+
callAbpWithSession,
|
|
523
|
+
cookieAttributesOf,
|
|
524
|
+
createAbpAuthRuntime,
|
|
525
|
+
createAbpIdentityResolver,
|
|
526
|
+
createAbpProxy,
|
|
527
|
+
deriveIdentity,
|
|
528
|
+
handleCallback,
|
|
529
|
+
handleLogin,
|
|
530
|
+
handleLogout,
|
|
531
|
+
handleSetCulture,
|
|
532
|
+
handleSetTenant,
|
|
533
|
+
loadAppState,
|
|
534
|
+
resolveAbpAuthEnv
|
|
535
|
+
};
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { A as ApplicationConfiguration } from './application-configuration-DhOZRqtz.js';
|
|
3
|
+
import { F as FrontendCatalog, a as TranslatorOptions, T as Translator } from './translator-B3hyoZmK.js';
|
|
4
|
+
import { G as GrantedPolicies } from './is-granted-C0-1wvoW.js';
|
|
5
|
+
import { I as Identity } from './types-Bj0MpXtI.js';
|
|
6
|
+
import { PermissionChecker } from './permissions.js';
|
|
7
|
+
import 'zod';
|
|
8
|
+
|
|
9
|
+
interface AppConfigProviderProps {
|
|
10
|
+
config: ApplicationConfiguration;
|
|
11
|
+
messages?: FrontendCatalog;
|
|
12
|
+
fallbackCulture?: string;
|
|
13
|
+
/** 缺 key 回调;始终读最新一次传入的实现,可安全写成内联箭头函数。 */
|
|
14
|
+
onMissingKey?: (key: string) => void;
|
|
15
|
+
/** translator 工厂注入点(如换 ICU 引擎);只在 translator 重建时读取,不必引用稳定。 */
|
|
16
|
+
createTranslator?: (opts: TranslatorOptions) => Translator;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
/** app-config(localization/settings/features)Provider;auth 相关内容走 SessionProvider。 */
|
|
20
|
+
declare function AppConfigProvider(props: AppConfigProviderProps): ReactNode;
|
|
21
|
+
/** 解析后的 ABP application-configuration。 */
|
|
22
|
+
declare function useAppConfig(): ApplicationConfiguration;
|
|
23
|
+
/** 可调用的本地化助手:L(key, ...args)、L.plural、L.has。 */
|
|
24
|
+
interface Localize {
|
|
25
|
+
(key: string, ...args: unknown[]): string;
|
|
26
|
+
plural(key: string, count: number, ...args: unknown[]): string;
|
|
27
|
+
has(key: string): boolean;
|
|
28
|
+
}
|
|
29
|
+
declare function useLocalization(): Localize;
|
|
30
|
+
/** 当前 culture 名(如 "en"、"zh-Hans")。 */
|
|
31
|
+
declare function useCulture(): string;
|
|
32
|
+
declare function useSetting(name: string): string | undefined;
|
|
33
|
+
declare function useSettingBoolean(name: string): boolean;
|
|
34
|
+
declare function useFeature(name: string): string | undefined;
|
|
35
|
+
declare function useFeatureEnabled(name: string): boolean;
|
|
36
|
+
interface FeatureGuardProps {
|
|
37
|
+
feature: string;
|
|
38
|
+
fallback?: ReactNode;
|
|
39
|
+
children: ReactNode;
|
|
40
|
+
}
|
|
41
|
+
/** ABP feature 开启时渲染 children,否则 fallback(默认 null)。 */
|
|
42
|
+
declare function FeatureGuard(props: FeatureGuardProps): ReactNode;
|
|
43
|
+
|
|
44
|
+
/** A declarative menu node; pruned by buildMenu against permissions / features / auth. */
|
|
45
|
+
interface MenuItem<To extends string = string> {
|
|
46
|
+
key: string;
|
|
47
|
+
label: string;
|
|
48
|
+
to?: To;
|
|
49
|
+
icon?: ReactNode;
|
|
50
|
+
order?: number;
|
|
51
|
+
requiredPolicy?: string | string[];
|
|
52
|
+
requiredFeature?: string;
|
|
53
|
+
requireAuth?: boolean;
|
|
54
|
+
children?: MenuItem<To>[];
|
|
55
|
+
}
|
|
56
|
+
/** Inputs buildMenu prunes against: granted policies, feature values, and auth state. */
|
|
57
|
+
interface MenuBuildContext {
|
|
58
|
+
grantedPolicies: GrantedPolicies;
|
|
59
|
+
features?: Record<string, string | undefined>;
|
|
60
|
+
isAuthenticated?: boolean;
|
|
61
|
+
}
|
|
62
|
+
/** Prune a menu tree by permissions/features/auth; parents with no link and no surviving children are dropped; a linked parent that loses every child comes back without a `children` key at all, so renderers never draw an empty expander; sorted by order ascending. Pure. */
|
|
63
|
+
declare function buildMenu<To extends string>(items: MenuItem<To>[], ctx: MenuBuildContext): MenuItem<To>[];
|
|
64
|
+
/** Statically concatenate multiple menu lists into one (v1: no dedup). */
|
|
65
|
+
declare function mergeMenu<To extends string>(...lists: MenuItem<To>[][]): MenuItem<To>[];
|
|
66
|
+
/** Depth-first: the ancestor chain (inclusive) of the exact `to === pathname` match, else the longest `to` prefix match, else []. Pure. */
|
|
67
|
+
declare function findBreadcrumbs<To extends string>(items: MenuItem<To>[], pathname: string): MenuItem<To>[];
|
|
68
|
+
/** The breadcrumb chain for the given pathname against a menu tree; pass the pathname from your router. Memoized. */
|
|
69
|
+
declare function useBreadcrumbs<To extends string>(items: MenuItem<To>[], pathname: string): MenuItem<To>[];
|
|
70
|
+
/** The pruned menu tree for the given config, built against the current ABP context. Memoized. */
|
|
71
|
+
declare function useMenu<To extends string>(items: MenuItem<To>[]): MenuItem<To>[];
|
|
72
|
+
|
|
73
|
+
/** 会话上下文值:Identity + 派生的状态/权限检查 + 服务端重取。 */
|
|
74
|
+
interface SessionValue {
|
|
75
|
+
identity: Identity;
|
|
76
|
+
status: "authenticated" | "anonymous";
|
|
77
|
+
can: PermissionChecker;
|
|
78
|
+
/** 从服务端重取身份;被更新的注水身份或更晚发起的 reload 超越时,本次结果被丢弃。 */
|
|
79
|
+
reload(): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
interface SessionProviderProps {
|
|
82
|
+
/** SSR 注水的初始身份;prop 变化(路由 invalidate)时跟随。 */
|
|
83
|
+
identity: Identity;
|
|
84
|
+
/** reload 的取数通道(应用注入 getIdentityFn 这类 server fn)。 */
|
|
85
|
+
fetchIdentity?: () => Promise<Identity>;
|
|
86
|
+
children: ReactNode;
|
|
87
|
+
}
|
|
88
|
+
/** 以 Identity 为中心的会话 Provider;token 永远不会出现在这棵树里。 */
|
|
89
|
+
declare function SessionProvider(props: SessionProviderProps): ReactNode;
|
|
90
|
+
/** 读会话上下文;在 <SessionProvider> 外使用时抛错。 */
|
|
91
|
+
declare function useSession(): SessionValue;
|
|
92
|
+
/** 当前用户(匿名时 null)。 */
|
|
93
|
+
declare function useCurrentUser(): Identity["user"];
|
|
94
|
+
/** 能力转述表(policy → boolean);单点检查用 usePermission。 */
|
|
95
|
+
declare function useGrantedPolicies(): Record<string, boolean>;
|
|
96
|
+
declare function usePermissionChecker(): PermissionChecker;
|
|
97
|
+
/** 单个 policy(或数组=全部)是否被授予。 */
|
|
98
|
+
declare function usePermission(policy: string | string[]): boolean;
|
|
99
|
+
interface PermissionGuardBaseProps {
|
|
100
|
+
requireAuth?: boolean;
|
|
101
|
+
fallback?: ReactNode;
|
|
102
|
+
children: ReactNode;
|
|
103
|
+
}
|
|
104
|
+
/** 权限检查三选一(省略三者则只剩 requireAuth 门槛);`never` 分支让同传两个在编译期即被拒。 */
|
|
105
|
+
type PermissionGuardProps = PermissionGuardBaseProps & ({
|
|
106
|
+
policy: string | string[];
|
|
107
|
+
all?: never;
|
|
108
|
+
any?: never;
|
|
109
|
+
} | {
|
|
110
|
+
policy?: never;
|
|
111
|
+
all: string[];
|
|
112
|
+
any?: never;
|
|
113
|
+
} | {
|
|
114
|
+
policy?: never;
|
|
115
|
+
all?: never;
|
|
116
|
+
any: string[];
|
|
117
|
+
} | {
|
|
118
|
+
policy?: never;
|
|
119
|
+
all?: never;
|
|
120
|
+
any?: never;
|
|
121
|
+
});
|
|
122
|
+
/** 权限检查通过时渲染 children,否则 fallback(默认 null)。feature 门槛请外套 FeatureGuard。 */
|
|
123
|
+
declare function PermissionGuard(props: PermissionGuardProps): ReactNode;
|
|
124
|
+
|
|
125
|
+
export { AppConfigProvider, type AppConfigProviderProps, FeatureGuard, type FeatureGuardProps, type Localize, type MenuBuildContext, type MenuItem, PermissionGuard, type PermissionGuardProps, SessionProvider, type SessionProviderProps, type SessionValue, buildMenu, findBreadcrumbs, mergeMenu, useAppConfig, useBreadcrumbs, useCulture, useCurrentUser, useFeature, useFeatureEnabled, useGrantedPolicies, useLocalization, useMenu, usePermission, usePermissionChecker, useSession, useSetting, useSettingBoolean };
|