-
Notifications
You must be signed in to change notification settings - Fork 12
Feature/trust and varification #104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e48f686
c146f94
17bc82d
496977c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 }; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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); | ||
| } | ||
|
|
||
| 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; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
|
||
|
|
||
| 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; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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.