From 77cb38294cc471bef691a9195ebb112fef644357 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Wed, 7 May 2025 23:40:03 +0100 Subject: [PATCH] implement sign up page --- packages/server/src/auth/auth.ts | 3 +- packages/server/src/auth/session.ts | 8 +- packages/web/src/api.ts | 2 +- packages/web/src/app/-route-tree.gen.ts | 31 +++++++- packages/web/src/app/bookmarks.tsx | 20 ++++- packages/web/src/app/index.tsx | 2 +- packages/web/src/app/signup.tsx | 88 ++++++++++++++++++++++ packages/web/src/auth.ts | 23 +++++- packages/web/src/components/form-field.tsx | 2 +- 9 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 packages/web/src/app/signup.tsx diff --git a/packages/server/src/auth/auth.ts b/packages/server/src/auth/auth.ts index 89a1bc4..be5c407 100644 --- a/packages/server/src/auth/auth.ts +++ b/packages/server/src/auth/auth.ts @@ -142,8 +142,7 @@ async function login(request: Bun.BunRequest<"/api/login">) { async function logout(request: Bun.BunRequest<"/api/logout">, user: User): Promise { const deleteAllAuthTokensQuery = db.query("DELETE FROM auth_tokens WHERE user_id = $userId") - - forgetAllSessions(user) + forgetAllSessions(request.cookies, user) deleteAllAuthTokensQuery.run({ userId: user.id }) return new Response(undefined, { status: 204 }) } diff --git a/packages/server/src/auth/session.ts b/packages/server/src/auth/session.ts index 751ee77..0d5f287 100644 --- a/packages/server/src/auth/session.ts +++ b/packages/server/src/auth/session.ts @@ -110,7 +110,7 @@ function verifySession(cookie: Bun.CookieMap): Session | null { function extendSession(session: Session): Session { const newExpiryDate = dayjs().add(30, "minutes").valueOf() const extendSessionQuery = db.query( - "UPDATE sessions SET expires_at_unix_ms = $newExpiryDate WHERE session_id = $session_id", + "UPDATE sessions SET expires_at_unix_ms = $newExpiryDate WHERE session_id = $sessionId", ) extendSessionQuery.run({ sessionId: session.id, @@ -122,7 +122,11 @@ function extendSession(session: Session): Session { } } -function forgetAllSessions(user: User) { +function forgetAllSessions(cookies: Bun.CookieMap, user: User) { + cookies.set(SESSION_ID_COOKIE_NAME, "", { + expires: 0, + httpOnly: true, + }) const forgetAllSessionsQuery = db.query("DELETE FROM sessions WHERE user_id = $userId") forgetAllSessionsQuery.run({ userId: user.id }) } diff --git a/packages/web/src/api.ts b/packages/web/src/api.ts index c9bf585..da85103 100644 --- a/packages/web/src/api.ts +++ b/packages/web/src/api.ts @@ -28,8 +28,8 @@ type QueryKey = ["bookmarks", ...ReadonlyArray] async function fetchApi(route: string, init?: RequestInit): Promise { const response = await fetch(`${import.meta.env.VITE_API_URL}/api${route}`, { - ...init, credentials: "include", + ...init, }) switch (response.status) { case 200: diff --git a/packages/web/src/app/-route-tree.gen.ts b/packages/web/src/app/-route-tree.gen.ts index 69fe126..5528b34 100644 --- a/packages/web/src/app/-route-tree.gen.ts +++ b/packages/web/src/app/-route-tree.gen.ts @@ -11,12 +11,19 @@ // Import Routes import { Route as rootRoute } from "./__root" +import { Route as SignupImport } from "./signup" import { Route as LoginImport } from "./login" import { Route as BookmarksImport } from "./bookmarks" import { Route as IndexImport } from "./index" // Create/Update Routes +const SignupRoute = SignupImport.update({ + id: "/signup", + path: "/signup", + getParentRoute: () => rootRoute, +} as any) + const LoginRoute = LoginImport.update({ id: "/login", path: "/login", @@ -60,6 +67,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof LoginImport parentRoute: typeof rootRoute } + "/signup": { + id: "/signup" + path: "/signup" + fullPath: "/signup" + preLoaderRoute: typeof SignupImport + parentRoute: typeof rootRoute + } } } @@ -69,12 +83,14 @@ export interface FileRoutesByFullPath { "/": typeof IndexRoute "/bookmarks": typeof BookmarksRoute "/login": typeof LoginRoute + "/signup": typeof SignupRoute } export interface FileRoutesByTo { "/": typeof IndexRoute "/bookmarks": typeof BookmarksRoute "/login": typeof LoginRoute + "/signup": typeof SignupRoute } export interface FileRoutesById { @@ -82,14 +98,15 @@ export interface FileRoutesById { "/": typeof IndexRoute "/bookmarks": typeof BookmarksRoute "/login": typeof LoginRoute + "/signup": typeof SignupRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: "/" | "/bookmarks" | "/login" + fullPaths: "/" | "/bookmarks" | "/login" | "/signup" fileRoutesByTo: FileRoutesByTo - to: "/" | "/bookmarks" | "/login" - id: "__root__" | "/" | "/bookmarks" | "/login" + to: "/" | "/bookmarks" | "/login" | "/signup" + id: "__root__" | "/" | "/bookmarks" | "/login" | "/signup" fileRoutesById: FileRoutesById } @@ -97,12 +114,14 @@ export interface RootRouteChildren { IndexRoute: typeof IndexRoute BookmarksRoute: typeof BookmarksRoute LoginRoute: typeof LoginRoute + SignupRoute: typeof SignupRoute } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, BookmarksRoute: BookmarksRoute, LoginRoute: LoginRoute, + SignupRoute: SignupRoute, } export const routeTree = rootRoute @@ -117,7 +136,8 @@ export const routeTree = rootRoute "children": [ "/", "/bookmarks", - "/login" + "/login", + "/signup" ] }, "/": { @@ -128,6 +148,9 @@ export const routeTree = rootRoute }, "/login": { "filePath": "login.tsx" + }, + "/signup": { + "filePath": "signup.tsx" } } } diff --git a/packages/web/src/app/bookmarks.tsx b/packages/web/src/app/bookmarks.tsx index bb6ed53..ab09f2d 100644 --- a/packages/web/src/app/bookmarks.tsx +++ b/packages/web/src/app/bookmarks.tsx @@ -1,5 +1,5 @@ import type { LinkBookmark } from "@markone/core/bookmark" -import { createFileRoute } from "@tanstack/react-router" +import { createFileRoute, useNavigate } from "@tanstack/react-router" import clsx from "clsx" import { useEffect, useId, useState } from "react" import { create } from "zustand" @@ -11,6 +11,7 @@ import { Message, MessageVariant } from "~/components/message" import { FormField } from "~/components/form-field" import { LoadingSpinner } from "~/components/loading-spinner" import { useMnemonics } from "~/hooks/use-mnemonics" +import { useLogOut } from "~/auth" const LAYOUT_MODE = { popup: "popup", @@ -593,10 +594,27 @@ function ActionBar() { + ) } +function LogOutButton() { + const logOutMutation = useLogOut() + const navigate = useNavigate() + + function logOut() { + logOutMutation.mutate() + navigate({ to: "/", replace: true }) + } + + return ( + + ) +} + export const Route = createFileRoute("/bookmarks")({ component: Page, }) diff --git a/packages/web/src/app/index.tsx b/packages/web/src/app/index.tsx index d0e89b6..130ebb1 100644 --- a/packages/web/src/app/index.tsx +++ b/packages/web/src/app/index.tsx @@ -14,7 +14,7 @@ function Index() {

BOOKMARK MANAGER

LOGIN - SIGN UP + SIGN UP
diff --git a/packages/web/src/app/signup.tsx b/packages/web/src/app/signup.tsx new file mode 100644 index 0000000..fb7fa1e --- /dev/null +++ b/packages/web/src/app/signup.tsx @@ -0,0 +1,88 @@ +import { useNavigate } from "@tanstack/react-router" +import { useState } from "react" +import { createFileRoute } from "@tanstack/react-router" +import { FormField } from "~/components/form-field" +import { Button } from "~/components/button" +import { useSignUp } from "~/auth" + +function Page() { + const [error, setError] = useState("") + const navigate = useNavigate() + const signUpMutation = useSignUp() + + async function submitSignUpForm(event: React.FormEvent) { + event.preventDefault() + + if (!event.currentTarget) { + return + } + + const formData = new FormData(event.currentTarget) + + const username = formData.get("username") + const password = formData.get("password") + const passwordConfirmation = formData.get("confirmPassword") + + if ( + typeof username === "string" && + typeof password === "string" && + typeof passwordConfirmation === "string" && + username && + password && + passwordConfirmation + ) { + if (password !== passwordConfirmation) { + setError("Mistmatched password!") + } else { + try { + const res = await signUpMutation.mutateAsync({ + username, + password, + }) + switch (res.status) { + case 200: + navigate({ to: "/bookmarks" }) + break + default: + setError(`Server returned an unexpected response (status ${res.status}.)`) + break + } + } catch (error) { + setError("Unable to connect to the server!") + } + } + } + } + + return ( +
+
+

+ WELCOME TO +
+ MARKONE +

+
+
+

CREATE ACCOUNT

+ {error ? ( +
+

{error}

+
+ ) : null} +
+ + + + + +
+
+ ) +} + +export const Route = createFileRoute("/signup")({ + component: Page, +}) diff --git a/packages/web/src/auth.ts b/packages/web/src/auth.ts index 827bf61..acb5c5f 100644 --- a/packages/web/src/auth.ts +++ b/packages/web/src/auth.ts @@ -1,4 +1,5 @@ import { useMutation } from "@tanstack/react-query" +import { fetchApi } from "./api" function useLogin() { return useMutation({ @@ -12,4 +13,24 @@ function useLogin() { }) } -export { useLogin } +function useLogOut() { + return useMutation({ + mutationFn: () => + fetchApi("/logout", { + method: "POST", + }), + }) +} + +function useSignUp() { + return useMutation({ + mutationFn: (creds: { username: string; password: string }) => + fetchApi("/sign-up", { + method: "POST", + credentials: "omit", + body: JSON.stringify(creds), + }), + }) +} + +export { useLogin, useLogOut, useSignUp } diff --git a/packages/web/src/components/form-field.tsx b/packages/web/src/components/form-field.tsx index 4800611..28442a4 100644 --- a/packages/web/src/components/form-field.tsx +++ b/packages/web/src/components/form-field.tsx @@ -26,7 +26,7 @@ function FormField({ name, label, type, className, labelClassName, required = fa