Moved ownedProjects related functionalities to the user (#11)

* moved owned projects to user

* added internal user type

* removed old comments

* fixed type bug
This commit is contained in:
Zai Shi 2024-04-16 15:23:53 +02:00 committed by GitHub
parent 246669d779
commit 1ca01ca261
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 115 additions and 57 deletions

View File

@ -6,7 +6,7 @@ import Box from '@mui/joy/Box';
import IconButton from '@mui/joy/IconButton';
import { useAdminApp } from './use-admin-app';
import { redirect, usePathname } from 'next/navigation';
import { useStackApp } from '@stackframe/stack';
import { useUser } from '@stackframe/stack';
import { Icon } from '@/components/icon';
import Breadcrumbs from '@mui/joy/Breadcrumbs';
@ -37,9 +37,9 @@ function ProjectSwitchItem({ label }: { label: string }) {
}
function ProjectSwitch() {
const stackApp = useStackApp({ projectIdMustMatch: "internal" });
const stackAdminApp = useAdminApp();
const projects = stackApp.useOwnedProjects();
const user = useUser({ or: 'redirect', projectIdMustMatch: "internal" });
const projects = user.useOwnedProjects();
const project = projects.find((project) => project.id === stackAdminApp.projectId);
const renderValue = (option: SelectOption<string> | null) => {

View File

@ -2,7 +2,7 @@
import { Button, Card, CardContent, CardOverflow, Divider, FormControl, FormLabel, Grid, Input, Stack, Textarea, Typography } from "@mui/joy";
import { useId, useRef, useState } from "react";
import { useStackApp } from "@stackframe/stack";
import { useUser } from "@stackframe/stack";
import { prettyPrintWithMagnitudes } from "@stackframe/stack-shared/dist/utils/numbers";
import { Dialog } from "@/components/dialog";
import { Paragraph } from "@/components/paragraph";
@ -14,9 +14,8 @@ import { useRouter } from "next/navigation";
export default function ProjectsPageClient() {
const stackApp = useStackApp({ projectIdMustMatch: "internal" });
const projects = stackApp.useOwnedProjects();
const user = useUser({ or: 'redirect', projectIdMustMatch: "internal" });
const projects = user.useOwnedProjects();
const [createDialogOpen, setCreateDialogOpen] = useState(projects.length === 0);
@ -87,7 +86,7 @@ function CreateProjectDialog(props: { open: boolean, onClose(): void }) {
const formRef = useRef<HTMLFormElement>(null);
const formId = useId();
const [isCreating, setIsCreating] = useState(false);
const stackApp = useStackApp({ projectIdMustMatch: "internal" });
const user = useUser({ or: 'redirect', projectIdMustMatch: "internal" });
const router = useRouter();
return (
@ -116,7 +115,7 @@ function CreateProjectDialog(props: { open: boolean, onClose(): void }) {
setIsCreating(true);
try {
const formData = new FormData(event.currentTarget);
const project = await stackApp.createProject({
const project = await user.createProject({
displayName: `${formData.get('name')}`,
description: `${formData.get('description')}`,
});

View File

@ -1,4 +1,4 @@
import { CurrentUser, GetUserOptions, StackClientApp } from "./stack-app";
import { CurrentUser, GetUserOptions as AppGetUserOptions, StackClientApp, CurrentInternalUser } from "./stack-app";
import { StackContext } from "../providers/stack-provider-client";
import { useContext } from "react";
@ -8,11 +8,24 @@ import { useContext } from "react";
* @returns the current user
*/
export function useUser(options: GetUserOptions & { or: 'redirect' }): CurrentUser;
export function useUser(options: GetUserOptions & { or: 'throw' }): CurrentUser;
export function useUser(options?: GetUserOptions): CurrentUser | null;
export function useUser(options?: GetUserOptions): CurrentUser | null {
return useStackApp().useUser(options);
type GetUserOptions = AppGetUserOptions & {
projectIdMustMatch?: string,
}
export function useUser(options: GetUserOptions & { or: 'redirect' | 'throw', projectIdMustMatch: "internal" }): CurrentInternalUser;
export function useUser(options: GetUserOptions & { or: 'redirect' | 'throw' }): CurrentUser;
export function useUser(options: GetUserOptions & { projectIdMustMatch: "internal" }): CurrentInternalUser | null;
export function useUser(options?: GetUserOptions): CurrentUser | CurrentInternalUser | null;
export function useUser(options: GetUserOptions = {}): CurrentUser | CurrentInternalUser | null {
const stackApp = useStackApp(options);
if (options.projectIdMustMatch && stackApp.projectId !== options.projectIdMustMatch) {
throw new Error("Unexpected project ID in useStackApp: " + stackApp.projectId);
}
if (options.projectIdMustMatch === "internal") {
return stackApp.useUser(options) as CurrentInternalUser;
} else {
return stackApp.useUser(options) as CurrentUser;
}
}
/**

View File

@ -40,6 +40,9 @@ export type HandlerUrls = {
accountSettings: string,
}
type ProjectCurrentUser<ProjectId> = ProjectId extends "internal" ? CurrentInternalUser : CurrentUser;
type ProjectCurrentSeverUser<ProjectId> = ProjectId extends "internal" ? CurrentInternalServerUser : CurrentServerUser;
function getUrls(partial: Partial<HandlerUrls>): HandlerUrls {
const handler = partial.handler ?? "/handler";
return {
@ -351,12 +354,12 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
};
}
protected _currentUserFromJson(json: UserJson, tokenStore: TokenStore): CurrentUser;
protected _currentUserFromJson(json: UserJson | null, tokenStore: TokenStore): CurrentUser | null;
protected _currentUserFromJson(json: UserJson | null, tokenStore: TokenStore): CurrentUser | null {
protected _currentUserFromJson(json: UserJson, tokenStore: TokenStore): ProjectCurrentUser<ProjectId>;
protected _currentUserFromJson(json: UserJson | null, tokenStore: TokenStore): ProjectCurrentUser<ProjectId> | null;
protected _currentUserFromJson(json: UserJson | null, tokenStore: TokenStore): ProjectCurrentUser<ProjectId> | null {
if (json === null) return null;
const app = this;
const res: CurrentUser = {
const currentUser: CurrentUser = {
...this._userFromJson(json),
tokenStore,
update(update) {
@ -370,10 +373,30 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
},
updatePassword(options: { oldPassword: string, newPassword: string}) {
return app._updatePassword(options, tokenStore);
}
},
};
Object.freeze(res);
return res;
if (this.isInternalProject()) {
const internalUser: CurrentInternalUser = {
...currentUser,
createProject(newProject: Pick<Project, "displayName" | "description">) {
return app._createProject(newProject);
},
listOwnedProjects() {
return app._listOwnedProjects();
},
useOwnedProjects() {
return app._useOwnedProjects();
},
onOwnedProjectsChange(callback: (projects: Project[]) => void) {
return app._onOwnedProjectsChange(callback);
}
};
Object.freeze(internalUser);
return internalUser;
} else {
Object.freeze(currentUser);
return currentUser as any;
}
}
protected _userToJson(user: User): UserJson {
@ -482,10 +505,10 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
return await this._interface.verifyEmail(code);
}
async getUser(options: GetUserOptions & { or: 'redirect' }): Promise<CurrentUser>;
async getUser(options: GetUserOptions & { or: 'throw' }): Promise<CurrentUser>;
async getUser(options?: GetUserOptions): Promise<CurrentUser | null>;
async getUser(options?: GetUserOptions): Promise<CurrentUser | null> {
async getUser(options: GetUserOptions & { or: 'redirect' }): Promise<ProjectCurrentUser<ProjectId>>;
async getUser(options: GetUserOptions & { or: 'throw' }): Promise<ProjectCurrentUser<ProjectId>>;
async getUser(options?: GetUserOptions): Promise<ProjectCurrentUser<ProjectId> | null>;
async getUser(options?: GetUserOptions): Promise<ProjectCurrentUser<ProjectId> | null> {
this._ensurePersistentTokenStore();
const tokenStore = getTokenStore(this._tokenStoreOptions);
const userJson = await this._currentUserCache.getOrWait([tokenStore], "write-only");
@ -508,10 +531,10 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
return this._currentUserFromJson(userJson, tokenStore);
}
useUser(options: GetUserOptions & { or: 'redirect' }): CurrentUser;
useUser(options: GetUserOptions & { or: 'throw' }): CurrentUser;
useUser(options?: GetUserOptions): CurrentUser | null;
useUser(options?: GetUserOptions): CurrentUser | null {
useUser(options: GetUserOptions & { or: 'redirect' }): ProjectCurrentUser<ProjectId>;
useUser(options: GetUserOptions & { or: 'throw' }): ProjectCurrentUser<ProjectId>;
useUser(options?: GetUserOptions): ProjectCurrentUser<ProjectId> | null;
useUser(options?: GetUserOptions): ProjectCurrentUser<ProjectId> | null {
this._ensurePersistentTokenStore();
const router = useRouter();
@ -641,7 +664,7 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
return this._currentProjectCache.onChange([], callback);
}
async listOwnedProjects(): Promise<Project[]> {
protected async _listOwnedProjects(): Promise<Project[]> {
this._ensureInternalProject();
const tokenStore = getTokenStore(this._tokenStoreOptions);
const json = await this._ownedProjectsCache.getOrWait([tokenStore], "write-only");
@ -652,7 +675,7 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
));
}
useOwnedProjects(): Project[] {
protected _useOwnedProjects(): Project[] {
this._ensureInternalProject();
const tokenStore = getTokenStore(this._tokenStoreOptions);
const json = useCache(this._ownedProjectsCache, [tokenStore], "useOwnedProjects()");
@ -663,7 +686,7 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
)), [json]);
}
onOwnedProjectsChange(callback: (projects: Project[]) => void) {
protected _onOwnedProjectsChange(callback: (projects: Project[]) => void) {
this._ensureInternalProject();
const tokenStore = getTokenStore(this._tokenStoreOptions);
return this._ownedProjectsCache.onChange([tokenStore], (projects) => {
@ -675,7 +698,7 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
});
}
async createProject(newProject: Pick<Project, "displayName" | "description">): Promise<Project> {
protected async _createProject(newProject: Pick<Project, "displayName" | "description">): Promise<Project> {
this._ensureInternalProject();
const tokenStore = getTokenStore(this._tokenStoreOptions);
const json = await this._interface.createProject(newProject, tokenStore);
@ -823,13 +846,13 @@ class _StackServerAppImpl<HasTokenStore extends boolean, ProjectId extends strin
};
}
protected _currentServerUserFromJson(json: ServerUserJson, tokenStore: TokenStore): CurrentServerUser;
protected _currentServerUserFromJson(json: ServerUserJson | null, tokenStore: TokenStore): CurrentServerUser | null;
protected _currentServerUserFromJson(json: ServerUserJson | null, tokenStore: TokenStore): CurrentServerUser | null {
protected _currentServerUserFromJson(json: ServerUserJson, tokenStore: TokenStore): ProjectCurrentSeverUser<ProjectId>;
protected _currentServerUserFromJson(json: ServerUserJson | null, tokenStore: TokenStore): ProjectCurrentSeverUser<ProjectId> | null;
protected _currentServerUserFromJson(json: ServerUserJson | null, tokenStore: TokenStore): ProjectCurrentSeverUser<ProjectId> | null {
if (json === null) return null;
const app = this;
const nonCurrentServerUser = this._serverUserFromJson(json);
const res: CurrentServerUser = {
const currentUser: CurrentServerUser = {
...nonCurrentServerUser,
tokenStore,
async delete() {
@ -853,10 +876,31 @@ class _StackServerAppImpl<HasTokenStore extends boolean, ProjectId extends strin
},
updatePassword(options: { oldPassword: string, newPassword: string}) {
return app._updatePassword(options, tokenStore);
}
},
};
Object.freeze(res);
return res;
if (this.isInternalProject()) {
const internalUser: CurrentInternalServerUser = {
...currentUser,
createProject(newProject: Pick<Project, "displayName" | "description">) {
return app._createProject(newProject);
},
listOwnedProjects() {
return app._listOwnedProjects();
},
useOwnedProjects() {
return app._useOwnedProjects();
},
onOwnedProjectsChange(callback: (projects: Project[]) => void) {
return app._onOwnedProjectsChange(callback);
}
};
Object.freeze(internalUser);
return internalUser;
} else {
Object.freeze(currentUser);
return currentUser as any;
}
}
protected _serverUserToJson(user: ServerUser): ServerUserJson {
@ -874,14 +918,14 @@ class _StackServerAppImpl<HasTokenStore extends boolean, ProjectId extends strin
};
}
async getServerUser(): Promise<CurrentServerUser | null> {
async getServerUser(): Promise<ProjectCurrentSeverUser<ProjectId> | null> {
this._ensurePersistentTokenStore();
const tokenStore = getTokenStore(this._tokenStoreOptions);
const userJson = await this._currentServerUserCache.getOrWait([tokenStore], "write-only");
return this._currentServerUserFromJson(userJson, tokenStore);
}
useServerUser(options?: { required: boolean }): CurrentServerUser | null {
useServerUser(options?: { required: boolean }): ProjectCurrentSeverUser<ProjectId> | null {
this._ensurePersistentTokenStore();
const tokenStore = getTokenStore(this._tokenStoreOptions);
@ -1080,6 +1124,13 @@ type Auth<T, C> = {
updatePassword(this: T, options: { oldPassword: string, newPassword: string}): Promise<KnownErrors["PasswordMismatch"] | KnownErrors["PasswordRequirementsNotMet"] | undefined>,
};
type InternalAuth<T> = {
createProject(this: T, newProject: Pick<Project, "displayName" | "description">): Promise<Project>,
listOwnedProjects(this: T): Promise<Project[]>,
useOwnedProjects(this: T): Project[],
onOwnedProjectsChange(this: T, callback: (projects: Project[]) => void): void,
}
export type User = {
readonly projectId: string,
readonly id: string,
@ -1106,6 +1157,7 @@ export type User = {
export type CurrentUser = Auth<User, UserCustomizableJson> & User;
export type CurrentInternalUser = CurrentUser & InternalAuth<CurrentUser>;
/**
* A user including sensitive fields that should only be used on the server, never sent to the client
@ -1129,6 +1181,8 @@ export type CurrentServerUser = Auth<ServerUser, ServerUserCustomizableJson> & O
getClientUser(this: CurrentServerUser): CurrentUser,
};
export type CurrentInternalServerUser = CurrentServerUser & InternalAuth<CurrentServerUser>;
export type Project = {
readonly id: string,
readonly displayName: string,
@ -1222,22 +1276,14 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
? {}
: {
redirectToOAuthCallback(): Promise<never>,
useUser(options: GetUserOptions & { or: 'redirect' }): CurrentUser,
useUser(options: GetUserOptions & { or: 'throw' }): CurrentUser,
useUser(options?: GetUserOptions): CurrentUser | null,
getUser(options: GetUserOptions & { or: 'redirect' }): Promise<CurrentUser>,
getUser(options: GetUserOptions & { or: 'throw' }): Promise<CurrentUser>,
getUser(options?: GetUserOptions): Promise<CurrentUser | null>,
useUser(options: GetUserOptions & { or: 'redirect' }): ProjectCurrentUser<ProjectId>,
useUser(options: GetUserOptions & { or: 'throw' }): ProjectCurrentUser<ProjectId>,
useUser(options?: GetUserOptions): ProjectCurrentUser<ProjectId> | null,
getUser(options: GetUserOptions & { or: 'redirect' }): Promise<ProjectCurrentUser<ProjectId>>,
getUser(options: GetUserOptions & { or: 'throw' }): Promise<ProjectCurrentUser<ProjectId>>,
getUser(options?: GetUserOptions): Promise<ProjectCurrentUser<ProjectId> | null>,
onUserChange: AsyncStoreProperty<"user", CurrentUser | null, false>["onUserChange"],
})
& (
ProjectId extends "internal" ? (
& AsyncStoreProperty<"ownedProjects", Project[], true>
& {
createProject(project: Pick<Project, "displayName" | "description">): Promise<Project>,
}
) : {}
)
);
type StackClientAppConstructor = {
new <