diff --git a/app/(auth)/Verification/page.tsx b/app/(auth)/Verification/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5a9dc37cdb8fd86dcbe6b0f09fa1d84398689605 --- /dev/null +++ b/app/(auth)/Verification/page.tsx @@ -0,0 +1,25 @@ +import Link from "next/link" +import { GameUnityLogo } from "@/components/logo" +import { buttonVariants } from "@/components/ui/button" +import { cn } from "@/lib/utils" + +export default function EmailVerification() { + return ( + + <div className="container flex max-w-[64rem] flex-col items-center gap-4 text-center"> + <div className="flex items-center"> + <Link href="/home" className={cn("rounded-full p-3 hover:bg-accent")}> + <GameUnityLogo className="h-10 w-10" /> + </Link> + </div> + <p className="max-w-[42rem] leading-normal sm:text-xl sm:leading-8"> + Your Email has been Verified and your Account was Activated <br /> You can now login + </p> + <div className="align-middle"> + <Link href="/login" className={cn(buttonVariants({ size: "lg" }), "mr-6")}> + Login + </Link> + </div> + </div> + ) +} \ No newline at end of file diff --git a/app/api/verification/[token]/route.ts b/app/api/verification/[token]/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..5f72bbb4d3184c810fe6d732972676a0066f102b --- /dev/null +++ b/app/api/verification/[token]/route.ts @@ -0,0 +1,61 @@ +import { db } from '@/lib/db' +import { redirect } from 'next/navigation' +import { NextRequest } from 'next/server' + + +export async function GET( + _request: NextRequest, + { + params, + }: { + params: { token: string } + } +) { + const { token } = params + + const user = await db.user.findFirst({ + where: { + ActivationToken: { + some: { + AND: [ + { + activationDate: null, + }, + { + createdAt: { + gt: new Date(Date.now() - 24 * 60 * 60 * 1000), // 24 hours ago + }, + }, + { + token + }, + ], + }, + }, + }, + }) + + if (!user) { + throw new Error('Token is invalid or expired') + } + + const userUpdate = await db.user.update({ + where: { + id: user.id, + }, + data: { + emailVerified: new Date(Date.now()), + }, + }) + + const actiaction = await db.activationToken.update({ + where: { + token, + }, + data: { + activationDate: new Date(), + }, + }) + + redirect('/Verification') +} \ No newline at end of file diff --git a/app/api/verifyEmail/route.ts b/app/api/verifyEmail/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..da3496d68019c29465ea5b2a205531781c0c53c3 --- /dev/null +++ b/app/api/verifyEmail/route.ts @@ -0,0 +1,53 @@ + +import nodemailer from "nodemailer"; +import { NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { randomUUID } from "crypto"; +import getURL from "@/lib/utils"; +import { env } from "@/env.mjs"; + +export async function POST(req: Request) { + const { email} = await req.json(); + const transporter = nodemailer.createTransport({ + service: 'gmail', + host: 'smtp.gmail.com', + auth: { + user: env.NODEMAIL_MAIL, + pass: env.NODEMAIL_PW, + }, + }); + + const user = await db.user.findFirst({ + where: { + email: email + } + }); + + const token = await db.activationToken.create({ + data: { + token: `${randomUUID()}${randomUUID()}`.replace(/-/g, ''), + userId: user?.id! + }, + }); + + const mailData = { + from: env.NODEMAIL_MAIL, + to: email, + subject: `Email Verification for your GameUnity Account`, + html: `Hello ${user?.name} Please follow the Link: ${getURL(`/api/verification/${token.token}`)} and verify your email address.`, + }; + + let emailRes; + try { + emailRes = await transporter.sendMail(mailData); + + console.log("Message sent", emailRes.messageId); + } catch (err) { + + console.log(err); + + console.error("Error email could not be send"); + } + console.log(emailRes?.messageId) + return NextResponse.json({ success: true, messageId: emailRes?.messageId }); +} \ No newline at end of file diff --git a/components/user-auth-form.tsx b/components/user-auth-form.tsx index 9ce00468bcd0ab0f760f86e1a5ae7a477e35e16a..48b9ea258ec082ad7e7c233112ecd99aad3e6020 100644 --- a/components/user-auth-form.tsx +++ b/components/user-auth-form.tsx @@ -15,6 +15,7 @@ import { ToastAction } from "@/components/ui/toast" import { toast } from "@/components/ui/use-toast" import { cn } from '@/lib/utils' import { userAuthSchema } from "@/lib/validations/auth" +import { sendVerificationEmail } from "@/lib/validations/sendVerificationEmail" interface UserAuthFormProps extends HTMLAttributes<HTMLDivElement> { type: "login" | "signup" @@ -22,6 +23,7 @@ interface UserAuthFormProps extends HTMLAttributes<HTMLDivElement> { type FormData = z.infer<typeof userAuthSchema> + export function UserAuthForm({ type, className, ...props }: UserAuthFormProps) { const { register, @@ -35,6 +37,7 @@ export function UserAuthForm({ type, className, ...props }: UserAuthFormProps) { const [isGitHubLoading, setIsGitHubLoading] = useState<boolean>(false) const router = useRouter() const searchParams = useSearchParams() + //muss noch exportiert werden async function onSubmit(data: FormData) { setIsLoading(true) @@ -66,6 +69,7 @@ export function UserAuthForm({ type, className, ...props }: UserAuthFormProps) { description: "Your sign up request failed. Please try again.", }) } + await sendVerificationEmail(data.email!) } const signInResult = await signIn("credentials", { @@ -90,6 +94,12 @@ export function UserAuthForm({ type, className, ...props }: UserAuthFormProps) { message: 'Sorry, but it seems like the password you entered is invalid. Please try again.' }) } + if (signInResult.error === "Email is not verified") { + return toast({ + title: "Please verify your account.", + description: "We send you a email to verify your Account. Please check your Mailbox!🚀", + }) + } return toast({ variant: "destructive", title: "Uh oh! Something went wrong.", @@ -101,12 +111,7 @@ export function UserAuthForm({ type, className, ...props }: UserAuthFormProps) { router.push("/home") router.refresh() - if (type === "signup") { - return toast({ - title: "Congratulations!", - description: "Your account has been created. You will be redirected shortly.", - }) - } else { + if (type !== "signup") { return toast({ title: "Login successful.", description: "You will be redirected shortly.", diff --git a/env.mjs b/env.mjs index f2ba50c268786a06d88998fcb2749adc6c3ec88c..c6836ad32162b75dd45b6365c3aa9feae4c196de 100644 --- a/env.mjs +++ b/env.mjs @@ -15,6 +15,8 @@ export const env = createEnv({ TWITCH_AUTH_BASE_URL: z.string().url().optional(), IGDB_BASE_URL: z.string().url().optional(), IGDB_IMG_BASE_URL: z.string().url().optional(), + NODEMAIL_MAIL: z.string().min(1), + NODEMAIL_PW: z.string().min(1), }, client: { NEXT_PUBLIC_APP_URL: z.string().min(1), @@ -33,5 +35,7 @@ export const env = createEnv({ TWITCH_AUTH_BASE_URL: process.env.TWITCH_AUTH_BASE_URL, IGDB_BASE_URL: process.env.IGDB_BASE_URL, IGDB_IMG_BASE_URL: process.env.IGDB_IMG_BASE_URL, + NODEMAIL_MAIL: process.env.NODEMAIL_MAIL, + NODEMAIL_PW: process.env.NODEMAIL_PW, }, }) \ No newline at end of file diff --git a/lib/auth.ts b/lib/auth.ts index 382e702eb3987843f54ad37f2b04617a4061d90a..bb2d397262d138ced1f46150b7b5819c44b9dad3 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -7,6 +7,7 @@ import { Adapter } from "next-auth/adapters" import CredentialsProvider from 'next-auth/providers/credentials' import GitHubProvider from "next-auth/providers/github" import { normalize } from "normalize-diacritics" +import { sendVerificationEmail } from "./validations/sendVerificationEmail" export const authOptions: NextAuthOptions = { adapter: PrismaAdapter(db as any) as Adapter, @@ -51,6 +52,10 @@ export const authOptions: NextAuthOptions = { user.password ) + if(!user.emailVerified){ + throw new Error('Email is not verified') + } + if (!isPasswordValid) { throw new Error('invalid password') } diff --git a/lib/validations/sendVerificationEmail.ts b/lib/validations/sendVerificationEmail.ts new file mode 100644 index 0000000000000000000000000000000000000000..e97b3e1122db5a90c2cf14f56b3f0edf7e357cec --- /dev/null +++ b/lib/validations/sendVerificationEmail.ts @@ -0,0 +1,26 @@ + +export async function sendVerificationEmail(email: string) { + + try { + + const res = await fetch('/api/verifyEmail', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + }), + }); + + // if (res.ok) { + // alert(`Verification Email was send 🚀,/n Please Verify your Account!`); + // } + + if (res.status === 400) { + alert(`Something went wrong! Verification Email could not be send 😢`); + } + } catch (err) { + console.log('Something went wrong: ', err); + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index aaa77bde0066aebff1711eca290088da92ff6d04..845a1861a179f2a7dfe9feb821513bdd534ce602 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.2.0", "hasInstallScript": true, "dependencies": { - "@auth/prisma-adapter": "^1.0.0", + "@auth/prisma-adapter": "^1.0.1", "@hookform/resolvers": "^3.1.1", "@prisma/client": "^4.16.2", "@radix-ui/react-alert-dialog": "^1.0.4", @@ -38,6 +38,7 @@ "next-auth": "^4.22.1", "next-swagger-doc": "^0.4.0", "next-themes": "^0.2.1", + "nodemailer": "^6.9.3", "normalize-diacritics": "^4.0.0", "react": "18.2.0", "react-dom": "18.2.0", @@ -56,8 +57,9 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@types/bcrypt": "^5.0.0", - "@types/jest": "^29.5.2", + "@types/jest": "^29.5.3", "@types/node": "^20.4.1", + "@types/nodemailer": "^6.4.8", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@types/swagger-ui-react": "^4.18.0", @@ -152,9 +154,9 @@ } }, "node_modules/@auth/core": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.8.1.tgz", - "integrity": "sha512-WudBmZudZ/cvykxHV5hIwrYsd7AlETQ535O7w3sSiiumT28+U9GvBb8oSRtfzxpW9rym3lAdfeTJqGA8U4FecQ==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.9.0.tgz", + "integrity": "sha512-W2WO0WCBg1T3P8+yjQPzurTQhPv6ecBYfJ2oE3uvXPAX5ZLWAMSjKFAIa9oLZy5pwrB+YehJZPnlIxVilhrVcg==", "dependencies": { "@panva/hkdf": "^1.0.4", "cookie": "0.5.0", @@ -173,11 +175,11 @@ } }, "node_modules/@auth/prisma-adapter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-1.0.0.tgz", - "integrity": "sha512-+x+s5dgpNmqrcQC2ZRAXZIM6yhkWP/EXjIUgqUyMepLiX1OHi2AXIUAAbXsW4oG9OpYr/rvPIzPBpuGt6sPFwQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-1.0.1.tgz", + "integrity": "sha512-sBp9l/jVr7l9y7rp2Pv6eoP7i8X2CgRNE3jDWJ0B/u+HnKRofXflD1cldPqRSAkJhqH3UxhVtMTEijT9FoofmQ==", "dependencies": { - "@auth/core": "0.8.1" + "@auth/core": "0.9.0" }, "peerDependencies": { "@prisma/client": ">=2.26.0 || >=3 || >=4" @@ -3283,9 +3285,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.2", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.2.tgz", - "integrity": "sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==", + "version": "29.5.3", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.3.tgz", + "integrity": "sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -3352,6 +3354,15 @@ "integrity": "sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==", "dev": true }, + "node_modules/@types/nodemailer": { + "version": "6.4.8", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.8.tgz", + "integrity": "sha512-oVsJSCkqViCn8/pEu2hfjwVO+Gb3e+eTWjg3PcjeFKRItfKpKwHphQqbYmPQrlMk+op7pNNWPbsJIEthpFN/OQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prettier": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", @@ -9093,6 +9104,14 @@ "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", "dev": true }, + "node_modules/nodemailer": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.3.tgz", + "integrity": "sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", diff --git a/package.json b/package.json index 3c732185bbbf22def9135de92e764241cc542ed8..2e35319ead7fe0eb6ea3b72d0c7b0563decd15f6 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "postinstall": "prisma generate" }, "dependencies": { - "@auth/prisma-adapter": "^1.0.0", + "@auth/prisma-adapter": "^1.0.1", "@hookform/resolvers": "^3.1.1", "@prisma/client": "^4.16.2", "@radix-ui/react-alert-dialog": "^1.0.4", @@ -43,6 +43,7 @@ "next-auth": "^4.22.1", "next-swagger-doc": "^0.4.0", "next-themes": "^0.2.1", + "nodemailer": "^6.9.3", "normalize-diacritics": "^4.0.0", "react": "18.2.0", "react-dom": "18.2.0", @@ -61,8 +62,9 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@types/bcrypt": "^5.0.0", - "@types/jest": "^29.5.2", + "@types/jest": "^29.5.3", "@types/node": "^20.4.1", + "@types/nodemailer": "^6.4.8", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@types/swagger-ui-react": "^4.18.0", @@ -80,4 +82,4 @@ "semver": "^7.5.3", "optionator": "^0.9.3" } -} +} \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d6b3ef1cdc15ec826b97a8e27954672bc783f996..e9596dd527b903bb2d93a0393ffec9121ee0ca67 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -84,9 +84,23 @@ model User { following User[] @relation("followers") followers User[] @relation("followers") + ActivationToken ActivationToken[] + @@map("users") } +model ActivationToken { + id String @id @default(cuid()) + token String @unique + activationDate DateTime? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String +} + model Gweet { id String @id @default(cuid()) authorId String @map("user_id")