1
0
forked from baron/baron-sso

테넌트 목록 조회 cursor기반으로 재구성. 사용자 metadata 미사용 필드 제거

This commit is contained in:
2026-05-13 18:05:51 +09:00
parent a4d707d4d8
commit 5e7b7b878c
85 changed files with 4808 additions and 734 deletions

View File

@@ -11,6 +11,13 @@ export type CommonOidcConfigOptions<TUserStore = unknown> = {
userStore: TUserStore;
};
export type LoginRedirectGuardParams = {
pathname: string;
isRedirecting: boolean;
loginPath?: string;
callbackPath?: string;
};
type CommonOidcRuntimeConfig<TUserStore> = {
authority: string;
client_id: string;
@@ -61,3 +68,20 @@ export function buildCommonUserManagerSettings<
redirect_uri: config.redirect_uri || "",
};
}
export function shouldStartLoginRedirect({
pathname,
isRedirecting,
loginPath = "/login",
callbackPath = DEFAULT_OIDC_REDIRECT_PATH,
}: LoginRedirectGuardParams) {
if (isRedirecting) {
return false;
}
if (pathname === loginPath || pathname.startsWith(callbackPath)) {
return false;
}
return true;
}

View File

@@ -0,0 +1,82 @@
import type { CursorFetchRequest, CursorPageResponse } from "./cursorFetchCore";
import { fetchAllCursorPagesMainThread } from "./cursorFetchCore";
type CursorWorkerResponseMessage<TItem> =
| {
id: string;
ok: true;
response: CursorPageResponse<TItem>;
}
| {
id: string;
ok: false;
error: string;
};
function createRequestId() {
if (globalThis.crypto?.randomUUID) {
return globalThis.crypto.randomUUID();
}
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
function shouldUseWorker(useWorker: boolean | undefined) {
if (useWorker === false || typeof Worker === "undefined") {
return false;
}
const maybeWindow = globalThis as typeof globalThis & {
window?: Window & typeof globalThis & { _IS_TEST_MODE?: boolean };
};
return maybeWindow.window?._IS_TEST_MODE !== true;
}
async function fetchAllCursorPagesInWorker<TItem>(
request: CursorFetchRequest,
): Promise<CursorPageResponse<TItem>> {
const worker = new Worker(new URL("./cursorFetch.worker.ts", import.meta.url), {
type: "module",
});
const id = createRequestId();
return new Promise((resolve, reject) => {
worker.onmessage = (
event: MessageEvent<CursorWorkerResponseMessage<TItem>>,
) => {
if (event.data.id !== id) {
return;
}
worker.terminate();
if (event.data.ok) {
resolve(event.data.response);
} else {
reject(new Error(event.data.error));
}
};
worker.onerror = (event) => {
worker.terminate();
reject(new Error(event.message || "Cursor worker failed"));
};
worker.postMessage({ id, request });
});
}
export async function fetchAllCursorPages<TItem>(
request: CursorFetchRequest & { useWorker?: boolean },
): Promise<CursorPageResponse<TItem>> {
if (shouldUseWorker(request.useWorker)) {
try {
return await fetchAllCursorPagesInWorker<TItem>(request);
} catch {
return fetchAllCursorPagesMainThread<TItem>(request);
}
}
return fetchAllCursorPagesMainThread<TItem>(request);
}
export type { CursorFetchRequest, CursorPageResponse } from "./cursorFetchCore";
export { fetchAllCursorPagesMainThread } from "./cursorFetchCore";

View File

@@ -0,0 +1,43 @@
import {
fetchAllCursorPagesMainThread,
type CursorFetchRequest,
type CursorPageResponse,
} from "./cursorFetchCore";
type CursorWorkerRequestMessage = {
id: string;
request: CursorFetchRequest;
};
type CursorWorkerResponseMessage<TItem> =
| {
id: string;
ok: true;
response: CursorPageResponse<TItem>;
}
| {
id: string;
ok: false;
error: string;
};
self.addEventListener("message", async (event: MessageEvent<CursorWorkerRequestMessage>) => {
const { id, request } = event.data;
try {
const response = await fetchAllCursorPagesMainThread(request);
self.postMessage({
id,
ok: true,
response,
} satisfies CursorWorkerResponseMessage<unknown>);
} catch (error) {
self.postMessage({
id,
ok: false,
error: error instanceof Error ? error.message : String(error),
} satisfies CursorWorkerResponseMessage<unknown>);
}
});
export {};

View File

@@ -0,0 +1,106 @@
export type CursorPageResponse<TItem> = {
items: TItem[];
limit?: number;
offset?: number;
total?: number;
cursor?: string;
nextCursor?: string;
next_cursor?: string;
};
export type CursorFetchParams = Record<
string,
string | number | boolean | null | undefined
>;
export type CursorFetchRequest = {
baseUrl: string;
path: string;
pageSize?: number;
params?: CursorFetchParams;
headers?: Record<string, string>;
credentials?: RequestCredentials;
maxPages?: number;
};
function normalizeBaseUrl(baseUrl: string) {
const value = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
return new URL(value, globalThis.location?.origin ?? "http://localhost");
}
function buildCursorFetchUrl(
request: Required<Pick<CursorFetchRequest, "baseUrl" | "path">> &
Pick<CursorFetchRequest, "params">,
pageSize: number,
cursor: string | undefined,
) {
const path = request.path.replace(/^\/+/, "");
const url = new URL(path, normalizeBaseUrl(request.baseUrl));
for (const [key, value] of Object.entries(request.params ?? {})) {
if (value !== undefined && value !== null && value !== "") {
url.searchParams.set(key, String(value));
}
}
url.searchParams.set("limit", String(pageSize));
url.searchParams.set("offset", "0");
if (cursor) {
url.searchParams.set("cursor", cursor);
} else {
url.searchParams.delete("cursor");
}
return url;
}
function readNextCursor<TItem>(page: CursorPageResponse<TItem>) {
return page.nextCursor || page.next_cursor || undefined;
}
export async function fetchAllCursorPagesMainThread<TItem>({
pageSize = 100,
credentials = "same-origin",
maxPages = 1000,
...request
}: CursorFetchRequest): Promise<CursorPageResponse<TItem>> {
const items: TItem[] = [];
let cursor: string | undefined;
for (let pageIndex = 0; pageIndex < maxPages; pageIndex += 1) {
const url = buildCursorFetchUrl(request, pageSize, cursor);
const response = await fetch(url, {
headers: request.headers,
credentials,
});
if (!response.ok) {
throw new Error(`Cursor page request failed with status ${response.status}`);
}
const page = (await response.json()) as CursorPageResponse<TItem>;
items.push(...page.items);
const nextCursor = readNextCursor(page);
if (!nextCursor) {
return {
...page,
items,
limit: pageSize,
offset: 0,
total: items.length,
cursor,
nextCursor: undefined,
next_cursor: undefined,
};
}
if (nextCursor === cursor) {
throw new Error("Cursor page request returned the same next cursor");
}
cursor = nextCursor;
}
throw new Error(`Cursor page request exceeded ${maxPages} pages`);
}

View File

@@ -0,0 +1,6 @@
export {
fetchAllCursorPages,
fetchAllCursorPagesMainThread,
type CursorFetchRequest,
type CursorPageResponse,
} from "./cursorFetch";