- Welcome Back 👋
+ Login
diff --git a/src/app/api/auth/email-verify/route.ts b/src/app/api/auth/email-verify/route.ts
deleted file mode 100644
index 1a9fac4..0000000
--- a/src/app/api/auth/email-verify/route.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
-import { cookies } from "next/headers";
-import { type NextRequest } from "next/server";
-import { isWithinExpirationDate } from "oslo";
-import db from "~/lib/db";
-import { lucia } from "~/lib/lucia";
-
-export const GET = async (req: NextRequest) => {
- const url = new URL(req.url);
- const verificationToken = url.searchParams.get("token");
-
- if (!verificationToken) {
- return new Response("Invalid token", {
- status: 400,
- });
- }
-
- try {
- const [token] = await db.$transaction([
- db.emailVerificationToken.findFirst({
- where: {
- id: verificationToken!,
- },
- }),
- db.emailVerificationToken.delete({
- where: {
- id: verificationToken!,
- },
- }),
- ]);
-
- if (!token || !isWithinExpirationDate(token.expiresAt)) {
- return new Response("Token expired", {
- status: 400,
- });
- }
- const user = await db.user.findFirst({
- where: {
- id: token.userId,
- },
- });
- if (!user || user.email !== token.email) {
- return new Response("Invalid token", {
- status: 400,
- });
- }
-
- await lucia.invalidateUserSessions(user.id);
- await db.user.upsert({
- where: {
- id: user.id,
- emailVerified: false,
- },
- update: {
- emailVerified: true,
- },
- create: {},
- });
-
- const session = await lucia.createSession(user.id, {});
- const sessionCookie = lucia.createSessionCookie(session.id);
- cookies().set(
- sessionCookie.name,
- sessionCookie.value,
- sessionCookie.attributes
- );
- return new Response(null, {
- status: 302,
- headers: {
- Location: "/dashboard",
- },
- });
- } catch (error) {
- console.log(error);
- if (error instanceof PrismaClientKnownRequestError) {
- if (
- error.code === "P2025" ||
- error.meta?.cause === "Record to delete does not exist."
- ) {
- return new Response("Token already used", {
- status: 404,
- });
- }
- }
-
- return new Response("Something went wrong.", {
- status: 500,
- });
- }
-};
diff --git a/src/app/api/auth/login/github/callback/route.ts b/src/app/api/auth/login/github/callback/route.ts
index a032c81..eb3e143 100644
--- a/src/app/api/auth/login/github/callback/route.ts
+++ b/src/app/api/auth/login/github/callback/route.ts
@@ -1,9 +1,10 @@
import { OAuth2RequestError } from "arctic";
import { cookies } from "next/headers";
import type { NextRequest } from "next/server";
+import { sendWelcomeEmail } from "~/actions/mail";
import db from "~/lib/db";
-import { github, lucia } from "~/lib/lucia";
-import { sendWelcomeEmail } from "~/server/mail";
+import { github } from "~/lib/github";
+import { lucia } from "~/lib/lucia";
export const GET = async (request: NextRequest) => {
const url = new URL(request.url);
diff --git a/src/app/api/auth/login/github/route.ts b/src/app/api/auth/login/github/route.ts
index 44c2f99..d88026a 100644
--- a/src/app/api/auth/login/github/route.ts
+++ b/src/app/api/auth/login/github/route.ts
@@ -1,6 +1,6 @@
import { generateState } from "arctic";
import { cookies } from "next/headers";
-import { github } from "~/lib/lucia";
+import { github } from "~/lib/github";
export const GET = async () => {
const state = generateState();
diff --git a/src/app/api/auth/login/magic-link/route.ts b/src/app/api/auth/login/send-otp/route.ts
similarity index 61%
rename from src/app/api/auth/login/magic-link/route.ts
rename to src/app/api/auth/login/send-otp/route.ts
index 01c7b8c..7123d5b 100644
--- a/src/app/api/auth/login/magic-link/route.ts
+++ b/src/app/api/auth/login/send-otp/route.ts
@@ -1,7 +1,7 @@
+import { generateEmailVerificationCode } from "~/actions/auth";
+import { sendOTP } from "~/actions/mail";
import { siteUrl } from "~/config/site";
import db from "~/lib/db";
-import { createEmailVerificationToken } from "~/server/auth";
-import { sendVerificationEmail } from "~/server/mail";
export const POST = async (req: Request) => {
const body = await req.json();
@@ -18,15 +18,10 @@ export const POST = async (req: Request) => {
},
});
- const verificationToken = await createEmailVerificationToken(
- user.id,
- body.email
- );
- const verificationUrl =
- siteUrl + "/api/auth/email-verify?token=" + verificationToken;
- await sendVerificationEmail({
+ const otp = await generateEmailVerificationCode(user.id, body.email);
+ await sendOTP({
toMail: body.email,
- verificationUrl,
+ code: otp,
userName: user.name?.split(" ")[0] || "",
});
diff --git a/src/app/api/auth/login/verify-otp/route.ts b/src/app/api/auth/login/verify-otp/route.ts
new file mode 100644
index 0000000..d36f269
--- /dev/null
+++ b/src/app/api/auth/login/verify-otp/route.ts
@@ -0,0 +1,68 @@
+import { cookies } from "next/headers";
+import { verifyVerificationCode } from "~/actions/auth";
+import db from "~/lib/db";
+import { lucia } from "~/lib/lucia";
+
+export const POST = async (req: Request) => {
+ const body = await req.json();
+
+ try {
+ const user = await db.user.findFirst({
+ where: {
+ email: body.email,
+ },
+ select: {
+ id: true,
+ email: true,
+ emailVerified: true,
+ },
+ });
+
+ if (!user) {
+ return new Response(null, {
+ status: 400,
+ });
+ }
+
+ const isValid = await verifyVerificationCode(
+ { id: user.id, email: body.email },
+ body.code
+ );
+
+ if (!isValid) {
+ return new Response(null, {
+ status: 400,
+ });
+ }
+
+ await lucia.invalidateUserSessions(user.id);
+
+ if (!user.emailVerified) {
+ await db.user.update({
+ where: {
+ id: user.id,
+ },
+ data: {
+ emailVerified: true,
+ },
+ });
+ }
+
+ const session = await lucia.createSession(user.id, {});
+ const sessionCookie = lucia.createSessionCookie(session.id);
+ cookies().set(
+ sessionCookie.name,
+ sessionCookie.value,
+ sessionCookie.attributes
+ );
+ return new Response(null, {
+ status: 200,
+ });
+ } catch (error) {
+ console.log(error);
+
+ return new Response(null, {
+ status: 500,
+ });
+ }
+};
diff --git a/src/app/api/og/route.ts b/src/app/api/og/route.ts
index 1ec9097..4a35261 100644
--- a/src/app/api/og/route.ts
+++ b/src/app/api/og/route.ts
@@ -23,7 +23,6 @@ export async function GET(request: Request) {
height: 630,
}
);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
console.log(`${e.message}`);
return new Response(`Failed to generate the image`, {
diff --git a/src/app/api/stripe/route.ts b/src/app/api/stripe/route.ts
index f9eb5df..3fe440c 100644
--- a/src/app/api/stripe/route.ts
+++ b/src/app/api/stripe/route.ts
@@ -1,10 +1,10 @@
import { type NextRequest } from "next/server";
import { z } from "zod";
+import { validateRequest } from "~/actions/auth";
import { siteConfig } from "~/config/site";
import { proPlan } from "~/config/subscription";
import { stripe } from "~/lib/stripe";
import { getUserSubscriptionPlan } from "~/lib/subscription";
-import { validateRequest } from "~/server/auth";
export async function GET(req: NextRequest) {
const locale = req.cookies.get("Next-Locale")?.value || "en";
diff --git a/src/app/api/uploadthing/core.ts b/src/app/api/uploadthing/core.ts
index b6106f6..2eb6926 100644
--- a/src/app/api/uploadthing/core.ts
+++ b/src/app/api/uploadthing/core.ts
@@ -1,5 +1,5 @@
import { createUploadthing, type FileRouter } from "uploadthing/next";
-import { validateRequest } from "~/server/auth";
+import { validateRequest } from "~/actions/auth";
const f = createUploadthing();
diff --git a/src/components/layout/auth-form.tsx b/src/components/layout/auth-form.tsx
index 7d9db73..9546b24 100644
--- a/src/components/layout/auth-form.tsx
+++ b/src/components/layout/auth-form.tsx
@@ -4,13 +4,15 @@ import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useState } from "react";
import { useForm } from "react-hook-form";
-import { z } from "zod";
+import { set, z } from "zod";
import { Button, buttonVariants } from "~/components/ui/button";
import { cn } from "~/lib/utils";
import Icons from "../shared/icons";
import { Input } from "../ui/input";
+import { InputOTP, InputOTPGroup, InputOTPSlot } from "../ui/input-otp";
import { Label } from "../ui/label";
import { toast } from "../ui/use-toast";
+import { useRouter } from "next/navigation";
const userAuthSchema = z.object({
email: z.string().email("Please enter a valid email address."),
@@ -19,8 +21,11 @@ const userAuthSchema = z.object({
type FormData = z.infer
;
export default function AuthForm() {
+ const router = useRouter();
+ const [currentStep, setCurrentStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [isGithubLoading, setIsGithubLoading] = useState(false);
+ const [otp, setOTP] = useState("");
const {
register,
@@ -31,83 +36,150 @@ export default function AuthForm() {
resolver: zodResolver(userAuthSchema),
});
- async function onSubmit(data: FormData) {
+ async function onEmailSubmit(data: FormData) {
setIsLoading(true);
try {
- const res = await fetch("/api/auth/login/magic-link", {
+ const res = await fetch("/api/auth/login/send-otp", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
+ if (!res.ok) {
+ throw new Error("Failed to send OTP");
+ }
+ setCurrentStep(2);
+ toast({
+ title: "OTP sent!",
+ description: "Please check your mail inbox",
+ });
+ } catch (error) {
+ toast({
+ title: "Failed to send OTP",
+ description: "Please try again later",
+ variant: "destructive",
+ });
+ } finally {
setIsLoading(false);
+ }
+ }
+
+ async function onOTPSubmit(data: FormData) {
+ setIsLoading(true);
+
+ try {
+ const res = await fetch("/api/auth/login/verify-otp", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email: data.email, code: otp }),
+ });
if (!res.ok) {
- return toast({
- title: "Failed to send Magic link!",
- description: "Please try again later.",
- variant: "destructive",
- });
+ throw new Error("Invalid OTP");
}
-
- reset();
toast({
- title: "Magic Link sent!",
- description: "Please check your mail inbox",
+ title: "Successfully verified!",
});
+ router.push("/dashboard");
} catch (error) {
- console.log(error);
+ toast({
+ title: "Invalid OTP",
+ description: "Please try again",
+ variant: "destructive",
+ });
+ } finally {
+ setIsLoading(false);
}
}
return (