diff options
| author | MohamedBassem <me@mbassem.com> | 2024-02-22 15:32:40 +0000 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-02-22 15:32:40 +0000 |
| commit | 942aac691225f4895c159a0260890ad2c576e0c9 (patch) | |
| tree | 06a055fcd59c2753531f498ab58d0af4c7e8464c /packages/web/app | |
| parent | 08e7cbcfcb5e0b992d10ada324712c224b7a4d07 (diff) | |
| download | karakeep-942aac691225f4895c159a0260890ad2c576e0c9.tar.zst | |
feature: Add support for credentials registration and sign in
Diffstat (limited to 'packages/web/app')
| -rw-r--r-- | packages/web/app/signin/components/CredentialsForm.tsx | 222 | ||||
| -rw-r--r-- | packages/web/app/signin/components/SignInForm.tsx | 35 | ||||
| -rw-r--r-- | packages/web/app/signin/page.tsx | 18 |
3 files changed, 262 insertions, 13 deletions
diff --git a/packages/web/app/signin/components/CredentialsForm.tsx b/packages/web/app/signin/components/CredentialsForm.tsx new file mode 100644 index 00000000..60b61156 --- /dev/null +++ b/packages/web/app/signin/components/CredentialsForm.tsx @@ -0,0 +1,222 @@ +"use client"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { ActionButton } from "@/components/ui/action-button"; +import { zSignUpSchema } from "@/lib/types/api/users"; +import { signIn } from "next-auth/react"; +import { useState } from "react"; +import { api } from "@/lib/trpc"; +import { useRouter } from "next/navigation"; +import { TRPCClientError } from "@trpc/client"; + +const signInSchema = z.object({ + email: z.string().email(), + password: z.string(), +}); + +function SignIn() { + const [signinError, setSigninError] = useState(false); + const router = useRouter(); + const form = useForm<z.infer<typeof signInSchema>>({ + resolver: zodResolver(signInSchema), + }); + + return ( + <Form {...form}> + <form + onSubmit={form.handleSubmit(async (value) => { + const resp = await signIn("credentials", { + redirect: false, + email: value.email, + password: value.password, + }); + if (!resp || !resp?.ok) { + setSigninError(true); + return; + } + router.replace("/"); + })} + > + <div className="flex w-full flex-col space-y-2"> + {signinError && ( + <p className="w-full text-center text-red-500"> + Incorrect username or password + </p> + )} + <FormField + control={form.control} + name="email" + render={({ field }) => { + return ( + <FormItem> + <FormLabel>Email</FormLabel> + <FormControl> + <Input type="text" placeholder="Email" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + ); + }} + /> + <FormField + control={form.control} + name="password" + render={({ field }) => { + return ( + <FormItem> + <FormLabel>Password</FormLabel> + <FormControl> + <Input type="password" placeholder="Password" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + ); + }} + /> + <ActionButton type="submit" loading={false}> + Sign In + </ActionButton> + </div> + </form> + </Form> + ); +} + +function SignUp() { + const form = useForm<z.infer<typeof zSignUpSchema>>({ + resolver: zodResolver(zSignUpSchema), + }); + const [errorMessage, setErrorMessage] = useState(""); + + const router = useRouter(); + + const createUserMutation = api.users.create.useMutation(); + + return ( + <Form {...form}> + <form + onSubmit={form.handleSubmit(async (value) => { + try { + await createUserMutation.mutateAsync(value); + } catch (e) { + if (e instanceof TRPCClientError) { + setErrorMessage(e.message); + } + return; + } + const resp = await signIn("credentials", { + redirect: false, + email: value.email, + password: value.password, + }); + if (!resp || !resp.ok) { + setErrorMessage("Hit an unexpected error while signing in"); + return; + } + router.replace("/"); + })} + > + <div className="flex w-full flex-col space-y-2"> + {errorMessage && ( + <p className="w-full text-center text-red-500">{errorMessage}</p> + )} + <FormField + control={form.control} + name="name" + render={({ field }) => { + return ( + <FormItem> + <FormLabel>Name</FormLabel> + <FormControl> + <Input type="text" placeholder="Name" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + ); + }} + /> + <FormField + control={form.control} + name="email" + render={({ field }) => { + return ( + <FormItem> + <FormLabel>Email</FormLabel> + <FormControl> + <Input type="text" placeholder="Email" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + ); + }} + /> + <FormField + control={form.control} + name="password" + render={({ field }) => { + return ( + <FormItem> + <FormLabel>Password</FormLabel> + <FormControl> + <Input type="password" placeholder="Password" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + ); + }} + /> + <FormField + control={form.control} + name="confirmPassword" + render={({ field }) => { + return ( + <FormItem> + <FormLabel>Confirm Password</FormLabel> + <FormControl> + <Input + type="password" + placeholder="Confirm Password" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + ); + }} + /> + <ActionButton type="submit" loading={false}> + Sign Up + </ActionButton> + </div> + </form> + </Form> + ); +} + +export default function CredentialsForm() { + return ( + <Tabs defaultValue="signin" className="w-full"> + <TabsList className="grid w-full grid-cols-2"> + <TabsTrigger value="signin">Sign In</TabsTrigger> + <TabsTrigger value="signup">Sign Up</TabsTrigger> + </TabsList> + <TabsContent value="signin"> + <SignIn /> + </TabsContent> + <TabsContent value="signup"> + <SignUp /> + </TabsContent> + </Tabs> + ); +} diff --git a/packages/web/app/signin/components/SignInForm.tsx b/packages/web/app/signin/components/SignInForm.tsx index 0b625f1e..986718bf 100644 --- a/packages/web/app/signin/components/SignInForm.tsx +++ b/packages/web/app/signin/components/SignInForm.tsx @@ -1,16 +1,37 @@ import { getProviders } from "next-auth/react"; import SignInProviderButton from "./SignInProviderButton"; +import CredentialsForm from "./CredentialsForm"; export default async function SignInForm() { - const providers = (await getProviders()) ?? []; + const providers = await getProviders(); + let providerValues; + if (providers) { + providerValues = Object.values(providers).filter( + // Credentials are handled manually by the sign in form + (p) => p.id != "credentials", + ); + } return ( - <div> - {Object.values(providers).map((provider) => ( - <div key={provider.name}> - <SignInProviderButton provider={provider} /> - </div> - ))} + <div className="flex flex-col items-center space-y-2"> + <CredentialsForm /> + + {providerValues && ( + <> + <div className="flex w-full items-center"> + <div className="flex-1 grow border-t-2 border-gray-200"></div> + <span className="bg-white px-3 text-gray-500">Or</span> + <div className="flex-1 grow border-t-2 border-gray-200"></div> + </div> + <div className="space-y-2"> + {providerValues.map((provider) => ( + <div key={provider.id}> + <SignInProviderButton provider={provider} /> + </div> + ))} + </div> + </> + )} </div> ); } diff --git a/packages/web/app/signin/page.tsx b/packages/web/app/signin/page.tsx index 1556ff2c..f578a845 100644 --- a/packages/web/app/signin/page.tsx +++ b/packages/web/app/signin/page.tsx @@ -1,17 +1,23 @@ import { PackageOpen } from "lucide-react"; import SignInForm from "./components/SignInForm"; +import { redirect } from "next/dist/client/components/navigation"; +import { getServerAuthSession } from "@/server/auth"; export default async function SignInPage() { - // TODO Add support for email and credential signin form + const session = await getServerAuthSession(); + if (session) { + redirect("/"); + } + return ( - <div className="flex min-h-screen flex-col items-center justify-center"> - <div className="flex space-x-2"> + <div className="grid min-h-screen grid-rows-6 justify-center"> + <div className="row-span-2 flex w-96 items-center justify-center space-x-2"> <span> - <PackageOpen size="30" className="h-full" /> + <PackageOpen size="60" className="" /> </span> - <span className="text-4xl">Hoarder</span> + <p className="text-6xl">Hoarder</p> </div> - <div className="mt-20 flex w-96 flex-col items-center rounded-xl border border-gray-300 p-20"> + <div className="row-span-4 w-96"> <SignInForm /> </div> </div> |
