mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
Decrease page load latency
This commit is contained in:
parent
3613f30af3
commit
244ac7c9e3
@ -1,95 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { Box, Drawer, Stack, useTheme } from "@mui/joy";
|
||||
import { useState } from "react";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { AdminAppProvider } from "./use-admin-app";
|
||||
import { Header } from "./header";
|
||||
import { Icon } from '@/components/icon';
|
||||
import { OnboardingDialog } from "./onboarding-dialog";
|
||||
|
||||
const navigationItems = [
|
||||
{
|
||||
name: "Users",
|
||||
href: "/auth/users",
|
||||
icon: <Icon icon="people_outline" />,
|
||||
},
|
||||
{
|
||||
name: "Providers",
|
||||
href: "/auth/providers",
|
||||
icon: <Icon icon="device_hub" />,
|
||||
},
|
||||
{
|
||||
name: "Domains & Handlers",
|
||||
href: "/auth/urls-and-callbacks",
|
||||
icon: <Icon icon="link" />,
|
||||
},
|
||||
{
|
||||
name: "Environment",
|
||||
href: "/settings/environment",
|
||||
icon: <Icon icon="list_alt" />,
|
||||
},
|
||||
{
|
||||
name: "API Keys",
|
||||
href: "/settings/api-keys",
|
||||
icon: <Icon icon="key" />,
|
||||
},
|
||||
];
|
||||
|
||||
import SidebarLayout from "./sidebar-layout";
|
||||
|
||||
export default function Layout(props: { children: React.ReactNode, params: { projectId: string } }) {
|
||||
const theme = useTheme();
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
const isCompactMediaQuery = theme.breakpoints.down("md");
|
||||
|
||||
const headerHeight = 50;
|
||||
|
||||
return (
|
||||
<AdminAppProvider projectId={props.params.projectId}>
|
||||
<OnboardingDialog />
|
||||
<Stack
|
||||
flexGrow={1}
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Sidebar
|
||||
isCompactMediaQuery={isCompactMediaQuery}
|
||||
headerHeight={headerHeight}
|
||||
navigationItems={navigationItems}
|
||||
mode='full'
|
||||
/>
|
||||
<Stack flexGrow={1} direction="column" sx={{ overflow: 'hidden', height: '100vh' }}>
|
||||
<Header
|
||||
headerHeight={headerHeight}
|
||||
navigationItems={navigationItems}
|
||||
isCompactMediaQuery={isCompactMediaQuery}
|
||||
onShowSidebar={() => setIsSidebarOpen(true)}
|
||||
/>
|
||||
<Stack
|
||||
paddingX={{ md: 4, xs: 2 }}
|
||||
flexGrow={1}
|
||||
paddingY={2}
|
||||
minWidth={0}
|
||||
overflow='auto'
|
||||
>
|
||||
<Stack spacing={2} component="main">
|
||||
{props.children}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Drawer
|
||||
open={isSidebarOpen}
|
||||
onClose={() => setIsSidebarOpen(false)}
|
||||
>
|
||||
<Sidebar
|
||||
isCompactMediaQuery={isCompactMediaQuery}
|
||||
headerHeight={headerHeight}
|
||||
navigationItems={navigationItems}
|
||||
mode='compact'
|
||||
/>
|
||||
</Drawer>
|
||||
<SidebarLayout params={props.params}>
|
||||
{props.children}
|
||||
</SidebarLayout>
|
||||
</AdminAppProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Dialog } from "@/components/dialog";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAdminApp } from "./use-admin-app";
|
||||
@ -37,7 +39,6 @@ export function OnboardingDialog() {
|
||||
<Dialog
|
||||
titleIcon="library_add"
|
||||
title="Onboarding"
|
||||
cancelButton
|
||||
okButton={{
|
||||
label: "Continue",
|
||||
onClick: async () => setApiKey(null),
|
||||
|
||||
@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import { Drawer, Stack, useTheme } from "@mui/joy";
|
||||
import { useState } from "react";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { Header } from "./header";
|
||||
import { Icon } from '@/components/icon';
|
||||
|
||||
const navigationItems = [
|
||||
{
|
||||
name: "Users",
|
||||
href: "/auth/users",
|
||||
icon: <Icon icon="people_outline" />,
|
||||
},
|
||||
{
|
||||
name: "Providers",
|
||||
href: "/auth/providers",
|
||||
icon: <Icon icon="device_hub" />,
|
||||
},
|
||||
{
|
||||
name: "Domains & Handlers",
|
||||
href: "/auth/urls-and-callbacks",
|
||||
icon: <Icon icon="link" />,
|
||||
},
|
||||
{
|
||||
name: "Environment",
|
||||
href: "/settings/environment",
|
||||
icon: <Icon icon="list_alt" />,
|
||||
},
|
||||
{
|
||||
name: "API Keys",
|
||||
href: "/settings/api-keys",
|
||||
icon: <Icon icon="key" />,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
export default function SidebarLayout(props: { children: React.ReactNode, params: { projectId: string } }) {
|
||||
const theme = useTheme();
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
const isCompactMediaQuery = theme.breakpoints.down("md");
|
||||
|
||||
const headerHeight = 50;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
flexGrow={1}
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Sidebar
|
||||
isCompactMediaQuery={isCompactMediaQuery}
|
||||
headerHeight={headerHeight}
|
||||
navigationItems={navigationItems}
|
||||
mode='full'
|
||||
/>
|
||||
<Stack flexGrow={1} direction="column" sx={{ overflow: 'hidden', height: '100vh' }}>
|
||||
<Header
|
||||
headerHeight={headerHeight}
|
||||
navigationItems={navigationItems}
|
||||
isCompactMediaQuery={isCompactMediaQuery}
|
||||
onShowSidebar={() => setIsSidebarOpen(true)}
|
||||
/>
|
||||
<Stack
|
||||
paddingX={{ md: 4, xs: 2 }}
|
||||
flexGrow={1}
|
||||
paddingY={2}
|
||||
minWidth={0}
|
||||
overflow='auto'
|
||||
>
|
||||
<Stack spacing={2} component="main">
|
||||
{props.children}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Drawer
|
||||
open={isSidebarOpen}
|
||||
onClose={() => setIsSidebarOpen(false)}
|
||||
>
|
||||
<Sidebar
|
||||
isCompactMediaQuery={isCompactMediaQuery}
|
||||
headerHeight={headerHeight}
|
||||
navigationItems={navigationItems}
|
||||
mode='compact'
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -6,16 +6,15 @@ import { throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { cacheFunction } from "@stackframe/stack-shared/dist/utils/caches";
|
||||
import { CurrentUser, StackAdminApp } from "@stackframe/stack/dist/lib/stack-app";
|
||||
|
||||
const StackAdminAppContext = React.createContext<StackAdminApp<true> | null>(null);
|
||||
const StackAdminAppContext = React.createContext<StackAdminApp<false> | null>(null);
|
||||
|
||||
const usersMap = new Map<string, CurrentUser>();
|
||||
|
||||
const createAdminApp = cacheFunction((baseUrl: string, projectId: string, userId: string) => {
|
||||
console.log("new app", baseUrl, projectId, userId, usersMap);
|
||||
return new StackAdminApp({
|
||||
return new StackAdminApp<false, string>({
|
||||
baseUrl,
|
||||
projectId,
|
||||
tokenStore: "nextjs-cookie",
|
||||
tokenStore: null,
|
||||
projectOwnerTokens: usersMap.get(userId)!.tokenStore,
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"use client";;
|
||||
"use client";
|
||||
import { Logo } from "@/components/logo";
|
||||
import { Sheet, SheetProps, Stack } from "@mui/joy";
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ export const GET = deprecatedSmartRouteHandler(async (req: NextRequest) => {
|
||||
} = await deprecatedParseRequest(req, getSchema);
|
||||
|
||||
if (!await checkApiKeySet(projectId, { superSecretAdminKey }) && !await isProjectAdmin(projectId, adminAccessToken)) {
|
||||
throw new StatusError(StatusError.Forbidden);
|
||||
throw new StatusError(StatusError.Forbidden, "Invalid API key or insufficient permissions");
|
||||
}
|
||||
|
||||
const apiKeys = await listApiKeySets(
|
||||
|
||||
@ -7,6 +7,7 @@ import { Icon } from "./icon";
|
||||
|
||||
export const SmartLink = React.forwardRef((props: LinkProps & { hideExternalIndicator?: boolean }, ref) => {
|
||||
const [isExternal, setIsExternal] = useState(!!props.href?.match(/^[a-z]+:/));
|
||||
const [isClicked, setIsClicked] = useState(false);
|
||||
|
||||
const { hideExternalIndicator, ...linkProps } = props;
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ import * as yup from 'yup';
|
||||
import { ApiKeySetFirstViewJson, ApiKeySetJson } from '@stackframe/stack-shared';
|
||||
import { ApiKeySet } from '@prisma/client';
|
||||
import { generateSecureRandomString } from '@stackframe/stack-shared/dist/utils/crypto';
|
||||
import * as crypto from 'node:crypto';
|
||||
import { prismaClient } from '@/prisma-client';
|
||||
import { generateUuid } from '@stackframe/stack-shared/dist/utils/uuids';
|
||||
|
||||
|
||||
@ -93,7 +93,7 @@ async function parseBody(req: NextRequest): Promise<SmartRequest["body"]> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseRequest<T extends DeepPartial<SmartRequest>>(req: NextRequest, schema: yup.Schema<T>, options?: { params: Record<string, string> }): Promise<T> {
|
||||
async function parseRequest<T extends DeepPartial<SmartRequest>>(req: NextRequest, schema: yup.Schema<T>, options?: { params: Record<string, string> }): Promise<T> {
|
||||
const urlObject = new URL(req.url);
|
||||
const toValidate: SmartRequest = {
|
||||
url: req.url,
|
||||
@ -132,7 +132,7 @@ function isBinaryBody(body: unknown): body is BodyInit {
|
||||
|| ArrayBuffer.isView(body);
|
||||
}
|
||||
|
||||
export async function createResponse<T extends SmartResponse>(req: NextRequest, requestId: string, obj: T, schema: yup.Schema<T>): Promise<Response> {
|
||||
async function createResponse<T extends SmartResponse>(req: NextRequest, requestId: string, obj: T, schema: yup.Schema<T>): Promise<Response> {
|
||||
const validated = await validate(obj, schema);
|
||||
|
||||
let status = validated.statusCode;
|
||||
@ -241,7 +241,7 @@ export function deprecatedSmartRouteHandler(handler: (req: NextRequest, options:
|
||||
}
|
||||
|
||||
console.log(`[ ERR] [${requestId}] ${req.method} ${req.url}: ${statusError.message}`);
|
||||
console.debug(`For the error above with request ID ${requestId}, the full error is:`, statusError);
|
||||
console.log(`For the error above with request ID ${requestId}, the full error is:`, statusError);
|
||||
|
||||
const res = await createResponse(req, requestId, {
|
||||
statusCode: statusError.statusCode,
|
||||
|
||||
@ -2,8 +2,7 @@ import * as oauth from 'oauth4webapi';
|
||||
import crypto from "crypto";
|
||||
|
||||
import { AsyncResult, Result } from "../utils/results";
|
||||
import { ReadonlyJson, parseJson } from '../utils/json';
|
||||
import { typedAssign } from '../utils/objects';
|
||||
import { ReadonlyJson } from '../utils/json';
|
||||
import { AsyncStore, ReadonlyAsyncStore } from '../utils/stores';
|
||||
import { KnownError, KnownErrors } from '../known-errors';
|
||||
|
||||
@ -280,7 +279,15 @@ export class StackClientInterface {
|
||||
}
|
||||
|
||||
const url = this.getApiUrl() + path;
|
||||
const params = {
|
||||
const params: RequestInit = {
|
||||
/**
|
||||
* This fetch may be cross-origin, in which case we don't want to send cookies of the
|
||||
* original origin (this is the default behaviour of `credentials`).
|
||||
*
|
||||
* To help debugging, also omit cookies on same-origin, so we don't accidentally
|
||||
* implement reliance on cookies anywhere.
|
||||
*/
|
||||
credentials: "omit",
|
||||
...options,
|
||||
headers: {
|
||||
"X-Stack-Override-Error-Status": "true",
|
||||
@ -292,7 +299,7 @@ export class StackClientInterface {
|
||||
"X-Stack-Publishable-Client-Key": this.options.publishableClientKey,
|
||||
} : {},
|
||||
...'projectOwnerTokens' in this.options ? {
|
||||
"X-Stack-Admin-Access-Token": AsyncResult.or(this.options.projectOwnerTokens?.get(), null)?.accessToken ?? "",
|
||||
"X-Stack-Admin-Access-Token": (await this.options.projectOwnerTokens?.getOrWait())?.accessToken ?? "",
|
||||
} : {},
|
||||
...options.headers,
|
||||
},
|
||||
|
||||
@ -63,6 +63,7 @@ export class AsyncCache<D extends any[], T> {
|
||||
readonly isCacheAvailable = this._createKeyed("isCacheAvailable");
|
||||
readonly getIfCached = this._createKeyed("getIfCached");
|
||||
readonly getOrWait = this._createKeyed("getOrWait");
|
||||
readonly forceSetCachedValue = this._createKeyed("forceSetCachedValue");
|
||||
readonly refresh = this._createKeyed("refresh");
|
||||
readonly invalidate = this._createKeyed("invalidate");
|
||||
readonly onChange = this._createKeyed("onChange");
|
||||
@ -70,6 +71,7 @@ export class AsyncCache<D extends any[], T> {
|
||||
|
||||
class AsyncValueCache<T> {
|
||||
private _store: AsyncStore<T>;
|
||||
private _pendingPromise: ReactPromise<T> | undefined;
|
||||
private _fetcher: () => Promise<T>;
|
||||
private readonly _rateLimitOptions: Omit<RateLimitOptions, "batchCalls">;
|
||||
private _subscriptionsCount = 0;
|
||||
@ -85,7 +87,7 @@ class AsyncValueCache<T> {
|
||||
this._store = new AsyncStore();
|
||||
this._rateLimitOptions = {
|
||||
concurrency: 1,
|
||||
debounceMs: 300,
|
||||
throttleMs: 300,
|
||||
...filterUndefined(_options.rateLimiter ?? {}),
|
||||
};
|
||||
|
||||
@ -110,28 +112,31 @@ class AsyncValueCache<T> {
|
||||
return resolved(cached.data);
|
||||
}
|
||||
|
||||
return pending(this._refetch(cacheStrategy === "read-write" ? "write-only" : cacheStrategy));
|
||||
return this._refetch(cacheStrategy);
|
||||
}
|
||||
|
||||
private _set(value: T): void {
|
||||
this._store.set(value);
|
||||
}
|
||||
|
||||
private async _setAsync(value: Promise<T>): Promise<boolean> {
|
||||
return await this._store.setAsync(value);
|
||||
private _setAsync(value: Promise<T>): ReactPromise<boolean> {
|
||||
return pending(this._store.setAsync(value));
|
||||
}
|
||||
|
||||
private async _refetch(cacheStrategy: "write-only" | "never"): Promise<T> {
|
||||
try {
|
||||
const res = this._fetcher();
|
||||
if (cacheStrategy === "write-only") {
|
||||
await this._setAsync(res);
|
||||
}
|
||||
return await res;
|
||||
} catch (e) {
|
||||
this._store.setRejected(e);
|
||||
throw e;
|
||||
private _refetch(cacheStrategy: CacheStrategy): ReactPromise<T> {
|
||||
if (cacheStrategy === "read-write" && this._pendingPromise) {
|
||||
return this._pendingPromise;
|
||||
}
|
||||
const promise = pending(this._fetcher());
|
||||
if (cacheStrategy === "never") {
|
||||
return promise;
|
||||
}
|
||||
this._pendingPromise = promise;
|
||||
return pending(this._setAsync(promise).then(() => promise));
|
||||
}
|
||||
|
||||
forceSetCachedValue(value: T): void {
|
||||
this._set(value);
|
||||
}
|
||||
|
||||
async refresh(): Promise<T> {
|
||||
@ -140,6 +145,7 @@ class AsyncValueCache<T> {
|
||||
|
||||
async invalidate(): Promise<T> {
|
||||
this._store.setUnavailable();
|
||||
this._pendingPromise = undefined;
|
||||
return await this.refresh();
|
||||
}
|
||||
|
||||
|
||||
@ -63,9 +63,20 @@ export function neverResolve(): ReactPromise<never> {
|
||||
}
|
||||
|
||||
export function pending<T>(promise: Promise<T>): ReactPromise<T> {
|
||||
return Object.assign(promise, {
|
||||
const res = Object.assign(promise, {
|
||||
status: "pending",
|
||||
} as const);
|
||||
} as Pick<ReactPromise<T>, "status"> & { value: T, reason: unknown });
|
||||
res.then(
|
||||
value => {
|
||||
res.status = "fulfilled";
|
||||
res.value = value;
|
||||
},
|
||||
reason => {
|
||||
res.status = "rejected";
|
||||
res.reason = reason;
|
||||
},
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function wait(ms: number) {
|
||||
@ -78,12 +89,17 @@ export async function waitUntil(date: Date) {
|
||||
|
||||
class ErrorDuringRunAsynchronously extends Error {
|
||||
constructor() {
|
||||
super("The error above originated in a runAsynchronously() call. Below is the stacktrace associated with it.");
|
||||
super("The error above originated in a runAsynchronously() call. Here is the stacktrace associated with it.");
|
||||
this.name = "ErrorDuringRunAsynchronously";
|
||||
}
|
||||
}
|
||||
|
||||
export function runAsynchronously(promiseOrFunc: Promise<unknown> | (() => Promise<unknown>) | undefined): void {
|
||||
export function runAsynchronously(
|
||||
promiseOrFunc: Promise<unknown> | (() => Promise<unknown>) | undefined,
|
||||
options: {
|
||||
ignoreErrors?: boolean,
|
||||
} = {},
|
||||
): void {
|
||||
if (typeof promiseOrFunc === "function") {
|
||||
promiseOrFunc = promiseOrFunc();
|
||||
}
|
||||
@ -95,8 +111,10 @@ export function runAsynchronously(promiseOrFunc: Promise<unknown> | (() => Promi
|
||||
cause: error,
|
||||
}
|
||||
);
|
||||
console.error(newError);
|
||||
console.error(duringError);
|
||||
if (!options.ignoreErrors) {
|
||||
console.error(newError);
|
||||
console.error(duringError);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import * as crypto from "crypto";
|
||||
import { AsyncResult } from "./results";
|
||||
import { AsyncResult, Result } from "./results";
|
||||
import { generateUuid } from "./uuids";
|
||||
import { ReactPromise, pending, rejected, resolved } from "./promises";
|
||||
|
||||
@ -70,24 +70,38 @@ export class AsyncStore<T> implements ReadonlyAsyncStore<T> {
|
||||
return pending(withFinally);
|
||||
}
|
||||
|
||||
_setIfLatest(value: T, curCounter: number) {
|
||||
if (!this._isAvailable || this._isRejected || this._value !== value) {
|
||||
const oldValue = this._value;
|
||||
if (curCounter > this._lastSuccessfulUpdate) {
|
||||
this._lastSuccessfulUpdate = curCounter;
|
||||
this._isAvailable = true;
|
||||
this._isRejected = false;
|
||||
this._value = value;
|
||||
this._callbacks.forEach((callback) => callback(value, oldValue));
|
||||
return true;
|
||||
_setIfLatest(result: Result<T>, curCounter: number) {
|
||||
if (curCounter > this._lastSuccessfulUpdate) {
|
||||
switch (result.status) {
|
||||
case "ok": {
|
||||
if (!this._isAvailable || this._isRejected || this._value !== result.data) {
|
||||
const oldValue = this._value;
|
||||
this._lastSuccessfulUpdate = curCounter;
|
||||
this._isAvailable = true;
|
||||
this._isRejected = false;
|
||||
this._value = result.data;
|
||||
this._rejectionError = undefined;
|
||||
this._callbacks.forEach((callback) => callback(result.data, oldValue));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case "error": {
|
||||
this._lastSuccessfulUpdate = curCounter;
|
||||
this._isAvailable = false;
|
||||
this._isRejected = true;
|
||||
this._value = undefined;
|
||||
this._rejectionError = result.error;
|
||||
this._waitingRejectFunctions.forEach((reject) => reject(result.error));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
set(value: T): void {
|
||||
this._setIfLatest(value, ++this._updateCounter);
|
||||
this._setIfLatest(Result.ok(value), ++this._updateCounter);
|
||||
}
|
||||
|
||||
update(updater: (value: T | undefined) => T): T {
|
||||
@ -98,21 +112,20 @@ export class AsyncStore<T> implements ReadonlyAsyncStore<T> {
|
||||
|
||||
async setAsync(promise: Promise<T>): Promise<boolean> {
|
||||
const curCounter = ++this._updateCounter;
|
||||
const value = await promise;
|
||||
return this._setIfLatest(value, curCounter);
|
||||
const result = await Result.fromPromise(promise);
|
||||
return this._setIfLatest(result, curCounter);
|
||||
}
|
||||
|
||||
setUnavailable(): void {
|
||||
this._lastSuccessfulUpdate = ++this._updateCounter;
|
||||
this._isAvailable = false;
|
||||
this._isRejected = false;
|
||||
this._value = undefined;
|
||||
this._rejectionError = undefined;
|
||||
}
|
||||
|
||||
setRejected(error: unknown): void {
|
||||
this._isRejected = true;
|
||||
this._value = undefined;
|
||||
this._rejectionError = error;
|
||||
this._waitingRejectFunctions.forEach((reject) => reject(error));
|
||||
this._setIfLatest(Result.error(error), ++this._updateCounter);
|
||||
}
|
||||
|
||||
map<U>(mapper: (value: T) => U): AsyncStore<U> {
|
||||
|
||||
@ -13,7 +13,7 @@ import { RedirectType, redirect, useRouter } from "next/navigation";
|
||||
import { ReadonlyJson } from "@stackframe/stack-shared/dist/utils/json";
|
||||
import { constructRedirectUrl } from "../utils/url";
|
||||
import { filterUndefined } from "@stackframe/stack-shared/dist/utils/objects";
|
||||
import { neverResolve, resolved } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { neverResolve, resolved, runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { AsyncCache } from "@stackframe/stack-shared/dist/utils/caches";
|
||||
import { ApiKeySetBaseJson, ApiKeySetCreateOptions, ApiKeySetFirstViewJson, ApiKeySetJson, ProjectUpdateOptions } from "@stackframe/stack-shared/dist/interface/adminInterface";
|
||||
import { suspend } from "@stackframe/stack-shared/dist/utils/react";
|
||||
@ -109,6 +109,8 @@ export type StackAdminAppConstructorOptions<HasTokenStore extends boolean, Proje
|
||||
|
||||
export type StackClientAppJson<HasTokenStore extends boolean, ProjectId extends string> = StackClientAppConstructorOptions<HasTokenStore, ProjectId> & {
|
||||
uniqueIdentifier: string,
|
||||
currentClientUserJson: UserJson | null,
|
||||
currentProjectJson: ClientProjectJson,
|
||||
};
|
||||
|
||||
const defaultBaseUrl = "https://app.stackframe.co";
|
||||
@ -267,13 +269,13 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
& {
|
||||
uniqueIdentifier?: string,
|
||||
checkString?: string,
|
||||
currentClientUserJson?: UserJson | null,
|
||||
currentProjectJson?: ClientProjectJson,
|
||||
}
|
||||
& (
|
||||
| StackClientAppConstructorOptions<HasTokenStore, ProjectId>
|
||||
| Pick<StackClientAppConstructorOptions<HasTokenStore, ProjectId>, "tokenStore" | "urls"> & {
|
||||
interface: StackClientInterface,
|
||||
tokenStore: TokenStoreOptions<HasTokenStore>,
|
||||
urls: Partial<HandlerUrls> | undefined,
|
||||
}
|
||||
)
|
||||
) {
|
||||
@ -295,16 +297,36 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
throw new Error("A Stack client app with the same unique identifier already exists");
|
||||
}
|
||||
allClientApps.set(this._uniqueIdentifier, [options.checkString ?? "default check string", this]);
|
||||
|
||||
// For some important calls, either use the provided cached values or start fetching them now
|
||||
if (options.currentClientUserJson !== undefined) {
|
||||
this._currentUserCache.forceSetCachedValue([getTokenStore(this._tokenStoreOptions)], options.currentClientUserJson);
|
||||
} else if (this.hasPersistentTokenStore()) {
|
||||
runAsynchronously(() => this.getUser());
|
||||
}
|
||||
if (options.currentProjectJson !== undefined) {
|
||||
this._currentProjectCache.forceSetCachedValue([], options.currentProjectJson);
|
||||
} else {
|
||||
runAsynchronously(this.getProject());
|
||||
}
|
||||
}
|
||||
|
||||
protected hasPersistentTokenStore(): this is StackClientApp<true, ProjectId> {
|
||||
return this._tokenStoreOptions !== null;
|
||||
}
|
||||
|
||||
protected _ensurePersistentTokenStore(): asserts this is StackClientApp<true, ProjectId> {
|
||||
if (!this._tokenStoreOptions) {
|
||||
if (!this.hasPersistentTokenStore()) {
|
||||
throw new Error("Cannot call this function on a Stack app without a persistent token store. Make sure the tokenStore option is set to a non-null value when initializing Stack.");
|
||||
}
|
||||
}
|
||||
|
||||
protected isInternalProject(): this is { projectId: "internal" } {
|
||||
return this.projectId === "internal";
|
||||
}
|
||||
|
||||
protected _ensureInternalProject(): asserts this is { projectId: "internal" } {
|
||||
if (this.projectId !== "internal") {
|
||||
if (!this.isInternalProject()) {
|
||||
throw new Error("Cannot call this function on a Stack app with a project ID other than 'internal'.");
|
||||
}
|
||||
}
|
||||
@ -453,7 +475,7 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
async getUser(options?: GetUserOptions): Promise<CurrentUser | null> {
|
||||
this._ensurePersistentTokenStore();
|
||||
const tokenStore = getTokenStore(this._tokenStoreOptions);
|
||||
const userJson = await this._currentUserCache.getOrWait([tokenStore], "never");
|
||||
const userJson = await this._currentUserCache.getOrWait([tokenStore], "write-only");
|
||||
|
||||
if (userJson === null) {
|
||||
switch (options?.or) {
|
||||
@ -580,7 +602,7 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
}
|
||||
|
||||
async getProject(): Promise<ClientProjectJson> {
|
||||
return await this._currentProjectCache.getOrWait([], "never");
|
||||
return await this._currentProjectCache.getOrWait([], "write-only");
|
||||
}
|
||||
|
||||
useProject(): ClientProjectJson {
|
||||
@ -594,7 +616,7 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
async listOwnedProjects(): Promise<Project[]> {
|
||||
this._ensureInternalProject();
|
||||
const tokenStore = getTokenStore(this._tokenStoreOptions);
|
||||
const json = await this._ownedProjectsCache.getOrWait([tokenStore], "never");
|
||||
const json = await this._ownedProjectsCache.getOrWait([tokenStore], "write-only");
|
||||
return json.map((j) => this._projectAdminFromJson(
|
||||
j,
|
||||
this._createAdminInterface(j.id, tokenStore),
|
||||
@ -683,6 +705,12 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
// TODO find a way to do this
|
||||
throw Error("Cannot serialize to JSON from an application without a publishable client key");
|
||||
}
|
||||
|
||||
const [user, project] = await Promise.all([
|
||||
this.getUser(),
|
||||
this.getProject(),
|
||||
]);
|
||||
|
||||
return {
|
||||
baseUrl: this._interface.options.baseUrl,
|
||||
projectId: this.projectId,
|
||||
@ -690,6 +718,8 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
tokenStore: this._tokenStoreOptions,
|
||||
urls: this._urlOptions,
|
||||
uniqueIdentifier: this._uniqueIdentifier,
|
||||
currentClientUserJson: user?.toJson() ?? null,
|
||||
currentProjectJson: project,
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -811,7 +841,7 @@ class _StackServerAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
async getServerUser(): Promise<CurrentServerUser | null> {
|
||||
this._ensurePersistentTokenStore();
|
||||
const tokenStore = getTokenStore(this._tokenStoreOptions);
|
||||
const userJson = await this._currentServerUserCache.getOrWait([tokenStore], "never");
|
||||
const userJson = await this._currentServerUserCache.getOrWait([tokenStore], "write-only");
|
||||
return this._currentServerUserFromJson(userJson, tokenStore);
|
||||
}
|
||||
|
||||
@ -839,7 +869,7 @@ class _StackServerAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
}
|
||||
|
||||
async listServerUsers(): Promise<ServerUser[]> {
|
||||
const json = await this._serverUsersCache.getOrWait([], "never");
|
||||
const json = await this._serverUsersCache.getOrWait([], "write-only");
|
||||
return json.map((j) => this._serverUserFromJson(j));
|
||||
}
|
||||
|
||||
@ -945,7 +975,7 @@ class _StackAdminAppImpl<HasTokenStore extends boolean, ProjectId extends string
|
||||
|
||||
async getProjectAdmin(): Promise<Project> {
|
||||
return this._projectAdminFromJson(
|
||||
await this._adminProjectCache.getOrWait([], "never"),
|
||||
await this._adminProjectCache.getOrWait([], "write-only"),
|
||||
this._interface,
|
||||
() => this._refreshProject()
|
||||
);
|
||||
@ -971,7 +1001,7 @@ class _StackAdminAppImpl<HasTokenStore extends boolean, ProjectId extends string
|
||||
}
|
||||
|
||||
async listApiKeySets(): Promise<ApiKeySet[]> {
|
||||
const json = await this._apiKeySetsCache.getOrWait([], "never");
|
||||
const json = await this._apiKeySetsCache.getOrWait([], "write-only");
|
||||
return json.map((j) => this._createApiKeySetFromJson(j));
|
||||
}
|
||||
|
||||
@ -1212,8 +1242,7 @@ export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId ext
|
||||
);
|
||||
type StackAdminAppConstructor = {
|
||||
new <
|
||||
TokenStoreType extends string,
|
||||
HasTokenStore extends (TokenStoreType extends {} ? true : boolean),
|
||||
HasTokenStore extends boolean,
|
||||
ProjectId extends string
|
||||
>(options: StackAdminAppConstructorOptions<HasTokenStore, ProjectId>): StackAdminApp<HasTokenStore, ProjectId>,
|
||||
new (options: StackAdminAppConstructorOptions<boolean, string>): StackAdminApp<boolean, string>,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user