From 140311d7419fa2192e5149df8f589c3c3733a399 Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Thu, 10 Jul 2025 20:50:19 +0000
Subject: feat: Support forget and reset password

---
 apps/web/components/signin/ResetPasswordForm.tsx | 181 +++++++++++++++++++++++
 1 file changed, 181 insertions(+)
 create mode 100644 apps/web/components/signin/ResetPasswordForm.tsx

(limited to 'apps/web/components/signin/ResetPasswordForm.tsx')

diff --git a/apps/web/components/signin/ResetPasswordForm.tsx b/apps/web/components/signin/ResetPasswordForm.tsx
new file mode 100644
index 00000000..d4d8a285
--- /dev/null
+++ b/apps/web/components/signin/ResetPasswordForm.tsx
@@ -0,0 +1,181 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { ActionButton } from "@/components/ui/action-button";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from "@/components/ui/card";
+import {
+  Form,
+  FormControl,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { api } from "@/lib/trpc";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { TRPCClientError } from "@trpc/client";
+import { AlertCircle, CheckCircle } from "lucide-react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { zResetPasswordSchema } from "@karakeep/shared/types/users";
+
+const resetPasswordSchema = z
+  .object({
+    confirmPassword: z.string(),
+  })
+  .merge(zResetPasswordSchema.pick({ newPassword: true }))
+  .refine((data) => data.newPassword === data.confirmPassword, {
+    message: "Passwords don't match",
+    path: ["confirmPassword"],
+  });
+
+interface ResetPasswordFormProps {
+  token: string;
+}
+
+export default function ResetPasswordForm({ token }: ResetPasswordFormProps) {
+  const [isSuccess, setIsSuccess] = useState(false);
+  const [errorMessage, setErrorMessage] = useState("");
+  const router = useRouter();
+
+  const form = useForm<z.infer<typeof resetPasswordSchema>>({
+    resolver: zodResolver(resetPasswordSchema),
+  });
+
+  const resetPasswordMutation = api.users.resetPassword.useMutation();
+
+  const onSubmit = async (values: z.infer<typeof resetPasswordSchema>) => {
+    try {
+      setErrorMessage("");
+      await resetPasswordMutation.mutateAsync({
+        token,
+        newPassword: values.newPassword,
+      });
+      setIsSuccess(true);
+    } catch (error) {
+      if (error instanceof TRPCClientError) {
+        setErrorMessage(error.message);
+      } else {
+        setErrorMessage("An unexpected error occurred. Please try again.");
+      }
+    }
+  };
+
+  return (
+    <Card className="w-full">
+      <CardHeader className="text-center">
+        <CardTitle className="text-2xl font-bold">
+          {isSuccess ? "Password reset successful" : "Reset your password"}
+        </CardTitle>
+        <CardDescription>
+          {isSuccess
+            ? "Your password has been successfully reset. You can now sign in with your new password."
+            : "Enter your new password below."}
+        </CardDescription>
+      </CardHeader>
+      <CardContent className="space-y-6">
+        {isSuccess ? (
+          <>
+            <div className="flex items-center justify-center">
+              <CheckCircle className="h-12 w-12 text-green-600" />
+            </div>
+            <Alert>
+              <AlertDescription className="text-center">
+                Your password has been successfully reset. You can now sign in
+                with your new password.
+              </AlertDescription>
+            </Alert>
+            <ActionButton
+              loading={false}
+              onClick={() => router.push("/signin")}
+              className="w-full"
+            >
+              Go to Sign In
+            </ActionButton>
+          </>
+        ) : (
+          <>
+            <Form {...form}>
+              <form
+                onSubmit={form.handleSubmit(onSubmit)}
+                className="space-y-4"
+              >
+                {errorMessage && (
+                  <Alert variant="destructive">
+                    <AlertCircle className="h-4 w-4" />
+                    <AlertDescription>{errorMessage}</AlertDescription>
+                  </Alert>
+                )}
+
+                <FormField
+                  control={form.control}
+                  name="newPassword"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>New Password</FormLabel>
+                      <FormControl>
+                        <Input
+                          type="password"
+                          placeholder="Enter your new password"
+                          {...field}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name="confirmPassword"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>Confirm New Password</FormLabel>
+                      <FormControl>
+                        <Input
+                          type="password"
+                          placeholder="Confirm your new password"
+                          {...field}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <ActionButton
+                  type="submit"
+                  loading={form.formState.isSubmitting}
+                  className="w-full"
+                >
+                  Reset Password
+                </ActionButton>
+              </form>
+            </Form>
+
+            <div className="text-center">
+              <ActionButton
+                variant="ghost"
+                loading={false}
+                onClick={() => router.push("/signin")}
+                className="w-full"
+              >
+                Back to Sign In
+              </ActionButton>
+            </div>
+          </>
+        )}
+      </CardContent>
+    </Card>
+  );
+}
-- 
cgit v1.2.3-70-g09d2

