diff --git a/.env.template b/.env.template index 3c05bdc..c9db31d 100644 --- a/.env.template +++ b/.env.template @@ -14,4 +14,6 @@ SENDGRID_SENDER_ID="" NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_SECRET="change-me" -NEXTAUTH_ROOT_DOMAIN="http://localhost" \ No newline at end of file +NEXTAUTH_ROOT_DOMAIN="http://localhost" + +SECRET_LINK="http://localhost:3000/verify-email/" diff --git a/package-lock.json b/package-lock.json index b75a06c..b2bcdb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,10 @@ "dependencies": { "@prisma/client": "^5.9.1", "@sendgrid/mail": "^8.1.0", + "@types/jsonwebtoken": "^9.0.6", "bcrypt": "^5.1.1", - "eclipse-components": "^0.0.141", + "eclipse-components": "^0.0.149", + "jsonwebtoken": "^9.0.2", "next": "14.1.0", "next-auth": "^4.24.5", "prisma": "^5.9.1", @@ -586,11 +588,18 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.11.16", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1223,6 +1232,11 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -1588,10 +1602,18 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/eclipse-components": { - "version": "0.0.141", - "resolved": "https://registry.npmjs.org/eclipse-components/-/eclipse-components-0.0.141.tgz", - "integrity": "sha512-1uBiEY/gYK8SBp3lY6bv3Ab+cuo5qe0YvvMkOwsbnqCVuTQu19UoFrhiXHwPbBgQEGrGQxLR2wrHmy418SR4qw==", + "version": "0.0.149", + "resolved": "https://registry.npmjs.org/eclipse-components/-/eclipse-components-0.0.149.tgz", + "integrity": "sha512-0d3gxDuoKl3X99ETLJ9JFgpe8jy52MInqGounJoF3hPRcwwpRrlwMLohtDl7msztnkjjfvuFUqRJ+ib27kY2Ew==", "peerDependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" @@ -3291,6 +3313,27 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -3306,6 +3349,25 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3376,12 +3438,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5239,8 +5336,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/update-browserslist-db": { "version": "1.0.13", diff --git a/package.json b/package.json index 3adace8..002ace3 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,10 @@ "dependencies": { "@prisma/client": "^5.9.1", "@sendgrid/mail": "^8.1.0", + "@types/jsonwebtoken": "^9.0.6", "bcrypt": "^5.1.1", - "eclipse-components": "^0.0.141", + "eclipse-components": "^0.0.149", + "jsonwebtoken": "^9.0.2", "next": "14.1.0", "next-auth": "^4.24.5", "prisma": "^5.9.1", @@ -31,4 +33,4 @@ "tailwindcss": "^3.3.0", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/prisma/migrations/20240312222610_add_email_verification_auth_table/migration.sql b/prisma/migrations/20240312222610_add_email_verification_auth_table/migration.sql new file mode 100644 index 0000000..aea1564 --- /dev/null +++ b/prisma/migrations/20240312222610_add_email_verification_auth_table/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Auth" ADD COLUMN "emailVerified" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8e8415e..736bf9d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -31,6 +31,8 @@ model Auth { createdAt DateTime @default(now()) @db.Timestamp(0) updatedAt DateTime @default(now()) @updatedAt @db.Timestamp(0) + + emailVerified Boolean @default(false) } model MailingList { diff --git a/src/app/api/auth/signup/route.ts b/src/app/api/auth/signup/route.ts index edb7b3b..c24bd74 100644 --- a/src/app/api/auth/signup/route.ts +++ b/src/app/api/auth/signup/route.ts @@ -1,6 +1,8 @@ import { prisma } from "@/lib/prisma"; import bcrypt from "bcrypt"; import { NextResponse } from "next/server"; +import {tokenGeneration} from "@/app/api/auth/signup/verifyEmail"; +import internal from "stream"; /** * @@ -60,6 +62,7 @@ export async function POST(request: Request) { console.log(email, " Account Created"); + await tokenGeneration(newUser.id, email); return NextResponse.json({ user: newUser }, { status: 201 }); } @@ -80,5 +83,6 @@ export async function POST(request: Request) { console.log(email, " Account Created"); + await tokenGeneration(newUser.id, email); return NextResponse.json({ user: newUser }, { status: 201 }); } diff --git a/src/app/api/auth/signup/verifyEmail.ts b/src/app/api/auth/signup/verifyEmail.ts new file mode 100644 index 0000000..f644461 --- /dev/null +++ b/src/app/api/auth/signup/verifyEmail.ts @@ -0,0 +1,15 @@ +import jwt from "jsonwebtoken"; +import { sendEmail } from "@/lib/email"; +import { send } from "process"; + +export async function tokenGeneration(userId:string, email:string) { + const secret = process.env.NEXTAUTH_SECRET as string; + + const token = jwt.sign({userId, email}, secret, {expiresIn: "7d"}); + const link = process.env.SECRET_LINK + token; + + const subject = "Verify your email"; + const body = `Click this link to verify your email: ${link}`; + + await sendEmail(email, subject, body); +} diff --git a/src/app/login/LoginForm.tsx b/src/app/login/LoginForm.tsx index 6392cea..5fea9f0 100644 --- a/src/app/login/LoginForm.tsx +++ b/src/app/login/LoginForm.tsx @@ -3,7 +3,7 @@ import { TextField, Button, - EclipseLogoLong, + EclipseLogoTextOrbGlow, Notification, } from "eclipse-components"; import { signIn } from "next-auth/react"; @@ -22,7 +22,7 @@ export default function LoginForm() { return ( <> - +
{ e.preventDefault(); diff --git a/src/app/signup/SignupForm.tsx b/src/app/signup/SignupForm.tsx index 3188730..72d6582 100644 --- a/src/app/signup/SignupForm.tsx +++ b/src/app/signup/SignupForm.tsx @@ -3,7 +3,7 @@ import { TextField, Button, - EclipseLogoLong, + EclipseLogoTextOrbGlow, Notification, } from "eclipse-components"; import { signIn } from "next-auth/react"; @@ -18,7 +18,7 @@ export default function SignupForm() { const searchParams = useSearchParams(); return ( <> - + { e.preventDefault(); diff --git a/src/app/verify-email/[jwt]/page.tsx b/src/app/verify-email/[jwt]/page.tsx new file mode 100644 index 0000000..bd496dc --- /dev/null +++ b/src/app/verify-email/[jwt]/page.tsx @@ -0,0 +1,69 @@ +"use client" + +import { useEffect, useState } from "react"; +import { + LoadingSpinner, + EclipseLogoTextOrbGlow, + StarBackground, + MainWrapper + } from "eclipse-components"; +import {verifyUserToken} from "./verifyUserToken" + + +export default function Home({ params }: { params: { jwt: string } }) { + return ( + + ); +} + +enum Status { + IDLE, + LOADING, + SUCCESS, + ERROR, +} + +interface Props { + token: string; +} + +function Components(props: Props) { + + const [status, setStatus] = useState(Status.LOADING); + const [error, setError] = useState(""); + + const userToken = props.token; + + async function fetchVerification(userToken: string){ + const verification = await verifyUserToken(userToken); + if (verification.success) { + setStatus(Status.SUCCESS); + } + else { + setStatus(Status.ERROR); + setError(verification.error as string); + } + } + + useEffect(() => { + fetchVerification(userToken); + }, [userToken]); + + + return ( + <> + + + + + + {status === Status.LOADING && } + {status === Status.SUCCESS &&

Your email has been verified

} + {status === Status.ERROR && +

{error}

+ } + +
+ + ); +} diff --git a/src/app/verify-email/[jwt]/verifyUserToken.ts b/src/app/verify-email/[jwt]/verifyUserToken.ts new file mode 100644 index 0000000..ebcc35e --- /dev/null +++ b/src/app/verify-email/[jwt]/verifyUserToken.ts @@ -0,0 +1,59 @@ +"use server" + +import { verifyToken } from "@/lib/user/verifyToken"; +import { prisma } from "@/lib/prisma"; + +export type Result = {message: string} & ( + | { + success: true + } + | { + success: false, + error: "date expired" | "Verification failed" | "User not found" + } +); + +export async function verifyUserToken (token: string) { + try{ + const verified = await verifyToken(token) + console.log(verified) + + if(verified.verified){ + const {userId, email, ...date} = verified.decodedParamters; + + // console.log(userId); + const checkIfUserExists = await prisma.user.findUnique({ + where: { + id: userId, + email: email + } + }); + + if(!checkIfUserExists){ + return {success: false, error: "User not found"} + } + + const user = await prisma.user.update({ + where: { + id: userId, + email: email + }, + data: + { + auth: { + update: { + emailVerified: true + } + } + } + }); + return {success: true, message: "email verified successfully"} + } + else{ + return {success: false, message: "Verification failed", error: verified.error} + } + } + catch { + return {success: false, error: "Verification failed"} + } +} diff --git a/src/lib/user/verifyToken.ts b/src/lib/user/verifyToken.ts new file mode 100644 index 0000000..6686135 --- /dev/null +++ b/src/lib/user/verifyToken.ts @@ -0,0 +1,27 @@ +import jwt from "jsonwebtoken"; + +type Decoded = { + userId: string; + email: string; + iat: number, + exp: number +} + +export async function verifyToken(token: string) { + let verified = false; + let error = ""; + let decodedParamters = {} as Decoded; + const secret = process.env.NEXTAUTH_SECRET as string; + await jwt.verify(token, secret, (err, decoded) => { + if (err) { + verified = false; + error = err.message; + } else { + verified = true; + decodedParamters = decoded as Decoded; + // console.log(decoded) + } + }); + + return { verified, error, decodedParamters }; +}