add user authentication
This commit is contained in:
33
web/bun.lock
33
web/bun.lock
@@ -5,9 +5,14 @@
|
||||
"": {
|
||||
"name": "web",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"axios": "^1.13.2",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.19",
|
||||
"next": "16.0.10",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"react-hook-form": "^7.68.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
@@ -213,6 +218,10 @@
|
||||
|
||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
@@ -317,10 +326,14 @@
|
||||
|
||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="],
|
||||
|
||||
"axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
|
||||
@@ -349,10 +362,14 @@
|
||||
|
||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
@@ -369,6 +386,8 @@
|
||||
|
||||
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
|
||||
|
||||
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
@@ -377,6 +396,8 @@
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||
@@ -463,8 +484,12 @@
|
||||
|
||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
|
||||
@@ -643,6 +668,10 @@
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
@@ -703,6 +732,8 @@
|
||||
|
||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
@@ -711,6 +742,8 @@
|
||||
|
||||
"react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="],
|
||||
|
||||
"react-hook-form": ["react-hook-form@7.68.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q=="],
|
||||
|
||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
@@ -9,9 +9,14 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"axios": "^1.13.2",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.19",
|
||||
"next": "16.0.10",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1"
|
||||
"react-dom": "19.2.1",
|
||||
"react-hook-form": "^7.68.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
||||
19
web/src/api/auth/useLoginMutation.ts
Normal file
19
web/src/api/auth/useLoginMutation.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { $axios } from "@/lib/axios";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
export type LoginData = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export const useLoginMutation = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (data: LoginData) => {
|
||||
const resp = await $axios.post<{ access_token: string }>(
|
||||
"/auth/login",
|
||||
data,
|
||||
);
|
||||
return resp.data;
|
||||
},
|
||||
});
|
||||
};
|
||||
19
web/src/api/auth/useRegisterMutation.ts
Normal file
19
web/src/api/auth/useRegisterMutation.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { $axios } from "@/lib/axios";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
export type RegisterData = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export const useRegisterMutation = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (data: RegisterData) => {
|
||||
const resp = await $axios.post<{ access_token: string }>(
|
||||
"/auth/register",
|
||||
data,
|
||||
);
|
||||
return resp.data;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { Providers } from "@/app/providers";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "archive.local",
|
||||
@@ -9,11 +11,13 @@ export const metadata: Metadata = {
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className="h-full">
|
||||
<body className="h-full flex flex-col">{children}</body>
|
||||
</html>
|
||||
<Providers>
|
||||
<html lang="en" className="h-full">
|
||||
<body className="h-full flex flex-col">{children}</body>
|
||||
</html>
|
||||
</Providers>
|
||||
);
|
||||
}
|
||||
|
||||
63
web/src/app/login/LoginForm.tsx
Normal file
63
web/src/app/login/LoginForm.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { LoginData, useLoginMutation } from "@/api/auth/useLoginMutation";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
import dayjs from "dayjs";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export const LoginForm = () => {
|
||||
const router = useRouter();
|
||||
const mutation = useLoginMutation();
|
||||
|
||||
const form = useForm<LoginData>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
mutation.mutate(data, {
|
||||
onSuccess(resp) {
|
||||
const expDate = dayjs().add(7, "days").toString();
|
||||
document.cookie = `access_token=${resp.access_token}; SameSite=None; Secure; Expires=${expDate}; Path=/`;
|
||||
router.push("/");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
className="max-w-[500px] w-full bg-white rounded-lg p-4 shadow-2xl"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="johndoe@gmail.com"
|
||||
label="Email"
|
||||
{...form.register("email", { required: true })}
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="******"
|
||||
label="Password"
|
||||
{...form.register("password", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full mt-6">
|
||||
Sign in
|
||||
</Button>
|
||||
<p className="mt-2 text-sm">
|
||||
Don't have an account? Create one here{" "}
|
||||
<Link href="/register" className="underline hover:no-underline">
|
||||
here
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
18
web/src/app/login/page.tsx
Normal file
18
web/src/app/login/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { LoginForm } from "@/app/login/LoginForm";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function Login() {
|
||||
const c = await cookies();
|
||||
const token = c.get("access_token");
|
||||
if (token) {
|
||||
throw redirect("/");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col justify-center items-center bg-neutral-100">
|
||||
<h2 className="mb-10 text-5xl font-medium">archive.local</h2>
|
||||
<LoginForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,14 @@
|
||||
export default function Home() {
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function Home() {
|
||||
const c = await cookies();
|
||||
const token = c.get("access_token");
|
||||
|
||||
if (!token) {
|
||||
throw redirect("/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>archive.local</h1>
|
||||
|
||||
21
web/src/app/providers.tsx
Normal file
21
web/src/app/providers.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactNode, useState } from "react";
|
||||
|
||||
export const Providers = ({ children }: { children: ReactNode }) => {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
66
web/src/app/register/RegisterForm.tsx
Normal file
66
web/src/app/register/RegisterForm.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
RegisterData,
|
||||
useRegisterMutation,
|
||||
} from "@/api/auth/useRegisterMutation";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
import dayjs from "dayjs";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export const RegisterForm = () => {
|
||||
const router = useRouter();
|
||||
const mutation = useRegisterMutation();
|
||||
|
||||
const form = useForm<RegisterData>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
mutation.mutate(data, {
|
||||
onSuccess(resp) {
|
||||
const expDate = dayjs().add(7, "days").toString();
|
||||
document.cookie = `access_token=${resp.access_token}; SameSite=None; Secure; Expires=${expDate}; Path=/`;
|
||||
router.push("/");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
className="max-w-[500px] w-full bg-white rounded-lg p-4 shadow-2xl"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="johndoe@gmail.com"
|
||||
label="Email"
|
||||
{...form.register("email", { required: true })}
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="******"
|
||||
label="Password"
|
||||
{...form.register("password", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full mt-6">
|
||||
Sign up
|
||||
</Button>
|
||||
<p className="mt-2 text-sm">
|
||||
Already have an account? Login{" "}
|
||||
<Link href="/login" className="underline hover:no-underline">
|
||||
here
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
18
web/src/app/register/page.tsx
Normal file
18
web/src/app/register/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { RegisterForm } from "@/app/register/RegisterForm";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function Register() {
|
||||
const c = await cookies();
|
||||
const token = c.get("access_token");
|
||||
if (token) {
|
||||
throw redirect("/");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col justify-center items-center bg-neutral-100">
|
||||
<h2 className="mb-10 text-5xl font-medium">archive.local</h2>
|
||||
<RegisterForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
web/src/components/ui/Button.tsx
Normal file
30
web/src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { clsx } from "clsx";
|
||||
import { forwardRef, JSX } from "react";
|
||||
|
||||
const sizeClasses = {
|
||||
medium: "py-2 px-4",
|
||||
};
|
||||
|
||||
export type ButtonProps = JSX.IntrinsicElements["button"] & {
|
||||
size?: keyof typeof sizeClasses;
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ size = "medium", className, children, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
"border border-neutral-800 rounded-md not-disabled:cursor-pointer not-disabled:hover:bg-neutral-800 not-disabled:hover:text-white/90 disabled:cursor-not-allowed active:scale-95 transition duration-200",
|
||||
className,
|
||||
sizeClasses[size],
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = "Button";
|
||||
26
web/src/components/ui/Input.tsx
Normal file
26
web/src/components/ui/Input.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import clsx from "clsx";
|
||||
import { forwardRef, JSX, useId } from "react";
|
||||
|
||||
export type InputProps = JSX.IntrinsicElements["input"] & {
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ label, className, ...props }, ref) => {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<div className={clsx("flex flex-col gap-1", className)}>
|
||||
{label && <label htmlFor={id}>{label}</label>}
|
||||
<input
|
||||
ref={ref}
|
||||
id={id}
|
||||
className="py-1 px-2 rounded-md border border-neutral-800"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Input.displayName = "Input";
|
||||
19
web/src/lib/axios.ts
Normal file
19
web/src/lib/axios.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import axios from "axios";
|
||||
|
||||
export const $axios = axios.create({
|
||||
baseURL: "http://localhost:5000",
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
$axios.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user