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:
+
+
+ Click the verification link below
+ Enter the verification code
+ Set your password
+
+
+
+
Your Verification Code:
+
${otp}
+
This code will expire in 15 minutes.
+
+
+
+
+
+ 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
+
+
+
+
+
+
+ );
+}
\ 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
-
-
-
- Generate private key
-
-
- {privateKey && (
-
- Generate wallet Address
-
- )}
- {privateKey && (
-
- Deploy Address
-
- )}
-
- {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: {