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 (
-