SecretSwap / server.js
mlmPenguin's picture
Upload 6 files
badff6c verified
/* 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}`));