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}`)); | |