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 - - - - - - - - - React App - - - -
- - + + + + Helping Hand Signup + + + +
+

Helping Hand — Register

+

Please provide honest information. Photos and NID are sensitive — only submit if you consent.

+
+ + + + + +

+
+
+ + + +``` + +--- README / Setup --- +``` +1. Install dependencies: + npm install + +2. Create a 32-byte key (hex) and put it in .env as ENCRYPTION_KEY. Example (generate securely): + openssl rand -hex 32 + +3. Start server: + node server.js + +4. Open http://localhost:3000 in your browser. + +Security reminders: +- Use HTTPS (TLS) in production. +- Store ENCRYPTION_KEY in a secure secrets manager, never commit it to source control. +- Limit who can access stored data; implement authentication for any admin endpoints. +- Store the AES GCM auth tag with ciphertext to allow decryption. +- Consider storing photos in an object store (S3) with restricted access, not on local disk. +- Follow legal requirements for processing personal data in your country. +``` + +--- Next steps I can do for you --- +- Add proper storage of the AES-GCM auth tag and provide a secure decrypt route (admin-only). +- Replace SQLite with MongoDB or provide an S3 example for photo storage. +- Add authentication (JWT or session) and an admin dashboard to view submissions. +- Harden the app for production (helmet CSP, HSTS, CSP, CSP nonce, input sanitization, logging/monitoring). + +If you want any of those, tell me which and I will update the code. + + +# --- Update: Added Admin Login + Vibrant UI Styling --- + +## ✔ Added Features +- **Admin Login Page** (`public/admin-login.html`) +- **Admin Dashboard Page** (`public/admin-dashboard.html`) +- **Vibrant modern UI colors, gradients, rounded cards, nicer typography** +- **Backend routes for login + session-based admin auth** + +> NOTE: This is still a minimal demo. For real-world use you must add strong password hashing, real sessions (Redis-based), HTTPS, and rate-limits. + +--- + +## 🔐 server.js — Add Admin Auth + Session (INSERT BELOW EXISTING CODE) +```javascript +// --- Admin Auth (demo only: in real apps, hash passwords!) --- +const session = require('express-session'); +app.use(session({ + secret: 'supersecretkey-change-this', + resave: false, + saveUninitialized: false, + cookie: { maxAge: 1000 * 60 * 30 } // 30 min +})); + +// demo credentials +const ADMIN_USER = "admin"; +const ADMIN_PASS = "12345"; + +app.post('/admin/login', (req,res) => { + const { username, password } = req.body; + if(username === ADMIN_USER && password === ADMIN_PASS){ + req.session.admin = true; + return res.json({ ok:true }); + } + return res.status(401).json({ error:'Invalid credentials' }); +}); + +function requireAdmin(req,res,next){ + if(!req.session.admin) return res.status(403).json({ error:'Forbidden' }); + next(); +} + +app.get('/admin/data', requireAdmin, (req,res)=>{ + const rows = db.prepare('SELECT id, name, address, photo_path, created_at FROM helpers ORDER BY created_at DESC').all(); + res.json(rows); +}); +``` + +--- + +## 🎨 public/admin-login.html (NEW FILE) +```html + + + + +Admin Login + + + +
+

Admin Login

+ + + +
+
+ + +``` + +--- + +## 📊 public/admin-dashboard.html (NEW FILE) +```html + + + + +Admin Dashboard + + + +

Admin Dashboard

+
+ + + +``` + +--- + +## 🎨 Vibrant UI Update for `public/index.html` +Replace the ` +``` + +--- + +Your website now has: +- A bright, modern gradient background +- Vibrant cards with shadows +- Full **Admin Login** system +- A **Dashboard** with image cards & a responsive grid + +If you want animations, icons, dark mode, or a fully professional dashboard UI, tell me—I can upgrade it further. + +# --- UI UPGRADE PACK v2 — Animations, Neon Buttons, Dark Mode, Mobile-first, Charts --- + +Below are improvements you can paste into your existing project. Everything is added **without breaking old code**. + +--- + +## 🌙 1. Add Dark Mode Toggle (Add to `public/index.html` inside ) +```html + + +``` + +### Add Dark Mode CSS (append to index.html + + + + +# ==== BACKEND FEATURES ADDED: FILTER SEARCH, REVIEWS, GALLERY UPLOAD, ADMIN APPROVAL ==== + +// --- server.js additions --- + +// 1. ADVANCED FILTER SEARCH ENDPOINT +app.get('/api/helpinghands/filter', async (req, res) => { + const { area, rating, employer, age, skill } = req.query; + + let query = "SELECT * FROM helpinghands WHERE 1=1"; + let params = []; + + if (area) { query += " AND area LIKE ?"; params.push(`%${area}%`); } + if (rating) { query += " AND rating >= ?"; params.push(rating); } + if (employer) { query += " AND previousEmployer LIKE ?"; params.push(`%${employer}%`); } + if (age) { query += " AND age = ?"; params.push(age); } + if (skill) { query += " AND skill = ?"; params.push(skill); } + + const results = await db.all(query, params); + res.json({ success: true, results }); +}); + + +// 2. GALLERY IMAGE UPLOAD +import multer from 'multer'; +const upload = multer({ dest: 'uploads/gallery/' }); + +app.post('/api/helpinghands/gallery/upload/:id', upload.single('photo'), async (req, res) => { + const id = req.params.id; + const filename = req.file.filename; + + await db.run("INSERT INTO gallery (handId, file) VALUES (?, ?)", [id, filename]); + + res.json({ success: true, message: "Gallery image uploaded", file: filename }); +}); + + +// 3. REVIEW + RATING WRITE SYSTEM +app.post('/api/helpinghands/review/:id', async (req, res) => { + const id = req.params.id; + const { reviewerName, rating, reviewText } = req.body; + + await db.run( + "INSERT INTO reviews (handId, reviewerName, rating, reviewText, createdAt) VALUES (?, ?, ?, ?, datetime('now'))", + [id, reviewerName, rating, reviewText] + ); + + res.json({ success: true, message: "Review added" }); +}); + +// Get reviews for profile +app.get('/api/helpinghands/reviews/:id', async (req, res) => { + const id = req.params.id; + const result = await db.all("SELECT * FROM reviews WHERE handId = ? ORDER BY createdAt DESC", [id]); + res.json(result); +}); + + +// 4. ADMIN APPROVAL FLOW +app.post('/api/admin/approve/:id', async (req, res) => { + const id = req.params.id; + await db.run("UPDATE helpinghands SET approved = 1 WHERE id = ?", [id]); + res.json({ success: true, message: "Approved" }); +}); + +app.post('/api/admin/reject/:id', async (req, res) => { + const id = req.params.id; + await db.run("UPDATE helpinghands SET approved = -1 WHERE id = ?", [id]); + res.json({ success: true, message: "Rejected" }); +}); + + +// 5. SEO-FRIENDLY PUBLIC PROFILES +// Example: /profile/123 or /profile/asma-begum +app.get('/profile/:slug', async (req, res) => { + const slug = req.params.slug; + + let result = await db.get("SELECT * FROM helpinghands WHERE slug = ?", [slug]); + + if (!result) { + result = await db.get("SELECT * FROM helpinghands WHERE id = ?", [slug]); + } + + if (!result) return res.status(404).send("Profile not found"); + + res.sendFile(path.resolve('public/profile.html')); +}); + + +// --- SQL MIGRATIONS --- +// Add this to your database setup section: + +await db.exec(` +CREATE TABLE IF NOT EXISTS gallery ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + handId INTEGER, + file TEXT +); + +CREATE TABLE IF NOT EXISTS reviews ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + handId INTEGER, + reviewerName TEXT, + rating INTEGER, + reviewText TEXT, + createdAt TEXT +); + +ALTER TABLE helpinghands ADD COLUMN approved INTEGER DEFAULT 0; +ALTER TABLE helpinghands ADD COLUMN slug TEXT; +`); + +