aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/app/check-email/page.tsx128
-rw-r--r--apps/web/app/verify-email/page.tsx152
-rw-r--r--apps/web/components/signin/CredentialsForm.tsx28
-rw-r--r--apps/web/server/auth.ts36
4 files changed, 329 insertions, 15 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>
+ );
+}
diff --git a/apps/web/components/signin/CredentialsForm.tsx b/apps/web/components/signin/CredentialsForm.tsx
index 3772db09..05aa1cef 100644
--- a/apps/web/components/signin/CredentialsForm.tsx
+++ b/apps/web/components/signin/CredentialsForm.tsx
@@ -28,9 +28,11 @@ const signInSchema = z.object({
password: z.string(),
});
-const SIGNIN_FAILED = "Incorrect username or password";
+const SIGNIN_FAILED = "Incorrect email or password";
const OAUTH_FAILED = "OAuth login failed: ";
+const VERIFY_EMAIL_ERROR = "Please verify your email address before signing in";
+
function SignIn() {
const [signinError, setSigninError] = useState("");
const router = useRouter();
@@ -68,8 +70,16 @@ function SignIn() {
email: value.email.trim(),
password: value.password,
});
- if (!resp || !resp?.ok) {
- setSigninError(SIGNIN_FAILED);
+ if (!resp || !resp?.ok || resp.error) {
+ if (resp?.error === "CredentialsSignin") {
+ setSigninError(SIGNIN_FAILED);
+ } else if (resp?.error === VERIFY_EMAIL_ERROR) {
+ router.replace(
+ `/check-email?email=${encodeURIComponent(value.email.trim())}`,
+ );
+ } else {
+ setSigninError(resp?.error ?? SIGNIN_FAILED);
+ }
return;
}
router.replace("/");
@@ -149,8 +159,16 @@ function SignUp() {
email: value.email.trim(),
password: value.password,
});
- if (!resp || !resp.ok) {
- setErrorMessage("Hit an unexpected error while signing in");
+ if (!resp || !resp.ok || resp.error) {
+ if (resp?.error === VERIFY_EMAIL_ERROR) {
+ router.replace(
+ `/check-email?email=${encodeURIComponent(value.email.trim())}`,
+ );
+ } else {
+ setErrorMessage(
+ resp?.error ?? "Hit an unexpected error while signing in",
+ );
+ }
return;
}
router.replace("/");
diff --git a/apps/web/server/auth.ts b/apps/web/server/auth.ts
index 3d32f702..e7b5e1cb 100644
--- a/apps/web/server/auth.ts
+++ b/apps/web/server/auth.ts
@@ -1,6 +1,6 @@
import { Adapter, AdapterUser } from "@auth/core/adapters";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
-import { and, count, eq } from "drizzle-orm";
+import { count, eq } from "drizzle-orm";
import NextAuth, {
DefaultSession,
getServerSession,
@@ -169,22 +169,38 @@ export const authOptions: NextAuthOptions = {
newUser: "/signin",
},
callbacks: {
- async signIn({ credentials, profile }) {
+ async signIn({ user: credUser, credentials, profile }) {
+ const email = credUser.email || profile?.email;
+ if (!email) {
+ throw new Error("Provider didn't provide an email during signin");
+ }
+ const user = await db.query.users.findFirst({
+ columns: { emailVerified: true },
+ where: eq(users.email, email),
+ });
+
if (credentials) {
+ if (!user) {
+ throw new Error("Invalid credentials");
+ }
+ if (
+ serverConfig.auth.emailVerificationRequired &&
+ !user.emailVerified
+ ) {
+ throw new Error("Please verify your email address before signing in");
+ }
return true;
}
- if (!profile?.email) {
- throw new Error("No profile");
- }
- const [{ count: userCount }] = await db
- .select({ count: count() })
- .from(users)
- .where(and(eq(users.email, profile.email)));
// If it's a new user and signups are disabled, fail the sign in
- if (userCount === 0 && serverConfig.auth.disableSignups) {
+ if (!user && serverConfig.auth.disableSignups) {
throw new Error("Signups are disabled in server config");
}
+
+ // TODO: We're blindly trusting oauth providers to validate emails
+ // As such, oauth users can sign in even if email verification is enabled.
+ // We might want to change this in the future.
+
return true;
},
async jwt({ token, user }) {