From 581cedbd5a6a2a6dc292b5b49ec7ce787a1cdf3b Mon Sep 17 00:00:00 2001 From: Teppy Date: Thu, 5 Dec 2024 13:05:35 -0500 Subject: [PATCH] changes --- foftickets.js | 415 +++++++++++++++++++++++++++++++++++++++++ views/error.ejs | 9 + views/error.ejs~ | 9 + views/notickets.ejs | 9 + views/notickets.ejs~ | 27 +++ views/simpleowner.ejs | 11 ++ views/simpleowner.ejs~ | 28 +++ views/transfer.ejs | 66 +++++++ views/transfer.ejs~ | 28 +++ 9 files changed, 602 insertions(+) create mode 100644 foftickets.js create mode 100644 views/error.ejs create mode 100644 views/error.ejs~ create mode 100644 views/notickets.ejs create mode 100644 views/notickets.ejs~ create mode 100644 views/simpleowner.ejs create mode 100644 views/simpleowner.ejs~ create mode 100644 views/transfer.ejs create mode 100644 views/transfer.ejs~ diff --git a/foftickets.js b/foftickets.js new file mode 100644 index 0000000..915adf6 --- /dev/null +++ b/foftickets.js @@ -0,0 +1,415 @@ +const express = require('express'); +const bodyParser = require('body-parser'); +const session = require('express-session'); +const QRCode=require('qrcode'); +const crypto=require('crypto'); + +const app = express(); +app.set('view engine','ejs'); +app.use(express.json()); +const PORT = 3000; +const PWSalt="!SaltyMagic7283715374"; +const QRSalt="!SaltyMagic5392370662"; + +// +// ToDo, Actions: +// + Login with Password +// + Login with Token +// + Email Link +// Signup +// Set Password +// Issue Tickets +// Transfer Tickets Complicated page. (One ticket: static with transfer and QR code. Multi: buttons to transfer or show QR code) +// Claim Tickets +// + Use Ticket +// Edit Tickets +// +// ToDo, Later +// Use a templating engine +// Store password hashed and salted +// Make all HTML look nice +// Logging and Replay system +// + +function hashPW(pw) { + const hash0=crypto.createHash('sha256'); + const hash1=hash0.update(pw+PWSalt); + const hash=hash1.digest("base64"); + return(hash); +} + +function hashQR(t,username) { + const hash0=crypto.createHash('sha256'); + const hash1=hash0.update(pw+QRSalt); + const hash=hash1.digest("base64").slice(0,6); + return(hash); +} + +// +// In-memory data structures +// +// There are two ways to log in: with a username/password or with a token. +// +const users = { "teppy@egenesis.com" : { password: hashPW("x6321872X.1") , superuser:1 }, + "fallsonfire@gmail.com" : { password: hashPW("indiantreeonfire"), superuser:1 } + }; + +const tickets = { "habitat-1" : { owner: "teppy@egenesis.com" , offered: "", paid: 0.00, used: false }, + "habitat-2" : { owner: "teppy@egenesis.com" , offered: "", paid: 0.00 }, + "habitat-3" : { owner: "ginachen94@gmail.com", offered: "teppy@egenesis.com", paid: 0.00 }, + "habitat-4" : { owner: "teppy@egenesis.com" , offered: "noahstern00@gmail.com", paid: 0.00 }, + "habitat-5" : { owner: "teppy@egenesis.com" , offered: "", paid: 0.00, used: true }, + "habitat-6" : { owner: "teppy@egenesis.com" , offered: "", paid: 0.00 }, + }; + +const tokens = { "abc" : { username: "teppy@egenesis.com", expires: 0 } + }; +const tokensu = { "teppy@egenesis.com" : "abc" + }; + +// Middleware setup +app.use(bodyParser.urlencoded({ extended: true })); +app.use(session({ + secret: 'supersecretkey', + resave: false, + saveUninitialized: false, +})); + +// Middleware to protect routes +function requireLogin(req, res, next) { + if (!req.session.username) { + return res.redirect('/login'); + } + next(); +} + + + + +function generateSecureToken(length = 8) { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let token = ''; + const array = new Uint8Array(length); + crypto.getRandomValues(array); + for (let i = 0; i < length; i++) { + token += characters.charAt(array[i] % characters.length); + } + return token; +} + +const postmark = require('postmark'); +const client = new postmark.ServerClient('f45bcb9f-6556-4420-9e21-05d16739b5e8'); + +app.get('/loginpassword',(req,res) => { + res.send(`

Sign Up

+
+
+
+ +
`); +}) + +app.post('/loginpassword', (req, res) => { + const { username, password } = req.body; + if (username in users && hashPW(password)==users[username].password) { + req.session.username=username; + req.session.superuser=users[username].superuser; + res.redirect('/transfer'); + } else { res.redirect('/transfer'); } +}) + +app.get('/logintoken', (req, res) => { + const tok=req.query.token; + const tokenData = tokens[tok]; + if (!consumeToken(tok)) return res.status(200).send("Missing, Invalid or Expired Token."); + req.session.username = tokenData.username; + req.session.superuser= tokenData.superuser; + res.redirect('/transfer'); + }); + +app.get('/emaillink',(req,res) => { + res.send(` +

Email me a login link

+
+ + ` + ) +}) + +// These can be owned, offered, of offering +function categorizeTickets(username) { + let rval="none"; + let numtickets=0; + let simpleowner=0; + let offering=0; + let simpleoffered=0; + let thet=""; + for (const t in tickets) if (!tickets[t].used) { + if (tickets[t].owner==username && tickets[t].offered=="") simpleowner+=1; + else if (tickets[t].owner==username && tickets[t].offered!=username) offering+=1; + else if (tickets[t].owner!=username && tickets[t].offered==username) simpleoffered+=1; + if (tickets[t].owner==username || tickets[t].offered==username) numtickets+=1; + if (numtickets==1) thet=t; else thet=""; + } + console.log("Simpleowner: "+simpleowner+" Offering "+offering+" SimpleOffered "+simpleoffered+" Numtickets "+numtickets); + if (numtickets==0) return [ "none", ""]; + if (numtickets >1) return [ "complex", ""]; + if (simpleowner+offering+simpleoffered>1) + return [ "error", "" ] + if (simpleowner==1) return [ "simpleowner", thet ]; + if (offering==1) return [ "simpleoffering", thet ]; + if (simpleoffered==1) return [ "simpleoffered", thet ]; + return [ "complex", "" ]; +} + + + +// Big Kahuna +// If you have zero tickets, show something saying that +// For each ticket owned, display options to offer it, use it, or (eventually) pay for it. +// For each ticket offered, show a button to claim it. +// Have a button to claim all tickets, if there are multiple offered to you +app.get('/transfer', requireLogin, async (req,res) => { + let username=req.session.username; + const [ cat, ticket ] = categorizeTickets(username); + const simpledata={ username: username, ticket: ticket }; + console.log("Transfer type "+cat); + if (cat=="none") return res.render("notickets",simpledata); + if (cat=="error") return res.render("error",simpledata); + if (cat=="simpleoffered") return res.render("claimone",simpledata); + if (cat=="simpleoffering") return res.render("retractoffer",simpledata); + if (cat=="complex") { + const edit={ username: username, tickets: {} }; + for (const t in tickets) + if (tickets[t].owner==username || tickets[t].offered==username) { + edit.tickets[t]={}; + edit.tickets[t].owner=tickets[t].owner; + edit.tickets[t].offered=tickets[t].offered; + edit.tickets[t].used=tickets[t].used; + } + return res.render("transfer",edit); + } + if (cat=="simpleowner") { + try { + const hash0=crypto.createHash('sha256'); + const hash1=hash0.update(ticket+QRSalt); + const hash=hash1.digest("base64").slice(0,6); + const dataURL=await QRCode.toDataURL('localhost:3000/useticket?t='+ticket+'&h='+hashQR(ticket,username)); + simpledata.qrcode=dataURL; + return res.render("simpleowner",simpledata); + } + catch(err) { + res.status(500).send('Error generating QR code'); + } + } + if (cat=="complex") return res.render('transfer',simpledata); +}) + +app.post("/toggle", (req,res) => { + console.log("Here is Tickets...",tickets); + const ticket=req.body.ticket; + const isChecked = req.body.checked; + tickets[ticket].used=isChecked; + // Perform any server-side logic here + res.json({ message: 'Checkbox state received', checked: isChecked }); + }) + + +app.get("/useticket",(req,res) => { + let ticket=req.t; + let hash=req.h; + if (hashQR(ticket,req.session.username)!=hash) res.status(500).send("Ticket "+ticket+" was transferred to "+tickets[ticket].username); + else if (tickets[ticket].used) res.status(500).send("Ticket "+ticket+" has already been used."); + else { + tickets[ticket].used=new Date().toISOString(); + res.send("

Welcome, "+tickets[ticket].username+" to Falls on Fire! Ticket "+ticket+" has now been used.

"); + } +}) + + + +function knownEmail(email) { + if (users[email]) return true; + for (const key in tickets) + if (tickets[key].owner==email || tickets[key].offered==email) return true; + return false; +} + +app.post('emaillink',(req,res) => { + if (!knownUser(req.email)) return res.send("Unknown User"); + const tok=createtoken(email); + client.sendEmail({ From: 'tickets@fallsonfire.net', + To: 'teppy@egenesis.com', + Subject: 'Falls on Fire Tickets, Login Link', + TextBody: 'Your Login Link: http://www.fallonfire.net/longinlink?token='+tok, + HtmlBody: '

Your Login Link:

', + }).then(() => { + console.log('Email sent'); + res.send("

Email sent

"); + }).catch((error) => { + console.error('Error sending email:', error); + res.send("

Email failed

"); + }); + res.send("Email link sent"); +}) + + + +app.get('/testemail', + (req,res) => { + client.sendEmail({ From: 'tickets@fallsonfire.net', + To: 'teppy@egenesis.com', + Subject: 'Email from Ticketing System', + TextBody: 'This is a test email.', + HtmlBody: '

This is a test email.

', + }).then(() => { + console.log('Email sent'); + res.send("

Email sent

"); + }).catch((error) => { + console.error('Error sending email:', error); + res.send("

Email failed

"); + })}); + + + + +// Routes +app.get('/', (req, res) => { + if (req.session.username) { + res.send(` +

Welcome, ${req.session.username}

+ Products
+ FAQ
+ Log Out + `); + } else { + res.send(` +

Welcome to our website!

+ Log In
+ Sign Up + `); + } +}); + +app.get('/signup', (req, res) => { + res.send(` +

Sign Up

+ +
+
+ +
+ Log In + `); +}); + + +function consumeToken(tok) { + console.log("ConsumeToken 1"); + if (!(tok in tokens)) return false; + console.log("ConsumeToken 2"); + const rval=tokens[tok].expires==0 || tokens[tok].expires>=Date.now(); + delete tokensu[tokens[tok].username]; + delete tokens[tok]; + return rval; +} + +function createToken(username) { + const token=generateSecureToken(); + tokens[token]={ username: username, expires:0 }; + tokensu[username]=token; + return token; +} + +app.get('/test', (req, res) => { + res.send("Test worked:" + consumeToken("abc")); +}) + + +app.post('/signup', (req, res) => { + const { username, password } = req.body; + if (users[username]) { + return res.send('User already exists. Try again'); + } + users[username] = { password }; + res.redirect('/login'); +}); + +app.get('/changepassword', (req, res) => { + res.send(` +

Change Password

+
+
+
+ +
+ Home + `); +}); + +app.post('/changepassword', (req, res) => { + const { password1, password2 } = req.body; + if (!req.session.username) { + return res.send('You are not logged inBack'); + } + if (password1!=password2) { + return res.send('Passwords do not matchBack'); + } + users[req.session.username].password=hashPW(password1); + res.redirect('/login'); +}); + +app.get('/login', (req, res) => { + res.send(` +

Log In

+
+
+
+ +
+ Sign Up + `); +}); + +app.post('/login', (req, res) => { + const { username, password } = req.body; + if (users[username] && users[username].password === hashPW(password)) { + req.session.username = username; + return res.redirect('/transfer'); + } + res.send('Invalid username or password. Try again'); +}); + +app.get('/logout', (req, res) => { + req.session.destroy(() => { + res.redirect('/'); + }); +}); + +// Protected routes +app.get('/products', requireLogin, (req, res) => { + res.send(` +

Products Page

+

Here is a list of products!

+ Home + `); +}); + +app.get('/tickets', requireLogin, (req, res) => { + res.write(`

Tickets for `+req.session.username+`

`); + res.write(`Home`); + res.end(); +}); + +app.get('/faq', requireLogin, (req, res) => { + res.send(` +

FAQ Page

+

Here are some frequently asked questions.

+ Home + `); +}); + +// Start the server +app.listen(PORT, () => { + console.log(`Server is running at http://localhost:${PORT}`); +}); diff --git a/views/error.ejs b/views/error.ejs new file mode 100644 index 0000000..666f1aa --- /dev/null +++ b/views/error.ejs @@ -0,0 +1,9 @@ + + + + Error Page + + +

Some kind of error

+ + diff --git a/views/error.ejs~ b/views/error.ejs~ new file mode 100644 index 0000000..65cedf5 --- /dev/null +++ b/views/error.ejs~ @@ -0,0 +1,9 @@ + + + + You don't have any tickets + + +

No tickets for you!!

+ + diff --git a/views/notickets.ejs b/views/notickets.ejs new file mode 100644 index 0000000..65cedf5 --- /dev/null +++ b/views/notickets.ejs @@ -0,0 +1,9 @@ + + + + You don't have any tickets + + +

No tickets for you!!

+ + diff --git a/views/notickets.ejs~ b/views/notickets.ejs~ new file mode 100644 index 0000000..c6403f7 --- /dev/null +++ b/views/notickets.ejs~ @@ -0,0 +1,27 @@ + + + + Your Tickets + + +

Welcome, <%= username %>!

+ + + + + + + + <% for (const t in tlist) { %> + + + + + + + <% } %> + + +
TicketOwnerOffered ToQRCode
<%= t %> <%= tlist[t].owner %> <%= tlist[t].offered %> <%= QRCode.toDataURL("https://abc.com", function(err,url) { if (err) { console.error(err); return; } console.log(url); }); %>
+ + diff --git a/views/simpleowner.ejs b/views/simpleowner.ejs new file mode 100644 index 0000000..eb82754 --- /dev/null +++ b/views/simpleowner.ejs @@ -0,0 +1,11 @@ + + + + You have a Ticket! + + +

Welcome, <%= username %>!

+ Your ticket is: QR Code + <%= ticket %> + + diff --git a/views/simpleowner.ejs~ b/views/simpleowner.ejs~ new file mode 100644 index 0000000..f6ac4ef --- /dev/null +++ b/views/simpleowner.ejs~ @@ -0,0 +1,28 @@ + + + + Your Tickets + + +

Welcome, <%= username %>!

+ + + + + + + + <% for (const t in tlist) { %> + + + + + + + + <% } %> + + +
TicketOwnerOffered ToQRCode
<%= t %> <%= tlist[t].owner %> <%= tlist[t].offered %> <%= simpledata %> <%= QRCode.toDataURL("https://abc.com", function(err,url) { if (err) { console.error(err); return; } console.log(url); }); %>
+ + diff --git a/views/transfer.ejs b/views/transfer.ejs new file mode 100644 index 0000000..c19fa18 --- /dev/null +++ b/views/transfer.ejs @@ -0,0 +1,66 @@ + + + + Your Tickets + + +

Welcome, <%= username %>!

+
+ + + + + + + + + <% for (const t in tickets) { %> + + + + + + + + <% } %> + + +
TicketOwnerOffered ToUsedAction
<%=t%><%=tickets[t].owner%>>
+
+ + + diff --git a/views/transfer.ejs~ b/views/transfer.ejs~ new file mode 100644 index 0000000..f6ac4ef --- /dev/null +++ b/views/transfer.ejs~ @@ -0,0 +1,28 @@ + + + + Your Tickets + + +

Welcome, <%= username %>!

+ + + + + + + + <% for (const t in tlist) { %> + + + + + + + + <% } %> + + +
TicketOwnerOffered ToQRCode
<%= t %> <%= tlist[t].owner %> <%= tlist[t].offered %> <%= simpledata %> <%= QRCode.toDataURL("https://abc.com", function(err,url) { if (err) { console.error(err); return; } console.log(url); }); %>
+ +