Skip to content
Closed
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
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
60 changes: 60 additions & 0 deletions backend/email.js
Original file line number Diff line number Diff line change
@@ -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 = `
<div style="font-family: Arial, sans-serif; line-height: 1.5; color: #111827;">
<h2 style="margin:0 0 12px;">${appName} Verification</h2>
<p>Your verification code is:</p>
<p style="font-size: 24px; font-weight: bold; letter-spacing: 4px;">${code}</p>
<p style="color:#6b7280;">This code will expire in 10 minutes.</p>
<hr style="border:none;border-top:1px solid #e5e7eb;margin:16px 0;" />
<p style="font-size:12px;color:#6b7280;">If you did not request this, please ignore this email.</p>
</div>
`;

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 };
1 change: 1 addition & 0 deletions backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
134 changes: 134 additions & 0 deletions backend/routes/verification.js
Original file line number Diff line number Diff line change
@@ -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);
}
Comment on lines +9 to +13
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generateNumericCode function defaults to 6 digits but is called with 4 digits on line 41. Consider removing the default parameter to make the length requirement explicit, or ensure consistency across the codebase.

Copilot uses AI. Check for mistakes.

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;
7 changes: 4 additions & 3 deletions frontend/src/components/ui/ProfileBadge.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<span className={`${baseClasses} ${variantClasses} ${className}`}>
Expand Down
45 changes: 45 additions & 0 deletions frontend/src/firebase/firestore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>}
*/
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<string>} The generated code
*/
export async function generateAndSaveVerificationCode(userId) {
try {
// Generate random 4-digit code
const code = Math.floor(1000 + Math.random() * 9000).toString();
Comment on lines +425 to +426
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code generation logic duplicates the backend's generateNumericCode function. Consider extracting this to a shared utility function or using a consistent approach across frontend and backend.

Copilot uses AI. Check for mistakes.

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;
}
}
Loading