Spaces:
Sleeping
Sleeping
| /* Secret-Swap β minimal Express server with flat-file storage */ | |
| import express from 'express'; | |
| import fs from 'fs'; | |
| import path from 'path'; | |
| import crypto from 'crypto'; | |
| import { fileURLToPath } from 'url'; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = path.dirname(__filename); | |
| const USERS_FILE = path.join(__dirname, 'users.json'); | |
| const EXCH_FILE = path.join(__dirname, 'exchanges.json'); | |
| const PORT = process.env.PORT || 3000; | |
| /* ---------- helpers ---------- */ | |
| const readJSON = (f, d = {}) => (fs.existsSync(f) ? JSON.parse(fs.readFileSync(f)) : d); | |
| const writeJSON = (f, o) => fs.writeFileSync(f, JSON.stringify(o, null, 2)); | |
| const sessions = new Map(); // token β username | |
| const genToken = () => crypto.randomUUID(); | |
| function hashPass(pw, salt = crypto.randomBytes(16).toString('hex')) { | |
| const hash = crypto.scryptSync(pw, salt, 64).toString('hex'); | |
| return `${salt}:${hash}`; | |
| } | |
| function checkPass(pw, stored) { | |
| const [salt, ref] = stored.split(':'); | |
| const hash = crypto.scryptSync(pw, salt, 64).toString('hex'); | |
| return crypto.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(ref, 'hex')); | |
| } | |
| /* ---------- tiny templating ---------- */ | |
| const page = (title, body) => `<!DOCTYPE html><html><head><meta charset=utf-8><title>${title}</title><link rel="stylesheet" href="/style.css"></head><body><h1>${title}</h1>${body}</body></html>`; | |
| const input = (n,l,t='text') => `<label>${l}: <input type="${t}" name="${n}" required></label><br>`; | |
| /* ---------- app ---------- */ | |
| const app = express(); | |
| app.use(express.urlencoded({extended:true})); | |
| app.use(express.static(path.join(__dirname,'public'))); | |
| app.use((req, _res, next) => { | |
| const token = (req.headers.cookie||'').split(';').map(c=>c.trim().split('='))[0]?.[1]; | |
| req.user = sessions.get(token); | |
| req.token = token; | |
| next(); | |
| }); | |
| const needAuth = (req,res,next)=>req.user?next():res.redirect('/login'); | |
| /* ---------- auth ---------- */ | |
| app.get('/register', (req,res)=>res.send(page('Register',` | |
| <form method=post action=/register> | |
| ${input('username','Username')} | |
| ${input('password','Password','password')} | |
| <button>Register</button> | |
| </form> | |
| <p><a href=/login>Have an account? Login</a></p>`))); | |
| app.post('/register',(req,res)=>{ | |
| const {username,password}=req.body; | |
| const users=readJSON(USERS_FILE,{}); | |
| if(users[username])return res.send(page('Error','<p>User exists.</p>')); | |
| users[username]=hashPass(password); | |
| writeJSON(USERS_FILE,users); | |
| res.redirect('/login'); | |
| }); | |
| app.get('/login',(req,res)=>res.send(page('Login',` | |
| <form method=post action=/login> | |
| ${input('username','Username')} | |
| ${input('password','Password','password')} | |
| <button>Login</button> | |
| </form> | |
| <p><a href=/register>No account? Register</a></p>`))); | |
| app.post('/login',(req,res)=>{ | |
| const {username,password}=req.body; | |
| const users=readJSON(USERS_FILE,{}); | |
| if(!users[username]||!checkPass(password,users[username])) | |
| return res.send(page('Error','<p>Bad credentials.</p>')); | |
| const token=genToken(); | |
| sessions.set(token,username); | |
| res.setHeader('Set-Cookie',`token=${token}; HttpOnly; Path=/`); | |
| res.redirect('/dashboard'); | |
| }); | |
| app.get('/logout',(req,res)=>{ | |
| if(req.token) sessions.delete(req.token); | |
| res.setHeader('Set-Cookie','token=; Max-Age=0; Path=/'); | |
| res.redirect('/login'); | |
| }); | |
| /* ---------- dashboard ---------- */ | |
| app.get(['/','/dashboard'],needAuth,(req,res)=>{ | |
| const exchanges=readJSON(EXCH_FILE,{}); | |
| const list=Object.entries(exchanges) | |
| .filter(([id,x])=>x.owner===req.user||x.partner===req.user) | |
| .map(([id,x])=>{ | |
| const role = x.owner===req.user? 'owner':'partner'; | |
| return `<li>[${role}] β${x.secret}β β <a href=/respond/${id}>link</a>${x.responses.length?` (${x.responses.length} reply)`:' '}</li>`; | |
| }).join(''); | |
| res.send(page('Dashboard',` | |
| <p>Logged in as <strong>${req.user}</strong> | <a href=/logout>Logout</a></p> | |
| <h2>Create new secret swap</h2> | |
| <form method=post action=/create> | |
| ${input('partner','Partner username')} | |
| ${input('secret','Your secret')} | |
| <button>Create & share</button> | |
| </form> | |
| <h2>Your swaps</h2> | |
| <ul>${list||'<li>(none yet)</li>'}</ul>`)); | |
| }); | |
| /* ---------- create ---------- */ | |
| app.post('/create',needAuth,(req,res)=>{ | |
| const {secret,partner}=req.body; | |
| const users=readJSON(USERS_FILE,{}); | |
| if(!users[partner]) return res.send(page('Error','<p>Partner username not found.</p>')); | |
| const exchanges=readJSON(EXCH_FILE,{}); | |
| const id=crypto.randomUUID(); | |
| exchanges[id]={id,owner:req.user,partner,secret,responses:[]}; | |
| writeJSON(EXCH_FILE,exchanges); | |
| const host=req.headers.host; | |
| res.send(page('Swap created',` | |
| <p>Send this link to <strong>${partner}</strong>:</p> | |
| <p><a href=/respond/${id}>http://${host}/respond/${id}</a></p> | |
| <p><a href=/dashboard>Return to dashboard</a></p>`)); | |
| }); | |
| /* ---------- respond ---------- */ | |
| app.get('/respond/:id',needAuth,(req,res)=>{ | |
| const ex=readJSON(EXCH_FILE,{})[req.params.id]; | |
| if(!ex) return res.send(page('Error','<p>Swap not found.</p>')); | |
| if(req.user!==ex.partner && req.user!==ex.owner) | |
| return res.send(page('Forbidden','<p>You are not part of this swap.</p>')); | |
| if(req.user===ex.owner) | |
| return res.redirect(`/view/${req.params.id}`); | |
| const done=ex.responses.some(r=>r.from===req.user); | |
| if(done) return res.redirect(`/view/${req.params.id}`); | |
| res.send(page('Respond to secret', | |
| `<p>Original secret will be revealed after you submit yours.</p> | |
| <form method=post action=/respond/${req.params.id}> | |
| ${input('response','Your secret')} | |
| <button>Submit</button> | |
| </form>`)); | |
| }); | |
| app.post('/respond/:id',needAuth,(req,res)=>{ | |
| const data=readJSON(EXCH_FILE,{}); | |
| const ex=data[req.params.id]; | |
| if(!ex||req.user!==ex.partner) return res.send(page('Error','<p>Not allowed.</p>')); | |
| ex.responses.push({from:req.user,secret:req.body.response}); | |
| writeJSON(EXCH_FILE,data); | |
| res.send(page('Secret revealed',`<p>Original secret from ${ex.owner}: <strong>${ex.secret}</strong></p> | |
| <p><a href=/dashboard>Back to dashboard</a></p>`)); | |
| }); | |
| /* ---------- view (owner) ---------- */ | |
| app.get('/view/:id',needAuth,(req,res)=>{ | |
| const ex=readJSON(EXCH_FILE,{})[req.params.id]; | |
| if(!ex||req.user!==ex.owner) return res.send(page('Error','<p>Not allowed.</p>')); | |
| const list=ex.responses.map(r=>`<li>${r.from}: ${r.secret}</li>`).join('')||'<li>(no response yet)</li>'; | |
| res.send(page('Swap responses',` | |
| <p>Your secret: <strong>${ex.secret}</strong></p> | |
| <ul>${list}</ul> | |
| <p><a href=/dashboard>Back to dashboard</a></p>`)); | |
| }); | |
| /* ---------- start ---------- */ | |
| app.listen(PORT,()=>console.log(`Secret-Swap listening on ${PORT}`)); | |