implement sign up page

This commit is contained in:
2025-05-07 23:40:03 +01:00
parent d3638ffc80
commit 77cb38294c
9 changed files with 166 additions and 13 deletions

View File

@@ -142,8 +142,7 @@ async function login(request: Bun.BunRequest<"/api/login">) {
async function logout(request: Bun.BunRequest<"/api/logout">, user: User): Promise<Response> {
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 })
}

View File

@@ -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 })
}

View File

@@ -28,8 +28,8 @@ type QueryKey = ["bookmarks", ...ReadonlyArray<unknown>]
async function fetchApi(route: string, init?: RequestInit): Promise<Response> {
const response = await fetch(`${import.meta.env.VITE_API_URL}/api${route}`, {
...init,
credentials: "include",
...init,
})
switch (response.status) {
case 200:

View File

@@ -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"
}
}
}

View File

@@ -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() {
<Button>
<span className="underline">S</span>EARCH
</Button>
<LogOutButton />
</div>
)
}
function LogOutButton() {
const logOutMutation = useLogOut()
const navigate = useNavigate()
function logOut() {
logOutMutation.mutate()
navigate({ to: "/", replace: true })
}
return (
<Button disabled={logOutMutation.isPending} onClick={logOut}>
LOG OUT
</Button>
)
}
export const Route = createFileRoute("/bookmarks")({
component: Page,
})

View File

@@ -14,7 +14,7 @@ function Index() {
<p className="pb-4 text-2xl uppercase">BOOKMARK MANAGER</p>
<div className="flex flex-col text-lg">
<Link href="/login">LOGIN</Link>
<Link>SIGN UP</Link>
<Link href="/signup">SIGN UP</Link>
<DemoButton />
</div>
</div>

View File

@@ -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<HTMLFormElement>) {
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 (
<main className="p-48 flex flex-row items-start justify-center space-x-24">
<div className="flex flex-col items-start">
<h1 className="text-2xl">
<span>WELCOME TO</span>
<br />
<span className="font-bold">MARKONE</span>
</h1>
</div>
<div className="container max-w-2xl">
<h2 className="font-bold">CREATE ACCOUNT</h2>
{error ? (
<div className="border border-red-500 text-red-500 p-3 my-4">
<p className="text-red-500 font-bold">{error}</p>
</div>
) : null}
<form onSubmit={submitSignUpForm}>
<FormField type="text" name="username" label="USERNAME" className="mb-2" />
<FormField type="password" name="password" label="PASSWORD" className="mb-2" />
<FormField type="password" name="confirmPassword" label="CONFIRM PASSWORD" className="mb-8" />
<Button className="w-full py-2 md:py-2" type="submit">
Create account
</Button>
</form>
</div>
</main>
)
}
export const Route = createFileRoute("/signup")({
component: Page,
})

View File

@@ -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 }

View File

@@ -26,7 +26,7 @@ function FormField({ name, label, type, className, labelClassName, required = fa
<label
htmlFor={id}
className={twMerge(
"select-none border-x-2 border-transparent w-min translate-y-[55%] bg-stone-200 dark:bg-stone-900 mx-2 px-1 peer-focus:text-teal-600 peer-focus:font-bold",
"select-none border-x-2 border-transparent w-min text-nowrap translate-y-[55%] bg-stone-200 dark:bg-stone-900 mx-2 px-1 peer-focus:text-teal-600 peer-focus:font-bold",
labelClassName,
)}
>