diff --git a/api/db/init.sql b/api/db/init.sql
index 27a2f36..c4afde5 100644
--- a/api/db/init.sql
+++ b/api/db/init.sql
@@ -5,6 +5,8 @@ CREATE TABLE IF NOT EXISTS users (
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'free',
+ is_email_verified BOOLEAN DEFAULT false,
+ email_verification_token VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
diff --git a/api/db/migrations/02_app_settings.sql b/api/db/migrations/02_app_settings.sql
index 9ad79b1..c065285 100644
--- a/api/db/migrations/02_app_settings.sql
+++ b/api/db/migrations/02_app_settings.sql
@@ -11,5 +11,10 @@ INSERT INTO app_settings (key, value, category) VALUES
('icici_api_key', '', 'payments'),
('google_safe_browsing_key', '', 'security'),
('google_client_id', '', 'auth'),
- ('apple_service_id', '', 'auth')
+ ('apple_service_id', '', 'auth'),
+ ('smtp_host', 'smtp.office365.com', 'smtp'),
+ ('smtp_port', '587', 'smtp'),
+ ('smtp_user', 'noreply@cavininfotech.com', 'smtp'),
+ ('smtp_pass', 'xmkshjlszjbkcyzb', 'smtp'),
+ ('smtp_secure', 'false', 'smtp')
ON CONFLICT (key) DO NOTHING;
diff --git a/api/package-lock.json b/api/package-lock.json
index f359045..ea5bdf3 100644
--- a/api/package-lock.json
+++ b/api/package-lock.json
@@ -15,6 +15,7 @@
"dotenv": "^17.4.2",
"express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
+ "nodemailer": "^9.0.3",
"pg": "^8.22.0",
"qrcode": "^1.5.4",
"redis": "^6.1.0",
@@ -967,6 +968,15 @@
"node": ">= 0.6"
}
},
+ "node_modules/nodemailer": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz",
+ "integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==",
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
diff --git a/api/package.json b/api/package.json
index 3863260..746b7e2 100644
--- a/api/package.json
+++ b/api/package.json
@@ -16,6 +16,7 @@
"dotenv": "^17.4.2",
"express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
+ "nodemailer": "^9.0.3",
"pg": "^8.22.0",
"qrcode": "^1.5.4",
"redis": "^6.1.0",
diff --git a/api/routes/admin.js b/api/routes/admin.js
index d741846..37536f8 100644
--- a/api/routes/admin.js
+++ b/api/routes/admin.js
@@ -63,6 +63,13 @@ router.put('/users/:id/tier', async (req, res) => {
}
try {
+ if (role === 'pro' || role === 'enterprise') {
+ const userCheck = await pgPool.query('SELECT is_email_verified FROM users WHERE id = $1', [id]);
+ if (userCheck.rows.length === 0) return res.status(404).json({ error: 'User not found' });
+ if (!userCheck.rows[0].is_email_verified) {
+ return res.status(400).json({ error: 'Cannot upgrade tier: User email is not verified' });
+ }
+ }
const result = await pgPool.query(
'UPDATE users SET role = $1 WHERE id = $2 RETURNING id, email, role',
[role, id]
diff --git a/api/routes/auth.js b/api/routes/auth.js
index 5272cc2..cf5988f 100644
--- a/api/routes/auth.js
+++ b/api/routes/auth.js
@@ -2,7 +2,9 @@ const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
+const crypto = require('crypto');
const { Pool } = require('pg');
+const { sendMail } = require('../utils/emailService');
const pgPool = new Pool({
host: process.env.PGHOST || 'postgres',
@@ -72,7 +74,7 @@ router.post('/login', async (req, res) => {
const { verifyToken } = require('../middleware/auth');
router.get('/me', verifyToken, async (req, res) => {
try {
- const userQuery = await pgPool.query('SELECT id, email, role FROM users WHERE id = $1', [req.user.id]);
+ const userQuery = await pgPool.query('SELECT id, email, role, is_email_verified FROM users WHERE id = $1', [req.user.id]);
if (userQuery.rows.length === 0) return res.status(404).json({ error: 'User not found' });
res.json({ user: userQuery.rows[0] });
} catch (err) {
@@ -81,6 +83,61 @@ router.get('/me', verifyToken, async (req, res) => {
}
});
+// Send Verification Email
+router.post('/send-verification', verifyToken, async (req, res) => {
+ try {
+ const userQuery = await pgPool.query('SELECT email, is_email_verified FROM users WHERE id = $1', [req.user.id]);
+ if (userQuery.rows.length === 0) return res.status(404).json({ error: 'User not found' });
+
+ const user = userQuery.rows[0];
+ if (user.is_email_verified) {
+ return res.status(400).json({ error: 'Email is already verified' });
+ }
+
+ const token = crypto.randomBytes(32).toString('hex');
+ await pgPool.query('UPDATE users SET email_verification_token = $1 WHERE id = $2', [token, req.user.id]);
+
+ const baseUrl = process.env.FRONTEND_URL || 'https://qr.houseofwebsites.ai';
+ const verifyLink = `${baseUrl}/verify-email?token=${token}`;
+
+ const html = `
+
Verify Your Email
+ Thanks for using CK-QR! Please verify your email address to unlock Pro tier upgrades.
+ Verify Email
+ Or copy and paste this link:
${verifyLink}
+ `;
+
+ await sendMail(user.email, 'Verify your CK-QR account', html);
+ res.json({ message: 'Verification email sent' });
+
+ } catch (err) {
+ console.error(err);
+ res.status(500).json({ error: 'Failed to send verification email' });
+ }
+});
+
+// Verify Email Token
+router.get('/verify-email/:token', async (req, res) => {
+ const { token } = req.params;
+ if (!token) return res.status(400).json({ error: 'Token is required' });
+
+ try {
+ const result = await pgPool.query(
+ 'UPDATE users SET is_email_verified = true, email_verification_token = NULL WHERE email_verification_token = $1 RETURNING id, email',
+ [token]
+ );
+
+ if (result.rows.length === 0) {
+ return res.status(400).json({ error: 'Invalid or expired token' });
+ }
+
+ res.json({ message: 'Email verified successfully!' });
+ } catch (err) {
+ console.error(err);
+ res.status(500).json({ error: 'Server error during verification' });
+ }
+});
+
// Logout
router.post('/logout', (req, res) => {
res.clearCookie('token');
diff --git a/api/routes/payments.js b/api/routes/payments.js
index 7baa438..f8c355a 100644
--- a/api/routes/payments.js
+++ b/api/routes/payments.js
@@ -1,10 +1,29 @@
const express = require('express');
const router = express.Router();
+const { Pool } = require('pg');
+
+const pgPool = new Pool({
+ host: process.env.PGHOST || 'postgres',
+ port: process.env.PGPORT || 5432,
+ user: process.env.PGUSER || 'postgres',
+ password: process.env.PGPASSWORD || 'postgres',
+ database: process.env.PGDATABASE || 'ckqr',
+});
// Placeholder for ICICI Payment Gateway initialization
router.post('/icici/initiate', async (req, res) => {
const { amount, plan, userId } = req.body;
+ try {
+ const userCheck = await pgPool.query('SELECT is_email_verified FROM users WHERE id = $1', [userId]);
+ if (userCheck.rows.length > 0 && !userCheck.rows[0].is_email_verified) {
+ return res.status(400).json({ error: 'Please verify your email address before upgrading your plan.' });
+ }
+ } catch (err) {
+ console.error('Error checking verification status', err);
+ return res.status(500).json({ error: 'Server error' });
+ }
+
// Here you would construct the ICICI request payload, compute checksums, etc.
// and return the URL or parameters for the frontend to redirect the user to ICICI.
diff --git a/api/utils/emailService.js b/api/utils/emailService.js
new file mode 100644
index 0000000..5acf66e
--- /dev/null
+++ b/api/utils/emailService.js
@@ -0,0 +1,69 @@
+const nodemailer = require('nodemailer');
+const { Pool } = require('pg');
+
+const pgPool = new Pool({
+ host: process.env.PGHOST || 'postgres',
+ port: process.env.PGPORT || 5432,
+ user: process.env.PGUSER || 'postgres',
+ password: process.env.PGPASSWORD || 'postgres',
+ database: process.env.PGDATABASE || 'ckqr',
+});
+
+// Helper to fetch SMTP settings from the database
+async function getSmtpSettings() {
+ const result = await pgPool.query("SELECT key, value FROM app_settings WHERE category = 'smtp'");
+ const settings = {};
+ result.rows.forEach(row => {
+ settings[row.key] = row.value;
+ });
+ return settings;
+}
+
+// Helper to create and configure a transporter
+async function getTransporter() {
+ const settings = await getSmtpSettings();
+
+ return nodemailer.createTransport({
+ host: settings.smtp_host || 'smtp.office365.com',
+ port: parseInt(settings.smtp_port) || 587,
+ secure: settings.smtp_secure === 'true', // true for 465, false for other ports
+ auth: {
+ user: settings.smtp_user,
+ pass: settings.smtp_pass,
+ },
+ tls: {
+ ciphers: 'SSLv3', // sometimes required for Office365
+ rejectUnauthorized: false
+ }
+ });
+}
+
+/**
+ * Sends an email using dynamic SMTP settings from the DB.
+ *
+ * @param {string} to - Recipient email address
+ * @param {string} subject - Email subject
+ * @param {string} html - HTML body of the email
+ */
+async function sendMail(to, subject, html) {
+ const settings = await getSmtpSettings();
+ const transporter = await getTransporter();
+
+ const mailOptions = {
+ from: `"CK-QR Support" <${settings.smtp_user}>`,
+ to: to,
+ subject: subject,
+ html: html
+ };
+
+ try {
+ const info = await transporter.sendMail(mailOptions);
+ console.log('Message sent: %s', info.messageId);
+ return { success: true, messageId: info.messageId };
+ } catch (error) {
+ console.error('Error sending email:', error);
+ throw error;
+ }
+}
+
+module.exports = { sendMail, getSmtpSettings };
\ No newline at end of file
diff --git a/client/src/app/verify-email/page.tsx b/client/src/app/verify-email/page.tsx
new file mode 100644
index 0000000..208f7b8
--- /dev/null
+++ b/client/src/app/verify-email/page.tsx
@@ -0,0 +1,101 @@
+'use client';
+
+import { useEffect, useState, Suspense } from 'react';
+import { useRouter, useSearchParams } from 'next/navigation';
+import api from '@/lib/api';
+import Link from 'next/link';
+
+function VerifyEmailContent() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const token = searchParams.get('token');
+ const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
+ const [message, setMessage] = useState('Verifying your email address...');
+
+ useEffect(() => {
+ if (!token) {
+ setStatus('error');
+ setMessage('No verification token found. Please check your email link.');
+ return;
+ }
+
+ const verifyToken = async () => {
+ try {
+ await api.get(`/auth/verify-email/${token}`);
+ setStatus('success');
+ setMessage('Your email has been verified successfully!');
+
+ // Auto redirect to dashboard after 3 seconds
+ setTimeout(() => {
+ router.push('/dashboard');
+ }, 3000);
+ } catch (err: any) {
+ setStatus('error');
+ setMessage(err.response?.data?.error || 'Failed to verify email. The link may have expired.');
+ }
+ };
+
+ verifyToken();
+ }, [token, router]);
+
+ return (
+
+
+
+ Email Verification
+
+
+
+
+
+ {status === 'loading' && (
+
+ )}
+
+ {status === 'success' && (
+
+
+
{message}
+
Redirecting to your dashboard...
+
+ Go to Dashboard
+
+
+ )}
+
+ {status === 'error' && (
+
+
+
{message}
+
+ Back to Dashboard
+
+
+ )}
+
+
+
+ );
+}
+
+export default function VerifyEmailPage() {
+ return (
+ Loading...}>
+
+
+ )
+}
\ No newline at end of file