diff --git a/.changeset/khaki-dogs-reflect.md b/.changeset/khaki-dogs-reflect.md new file mode 100644 index 0000000..bbb778b --- /dev/null +++ b/.changeset/khaki-dogs-reflect.md @@ -0,0 +1,5 @@ +--- +"namesake": patch +--- + +Improve auth redirect logic diff --git a/package.json b/package.json index 7cbcb70..228a0c8 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@auth/core": "^0.34.2", - "@convex-dev/auth": "^0.0.67", + "@convex-dev/auth": "^0.0.69", "@faker-js/faker": "^9.0.1", "@mdxeditor/editor": "^3.11.4", "@pdfme/common": "^4.5.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6de4052..1f259fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,8 @@ importers: specifier: ^0.34.2 version: 0.34.2 '@convex-dev/auth': - specifier: ^0.0.67 - version: 0.0.67(convex@1.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + specifier: ^0.0.69 + version: 0.0.69(convex@1.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@faker-js/faker': specifier: ^9.0.1 version: 9.0.1 @@ -639,8 +639,8 @@ packages: react: ^16.8.0 || ^17 || ^18 react-dom: ^16.8.0 || ^17 || ^18 - '@convex-dev/auth@0.0.67': - resolution: {integrity: sha512-XUNTe3X7qxzAkpgP/qekuQ7Fc3qE1i2BrbBjyKO5jyFJ0qrvoNr0PRSKofnLYnjTrjG12wkm9KJx56og4pyJUw==} + '@convex-dev/auth@0.0.69': + resolution: {integrity: sha512-qM7Q1ntNTuOCtUILBzZhSANY/zDpntjtg31ME1bV43+RnM/JfiWpduvs/qTQXWUiil60DawuZ0tdpGse8uNxvg==} hasBin: true peerDependencies: convex: ^1.14.4 @@ -7648,7 +7648,7 @@ snapshots: transitivePeerDependencies: - '@lezer/common' - '@convex-dev/auth@0.0.67(convex@1.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@convex-dev/auth@0.0.69(convex@1.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: '@auth/core': 0.31.0 arctic: 1.9.2 diff --git a/src/components/AppHeader/AppHeader.tsx b/src/components/AppHeader/AppHeader.tsx index 6a4425d..e59d3be 100644 --- a/src/components/AppHeader/AppHeader.tsx +++ b/src/components/AppHeader/AppHeader.tsx @@ -1,7 +1,8 @@ import { useAuthActions } from "@convex-dev/auth/react"; +import { api } from "@convex/_generated/api"; import { RiAccountCircleFill } from "@remixicon/react"; -import { useQuery } from "convex/react"; -import { api } from "../../../convex/_generated/api"; +import { redirect } from "@tanstack/react-router"; +import { Authenticated, useQuery } from "convex/react"; import { Button } from "../Button"; import { Link } from "../Link"; import { Logo } from "../Logo"; @@ -12,32 +13,39 @@ export const AppHeader = () => { const role = useQuery(api.users.getCurrentUserRole); const isAdmin = role === "admin"; + const handleSignOut = () => { + signOut(); + redirect({ to: "/", throw: true }); + }; + return ( -
+
- {isAdmin && Admin} -
- - - - Settings - - - Support… - - - Sign Out - - -
+ + {isAdmin && Admin} +
+ + + + Settings + + + Support… + + + Sign out + + +
+
); }; diff --git a/src/main.tsx b/src/main.tsx index e8b6a30..7bb4706 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,13 +1,18 @@ import "./styles/index.css"; import { ConvexAuthProvider } from "@convex-dev/auth/react"; +import { api } from "@convex/_generated/api"; import { RiErrorWarningLine } from "@remixicon/react"; import { RouterProvider, createRouter } from "@tanstack/react-router"; -import { ConvexReactClient, useConvexAuth, useQuery } from "convex/react"; +import { + type ConvexAuthState, + ConvexReactClient, + useConvexAuth, + useQuery, +} from "convex/react"; import { ThemeProvider } from "next-themes"; -import { StrictMode } from "react"; +import { StrictMode, useEffect } from "react"; import { createRoot } from "react-dom/client"; import { HelmetProvider } from "react-helmet-async"; -import { api } from "../convex/_generated/api"; import { Empty } from "./components"; import { routeTree } from "./routeTree.gen"; @@ -20,6 +25,7 @@ const router = createRouter({ auth: undefined!, role: undefined!, }, + defaultPreload: "intent", defaultNotFoundComponent: () => ( void; +const authClient: Promise = new Promise((resolve) => { + resolveAuth = resolve; +}); + const InnerApp = () => { const title = "Namesake"; const auth = useConvexAuth(); const role = useQuery(api.users.getCurrentUserRole) ?? undefined; - return ; + useEffect(() => { + if (auth.isLoading) return; + + resolveAuth(auth); + }, [auth, auth.isLoading]); + + return ( + + ); }; const rootElement = document.getElementById("root")!; diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index f19d6ff..ba13332 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -11,225 +11,315 @@ // Import Routes import { Route as rootRoute } from './routes/__root' -import { Route as QuestsRouteImport } from './routes/quests/route' -import { Route as AdminRouteImport } from './routes/admin/route' -import { Route as IndexImport } from './routes/index' -import { Route as SettingsIndexImport } from './routes/settings/index' -import { Route as AdminIndexImport } from './routes/admin/index' -import { Route as QuestsQuestIdImport } from './routes/quests/$questId' -import { Route as AdminQuestsIndexImport } from './routes/admin/quests/index' -import { Route as AdminFormsIndexImport } from './routes/admin/forms/index' -import { Route as AdminQuestsQuestIdImport } from './routes/admin/quests/$questId' -import { Route as AdminFormsFormIdImport } from './routes/admin/forms/$formId' +import { Route as UnauthenticatedImport } from './routes/_unauthenticated' +import { Route as AuthenticatedImport } from './routes/_authenticated' +import { Route as AuthenticatedIndexImport } from './routes/_authenticated/index' +import { Route as UnauthenticatedLoginImport } from './routes/_unauthenticated/login' +import { Route as AuthenticatedQuestsRouteImport } from './routes/_authenticated/quests/route' +import { Route as AuthenticatedAdminRouteImport } from './routes/_authenticated/admin/route' +import { Route as AuthenticatedSettingsIndexImport } from './routes/_authenticated/settings/index' +import { Route as AuthenticatedAdminIndexImport } from './routes/_authenticated/admin/index' +import { Route as AuthenticatedQuestsQuestIdImport } from './routes/_authenticated/quests/$questId' +import { Route as AuthenticatedAdminQuestsIndexImport } from './routes/_authenticated/admin/quests/index' +import { Route as AuthenticatedAdminFormsIndexImport } from './routes/_authenticated/admin/forms/index' +import { Route as AuthenticatedAdminQuestsQuestIdImport } from './routes/_authenticated/admin/quests/$questId' +import { Route as AuthenticatedAdminFormsFormIdImport } from './routes/_authenticated/admin/forms/$formId' // Create/Update Routes -const QuestsRouteRoute = QuestsRouteImport.update({ - path: '/quests', +const UnauthenticatedRoute = UnauthenticatedImport.update({ + id: '/_unauthenticated', getParentRoute: () => rootRoute, } as any) -const AdminRouteRoute = AdminRouteImport.update({ - path: '/admin', +const AuthenticatedRoute = AuthenticatedImport.update({ + id: '/_authenticated', getParentRoute: () => rootRoute, } as any) -const IndexRoute = IndexImport.update({ +const AuthenticatedIndexRoute = AuthenticatedIndexImport.update({ path: '/', - getParentRoute: () => rootRoute, + getParentRoute: () => AuthenticatedRoute, } as any) -const SettingsIndexRoute = SettingsIndexImport.update({ - path: '/settings/', - getParentRoute: () => rootRoute, +const UnauthenticatedLoginRoute = UnauthenticatedLoginImport.update({ + path: '/login', + getParentRoute: () => UnauthenticatedRoute, } as any) -const AdminIndexRoute = AdminIndexImport.update({ - path: '/', - getParentRoute: () => AdminRouteRoute, +const AuthenticatedQuestsRouteRoute = AuthenticatedQuestsRouteImport.update({ + path: '/quests', + getParentRoute: () => AuthenticatedRoute, } as any) -const QuestsQuestIdRoute = QuestsQuestIdImport.update({ - path: '/$questId', - getParentRoute: () => QuestsRouteRoute, +const AuthenticatedAdminRouteRoute = AuthenticatedAdminRouteImport.update({ + path: '/admin', + getParentRoute: () => AuthenticatedRoute, } as any) -const AdminQuestsIndexRoute = AdminQuestsIndexImport.update({ - path: '/quests/', - getParentRoute: () => AdminRouteRoute, -} as any) +const AuthenticatedSettingsIndexRoute = AuthenticatedSettingsIndexImport.update( + { + path: '/settings/', + getParentRoute: () => AuthenticatedRoute, + } as any, +) -const AdminFormsIndexRoute = AdminFormsIndexImport.update({ - path: '/forms/', - getParentRoute: () => AdminRouteRoute, +const AuthenticatedAdminIndexRoute = AuthenticatedAdminIndexImport.update({ + path: '/', + getParentRoute: () => AuthenticatedAdminRouteRoute, } as any) -const AdminQuestsQuestIdRoute = AdminQuestsQuestIdImport.update({ - path: '/quests/$questId', - getParentRoute: () => AdminRouteRoute, -} as any) +const AuthenticatedQuestsQuestIdRoute = AuthenticatedQuestsQuestIdImport.update( + { + path: '/$questId', + getParentRoute: () => AuthenticatedQuestsRouteRoute, + } as any, +) -const AdminFormsFormIdRoute = AdminFormsFormIdImport.update({ - path: '/forms/$formId', - getParentRoute: () => AdminRouteRoute, -} as any) +const AuthenticatedAdminQuestsIndexRoute = + AuthenticatedAdminQuestsIndexImport.update({ + path: '/quests/', + getParentRoute: () => AuthenticatedAdminRouteRoute, + } as any) + +const AuthenticatedAdminFormsIndexRoute = + AuthenticatedAdminFormsIndexImport.update({ + path: '/forms/', + getParentRoute: () => AuthenticatedAdminRouteRoute, + } as any) + +const AuthenticatedAdminQuestsQuestIdRoute = + AuthenticatedAdminQuestsQuestIdImport.update({ + path: '/quests/$questId', + getParentRoute: () => AuthenticatedAdminRouteRoute, + } as any) + +const AuthenticatedAdminFormsFormIdRoute = + AuthenticatedAdminFormsFormIdImport.update({ + path: '/forms/$formId', + getParentRoute: () => AuthenticatedAdminRouteRoute, + } as any) // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexImport + '/_authenticated': { + id: '/_authenticated' + path: '' + fullPath: '' + preLoaderRoute: typeof AuthenticatedImport + parentRoute: typeof rootRoute + } + '/_unauthenticated': { + id: '/_unauthenticated' + path: '' + fullPath: '' + preLoaderRoute: typeof UnauthenticatedImport parentRoute: typeof rootRoute } - '/admin': { - id: '/admin' + '/_authenticated/admin': { + id: '/_authenticated/admin' path: '/admin' fullPath: '/admin' - preLoaderRoute: typeof AdminRouteImport - parentRoute: typeof rootRoute + preLoaderRoute: typeof AuthenticatedAdminRouteImport + parentRoute: typeof AuthenticatedImport } - '/quests': { - id: '/quests' + '/_authenticated/quests': { + id: '/_authenticated/quests' path: '/quests' fullPath: '/quests' - preLoaderRoute: typeof QuestsRouteImport - parentRoute: typeof rootRoute + preLoaderRoute: typeof AuthenticatedQuestsRouteImport + parentRoute: typeof AuthenticatedImport + } + '/_unauthenticated/login': { + id: '/_unauthenticated/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof UnauthenticatedLoginImport + parentRoute: typeof UnauthenticatedImport } - '/quests/$questId': { - id: '/quests/$questId' + '/_authenticated/': { + id: '/_authenticated/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof AuthenticatedIndexImport + parentRoute: typeof AuthenticatedImport + } + '/_authenticated/quests/$questId': { + id: '/_authenticated/quests/$questId' path: '/$questId' fullPath: '/quests/$questId' - preLoaderRoute: typeof QuestsQuestIdImport - parentRoute: typeof QuestsRouteImport + preLoaderRoute: typeof AuthenticatedQuestsQuestIdImport + parentRoute: typeof AuthenticatedQuestsRouteImport } - '/admin/': { - id: '/admin/' + '/_authenticated/admin/': { + id: '/_authenticated/admin/' path: '/' fullPath: '/admin/' - preLoaderRoute: typeof AdminIndexImport - parentRoute: typeof AdminRouteImport + preLoaderRoute: typeof AuthenticatedAdminIndexImport + parentRoute: typeof AuthenticatedAdminRouteImport } - '/settings/': { - id: '/settings/' + '/_authenticated/settings/': { + id: '/_authenticated/settings/' path: '/settings' fullPath: '/settings' - preLoaderRoute: typeof SettingsIndexImport - parentRoute: typeof rootRoute + preLoaderRoute: typeof AuthenticatedSettingsIndexImport + parentRoute: typeof AuthenticatedImport } - '/admin/forms/$formId': { - id: '/admin/forms/$formId' + '/_authenticated/admin/forms/$formId': { + id: '/_authenticated/admin/forms/$formId' path: '/forms/$formId' fullPath: '/admin/forms/$formId' - preLoaderRoute: typeof AdminFormsFormIdImport - parentRoute: typeof AdminRouteImport + preLoaderRoute: typeof AuthenticatedAdminFormsFormIdImport + parentRoute: typeof AuthenticatedAdminRouteImport } - '/admin/quests/$questId': { - id: '/admin/quests/$questId' + '/_authenticated/admin/quests/$questId': { + id: '/_authenticated/admin/quests/$questId' path: '/quests/$questId' fullPath: '/admin/quests/$questId' - preLoaderRoute: typeof AdminQuestsQuestIdImport - parentRoute: typeof AdminRouteImport + preLoaderRoute: typeof AuthenticatedAdminQuestsQuestIdImport + parentRoute: typeof AuthenticatedAdminRouteImport } - '/admin/forms/': { - id: '/admin/forms/' + '/_authenticated/admin/forms/': { + id: '/_authenticated/admin/forms/' path: '/forms' fullPath: '/admin/forms' - preLoaderRoute: typeof AdminFormsIndexImport - parentRoute: typeof AdminRouteImport + preLoaderRoute: typeof AuthenticatedAdminFormsIndexImport + parentRoute: typeof AuthenticatedAdminRouteImport } - '/admin/quests/': { - id: '/admin/quests/' + '/_authenticated/admin/quests/': { + id: '/_authenticated/admin/quests/' path: '/quests' fullPath: '/admin/quests' - preLoaderRoute: typeof AdminQuestsIndexImport - parentRoute: typeof AdminRouteImport + preLoaderRoute: typeof AuthenticatedAdminQuestsIndexImport + parentRoute: typeof AuthenticatedAdminRouteImport } } } // Create and export the route tree -interface AdminRouteRouteChildren { - AdminIndexRoute: typeof AdminIndexRoute - AdminFormsFormIdRoute: typeof AdminFormsFormIdRoute - AdminQuestsQuestIdRoute: typeof AdminQuestsQuestIdRoute - AdminFormsIndexRoute: typeof AdminFormsIndexRoute - AdminQuestsIndexRoute: typeof AdminQuestsIndexRoute +interface AuthenticatedAdminRouteRouteChildren { + AuthenticatedAdminIndexRoute: typeof AuthenticatedAdminIndexRoute + AuthenticatedAdminFormsFormIdRoute: typeof AuthenticatedAdminFormsFormIdRoute + AuthenticatedAdminQuestsQuestIdRoute: typeof AuthenticatedAdminQuestsQuestIdRoute + AuthenticatedAdminFormsIndexRoute: typeof AuthenticatedAdminFormsIndexRoute + AuthenticatedAdminQuestsIndexRoute: typeof AuthenticatedAdminQuestsIndexRoute +} + +const AuthenticatedAdminRouteRouteChildren: AuthenticatedAdminRouteRouteChildren = + { + AuthenticatedAdminIndexRoute: AuthenticatedAdminIndexRoute, + AuthenticatedAdminFormsFormIdRoute: AuthenticatedAdminFormsFormIdRoute, + AuthenticatedAdminQuestsQuestIdRoute: AuthenticatedAdminQuestsQuestIdRoute, + AuthenticatedAdminFormsIndexRoute: AuthenticatedAdminFormsIndexRoute, + AuthenticatedAdminQuestsIndexRoute: AuthenticatedAdminQuestsIndexRoute, + } + +const AuthenticatedAdminRouteRouteWithChildren = + AuthenticatedAdminRouteRoute._addFileChildren( + AuthenticatedAdminRouteRouteChildren, + ) + +interface AuthenticatedQuestsRouteRouteChildren { + AuthenticatedQuestsQuestIdRoute: typeof AuthenticatedQuestsQuestIdRoute } -const AdminRouteRouteChildren: AdminRouteRouteChildren = { - AdminIndexRoute: AdminIndexRoute, - AdminFormsFormIdRoute: AdminFormsFormIdRoute, - AdminQuestsQuestIdRoute: AdminQuestsQuestIdRoute, - AdminFormsIndexRoute: AdminFormsIndexRoute, - AdminQuestsIndexRoute: AdminQuestsIndexRoute, +const AuthenticatedQuestsRouteRouteChildren: AuthenticatedQuestsRouteRouteChildren = + { + AuthenticatedQuestsQuestIdRoute: AuthenticatedQuestsQuestIdRoute, + } + +const AuthenticatedQuestsRouteRouteWithChildren = + AuthenticatedQuestsRouteRoute._addFileChildren( + AuthenticatedQuestsRouteRouteChildren, + ) + +interface AuthenticatedRouteChildren { + AuthenticatedAdminRouteRoute: typeof AuthenticatedAdminRouteRouteWithChildren + AuthenticatedQuestsRouteRoute: typeof AuthenticatedQuestsRouteRouteWithChildren + AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute + AuthenticatedSettingsIndexRoute: typeof AuthenticatedSettingsIndexRoute } -const AdminRouteRouteWithChildren = AdminRouteRoute._addFileChildren( - AdminRouteRouteChildren, +const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { + AuthenticatedAdminRouteRoute: AuthenticatedAdminRouteRouteWithChildren, + AuthenticatedQuestsRouteRoute: AuthenticatedQuestsRouteRouteWithChildren, + AuthenticatedIndexRoute: AuthenticatedIndexRoute, + AuthenticatedSettingsIndexRoute: AuthenticatedSettingsIndexRoute, +} + +const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren( + AuthenticatedRouteChildren, ) -interface QuestsRouteRouteChildren { - QuestsQuestIdRoute: typeof QuestsQuestIdRoute +interface UnauthenticatedRouteChildren { + UnauthenticatedLoginRoute: typeof UnauthenticatedLoginRoute } -const QuestsRouteRouteChildren: QuestsRouteRouteChildren = { - QuestsQuestIdRoute: QuestsQuestIdRoute, +const UnauthenticatedRouteChildren: UnauthenticatedRouteChildren = { + UnauthenticatedLoginRoute: UnauthenticatedLoginRoute, } -const QuestsRouteRouteWithChildren = QuestsRouteRoute._addFileChildren( - QuestsRouteRouteChildren, +const UnauthenticatedRouteWithChildren = UnauthenticatedRoute._addFileChildren( + UnauthenticatedRouteChildren, ) export interface FileRoutesByFullPath { - '/': typeof IndexRoute - '/admin': typeof AdminRouteRouteWithChildren - '/quests': typeof QuestsRouteRouteWithChildren - '/quests/$questId': typeof QuestsQuestIdRoute - '/admin/': typeof AdminIndexRoute - '/settings': typeof SettingsIndexRoute - '/admin/forms/$formId': typeof AdminFormsFormIdRoute - '/admin/quests/$questId': typeof AdminQuestsQuestIdRoute - '/admin/forms': typeof AdminFormsIndexRoute - '/admin/quests': typeof AdminQuestsIndexRoute + '': typeof UnauthenticatedRouteWithChildren + '/admin': typeof AuthenticatedAdminRouteRouteWithChildren + '/quests': typeof AuthenticatedQuestsRouteRouteWithChildren + '/login': typeof UnauthenticatedLoginRoute + '/': typeof AuthenticatedIndexRoute + '/quests/$questId': typeof AuthenticatedQuestsQuestIdRoute + '/admin/': typeof AuthenticatedAdminIndexRoute + '/settings': typeof AuthenticatedSettingsIndexRoute + '/admin/forms/$formId': typeof AuthenticatedAdminFormsFormIdRoute + '/admin/quests/$questId': typeof AuthenticatedAdminQuestsQuestIdRoute + '/admin/forms': typeof AuthenticatedAdminFormsIndexRoute + '/admin/quests': typeof AuthenticatedAdminQuestsIndexRoute } export interface FileRoutesByTo { - '/': typeof IndexRoute - '/quests': typeof QuestsRouteRouteWithChildren - '/quests/$questId': typeof QuestsQuestIdRoute - '/admin': typeof AdminIndexRoute - '/settings': typeof SettingsIndexRoute - '/admin/forms/$formId': typeof AdminFormsFormIdRoute - '/admin/quests/$questId': typeof AdminQuestsQuestIdRoute - '/admin/forms': typeof AdminFormsIndexRoute - '/admin/quests': typeof AdminQuestsIndexRoute + '': typeof UnauthenticatedRouteWithChildren + '/quests': typeof AuthenticatedQuestsRouteRouteWithChildren + '/login': typeof UnauthenticatedLoginRoute + '/': typeof AuthenticatedIndexRoute + '/quests/$questId': typeof AuthenticatedQuestsQuestIdRoute + '/admin': typeof AuthenticatedAdminIndexRoute + '/settings': typeof AuthenticatedSettingsIndexRoute + '/admin/forms/$formId': typeof AuthenticatedAdminFormsFormIdRoute + '/admin/quests/$questId': typeof AuthenticatedAdminQuestsQuestIdRoute + '/admin/forms': typeof AuthenticatedAdminFormsIndexRoute + '/admin/quests': typeof AuthenticatedAdminQuestsIndexRoute } export interface FileRoutesById { __root__: typeof rootRoute - '/': typeof IndexRoute - '/admin': typeof AdminRouteRouteWithChildren - '/quests': typeof QuestsRouteRouteWithChildren - '/quests/$questId': typeof QuestsQuestIdRoute - '/admin/': typeof AdminIndexRoute - '/settings/': typeof SettingsIndexRoute - '/admin/forms/$formId': typeof AdminFormsFormIdRoute - '/admin/quests/$questId': typeof AdminQuestsQuestIdRoute - '/admin/forms/': typeof AdminFormsIndexRoute - '/admin/quests/': typeof AdminQuestsIndexRoute + '/_authenticated': typeof AuthenticatedRouteWithChildren + '/_unauthenticated': typeof UnauthenticatedRouteWithChildren + '/_authenticated/admin': typeof AuthenticatedAdminRouteRouteWithChildren + '/_authenticated/quests': typeof AuthenticatedQuestsRouteRouteWithChildren + '/_unauthenticated/login': typeof UnauthenticatedLoginRoute + '/_authenticated/': typeof AuthenticatedIndexRoute + '/_authenticated/quests/$questId': typeof AuthenticatedQuestsQuestIdRoute + '/_authenticated/admin/': typeof AuthenticatedAdminIndexRoute + '/_authenticated/settings/': typeof AuthenticatedSettingsIndexRoute + '/_authenticated/admin/forms/$formId': typeof AuthenticatedAdminFormsFormIdRoute + '/_authenticated/admin/quests/$questId': typeof AuthenticatedAdminQuestsQuestIdRoute + '/_authenticated/admin/forms/': typeof AuthenticatedAdminFormsIndexRoute + '/_authenticated/admin/quests/': typeof AuthenticatedAdminQuestsIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: - | '/' + | '' | '/admin' | '/quests' + | '/login' + | '/' | '/quests/$questId' | '/admin/' | '/settings' @@ -239,8 +329,10 @@ export interface FileRouteTypes { | '/admin/quests' fileRoutesByTo: FileRoutesByTo to: - | '/' + | '' | '/quests' + | '/login' + | '/' | '/quests/$questId' | '/admin' | '/settings' @@ -250,31 +342,30 @@ export interface FileRouteTypes { | '/admin/quests' id: | '__root__' - | '/' - | '/admin' - | '/quests' - | '/quests/$questId' - | '/admin/' - | '/settings/' - | '/admin/forms/$formId' - | '/admin/quests/$questId' - | '/admin/forms/' - | '/admin/quests/' + | '/_authenticated' + | '/_unauthenticated' + | '/_authenticated/admin' + | '/_authenticated/quests' + | '/_unauthenticated/login' + | '/_authenticated/' + | '/_authenticated/quests/$questId' + | '/_authenticated/admin/' + | '/_authenticated/settings/' + | '/_authenticated/admin/forms/$formId' + | '/_authenticated/admin/quests/$questId' + | '/_authenticated/admin/forms/' + | '/_authenticated/admin/quests/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute - AdminRouteRoute: typeof AdminRouteRouteWithChildren - QuestsRouteRoute: typeof QuestsRouteRouteWithChildren - SettingsIndexRoute: typeof SettingsIndexRoute + AuthenticatedRoute: typeof AuthenticatedRouteWithChildren + UnauthenticatedRoute: typeof UnauthenticatedRouteWithChildren } const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, - AdminRouteRoute: AdminRouteRouteWithChildren, - QuestsRouteRoute: QuestsRouteRouteWithChildren, - SettingsIndexRoute: SettingsIndexRoute, + AuthenticatedRoute: AuthenticatedRouteWithChildren, + UnauthenticatedRoute: UnauthenticatedRouteWithChildren, } export const routeTree = rootRoute @@ -289,57 +380,78 @@ export const routeTree = rootRoute "__root__": { "filePath": "__root.tsx", "children": [ - "/", - "/admin", - "/quests", - "/settings/" + "/_authenticated", + "/_unauthenticated" ] }, - "/": { - "filePath": "index.tsx" + "/_authenticated": { + "filePath": "_authenticated.tsx", + "children": [ + "/_authenticated/admin", + "/_authenticated/quests", + "/_authenticated/", + "/_authenticated/settings/" + ] + }, + "/_unauthenticated": { + "filePath": "_unauthenticated.tsx", + "children": [ + "/_unauthenticated/login" + ] }, - "/admin": { - "filePath": "admin/route.tsx", + "/_authenticated/admin": { + "filePath": "_authenticated/admin/route.tsx", + "parent": "/_authenticated", "children": [ - "/admin/", - "/admin/forms/$formId", - "/admin/quests/$questId", - "/admin/forms/", - "/admin/quests/" + "/_authenticated/admin/", + "/_authenticated/admin/forms/$formId", + "/_authenticated/admin/quests/$questId", + "/_authenticated/admin/forms/", + "/_authenticated/admin/quests/" ] }, - "/quests": { - "filePath": "quests/route.tsx", + "/_authenticated/quests": { + "filePath": "_authenticated/quests/route.tsx", + "parent": "/_authenticated", "children": [ - "/quests/$questId" + "/_authenticated/quests/$questId" ] }, - "/quests/$questId": { - "filePath": "quests/$questId.tsx", - "parent": "/quests" + "/_unauthenticated/login": { + "filePath": "_unauthenticated/login.tsx", + "parent": "/_unauthenticated" + }, + "/_authenticated/": { + "filePath": "_authenticated/index.tsx", + "parent": "/_authenticated" + }, + "/_authenticated/quests/$questId": { + "filePath": "_authenticated/quests/$questId.tsx", + "parent": "/_authenticated/quests" }, - "/admin/": { - "filePath": "admin/index.tsx", - "parent": "/admin" + "/_authenticated/admin/": { + "filePath": "_authenticated/admin/index.tsx", + "parent": "/_authenticated/admin" }, - "/settings/": { - "filePath": "settings/index.tsx" + "/_authenticated/settings/": { + "filePath": "_authenticated/settings/index.tsx", + "parent": "/_authenticated" }, - "/admin/forms/$formId": { - "filePath": "admin/forms/$formId.tsx", - "parent": "/admin" + "/_authenticated/admin/forms/$formId": { + "filePath": "_authenticated/admin/forms/$formId.tsx", + "parent": "/_authenticated/admin" }, - "/admin/quests/$questId": { - "filePath": "admin/quests/$questId.tsx", - "parent": "/admin" + "/_authenticated/admin/quests/$questId": { + "filePath": "_authenticated/admin/quests/$questId.tsx", + "parent": "/_authenticated/admin" }, - "/admin/forms/": { - "filePath": "admin/forms/index.tsx", - "parent": "/admin" + "/_authenticated/admin/forms/": { + "filePath": "_authenticated/admin/forms/index.tsx", + "parent": "/_authenticated/admin" }, - "/admin/quests/": { - "filePath": "admin/quests/index.tsx", - "parent": "/admin" + "/_authenticated/admin/quests/": { + "filePath": "_authenticated/admin/quests/index.tsx", + "parent": "/_authenticated/admin" } } } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 7f6938b..8dad0b1 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,13 +1,4 @@ -import { - AppHeader, - Button, - Card, - Form, - Link, - Logo, - TextField, -} from "@/components"; -import { useAuthActions } from "@convex-dev/auth/react"; +import type { Role } from "@convex/types"; import { type NavigateOptions, Outlet, @@ -15,10 +6,9 @@ import { createRootRouteWithContext, useRouter, } from "@tanstack/react-router"; -import { type ConvexAuthState, useConvexAuth } from "convex/react"; -import { useState } from "react"; +import type { ConvexAuthState } from "convex/react"; +import React, { Suspense } from "react"; import { RouterProvider } from "react-aria-components"; -import type { Role } from "../../convex/types"; declare module "react-aria-components" { interface RouterConfig { @@ -29,7 +19,7 @@ declare module "react-aria-components" { interface RouterContext { title: string; - auth: ConvexAuthState; + auth: Promise; role: Role; } @@ -39,105 +29,16 @@ export const Route = createRootRouteWithContext()({ function RootRoute() { const router = useRouter(); - const { isAuthenticated } = useConvexAuth(); - const isProd = process.env.NODE_ENV === "production"; - - const SignInWithMagicLink = ({ - handleLinkSent, - }: { - handleLinkSent: () => void; - }) => { - const { signIn } = useAuthActions(); - const [isSubmitting, setIsSubmitting] = useState(false); - const [error, setError] = useState(null); - - return ( -
{ - event.preventDefault(); - setIsSubmitting(true); - setError(null); - - const formData = new FormData(event.currentTarget); - signIn("resend", formData) - .then(handleLinkSent) - .catch((error) => { - console.error(error); - setError("Could not send sign-in link. Please try again."); - setIsSubmitting(false); - }); - }} - > - {/* TODO: Make a banner component */} - {error &&

{error}

} - - - - ); - }; - - const SignIn = () => { - const [step, setStep] = useState<"signIn" | "linkSent">("signIn"); - - return ( - - {step === "signIn" ? ( - setStep("linkSent")} /> - ) : ( -
-

Check your email.

-

A sign-in link has been sent to your email address.

- -
- )} -
- ); - }; - - const ClosedSignups = () => ( - -

- Namesake is in active development and currently closed to signups. For - name change support, join us on{" "} - Discord. -

-
- ); - - type AppProps = { - isAuthenticated: boolean; - isClosed: boolean; - }; - - const App = ({ isAuthenticated, isClosed }: AppProps) => { - if (isClosed || !isAuthenticated) { - return ( -
- - {isClosed ? : } -
- Namesake - Support - System Status -
-
- ); - } - return ( -
- - -
- ); - }; + const TanStackRouterDevtools = + process.env.NODE_ENV === "production" + ? () => null // Render nothing in production + : React.lazy(() => + // Lazy load in development + import("@tanstack/router-devtools").then((res) => ({ + default: res.TanStackRouterDevtools, + })), + ); return ( // TODO: Improve this API @@ -152,7 +53,10 @@ function RootRoute() { typeof path === "string" ? path : router.buildLocation(path).href } > - + + + +
); } diff --git a/src/routes/_authenticated.tsx b/src/routes/_authenticated.tsx new file mode 100644 index 0000000..b2ed590 --- /dev/null +++ b/src/routes/_authenticated.tsx @@ -0,0 +1,19 @@ +import { AppHeader } from "@/components"; +import { Outlet, createFileRoute, redirect } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_authenticated")({ + beforeLoad: async ({ context }) => { + const auth = await context.auth; + if (!auth.isAuthenticated) throw redirect({ to: "/login" }); + }, + component: AuthenticatedRoute, +}); + +function AuthenticatedRoute() { + return ( +
+ + +
+ ); +} diff --git a/src/routes/admin/forms/$formId.tsx b/src/routes/_authenticated/admin/forms/$formId.tsx similarity index 85% rename from src/routes/admin/forms/$formId.tsx rename to src/routes/_authenticated/admin/forms/$formId.tsx index 40c3cb7..fbb2296 100644 --- a/src/routes/admin/forms/$formId.tsx +++ b/src/routes/_authenticated/admin/forms/$formId.tsx @@ -1,10 +1,10 @@ import { Badge, PageHeader } from "@/components"; +import { api } from "@convex/_generated/api"; +import type { Id } from "@convex/_generated/dataModel"; import { createFileRoute } from "@tanstack/react-router"; import { useQuery } from "convex/react"; -import { api } from "../../../../convex/_generated/api"; -import type { Id } from "../../../../convex/_generated/dataModel"; -export const Route = createFileRoute("/admin/forms/$formId")({ +export const Route = createFileRoute("/_authenticated/admin/forms/$formId")({ component: AdminFormDetailRoute, }); diff --git a/src/routes/admin/forms/index.tsx b/src/routes/_authenticated/admin/forms/index.tsx similarity index 96% rename from src/routes/admin/forms/index.tsx rename to src/routes/_authenticated/admin/forms/index.tsx index b6b5b26..99772b5 100644 --- a/src/routes/admin/forms/index.tsx +++ b/src/routes/_authenticated/admin/forms/index.tsx @@ -19,15 +19,15 @@ import { TableRow, TextField, } from "@/components"; +import { api } from "@convex/_generated/api"; +import type { DataModel } from "@convex/_generated/dataModel"; +import { JURISDICTIONS } from "@convex/constants"; import { RiAddLine, RiFileTextLine, RiMoreFill } from "@remixicon/react"; import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery } from "convex/react"; import { useState } from "react"; -import { api } from "../../../../convex/_generated/api"; -import type { DataModel } from "../../../../convex/_generated/dataModel"; -import { JURISDICTIONS } from "../../../../convex/constants"; -export const Route = createFileRoute("/admin/forms/")({ +export const Route = createFileRoute("/_authenticated/admin/forms/")({ component: FormsRoute, }); diff --git a/src/routes/admin/index.tsx b/src/routes/_authenticated/admin/index.tsx similarity index 70% rename from src/routes/admin/index.tsx rename to src/routes/_authenticated/admin/index.tsx index 914904a..ac8afa6 100644 --- a/src/routes/admin/index.tsx +++ b/src/routes/_authenticated/admin/index.tsx @@ -1,6 +1,6 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; -export const Route = createFileRoute("/admin/")({ +export const Route = createFileRoute("/_authenticated/admin/")({ beforeLoad: () => { throw redirect({ to: "/admin/quests", diff --git a/src/routes/admin/quests/$questId.tsx b/src/routes/_authenticated/admin/quests/$questId.tsx similarity index 93% rename from src/routes/admin/quests/$questId.tsx rename to src/routes/_authenticated/admin/quests/$questId.tsx index e328528..d820fe3 100644 --- a/src/routes/admin/quests/$questId.tsx +++ b/src/routes/_authenticated/admin/quests/$questId.tsx @@ -7,14 +7,14 @@ import { RichTextEditor, TextField, } from "@/components"; +import { api } from "@convex/_generated/api"; +import type { Id } from "@convex/_generated/dataModel"; import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery } from "convex/react"; import { useState } from "react"; import Markdown from "react-markdown"; -import { api } from "../../../../convex/_generated/api"; -import type { Id } from "../../../../convex/_generated/dataModel"; -export const Route = createFileRoute("/admin/quests/$questId")({ +export const Route = createFileRoute("/_authenticated/admin/quests/$questId")({ component: AdminQuestDetailRoute, }); diff --git a/src/routes/admin/quests/index.tsx b/src/routes/_authenticated/admin/quests/index.tsx similarity index 94% rename from src/routes/admin/quests/index.tsx rename to src/routes/_authenticated/admin/quests/index.tsx index af7c72a..414f921 100644 --- a/src/routes/admin/quests/index.tsx +++ b/src/routes/_authenticated/admin/quests/index.tsx @@ -18,15 +18,15 @@ import { TableRow, TextField, } from "@/components"; +import { api } from "@convex/_generated/api"; +import type { DataModel } from "@convex/_generated/dataModel"; +import { JURISDICTIONS } from "@convex/constants"; import { RiAddLine, RiMoreFill, RiSignpostLine } from "@remixicon/react"; import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery } from "convex/react"; import { useState } from "react"; -import { api } from "../../../../convex/_generated/api"; -import type { DataModel } from "../../../../convex/_generated/dataModel"; -import { JURISDICTIONS } from "../../../../convex/constants"; -export const Route = createFileRoute("/admin/quests/")({ +export const Route = createFileRoute("/_authenticated/admin/quests/")({ component: QuestsRoute, }); @@ -95,7 +95,9 @@ const NewQuestModal = ({ const QuestTableRow = ({ quest, -}: { quest: DataModel["quests"]["document"] }) => { +}: { + quest: DataModel["quests"]["document"]; +}) => { const questCount = useQuery(api.usersQuests.getQuestCount, { questId: quest._id, }); diff --git a/src/routes/admin/route.tsx b/src/routes/_authenticated/admin/route.tsx similarity index 94% rename from src/routes/admin/route.tsx rename to src/routes/_authenticated/admin/route.tsx index 0d36784..8753f8e 100644 --- a/src/routes/admin/route.tsx +++ b/src/routes/_authenticated/admin/route.tsx @@ -7,7 +7,7 @@ import { } from "@remixicon/react"; import { Outlet, createFileRoute, redirect } from "@tanstack/react-router"; -export const Route = createFileRoute("/admin")({ +export const Route = createFileRoute("/_authenticated/admin")({ beforeLoad: ({ context }) => { const isAdmin = context.role === "admin"; diff --git a/src/routes/_authenticated/index.tsx b/src/routes/_authenticated/index.tsx new file mode 100644 index 0000000..d30f68f --- /dev/null +++ b/src/routes/_authenticated/index.tsx @@ -0,0 +1,7 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_authenticated/")({ + beforeLoad: () => { + throw redirect({ to: "/quests" }); + }, +}); diff --git a/src/routes/quests/$questId.tsx b/src/routes/_authenticated/quests/$questId.tsx similarity index 93% rename from src/routes/quests/$questId.tsx rename to src/routes/_authenticated/quests/$questId.tsx index fdcc943..492663a 100644 --- a/src/routes/quests/$questId.tsx +++ b/src/routes/_authenticated/quests/$questId.tsx @@ -6,14 +6,14 @@ import { MenuTrigger, PageHeader, } from "@/components"; +import { api } from "@convex/_generated/api"; +import type { Id } from "@convex/_generated/dataModel"; import { RiMoreLine } from "@remixicon/react"; import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery } from "convex/react"; import Markdown from "react-markdown"; -import { api } from "../../../convex/_generated/api"; -import type { Id } from "../../../convex/_generated/dataModel"; -export const Route = createFileRoute("/quests/$questId")({ +export const Route = createFileRoute("/_authenticated/quests/$questId")({ component: QuestDetailRoute, }); diff --git a/src/routes/quests/route.tsx b/src/routes/_authenticated/quests/route.tsx similarity index 96% rename from src/routes/quests/route.tsx rename to src/routes/_authenticated/quests/route.tsx index 8ad432b..c8326b6 100644 --- a/src/routes/quests/route.tsx +++ b/src/routes/_authenticated/quests/route.tsx @@ -8,6 +8,8 @@ import { GridListItem, Modal, } from "@/components"; +import { api } from "@convex/_generated/api"; +import type { Id } from "@convex/_generated/dataModel"; import { RiAddLine, RiSignpostLine } from "@remixicon/react"; import { Outlet, createFileRoute } from "@tanstack/react-router"; import { @@ -18,10 +20,8 @@ import { } from "convex/react"; import { useState } from "react"; import type { Selection } from "react-aria-components"; -import { api } from "../../../convex/_generated/api"; -import type { Id } from "../../../convex/_generated/dataModel"; -export const Route = createFileRoute("/quests")({ +export const Route = createFileRoute("/_authenticated/quests")({ component: IndexRoute, }); diff --git a/src/routes/settings/index.tsx b/src/routes/_authenticated/settings/index.tsx similarity index 96% rename from src/routes/settings/index.tsx rename to src/routes/_authenticated/settings/index.tsx index eb5477d..85ea9a9 100644 --- a/src/routes/settings/index.tsx +++ b/src/routes/_authenticated/settings/index.tsx @@ -10,16 +10,16 @@ import { TextField, } from "@/components"; import { useAuthActions } from "@convex-dev/auth/react"; +import { api } from "@convex/_generated/api"; +import type { Theme } from "@convex/types"; import { RiCheckLine, RiLoader4Line } from "@remixicon/react"; import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery } from "convex/react"; import { useTheme } from "next-themes"; import { useEffect, useState } from "react"; import { useDebouncedCallback } from "use-debounce"; -import { api } from "../../../convex/_generated/api"; -import type { Theme } from "../../../convex/types"; -export const Route = createFileRoute("/settings/")({ +export const Route = createFileRoute("/_authenticated/settings/")({ component: SettingsRoute, }); diff --git a/src/routes/_unauthenticated.tsx b/src/routes/_unauthenticated.tsx new file mode 100644 index 0000000..9b568d8 --- /dev/null +++ b/src/routes/_unauthenticated.tsx @@ -0,0 +1,14 @@ +import { Outlet, createFileRoute, redirect } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_unauthenticated")({ + beforeLoad: async ({ context }) => { + const auth = await context.auth; + // If already authenticated, redirect to dashboard + if (auth.isAuthenticated) throw redirect({ to: "/" }); + }, + component: UnauthenticatedRoute, +}); + +function UnauthenticatedRoute() { + return ; +} diff --git a/src/routes/_unauthenticated/login.tsx b/src/routes/_unauthenticated/login.tsx new file mode 100644 index 0000000..ee15a70 --- /dev/null +++ b/src/routes/_unauthenticated/login.tsx @@ -0,0 +1,88 @@ +import { Button, Card, Form, Link, Logo, TextField } from "@/components"; +import { useAuthActions } from "@convex-dev/auth/react"; +import { createFileRoute } from "@tanstack/react-router"; +import { useState } from "react"; + +export const Route = createFileRoute("/_unauthenticated/login")({ + component: LoginRoute, +}); + +const SignInWithMagicLink = ({ + handleLinkSent, +}: { + handleLinkSent: () => void; +}) => { + const { signIn } = useAuthActions(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + + return ( +
{ + event.preventDefault(); + setIsSubmitting(true); + setError(null); + + const formData = new FormData(event.currentTarget); + signIn("resend", formData) + .then(handleLinkSent) + .catch((error) => { + console.error(error); + setError("Could not send sign-in link. Please try again."); + setIsSubmitting(false); + }); + }} + > + {/* TODO: Make a banner component */} + {error &&

{error}

} + + + + ); +}; + +const SignIn = () => { + const [step, setStep] = useState<"signIn" | "linkSent">("signIn"); + + return ( + + {step === "signIn" ? ( + setStep("linkSent")} /> + ) : ( +
+

Check your email.

+

A sign-in link has been sent to your email address.

+ +
+ )} +
+ ); +}; + +const ClosedSignups = () => ( + +

+ Namesake is in active development and currently closed to signups. For + name change support, join us on{" "} + Discord. +

+
+); + +function LoginRoute() { + const isClosed = process.env.NODE_ENV === "production"; + + return ( +
+ + {isClosed ? : } +
+ Namesake + Support + System Status +
+
+ ); +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx deleted file mode 100644 index 8758562..0000000 --- a/src/routes/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute, redirect } from "@tanstack/react-router"; - -export const Route = createFileRoute("/")({ - beforeLoad: ({ context }) => { - if (context.auth.isAuthenticated) throw redirect({ to: "/quests" }); - }, -}); diff --git a/tsconfig.app.json b/tsconfig.app.json index 4bfd345..b27e830 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -17,7 +17,8 @@ "noFallthroughCasesInSwitch": true, "baseUrl": ".", "paths": { - "@/*": ["src/*"] + "@/*": ["src/*"], + "@convex/*": ["convex/*"] } } } diff --git a/tsconfig.node.json b/tsconfig.node.json index a3fd855..065a310 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -15,7 +15,8 @@ "noFallthroughCasesInSwitch": true, "baseUrl": ".", "paths": { - "@/*": ["src/*"] + "@/*": ["src/*"], + "@convex/*": ["convex/*"] } }, "include": ["vite.config.ts"] diff --git a/vite.config.ts b/vite.config.ts index a295c87..c5327a8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -20,7 +20,10 @@ export default defineConfig({ }, }, resolve: { - alias: [{ find: "@", replacement: "/src" }], + alias: [ + { find: "@", replacement: "/src" }, + { find: "@convex", replacement: "/convex" }, + ], }, test: { environment: "edge-runtime",