diff --git a/.gitignore b/.gitignore index 08646d7..32534ef 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ DevoteApp/package-lock.json DevoteApp/dist/ DevoteApp/.next/ DevoteApp/.env +DevoteApp/data/ .vscode/ node_modules yarn.lock diff --git a/DevoteApp/.env.local b/DevoteApp/.env.local index 817faa1..b01f0d6 100644 --- a/DevoteApp/.env.local +++ b/DevoteApp/.env.local @@ -1,4 +1,11 @@ NEXT_PUBLIC_SECRET_TOKEN=devote_superuser_secret_2024 NEXT_PUBLIC_API_URL=http://localhost:3000/api DATABASE_URL=postgresql://user:password@localhost:5432/devotedb -STARKNET_RPC_URL=http://localhost:5050 \ No newline at end of file +STARKNET_RPC_URL=http://localhost:5050 +MONGO_URI=mongodb://127.0.0.1:27017/devote +EMAIL_SMTP_HOST=smtp.gmail.com +EMAIL_SMTP_PORT=587 +EMAIL_SMTP_SECURE=false +EMAIL_USER=test@devote.com +EMAIL_PASS=testpassword +FRONTEND_URL=http://localhost:3000 \ No newline at end of file diff --git a/DevoteApp/app/api/resend-otp/route.ts b/DevoteApp/app/api/resend-otp/route.ts new file mode 100644 index 0000000..521fb22 --- /dev/null +++ b/DevoteApp/app/api/resend-otp/route.ts @@ -0,0 +1,71 @@ +import { NextResponse } from "next/server"; +import connectToDb from "../../../lib/mongodb/mongodb"; +import User from "../../../models/user"; +import { generateOTP, getOTPExpiryTime } from "../../../lib/otp"; +import { EmailService } from "../../../lib/email"; + +export async function POST(req: Request) { + try { + const { token } = await req.json(); + + if (!token) { + return NextResponse.json( + { message: "Verification token is required" }, + { status: 400 } + ); + } + + await connectToDb(); + + // Find user by verification token + const user = await User.findOne({ verificationToken: token }).exec(); + if (!user) { + return NextResponse.json( + { message: "Invalid verification token" }, + { status: 400 } + ); + } + + // Check if user already has password set + if (user.passwordSet) { + return NextResponse.json( + { message: "User already completed signup" }, + { status: 400 } + ); + } + + // Generate new OTP + const otp = generateOTP(); + const otpExpiry = getOTPExpiryTime(15); // 15 minutes + + // Update user with new OTP + user.otp = otp; + user.otpExpiry = otpExpiry; + await user.save(); + + // Send OTP email + const emailService = new EmailService(); + const subject = "Your DeVote Verification Code"; + const text = `Your verification code is: ${otp}\n\nThis code will expire in 15 minutes.`; + const html = ` +

DeVote Email Verification

+

Your verification code is:

+

${otp}

+

This code will expire in 15 minutes.

+

If you didn't request this code, please ignore this email.

+ `; + + await emailService.sendMail(user.email, subject, text, html); + + return NextResponse.json( + { message: "OTP sent successfully" }, + { status: 200 } + ); + } catch (error: any) { + console.error("Error resending OTP:", error?.message || error); + return NextResponse.json( + { message: "Internal server error" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/DevoteApp/app/api/superuser/route.ts b/DevoteApp/app/api/superuser/route.ts index bb7ba21..19939a2 100644 --- a/DevoteApp/app/api/superuser/route.ts +++ b/DevoteApp/app/api/superuser/route.ts @@ -1,6 +1,9 @@ import { NextResponse } from "next/server"; import connectToDb from "../../../lib/mongodb/mongodb"; -import User, { KYCStatus } from "../../../models/user"; +import User from "../../../models/user"; + +// [KYC RESTORE] Uncomment when restoring KYC functionality +// import User, { KYCStatus } from "../../../models/user"; export interface superUserRequestType { email: string; @@ -39,7 +42,8 @@ export async function POST(req: Request) { hashIne: hashIne, secretKey: privateKey, isAdmin: true, - kycStatus: KYCStatus.ACCEPTED, + // [KYC RESTORE] Use KYCStatus.ACCEPTED when restoring KYC functionality + // kycStatus: KYCStatus.ACCEPTED, }); await newUser.save(); diff --git a/DevoteApp/app/api/users/route.ts b/DevoteApp/app/api/users/route.ts index 7607c08..30e21ff 100644 --- a/DevoteApp/app/api/users/route.ts +++ b/DevoteApp/app/api/users/route.ts @@ -3,13 +3,12 @@ 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 { - generatePrivateKeyEncrypted, - getFutureWalletAdressFromPrivateKey, -} from "@/lib/starknet/createWallet"; + generateOTP, + generateVerificationToken, + getOTPExpiryTime, +} from "../../../lib/otp"; function hashIne(ine: string): string { return crypto.createHash("sha256").update(ine).digest("hex"); @@ -44,44 +43,79 @@ export async function POST(req: Request) { } const name = `${citizen.firstName} ${citizen.lastName}`; - const privateKey = generatePrivateKeyEncrypted("1234"); - const walletAddress = getFutureWalletAdressFromPrivateKey( - privateKey, - "1234" - ); + // Generate verification token and OTP + const verificationToken = generateVerificationToken(); + const otp = generateOTP(); + const otpExpiry = getOTPExpiryTime(15); // 15 minutes + + // Create user without wallet (wallet will be created after email verification) const newUser = new User({ - walletId: walletAddress, + walletId: "", // Will be set after email verification name, email, hashIne: hashedIne, - // KYC fields removed - lines 30-36, 48, 61-63, 69, 71 deleted - secretKey: privateKey, + secretKey: "", // Will be set after password creation + isEmailVerified: false, + passwordSet: false, + verificationToken, + otp, + otpExpiry, }); 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 with OTP 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 = "Complete Your DeVote Account Setup"; + + const frontendUrl = + process.env.FRONTEND_URL || "https://devote-nine.vercel.app/"; + const verificationUrl = `${frontendUrl}/sign-up?token=${verificationToken}`; + + const text = `Welcome to DeVote! Your account has been created. Please complete your setup by verifying your email and setting your password. + + Verification Code: ${otp} + Verification Link: ${verificationUrl} + + This code will expire in 15 minutes.`; + + const html = ` +
+

Welcome to DeVote!

+

Your account has been created successfully. To complete your setup, please:

+ +
    +
  1. Click the verification link below
  2. +
  3. Enter the verification code
  4. +
  5. Set your password
  6. +
+ +
+

Your Verification Code:

+

${otp}

+

This code will expire in 15 minutes.

+
+ +
+ Complete Setup +
+ +

+ If you didn't request this account, please ignore this email. +

+
+ `; await emailService.sendMail(newUser.email, subject, text, html); return NextResponse.json( - { message: "User created successfully", user: newUser }, + { + message: + "User created successfully. Please check your email to complete setup.", + userId: newUser._id, + requiresVerification: true, + }, { status: 201 } ); } catch (error: any) { @@ -108,4 +142,4 @@ export async function GET(req: Request) { { status: 500 } ); } -} \ No newline at end of file +} diff --git a/DevoteApp/app/api/verify-email/route.ts b/DevoteApp/app/api/verify-email/route.ts new file mode 100644 index 0000000..d4fb119 --- /dev/null +++ b/DevoteApp/app/api/verify-email/route.ts @@ -0,0 +1,85 @@ +import { NextResponse } from "next/server"; +import connectToDb from "../../../lib/mongodb/mongodb"; +import User from "../../../models/user"; +import { isOTPExpired } from "../../../lib/otp"; +import { + generatePrivateKeyEncrypted, + getFutureWalletAdressFromPrivateKey, +} from "@/lib/starknet/createWallet"; + +export async function POST(req: Request) { + try { + const { token, otp, password } = await req.json(); + + if (!token || !otp || !password) { + return NextResponse.json( + { message: "Token, OTP, and password are required" }, + { status: 400 } + ); + } + + await connectToDb(); + + // Find user by verification token + const user = await User.findOne({ verificationToken: token }).exec(); + if (!user) { + return NextResponse.json( + { message: "Invalid verification token" }, + { status: 400 } + ); + } + + // Check if user already has password set + if (user.passwordSet) { + return NextResponse.json( + { message: "Password already set for this user" }, + { status: 400 } + ); + } + + // Verify OTP + if (!user.otp || user.otp !== otp) { + return NextResponse.json( + { message: "Invalid OTP" }, + { status: 400 } + ); + } + + // Check if OTP is expired + if (!user.otpExpiry || isOTPExpired(user.otpExpiry)) { + return NextResponse.json( + { message: "OTP has expired" }, + { status: 400 } + ); + } + + // Generate wallet with user's password + const privateKey = generatePrivateKeyEncrypted(password); + const walletAddress = getFutureWalletAdressFromPrivateKey(privateKey, password); + + // Update user + user.secretKey = privateKey; + user.walletId = walletAddress; + user.isEmailVerified = true; + user.passwordSet = true; + user.otp = undefined; + user.otpExpiry = undefined; + user.verificationToken = undefined; + + await user.save(); + + return NextResponse.json( + { + message: "Email verified and wallet created successfully", + walletAddress: user.walletId + }, + { status: 200 } + ); + } catch (error: any) { + console.error("Error verifying email:", error?.message || error); + return NextResponse.json( + { message: "Internal server error" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/DevoteApp/app/sign-up/page.tsx b/DevoteApp/app/sign-up/page.tsx new file mode 100644 index 0000000..fdd5bee --- /dev/null +++ b/DevoteApp/app/sign-up/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { Suspense } from "react"; +import VerifySignupContent from "./verify-content"; + +function LoadingFallback() { + return ( +
+
+
+

Loading verification page...

+
+
+ ); +} + +export default function VerifySignupPage() { + return ( + }> + + + ); +} \ No newline at end of file diff --git a/DevoteApp/app/sign-up/verify-content.tsx b/DevoteApp/app/sign-up/verify-content.tsx new file mode 100644 index 0000000..eddedb3 --- /dev/null +++ b/DevoteApp/app/sign-up/verify-content.tsx @@ -0,0 +1,227 @@ +"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 { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"; + +export default function VerifySignupContent() { + const [otp, setOtp] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [isResendingOtp, setIsResendingOtp] = useState(false); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + + const searchParams = useSearchParams(); + const router = useRouter(); + const token = searchParams.get("token"); + + useEffect(() => { + if (!token) { + setError("Invalid verification link. Please contact support."); + } + }, [token]); + + const handleResendOtp = async () => { + if (!token) return; + + setIsResendingOtp(true); + setError(""); + + try { + const response = await fetch("/api/resend-otp", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ token }), + }); + + const data = await response.json(); + + if (response.ok) { + setSuccess("A new OTP has been sent to your email address."); + } else { + setError(data.message || "Failed to resend OTP. Please try again."); + } + } catch (error) { + setError("Failed to resend OTP. Please check your internet connection."); + } finally { + setIsResendingOtp(false); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!token || !otp || !password || !confirmPassword) { + setError("Please fill in all fields."); + return; + } + + if (password !== confirmPassword) { + setError("Passwords do not match."); + return; + } + + if (password.length < 8) { + setError("Password must be at least 8 characters long."); + return; + } + + setIsLoading(true); + setError(""); + setSuccess(""); + + try { + const response = await fetch("/api/verify-email", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + token, + otp, + password, + }), + }); + + const data = await response.json(); + + if (response.ok) { + setSuccess("Email verified successfully! Your wallet has been created. Redirecting to login..."); + setTimeout(() => { + router.push("/"); + }, 2000); + } else { + setError(data.message || "Verification failed. Please try again."); + } + } catch (error) { + setError("Verification failed. Please check your internet connection."); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + + + Verify Your Email + + + Enter the verification code sent to your email and create your password + + + +
+ + {error && ( + + + {error} + + + )} + + {success && ( + + + {success} + + + )} + +
+ +
+ setOtp(value)} + disabled={isLoading} + > + + + + + + + + + +
+
+ +
+
+ +
+ + setPassword(e.target.value)} + placeholder="Enter your password" + className="border-gray-600 bg-gray-800 text-white placeholder-gray-400" + disabled={isLoading} + minLength={8} + required + /> +

+ Password must be at least 8 characters long +

+
+ +
+ + setConfirmPassword(e.target.value)} + placeholder="Confirm your password" + className="border-gray-600 bg-gray-800 text-white placeholder-gray-400" + disabled={isLoading} + minLength={8} + required + /> +
+
+ + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/DevoteApp/app/test-login/page.tsx b/DevoteApp/app/test-login/page.tsx deleted file mode 100644 index 45d3f6d..0000000 --- a/DevoteApp/app/test-login/page.tsx +++ /dev/null @@ -1,82 +0,0 @@ -"use client"; - -import Footer from "@/components/Footer"; -import { Button } from "@/components/ui/button"; -import { - generateAndDeployNewWalletFromPrivateKey, - generatePrivateKeyEncrypted, - getFutureWalletAdressFromPrivateKey, -} from "@/lib/starknet/createWallet"; -import { PlusCircle, UserPlus } from "lucide-react"; -import { useState } from "react"; -export default function TestingLoginPage() { - const [privateKey, setPrivateKey] = useState(undefined); - const [walletAddress, setWalletAddress] = useState( - undefined - ); - - const handleGeneratePrivateKey = () => { - const privateKey = generatePrivateKeyEncrypted("1234"); - setPrivateKey(privateKey); - } - - const handleGenerateWallet = () => { - if (!privateKey) { - console.error("No private key found"); - return; - } - const walletAddress = getFutureWalletAdressFromPrivateKey( - privateKey, - "1234" - ); - setWalletAddress(walletAddress); - }; - - const handleDeployWallet = async () => { - if (!privateKey) { - console.error("No private key found"); - return; - } - await generateAndDeployNewWalletFromPrivateKey(privateKey, "1234"); - }; - - return ( -
-
-
-

- Admin Dashboard -

-
- - - {privateKey && ( - - )} - {privateKey && ( - - )} -
- {privateKey &&
private key: {privateKey}
} - {walletAddress &&
Wallet address: {walletAddress}
} -
-
-
-
- ); -} diff --git a/DevoteApp/app/verification-submitted/page.tsx b/DevoteApp/app/verification-submitted/page.tsx index 3541cd5..e805bfc 100644 --- a/DevoteApp/app/verification-submitted/page.tsx +++ b/DevoteApp/app/verification-submitted/page.tsx @@ -73,9 +73,11 @@ function CreatePassword() { const handleConfirm = async () => { try { - // Llamar a la función para desplegar la wallet usando el secretKey obtenido del usuario + // Deploy wallet with user's password console.log("Deploying wallet with secretKey: ", secretKey); - await generateAndDeployNewWalletFromPrivateKey(secretKey, "1234"); + // Note: In this flow, we need to get the actual password from the user + // This appears to be legacy KYC flow - password should be collected from user + await generateAndDeployNewWalletFromPrivateKey(secretKey, password); // Luego, crear el admin en cadena utilizando el userId if (userId) { diff --git a/DevoteApp/hooks/use-contract.tsx b/DevoteApp/hooks/use-contract.tsx index 2a6c2bf..755824b 100644 --- a/DevoteApp/hooks/use-contract.tsx +++ b/DevoteApp/hooks/use-contract.tsx @@ -858,7 +858,13 @@ export function useContractCustom() { const DEVOTE_WALLET_ADDRESS = process.env.NEXT_PUBLIC_DEVOTE_PUBLIC_WALLET ?? ""; const DEVOTE_WALLET_PRIVATE_KEY_ENCRYPTED = process.env.NEXT_PUBLIC_DEVOTE_SECRET_KEY_ENCRYPTED ?? ""; - const DEVOTE_WALLET_PRIVATE_KEY = getDecryptedPrivateKey(DEVOTE_WALLET_PRIVATE_KEY_ENCRYPTED, '1234'); + const DEVOTE_WALLET_ENCRYPTION_KEY = process.env.NEXT_PUBLIC_DEVOTE_ENCRYPTION_KEY; + + if (!DEVOTE_WALLET_ENCRYPTION_KEY) { + throw new Error("NEXT_PUBLIC_DEVOTE_ENCRYPTION_KEY environment variable is required"); + } + + const DEVOTE_WALLET_PRIVATE_KEY = getDecryptedPrivateKey(DEVOTE_WALLET_PRIVATE_KEY_ENCRYPTED, DEVOTE_WALLET_ENCRYPTION_KEY); const provider = new RpcProvider({ nodeUrl: diff --git a/DevoteApp/lib/otp.ts b/DevoteApp/lib/otp.ts new file mode 100644 index 0000000..1ea97ee --- /dev/null +++ b/DevoteApp/lib/otp.ts @@ -0,0 +1,25 @@ +export function generateOTP(length: number = 6): string { + const digits = '0123456789'; + let otp = ''; + + for (let i = 0; i < length; i++) { + otp += digits[Math.floor(Math.random() * digits.length)]; + } + + return otp; +} + +export function generateVerificationToken(): string { + // Simple random token generation without crypto + return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); +} + +export function isOTPExpired(otpExpiry: Date): boolean { + return new Date() > otpExpiry; +} + +export function getOTPExpiryTime(minutes: number = 15): Date { + const expiry = new Date(); + expiry.setMinutes(expiry.getMinutes() + minutes); + return expiry; +} \ No newline at end of file diff --git a/DevoteApp/lib/starknet/createWallet.tsx b/DevoteApp/lib/starknet/createWallet.tsx index 9ba6f11..d8b5106 100644 --- a/DevoteApp/lib/starknet/createWallet.tsx +++ b/DevoteApp/lib/starknet/createWallet.tsx @@ -76,7 +76,7 @@ export const getFutureWalletAdressFromPrivateKey = ( AXConstructorCallData, 0 ); - console.log("✅ Precalculated account address:", AXcontractAddress); + return AXcontractAddress; }; @@ -87,8 +87,13 @@ export const generateAndDeployNewWalletFromPrivateKey = async ( ) => { const DEVOTE_WALLET_ADDRESS = process.env.NEXT_PUBLIC_DEVOTE_PUBLIC_WALLET ?? ""; const DEVOTE_WALLET_PRIVATE_KEY_ENCRYPTED = process.env.NEXT_PUBLIC_DEVOTE_SECRET_KEY_ENCRYPTED ?? ""; + const DEVOTE_WALLET_ENCRYPTION_KEY = process.env.NEXT_PUBLIC_DEVOTE_ENCRYPTION_KEY; - const DEVOTE_WALLET_PRIVATE_KEY = getDecryptedPrivateKey(DEVOTE_WALLET_PRIVATE_KEY_ENCRYPTED, '1234'); + if (!DEVOTE_WALLET_ENCRYPTION_KEY) { + throw new Error("NEXT_PUBLIC_DEVOTE_ENCRYPTION_KEY environment variable is required"); + } + + const DEVOTE_WALLET_PRIVATE_KEY = getDecryptedPrivateKey(DEVOTE_WALLET_PRIVATE_KEY_ENCRYPTED, DEVOTE_WALLET_ENCRYPTION_KEY); const provider = new RpcProvider({ nodeUrl: @@ -131,8 +136,7 @@ export const generateAndDeployNewWalletFromPrivateKey = async ( AXConstructorCallData, 0 ); - console.log("Precalculated account address=", AXcontractAddress); - + contractETH.connect(devoteAccount); const sendETHCall = contractETH.populate("transfer", { recipient: AXcontractAddress, diff --git a/DevoteApp/models/user.ts b/DevoteApp/models/user.ts index 2c0e384..19c2bc6 100644 --- a/DevoteApp/models/user.ts +++ b/DevoteApp/models/user.ts @@ -14,6 +14,11 @@ export interface IUser extends Document { email: string; hashIne: string; secretKey: string; + isEmailVerified: boolean; + passwordSet: boolean; + verificationToken?: string; + otp?: string; + otpExpiry?: Date; // [KYC RESTORE] Uncomment when restoring KYC functionality // kycStatus: "pending" | "inProcess" | "rejected" | "accepted"; @@ -26,7 +31,12 @@ const UserSchema = new Schema( name: { type: String, required: true }, email: { type: String, required: true, unique: true }, hashIne: { type: String, required: true }, - secretKey: { type: String, required: true }, + secretKey: { type: String, default: "" }, + isEmailVerified: { type: Boolean, default: false }, + passwordSet: { type: Boolean, default: false }, + verificationToken: { type: String }, + otp: { type: String }, + otpExpiry: { type: Date }, // [KYC RESTORE] Uncomment when restoring KYC functionality // kycStatus: {