diff --git a/DevoteApp/app/api/auth/check-verification-status/route.ts b/DevoteApp/app/api/auth/check-verification-status/route.ts new file mode 100644 index 0000000..e2b961b --- /dev/null +++ b/DevoteApp/app/api/auth/check-verification-status/route.ts @@ -0,0 +1,47 @@ +import { NextResponse } from "next/server"; +import connectToDb from "../../../../lib/mongodb/mongodb"; +import User from "../../../../models/user"; +import { VerificationUtils } from "../../../../lib/verification"; + +export async function GET(req: Request) { + try { + const { searchParams } = new URL(req.url); + const email = searchParams.get("email"); + + if (!email) { + return NextResponse.json( + { message: "Email is required" }, + { status: 400 } + ); + } + + await connectToDb(); + + const user = await User.findOne({ email }).exec(); + if (!user) { + return NextResponse.json( + { message: "User not found" }, + { status: 404 } + ); + } + + return NextResponse.json( + { + isEmailVerified: user.isEmailVerified, + hasVerificationCode: !!user.emailVerificationCode, + codeExpired: user.emailVerificationExpires ? + VerificationUtils.isCodeExpired(user.emailVerificationExpires) : true, + attemptsUsed: user.emailVerificationAttempts, + canRequestNew: VerificationUtils.canSendEmail(user.lastVerificationEmailSent) + }, + { status: 200 } + ); + + } catch (error: any) { + console.error("Error checking verification status:", error); + return NextResponse.json( + { message: "Internal server error" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/DevoteApp/app/api/auth/send-verification/route.ts b/DevoteApp/app/api/auth/send-verification/route.ts new file mode 100644 index 0000000..1e0964e --- /dev/null +++ b/DevoteApp/app/api/auth/send-verification/route.ts @@ -0,0 +1,79 @@ +import { NextResponse } from "next/server"; +import connectToDb from "../../../../lib/mongodb/mongodb"; +import User from "../../../../models/user"; +import { EmailService } from "../../../../lib/email"; +import { VerificationUtils } from "../../../../lib/verification"; + +export async function POST(req: Request) { + try { + const { email } = await req.json(); + + if (!email) { + return NextResponse.json( + { message: "Email is required" }, + { status: 400 } + ); + } + + await connectToDb(); + + const user = await User.findOne({ email }).exec(); + if (!user) { + return NextResponse.json( + { message: "User not found" }, + { status: 404 } + ); + } + + if (user.isEmailVerified) { + return NextResponse.json( + { message: "Email is already verified" }, + { status: 400 } + ); + } + + // Check rate limiting + if (!VerificationUtils.canSendEmail(user.lastVerificationEmailSent)) { + return NextResponse.json( + { message: "Please wait before requesting another verification email" }, + { status: 429 } + ); + } + + // Generate new verification code + const verificationCode = VerificationUtils.generateVerificationCode(); + const hashedCode = VerificationUtils.hashVerificationCode(verificationCode); + const expirationDate = VerificationUtils.getExpirationDate(); + + // Update user with new verification code + user.emailVerificationCode = hashedCode; + user.emailVerificationExpires = expirationDate; + user.emailVerificationAttempts = 0; // Reset attempts + user.lastVerificationEmailSent = new Date(); + await user.save(); + + // Send verification email + const emailService = new EmailService(); + const { subject, text, html } = VerificationUtils.generateVerificationEmail( + verificationCode, + user.name + ); + + await emailService.sendMail(user.email, subject, text, html); + + return NextResponse.json( + { + message: "Verification email sent successfully", + expiresAt: expirationDate + }, + { status: 200 } + ); + + } catch (error: any) { + console.error("Error sending verification email:", error); + return NextResponse.json( + { message: "Internal server error" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/DevoteApp/app/api/auth/verify-email/route.ts b/DevoteApp/app/api/auth/verify-email/route.ts new file mode 100644 index 0000000..28cd862 --- /dev/null +++ b/DevoteApp/app/api/auth/verify-email/route.ts @@ -0,0 +1,100 @@ +// app/api/auth/verify-email/route.ts +import { NextResponse } from "next/server"; +import connectToDb from "../../../../lib/mongodb/mongodb"; +import User from "../../../../models/user"; +import { VerificationUtils } from "../../../../lib/verification"; + +export async function POST(req: Request) { + try { + const { email, code } = await req.json(); + + if (!email || !code) { + return NextResponse.json( + { message: "Email and verification code are required" }, + { status: 400 } + ); + } + + await connectToDb(); + + const user = await User.findOne({ email }).exec(); + if (!user) { + return NextResponse.json( + { message: "User not found" }, + { status: 404 } + ); + } + + if (user.isEmailVerified) { + return NextResponse.json( + { message: "Email is already verified" }, + { status: 400 } + ); + } + + // Check if user has exceeded attempts + if (VerificationUtils.hasExceededAttempts(user.emailVerificationAttempts)) { + return NextResponse.json( + { + message: "Too many failed attempts. Please request a new verification code.", + requireNewCode: true + }, + { status: 429 } + ); + } + + // Check if code has expired + if (VerificationUtils.isCodeExpired(user.emailVerificationExpires)) { + return NextResponse.json( + { + message: "Verification code has expired. Please request a new one.", + expired: true + }, + { status: 400 } + ); + } + + // Increment attempts + user.emailVerificationAttempts += 1; + await user.save(); + + // Verify the code + if (!VerificationUtils.verifyCode(code, user.emailVerificationCode)) { + const attemptsLeft = 5 - user.emailVerificationAttempts; + return NextResponse.json( + { + message: `Invalid verification code. ${attemptsLeft} attempts remaining.`, + attemptsLeft + }, + { status: 400 } + ); + } + + // Success! Mark email as verified + user.isEmailVerified = true; + user.emailVerificationCode = ""; // Clear the code + user.emailVerificationExpires = new Date(); // Set to past date + user.emailVerificationAttempts = 0; + await user.save(); + + return NextResponse.json( + { + message: "Email verified successfully", + user: { + id: user._id, + name: user.name, + email: user.email, + isEmailVerified: user.isEmailVerified + } + }, + { status: 200 } + ); + + } catch (error: any) { + console.error("Error verifying email:", error); + return NextResponse.json( + { message: "Internal server error" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/DevoteApp/app/api/users/route.ts b/DevoteApp/app/api/users/route.ts index 7607c08..a5e5a41 100644 --- a/DevoteApp/app/api/users/route.ts +++ b/DevoteApp/app/api/users/route.ts @@ -3,9 +3,8 @@ import connectToDb from "../../../lib/mongodb/mongodb"; import Citizen from "../../../models/citizen"; import User from "../../../models/user"; import crypto from "crypto"; -// KYC imports removed - no longer needed -// import { createKyc, getSdkLink } from "../../../lib/kyc"; import { EmailService } from "../../../lib/email"; +import { VerificationUtils } from "../../../lib/verification"; import { generatePrivateKeyEncrypted, getFutureWalletAdressFromPrivateKey, @@ -43,6 +42,15 @@ export async function POST(req: Request) { ); } + // Check for existing email + const existingEmail = await User.findOne({ email }).exec(); + if (existingEmail) { + return NextResponse.json( + { message: "User already exists with provided email" }, + { status: 400 } + ); + } + const name = `${citizen.firstName} ${citizen.lastName}`; const privateKey = generatePrivateKeyEncrypted("1234"); const walletAddress = getFutureWalletAdressFromPrivateKey( @@ -50,38 +58,49 @@ export async function POST(req: Request) { "1234" ); + // Generate verification code for email verification + const verificationCode = VerificationUtils.generateVerificationCode(); + const hashedCode = VerificationUtils.hashVerificationCode(verificationCode); + const expirationDate = VerificationUtils.getExpirationDate(); + const newUser = new User({ walletId: walletAddress, name, email, hashIne: hashedIne, - // KYC fields removed - lines 30-36, 48, 61-63, 69, 71 deleted secretKey: privateKey, + // Email verification fields + isEmailVerified: false, + emailVerificationCode: hashedCode, + emailVerificationExpires: expirationDate, + emailVerificationAttempts: 0, + lastVerificationEmailSent: new Date(), }); await newUser.save(); - // KYC creation logic removed - no longer needed - // const kycId = await createKyc(String(newUser._id), newUser.email); - // newUser.kycId = kycId; - // await newUser.save(); - + // Send verification email instead of account creation email const emailService = new EmailService(); - const subject = "Account Created Successfully"; - - // Updated email message as requested (lines 75-84) - const frontendUrl = process.env.FRONTEND_URL || "https://devote-nine.vercel.app/"; - const verificationUrl = `${frontendUrl}/verification-submitted?id=${newUser._id}`; - - const text = `A user account has been created for you. Please click the following link to set your password: ${verificationUrl}`; - const html = `

A user account has been created for you. Please click the following link to set your password:

-

${verificationUrl}

-

⚠️ NOTE: Before testing this flow, make sure to send Sepolia ETH to the generated wallet before clicking the link in the email.

`; + const { subject, text, html } = VerificationUtils.generateVerificationEmail( + verificationCode, + newUser.name + ); await emailService.sendMail(newUser.email, subject, text, html); return NextResponse.json( - { message: "User created successfully", user: newUser }, + { + message: "User created successfully. Verification email sent.", + user: { + id: newUser._id, + name: newUser.name, + email: newUser.email, + walletId: newUser.walletId, + isEmailVerified: newUser.isEmailVerified + }, + verificationSent: true, + expiresAt: expirationDate + }, { status: 201 } ); } catch (error: any) { diff --git a/DevoteApp/app/verify-email/page.tsx b/DevoteApp/app/verify-email/page.tsx new file mode 100644 index 0000000..f0d9d98 --- /dev/null +++ b/DevoteApp/app/verify-email/page.tsx @@ -0,0 +1,315 @@ +"use client" + +import { useState, useEffect } from "react" +import { useSearchParams, useRouter } from "next/navigation" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { useToast } from "@/hooks/use-toast" +import { CheckCircle, Mail, Clock, AlertTriangle, ArrowLeft } from "lucide-react" + +export default function EmailVerificationPage() { + const [verificationCode, setVerificationCode] = useState("") + const [isVerifying, setIsVerifying] = useState(false) + const [isResending, setIsResending] = useState(false) + const [isVerified, setIsVerified] = useState(false) + const [attemptsLeft, setAttemptsLeft] = useState(5) + const [timeRemaining, setTimeRemaining] = useState("") + const [userInfo, setUserInfo] = useState(null) + + const searchParams = useSearchParams() + const router = useRouter() + const { toast } = useToast() + + const email = searchParams.get('email') + + useEffect(() => { + if (!email) { + router.push('/') + return + } + + // Check verification status on load + checkVerificationStatus() + }, [email]) + + useEffect(() => { + // Update time remaining every minute + const interval = setInterval(() => { + updateTimeRemaining() + }, 60000) + + return () => clearInterval(interval) + }, []) + + const checkVerificationStatus = async () => { + try { + const res = await fetch(`/api/auth/check-verification-status?email=${encodeURIComponent(email!)}`) + const data = await res.json() + + if (res.ok) { + if (data.isEmailVerified) { + setIsVerified(true) + return + } + + setAttemptsLeft(5 - data.attemptsUsed) + updateTimeRemaining() + } + } catch (error) { + console.error("Error checking verification status:", error) + } + } + + const updateTimeRemaining = () => { + // This would need to be implemented based on the backend response + // For now, i am using a placeholder + setTimeRemaining("4h 32m remaining") + } + + const handleVerifyCode = async () => { + if (!verificationCode || verificationCode.length !== 6) { + toast({ + title: "Invalid Code", + description: "Please enter a valid 6-digit verification code", + variant: "destructive", + }) + return + } + + setIsVerifying(true) + try { + const res = await fetch('/api/auth/verify-email', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, code: verificationCode }) + }) + + const data = await res.json() + + if (!res.ok) { + if (data.attemptsLeft !== undefined) { + setAttemptsLeft(data.attemptsLeft) + } + + toast({ + title: "Verification Failed", + description: data.message, + variant: "destructive", + }) + + if (data.requireNewCode) { + setVerificationCode("") + } + + setIsVerifying(false) + return + } + + setIsVerified(true) + setUserInfo(data.user) + + toast({ + title: "Email Verified!", + description: "Your account is now fully activated", + duration: 5000, + variant: "success", + }) + + } catch (error) { + console.error("Error verifying code:", error) + toast({ + title: "Error", + description: "Failed to verify code", + variant: "destructive", + }) + } finally { + setIsVerifying(false) + } + } + + const handleResendCode = async () => { + setIsResending(true) + try { + const res = await fetch('/api/auth/send-verification', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) + }) + + const data = await res.json() + + if (!res.ok) { + toast({ + title: "Error", + description: data.message, + variant: "destructive", + }) + setIsResending(false) + return + } + + setVerificationCode("") + setAttemptsLeft(5) + + toast({ + title: "Code Resent", + description: "A new verification code has been sent to your email", + duration: 3000, + variant: "success", + }) + + } catch (error) { + console.error("Error resending code:", error) + toast({ + title: "Error", + description: "Failed to resend verification code", + variant: "destructive", + }) + } finally { + setIsResending(false) + } + } + + if (!email) { + return ( +
+
+

Invalid verification link

+ +
+
+ ) + } + + if (isVerified) { + return ( +
+
+
+ +

+ Email Verified! +

+

+ Welcome, {userInfo?.name || 'User'}! Your email has been successfully verified and your account is now active. +

+ + {userInfo && ( +
+

Email: {userInfo.email}

+

Account ID: {userInfo.id}

+
+ )} + +
+ + +
+
+
+
+ ) + } + + return ( +
+
+
+
+ +

+ Verify Your Email +

+

+ We've sent a 6-digit verification code to: +

+

{email}

+
+ +
+
+ + setVerificationCode(e.target.value.replace(/\D/g, '').slice(0, 6))} + className="bg-gray-700 border-gray-600 text-white text-center text-2xl font-mono tracking-widest" + placeholder="000000" + maxLength={6} + autoFocus + /> +
+
+ + {timeRemaining} +
+
+ {attemptsLeft} attempts left +
+
+
+ + {attemptsLeft <= 2 && attemptsLeft > 0 && ( +
+
+ +

+ Only {attemptsLeft} attempts remaining. Double-check your code. +

+
+
+ )} + +
+ + +
+ +
+ +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/DevoteApp/components/CreateUserModal.tsx b/DevoteApp/components/CreateUserModal.tsx index 19bda42..99e6853 100644 --- a/DevoteApp/components/CreateUserModal.tsx +++ b/DevoteApp/components/CreateUserModal.tsx @@ -6,23 +6,49 @@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { useToast } from "@/hooks/use-toast" - +import { CheckCircle, Mail, Clock, AlertTriangle } from "lucide-react" interface CreateUserModalProps { isOpen: boolean onClose: () => void } +type ModalStep = 'create' | 'verification' | 'success' + export default function CreateUserModal({ isOpen, onClose }: CreateUserModalProps) { const [email, setEmail] = useState("") const [userId, setUserId] = useState("") const [userName, setUserName] = useState("") + const [verificationCode, setVerificationCode] = useState("") + + const [currentStep, setCurrentStep] = useState('create') const [isCreating, setIsCreating] = useState(false) const [searchLoading, setSearchLoading] = useState(false) const [searchError, setSearchError] = useState("") + const [isVerifying, setIsVerifying] = useState(false) + const [isResending, setIsResending] = useState(false) + + const [createdUser, setCreatedUser] = useState(null) + const [verificationExpiry, setVerificationExpiry] = useState(null) + const [attemptsLeft, setAttemptsLeft] = useState(5) + const { toast } = useToast() - // Función que se ejecuta al presionar el botón "Search" para buscar el citizen + // Reset all states when modal closes + const handleClose = () => { + setEmail("") + setUserId("") + setUserName("") + setVerificationCode("") + setCurrentStep('create') + setCreatedUser(null) + setVerificationExpiry(null) + setAttemptsLeft(5) + setSearchError("") + onClose() + } + + // Search citizen function (unchanged) const handleSearchCitizen = async () => { if (!userId) { setSearchError("Please enter a User ID.") @@ -55,37 +81,39 @@ export default function CreateUserModal({ isOpen, onClose }: CreateUserModalProp } } - // Función que se ejecuta al presionar "Create User" + // Create user function (updated for verification flow) const handleCreateUser = async () => { setIsCreating(true) try { const res = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - // Enviamos "ine" usando el valor de userId (que es el INE del citizen) body: JSON.stringify({ email, ine: userId }) }) + + const data = await res.json() + if (!res.ok) { - const errorData = await res.json() toast({ title: "Error", - description: errorData.message || "Failed to create user", + description: data.message || "Failed to create user", variant: "destructive", }) setIsCreating(false) return } - const data = await res.json() + + setCreatedUser(data.user) + setVerificationExpiry(new Date(data.expiresAt)) + setCurrentStep('verification') + toast({ title: "User Created", - description: `New user account created for ${data.user.name}`, - duration: 3000, + description: `Account created for ${data.user.name}. Verification email sent!`, + duration: 5000, variant: "success", }) - setEmail("") - setUserId("") - setUserName("") - onClose() + } catch (error) { console.error("Error creating user:", error) toast({ @@ -98,59 +126,312 @@ export default function CreateUserModal({ isOpen, onClose }: CreateUserModalProp } } - return ( - - - - Create New User - -
-
- + // Verify email code + const handleVerifyCode = async () => { + if (!verificationCode || verificationCode.length !== 6) { + toast({ + title: "Invalid Code", + description: "Please enter a valid 6-digit verification code", + variant: "destructive", + }) + return + } + + setIsVerifying(true) + try { + const res = await fetch('/api/auth/verify-email', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, code: verificationCode }) + }) + + const data = await res.json() + + if (!res.ok) { + if (data.attemptsLeft !== undefined) { + setAttemptsLeft(data.attemptsLeft) + } + + toast({ + title: "Verification Failed", + description: data.message, + variant: "destructive", + }) + + if (data.requireNewCode) { + // Reset to allow resending + setVerificationCode("") + } + + setIsVerifying(false) + return + } + + setCurrentStep('success') + + toast({ + title: "Email Verified!", + description: "User account is now fully activated", + duration: 5000, + variant: "success", + }) + + } catch (error) { + console.error("Error verifying code:", error) + toast({ + title: "Error", + description: "Failed to verify code", + variant: "destructive", + }) + } finally { + setIsVerifying(false) + } + } + + // Resend verification email + const handleResendCode = async () => { + setIsResending(true) + try { + const res = await fetch('/api/auth/send-verification', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) + }) + + const data = await res.json() + + if (!res.ok) { + toast({ + title: "Error", + description: data.message, + variant: "destructive", + }) + setIsResending(false) + return + } + + setVerificationExpiry(new Date(data.expiresAt)) + setVerificationCode("") + setAttemptsLeft(5) + + toast({ + title: "Code Resent", + description: "A new verification code has been sent to your email", + duration: 3000, + variant: "success", + }) + + } catch (error) { + console.error("Error resending code:", error) + toast({ + title: "Error", + description: "Failed to resend verification code", + variant: "destructive", + }) + } finally { + setIsResending(false) + } + } + + // Format time remaining + const getTimeRemaining = () => { + if (!verificationExpiry) return "" + + const now = new Date() + const timeLeft = verificationExpiry.getTime() - now.getTime() + + if (timeLeft <= 0) return "Expired" + + const hours = Math.floor(timeLeft / (1000 * 60 * 60)) + const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60)) + + if (hours > 0) { + return `${hours}h ${minutes}m remaining` + } + return `${minutes}m remaining` + } + + const renderCreateStep = () => ( + <> + + Create New User + +
+
+ + setEmail(e.target.value)} + className="bg-gray-800 border-gray-700 text-white" + placeholder="user@example.com" + /> +
+
+
+ setEmail(e.target.value)} + id="userId" + value={userId} + onChange={(e) => setUserId(e.target.value)} className="bg-gray-800 border-gray-700 text-white" + placeholder="Enter INE number" />
-
-
- - setUserId(e.target.value)} - className="bg-gray-800 border-gray-700 text-white" - /> + +
+ {searchError &&

{searchError}

} +
+ + +
+ +
+ + ) + + const renderVerificationStep = () => ( + <> + + + + Email Verification + + +
+
+
+ +
+

+ We've sent a 6-digit verification code to: +

+

{email}

-
- {searchError &&

{searchError}

} -
- - +
+ +
+ + setVerificationCode(e.target.value.replace(/\D/g, '').slice(0, 6))} + className="bg-gray-800 border-gray-700 text-white text-center text-2xl font-mono tracking-widest" + placeholder="000000" + maxLength={6} + /> +
+
+ + {getTimeRemaining()} +
+
+ {attemptsLeft} attempts left +
+
+ + {attemptsLeft <= 2 && attemptsLeft > 0 && ( +
+
+ +

+ Only {attemptsLeft} attempts remaining. Double-check your code. +

+
+
+ )} + +
+
+ +
+ +
+
+ + ) + + const renderSuccessStep = () => ( + <> + + + + Account Verified! + + +
+
+ +

+ Welcome, {createdUser?.name}! +

+

+ Your email has been successfully verified and your account is now active. +

+
+

Email: {createdUser?.email}

+

Wallet ID: {createdUser?.walletId?.slice(0, 20)}...

+
+
+ + +
+ + ) + + return ( + + + {currentStep === 'create' && renderCreateStep()} + {currentStep === 'verification' && renderVerificationStep()} + {currentStep === 'success' && renderSuccessStep()} ) diff --git a/DevoteApp/lib/verification.ts b/DevoteApp/lib/verification.ts new file mode 100644 index 0000000..a9bd646 --- /dev/null +++ b/DevoteApp/lib/verification.ts @@ -0,0 +1,126 @@ +import crypto from "crypto"; + +export class VerificationUtils { + /** + * Generate a 6-digit verification code + */ + static generateVerificationCode(): string { + return Math.floor(100000 + Math.random() * 900000).toString(); + } + + /** + * Hash verification code for secure storage + */ + static hashVerificationCode(code: string): string { + return crypto.createHash("sha256").update(code).digest("hex"); + } + + /** + * Verify if code matches the hashed version + */ + static verifyCode(inputCode: string, hashedCode: string): boolean { + const hashedInput = this.hashVerificationCode(inputCode); + return hashedInput === hashedCode; + } + + /** + * Check if verification code has expired (5 hours) + */ + static isCodeExpired(expirationDate: Date): boolean { + return new Date() > expirationDate; + } + + /** + * Get expiration date (5 hours from now) + */ + static getExpirationDate(): Date { + const expirationTime = new Date(); + expirationTime.setHours(expirationTime.getHours() + 5); + return expirationTime; + } + + /** + * Check rate limiting for email sending (max 1 email per hour) + */ + static canSendEmail(lastSent: Date | null): boolean { + if (!lastSent) return true; + + const oneHourAgo = new Date(); + oneHourAgo.setHours(oneHourAgo.getHours() - 1); + + return lastSent < oneHourAgo; + } + + /** + * Check if user has exceeded verification attempts (max 5 attempts) + */ + static hasExceededAttempts(attempts: number): boolean { + return attempts >= 5; + } + + /** + * Generate verification email template + */ + static generateVerificationEmail(code: string, userName: string) { + const subject = "Email Verification - Devote App"; + + const text = ` +Hello ${userName}, + +Welcome to Devote App! To complete your registration, please use the verification code below: + +Verification Code: ${code} + +This code will expire in 5 hours. If you didn't create an account, please ignore this email. + +Best regards, +Devote Team + `; + + const html = ` + + + + + + Email Verification + + + +
+

Welcome to Devote App!

+
+
+

Hello ${userName},

+

Thank you for creating an account with Devote App. To complete your registration, please use the verification code below:

+ +
+
${code}
+
+ +
+ ⚠️ Important: This verification code will expire in 5 hours. +
+ +

If you didn't create this account, please ignore this email and no account will be created.

+ +

For security reasons, never share your verification code with anyone.

+
+ + + + `; + + return { subject, text, html }; + } +} \ No newline at end of file diff --git a/DevoteApp/models/user.ts b/DevoteApp/models/user.ts index 2c0e384..867121d 100644 --- a/DevoteApp/models/user.ts +++ b/DevoteApp/models/user.ts @@ -15,6 +15,13 @@ export interface IUser extends Document { hashIne: string; secretKey: string; + // Email verification fields + isEmailVerified: boolean; + emailVerificationCode: string; + emailVerificationExpires: Date; + emailVerificationAttempts: number; + lastVerificationEmailSent: Date; + // [KYC RESTORE] Uncomment when restoring KYC functionality // kycStatus: "pending" | "inProcess" | "rejected" | "accepted"; // kycId: string; @@ -28,6 +35,13 @@ const UserSchema = new Schema( hashIne: { type: String, required: true }, secretKey: { type: String, required: true }, + // Email verification fields + isEmailVerified: { type: Boolean, default: false }, + emailVerificationCode: { type: String, default: "" }, + emailVerificationExpires: { type: Date }, + emailVerificationAttempts: { type: Number, default: 0 }, + lastVerificationEmailSent: { type: Date }, + // [KYC RESTORE] Uncomment when restoring KYC functionality // kycStatus: { // type: String, @@ -39,6 +53,9 @@ const UserSchema = new Schema( { timestamps: true } ); +// Index for faster verification code lookups +UserSchema.index({ emailVerificationCode: 1 }); +UserSchema.index({ email: 1, emailVerificationCode: 1 }); const User: Model = mongoose.models.User || mongoose.model("User", UserSchema);