aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/app
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-07-10 08:35:32 +0000
committerMohamed Bassem <me@mbassem.com>2025-07-10 08:37:44 +0000
commit93049e864ae6d281b60c23dee868bca3f585dd4a (patch)
treed39c0b4221486dbc82461a505f205d162a9e4def /apps/web/app
parentaae3ef17eccf0752edb5ce5638a58444ccb6ce3a (diff)
downloadkarakeep-93049e864ae6d281b60c23dee868bca3f585dd4a.tar.zst
feat: Add support for email verification
Diffstat (limited to 'apps/web/app')
-rw-r--r--apps/web/app/check-email/page.tsx128
-rw-r--r--apps/web/app/verify-email/page.tsx152
2 files changed, 280 insertions, 0 deletions
diff --git a/apps/web/app/check-email/page.tsx b/apps/web/app/check-email/page.tsx
new file mode 100644
index 00000000..96f0afb4
--- /dev/null
+++ b/apps/web/app/check-email/page.tsx
@@ -0,0 +1,128 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { api } from "@/lib/trpc";
+import { Loader2, Mail } from "lucide-react";
+
+export default function CheckEmailPage() {
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const [message, setMessage] = useState("");
+
+ const email = searchParams.get("email");
+
+ const resendEmailMutation = api.users.resendVerificationEmail.useMutation({
+ onSuccess: () => {
+ setMessage(
+ "A new verification email has been sent to your email address.",
+ );
+ },
+ onError: (error) => {
+ setMessage(error.message || "Failed to resend verification email.");
+ },
+ });
+
+ const handleResendEmail = () => {
+ if (email) {
+ resendEmailMutation.mutate({ email });
+ }
+ };
+
+ const handleBackToSignIn = () => {
+ router.push("/signin");
+ };
+
+ if (!email) {
+ return (
+ <div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
+ <Card className="w-full max-w-md">
+ <CardHeader className="text-center">
+ <CardTitle className="text-2xl font-bold">
+ Invalid Request
+ </CardTitle>
+ <CardDescription>
+ No email address provided. Please try signing up again.
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <Button onClick={handleBackToSignIn} className="w-full">
+ Back to Sign In
+ </Button>
+ </CardContent>
+ </Card>
+ </div>
+ );
+ }
+
+ return (
+ <div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
+ <Card className="w-full max-w-md">
+ <CardHeader className="text-center">
+ <CardTitle className="text-2xl font-bold">Check Your Email</CardTitle>
+ <CardDescription>
+ We&apos;ve sent a verification link to your email address
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="flex items-center justify-center">
+ <Mail className="h-12 w-12 text-blue-600" />
+ </div>
+
+ <div className="space-y-2 text-center">
+ <p className="text-sm text-gray-600">
+ We&apos;ve sent a verification email to:
+ </p>
+ <p className="font-medium text-gray-900">{email}</p>
+ <p className="text-sm text-gray-600">
+ Click the link in the email to verify your account and complete
+ your registration.
+ </p>
+ </div>
+
+ {message && (
+ <Alert>
+ <AlertDescription className="text-center">
+ {message}
+ </AlertDescription>
+ </Alert>
+ )}
+
+ <div className="space-y-2">
+ <Button
+ onClick={handleResendEmail}
+ variant="outline"
+ className="w-full"
+ disabled={resendEmailMutation.isPending}
+ >
+ {resendEmailMutation.isPending ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ Sending...
+ </>
+ ) : (
+ "Resend Verification Email"
+ )}
+ </Button>
+ <Button
+ onClick={handleBackToSignIn}
+ variant="ghost"
+ className="w-full"
+ >
+ Back to Sign In
+ </Button>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ );
+}
diff --git a/apps/web/app/verify-email/page.tsx b/apps/web/app/verify-email/page.tsx
new file mode 100644
index 00000000..e8792465
--- /dev/null
+++ b/apps/web/app/verify-email/page.tsx
@@ -0,0 +1,152 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { api } from "@/lib/trpc";
+import { CheckCircle, Loader2, XCircle } from "lucide-react";
+
+export default function VerifyEmailPage() {
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const [status, setStatus] = useState<"loading" | "success" | "error">(
+ "loading",
+ );
+ const [message, setMessage] = useState("");
+
+ const token = searchParams.get("token");
+ const email = searchParams.get("email");
+
+ const verifyEmailMutation = api.users.verifyEmail.useMutation({
+ onSuccess: () => {
+ setStatus("success");
+ setMessage(
+ "Your email has been successfully verified! You can now sign in.",
+ );
+ },
+ onError: (error) => {
+ setStatus("error");
+ setMessage(
+ error.message ||
+ "Failed to verify email. The link may be invalid or expired.",
+ );
+ },
+ });
+
+ const resendEmailMutation = api.users.resendVerificationEmail.useMutation({
+ onSuccess: () => {
+ setMessage(
+ "A new verification email has been sent to your email address.",
+ );
+ },
+ onError: (error) => {
+ setMessage(error.message || "Failed to resend verification email.");
+ },
+ });
+
+ useEffect(() => {
+ if (token && email) {
+ verifyEmailMutation.mutate({ token, email });
+ } else {
+ setStatus("error");
+ setMessage("Invalid verification link. Missing token or email.");
+ }
+ }, [token, email]);
+
+ const handleResendEmail = () => {
+ if (email) {
+ resendEmailMutation.mutate({ email });
+ }
+ };
+
+ const handleSignIn = () => {
+ router.push("/signin");
+ };
+
+ return (
+ <div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
+ <Card className="w-full max-w-md">
+ <CardHeader className="text-center">
+ <CardTitle className="text-2xl font-bold">
+ Email Verification
+ </CardTitle>
+ <CardDescription>
+ {status === "loading" && "Verifying your email address..."}
+ {status === "success" && "Email verified successfully!"}
+ {status === "error" && "Verification failed"}
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {status === "loading" && (
+ <div className="flex items-center justify-center">
+ <Loader2 className="h-8 w-8 animate-spin text-blue-600" />
+ </div>
+ )}
+
+ {status === "success" && (
+ <>
+ <div className="flex items-center justify-center">
+ <CheckCircle className="h-12 w-12 text-green-600" />
+ </div>
+ <Alert>
+ <AlertDescription className="text-center">
+ {message}
+ </AlertDescription>
+ </Alert>
+ <Button onClick={handleSignIn} className="w-full">
+ Sign In
+ </Button>
+ </>
+ )}
+
+ {status === "error" && (
+ <>
+ <div className="flex items-center justify-center">
+ <XCircle className="h-12 w-12 text-red-600" />
+ </div>
+ <Alert variant="destructive">
+ <AlertDescription className="text-center">
+ {message}
+ </AlertDescription>
+ </Alert>
+ {email && (
+ <div className="space-y-2">
+ <Button
+ onClick={handleResendEmail}
+ variant="outline"
+ className="w-full"
+ disabled={resendEmailMutation.isPending}
+ >
+ {resendEmailMutation.isPending ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ Sending...
+ </>
+ ) : (
+ "Resend Verification Email"
+ )}
+ </Button>
+ <Button
+ onClick={handleSignIn}
+ variant="ghost"
+ className="w-full"
+ >
+ Back to Sign In
+ </Button>
+ </div>
+ )}
+ </>
+ )}
+ </CardContent>
+ </Card>
+ </div>
+ );
+}