mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Remove most occurences of useStrictMemo
This commit is contained in:
parent
3737f68e07
commit
db5a2c859e
@ -1,23 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { AccordionGroup, Card, CardOverflow } from "@mui/joy";
|
||||
import { use, useState } from "react";
|
||||
import { Paragraph } from "@/components/paragraph";
|
||||
import { useStrictMemo } from "@stackframe/stack-shared/src/hooks/use-strict-memo";
|
||||
import { SmartSwitch } from "@/components/smart-switch";
|
||||
import { SimpleCard } from "@/components/simple-card";
|
||||
import { useAdminApp } from "../../useAdminInterface";
|
||||
import { ProviderAccordion, ProviderType, availableProviders } from "./provider-accordion";
|
||||
import { useAdminApp } from "../../use-admin-app";
|
||||
import { ProviderAccordion, availableProviders } from "./provider-accordion";
|
||||
import { OauthProviderConfigJson } from "@stackframe/stack-shared";
|
||||
|
||||
export default function ProvidersClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const [invalidationCounter, setInvalidationCounter] = useState(0);
|
||||
|
||||
const projectPromise = useStrictMemo(async () => {
|
||||
return await stackAdminApp.getProject({ showDisabledOauth: true });
|
||||
}, [stackAdminApp, invalidationCounter]);
|
||||
const project = use(projectPromise);
|
||||
const project = stackAdminApp.useProjectAdmin();
|
||||
|
||||
const oauthProviders = project.evaluatedConfig.oauthProviders;
|
||||
|
||||
@ -33,12 +27,11 @@ export default function ProvidersClient() {
|
||||
<SmartSwitch
|
||||
checked={project.evaluatedConfig.credentialEnabled}
|
||||
onChange={async (event) => {
|
||||
await stackAdminApp.updateProject({
|
||||
await project.update({
|
||||
config: {
|
||||
credentialEnabled: event.target.checked,
|
||||
},
|
||||
});
|
||||
setInvalidationCounter((counter) => counter + 1);
|
||||
}}
|
||||
>
|
||||
Enable password authentication
|
||||
@ -65,10 +58,9 @@ export default function ProvidersClient() {
|
||||
newOauthProviders.push(provider);
|
||||
}
|
||||
|
||||
await stackAdminApp.updateProject({
|
||||
await project.update({
|
||||
config: { oauthProviders: newOauthProviders },
|
||||
});
|
||||
setInvalidationCounter((counter) => counter + 1);
|
||||
}}
|
||||
/>;
|
||||
})}
|
||||
|
||||
@ -26,7 +26,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { Paragraph } from "@/components/paragraph";
|
||||
import { AsyncButton } from "@/components/async-button";
|
||||
import { SharedProvider, StandardProvider, sharedProviders, standardProviders, toSharedProvider, toStandardProvider } from "@stackframe/stack-shared/dist/interface/clientInterface";
|
||||
import { useAdminApp } from "../../useAdminInterface";
|
||||
import { useAdminApp } from "../../use-admin-app";
|
||||
import { SmartSwitch } from "@/components/smart-switch";
|
||||
import { Dialog } from "@/components/dialog";
|
||||
import { DialogContent, Icon } from "@mui/material";
|
||||
|
||||
@ -6,19 +6,14 @@ import { Paragraph } from "@/components/paragraph";
|
||||
import { Icon } from "@/components/icon";
|
||||
import { Dialog } from "@/components/dialog";
|
||||
import { AsyncButton } from "@/components/async-button";
|
||||
import { useStrictMemo } from "@stackframe/stack-shared/src/hooks/use-strict-memo";
|
||||
import { SimpleCard } from "@/components/simple-card";
|
||||
import { useAdminApp } from "../../useAdminInterface";
|
||||
import { useAdminApp } from "../../use-admin-app";
|
||||
import { SmartSwitch } from "@/components/smart-switch";
|
||||
|
||||
export default function UrlsAndCallbacksClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const project = stackAdminApp.useProjectAdmin();
|
||||
|
||||
const [invalidationCounter, setInvalidationCounter] = useState(0);
|
||||
const projectPromise = useStrictMemo(async () => {
|
||||
return await stackAdminApp.getProject();
|
||||
}, [stackAdminApp, invalidationCounter]);
|
||||
const project = use(projectPromise);
|
||||
const domains = new Set(project.evaluatedConfig.domains);
|
||||
|
||||
const [deleteDialogPrefix, setDeleteDialogDomain] = useState<string | null>(null);
|
||||
@ -38,12 +33,11 @@ export default function UrlsAndCallbacksClient() {
|
||||
<SmartSwitch
|
||||
checked={project.evaluatedConfig.allowLocalhost}
|
||||
onChange={async (event) => {
|
||||
await stackAdminApp.updateProject({
|
||||
await project.update({
|
||||
config: {
|
||||
allowLocalhost: event.target.checked,
|
||||
},
|
||||
});
|
||||
setInvalidationCounter(invalidationCounter + 1);
|
||||
}}
|
||||
>
|
||||
<Typography>Allow all localhost callbacks for development</Typography>
|
||||
@ -97,12 +91,11 @@ export default function UrlsAndCallbacksClient() {
|
||||
okButton={{
|
||||
label: "Delete",
|
||||
onClick: async () => {
|
||||
await stackAdminApp.updateProject({
|
||||
await project.update({
|
||||
config: {
|
||||
domains: [...domains].filter(({ domain }) => domain !== deleteDialogPrefix),
|
||||
}
|
||||
});
|
||||
setInvalidationCounter(invalidationCounter + 1);
|
||||
}
|
||||
}}
|
||||
cancelButton
|
||||
@ -130,7 +123,7 @@ export default function UrlsAndCallbacksClient() {
|
||||
setNewDomainError(true);
|
||||
return "prevent-close";
|
||||
}
|
||||
await stackAdminApp.updateProject({
|
||||
await project.update({
|
||||
config: {
|
||||
domains: [...domains, {
|
||||
domain: newDomain,
|
||||
@ -138,7 +131,6 @@ export default function UrlsAndCallbacksClient() {
|
||||
}],
|
||||
},
|
||||
});
|
||||
setInvalidationCounter(invalidationCounter + 1);
|
||||
}
|
||||
}}
|
||||
cancelButton
|
||||
|
||||
@ -1,21 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { use, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Paragraph } from "@/components/paragraph";
|
||||
import { useStrictMemo } from "@stackframe/stack-shared/src/hooks/use-strict-memo";
|
||||
import { UsersTable } from "./users-table";
|
||||
import { useAdminApp } from "../../useAdminInterface";
|
||||
import { useAdminApp } from "../../use-admin-app";
|
||||
|
||||
|
||||
export default function UsersDashboardClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const [invalidationCounter, setInvalidationCounter] = useState(0);
|
||||
|
||||
const allUsersPromise = useStrictMemo(() => {
|
||||
return stackAdminApp.listUsers();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [invalidationCounter]);
|
||||
const allUsers = use(allUsersPromise);
|
||||
const allUsers = stackAdminApp.useServerUsers();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -24,7 +17,7 @@ export default function UsersDashboardClient() {
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph body>
|
||||
<UsersTable rows={allUsers} onInvalidate={() => setInvalidationCounter(() => invalidationCounter + 1)} />
|
||||
<UsersTable rows={allUsers} />
|
||||
</Paragraph>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -7,18 +7,15 @@ import { getInputDatetimeLocalString } from '@stackframe/stack-shared/dist/utils
|
||||
import { Icon } from '@/components/icon';
|
||||
import { AsyncButton } from '@/components/async-button';
|
||||
import { Dialog } from '@/components/dialog';
|
||||
import { useAdminApp } from '../../useAdminInterface';
|
||||
import { useAdminApp } from '../../use-admin-app';
|
||||
import { runAsynchronously } from '@stackframe/stack-shared/src/utils/promises';
|
||||
import { ServerUserJson } from '@stackframe/stack-shared';
|
||||
import { ServerUser } from '@stackframe/stack/dist/lib/stack-app';
|
||||
|
||||
export function UsersTable(props: {
|
||||
rows: ServerUserJson[],
|
||||
onInvalidate(): void,
|
||||
rows: ServerUser[],
|
||||
}) {
|
||||
const stackAdminApp = useAdminApp();
|
||||
|
||||
const columns: (GridColDef & {
|
||||
stackOnProcessUpdate?: (updatedRow: ServerUserJson, oldRow: ServerUserJson) => Promise<void>,
|
||||
stackOnProcessUpdate?: (updatedRow: ServerUser, oldRow: ServerUser) => Promise<void>,
|
||||
})[] = [
|
||||
{
|
||||
field: 'profilePicture',
|
||||
@ -51,7 +48,7 @@ export function UsersTable(props: {
|
||||
flex: 1,
|
||||
editable: true,
|
||||
stackOnProcessUpdate: async (updatedRow, originalRow) => {
|
||||
await stackAdminApp.setServerUserCustomizableData(originalRow.id, { displayName: updatedRow.displayName });
|
||||
await originalRow.update({ displayName: updatedRow.displayName });
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -60,7 +57,7 @@ export function UsersTable(props: {
|
||||
width: 200,
|
||||
editable: true,
|
||||
stackOnProcessUpdate: async (updatedRow, originalRow) => {
|
||||
await stackAdminApp.setServerUserCustomizableData(originalRow.id, { primaryEmail: updatedRow.primaryEmail, primaryEmailVerified: false });
|
||||
await originalRow.update({ primaryEmail: updatedRow.primaryEmail, primaryEmailVerified: false });
|
||||
},
|
||||
renderCell: (params) => (
|
||||
<>
|
||||
@ -95,7 +92,7 @@ export function UsersTable(props: {
|
||||
type: 'actions',
|
||||
width: 48,
|
||||
getActions: (params) => [
|
||||
<Actions key="more_actions" params={params} onInvalidate={() => props.onInvalidate()} />
|
||||
<Actions key="more_actions" params={params} />
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -118,7 +115,6 @@ export function UsersTable(props: {
|
||||
await column.stackOnProcessUpdate(updatedRow, originalRow);
|
||||
}
|
||||
}
|
||||
props.onInvalidate();
|
||||
return originalRow;
|
||||
}}
|
||||
pageSizeOptions={[5, 15, 25]}
|
||||
@ -126,7 +122,7 @@ export function UsersTable(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function Actions(props: { params: any, onInvalidate: () => void}) {
|
||||
function Actions(props: { params: any }) {
|
||||
const stackAdminApp = useAdminApp();
|
||||
|
||||
const [isEditModalOpen, setIsEditModalOpen] = React.useState(false);
|
||||
@ -161,7 +157,6 @@ function Actions(props: { params: any, onInvalidate: () => void}) {
|
||||
user={props.params.row}
|
||||
open={isEditModalOpen}
|
||||
onClose={() => setIsEditModalOpen(false)}
|
||||
onInvalidate={() => props.onInvalidate()}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
@ -172,8 +167,7 @@ function Actions(props: { params: any, onInvalidate: () => void}) {
|
||||
okButton={{
|
||||
label: "Delete user",
|
||||
onClick: async () => {
|
||||
await stackAdminApp.deleteServerUser(props.params.row.id);
|
||||
props.onInvalidate();
|
||||
await props.params.row.delete();
|
||||
}
|
||||
}}
|
||||
cancelButton={true}
|
||||
@ -185,7 +179,7 @@ function Actions(props: { params: any, onInvalidate: () => void}) {
|
||||
}
|
||||
|
||||
|
||||
function EditUserModal(props: { user: ServerUserJson, open: boolean, onClose: () => void, onInvalidate: () => void }) {
|
||||
function EditUserModal(props: { user: ServerUser, open: boolean, onClose: () => void }) {
|
||||
const stackAdminApp = useAdminApp();
|
||||
|
||||
const formRef = React.useRef<HTMLFormElement>(null);
|
||||
@ -213,8 +207,7 @@ function EditUserModal(props: { user: ServerUserJson, open: boolean, onClose: ()
|
||||
primaryEmailVerified: formData.get('primaryEmailVerified') === "on",
|
||||
signedUpAtMillis: new Date(formData.get('signedUpAt') as string).getTime(),
|
||||
};
|
||||
await stackAdminApp.setServerUserCustomizableData(props.user.id, formJson);
|
||||
props.onInvalidate();
|
||||
await props.user.update(formJson);
|
||||
props.onClose();
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
@ -246,7 +239,7 @@ function EditUserModal(props: { user: ServerUserJson, open: boolean, onClose: ()
|
||||
</Stack>
|
||||
<FormControl disabled={isSaving}>
|
||||
<FormLabel htmlFor="signedUpAt">Signed up</FormLabel>
|
||||
<Input name="signedUpAt" type="datetime-local" defaultValue={getInputDatetimeLocalString(new Date(props.user.signedUpAtMillis))} required />
|
||||
<Input name="signedUpAt" type="datetime-local" defaultValue={getInputDatetimeLocalString(props.user.signedUpAt)} required />
|
||||
</FormControl>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
@ -4,9 +4,8 @@ import { Sheet, SheetProps, Select, Option, SelectOption, Stack, Typography } fr
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/joy/Box';
|
||||
import IconButton from '@mui/joy/IconButton';
|
||||
import { useAdminApp } from './useAdminInterface';
|
||||
import { useAdminApp } from './use-admin-app';
|
||||
import { redirect, usePathname } from 'next/navigation';
|
||||
import { useStrictMemo } from '@stackframe/stack-shared/src/hooks/use-strict-memo';
|
||||
import { useStackApp } from '@stackframe/stack';
|
||||
import { Icon } from '@/components/icon';
|
||||
import Breadcrumbs from '@mui/joy/Breadcrumbs';
|
||||
@ -40,10 +39,7 @@ function ProjectSwitchItem({ label }: { label: string }) {
|
||||
function ProjectSwitch() {
|
||||
const stackApp = useStackApp({ projectIdMustMatch: "internal" });
|
||||
const stackAdminApp = useAdminApp();
|
||||
const projectsPromise = useStrictMemo(() => {
|
||||
return stackApp.listOwnedProjects();
|
||||
}, []);
|
||||
const projects = React.use(projectsPromise);
|
||||
const projects = stackApp.useOwnedProjects();
|
||||
const project = projects.find((project) => project.id === stackAdminApp.projectId);
|
||||
|
||||
const renderValue = (option: SelectOption<string> | null) => {
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { Box, Drawer, Stack, useTheme } from "@mui/joy";
|
||||
import { useState } from "react";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { AdminAppProvider } from "./useAdminInterface";
|
||||
import { AdminAppProvider } from "./use-admin-app";
|
||||
import { Header } from "./header";
|
||||
import { Icon } from '@/components/icon';
|
||||
import { OnboardingDialog } from "./onboarding-dialog";
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Dialog } from "@/components/dialog";
|
||||
import { use, useId, useRef, useState } from "react";
|
||||
import { useStrictMemo } from "@stackframe/stack-shared/src/hooks/use-strict-memo";
|
||||
import { useAdminApp } from "./useAdminInterface";
|
||||
import { useAdminApp } from "./use-admin-app";
|
||||
import { Stack } from "@mui/joy";
|
||||
import { Paragraph } from "@/components/paragraph";
|
||||
import EnvKeys from "@/components/env-keys";
|
||||
|
||||
@ -2,18 +2,17 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { DataGrid, GridColDef, GridToolbar } from '@mui/x-data-grid';
|
||||
import { ApiKeySetSummary } from '@stackframe/stack-shared';
|
||||
import { Box, Checkbox, Stack, Tooltip, Typography } from '@mui/joy';
|
||||
import { Dialog } from '@/components/dialog';
|
||||
import { useAdminApp } from '../../useAdminInterface';
|
||||
import { useAdminApp } from '../../use-admin-app';
|
||||
import { ApiKeySet } from '@stackframe/stack/dist/lib/stack-app';
|
||||
|
||||
export function ApiKeysTable(props: {
|
||||
rows: ApiKeySetSummary[],
|
||||
onInvalidate(): void,
|
||||
rows: ApiKeySet[],
|
||||
}) {
|
||||
const stackAdminApp = useAdminApp();
|
||||
|
||||
const [revokeDialogApiKeySet, setRevokeDialogApiKeySet] = React.useState<ApiKeySetSummary | null>(null);
|
||||
const [revokeDialogApiKeySet, setRevokeDialogApiKeySet] = React.useState<ApiKeySet | null>(null);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
@ -128,9 +127,8 @@ export function ApiKeysTable(props: {
|
||||
label: "Revoke API key",
|
||||
onClick: async () => {
|
||||
if (revokeDialogApiKeySet) {
|
||||
await stackAdminApp.revokeApiKeySetById(revokeDialogApiKeySet.id);
|
||||
await revokeDialogApiKeySet.revoke();
|
||||
setRevokeDialogApiKeySet(null);
|
||||
props.onInvalidate();
|
||||
}
|
||||
},
|
||||
}}
|
||||
|
||||
@ -2,36 +2,22 @@
|
||||
|
||||
import { use, useId, useState } from "react";
|
||||
import { Box, Button, Checkbox, DialogActions, DialogContent, DialogTitle, Divider, FormControl, FormLabel, Input, Modal, ModalDialog, Select, Option, Stack, FormHelperText, Typography } from "@mui/joy";
|
||||
import { ApiKeySetFirstView } from "@stackframe/stack-shared";
|
||||
import { AsyncButton } from "@/components/async-button";
|
||||
import { Icon } from "@/components/icon";
|
||||
import { Paragraph } from "@/components/paragraph";
|
||||
import { CopyButton } from "@/components/copy-button";
|
||||
import { useStrictMemo } from "@stackframe/stack-shared/src/hooks/use-strict-memo";
|
||||
import { ApiKeysTable } from "./api-keys-table";
|
||||
import { useAdminApp } from "../../useAdminInterface";
|
||||
import { useAdminApp } from "../../use-admin-app";
|
||||
import { runAsynchronously } from "@stackframe/stack-shared/src/utils/promises";
|
||||
import EnvKeys from "@/components/env-keys";
|
||||
import Link from "next/link";
|
||||
import { SmartLink } from "@/components/smart-link";
|
||||
import { throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { ApiKeySetFirstView } from "@stackframe/stack/dist/lib/stack-app";
|
||||
|
||||
|
||||
export default function ApiKeysDashboardClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
|
||||
const [invalidationCounter, setInvalidationCounter] = useState(0);
|
||||
const apiKeySetsPromise = useStrictMemo(() => {
|
||||
return stackAdminApp.listApiKeySets();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [invalidationCounter]);
|
||||
const apiKeySets = use(apiKeySetsPromise);
|
||||
const apiKeySets = stackAdminApp.useApiKeySets();
|
||||
|
||||
const [isNewApiKeyDialogOpen, setIsNewApiKeyDialogOpen] = useState(false);
|
||||
|
||||
const invalidate = () => {
|
||||
setInvalidationCounter(c => c + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -50,13 +36,12 @@ export default function ApiKeysDashboardClient() {
|
||||
Please note that API keys cannot be viewed anymore after they have been created. If you lose them, you will have to create new ones.
|
||||
</Paragraph>
|
||||
|
||||
<ApiKeysTable rows={apiKeySets} onInvalidate={() => invalidate()} />
|
||||
<ApiKeysTable rows={apiKeySets} />
|
||||
|
||||
<CreateNewDialog
|
||||
key={`${isNewApiKeyDialogOpen}`}
|
||||
open={isNewApiKeyDialogOpen}
|
||||
onClose={() => setIsNewApiKeyDialogOpen(false)}
|
||||
onInvalidate={() => invalidate()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@ -71,8 +56,9 @@ const expiresInOptions = {
|
||||
[1000 * 60 * 60 * 24 * 365 * 200]: "Never",
|
||||
} as const;
|
||||
|
||||
function CreateNewDialog(props: { open: boolean, onClose(): void, onInvalidate(): void }) {
|
||||
function CreateNewDialog(props: { open: boolean, onClose(): void }) {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const project = stackAdminApp.useProjectAdmin();
|
||||
|
||||
const formId = useId();
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
@ -80,11 +66,6 @@ function CreateNewDialog(props: { open: boolean, onClose(): void, onInvalidate()
|
||||
const [returnedApiKey, setReturnedApiKey] = useState<ApiKeySetFirstView | null>(null);
|
||||
const [confirmedOnlyOnce, setConfirmedOnlyOnce] = useState(false);
|
||||
|
||||
const projectPromise = useStrictMemo(() => {
|
||||
return stackAdminApp.getProject();
|
||||
}, []);
|
||||
const project = use(projectPromise);
|
||||
|
||||
const close = () => {
|
||||
if (returnedApiKey && !confirmedOnlyOnce) return;
|
||||
props.onClose();
|
||||
@ -148,7 +129,6 @@ function CreateNewDialog(props: { open: boolean, onClose(): void, onInvalidate()
|
||||
description: formData.get("description") as string,
|
||||
});
|
||||
setReturnedApiKey(returned);
|
||||
props.onInvalidate();
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
|
||||
@ -1,29 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { Typography } from "@mui/joy";
|
||||
import React, { use, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { InlineCode } from "@/components/inline-code";
|
||||
import { Paragraph } from "@/components/paragraph";
|
||||
import { useStrictMemo } from "@stackframe/stack-shared/src/hooks/use-strict-memo";
|
||||
import { IconAlert } from "@/components/icon-alert";
|
||||
import { Enumeration, EnumerationItem } from "@/components/enumeration";
|
||||
import { SmartLink } from "@/components/smart-link";
|
||||
import { SmartSwitch } from "@/components/smart-switch";
|
||||
import { SimpleCard } from "@/components/simple-card";
|
||||
import { useAdminApp } from "../../useAdminInterface";
|
||||
import { getProductionModeErrors } from "@stackframe/stack-shared";
|
||||
import { useAdminApp } from "../../use-admin-app";
|
||||
|
||||
export default function EnvironmentClient() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const project = stackAdminApp.useProjectAdmin();
|
||||
|
||||
const [invalidationCounter, setInvalidationCounter] = useState(0);
|
||||
const projectPromise = useStrictMemo(async () => {
|
||||
return await stackAdminApp.getProject();
|
||||
// eslint-disable-next-line
|
||||
}, [stackAdminApp, invalidationCounter]);
|
||||
const project = use(projectPromise);
|
||||
|
||||
const productionModeErrors = getProductionModeErrors(project);
|
||||
const productionModeErrors = project.getProductionModeErrors();
|
||||
|
||||
const [productionModeUpdateLoading, setProductionModeUpdateLoading] = useState(false);
|
||||
|
||||
@ -46,11 +38,10 @@ export default function EnvironmentClient() {
|
||||
onChange={async (event) => {
|
||||
setProductionModeUpdateLoading(true);
|
||||
try {
|
||||
await stackAdminApp.updateProject({
|
||||
await project.update({
|
||||
isProductionMode: event.target.checked,
|
||||
});
|
||||
} finally {
|
||||
setInvalidationCounter((prev) => prev + 1);
|
||||
setProductionModeUpdateLoading(false);
|
||||
}
|
||||
}}
|
||||
|
||||
@ -10,7 +10,7 @@ import ListItem from '@mui/joy/ListItem';
|
||||
import ListItemButton from '@mui/joy/ListItemButton';
|
||||
import ListItemContent from '@mui/joy/ListItemContent';
|
||||
import Typography from '@mui/joy/Typography';
|
||||
import { useAdminApp } from './useAdminInterface';
|
||||
import { useAdminApp } from './use-admin-app';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useUser } from '@stackframe/stack';
|
||||
import { Dropdown, MenuButton, MenuItem, Menu, useColorScheme, Stack, Sheet, CircularProgress } from '@mui/joy';
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { StackAdminInterface } from "@stackframe/stack-shared";
|
||||
import React from "react";
|
||||
import { useUser } from "@stackframe/stack";
|
||||
import { throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { cacheFunction } from "@stackframe/stack-shared/dist/utils/caches";
|
||||
import { CurrentUser } from "@stackframe/stack/dist/lib/stack-app";
|
||||
import { CurrentUser, StackAdminApp } from "@stackframe/stack/dist/lib/stack-app";
|
||||
|
||||
const StackAdminInterfaceContext = React.createContext<StackAdminInterface | null>(null);
|
||||
const StackAdminInterfaceContext = React.createContext<StackAdminApp<true> | null>(null);
|
||||
|
||||
const createAdminInterface = cacheFunction((baseUrl: string, projectId: string, user: CurrentUser) => {
|
||||
return new StackAdminInterface({
|
||||
return new StackAdminApp({
|
||||
baseUrl,
|
||||
projectId,
|
||||
internalAdminAccessToken: user.accessToken ?? throwErr("User must have an access token"),
|
||||
tokenStore: "nextjs-cookie",
|
||||
projectOwnerTokens: user.tokenStore,
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,19 +8,14 @@ import { Dialog } from "@/components/dialog";
|
||||
import { Paragraph } from "@/components/paragraph";
|
||||
import { SmartLink } from "@/components/smart-link";
|
||||
import { useFromNow } from "@/hooks/use-from-now";
|
||||
import { useStrictMemo } from "@stackframe/stack-shared/src/hooks/use-strict-memo";
|
||||
import { runAsynchronously } from "@stackframe/stack-shared/src/utils/promises";
|
||||
import { Project } from "@stackframe/stack/dist/lib/stack-app";
|
||||
|
||||
|
||||
export default function ProjectsPageClient() {
|
||||
const [invalidationCounter, setInvalidationCounter] = useState(0);
|
||||
const stackApp = useStackApp({ projectIdMustMatch: "internal" });
|
||||
|
||||
const projectsPromise = useStrictMemo(() => {
|
||||
return stackApp.listOwnedProjects();
|
||||
}, [invalidationCounter]);
|
||||
const projects = use(projectsPromise);
|
||||
|
||||
const projects = stackApp.useOwnedProjects();
|
||||
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
|
||||
@ -47,7 +42,6 @@ export default function ProjectsPageClient() {
|
||||
<CreateProjectDialog
|
||||
open={createDialogOpen}
|
||||
onClose={() => setCreateDialogOpen(false)}
|
||||
onInvalidate={() => setInvalidationCounter((x) => x + 1)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@ -88,7 +82,7 @@ function ProjectCard(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function CreateProjectDialog(props: { open: boolean, onClose(): void, onInvalidate(): void }) {
|
||||
function CreateProjectDialog(props: { open: boolean, onClose(): void }) {
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const formId = useId();
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
@ -125,7 +119,6 @@ function CreateProjectDialog(props: { open: boolean, onClose(): void, onInvalida
|
||||
description: `${formData.get('description')}`,
|
||||
});
|
||||
|
||||
props.onInvalidate();
|
||||
props.onClose();
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
|
||||
@ -57,6 +57,9 @@ export const GET = smartRouteHandler(async (req: NextRequest, options: { params:
|
||||
if (!provider) {
|
||||
throw new StatusError(StatusError.NotFound, "Provider not found");
|
||||
}
|
||||
if (!provider.enabled) {
|
||||
throw new StatusError(StatusError.NotFound, "Provider not enabled");
|
||||
}
|
||||
|
||||
const innerCodeVerifier = generators.codeVerifier();
|
||||
const innerState = generators.state();
|
||||
|
||||
@ -73,10 +73,12 @@ export const GET = smartRouteHandler(async (req: NextRequest, options: { params:
|
||||
}
|
||||
|
||||
const provider = project.evaluatedConfig.oauthProviders.find((p) => p.id === providerId);
|
||||
|
||||
if (!provider) {
|
||||
throw new StatusError(StatusError.NotFound, "Provider not found");
|
||||
}
|
||||
if (!provider.enabled) {
|
||||
throw new StatusError(StatusError.NotFound, "Provider not enabled");
|
||||
}
|
||||
|
||||
const userInfo = await getAuthorizationCallback(
|
||||
provider,
|
||||
|
||||
@ -6,7 +6,7 @@ import { checkApiKeySet, publishableClientKeyHeaderSchema, superSecretAdminKeyHe
|
||||
import { getProject, isProjectAdmin, updateProject } from "@/lib/projects";
|
||||
import { ClientProjectJson, SharedProvider, StandardProvider, sharedProviders, standardProviders } from "@stackframe/stack-shared/dist/interface/clientInterface";
|
||||
import { ProjectIdOrKeyInvalidErrorCode, KnownError } from "@stackframe/stack-shared/dist/utils/types";
|
||||
import { OauthProviderUpdateOptions, ProjectUpdateOptions } from "@stackframe/stack-shared/dist/interface/adminInterface";
|
||||
import { ProjectUpdateOptions } from "@stackframe/stack-shared/dist/interface/adminInterface";
|
||||
|
||||
const putOrGetSchema = yup.object({
|
||||
headers: yup.object({
|
||||
@ -16,7 +16,6 @@ const putOrGetSchema = yup.object({
|
||||
"x-stack-project-id": yup.string().required(),
|
||||
}).required(),
|
||||
body: yup.object({
|
||||
showDisabledOauth: yup.boolean().optional(),
|
||||
isProductionMode: yup.boolean().optional(),
|
||||
config: yup.object({
|
||||
domains: yup.array(yup.object({
|
||||
@ -50,7 +49,7 @@ const handler = smartRouteHandler(async (req: NextRequest, options: { params: {
|
||||
body,
|
||||
} = await parseRequest(req, putOrGetSchema);
|
||||
|
||||
const { showDisabledOauth, ...update } = body ?? {};
|
||||
const { ...update } = body ?? {};
|
||||
|
||||
const pkValid = await checkApiKeySet(projectId, { publishableClientKey });
|
||||
const asValid = await isProjectAdmin(projectId, adminAccessToken);
|
||||
@ -96,16 +95,12 @@ const handler = smartRouteHandler(async (req: NextRequest, options: { params: {
|
||||
const project = await updateProject(
|
||||
projectId,
|
||||
typedUpdate,
|
||||
showDisabledOauth,
|
||||
);
|
||||
return NextResponse.json(project);
|
||||
} else if (asValid || pkValid) {
|
||||
if (Object.entries(update).length !== 0) {
|
||||
throw new StatusError(StatusError.Forbidden, "Can't update project with only publishable client key");
|
||||
}
|
||||
if (showDisabledOauth) {
|
||||
throw new StatusError(StatusError.Forbidden, "Can't show disabled oauth providers with only publishable client key");
|
||||
}
|
||||
|
||||
const project = await getProject(projectId);
|
||||
if (!project) {
|
||||
@ -117,6 +112,7 @@ const handler = smartRouteHandler(async (req: NextRequest, options: { params: {
|
||||
oauthProviders: project.evaluatedConfig.oauthProviders.map(
|
||||
(provider) => ({
|
||||
id: provider.id,
|
||||
enabled: provider.enabled,
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import * as yup from 'yup';
|
||||
import { ApiKeySetFirstViewJson, ApiKeySetSummaryJson } from '@stackframe/stack-shared';
|
||||
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';
|
||||
@ -28,7 +28,7 @@ export async function getApiKeySet(
|
||||
| { publishableClientKey: string }
|
||||
| { secretServerKey: string }
|
||||
| { superSecretAdminKey: string },
|
||||
): Promise<ApiKeySetSummaryJson | null> {
|
||||
): Promise<ApiKeySetJson | null> {
|
||||
const where = typeof whereOrId === 'string'
|
||||
? {
|
||||
projectId_id: {
|
||||
@ -51,7 +51,7 @@ export async function getApiKeySet(
|
||||
|
||||
export async function listApiKeySets(
|
||||
projectId: string,
|
||||
): Promise<ApiKeySetSummaryJson[]> {
|
||||
): Promise<ApiKeySetJson[]> {
|
||||
const sets = await prismaClient.apiKeySet.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
@ -121,7 +121,7 @@ export async function revokeApiKeySet(projectId: string, apiKeyId: string) {
|
||||
return createSummaryFromDbType(set);
|
||||
}
|
||||
|
||||
function createSummaryFromDbType(set: ApiKeySet): ApiKeySetSummaryJson {
|
||||
function createSummaryFromDbType(set: ApiKeySet): ApiKeySetJson {
|
||||
return {
|
||||
id: set.id,
|
||||
description: set.description,
|
||||
|
||||
@ -209,14 +209,13 @@ export async function createProject(
|
||||
return projectJsonFromDbType(project);
|
||||
}
|
||||
|
||||
export async function getProject(projectId: string, showDisabledOauth: boolean = false): Promise<ProjectJson | null> {
|
||||
return await updateProject(projectId, {}, showDisabledOauth);
|
||||
export async function getProject(projectId: string): Promise<ProjectJson | null> {
|
||||
return await updateProject(projectId, {});
|
||||
}
|
||||
|
||||
export async function updateProject(
|
||||
projectId: string,
|
||||
options: ProjectUpdateOptions,
|
||||
showDisabledOauth: boolean = false
|
||||
): Promise<ProjectJson | null> {
|
||||
// TODO: Validate production mode consistency
|
||||
const transaction = [];
|
||||
@ -393,10 +392,10 @@ export async function updateProject(
|
||||
return null;
|
||||
}
|
||||
|
||||
return projectJsonFromDbType(updatedProject, showDisabledOauth);
|
||||
return projectJsonFromDbType(updatedProject);
|
||||
}
|
||||
|
||||
function projectJsonFromDbType(project: ProjectDB, showDisabledOauth: boolean = false): ProjectJson {
|
||||
function projectJsonFromDbType(project: ProjectDB): ProjectJson {
|
||||
let emailConfig: EmailConfigJson | undefined;
|
||||
const emailServiceConfig = project.config.emailServiceConfig;
|
||||
if (emailServiceConfig) {
|
||||
@ -435,9 +434,6 @@ function projectJsonFromDbType(project: ProjectDB, showDisabledOauth: boolean =
|
||||
handlerPath: domain.handlerPath,
|
||||
})),
|
||||
oauthProviders: project.config.oauthProviderConfigs.flatMap((provider): OauthProviderConfigJson[] => {
|
||||
if (!showDisabledOauth && !provider.enabled) {
|
||||
return [];
|
||||
}
|
||||
if (provider.proxiedOauthConfig) {
|
||||
return [{
|
||||
id: provider.id,
|
||||
|
||||
@ -14,11 +14,8 @@ export {
|
||||
} from "./interface/serverInterface";
|
||||
export {
|
||||
StackAdminInterface,
|
||||
ApiKeySetBase,
|
||||
ApiKeySetBaseJson,
|
||||
ApiKeySetFirstView,
|
||||
ApiKeySetFirstViewJson,
|
||||
ApiKeySetSummary,
|
||||
ApiKeySetSummaryJson,
|
||||
ApiKeySetJson,
|
||||
} from "./interface/adminInterface";
|
||||
export { fetchTokenPrefix } from "./helpers/fetch-token";
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { ServerAuthApplicationOptions, StackServerInterface } from "./serverInterface";
|
||||
import { ProjectJson, SharedProvider, StandardProvider, TokenStore } from "./clientInterface";
|
||||
import { throwErr } from "../utils/errors";
|
||||
import { ReadonlyJson } from "../utils/json";
|
||||
import { ProjectJson, ReadonlyTokenStore, SharedProvider, StandardProvider, TokenStore } from "./clientInterface";
|
||||
|
||||
export type AdminAuthApplicationOptions = Readonly<
|
||||
ServerAuthApplicationOptions &
|
||||
@ -10,7 +8,7 @@ export type AdminAuthApplicationOptions = Readonly<
|
||||
superSecretAdminKey: string,
|
||||
}
|
||||
| {
|
||||
internalAdminAccessToken: string,
|
||||
projectOwnerTokens: ReadonlyTokenStore,
|
||||
}
|
||||
)
|
||||
>
|
||||
@ -30,7 +28,7 @@ export type OauthProviderUpdateOptions = {
|
||||
}
|
||||
)
|
||||
|
||||
export type ProjectUpdateOptions = Readonly<{
|
||||
export type ProjectUpdateOptions = {
|
||||
isProductionMode?: boolean,
|
||||
config?: {
|
||||
domains?: {
|
||||
@ -41,69 +39,41 @@ export type ProjectUpdateOptions = Readonly<{
|
||||
credentialEnabled?: boolean,
|
||||
allowLocalhost?: boolean,
|
||||
},
|
||||
}>
|
||||
};
|
||||
|
||||
export type ApiKeySetBase = Readonly<{
|
||||
id: string,
|
||||
description: string,
|
||||
expiresAt: Date,
|
||||
manuallyRevokedAt: Date | null,
|
||||
createdAt: Date,
|
||||
isValid(): boolean,
|
||||
whyInvalid(): "expired" | "manually-revoked" | null,
|
||||
}>
|
||||
|
||||
export type ApiKeySetBaseJson = Readonly<{
|
||||
export type ApiKeySetBaseJson = {
|
||||
id: string,
|
||||
description: string,
|
||||
expiresAtMillis: number,
|
||||
manuallyRevokedAtMillis: number | null,
|
||||
createdAtMillis: number,
|
||||
}>
|
||||
};
|
||||
|
||||
export type ApiKeySetFirstView = Readonly<
|
||||
ApiKeySetBase & {
|
||||
publishableClientKey?: string,
|
||||
secretServerKey?: string,
|
||||
superSecretAdminKey?: string,
|
||||
}
|
||||
>
|
||||
export type ApiKeySetFirstViewJson = ApiKeySetBaseJson & {
|
||||
publishableClientKey?: string,
|
||||
secretServerKey?: string,
|
||||
superSecretAdminKey?: string,
|
||||
};
|
||||
|
||||
export type ApiKeySetFirstViewJson = Readonly<
|
||||
ApiKeySetBaseJson & {
|
||||
publishableClientKey?: string,
|
||||
secretServerKey?: string,
|
||||
superSecretAdminKey?: string,
|
||||
}
|
||||
>
|
||||
export type ApiKeySetJson = ApiKeySetBaseJson & {
|
||||
publishableClientKey: null | {
|
||||
lastFour: string,
|
||||
},
|
||||
secretServerKey: null | {
|
||||
lastFour: string,
|
||||
},
|
||||
superSecretAdminKey: null | {
|
||||
lastFour: string,
|
||||
},
|
||||
};
|
||||
|
||||
export type ApiKeySetSummary = Readonly<
|
||||
ApiKeySetBase & {
|
||||
publishableClientKey: null | {
|
||||
lastFour: string,
|
||||
},
|
||||
secretServerKey: null | {
|
||||
lastFour: string,
|
||||
},
|
||||
superSecretAdminKey: null | {
|
||||
lastFour: string,
|
||||
},
|
||||
}
|
||||
>
|
||||
|
||||
export type ApiKeySetSummaryJson = Readonly<
|
||||
ApiKeySetBaseJson & {
|
||||
publishableClientKey: null | {
|
||||
lastFour: string,
|
||||
},
|
||||
secretServerKey: null | {
|
||||
lastFour: string,
|
||||
},
|
||||
superSecretAdminKey: null | {
|
||||
lastFour: string,
|
||||
},
|
||||
}
|
||||
>
|
||||
export type ApiKeySetCreateOptions = {
|
||||
hasPublishableClientKey: boolean,
|
||||
hasSecretServerKey: boolean,
|
||||
hasSuperSecretAdminKey: boolean,
|
||||
expiresAt: Date,
|
||||
description: string,
|
||||
};
|
||||
|
||||
export class StackAdminInterface extends StackServerInterface {
|
||||
constructor(public readonly options: AdminAuthApplicationOptions) {
|
||||
@ -155,14 +125,8 @@ export class StackAdminInterface extends StackServerInterface {
|
||||
}
|
||||
|
||||
async createApiKeySet(
|
||||
options: {
|
||||
hasPublishableClientKey: boolean,
|
||||
hasSecretServerKey: boolean,
|
||||
hasSuperSecretAdminKey: boolean,
|
||||
expiresAt: Date,
|
||||
description: string,
|
||||
},
|
||||
): Promise<ApiKeySetFirstView> {
|
||||
options: ApiKeySetCreateOptions,
|
||||
): Promise<ApiKeySetFirstViewJson> {
|
||||
const response = await this.sendServerRequest(
|
||||
"/api-keys",
|
||||
{
|
||||
@ -174,13 +138,13 @@ export class StackAdminInterface extends StackServerInterface {
|
||||
},
|
||||
null,
|
||||
);
|
||||
return createApiKeySetFirstViewFromJson(await response.json());
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async listApiKeySets(): Promise<ApiKeySetSummary[]> {
|
||||
async listApiKeySets(): Promise<ApiKeySetJson[]> {
|
||||
const response = await this.sendAdminRequest("/api-keys", {}, null);
|
||||
const json = await response.json();
|
||||
return json.map((k: ApiKeySetSummaryJson) => createApiKeySetSummaryFromJson(k));
|
||||
return json.map((k: ApiKeySetJson) => k);
|
||||
}
|
||||
|
||||
async revokeApiKeySetById(id: string) {
|
||||
@ -198,44 +162,9 @@ export class StackAdminInterface extends StackServerInterface {
|
||||
);
|
||||
}
|
||||
|
||||
async getApiKeySet(id: string, tokenStore: TokenStore): Promise<ApiKeySetSummary> {
|
||||
async getApiKeySet(id: string, tokenStore: TokenStore): Promise<ApiKeySetJson> {
|
||||
const response = await this.sendAdminRequest(`/api-keys/${id}`, {}, tokenStore);
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
function createApiKeySetBaseFromJson(data: ApiKeySetBaseJson): ApiKeySetBase {
|
||||
return {
|
||||
id: data.id,
|
||||
description: data.description,
|
||||
expiresAt: new Date(data.expiresAtMillis),
|
||||
manuallyRevokedAt: data.manuallyRevokedAtMillis ? new Date(data.manuallyRevokedAtMillis) : null,
|
||||
createdAt: new Date(data.createdAtMillis),
|
||||
isValid() {
|
||||
return this.whyInvalid() === null;
|
||||
},
|
||||
whyInvalid() {
|
||||
if (this.expiresAt.getTime() < Date.now()) return "expired";
|
||||
if (this.manuallyRevokedAt) return "manually-revoked";
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createApiKeySetSummaryFromJson(data: ApiKeySetSummaryJson): ApiKeySetSummary {
|
||||
return {
|
||||
...createApiKeySetBaseFromJson(data),
|
||||
publishableClientKey: data.publishableClientKey ? { lastFour: data.publishableClientKey.lastFour } : null,
|
||||
secretServerKey: data.secretServerKey ? { lastFour: data.secretServerKey.lastFour } : null,
|
||||
superSecretAdminKey: data.superSecretAdminKey ? { lastFour: data.superSecretAdminKey.lastFour } : null,
|
||||
};
|
||||
}
|
||||
|
||||
function createApiKeySetFirstViewFromJson(data: ApiKeySetFirstViewJson): ApiKeySetFirstView {
|
||||
return {
|
||||
...createApiKeySetBaseFromJson(data),
|
||||
publishableClientKey: data.publishableClientKey,
|
||||
secretServerKey: data.secretServerKey,
|
||||
superSecretAdminKey: data.superSecretAdminKey,
|
||||
};
|
||||
}
|
||||
|
||||
@ -15,11 +15,11 @@ import {
|
||||
PasswordResetLinkErrorCodes,
|
||||
PasswordResetLinkErrorCode
|
||||
} from "../utils/types";
|
||||
import { Result } from "../utils/results";
|
||||
import { AsyncResult, Result } from "../utils/results";
|
||||
import { ReadonlyJson, parseJson } from '../utils/json';
|
||||
import { AsyncCache } from '../utils/caches';
|
||||
import { typedAssign } from '../utils/objects';
|
||||
import { AsyncStore } from '../utils/stores';
|
||||
import { AsyncStore, ReadonlyAsyncStore } from '../utils/stores';
|
||||
import { neverResolve, runAsynchronously } from '../utils/promises';
|
||||
|
||||
export type UserCustomizableJson = {
|
||||
@ -43,6 +43,7 @@ export type ClientProjectJson = {
|
||||
readonly credentialEnabled: boolean,
|
||||
readonly oauthProviders: readonly {
|
||||
id: string,
|
||||
enabled: boolean,
|
||||
}[],
|
||||
};
|
||||
|
||||
@ -52,7 +53,7 @@ export type ClientInterfaceOptions = {
|
||||
} & ({
|
||||
readonly publishableClientKey: string,
|
||||
} | {
|
||||
readonly internalAdminAccessToken: string,
|
||||
readonly projectOwnerTokens: ReadonlyTokenStore,
|
||||
});
|
||||
|
||||
export type SharedProvider = "shared-github" | "shared-google" | "shared-facebook" | "shared-microsoft";
|
||||
@ -84,6 +85,7 @@ function getSessionCookieName(projectId: string) {
|
||||
return "__stack-token-" + crypto.createHash("sha256").update(projectId).digest("hex");
|
||||
}
|
||||
|
||||
export type ReadonlyTokenStore = ReadonlyAsyncStore<TokenObject>;
|
||||
export type TokenStore = AsyncStore<TokenObject>;
|
||||
|
||||
export type TokenObject = Readonly<{
|
||||
@ -142,6 +144,11 @@ export type DomainConfigJson = {
|
||||
handlerPath: string,
|
||||
}
|
||||
|
||||
export type ProductionModeError = {
|
||||
errorMessage: string,
|
||||
fixUrlRelative: string,
|
||||
};
|
||||
|
||||
export class StackClientInterface {
|
||||
constructor(public readonly options: ClientInterfaceOptions) {
|
||||
// nothing here
|
||||
@ -238,25 +245,13 @@ export class StackClientInterface {
|
||||
});
|
||||
|
||||
|
||||
try {
|
||||
return await Result.orThrowAsync(
|
||||
Result.retry(
|
||||
() => this.sendClientRequestInner(path, requestOptions, tokenStore!),
|
||||
5,
|
||||
{ exponentialDelayBase: 1000 },
|
||||
)
|
||||
);
|
||||
} catch (error: any) {
|
||||
// TODO this is a hack. Occurs when the admin access token is invalid, or expired. Has plenty of weird side effects so we should replace this
|
||||
if ("internalAdminAccessToken" in this.options && error?.message?.includes?.("Invalid API key") && typeof window !== "undefined") {
|
||||
alert("Your session has expired. The page will now reload." + (process.env.NODE_ENV == "development" ? "\n\nThis is a hack and we should probably fix this at some point." : ""));
|
||||
window.location.href = "/";
|
||||
document.body.innerHTML = "Reloading...";
|
||||
await neverResolve();
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
return await Result.orThrowAsync(
|
||||
Result.retry(
|
||||
() => this.sendClientRequestInner(path, requestOptions, tokenStore!),
|
||||
5,
|
||||
{ exponentialDelayBase: 1000 },
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected async sendClientRequestAndCatchKnownError<E>(
|
||||
@ -302,8 +297,8 @@ export class StackClientInterface {
|
||||
...'publishableClientKey' in this.options ? {
|
||||
"x-stack-publishable-client-key": this.options.publishableClientKey,
|
||||
} : {},
|
||||
...'internalAdminAccessToken' in this.options ? {
|
||||
"x-stack-admin-access-token": this.options.internalAdminAccessToken,
|
||||
...'projectOwnerTokens' in this.options ? {
|
||||
"x-stack-admin-access-token": AsyncResult.or(this.options.projectOwnerTokens?.get(), null)?.accessToken ?? "",
|
||||
} : {},
|
||||
...options.headers,
|
||||
},
|
||||
@ -672,8 +667,8 @@ export class StackClientInterface {
|
||||
}
|
||||
}
|
||||
|
||||
export function getProductionModeErrors(project: ProjectJson): { errorMessage: string, fixUrlRelative: string }[] {
|
||||
const errors: { errorMessage: string, fixUrlRelative: string }[] = [];
|
||||
export function getProductionModeErrors(project: ProjectJson): ProductionModeError[] {
|
||||
const errors: ProductionModeError[] = [];
|
||||
|
||||
for (const { domain, handlerPath } of project.evaluatedConfig.domains) {
|
||||
// TODO: check if handlerPath is valid
|
||||
|
||||
@ -3,7 +3,8 @@ import {
|
||||
UserCustomizableJson,
|
||||
UserJson,
|
||||
TokenStore,
|
||||
StackClientInterface,
|
||||
StackClientInterface,
|
||||
ReadonlyTokenStore,
|
||||
} from "./clientInterface";
|
||||
import { Result } from "../utils/results";
|
||||
import { AsyncCache } from "../utils/caches";
|
||||
@ -28,7 +29,7 @@ export type ServerAuthApplicationOptions = (
|
||||
readonly secretServerKey: string,
|
||||
}
|
||||
| {
|
||||
readonly internalAdminAccessToken: string,
|
||||
readonly projectOwnerTokens: ReadonlyTokenStore,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@ -85,7 +85,7 @@ class AsyncValueCache<T> {
|
||||
this._store = new AsyncStore();
|
||||
this._rateLimitOptions = {
|
||||
concurrency: 1,
|
||||
debounceMs: 3_000,
|
||||
debounceMs: 300,
|
||||
...filterUndefined(_options.rateLimiter ?? {}),
|
||||
};
|
||||
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import MessageCard from "../elements/MessageCard";
|
||||
import { useStackApp } from "..";
|
||||
import { StackClientApp, useStackApp } from "..";
|
||||
import { use } from "react";
|
||||
import PasswordResetInner from "../elements/PasswordResetInner";
|
||||
import { PasswordResetLinkExpiredErrorCode, PasswordResetLinkInvalidErrorCode, PasswordResetLinkUsedErrorCode } from "@stackframe/stack-shared/dist/utils/types";
|
||||
import { useStrictMemo } from "@stackframe/stack-shared/dist/hooks/use-strict-memo";
|
||||
import { cacheFunction } from "@stackframe/stack-shared/dist/utils/caches";
|
||||
|
||||
const cachedVerifyPasswordResetCode = cacheFunction(async (stackApp: StackClientApp<true>, code: string) => {
|
||||
return await stackApp.verifyPasswordResetCode(code);
|
||||
});
|
||||
|
||||
export default function PasswordReset({
|
||||
searchParams,
|
||||
@ -39,10 +43,7 @@ export default function PasswordReset({
|
||||
return invalidJsx;
|
||||
}
|
||||
|
||||
const errorCdoePromise = useStrictMemo(() => {
|
||||
return stackApp.verifyPasswordResetCode(code);
|
||||
}, [code]);
|
||||
const errorCode = use(errorCdoePromise);
|
||||
const errorCode = use(cachedVerifyPasswordResetCode(stackApp, code));
|
||||
|
||||
switch (errorCode) {
|
||||
case PasswordResetLinkInvalidErrorCode: {
|
||||
|
||||
@ -13,8 +13,8 @@ export default function OauthGroup({
|
||||
|
||||
return (
|
||||
<div className="wl_space-y-4 wl_flex wl_flex-col wl_items-stretch">
|
||||
{project.oauthProviders.map(({ id }) => (
|
||||
<OauthButton key={id} provider={id} type={type} redirectUrl={redirectUrl} />
|
||||
{project.oauthProviders.filter(p => p.enabled).map(p => (
|
||||
<OauthButton key={p.id} provider={p.id} type={type} redirectUrl={redirectUrl} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -6,7 +6,7 @@ import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids";
|
||||
import { AsyncResult, Result } from "@stackframe/stack-shared/dist/utils/results";
|
||||
import { suspendIfSsr } from "@stackframe/stack-shared/dist/utils/react";
|
||||
import { AsyncStore } from "@stackframe/stack-shared/dist/utils/stores";
|
||||
import { ClientProjectJson, UserCustomizableJson, UserJson, TokenObject, TokenStore, ProjectJson, EmailConfigJson, DomainConfigJson } from "@stackframe/stack-shared/dist/interface/clientInterface";
|
||||
import { ClientProjectJson, UserCustomizableJson, UserJson, TokenObject, TokenStore, ProjectJson, EmailConfigJson, DomainConfigJson, ReadonlyTokenStore, getProductionModeErrors, ProductionModeError } from "@stackframe/stack-shared/dist/interface/clientInterface";
|
||||
import { isClient } from "../utils/next";
|
||||
import { callOauthCallback, signInWithCredential, signInWithOauth, signUpWithCredential } from "./auth";
|
||||
import { RedirectType, redirect, useRouter } from "next/navigation";
|
||||
@ -14,8 +14,9 @@ import { ReadonlyJson } from "../utils/types";
|
||||
import { constructRedirectUrl } from "../utils/url";
|
||||
import { EmailVerificationLinkErrorCode, PasswordResetLinkErrorCode, SignInErrorCode, SignUpErrorCode } from "@stackframe/stack-shared/dist/utils/types";
|
||||
import { filterUndefined } from "@stackframe/stack-shared/dist/utils/objects";
|
||||
import { neverResolve, resolved, runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { neverResolve, resolved } 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";
|
||||
|
||||
|
||||
export type TokenStoreOptions<HasTokenStore extends boolean = boolean> =
|
||||
@ -87,9 +88,20 @@ export type StackServerAppConstructorOptions<HasTokenStore extends boolean, Proj
|
||||
secretServerKey?: string,
|
||||
};
|
||||
|
||||
export type StackAdminAppConstructorOptions<HasTokenStore extends boolean, ProjectId extends string> = StackServerAppConstructorOptions<HasTokenStore, ProjectId> & {
|
||||
superSecretAdminKey?: string,
|
||||
};
|
||||
export type StackAdminAppConstructorOptions<HasTokenStore extends boolean, ProjectId extends string> = (
|
||||
| (
|
||||
& StackServerAppConstructorOptions<HasTokenStore, ProjectId>
|
||||
& {
|
||||
superSecretAdminKey?: string,
|
||||
}
|
||||
)
|
||||
| (
|
||||
& Omit<StackServerAppConstructorOptions<HasTokenStore, ProjectId>, "publishableClientKey" | "secretServerKey">
|
||||
& {
|
||||
projectOwnerTokens: ReadonlyTokenStore,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export type StackClientAppJson<HasTokenStore extends boolean, ProjectId extends string> = StackClientAppConstructorOptions<HasTokenStore, ProjectId> & {
|
||||
uniqueIdentifier: string,
|
||||
@ -218,6 +230,7 @@ const createCacheByTokenStore = <D extends any[], T>(fetcher: (tokenStore: Token
|
||||
async ([tokenStore, ...extraDependencies]) => await fetcher(tokenStore, extraDependencies),
|
||||
{
|
||||
onSubscribe: ([tokenStore], refresh) => {
|
||||
// TODO find a *clean* way to not refresh when the token change was made inside the fetcher (for example due to expired access token)
|
||||
const handlerObj = tokenStore.onChange((newValue, oldValue) => {
|
||||
if (JSON.stringify(newValue) === JSON.stringify(oldValue)) return;
|
||||
refresh();
|
||||
@ -242,6 +255,9 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
private readonly _currentProjectCache = createCache(async () => {
|
||||
return Result.orThrow(await this._interface.getClientProject());
|
||||
});
|
||||
private readonly _ownedProjectsCache = createCacheByTokenStore(async (tokenStore) => {
|
||||
return await this._interface.listProjects(tokenStore);
|
||||
});
|
||||
|
||||
constructor(options:
|
||||
& {
|
||||
@ -312,12 +328,7 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
const app = this;
|
||||
const res: CurrentUser = {
|
||||
...this._userFromJson(json),
|
||||
get accessToken() {
|
||||
return AsyncResult.or(tokenStore.get(), null)?.accessToken ?? null;
|
||||
},
|
||||
get refreshToken() {
|
||||
return AsyncResult.or(tokenStore.get(), null)?.refreshToken ?? null;
|
||||
},
|
||||
tokenStore,
|
||||
update(update) {
|
||||
return app._updateUser(update, tokenStore);
|
||||
},
|
||||
@ -342,7 +353,11 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
};
|
||||
}
|
||||
|
||||
protected _projectAdminFromJson(data: ProjectJson): Project {
|
||||
protected _projectAdminFromJson(data: ProjectJson, adminInterface: StackAdminInterface, onRefresh: () => Promise<void>): Project {
|
||||
if (data.id !== adminInterface.projectId) {
|
||||
throw new Error(`The project ID of the provided project JSON (${data.id}) does not match the project ID of the app (${adminInterface.projectId})! This is a Stack bug.`);
|
||||
}
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
displayName: data.displayName,
|
||||
@ -352,14 +367,36 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
isProductionMode: data.isProductionMode,
|
||||
evaluatedConfig: {
|
||||
id: data.evaluatedConfig.id,
|
||||
credentialEnabled: data.evaluatedConfig.credentialEnabled,
|
||||
allowLocalhost: data.evaluatedConfig.allowLocalhost,
|
||||
oauthProviders: data.evaluatedConfig.oauthProviders,
|
||||
emailConfig: data.evaluatedConfig.emailConfig,
|
||||
domains: data.evaluatedConfig.domains,
|
||||
},
|
||||
|
||||
async update(update: ProjectUpdateOptions) {
|
||||
await adminInterface.updateProject(update);
|
||||
await onRefresh();
|
||||
},
|
||||
|
||||
toJson() {
|
||||
return data;
|
||||
},
|
||||
|
||||
getProductionModeErrors() {
|
||||
return getProductionModeErrors(this.toJson());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected _createAdminInterface(forProjectId: string, tokenStore: TokenStore): StackAdminInterface {
|
||||
return new StackAdminInterface({
|
||||
baseUrl: this._interface.options.baseUrl,
|
||||
projectId: forProjectId,
|
||||
projectOwnerTokens: tokenStore,
|
||||
});
|
||||
}
|
||||
|
||||
get projectId(): ProjectId {
|
||||
return this._interface.projectId as ProjectId;
|
||||
}
|
||||
@ -542,25 +579,66 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
async listOwnedProjects(): Promise<Project[]> {
|
||||
this._ensureInternalProject();
|
||||
const tokenStore = getTokenStore(this._tokenStoreOptions);
|
||||
const json = await this._interface.listProjects(tokenStore);
|
||||
return json.map((j) => this._projectAdminFromJson(j));
|
||||
const json = await this._ownedProjectsCache.getOrWait([tokenStore], "never");
|
||||
return json.map((j) => this._projectAdminFromJson(
|
||||
j,
|
||||
this._createAdminInterface(j.id, tokenStore),
|
||||
() => this._refreshOwnedProjects(tokenStore),
|
||||
));
|
||||
}
|
||||
|
||||
useOwnedProjects(): Project[] {
|
||||
this._ensureInternalProject();
|
||||
const tokenStore = getTokenStore(this._tokenStoreOptions);
|
||||
const json = useCache(this._ownedProjectsCache, [tokenStore]);
|
||||
return json.map((j) => this._projectAdminFromJson(
|
||||
j,
|
||||
this._createAdminInterface(j.id, tokenStore),
|
||||
() => this._refreshOwnedProjects(tokenStore),
|
||||
));
|
||||
}
|
||||
|
||||
onOwnedProjectsChange(callback: (projects: Project[]) => void) {
|
||||
this._ensureInternalProject();
|
||||
const tokenStore = getTokenStore(this._tokenStoreOptions);
|
||||
return this._ownedProjectsCache.onChange([tokenStore], (projects) => {
|
||||
callback(projects.map((j) => this._projectAdminFromJson(
|
||||
j,
|
||||
this._createAdminInterface(j.id, tokenStore),
|
||||
() => this._refreshOwnedProjects(tokenStore),
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
async createProject(newProject: Pick<Project, "displayName" | "description">): Promise<Project> {
|
||||
this._ensureInternalProject();
|
||||
const tokenStore = getTokenStore(this._tokenStoreOptions);
|
||||
const json = await this._interface.createProject(newProject, tokenStore);
|
||||
return this._projectAdminFromJson(json);
|
||||
const res = this._projectAdminFromJson(
|
||||
json,
|
||||
this._createAdminInterface(json.id, tokenStore),
|
||||
() => this._refreshOwnedProjects(tokenStore),
|
||||
);
|
||||
await this._refreshOwnedProjects(tokenStore);
|
||||
return res;
|
||||
}
|
||||
|
||||
protected async _refreshUser(tokenStore: TokenStore) {
|
||||
await this._currentUserCache.refresh([tokenStore]);
|
||||
}
|
||||
|
||||
protected async _refreshUsers() {
|
||||
// nothing yet
|
||||
}
|
||||
|
||||
protected async _refreshProject() {
|
||||
await this._currentProjectCache.refresh([]);
|
||||
}
|
||||
|
||||
protected async _refreshOwnedProjects(tokenStore: TokenStore) {
|
||||
await this._ownedProjectsCache.refresh([tokenStore]);
|
||||
}
|
||||
|
||||
static get [stackAppInternalsSymbol]() {
|
||||
return {
|
||||
fromClientJson: <HasTokenStore extends boolean, ProjectId extends string>(
|
||||
@ -612,6 +690,9 @@ class _StackServerAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
const user = await this._interface.getServerUserByToken(tokenStore);
|
||||
return Result.or(user, null);
|
||||
});
|
||||
private readonly _serverUsersCache = createCache(async () => {
|
||||
return await this._interface.listUsers();
|
||||
});
|
||||
|
||||
constructor(options:
|
||||
| StackServerAppConstructorOptions<HasTokenStore, ProjectId>
|
||||
@ -650,15 +731,17 @@ class _StackServerAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
...this._userFromJson(json),
|
||||
serverMetadata: json.serverMetadata,
|
||||
async delete() {
|
||||
await app._interface.deleteServerUser(this.id);
|
||||
const res = await app._interface.deleteServerUser(this.id);
|
||||
await app._refreshUsers();
|
||||
return res;
|
||||
},
|
||||
async update(update: Partial<ServerUserCustomizableJson>) {
|
||||
await app._interface.setServerUserCustomizableData(this.id, update);
|
||||
const res = await app._interface.setServerUserCustomizableData(this.id, update);
|
||||
await app._refreshUsers();
|
||||
return res;
|
||||
},
|
||||
getClientUser() {
|
||||
return {
|
||||
...app._userFromJson(json),
|
||||
};
|
||||
return app._userFromJson(json);
|
||||
},
|
||||
toJson() {
|
||||
return app._serverUserToJson(this);
|
||||
@ -674,12 +757,7 @@ class _StackServerAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
const nonCurrentServerUser = this._serverUserFromJson(json);
|
||||
const res: CurrentServerUser = {
|
||||
...nonCurrentServerUser,
|
||||
get accessToken() {
|
||||
return AsyncResult.or(tokenStore.get(), null)?.accessToken ?? null;
|
||||
},
|
||||
get refreshToken() {
|
||||
return AsyncResult.or(tokenStore.get(), null)?.refreshToken ?? null;
|
||||
},
|
||||
tokenStore,
|
||||
async delete() {
|
||||
const res = await nonCurrentServerUser.delete();
|
||||
await app._refreshUser(tokenStore);
|
||||
@ -694,22 +772,7 @@ class _StackServerAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
return app._signOut(tokenStore, redirectUrl);
|
||||
},
|
||||
getClientUser() {
|
||||
const currentServerUser = this;
|
||||
return {
|
||||
...app._userFromJson(json),
|
||||
get accessToken() {
|
||||
return currentServerUser.accessToken;
|
||||
},
|
||||
get refreshToken() {
|
||||
return currentServerUser.refreshToken;
|
||||
},
|
||||
update(update: Partial<ServerUserCustomizableJson>) {
|
||||
return currentServerUser.update(update);
|
||||
},
|
||||
signOut(redirectUrl?: string) {
|
||||
return currentServerUser.signOut(redirectUrl);
|
||||
},
|
||||
};
|
||||
return app._currentUserFromJson(json, tokenStore);
|
||||
},
|
||||
};
|
||||
Object.freeze(res);
|
||||
@ -758,12 +821,35 @@ class _StackServerAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
});
|
||||
}
|
||||
|
||||
async listServerUsers(): Promise<ServerUser[]> {
|
||||
const json = await this._serverUsersCache.getOrWait([], "never");
|
||||
return json.map((j) => this._serverUserFromJson(j));
|
||||
}
|
||||
|
||||
useServerUsers(): ServerUser[] {
|
||||
const json = useCache(this._serverUsersCache, []);
|
||||
return json.map((j) => this._serverUserFromJson(j));
|
||||
}
|
||||
|
||||
onServerUsersChange(callback: (users: ServerUser[]) => void) {
|
||||
return this._serverUsersCache.onChange([], (users) => {
|
||||
callback(users.map((j) => this._serverUserFromJson(j)));
|
||||
});
|
||||
}
|
||||
|
||||
protected override async _refreshUser(tokenStore: TokenStore) {
|
||||
await Promise.all([
|
||||
super._refreshUser(tokenStore),
|
||||
this._currentServerUserCache.refresh([tokenStore]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected override async _refreshUsers() {
|
||||
await Promise.all([
|
||||
super._refreshUsers(),
|
||||
this._serverUsersCache.refresh([]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class _StackAdminAppImpl<HasTokenStore extends boolean, ProjectId extends string> extends _StackServerAppImpl<HasTokenStore, ProjectId>
|
||||
@ -773,46 +859,133 @@ class _StackAdminAppImpl<HasTokenStore extends boolean, ProjectId extends string
|
||||
private readonly _adminProjectCache = createCache(async () => {
|
||||
return await this._interface.getProject();
|
||||
});
|
||||
private readonly _apiKeySetsCache = createCache(async () => {
|
||||
return await this._interface.listApiKeySets();
|
||||
});
|
||||
|
||||
constructor(options: StackAdminAppConstructorOptions<HasTokenStore, ProjectId>) {
|
||||
super({
|
||||
interface: new StackAdminInterface({
|
||||
baseUrl: options.baseUrl ?? getDefaultBaseUrl(),
|
||||
projectId: options.projectId ?? getDefaultProjectId(),
|
||||
publishableClientKey: options.publishableClientKey ?? getDefaultPublishableClientKey(),
|
||||
secretServerKey: options.secretServerKey ?? getDefaultSecretServerKey(),
|
||||
superSecretAdminKey: options.superSecretAdminKey ?? getDefaultSuperSecretAdminKey(),
|
||||
..."projectOwnerTokens" in options ? {
|
||||
projectOwnerTokens: options.projectOwnerTokens,
|
||||
} : {
|
||||
publishableClientKey: options.publishableClientKey ?? getDefaultPublishableClientKey(),
|
||||
secretServerKey: options.secretServerKey ?? getDefaultSecretServerKey(),
|
||||
superSecretAdminKey: options.superSecretAdminKey ?? getDefaultSuperSecretAdminKey(),
|
||||
},
|
||||
}),
|
||||
tokenStore: options.tokenStore,
|
||||
urls: options.urls,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
protected _createApiKeySetBaseFromJson(data: ApiKeySetBaseJson): ApiKeySetBase {
|
||||
const app = this;
|
||||
return {
|
||||
id: data.id,
|
||||
description: data.description,
|
||||
expiresAt: new Date(data.expiresAtMillis),
|
||||
manuallyRevokedAt: data.manuallyRevokedAtMillis ? new Date(data.manuallyRevokedAtMillis) : null,
|
||||
createdAt: new Date(data.createdAtMillis),
|
||||
isValid() {
|
||||
return this.whyInvalid() === null;
|
||||
},
|
||||
whyInvalid() {
|
||||
if (this.expiresAt.getTime() < Date.now()) return "expired";
|
||||
if (this.manuallyRevokedAt) return "manually-revoked";
|
||||
return null;
|
||||
},
|
||||
async revoke() {
|
||||
const res = await app._interface.revokeApiKeySetById(data.id);
|
||||
await app._refreshApiKeySets();
|
||||
return res;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected _createApiKeySetFromJson(data: ApiKeySetJson): ApiKeySet {
|
||||
return {
|
||||
...this._createApiKeySetBaseFromJson(data),
|
||||
publishableClientKey: data.publishableClientKey ? { lastFour: data.publishableClientKey.lastFour } : null,
|
||||
secretServerKey: data.secretServerKey ? { lastFour: data.secretServerKey.lastFour } : null,
|
||||
superSecretAdminKey: data.superSecretAdminKey ? { lastFour: data.superSecretAdminKey.lastFour } : null,
|
||||
};
|
||||
}
|
||||
|
||||
protected _createApiKeySetFirstViewFromJson(data: ApiKeySetFirstViewJson): ApiKeySetFirstView {
|
||||
return {
|
||||
...this._createApiKeySetBaseFromJson(data),
|
||||
publishableClientKey: data.publishableClientKey,
|
||||
secretServerKey: data.secretServerKey,
|
||||
superSecretAdminKey: data.superSecretAdminKey,
|
||||
};
|
||||
}
|
||||
|
||||
async getProjectAdmin(): Promise<Project> {
|
||||
return this._projectAdminFromJson(await this._adminProjectCache.getOrWait([], "never"));
|
||||
return this._projectAdminFromJson(
|
||||
await this._adminProjectCache.getOrWait([], "never"),
|
||||
this._interface,
|
||||
() => this._refreshProject()
|
||||
);
|
||||
}
|
||||
|
||||
useProjectAdmin(): Project {
|
||||
return this._projectAdminFromJson(useCache(this._adminProjectCache, []));
|
||||
return this._projectAdminFromJson(
|
||||
useCache(this._adminProjectCache, []),
|
||||
this._interface,
|
||||
() => this._refreshProject()
|
||||
);
|
||||
}
|
||||
|
||||
onProjectAdminChange(callback: (project: Project) => void) {
|
||||
return this._adminProjectCache.onChange([], () => {
|
||||
callback(this._projectAdminFromJson(useCache(this._adminProjectCache, [])));
|
||||
return this._adminProjectCache.onChange([], (project) => {
|
||||
callback(this._projectAdminFromJson(
|
||||
project,
|
||||
this._interface,
|
||||
() => this._refreshProject()
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
async listApiKeySets(): Promise<ApiKeySet[]> {
|
||||
const json = await this._apiKeySetsCache.getOrWait([], "never");
|
||||
return json.map((j) => this._createApiKeySetFromJson(j));
|
||||
}
|
||||
|
||||
useApiKeySets(): ApiKeySet[] {
|
||||
const json = useCache(this._apiKeySetsCache, []);
|
||||
return json.map((j) => this._createApiKeySetFromJson(j));
|
||||
}
|
||||
|
||||
onApiKeySetsChange(callback: (apiKeySets: ApiKeySet[]) => void) {
|
||||
return this._apiKeySetsCache.onChange([], (apiKeySets) => {
|
||||
callback(apiKeySets.map((j) => this._createApiKeySetFromJson(j)));
|
||||
});
|
||||
}
|
||||
|
||||
async createApiKeySet(options: ApiKeySetCreateOptions): Promise<ApiKeySetFirstView> {
|
||||
const json = await this._interface.createApiKeySet(options);
|
||||
await this._refreshApiKeySets();
|
||||
return this._createApiKeySetFirstViewFromJson(json);
|
||||
}
|
||||
|
||||
protected override async _refreshProject() {
|
||||
await Promise.all([
|
||||
super._refreshProject(),
|
||||
this._adminProjectCache.refresh([]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected async _refreshApiKeySets() {
|
||||
await this._apiKeySetsCache.refresh([]);
|
||||
}
|
||||
}
|
||||
|
||||
type Auth<T, C> = {
|
||||
readonly accessToken: string | null,
|
||||
readonly refreshToken: string | null,
|
||||
readonly tokenStore: ReadonlyTokenStore,
|
||||
update(this: T, user: Partial<C>): Promise<void>,
|
||||
signOut(this: T, redirectUrl?: string): Promise<never>,
|
||||
};
|
||||
@ -865,21 +1038,58 @@ export type CurrentServerUser = Auth<ServerUser, ServerUserCustomizableJson> & O
|
||||
getClientUser(this: CurrentServerUser): CurrentUser,
|
||||
};
|
||||
|
||||
export type Project = Readonly<{
|
||||
id: string,
|
||||
displayName: string,
|
||||
description?: string,
|
||||
createdAt: Date,
|
||||
userCount: number,
|
||||
isProductionMode: boolean,
|
||||
evaluatedConfig: {
|
||||
id: string,
|
||||
allowLocalhost: boolean,
|
||||
oauthProviders: OauthProviderConfig[],
|
||||
emailConfig?: EmailConfig,
|
||||
domains: DomainConfig[],
|
||||
export type Project = {
|
||||
readonly id: string,
|
||||
readonly displayName: string,
|
||||
readonly description?: string,
|
||||
readonly createdAt: Date,
|
||||
readonly userCount: number,
|
||||
readonly isProductionMode: boolean,
|
||||
readonly evaluatedConfig: {
|
||||
readonly id: string,
|
||||
readonly allowLocalhost: boolean,
|
||||
readonly credentialEnabled: boolean,
|
||||
readonly oauthProviders: OauthProviderConfig[],
|
||||
readonly emailConfig?: EmailConfig,
|
||||
readonly domains: DomainConfig[],
|
||||
},
|
||||
}>;
|
||||
|
||||
update(this: Project, update: ProjectUpdateOptions): Promise<void>,
|
||||
|
||||
toJson(this: Project): ProjectJson,
|
||||
|
||||
getProductionModeErrors(this: Project): ProductionModeError[],
|
||||
};
|
||||
|
||||
export type ApiKeySetBase = {
|
||||
id: string,
|
||||
description: string,
|
||||
expiresAt: Date,
|
||||
manuallyRevokedAt: Date | null,
|
||||
createdAt: Date,
|
||||
isValid(): boolean,
|
||||
whyInvalid(): "expired" | "manually-revoked" | null,
|
||||
revoke(): Promise<void>,
|
||||
};
|
||||
|
||||
export type ApiKeySetFirstView = ApiKeySetBase & {
|
||||
publishableClientKey?: string,
|
||||
secretServerKey?: string,
|
||||
superSecretAdminKey?: string,
|
||||
};
|
||||
|
||||
export type ApiKeySet = ApiKeySetBase & {
|
||||
publishableClientKey: null | {
|
||||
lastFour: string,
|
||||
},
|
||||
secretServerKey: null | {
|
||||
lastFour: string,
|
||||
},
|
||||
superSecretAdminKey: null | {
|
||||
lastFour: string,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export type EmailConfig = EmailConfigJson;
|
||||
|
||||
@ -891,12 +1101,12 @@ export type GetUserOptions = {
|
||||
or?: 'redirect' | 'throw' | 'return-null',
|
||||
};
|
||||
|
||||
type AsyncStoreProperty<Name extends string, Value> =
|
||||
& { [key in `get${Capitalize<Name>}`]: () => Promise<Value> }
|
||||
type AsyncStoreProperty<Name extends string, Value, IsMultiple extends boolean> =
|
||||
& { [key in `${IsMultiple extends true ? "list" : "get"}${Capitalize<Name>}`]: () => Promise<Value> }
|
||||
& { [key in `on${Capitalize<Name>}Change`]: (callback: (value: Value) => void) => void }
|
||||
& { [key in `use${Capitalize<Name>}`]: () => Value }
|
||||
|
||||
export type StackClientApp<HasTokenStore extends boolean, ProjectId extends string = string> = (
|
||||
export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId extends string = string> = (
|
||||
& {
|
||||
readonly projectId: ProjectId,
|
||||
|
||||
@ -915,7 +1125,7 @@ export type StackClientApp<HasTokenStore extends boolean, ProjectId extends stri
|
||||
toClientJson(): Promise<StackClientAppJson<HasTokenStore, ProjectId>>,
|
||||
},
|
||||
}
|
||||
& AsyncStoreProperty<"project", ClientProjectJson>
|
||||
& AsyncStoreProperty<"project", ClientProjectJson, false>
|
||||
& { [K in `redirectTo${Capitalize<keyof HandlerUrls>}`]: () => Promise<never> }
|
||||
& (HasTokenStore extends false
|
||||
? {}
|
||||
@ -926,13 +1136,15 @@ export type StackClientApp<HasTokenStore extends boolean, ProjectId extends stri
|
||||
getUser(options: GetUserOptions & { or: 'redirect' }): Promise<CurrentUser>,
|
||||
getUser(options: GetUserOptions & { or: 'throw' }): Promise<CurrentUser>,
|
||||
getUser(options?: GetUserOptions): Promise<CurrentUser | null>,
|
||||
onUserChange: AsyncStoreProperty<"user", CurrentUser | null>["onUserChange"],
|
||||
onUserChange: AsyncStoreProperty<"user", CurrentUser | null, false>["onUserChange"],
|
||||
})
|
||||
& (
|
||||
ProjectId extends "internal" ? {
|
||||
listOwnedProjects(): Promise<Project[]>,
|
||||
createProject(project: Pick<Project, "displayName" | "description">): Promise<Project>,
|
||||
} : {}
|
||||
ProjectId extends "internal" ? (
|
||||
& AsyncStoreProperty<"ownedProjects", Project[], true>
|
||||
& {
|
||||
createProject(project: Pick<Project, "displayName" | "description">): Promise<Project>,
|
||||
}
|
||||
) : {}
|
||||
)
|
||||
);
|
||||
type StackClientAppConstructor = {
|
||||
@ -951,9 +1163,10 @@ type StackClientAppConstructor = {
|
||||
};
|
||||
export const StackClientApp: StackClientAppConstructor = _StackClientAppImpl;
|
||||
|
||||
export type StackServerApp<HasTokenStore extends boolean, ProjectId extends string = string> = (
|
||||
export type StackServerApp<HasTokenStore extends boolean = boolean, ProjectId extends string = string> = (
|
||||
& StackClientApp<HasTokenStore, ProjectId>
|
||||
& AsyncStoreProperty<"serverUser", CurrentServerUser | null>
|
||||
& AsyncStoreProperty<"serverUser", CurrentServerUser | null, false>
|
||||
& AsyncStoreProperty<"serverUsers", ServerUser[], true>
|
||||
& {}
|
||||
);
|
||||
type StackServerAppConstructor = {
|
||||
@ -966,9 +1179,13 @@ type StackServerAppConstructor = {
|
||||
};
|
||||
export const StackServerApp: StackServerAppConstructor = _StackServerAppImpl;
|
||||
|
||||
export type StackAdminApp<HasTokenStore extends boolean, ProjectId extends string = string> = (
|
||||
export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId extends string = string> = (
|
||||
& StackServerApp<HasTokenStore, ProjectId>
|
||||
& AsyncStoreProperty<"projectAdmin", Project>
|
||||
& AsyncStoreProperty<"projectAdmin", Project, false>
|
||||
& AsyncStoreProperty<"apiKeySets", ApiKeySet[], true>
|
||||
& {
|
||||
createApiKeySet(options: ApiKeySetCreateOptions): Promise<ApiKeySetFirstView>,
|
||||
}
|
||||
);
|
||||
type StackAdminAppConstructor = {
|
||||
new <
|
||||
|
||||
Loading…
Reference in New Issue
Block a user