Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ DevoteApp/package-lock.json
DevoteApp/dist/
DevoteApp/.next/
DevoteApp/.env
DevoteApp/data/
.vscode/
node_modules
yarn.lock
Expand Down
9 changes: 8 additions & 1 deletion DevoteApp/.env.local
Original file line number Diff line number Diff line change
@@ -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
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
71 changes: 71 additions & 0 deletions DevoteApp/app/api/resend-otp/route.ts
Original file line number Diff line number Diff line change
@@ -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 = `
<h2>DeVote Email Verification</h2>
<p>Your verification code is:</p>
<h1 style="font-size: 32px; color: #4F46E5; letter-spacing: 8px;">${otp}</h1>
<p>This code will expire in 15 minutes.</p>
<p>If you didn't request this code, please ignore this email.</p>
`;

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 }
);
}
}
8 changes: 6 additions & 2 deletions DevoteApp/app/api/superuser/route.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();
Expand Down
94 changes: 64 additions & 30 deletions DevoteApp/app/api/users/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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 = `<p>A user account has been created for you. Please click the following link to set your password:</p>
<p><a href="${verificationUrl}">${verificationUrl}</a></p>
<p><strong>⚠️ NOTE: Before testing this flow, make sure to send Sepolia ETH to the generated wallet before clicking the link in the email.</strong></p>`;
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 = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #4F46E5;">Welcome to DeVote!</h2>
<p>Your account has been created successfully. To complete your setup, please:</p>

<ol>
<li>Click the verification link below</li>
<li>Enter the verification code</li>
<li>Set your password</li>
</ol>

<div style="background: #F3F4F6; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h3>Your Verification Code:</h3>
<p style="font-size: 24px; font-weight: bold; color: #4F46E5; letter-spacing: 4px; text-align: center;">${otp}</p>
<p style="font-size: 14px; color: #6B7280;">This code will expire in 15 minutes.</p>
</div>

<div style="text-align: center; margin: 30px 0;">
<a href="${verificationUrl}" style="background: #4F46E5; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">Complete Setup</a>
</div>

<p style="font-size: 14px; color: #6B7280;">
If you didn't request this account, please ignore this email.
</p>
</div>
`;

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) {
Expand All @@ -108,4 +142,4 @@ export async function GET(req: Request) {
{ status: 500 }
);
}
}
}
85 changes: 85 additions & 0 deletions DevoteApp/app/api/verify-email/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
);
}
}
23 changes: 23 additions & 0 deletions DevoteApp/app/sign-up/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"use client";

import { Suspense } from "react";
import VerifySignupContent from "./verify-content";

function LoadingFallback() {
return (
<div className="min-h-screen flex items-center justify-center bg-black text-white">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-[#f7cf1d] mx-auto mb-4"></div>
<p>Loading verification page...</p>
</div>
</div>
);
}

export default function VerifySignupPage() {
return (
<Suspense fallback={<LoadingFallback />}>
<VerifySignupContent />
</Suspense>
);
}
Loading