diff --git a/public/index.html b/public/index.html index 7bee027..c672fc6 100644 --- a/public/index.html +++ b/public/index.html @@ -1,40 +1,905 @@ +# Helping-Hand Signup — Fullstack Node.js Example + +This single-file code document contains a complete example project: a secure minimal website where people can submit their **name, photo, address, and NID (National ID)**. It includes a frontend (HTML + JS) and a Node.js/Express backend that: + +- Accepts a file upload (photo) with `multer` +- Encrypts the NID before saving to a local SQLite database +- Validates input on client and server +- Uses basic security middleware (helmet, rate-limiter) + +> **Important security note:** Collecting NID and photos is sensitive. This example demonstrates safer handling (encryption at rest, validation, limited exposure), but it's **not** production-ready. You must deploy over HTTPS, protect access to your encryption key, implement strong authentication & authorization, follow applicable laws (GDPR, local privacy rules), and perform an independent security review before storing real personal data. + +--- + +## Files included (copy each into your project) + +--- package.json --- +```json +{ + "name": "helping-hand-signup", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "better-sqlite3": "^8.4.0", + "express": "^4.18.2", + "helmet": "^6.0.0", + "multer": "^1.4.5-lts.1", + "express-rate-limit": "^6.7.0", + "dotenv": "^16.0.0" + } +} +``` + +--- .env (example) --- +``` +PORT=3000 +DB_FILE=./data/app.db +# 32-byte key in hex (generate securely) e.g. using: openssl rand -hex 32 +ENCRYPTION_KEY=your_64_hex_chars_here +``` + +--- server.js --- +```javascript +// Minimal secure server example +const express = require('express'); +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); +const helmet = require('helmet'); +const rateLimit = require('express-rate-limit'); +const Database = require('better-sqlite3'); +const crypto = require('crypto'); +require('dotenv').config(); + +const app = express(); +const PORT = process.env.PORT || 3000; +const DB_FILE = process.env.DB_FILE || './data/app.db'; +const ENCRYPTION_KEY_HEX = process.env.ENCRYPTION_KEY; // MUST be 32 bytes (64 hex chars) + +if (!ENCRYPTION_KEY_HEX || ENCRYPTION_KEY_HEX.length !== 64) { + console.error('ENCRYPTION_KEY must be a 64-hex-character string (32 bytes). Set it in .env'); + process.exit(1); +} +const ENCRYPTION_KEY = Buffer.from(ENCRYPTION_KEY_HEX, 'hex'); + +// Ensure uploads and data directories +fs.mkdirSync(path.join(__dirname, 'uploads'), { recursive: true }); +fs.mkdirSync(path.join(__dirname, 'data'), { recursive: true }); + +// Set up DB +const db = new Database(DB_FILE); +db.exec(` +CREATE TABLE IF NOT EXISTS helpers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + address TEXT NOT NULL, + photo_path TEXT NOT NULL, + nid_encrypted BLOB NOT NULL, + nid_iv BLOB NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +`); + +// Multer config for photo upload +const storage = multer.diskStorage({ + destination: (req, file, cb) => cb(null, path.join(__dirname, 'uploads')), + filename: (req, file, cb) => { + const ext = path.extname(file.originalname); + const filename = Date.now() + '-' + Math.round(Math.random() * 1e9) + ext; + cb(null, filename); + } +}); + +const upload = multer({ + storage, + limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB limit + fileFilter: (req, file, cb) => { + const allowed = /jpeg|jpg|png/; + const mimetype = allowed.test(file.mimetype); + const ext = allowed.test(path.extname(file.originalname).toLowerCase()); + if (mimetype && ext) cb(null, true); + else cb(new Error('Only JPEG/PNG images are allowed')); + } +}); + +// Security middleware +app.use(helmet()); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(express.static(path.join(__dirname, 'public'))); + +const limiter = rateLimit({ windowMs: 60 * 1000, max: 30 }); // 30 requests per minute per IP +app.use(limiter); + +// Encryption helpers (AES-256-GCM) +function encryptText(plaintext) { + const iv = crypto.randomBytes(12); // recommended 12 bytes for GCM + const cipher = crypto.createCipheriv('aes-256-gcm', ENCRYPTION_KEY, iv); + const encrypted = Buffer.concat([cipher.update(String(plaintext), 'utf8'), cipher.final()]); + const tag = cipher.getAuthTag(); + // store tag + encrypted together or separately + return { encrypted, iv, tag }; +} + +function decryptText(encrypted, iv, tag) { + const decipher = crypto.createDecipheriv('aes-256-gcm', ENCRYPTION_KEY, iv); + decipher.setAuthTag(tag); + const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]); + return decrypted.toString('utf8'); +} + +// POST endpoint to receive form +app.post('/submit', upload.single('photo'), (req, res) => { + try { + const { name, address, nid } = req.body; + if (!name || !address || !nid) { + // remove uploaded file if present + if (req.file) fs.unlinkSync(req.file.path); + return res.status(400).json({ error: 'name, address and nid are required' }); + } + if (!req.file) return res.status(400).json({ error: 'photo is required' }); + + // Basic server-side validation for NID (example: digits only, 10-20 length) — adjust to local rules + if (!/^\d{6,20}$/.test(nid)) { + fs.unlinkSync(req.file.path); + return res.status(400).json({ error: 'NID format looks invalid' }); + } + + // Encrypt the NID + const { encrypted, iv, tag } = encryptText(nid); + + const stmt = db.prepare(`INSERT INTO helpers (name, address, photo_path, nid_encrypted, nid_iv) VALUES (?, ?, ?, ?, ?)`); + stmt.run(name, address, req.file.filename, encrypted, iv); + + return res.json({ ok: true, message: 'Submitted successfully' }); + } catch (err) { + console.error(err); + // cleanup uploaded file on error + if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path); + return res.status(500).json({ error: 'Server error' }); + } +}); + +// Example admin-only endpoint to list entries (DEMO ONLY) +// WARNING: In production, protect this route with strong auth (JWT, session, admin role). Here it's intentionally minimal. +app.get('/admin/list', (req, res) => { + try { + const rows = db.prepare('SELECT id, name, address, photo_path, nid_encrypted, nid_iv, created_at FROM helpers ORDER BY created_at DESC LIMIT 100').all(); + // Do NOT return decrypted NIDs in normal APIs. This is just an example showing how to decrypt if necessary. + const result = rows.map(r => ({ + id: r.id, + name: r.name, + address: r.address, + photo: '/uploads/' + r.photo_path, + created_at: r.created_at + // nid: decryptText(r.nid_encrypted, r.nid_iv, Buffer.alloc(16)) // tag isn't stored separately here in DB — for a full implementation you'd store tag too + })); + res.json(result); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// Serve uploads (ensure directory listings are disabled by your webserver in production) +app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); + +app.listen(PORT, () => console.log(`Server listening on port ${PORT}`)); +``` + +> NOTE: In the example above, we saved `nid_encrypted` and `nid_iv` but **not** the auth tag separately for brevity. For AES-GCM, you must store the tag along with ciphertext (e.g. add a `nid_tag` column) to be able to decrypt. This demo focuses on structure — if you need the full decrypt flow, I can update the example to store & retrieve tags correctly. + +--- public/index.html --- +```html -
- - - - - - - -Please provide honest information. Photos and NID are sensitive — only submit if you consent.
+ +