mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-28 02:08:33 -04:00
Add email verification
This commit is contained in:
@@ -90,7 +90,17 @@ export {authHandler as GET, authHandler as POST};
|
||||
|
||||
declare module "next-auth" {
|
||||
interface Session {
|
||||
user: { id: string; name: string; email: string ; image: string };
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image: string;
|
||||
emailVerified?: Date | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface User {
|
||||
emailVerified?: Date | null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma }from "@/lib/prisma";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { Resend } from "resend";
|
||||
import { render } from "@react-email/render";
|
||||
import { VerificationEmail } from "@/emails/VerificationEmail";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
// Helper function to generate a verification token
|
||||
const generateVerificationToken = () => {
|
||||
return uuidv4();
|
||||
};
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
@@ -16,11 +27,46 @@ export async function POST(req: Request) {
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const verificationToken = generateVerificationToken();
|
||||
const verificationTokenExpires = new Date();
|
||||
verificationTokenExpires.setHours(verificationTokenExpires.getHours() + 24); // Token expires in 24 hours
|
||||
|
||||
// Create user with verification token
|
||||
const user = await prisma.user.create({
|
||||
data: { email, password: hashedPassword, name },
|
||||
data: {
|
||||
email,
|
||||
password: hashedPassword,
|
||||
name,
|
||||
emailVerified: null, // Will be set when email is verified
|
||||
verificationToken,
|
||||
verificationTokenExpires,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ message: "User created", userId: user.id }, { status: 201 });
|
||||
// Send verification email. TODO once we have a domain
|
||||
// You can only send testing emails to your own email address.
|
||||
// To send emails to other recipients, please verify a domain at resend.com/domains,
|
||||
// and change the `from` address to an email using this domain.
|
||||
// const verificationUrl = `${process.env.NEXTAUTH_URL}/api/auth/verify-email?token=${verificationToken}`;
|
||||
// const emailHtml = await render(VerificationEmail({ url: verificationUrl }));
|
||||
// try {
|
||||
// let payload = {
|
||||
// from: `BayesBond <${process.env.EMAIL_FROM!}>`,
|
||||
// to: email,
|
||||
// subject: 'Verify your email',
|
||||
// html: emailHtml,
|
||||
// };
|
||||
// console.log(`Verification email: ${payload}`);
|
||||
// await resend.emails.send(payload);
|
||||
// } catch (emailError) {
|
||||
// console.error('Failed to send verification email:', emailError);
|
||||
// }
|
||||
|
||||
return NextResponse.json({
|
||||
message: "User created. Please check your email to verify your account.",
|
||||
userId: user.id
|
||||
}, { status: 201 });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
||||
|
||||
43
app/api/auth/verify-email/route.ts
Normal file
43
app/api/auth/verify-email/route.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const token = searchParams.get('token');
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.redirect(new URL('/auth/error?error=InvalidToken', req.url));
|
||||
}
|
||||
|
||||
// Find user with this verification token
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
verificationToken: token,
|
||||
verificationTokenExpires: {
|
||||
gt: new Date(), // Check if token is not expired
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.redirect(new URL('/auth/error?error=InvalidOrExpiredToken', req.url));
|
||||
}
|
||||
|
||||
// Update user as verified
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
emailVerified: new Date(),
|
||||
verificationToken: null,
|
||||
verificationTokenExpires: null,
|
||||
},
|
||||
});
|
||||
|
||||
// Redirect to success page
|
||||
return NextResponse.redirect(new URL('/auth/verification-success', req.url));
|
||||
} catch (error) {
|
||||
console.error('Email verification error:', error);
|
||||
return NextResponse.redirect(new URL('/auth/error?error=VerificationFailed', req.url));
|
||||
}
|
||||
}
|
||||
64
app/auth/error/page.tsx
Normal file
64
app/auth/error/page.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function AuthError({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams?: { [key: string]: string | string[] | undefined };
|
||||
}) {
|
||||
const error = searchParams?.error;
|
||||
let errorMessage = "An error occurred during authentication.";
|
||||
|
||||
switch (error) {
|
||||
case "InvalidToken":
|
||||
errorMessage = "The verification link is invalid.";
|
||||
break;
|
||||
case "InvalidOrExpiredToken":
|
||||
errorMessage = "The verification link is invalid or has expired. Please request a new one.";
|
||||
break;
|
||||
case "VerificationFailed":
|
||||
errorMessage = "Email verification failed. Please try again later.";
|
||||
break;
|
||||
default:
|
||||
errorMessage = "An unexpected error occurred. Please try again.";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8 text-center">
|
||||
<div className="rounded-full bg-red-100 p-3 inline-flex items-center justify-center">
|
||||
<svg
|
||||
className="h-12 w-12 text-red-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-gray-900">
|
||||
Verification Error
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-gray-600">{errorMessage}</p>
|
||||
<div className="mt-6 space-y-4">
|
||||
<Link
|
||||
href="/register"
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Back to Registration
|
||||
</Link>
|
||||
<Link
|
||||
href="/login"
|
||||
className="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Go to Login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
app/auth/verification-success/page.tsx
Normal file
39
app/auth/verification-success/page.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function VerificationSuccess() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8 text-center">
|
||||
<div className="rounded-full bg-green-100 p-3 inline-flex items-center justify-center">
|
||||
<svg
|
||||
className="h-12 w-12 text-green-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-gray-900">
|
||||
Email Verified Successfully!
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Your email has been successfully verified. You can now log in to your account.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<Link
|
||||
href="/login"
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Go to Login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {useState} from "react";
|
||||
import Link from "next/link";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
import {signIn} from "next-auth/react";
|
||||
import {FcGoogle} from "react-icons/fc";
|
||||
import {useSearchParams} from "next/navigation";
|
||||
|
||||
export default function RegisterPage() {
|
||||
const router = useRouter();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const searchParams = useSearchParams();
|
||||
const [error, setError] = useState<string | null>(searchParams.get('error'));
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [registrationSuccess, setRegistrationSuccess] = useState(false);
|
||||
const [registeredEmail, setRegisteredEmail] = useState('');
|
||||
|
||||
const handleGoogleSignUp = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await signIn('google', { callbackUrl: '/' });
|
||||
await signIn('google', {callbackUrl: '/'});
|
||||
} catch (error) {
|
||||
setError('Failed to sign up with Google');
|
||||
setIsLoading(false);
|
||||
@@ -26,7 +28,7 @@ export default function RegisterPage() {
|
||||
event.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const email = formData.get("email") as string;
|
||||
const password = formData.get("password") as string;
|
||||
@@ -39,8 +41,8 @@ export default function RegisterPage() {
|
||||
|
||||
const res = await fetch("/api/auth/signup", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password, name }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({email, password, name}),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
@@ -48,19 +50,21 @@ export default function RegisterPage() {
|
||||
throw new Error(data.error || "Registration failed");
|
||||
}
|
||||
|
||||
// Show a success message with email verification notice
|
||||
// setRegistrationSuccess(true);
|
||||
// setRegisteredEmail(email);
|
||||
|
||||
// Sign in after successful registration
|
||||
const response = await signIn("credentials", {
|
||||
email,
|
||||
password,
|
||||
redirect: false,
|
||||
redirect: true,
|
||||
callbackUrl: "/",
|
||||
});
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error("Failed to sign in after registration");
|
||||
}
|
||||
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : "Registration failed");
|
||||
setIsLoading(false);
|
||||
@@ -70,95 +74,144 @@ export default function RegisterPage() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
Create your account
|
||||
</h2>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label htmlFor="name" className="sr-only">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Full name"
|
||||
/>
|
||||
{registrationSuccess ? (
|
||||
<div className="text-center">
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
|
||||
<svg
|
||||
className="h-6 w-6 text-green-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email" className="sr-only">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Email address"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="sr-only">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Password"
|
||||
/>
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-gray-900">
|
||||
Check your email
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
We've sent a verification link to <span className="font-medium">{registeredEmail}</span>.
|
||||
Please click the link in the email to verify your account.
|
||||
</p>
|
||||
<p className="mt-4 text-sm text-gray-500">
|
||||
Didn't receive the email? Check your spam folder or{' '}
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-blue-600 hover:text-blue-500"
|
||||
onClick={() => setRegistrationSuccess(false)}
|
||||
>
|
||||
try again
|
||||
</button>
|
||||
.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<Link
|
||||
href="/login"
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Back to Login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-red-500 text-sm text-center">{error}</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className={`group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${isLoading ? 'opacity-70 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{isLoading ? 'Creating account...' : 'Sign up with Email'}
|
||||
</button>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-gray-50 text-gray-500">Or sign up with</span>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
Create your account
|
||||
</h2>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label htmlFor="name" className="sr-only">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Full name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email" className="sr-only">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Email address"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="sr-only">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGoogleSignUp}
|
||||
disabled={isLoading}
|
||||
className="w-full flex items-center justify-center gap-2 py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-70 disabled:cursor-not-allowed"
|
||||
>
|
||||
<FcGoogle className="w-5 h-5" />
|
||||
Continue with Google
|
||||
</button>
|
||||
{error && (
|
||||
<div className="text-red-500 text-sm text-center">{error}</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className={`group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${isLoading ? 'opacity-70 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{isLoading ? 'Creating account...' : 'Sign up with Email'}
|
||||
</button>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-gray-50 text-gray-500">Or sign up with</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGoogleSignUp}
|
||||
disabled={isLoading}
|
||||
className="w-full flex items-center justify-center gap-2 py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-70 disabled:cursor-not-allowed"
|
||||
>
|
||||
<FcGoogle className="w-5 h-5"/>
|
||||
Continue with Google
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center text-sm">
|
||||
<p className="text-gray-600">
|
||||
Already have an account?{' '}
|
||||
<Link href="/login" className="font-medium text-blue-600 hover:text-blue-500">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center text-sm">
|
||||
<p className="text-gray-600">
|
||||
Already have an account?{' '}
|
||||
<Link href="/login" className="font-medium text-blue-600 hover:text-blue-500">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
85
emails/VerificationEmail.tsx
Normal file
85
emails/VerificationEmail.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import * as React from 'react';
|
||||
|
||||
interface VerificationEmailProps {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export function VerificationEmail({ url }: VerificationEmailProps) {
|
||||
return (
|
||||
<div style={container}>
|
||||
<div style={content}>
|
||||
<h1 style={heading}>Verify your email</h1>
|
||||
<p style={text}>
|
||||
Thanks for signing up! Please verify your email address by clicking the button below:
|
||||
</p>
|
||||
<a href={url} style={button}>
|
||||
Verify Email
|
||||
</a>
|
||||
<p style={text}>
|
||||
If you didn't create an account, you can safely ignore this email.
|
||||
</p>
|
||||
<p style={text}>
|
||||
<small style={smallText}>
|
||||
Or copy and paste this link into your browser:<br />
|
||||
<a href={url} style={link}>
|
||||
{url}
|
||||
</a>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Email styles
|
||||
const container = {
|
||||
backgroundColor: '#f6f9fc',
|
||||
padding: '20px 0',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
};
|
||||
|
||||
const content = {
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto',
|
||||
backgroundColor: '#ffffff',
|
||||
padding: '30px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)',
|
||||
};
|
||||
|
||||
const heading = {
|
||||
color: '#2d3748',
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
margin: '0 0 20px 0',
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
const text = {
|
||||
color: '#4a5568',
|
||||
fontSize: '16px',
|
||||
lineHeight: '1.5',
|
||||
margin: '0 0 20px 0',
|
||||
};
|
||||
|
||||
const smallText = {
|
||||
fontSize: '14px',
|
||||
color: '#718096',
|
||||
lineHeight: '1.5',
|
||||
};
|
||||
|
||||
const button = {
|
||||
display: 'inline-block',
|
||||
backgroundColor: '#3182ce',
|
||||
color: '#ffffff',
|
||||
textDecoration: 'none',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '4px',
|
||||
fontWeight: '600',
|
||||
marginBottom: '20px',
|
||||
};
|
||||
|
||||
const link = {
|
||||
color: '#3182ce',
|
||||
wordBreak: 'break-all' as const,
|
||||
};
|
||||
257
package-lock.json
generated
257
package-lock.json
generated
@@ -13,6 +13,8 @@
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^6.12.0",
|
||||
"@prisma/extension-accelerate": "^2.0.2",
|
||||
"@react-email/render": "^1.1.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.503.0",
|
||||
@@ -20,7 +22,9 @@
|
||||
"next-auth": "^4.24.11",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-icons": "^5.5.0"
|
||||
"react-icons": "^5.5.0",
|
||||
"resend": "^4.7.0",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
@@ -1518,6 +1522,23 @@
|
||||
"@prisma/debug": "6.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-email/render": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.1.3.tgz",
|
||||
"integrity": "sha512-TjjF1tdTmOqYEIWWg9wMx5q9JbQRbWmnG7owQbSGEHkNfc/c/vBu7hjfrki907lgQEAkYac9KPTyIjOKhvhJCg==",
|
||||
"dependencies": {
|
||||
"html-to-text": "^9.0.5",
|
||||
"prettier": "^3.5.3",
|
||||
"react-promise-suspense": "^0.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
@@ -1530,6 +1551,18 @@
|
||||
"integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@selderee/plugin-htmlparser2": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
|
||||
"integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
|
||||
"dependencies": {
|
||||
"domhandler": "^5.0.3",
|
||||
"selderee": "^0.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
@@ -1603,6 +1636,11 @@
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
|
||||
@@ -2800,6 +2838,14 @@
|
||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
@@ -2867,6 +2913,57 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -2901,6 +2998,17 @@
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-abstract": {
|
||||
"version": "1.24.0",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
|
||||
@@ -3979,6 +4087,39 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-text": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
|
||||
"integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
|
||||
"dependencies": {
|
||||
"@selderee/plugin-htmlparser2": "^0.11.0",
|
||||
"deepmerge": "^4.3.1",
|
||||
"dom-serializer": "^2.0.0",
|
||||
"htmlparser2": "^8.0.2",
|
||||
"selderee": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1",
|
||||
"entities": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -4620,6 +4761,14 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/leac": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
|
||||
"integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
@@ -4935,6 +5084,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-auth/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/next/node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
@@ -5203,6 +5360,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parseley": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
|
||||
"integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
|
||||
"dependencies": {
|
||||
"leac": "^0.6.0",
|
||||
"peberminta": "^0.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -5249,6 +5418,14 @@
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/peberminta": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
|
||||
"integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -5430,6 +5607,20 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
||||
@@ -5533,6 +5724,19 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/react-promise-suspense": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz",
|
||||
"integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-promise-suspense/node_modules/fast-deep-equal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||
"integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -5596,6 +5800,34 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/resend": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/resend/-/resend-4.7.0.tgz",
|
||||
"integrity": "sha512-30IbXGBUbmDweQH2IlO53XOXX7ndjYV9xFZ8IEBiWqefqQ/qmTsgrX0Ab6MUnmobJXbpdReVv+iXGRQPubQL5Q==",
|
||||
"dependencies": {
|
||||
"@react-email/render": "1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/resend/node_modules/@react-email/render": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.1.2.tgz",
|
||||
"integrity": "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw==",
|
||||
"dependencies": {
|
||||
"html-to-text": "^9.0.5",
|
||||
"prettier": "^3.5.3",
|
||||
"react-promise-suspense": "^0.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
@@ -5743,6 +5975,17 @@
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="
|
||||
},
|
||||
"node_modules/selderee": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
|
||||
"integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
|
||||
"dependencies": {
|
||||
"parseley": "^0.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
@@ -6695,11 +6938,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^6.12.0",
|
||||
"@prisma/extension-accelerate": "^2.0.2",
|
||||
"@react-email/render": "^1.1.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.503.0",
|
||||
@@ -21,7 +23,9 @@
|
||||
"next-auth": "^4.24.11",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-icons": "^5.5.0"
|
||||
"react-icons": "^5.5.0",
|
||||
"resend": "^4.7.0",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
|
||||
@@ -12,8 +12,10 @@ model User {
|
||||
name String?
|
||||
email String? @unique
|
||||
password String? // <-- Add this for email/password auth
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
emailVerified DateTime?
|
||||
verificationToken String? @unique
|
||||
verificationTokenExpires DateTime?
|
||||
image String?
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
// Optional for WebAuthn support
|
||||
|
||||
Reference in New Issue
Block a user