From 1ca01ca26165e6b4b1a1e9afda0928be250ec212 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Tue, 16 Apr 2024 15:23:53 +0200 Subject: [PATCH] Moved ownedProjects related functionalities to the user (#11) * moved owned projects to user * added internal user type * removed old comments * fixed type bug --- .../projects/[projectId]/header.tsx | 6 +- .../(protected)/projects/page-client.tsx | 11 +- packages/stack/src/lib/hooks.ts | 25 +++- packages/stack/src/lib/stack-app.ts | 130 ++++++++++++------ 4 files changed, 115 insertions(+), 57 deletions(-) diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/header.tsx b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/header.tsx index bf7acb7cc..bbc390049 100644 --- a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/header.tsx +++ b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/header.tsx @@ -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 | null) => { diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/page-client.tsx b/packages/stack-server/src/app/(main)/(protected)/projects/page-client.tsx index 35c51ea2f..1a7695174 100644 --- a/packages/stack-server/src/app/(main)/(protected)/projects/page-client.tsx +++ b/packages/stack-server/src/app/(main)/(protected)/projects/page-client.tsx @@ -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(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')}`, }); diff --git a/packages/stack/src/lib/hooks.ts b/packages/stack/src/lib/hooks.ts index 80e9d1599..3d2821aef 100644 --- a/packages/stack/src/lib/hooks.ts +++ b/packages/stack/src/lib/hooks.ts @@ -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; + } } /** diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts index dfff57282..629c8b0a7 100644 --- a/packages/stack/src/lib/stack-app.ts +++ b/packages/stack/src/lib/stack-app.ts @@ -40,6 +40,9 @@ export type HandlerUrls = { accountSettings: string, } +type ProjectCurrentUser = ProjectId extends "internal" ? CurrentInternalUser : CurrentUser; +type ProjectCurrentSeverUser = ProjectId extends "internal" ? CurrentInternalServerUser : CurrentServerUser; + function getUrls(partial: Partial): HandlerUrls { const handler = partial.handler ?? "/handler"; return { @@ -351,12 +354,12 @@ class _StackClientAppImpl; + protected _currentUserFromJson(json: UserJson | null, tokenStore: TokenStore): ProjectCurrentUser | null; + protected _currentUserFromJson(json: UserJson | null, tokenStore: TokenStore): ProjectCurrentUser | 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) { + 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; - async getUser(options: GetUserOptions & { or: 'throw' }): Promise; - async getUser(options?: GetUserOptions): Promise; - async getUser(options?: GetUserOptions): Promise { + async getUser(options: GetUserOptions & { or: 'redirect' }): Promise>; + async getUser(options: GetUserOptions & { or: 'throw' }): Promise>; + async getUser(options?: GetUserOptions): Promise | null>; + async getUser(options?: GetUserOptions): Promise | null> { this._ensurePersistentTokenStore(); const tokenStore = getTokenStore(this._tokenStoreOptions); const userJson = await this._currentUserCache.getOrWait([tokenStore], "write-only"); @@ -508,10 +531,10 @@ class _StackClientAppImpl; + useUser(options: GetUserOptions & { or: 'throw' }): ProjectCurrentUser; + useUser(options?: GetUserOptions): ProjectCurrentUser | null; + useUser(options?: GetUserOptions): ProjectCurrentUser | null { this._ensurePersistentTokenStore(); const router = useRouter(); @@ -641,7 +664,7 @@ class _StackClientAppImpl { + protected async _listOwnedProjects(): Promise { this._ensureInternalProject(); const tokenStore = getTokenStore(this._tokenStoreOptions); const json = await this._ownedProjectsCache.getOrWait([tokenStore], "write-only"); @@ -652,7 +675,7 @@ class _StackClientAppImpl 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): Promise { + protected async _createProject(newProject: Pick): Promise { this._ensureInternalProject(); const tokenStore = getTokenStore(this._tokenStoreOptions); const json = await this._interface.createProject(newProject, tokenStore); @@ -823,13 +846,13 @@ class _StackServerAppImpl; + protected _currentServerUserFromJson(json: ServerUserJson | null, tokenStore: TokenStore): ProjectCurrentSeverUser | null; + protected _currentServerUserFromJson(json: ServerUserJson | null, tokenStore: TokenStore): ProjectCurrentSeverUser | 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) { + 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 { + async getServerUser(): Promise | 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 | null { this._ensurePersistentTokenStore(); const tokenStore = getTokenStore(this._tokenStoreOptions); @@ -1080,6 +1124,13 @@ type Auth = { updatePassword(this: T, options: { oldPassword: string, newPassword: string}): Promise, }; +type InternalAuth = { + createProject(this: T, newProject: Pick): Promise, + listOwnedProjects(this: T): Promise, + 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; +export type CurrentInternalUser = CurrentUser & InternalAuth; /** * 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 & O getClientUser(this: CurrentServerUser): CurrentUser, }; +export type CurrentInternalServerUser = CurrentServerUser & InternalAuth; + export type Project = { readonly id: string, readonly displayName: string, @@ -1222,22 +1276,14 @@ export type StackClientApp, - useUser(options: GetUserOptions & { or: 'redirect' }): CurrentUser, - useUser(options: GetUserOptions & { or: 'throw' }): CurrentUser, - useUser(options?: GetUserOptions): CurrentUser | null, - getUser(options: GetUserOptions & { or: 'redirect' }): Promise, - getUser(options: GetUserOptions & { or: 'throw' }): Promise, - getUser(options?: GetUserOptions): Promise, + useUser(options: GetUserOptions & { or: 'redirect' }): ProjectCurrentUser, + useUser(options: GetUserOptions & { or: 'throw' }): ProjectCurrentUser, + useUser(options?: GetUserOptions): ProjectCurrentUser | null, + getUser(options: GetUserOptions & { or: 'redirect' }): Promise>, + getUser(options: GetUserOptions & { or: 'throw' }): Promise>, + getUser(options?: GetUserOptions): Promise | null>, onUserChange: AsyncStoreProperty<"user", CurrentUser | null, false>["onUserChange"], }) - & ( - ProjectId extends "internal" ? ( - & AsyncStoreProperty<"ownedProjects", Project[], true> - & { - createProject(project: Pick): Promise, - } - ) : {} - ) ); type StackClientAppConstructor = { new <