implement sign up page
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
|
@@ -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 })
|
||||
}
|
||||
|
@@ -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:
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
})
|
||||
|
@@ -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>
|
||||
|
88
packages/web/src/app/signup.tsx
Normal file
88
packages/web/src/app/signup.tsx
Normal 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,
|
||||
})
|
@@ -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 }
|
||||
|
@@ -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,
|
||||
)}
|
||||
>
|
||||
|
Reference in New Issue
Block a user