From 76db1dd640256b1b67dc87ff6b06de142f0ae3e8 Mon Sep 17 00:00:00 2001 From: Anton Bezborody Date: Mon, 16 Dec 2024 19:16:50 +0300 Subject: [PATCH] auth, middleware for auth routes --- frontend/index.html | 2 +- frontend/src/lib/api.ts | 16 ++ frontend/src/main.tsx | 2 +- frontend/src/routeTree.gen.ts | 185 ++++++++++++------ frontend/src/routes/__root.tsx | 20 +- frontend/src/routes/_authenticated.tsx | 43 ++++ .../{ => _authenticated}/create-expense.tsx | 26 +-- .../routes/{ => _authenticated}/expenses.tsx | 18 +- .../src/routes/{ => _authenticated}/index.tsx | 20 +- .../src/routes/_authenticated/profile.tsx | 25 +++ frontend/src/routes/about.tsx | 2 +- server/routes/expenses.ts | 9 +- 12 files changed, 267 insertions(+), 101 deletions(-) create mode 100644 frontend/src/routes/_authenticated.tsx rename frontend/src/routes/{ => _authenticated}/create-expense.tsx (77%) rename frontend/src/routes/{ => _authenticated}/expenses.tsx (82%) rename frontend/src/routes/{ => _authenticated}/index.tsx (58%) create mode 100644 frontend/src/routes/_authenticated/profile.tsx diff --git a/frontend/index.html b/frontend/index.html index 1ce90b3..b9248ed 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,4 +1,4 @@ - + diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 1e9f593..7a849fd 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,6 +1,22 @@ import { hc } from "hono/client" import { type ApiRoutes } from "@server/app" +import { queryOptions } from "@tanstack/react-query" const client = hc("/") export const api = client.api + +async function getCurrentUser() { + const result = await api.me.$get() + if (!result.ok) { + throw new Error('error') + } + const data = await result.json() + return data + } + +export const userQueryOptions = queryOptions({ + queryKey: ['getCurrentUser'], + queryFn: getCurrentUser, + staleTime: Infinity + }) \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index ef226d0..2c914ee 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -11,7 +11,7 @@ const queryClient = new QueryClient() import { routeTree } from "./routeTree.gen" // Create a new router instance -const router = createRouter({ routeTree }) +const router = createRouter({ routeTree, context: { queryClient } }) // Register the router instance for type safety declare module "@tanstack/react-router" { diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 52c0fa6..535b5b1 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -11,46 +11,61 @@ // Import Routes import { Route as rootRoute } from './routes/__root' -import { Route as ExpensesImport } from './routes/expenses' -import { Route as CreateExpenseImport } from './routes/create-expense' import { Route as AboutImport } from './routes/about' -import { Route as IndexImport } from './routes/index' +import { Route as AuthenticatedImport } from './routes/_authenticated' +import { Route as AuthenticatedIndexImport } from './routes/_authenticated/index' +import { Route as AuthenticatedProfileImport } from './routes/_authenticated/profile' +import { Route as AuthenticatedExpensesImport } from './routes/_authenticated/expenses' +import { Route as AuthenticatedCreateExpenseImport } from './routes/_authenticated/create-expense' // Create/Update Routes -const ExpensesRoute = ExpensesImport.update({ - id: '/expenses', - path: '/expenses', - getParentRoute: () => rootRoute, -} as any) - -const CreateExpenseRoute = CreateExpenseImport.update({ - id: '/create-expense', - path: '/create-expense', - getParentRoute: () => rootRoute, -} as any) - const AboutRoute = AboutImport.update({ id: '/about', path: '/about', getParentRoute: () => rootRoute, } as any) -const IndexRoute = IndexImport.update({ +const AuthenticatedRoute = AuthenticatedImport.update({ + id: '/_authenticated', + getParentRoute: () => rootRoute, +} as any) + +const AuthenticatedIndexRoute = AuthenticatedIndexImport.update({ id: '/', path: '/', - getParentRoute: () => rootRoute, + getParentRoute: () => AuthenticatedRoute, +} as any) + +const AuthenticatedProfileRoute = AuthenticatedProfileImport.update({ + id: '/profile', + path: '/profile', + getParentRoute: () => AuthenticatedRoute, +} as any) + +const AuthenticatedExpensesRoute = AuthenticatedExpensesImport.update({ + id: '/expenses', + path: '/expenses', + getParentRoute: () => AuthenticatedRoute, } as any) +const AuthenticatedCreateExpenseRoute = AuthenticatedCreateExpenseImport.update( + { + id: '/create-expense', + path: '/create-expense', + getParentRoute: () => AuthenticatedRoute, + } 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 } '/about': { @@ -60,68 +75,108 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AboutImport parentRoute: typeof rootRoute } - '/create-expense': { - id: '/create-expense' + '/_authenticated/create-expense': { + id: '/_authenticated/create-expense' path: '/create-expense' fullPath: '/create-expense' - preLoaderRoute: typeof CreateExpenseImport - parentRoute: typeof rootRoute + preLoaderRoute: typeof AuthenticatedCreateExpenseImport + parentRoute: typeof AuthenticatedImport } - '/expenses': { - id: '/expenses' + '/_authenticated/expenses': { + id: '/_authenticated/expenses' path: '/expenses' fullPath: '/expenses' - preLoaderRoute: typeof ExpensesImport - parentRoute: typeof rootRoute + preLoaderRoute: typeof AuthenticatedExpensesImport + parentRoute: typeof AuthenticatedImport + } + '/_authenticated/profile': { + id: '/_authenticated/profile' + path: '/profile' + fullPath: '/profile' + preLoaderRoute: typeof AuthenticatedProfileImport + parentRoute: typeof AuthenticatedImport + } + '/_authenticated/': { + id: '/_authenticated/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof AuthenticatedIndexImport + parentRoute: typeof AuthenticatedImport } } } // Create and export the route tree +interface AuthenticatedRouteChildren { + AuthenticatedCreateExpenseRoute: typeof AuthenticatedCreateExpenseRoute + AuthenticatedExpensesRoute: typeof AuthenticatedExpensesRoute + AuthenticatedProfileRoute: typeof AuthenticatedProfileRoute + AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute +} + +const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { + AuthenticatedCreateExpenseRoute: AuthenticatedCreateExpenseRoute, + AuthenticatedExpensesRoute: AuthenticatedExpensesRoute, + AuthenticatedProfileRoute: AuthenticatedProfileRoute, + AuthenticatedIndexRoute: AuthenticatedIndexRoute, +} + +const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren( + AuthenticatedRouteChildren, +) + export interface FileRoutesByFullPath { - '/': typeof IndexRoute + '': typeof AuthenticatedRouteWithChildren '/about': typeof AboutRoute - '/create-expense': typeof CreateExpenseRoute - '/expenses': typeof ExpensesRoute + '/create-expense': typeof AuthenticatedCreateExpenseRoute + '/expenses': typeof AuthenticatedExpensesRoute + '/profile': typeof AuthenticatedProfileRoute + '/': typeof AuthenticatedIndexRoute } export interface FileRoutesByTo { - '/': typeof IndexRoute '/about': typeof AboutRoute - '/create-expense': typeof CreateExpenseRoute - '/expenses': typeof ExpensesRoute + '/create-expense': typeof AuthenticatedCreateExpenseRoute + '/expenses': typeof AuthenticatedExpensesRoute + '/profile': typeof AuthenticatedProfileRoute + '/': typeof AuthenticatedIndexRoute } export interface FileRoutesById { __root__: typeof rootRoute - '/': typeof IndexRoute + '/_authenticated': typeof AuthenticatedRouteWithChildren '/about': typeof AboutRoute - '/create-expense': typeof CreateExpenseRoute - '/expenses': typeof ExpensesRoute + '/_authenticated/create-expense': typeof AuthenticatedCreateExpenseRoute + '/_authenticated/expenses': typeof AuthenticatedExpensesRoute + '/_authenticated/profile': typeof AuthenticatedProfileRoute + '/_authenticated/': typeof AuthenticatedIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/about' | '/create-expense' | '/expenses' + fullPaths: '' | '/about' | '/create-expense' | '/expenses' | '/profile' | '/' fileRoutesByTo: FileRoutesByTo - to: '/' | '/about' | '/create-expense' | '/expenses' - id: '__root__' | '/' | '/about' | '/create-expense' | '/expenses' + to: '/about' | '/create-expense' | '/expenses' | '/profile' | '/' + id: + | '__root__' + | '/_authenticated' + | '/about' + | '/_authenticated/create-expense' + | '/_authenticated/expenses' + | '/_authenticated/profile' + | '/_authenticated/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute + AuthenticatedRoute: typeof AuthenticatedRouteWithChildren AboutRoute: typeof AboutRoute - CreateExpenseRoute: typeof CreateExpenseRoute - ExpensesRoute: typeof ExpensesRoute } const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, + AuthenticatedRoute: AuthenticatedRouteWithChildren, AboutRoute: AboutRoute, - CreateExpenseRoute: CreateExpenseRoute, - ExpensesRoute: ExpensesRoute, } export const routeTree = rootRoute @@ -134,23 +189,37 @@ export const routeTree = rootRoute "__root__": { "filePath": "__root.tsx", "children": [ - "/", - "/about", - "/create-expense", - "/expenses" + "/_authenticated", + "/about" ] }, - "/": { - "filePath": "index.tsx" + "/_authenticated": { + "filePath": "_authenticated.tsx", + "children": [ + "/_authenticated/create-expense", + "/_authenticated/expenses", + "/_authenticated/profile", + "/_authenticated/" + ] }, "/about": { "filePath": "about.tsx" }, - "/create-expense": { - "filePath": "create-expense.tsx" + "/_authenticated/create-expense": { + "filePath": "_authenticated/create-expense.tsx", + "parent": "/_authenticated" + }, + "/_authenticated/expenses": { + "filePath": "_authenticated/expenses.tsx", + "parent": "/_authenticated" + }, + "/_authenticated/profile": { + "filePath": "_authenticated/profile.tsx", + "parent": "/_authenticated" }, - "/expenses": { - "filePath": "expenses.tsx" + "/_authenticated/": { + "filePath": "_authenticated/index.tsx", + "parent": "/_authenticated" } } } diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index b66b63f..513ac35 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -1,24 +1,36 @@ -import { Link, Outlet, createRootRoute } from "@tanstack/react-router" +import { type QueryClient } from "@tanstack/react-query" +import { + Link, + Outlet, + createRootRouteWithContext, +} from "@tanstack/react-router" -export const Route = createRootRoute({ +interface MyRouterContext { + queryClient: QueryClient +} + +export const Route = createRootRouteWithContext()({ component: Root, }) function NavBar() { return ( -
+
Home About - {" "} + Expenses Create expense + + Profile +
) } diff --git a/frontend/src/routes/_authenticated.tsx b/frontend/src/routes/_authenticated.tsx new file mode 100644 index 0000000..72b9a3e --- /dev/null +++ b/frontend/src/routes/_authenticated.tsx @@ -0,0 +1,43 @@ +import { Button } from "@/components/ui/button" +import { userQueryOptions } from "@/lib/api" +import { createFileRoute, Outlet } from "@tanstack/react-router" + +const Login = () => { + return ( +
+ + You have to login +
+ ) +} + +const Component = () => { + const { user } = Route.useRouteContext() + if (!user) { + return + } + + return +} + +// src/routes/_authenticated.tsx +export const Route = createFileRoute("/_authenticated")({ + beforeLoad: async ({ context }) => { + const queryClient = context.queryClient + + try { + const data = await queryClient.fetchQuery(userQueryOptions) + return data + } catch (e) { + console.error(e) + return { user: null } + } + // userQueryOptions + // check if the user is authenticated + return { user: { name: "John Doe" } } + // return { user: null } + }, + component: Component, +}) diff --git a/frontend/src/routes/create-expense.tsx b/frontend/src/routes/_authenticated/create-expense.tsx similarity index 77% rename from frontend/src/routes/create-expense.tsx rename to frontend/src/routes/_authenticated/create-expense.tsx index 49690c1..d01fbc3 100644 --- a/frontend/src/routes/create-expense.tsx +++ b/frontend/src/routes/_authenticated/create-expense.tsx @@ -1,11 +1,11 @@ -import { createFileRoute, useNavigate } from "@tanstack/react-router" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Button } from "@/components/ui/button" -import { useForm } from "@tanstack/react-form" -import { api } from "@/lib/api" +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/button' +import { useForm } from '@tanstack/react-form' +import { api } from '@/lib/api' -export const Route = createFileRoute("/create-expense")({ +export const Route = createFileRoute('/_authenticated/create-expense')({ component: CreateExpense, }) @@ -13,16 +13,16 @@ function CreateExpense() { const navigate = useNavigate() const form = useForm({ defaultValues: { - title: "", + title: '', amount: 0, }, onSubmit: async ({ value }) => { // Do something with form data const res = await api.expenses.$post({ json: value }) if (!res.ok) { - throw new Error("server error") + throw new Error('server error') } - navigate({ to: "/expenses" }) + navigate({ to: '/expenses' }) }, }) @@ -51,7 +51,7 @@ function CreateExpense() { type="text" /> {field.state.meta.isTouched && field.state.meta.errors.length ? ( - {field.state.meta.errors.join(", ")} + {field.state.meta.errors.join(', ')} ) : null} )} @@ -70,7 +70,7 @@ function CreateExpense() { type="number" /> {field.state.meta.isTouched && field.state.meta.errors.length ? ( - {field.state.meta.errors.join(", ")} + {field.state.meta.errors.join(', ')} ) : null} )} @@ -79,7 +79,7 @@ function CreateExpense() { selector={(state) => [state.canSubmit, state.isSubmitting]} children={([canSubmit, isSubmitting]) => ( )} /> diff --git a/frontend/src/routes/expenses.tsx b/frontend/src/routes/_authenticated/expenses.tsx similarity index 82% rename from frontend/src/routes/expenses.tsx rename to frontend/src/routes/_authenticated/expenses.tsx index 88b751f..43ff349 100644 --- a/frontend/src/routes/expenses.tsx +++ b/frontend/src/routes/_authenticated/expenses.tsx @@ -1,6 +1,6 @@ -import { createFileRoute } from "@tanstack/react-router" -import { api } from "@/lib/api" -import { useQuery } from "@tanstack/react-query" +import { createFileRoute } from '@tanstack/react-router' +import { api } from '@/lib/api' +import { useQuery } from '@tanstack/react-query' import { Table, TableBody, @@ -9,17 +9,17 @@ import { TableHead, TableHeader, TableRow, -} from "@/components/ui/table" -import { Skeleton } from "@/components/ui/skeleton" +} from '@/components/ui/table' +import { Skeleton } from '@/components/ui/skeleton' -export const Route = createFileRoute("/expenses")({ +export const Route = createFileRoute('/_authenticated/expenses')({ component: Expenses, }) async function getAllExpenses() { const result = await api.expenses.$get() if (!result.ok) { - throw new Error("error") + throw new Error('error') } const data = await result.json() return data @@ -28,12 +28,12 @@ async function getAllExpenses() { function Expenses() { // Queries const { isPending, error, data } = useQuery({ - queryKey: ["getAllExpenses"], + queryKey: ['getAllExpenses'], queryFn: getAllExpenses, }) if (error) { - return "Error:" + error.message + return 'Error:' + error.message } return ( diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/_authenticated/index.tsx similarity index 58% rename from frontend/src/routes/index.tsx rename to frontend/src/routes/_authenticated/index.tsx index d4e214f..a05ff25 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/_authenticated/index.tsx @@ -1,21 +1,21 @@ -import { createFileRoute } from "@tanstack/react-router" +import { createFileRoute } from '@tanstack/react-router' import { Card, CardContent, CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card" -import { api } from "../lib/api" -import { useQuery } from "@tanstack/react-query" +} from '@/components/ui/card' +import { api } from '../../lib/api' +import { useQuery } from '@tanstack/react-query' -export const Route = createFileRoute("/")({ +export const Route = createFileRoute('/_authenticated/')({ component: Index, }) async function getTotalSpent() { - const result = await api.expenses["getTotalSpent"].$get() + const result = await api.expenses['getTotalSpent'].$get() if (!result.ok) { - throw new Error("error") + throw new Error('error') } const data = await result.json() return data @@ -24,12 +24,12 @@ async function getTotalSpent() { function Index() { // Queries const { isPending, error, data } = useQuery({ - queryKey: ["getTotalSpent"], + queryKey: ['getTotalSpent'], queryFn: getTotalSpent, }) if (error) { - return "Error:" + error.message + return 'Error:' + error.message } return ( @@ -39,7 +39,7 @@ function Index() { Total spent Total amount you've spent - {isPending ? "Loading..." : data.total} + {isPending ? 'Loading...' : data.total}
) diff --git a/frontend/src/routes/_authenticated/profile.tsx b/frontend/src/routes/_authenticated/profile.tsx new file mode 100644 index 0000000..8300350 --- /dev/null +++ b/frontend/src/routes/_authenticated/profile.tsx @@ -0,0 +1,25 @@ +import { createFileRoute } from "@tanstack/react-router" + +import { userQueryOptions } from "../../lib/api" +import { useQuery } from "@tanstack/react-query" +import { Button } from "@/components/ui/button" + +export const Route = createFileRoute("/_authenticated/profile")({ + component: Profile, +}) + +function Profile() { + // Queries + const { isPending, error, data } = useQuery(userQueryOptions) + if (isPending) return "Loading..." + if (error) return "Not logged in" + + return ( +
+ + Hello, {data.user.given_name}! Nice to see you! +
+ ) +} diff --git a/frontend/src/routes/about.tsx b/frontend/src/routes/about.tsx index c3f3cb2..82bced2 100644 --- a/frontend/src/routes/about.tsx +++ b/frontend/src/routes/about.tsx @@ -5,5 +5,5 @@ export const Route = createFileRoute("/about")({ }) function About() { - return
Hello "/about"!
+ return
Hello "/about"!
} diff --git a/server/routes/expenses.ts b/server/routes/expenses.ts index 76aeaa2..c93ccd8 100644 --- a/server/routes/expenses.ts +++ b/server/routes/expenses.ts @@ -1,6 +1,7 @@ import { Hono } from "hono" import { z } from "zod" import { zValidator } from "@hono/zod-validator" +import { getUser } from "../kinde" const expenseSchema = z.object({ id: z.number().int().positive().min(1), @@ -31,23 +32,23 @@ const fakeExpenses: Expense[] = [ ] export const exprensesRoute = new Hono() - .get("/", (c) => { + .get("/", getUser, (c) => { return c.json({ expenses: fakeExpenses, }) }) - .post("/", zValidator("json", createPostSchema), async (c) => { + .post("/", getUser, zValidator("json", createPostSchema), async (c) => { const data = await c.req.valid("json") const expense = createPostSchema.parse(data) fakeExpenses.push({ id: fakeExpenses.length + 1, ...expense }) c.status(201) return c.json(expense) }) - .get("/getTotalSpent", (c) => { + .get("/getTotalSpent", getUser, (c) => { const total = fakeExpenses.reduce((acc, curr) => acc + curr.amount, 0) return c.json({ total }) }) - .get("/:id{[0-9]+}", (c) => { + .get("/:id{[0-9]+}", getUser, (c) => { const id = Number.parseInt(c.req.param("id")) const expense = fakeExpenses.find((e) => e.id === id) if (!expense) {