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 54b1b25..cba6755 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'));
app.use('/api/users', require('./routes/users'));
// 404 handler - catch any routes that don't exist
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/firebase/firestore.js b/frontend/src/firebase/firestore.js
index 02929d2..f4cc411 100644
--- a/frontend/src/firebase/firestore.js
+++ b/frontend/src/firebase/firestore.js
@@ -393,4 +393,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 32ac457..6c2f50a 100644
--- a/frontend/src/pages/ProfilePage.js
+++ b/frontend/src/pages/ProfilePage.js
@@ -5,7 +5,7 @@ import { doc, getDoc, updateDoc } from "firebase/firestore"
import { db } from "../firebase/config"
import { LogOut, Trash2, Edit3, X, Camera } from "lucide-react"
import PropTypes from 'prop-types'
-import { getUserPosts, formatTimestamp, updateItemStatus, updateItem } 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'
import { extractDate, normalizeTimestamp } from "../services/firestoreNormalizer"
@@ -169,13 +169,19 @@ export default function ProfilePage() {
title: '',
description: '',
category: '',
- location: '',
- kind: '',
+ location: '', // only for campus locations
+ kind: '', // 'lost' or 'found'
imageUrl: '',
date: '',
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("")
// New state for profile picture functionality
const [userData, setUserData] = useState(null)
@@ -220,7 +226,10 @@ export default function ProfilePage() {
// Fetch user posts
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.')
@@ -532,11 +541,120 @@ 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}
+ )}