From e48f6868433fdfedab9aa93a33dd6facac0ad8ee Mon Sep 17 00:00:00 2001 From: Minseo421 <162323193+Minseo421@users.noreply.github.com> Date: Sun, 19 Oct 2025 18:34:03 +1300 Subject: [PATCH 1/3] Verification frontend UI update with typing in upi and 4 digits of varification code --- README.md | 2 - backend/email.js | 60 +++++++++ backend/index.js | 1 + backend/package-lock.json | 10 ++ backend/package.json | 1 + backend/routes/verification.js | 134 +++++++++++++++++++++ frontend/src/components/ui/ProfileBadge.js | 7 +- frontend/src/pages/ProfilePage.js | 116 +++++++++++++++++- 8 files changed, 322 insertions(+), 9 deletions(-) create mode 100644 backend/email.js create mode 100644 backend/routes/verification.js diff --git a/README.md b/README.md index cc29abc..adbd023 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,6 @@ The Lost & Found Community Platform (Lost No More) is a web application that hel --- -## 🛠️ Tech Stack - - **Frontend**: React + Tailwind CSS - **Auth / Database / Storage**: Firebase — the app uses Firebase Authentication for user sign-in, Cloud Firestore for application data, and Firebase Storage for item images. - **Backend**: Node.js + Express — a lightweight API server is included (health endpoints). The frontend currently communicates directly with Firebase; the backend contains minimal Express code and `firebase-admin` is available in package.json for optional server-side admin tasks. diff --git a/backend/email.js b/backend/email.js new file mode 100644 index 0000000..8c2be31 --- /dev/null +++ b/backend/email.js @@ -0,0 +1,60 @@ +const nodemailer = require('nodemailer'); + +// Create a reusable transporter object using SMTP transport if env vars are present +function createTransporter() { + const { SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS } = process.env; + if (SMTP_HOST && SMTP_PORT && SMTP_USER && SMTP_PASS) { + return nodemailer.createTransport({ + host: SMTP_HOST, + port: Number(SMTP_PORT), + secure: Number(SMTP_PORT) === 465, // true for 465, false for other ports + auth: { + user: SMTP_USER, + pass: SMTP_PASS, + }, + }); + } + return null; +} + +const transporter = createTransporter(); + +/** + * Send a verification code email. + * In development (or when SMTP is not configured), this will log the email to console instead. + * @param {string} toEmail - Recipient email address + * @param {string} code - Verification code + * @param {object} options - Additional options + */ +async function sendVerificationCodeEmail(toEmail, code, options = {}) { + const from = process.env.FROM_EMAIL || 'no-reply@lost-found.local'; + const subject = options.subject || 'Your verification code'; + const appName = options.appName || 'Lost & Found'; + + const text = `Hello, + +Your ${appName} verification code is: ${code} +This code will expire in 10 minutes. + +If you did not request this, please ignore this email.`; + const html = ` +
+

${appName} Verification

+

Your verification code is:

+

${code}

+

This code will expire in 10 minutes.

+
+

If you did not request this, please ignore this email.

+
+ `; + + if (!transporter) { + console.log('Email transport not configured. Would send email:', { toEmail, subject, text }); + return { mocked: true }; + } + + const info = await transporter.sendMail({ from, to: toEmail, subject, text, html }); + return info; +} + +module.exports = { sendVerificationCodeEmail }; diff --git a/backend/index.js b/backend/index.js index 9fa7505..ec3a05c 100644 --- a/backend/index.js +++ b/backend/index.js @@ -109,6 +109,7 @@ app.locals.storage = admin.storage(); app.use('/api/items', require('./routes/items')); app.use('/api/messages', require('./routes/messages')); app.use('/api/notifications', require('./routes/notifications')); +app.use('/api/verification', require('./routes/verification')); // 404 handler - catch any routes that don't exist app.use('*', (req, res) => { diff --git a/backend/package-lock.json b/backend/package-lock.json index 6575d18..a77ba7c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,6 +16,7 @@ "firebase": "^12.0.0", "firebase-admin": "^13.4.0", "multer": "^2.0.2", + "nodemailer": "^6.9.15", "uuid": "^11.1.0" }, "devDependencies": { @@ -2552,6 +2553,15 @@ "node": ">= 6.13.0" } }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", diff --git a/backend/package.json b/backend/package.json index 853c968..9084cf4 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,6 +26,7 @@ "express": "^4.21.2", "firebase": "^12.0.0", "firebase-admin": "^13.4.0", + "nodemailer": "^6.9.15", "multer": "^2.0.2", "uuid": "^11.1.0" }, diff --git a/backend/routes/verification.js b/backend/routes/verification.js new file mode 100644 index 0000000..4afb1d1 --- /dev/null +++ b/backend/routes/verification.js @@ -0,0 +1,134 @@ +const express = require('express'); +const crypto = require('node:crypto'); +const authenticate = require('../middleware/auth'); +const { sendVerificationCodeEmail } = require('../email'); + +const router = express.Router(); + +// Helper to generate a numeric code of given length +function generateNumericCode(length = 6) { + const min = Math.pow(10, length - 1); + const max = Math.pow(10, length) - 1; + return String(Math.floor(Math.random() * (max - min + 1)) + min); +} + +function hashCode(code, salt) { + return crypto.createHash('sha256').update(`${code}:${salt}`).digest('hex'); +} + +// Request a verification code sent to user's university email +router.post('/request-code', authenticate, async (req, res) => { + try { + const db = req.app.locals.db; + const { upi } = req.body || {}; + const user = req.user; // Firebase decoded token + + if (!upi || typeof upi !== 'string' || upi.trim().length < 3) { + return res.status(400).json({ message: 'Invalid UPI' }); + } + + // Build university email from UPI (assumption per requirements) + const email = `${upi}@aucklanduni.ac.nz`; + + const usersRef = db.collection('users').doc(user.uid); + const userSnap = await usersRef.get(); + if (!userSnap.exists) { + return res.status(404).json({ message: 'User not found' }); + } + + const now = new Date(); + const expiresAt = new Date(now.getTime() + 10 * 60 * 1000); // 10 minutes + const code = generateNumericCode(4); // As per example 5678 + const salt = crypto.randomBytes(8).toString('hex'); + const codeHash = hashCode(code, salt); + + // Store pending verification state on the user document + await usersRef.set({ + upi, + verification: { + method: 'email', + target: email, + codeHash, + salt, + expiresAt: expiresAt.toISOString(), + attempts: 0, + status: 'pending', + requestedAt: now.toISOString(), + } + }, { merge: true }); + + // Send the email + await sendVerificationCodeEmail(email, code, { appName: 'Lost & Found' }); + + return res.json({ message: 'Verification code sent', target: email, expiresAt }); + } catch (err) { + console.error('request-code error:', err); + return res.status(500).json({ message: 'Failed to send verification code' }); + } +}); + +// Verify a code +router.post('/verify-code', authenticate, async (req, res) => { + try { + const db = req.app.locals.db; + const { code } = req.body || {}; + const user = req.user; + + if (!code || typeof code !== 'string') { + return res.status(400).json({ message: 'Code is required' }); + } + + const usersRef = db.collection('users').doc(user.uid); + const userSnap = await usersRef.get(); + if (!userSnap.exists) { + return res.status(404).json({ message: 'User not found' }); + } + + const data = userSnap.data() || {}; + const vf = data.verification; + if (!vf || !vf.codeHash || !vf.salt || !vf.expiresAt) { + return res.status(400).json({ message: 'No pending verification' }); + } + + // Check expiry + const now = new Date(); + const exp = new Date(vf.expiresAt); + if (now > exp) { + await usersRef.set({ verification: { ...vf, status: 'expired' } }, { merge: true }); + return res.status(400).json({ message: 'Code expired' }); + } + + // Check attempts limit + const attempts = Number(vf.attempts || 0); + if (attempts >= 5) { + await usersRef.set({ verification: { ...vf, status: 'locked' } }, { merge: true }); + return res.status(429).json({ message: 'Too many attempts. Please request a new code.' }); + } + + // Verify code + const attemptedHash = hashCode(code, vf.salt); + if (attemptedHash !== vf.codeHash) { + await usersRef.set({ verification: { ...vf, attempts: attempts + 1 } }, { merge: true }); + return res.status(400).json({ message: 'Invalid code' }); + } + + // Mark verified and clear sensitive fields + await usersRef.set({ + isVerified: true, + trustBadge: 'verified', + verification: { + method: 'email', + target: vf.target, + status: 'verified', + verifiedAt: new Date().toISOString() + } + }, { merge: true }); + + return res.json({ message: 'Verification successful', isVerified: true }); + } catch (err) { + console.error('verify-code error:', err); + return res.status(500).json({ message: 'Failed to verify code' }); + } +}); + +module.exports = router; diff --git a/frontend/src/components/ui/ProfileBadge.js b/frontend/src/components/ui/ProfileBadge.js index 424a38b..2b6c107 100644 --- a/frontend/src/components/ui/ProfileBadge.js +++ b/frontend/src/components/ui/ProfileBadge.js @@ -2,9 +2,10 @@ import React from 'react' export function ProfileBadge({ children, variant = 'default', className = "" }) { const baseClasses = "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium" - const variantClasses = variant === 'outline' - ? "border border-gray-200 text-gray-700" - : "bg-gray-100 text-gray-800" + let variantClasses = "bg-gray-100 text-gray-800"; + if (variant === 'outline') variantClasses = "border border-gray-200 text-gray-700"; + if (variant === 'success') variantClasses = "bg-green-100 text-green-800"; + if (variant === 'danger') variantClasses = "bg-red-100 text-red-800"; return ( diff --git a/frontend/src/pages/ProfilePage.js b/frontend/src/pages/ProfilePage.js index 2f0d82f..1f92c65 100644 --- a/frontend/src/pages/ProfilePage.js +++ b/frontend/src/pages/ProfilePage.js @@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom" import { getAuth, signOut, onAuthStateChanged } from "firebase/auth" import { LogOut, Trash2, Edit3, X } from "lucide-react" import PropTypes from 'prop-types' -import { getUserPosts, formatTimestamp, updateItemStatus, updateItem } from "../firebase/firestore" +import { getUserPosts, formatTimestamp, updateItemStatus, updateItem, getUserProfile } from "../firebase/firestore" import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card" import { ProfileBadge } from '../components/ui/ProfileBadge' @@ -164,6 +164,12 @@ export default function ProfilePage() { time: '' }) const [error, setError] = useState(null) + const [profile, setProfile] = useState(null) + const [upiInput, setUpiInput] = useState("") + const [codeInput, setCodeInput] = useState("") + const [sendingCode, setSendingCode] = useState(false) + const [verifying, setVerifying] = useState(false) + const [verificationMessage, setVerificationMessage] = useState("") const auth = getAuth() // Track auth user in state so changes trigger re-renders and effects @@ -189,7 +195,10 @@ export default function ProfilePage() { try { setLoading(true) const posts = await getUserPosts(currentUser.uid) + const prof = await getUserProfile(currentUser.uid) setMyPosts(posts) + setProfile(prof) + setUpiInput(prof?.upi || "") } catch (err) { console.error('Error fetching user data:', err) setError('Failed to load your data. Please try again.') @@ -355,11 +364,110 @@ export default function ProfilePage() { Trust & Verification - Unverified + {profile?.isVerified ? ( + Verified + ) : ( + Unverified + )} - - Connect university SSO to verify identity and earn trust badges for faster claims and higher credibility. + +

Verify your identity with your UPI to earn a trust badge. We'll send a 4-digit code to your university email.

+
+
+ + setUpiInput(e.target.value)} + placeholder="e.g. hlee345" + className="w-full border rounded px-3 py-2 text-sm" + /> +
+
+ +
+
+ +
+ setCodeInput(e.target.value)} + placeholder="4-digit code" + className="w-full border rounded px-3 py-2 text-sm" + /> + +
+
+
+ {verificationMessage && ( +

{verificationMessage}

+ )}
From c146f94581a25361a37b26623dbf7ab07802e9e8 Mon Sep 17 00:00:00 2001 From: Minseo421 <162323193+Minseo421@users.noreply.github.com> Date: Sun, 19 Oct 2025 20:02:43 +1300 Subject: [PATCH 2/3] send code: adding upi in firebase and generating random 4-digit code and store it in firebase --- frontend/src/firebase/firestore.js | 45 ++++++++++++++++++++++++++++++ frontend/src/pages/ProfilePage.js | 12 +++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/frontend/src/firebase/firestore.js b/frontend/src/firebase/firestore.js index 627504b..d73e9e8 100644 --- a/frontend/src/firebase/firestore.js +++ b/frontend/src/firebase/firestore.js @@ -390,4 +390,49 @@ export async function updateItem(itemId, updateData) { console.error('Error updating item:', error); throw error; } +} + +/** + * Update user's UPI in Firestore + * @param {string} userId - The user's UID + * @param {string} upi - The UPI to save + * @returns {Promise} + */ +export async function updateUserUpi(userId, upi) { + try { + const userRef = doc(db, 'users', userId); + await updateDoc(userRef, { + upi: upi, + updatedAt: new Date() + }); + console.log('User UPI updated successfully:', userId, upi); + } catch (error) { + console.error('Error updating user UPI:', error); + throw error; + } +} + +/** + * Generate and save a random 4-digit verification code to Firestore + * @param {string} userId - The user's UID + * @returns {Promise} The generated code + */ +export async function generateAndSaveVerificationCode(userId) { + try { + // Generate random 4-digit code + const code = Math.floor(1000 + Math.random() * 9000).toString(); + + const userRef = doc(db, 'users', userId); + await updateDoc(userRef, { + verificationCode: code, + codeGeneratedAt: new Date(), + updatedAt: new Date() + }); + + console.log('Verification code saved to Firestore:', code); + return code; + } catch (error) { + console.error('Error saving verification code:', error); + throw error; + } } \ No newline at end of file diff --git a/frontend/src/pages/ProfilePage.js b/frontend/src/pages/ProfilePage.js index 1f92c65..03de338 100644 --- a/frontend/src/pages/ProfilePage.js +++ b/frontend/src/pages/ProfilePage.js @@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom" import { getAuth, signOut, onAuthStateChanged } from "firebase/auth" import { LogOut, Trash2, Edit3, X } from "lucide-react" import PropTypes from 'prop-types' -import { getUserPosts, formatTimestamp, updateItemStatus, updateItem, getUserProfile } from "../firebase/firestore" +import { getUserPosts, formatTimestamp, updateItemStatus, updateItem, getUserProfile, updateUserUpi, generateAndSaveVerificationCode } from "../firebase/firestore" import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card" import { ProfileBadge } from '../components/ui/ProfileBadge' @@ -393,6 +393,15 @@ export default function ProfilePage() { setVerificationMessage("") setSendingCode(true) try { + // Step 1: Save UPI to Firestore first + await updateUserUpi(currentUser.uid, upiInput.trim()) + console.log('UPI saved to Firestore:', upiInput.trim()) + + // Step 2: Generate and save random 4-digit code to Firestore + const generatedCode = await generateAndSaveVerificationCode(currentUser.uid) + console.log('Random 4-digit code generated and saved:', generatedCode) + + // Step 3: Request verification code from backend const token = await currentUser.getIdToken() const resp = await fetch(`${process.env.REACT_APP_API_BASE || 'http://localhost:5876'}/api/verification/request-code`, { method: 'POST', @@ -406,6 +415,7 @@ export default function ProfilePage() { if (!resp.ok) throw new Error(data.message || 'Failed to send code') setVerificationMessage(`Code sent to ${data.target}`) } catch (e) { + console.error('Error sending code:', e) setVerificationMessage(e.message) } finally { setSendingCode(false) From 17bc82d49e91ddabdc1a96c3f2366b2b6e3a66bf Mon Sep 17 00:00:00 2001 From: Minseo421 <162323193+Minseo421@users.noreply.github.com> Date: Mon, 20 Oct 2025 11:46:22 +1300 Subject: [PATCH 3/3] little comments on the ProfilePage --- frontend/src/pages/ProfilePage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/ProfilePage.js b/frontend/src/pages/ProfilePage.js index 03de338..393fa9f 100644 --- a/frontend/src/pages/ProfilePage.js +++ b/frontend/src/pages/ProfilePage.js @@ -157,7 +157,7 @@ export default function ProfilePage() { title: '', description: '', category: '', - location: '', + location: '', // only for campus locations kind: '', // 'lost' or 'found' imageUrl: '', date: '',