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> {
|
async function logout(request: Bun.BunRequest<"/api/logout">, user: User): Promise<Response> {
|
||||||
const deleteAllAuthTokensQuery = db.query("DELETE FROM auth_tokens WHERE user_id = $userId")
|
const deleteAllAuthTokensQuery = db.query("DELETE FROM auth_tokens WHERE user_id = $userId")
|
||||||
|
forgetAllSessions(request.cookies, user)
|
||||||
forgetAllSessions(user)
|
|
||||||
deleteAllAuthTokensQuery.run({ userId: user.id })
|
deleteAllAuthTokensQuery.run({ userId: user.id })
|
||||||
return new Response(undefined, { status: 204 })
|
return new Response(undefined, { status: 204 })
|
||||||
}
|
}
|
||||||
|
@@ -110,7 +110,7 @@ function verifySession(cookie: Bun.CookieMap): Session | null {
|
|||||||
function extendSession(session: Session): Session {
|
function extendSession(session: Session): Session {
|
||||||
const newExpiryDate = dayjs().add(30, "minutes").valueOf()
|
const newExpiryDate = dayjs().add(30, "minutes").valueOf()
|
||||||
const extendSessionQuery = db.query(
|
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({
|
extendSessionQuery.run({
|
||||||
sessionId: session.id,
|
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")
|
const forgetAllSessionsQuery = db.query("DELETE FROM sessions WHERE user_id = $userId")
|
||||||
forgetAllSessionsQuery.run({ userId: user.id })
|
forgetAllSessionsQuery.run({ userId: user.id })
|
||||||
}
|
}
|
||||||
|
@@ -28,8 +28,8 @@ type QueryKey = ["bookmarks", ...ReadonlyArray<unknown>]
|
|||||||
|
|
||||||
async function fetchApi(route: string, init?: RequestInit): Promise<Response> {
|
async function fetchApi(route: string, init?: RequestInit): Promise<Response> {
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_URL}/api${route}`, {
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/api${route}`, {
|
||||||
...init,
|
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
...init,
|
||||||
})
|
})
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 200:
|
case 200:
|
||||||
|
@@ -11,12 +11,19 @@
|
|||||||
// Import Routes
|
// Import Routes
|
||||||
|
|
||||||
import { Route as rootRoute } from "./__root"
|
import { Route as rootRoute } from "./__root"
|
||||||
|
import { Route as SignupImport } from "./signup"
|
||||||
import { Route as LoginImport } from "./login"
|
import { Route as LoginImport } from "./login"
|
||||||
import { Route as BookmarksImport } from "./bookmarks"
|
import { Route as BookmarksImport } from "./bookmarks"
|
||||||
import { Route as IndexImport } from "./index"
|
import { Route as IndexImport } from "./index"
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
|
const SignupRoute = SignupImport.update({
|
||||||
|
id: "/signup",
|
||||||
|
path: "/signup",
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const LoginRoute = LoginImport.update({
|
const LoginRoute = LoginImport.update({
|
||||||
id: "/login",
|
id: "/login",
|
||||||
path: "/login",
|
path: "/login",
|
||||||
@@ -60,6 +67,13 @@ declare module "@tanstack/react-router" {
|
|||||||
preLoaderRoute: typeof LoginImport
|
preLoaderRoute: typeof LoginImport
|
||||||
parentRoute: typeof rootRoute
|
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
|
"/": typeof IndexRoute
|
||||||
"/bookmarks": typeof BookmarksRoute
|
"/bookmarks": typeof BookmarksRoute
|
||||||
"/login": typeof LoginRoute
|
"/login": typeof LoginRoute
|
||||||
|
"/signup": typeof SignupRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
"/": typeof IndexRoute
|
"/": typeof IndexRoute
|
||||||
"/bookmarks": typeof BookmarksRoute
|
"/bookmarks": typeof BookmarksRoute
|
||||||
"/login": typeof LoginRoute
|
"/login": typeof LoginRoute
|
||||||
|
"/signup": typeof SignupRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
@@ -82,14 +98,15 @@ export interface FileRoutesById {
|
|||||||
"/": typeof IndexRoute
|
"/": typeof IndexRoute
|
||||||
"/bookmarks": typeof BookmarksRoute
|
"/bookmarks": typeof BookmarksRoute
|
||||||
"/login": typeof LoginRoute
|
"/login": typeof LoginRoute
|
||||||
|
"/signup": typeof SignupRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: "/" | "/bookmarks" | "/login"
|
fullPaths: "/" | "/bookmarks" | "/login" | "/signup"
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: "/" | "/bookmarks" | "/login"
|
to: "/" | "/bookmarks" | "/login" | "/signup"
|
||||||
id: "__root__" | "/" | "/bookmarks" | "/login"
|
id: "__root__" | "/" | "/bookmarks" | "/login" | "/signup"
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,12 +114,14 @@ export interface RootRouteChildren {
|
|||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
BookmarksRoute: typeof BookmarksRoute
|
BookmarksRoute: typeof BookmarksRoute
|
||||||
LoginRoute: typeof LoginRoute
|
LoginRoute: typeof LoginRoute
|
||||||
|
SignupRoute: typeof SignupRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
BookmarksRoute: BookmarksRoute,
|
BookmarksRoute: BookmarksRoute,
|
||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
|
SignupRoute: SignupRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const routeTree = rootRoute
|
export const routeTree = rootRoute
|
||||||
@@ -117,7 +136,8 @@ export const routeTree = rootRoute
|
|||||||
"children": [
|
"children": [
|
||||||
"/",
|
"/",
|
||||||
"/bookmarks",
|
"/bookmarks",
|
||||||
"/login"
|
"/login",
|
||||||
|
"/signup"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
@@ -128,6 +148,9 @@ export const routeTree = rootRoute
|
|||||||
},
|
},
|
||||||
"/login": {
|
"/login": {
|
||||||
"filePath": "login.tsx"
|
"filePath": "login.tsx"
|
||||||
|
},
|
||||||
|
"/signup": {
|
||||||
|
"filePath": "signup.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import type { LinkBookmark } from "@markone/core/bookmark"
|
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 clsx from "clsx"
|
||||||
import { useEffect, useId, useState } from "react"
|
import { useEffect, useId, useState } from "react"
|
||||||
import { create } from "zustand"
|
import { create } from "zustand"
|
||||||
@@ -11,6 +11,7 @@ import { Message, MessageVariant } from "~/components/message"
|
|||||||
import { FormField } from "~/components/form-field"
|
import { FormField } from "~/components/form-field"
|
||||||
import { LoadingSpinner } from "~/components/loading-spinner"
|
import { LoadingSpinner } from "~/components/loading-spinner"
|
||||||
import { useMnemonics } from "~/hooks/use-mnemonics"
|
import { useMnemonics } from "~/hooks/use-mnemonics"
|
||||||
|
import { useLogOut } from "~/auth"
|
||||||
|
|
||||||
const LAYOUT_MODE = {
|
const LAYOUT_MODE = {
|
||||||
popup: "popup",
|
popup: "popup",
|
||||||
@@ -593,10 +594,27 @@ function ActionBar() {
|
|||||||
<Button>
|
<Button>
|
||||||
<span className="underline">S</span>EARCH
|
<span className="underline">S</span>EARCH
|
||||||
</Button>
|
</Button>
|
||||||
|
<LogOutButton />
|
||||||
</div>
|
</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")({
|
export const Route = createFileRoute("/bookmarks")({
|
||||||
component: Page,
|
component: Page,
|
||||||
})
|
})
|
||||||
|
@@ -14,7 +14,7 @@ function Index() {
|
|||||||
<p className="pb-4 text-2xl uppercase">BOOKMARK MANAGER</p>
|
<p className="pb-4 text-2xl uppercase">BOOKMARK MANAGER</p>
|
||||||
<div className="flex flex-col text-lg">
|
<div className="flex flex-col text-lg">
|
||||||
<Link href="/login">LOGIN</Link>
|
<Link href="/login">LOGIN</Link>
|
||||||
<Link>SIGN UP</Link>
|
<Link href="/signup">SIGN UP</Link>
|
||||||
<DemoButton />
|
<DemoButton />
|
||||||
</div>
|
</div>
|
||||||
</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 { useMutation } from "@tanstack/react-query"
|
||||||
|
import { fetchApi } from "./api"
|
||||||
|
|
||||||
function useLogin() {
|
function useLogin() {
|
||||||
return useMutation({
|
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
|
<label
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
className={twMerge(
|
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,
|
labelClassName,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
Reference in New Issue
Block a user