-
Or log in with
- {/* OAUTH */}
+
Or sign up with
- {/* Google */}
-
+ {/* Google OAuth button or additional providers */}
+
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index 86c19f8..6be9eda 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -3,6 +3,8 @@ import { Open_Sans, Roboto_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
+import { SessionProvider } from "@/context/SessionContext";
+
const openSans = Open_Sans({
subsets: ["latin"],
display: "swap",
@@ -33,13 +35,15 @@ export default function RootLayout({
return (
-
-
-
-
-
+
+
+
+
+
+
+
);
}
diff --git a/frontend/components/SessionProviderClient.tsx b/frontend/components/SessionProviderClient.tsx
new file mode 100644
index 0000000..9825d39
--- /dev/null
+++ b/frontend/components/SessionProviderClient.tsx
@@ -0,0 +1,12 @@
+"use client";
+
+import React, { ReactNode } from "react";
+import { SessionProvider } from "next-auth/react";
+
+interface Props {
+ children: ReactNode;
+}
+
+export function SessesionProviderClient(props: Props) {
+ return
{props.children};
+}
diff --git a/frontend/components/ui/form.tsx b/frontend/components/ui/form.tsx
new file mode 100644
index 0000000..b6daa65
--- /dev/null
+++ b/frontend/components/ui/form.tsx
@@ -0,0 +1,178 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { Slot } from "@radix-ui/react-slot"
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext,
+} from "react-hook-form"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath
= FieldPath
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState, formState } = useFormContext()
+
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ }
+}
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue
+)
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId()
+
+ return (
+
+
+
+ )
+})
+FormItem.displayName = "FormItem"
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField()
+
+ return (
+
+ )
+})
+FormLabel.displayName = "FormLabel"
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+
+ )
+})
+FormControl.displayName = "FormControl"
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField()
+
+ return (
+
+ )
+})
+FormDescription.displayName = "FormDescription"
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message) : children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+
+ {body}
+
+ )
+})
+FormMessage.displayName = "FormMessage"
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/frontend/context/SessionContext.tsx b/frontend/context/SessionContext.tsx
new file mode 100644
index 0000000..1f1ba21
--- /dev/null
+++ b/frontend/context/SessionContext.tsx
@@ -0,0 +1,56 @@
+"use client";
+
+import React, { createContext, useContext, useState, useEffect, ReactNode } from "react";
+
+interface SessionContextType {
+ token: string | null;
+ user: any | null;
+ setToken: (token: string | null) => void;
+ setUser: (user: any | null) => void;
+}
+
+const SessionContext = createContext(undefined);
+
+interface SessionProviderProps {
+ children: ReactNode;
+}
+
+export function SessionProvider({ children }: SessionProviderProps) {
+ const [token, setTokenState] = useState(null);
+ const [user, setUserState] = useState(null);
+
+ const setToken = (newToken: string | null) => {
+ if (newToken) {
+ localStorage.setItem("token", newToken);
+ } else {
+ localStorage.removeItem("token");
+ }
+ setTokenState(newToken);
+ };
+
+ const setUser = (newUser: any | null) => {
+ if (newUser) {
+ localStorage.setItem("user", JSON.stringify(newUser));
+ } else {
+ localStorage.removeItem("user");
+ }
+ setUserState(newUser);
+ };
+
+ useEffect(() => {
+ const storedToken = localStorage.getItem("token");
+ const storedUser = localStorage.getItem("user");
+ if (storedToken) {
+ setTokenState(storedToken);
+ }
+ if (storedUser) {
+ try {
+ setUserState(JSON.parse(storedUser));
+ } catch (error) {
+ console.error("Failed to parse stored user.", error);
+ }
+ }
+ }, []);
+
+ return {children};
+}
diff --git a/frontend/hooks/useSession.tsx b/frontend/hooks/useSession.tsx
new file mode 100644
index 0000000..f05d62c
--- /dev/null
+++ b/frontend/hooks/useSession.tsx
@@ -0,0 +1,12 @@
+"use client";
+
+import { SessionContext } from "next-auth/react";
+import { useContext } from "react";
+
+export function useSession() {
+ const context = useContext(SessionContext);
+ if (!context) {
+ throw new Error("useSession must be used within a SessionProvider");
+ }
+ return context;
+}
diff --git a/frontend/package.json b/frontend/package.json
index 50ec67f..6797fb8 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -9,6 +9,7 @@
"lint": "next lint"
},
"dependencies": {
+ "@hookform/resolvers": "^4.0.0",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.6",
@@ -23,6 +24,7 @@
"clsx": "^2.1.1",
"lucide-react": "^0.475.0",
"next": "15.1.0",
+ "next-auth": "^4.24.11",
"next-themes": "^0.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 7b5ccec..5ab3af8 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
+ '@hookform/resolvers':
+ specifier: ^4.0.0
+ version: 4.0.0(react-hook-form@7.54.2(react@19.0.0))
'@radix-ui/react-avatar':
specifier: ^1.1.3
version: 1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -50,6 +53,9 @@ importers:
next:
specifier: 15.1.0
version: 15.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ next-auth:
+ specifier: ^4.24.11
+ version: 4.24.11(next@15.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next-themes:
specifier: ^0.4.4
version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -106,6 +112,10 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
+ '@babel/runtime@7.26.7':
+ resolution: {integrity: sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==}
+ engines: {node: '>=6.9.0'}
+
'@emnapi/runtime@1.3.1':
resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
@@ -162,6 +172,11 @@ packages:
'@floating-ui/utils@0.2.9':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
+ '@hookform/resolvers@4.0.0':
+ resolution: {integrity: sha512-93ZueVlTaeMF0pRbrLbcnzrxeb2mGA/xyO3RgfrsKs5OCtcfjoWcdjBJm+N7096Jfg/JYlGPjuyQCgqVEodSTg==}
+ peerDependencies:
+ react-hook-form: ^7.0.0
+
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -379,6 +394,9 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'}
+ '@panva/hkdf@1.2.1':
+ resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
+
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -1016,6 +1034,10 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ cookie@0.7.2:
+ resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
+ engines: {node: '>= 0.6'}
+
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -1538,6 +1560,9 @@ packages:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true
+ jose@4.15.9:
+ resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==}
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -1603,6 +1628,10 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+ lru-cache@6.0.0:
+ resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
+ engines: {node: '>=10'}
+
lucide-react@0.475.0:
resolution: {integrity: sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==}
peerDependencies:
@@ -1648,6 +1677,20 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ next-auth@4.24.11:
+ resolution: {integrity: sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==}
+ peerDependencies:
+ '@auth/core': 0.34.2
+ next: ^12.2.5 || ^13 || ^14 || ^15
+ nodemailer: ^6.6.5
+ react: ^17.0.2 || ^18 || ^19
+ react-dom: ^17.0.2 || ^18 || ^19
+ peerDependenciesMeta:
+ '@auth/core':
+ optional: true
+ nodemailer:
+ optional: true
+
next-themes@0.4.4:
resolution: {integrity: sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==}
peerDependencies:
@@ -1679,10 +1722,17 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
+ oauth@0.9.15:
+ resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==}
+
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
+ object-hash@2.2.0:
+ resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
+ engines: {node: '>= 6'}
+
object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
@@ -1715,6 +1765,13 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'}
+ oidc-token-hash@5.0.3:
+ resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==}
+ engines: {node: ^10.13.0 || >=12.0.0}
+
+ openid-client@5.7.1:
+ resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
+
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -1821,10 +1878,21 @@ packages:
resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==}
engines: {node: ^10 || ^12 || >=14}
+ preact-render-to-string@5.2.6:
+ resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
+ peerDependencies:
+ preact: '>=10'
+
+ preact@10.25.4:
+ resolution: {integrity: sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==}
+
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
+ pretty-format@3.8.0:
+ resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
+
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
@@ -1894,6 +1962,9 @@ packages:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
+ regenerator-runtime@0.14.1:
+ resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
+
regexp.prototype.flags@1.5.4:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
@@ -2177,6 +2248,10 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+ uuid@8.3.2:
+ resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
+ hasBin: true
+
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -2210,6 +2285,9 @@ packages:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
+ yallist@4.0.0:
+ resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
+
yaml@2.7.0:
resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==}
engines: {node: '>= 14'}
@@ -2226,6 +2304,10 @@ snapshots:
'@alloc/quick-lru@5.2.0': {}
+ '@babel/runtime@7.26.7':
+ dependencies:
+ regenerator-runtime: 0.14.1
+
'@emnapi/runtime@1.3.1':
dependencies:
tslib: 2.8.1
@@ -2294,6 +2376,10 @@ snapshots:
'@floating-ui/utils@0.2.9': {}
+ '@hookform/resolvers@4.0.0(react-hook-form@7.54.2(react@19.0.0))':
+ dependencies:
+ react-hook-form: 7.54.2(react@19.0.0)
+
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
@@ -2452,6 +2538,8 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {}
+ '@panva/hkdf@1.2.1': {}
+
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -3117,6 +3205,8 @@ snapshots:
concat-map@0.0.1: {}
+ cookie@0.7.2: {}
+
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -3803,6 +3893,8 @@ snapshots:
jiti@1.21.7: {}
+ jose@4.15.9: {}
+
js-tokens@4.0.0: {}
js-yaml@4.1.0:
@@ -3861,6 +3953,10 @@ snapshots:
lru-cache@10.4.3: {}
+ lru-cache@6.0.0:
+ dependencies:
+ yallist: 4.0.0
+
lucide-react@0.475.0(react@19.0.0):
dependencies:
react: 19.0.0
@@ -3898,6 +3994,21 @@ snapshots:
natural-compare@1.4.0: {}
+ next-auth@4.24.11(next@15.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ dependencies:
+ '@babel/runtime': 7.26.7
+ '@panva/hkdf': 1.2.1
+ cookie: 0.7.2
+ jose: 4.15.9
+ next: 15.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ oauth: 0.9.15
+ openid-client: 5.7.1
+ preact: 10.25.4
+ preact-render-to-string: 5.2.6(preact@10.25.4)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ uuid: 8.3.2
+
next-themes@0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
react: 19.0.0
@@ -3930,8 +4041,12 @@ snapshots:
normalize-path@3.0.0: {}
+ oauth@0.9.15: {}
+
object-assign@4.1.1: {}
+ object-hash@2.2.0: {}
+
object-hash@3.0.0: {}
object-inspect@1.13.4: {}
@@ -3973,6 +4088,15 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
+ oidc-token-hash@5.0.3: {}
+
+ openid-client@5.7.1:
+ dependencies:
+ jose: 4.15.9
+ lru-cache: 6.0.0
+ object-hash: 2.2.0
+ oidc-token-hash: 5.0.3
+
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -4071,8 +4195,17 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
+ preact-render-to-string@5.2.6(preact@10.25.4):
+ dependencies:
+ preact: 10.25.4
+ pretty-format: 3.8.0
+
+ preact@10.25.4: {}
+
prelude-ls@1.2.1: {}
+ pretty-format@3.8.0: {}
+
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
@@ -4142,6 +4275,8 @@ snapshots:
get-proto: 1.0.1
which-builtin-type: 1.2.1
+ regenerator-runtime@0.14.1: {}
+
regexp.prototype.flags@1.5.4:
dependencies:
call-bind: 1.0.8
@@ -4520,6 +4655,8 @@ snapshots:
util-deprecate@1.0.2: {}
+ uuid@8.3.2: {}
+
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0
@@ -4578,6 +4715,8 @@ snapshots:
string-width: 5.1.2
strip-ansi: 7.1.0
+ yallist@4.0.0: {}
+
yaml@2.7.0: {}
yocto-queue@0.1.0: {}
diff --git a/frontend/schema/authSchema.ts b/frontend/schema/authSchema.ts
new file mode 100644
index 0000000..8b60871
--- /dev/null
+++ b/frontend/schema/authSchema.ts
@@ -0,0 +1,29 @@
+import { z } from "zod";
+
+export const signInSchema = z.object({
+ email: z
+ .string({ required_error: "Email is required" })
+ .min(1, { message: "Email is required" })
+ .email({ message: "Invalid email address" }),
+ password: z
+ .string({ required_error: "Password is required" })
+ .min(6, { message: "Password must be at least 6 characters long" }),
+});
+
+export const signUpSchema = z
+ .object({
+ email: z
+ .string({ required_error: "Email is required" })
+ .min(1, { message: "Email is required" })
+ .email({ message: "Invalid email address" }),
+ password: z
+ .string({ required_error: "Password is required" })
+ .min(6, { message: "Password must be at least 6 characters" }),
+ confirmPassword: z
+ .string({ required_error: "Confirm your password" })
+ .min(6, { message: "Confirm Password must be at least 6 characters" }),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ message: "Passwords do not match",
+ path: ["confirmPassword"],
+ });