From 1503af0e28a98d05e77d57a191b338e716f00c73 Mon Sep 17 00:00:00 2001 From: Teppy Date: Sat, 28 Dec 2024 19:31:45 -0500 Subject: [PATCH] changes --- foftickets.js | 230 +++++++++++++++++++++++------------------ public/styles.css | 62 +++-------- views/manytickets.ejs | 16 ++- views/oneticket.ejs | 5 +- views/partials/nav.ejs | 25 ++--- views/zerotickets.ejs | 14 +++ 6 files changed, 180 insertions(+), 172 deletions(-) create mode 100644 views/zerotickets.ejs diff --git a/foftickets.js b/foftickets.js index 81022ea..6867c29 100644 --- a/foftickets.js +++ b/foftickets.js @@ -10,6 +10,7 @@ app.set('view engine','ejs'); app.use(express.json()); app.use(express.static('public')); const PORT = 3000; +const MainURL ="http://localhost:3000"; const PWSalt ="!SaltyMagic7283715374"; const EmailSalt="!SaltyMagic3562196239"; const QRSalt ="!SaltyMagic5392370662"; @@ -26,8 +27,13 @@ const QRSalt ="!SaltyMagic5392370662"; // + Claim Any Tickets // + Purge Revoked Function (Admin) // + Issue Tickets from Camp Admin Page -// See how much each camp/ticket has paid (Admin) +// Make one Update button on manytickets (User) +// Make one Update button on editcamp (Admin) // Send email when new tickets are issued/offered +// Magic-link Login System +// Deactivate individual magic links (User) +// Deactivate individual magic links (Admin) +// See how much each camp/ticket has paid (Admin) // // ToDo, Later // + Use a templating engine @@ -53,7 +59,7 @@ const QRSalt ="!SaltyMagic5392370662"; function hashEmail(email) { const hash0=crypto.createHash('sha256'); - const usersalt=email in users ? (linksalt in users[email] ? users[email].linksalt : "") : ""; + const usersalt=email in users ? (users[email].linksalt ? users[email].linksalt : "") : ""; const hash1=hash0.update(email+EmailSalt+usersalt); const hash=hash1.digest("base64"); return(hash); @@ -73,25 +79,46 @@ function hashQR(t,ownername) { return(hash); } +function GetMagicLink(email) { + return MainURL+"/login?u="+email+"&h="+hashEmail(email); + } + +function MagicLinkValid(email,hash) { + if (HasPW(email)) return false; + return hashEmail(email)==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:true, linksalt:"" }, - "fallsonfire@gmail.com" : { password: hashPW("indiantreeonfire"), superuser:false, linksalt:"boobsarefun" } - }; - +//let users = { "teppy@egenesis.com" : { password: hashPW("x6321872X.1"), superuser:true, linksalt:"" }, +// "fallsonfire@gmail.com" : { password: hashPW("indiantreeonfire"), superuser:false, linksalt:"boobsarefun" } +// }; +// // Status can be "i"=issued, "u"=used, "r"=revoked" -const tickets = { "habitat-1" : { owner: "teppyx@egenesis.com" , offered: "", paid: 0.00, status:"i" }, - "habitat-2" : { owner: "teppyx@egenesis.com" , offered: "", paid: 0.00, status:"u" }, - "habitat-3" : { owner: "ginachen94@gmail.com", offered: "teppyx@egenesis.com", paid: 0.00, status:"i" }, - "habitat-4" : { owner: "teppyx@egenesis.com" , offered: "noahstern00@gmail.com", paid: 0.00, status:"i" }, - "habitat-5" : { owner: "teppyx@egenesis.com" , offered: "", paid: 0.00, status:"r" }, - "habitat-6" : { owner: "teppyx@egenesis.com" , offered: "", paid: 0.00, status:"i" }, - }; +// const tickets = { "habitat-1" : { owner: "teppy@egenesis.com" , offered: "" , paid: 0.00, status:"i" }, +// "habitat-2" : { owner: "teppy@egenesis.com" , offered: "" , paid: 0.00, status:"u" }, +// "habitat-3" : { owner: "ginachen94@gmail.com", offered: "teppy@egenesis.com" , paid: 0.00, status:"i" }, +// "habitat-4" : { owner: "teppy@egenesis.com" , offered: "noahstern00@gmail.com", paid: 0.00, status:"i" }, +// "habitat-5" : { owner: "teppy@egenesis.com" , offered: "" , paid: 0.00, status:"r" }, +// "habitat-6" : { owner: "teppy@egenesis.com" , offered: "" , paid: 0.00, status:"i" }, +// }; +// const camps = { "habitat": { leader: "teppy@egenesis.com", lastid:6 } }; -const camps = { "habitat": { leader: "teppy@egenesis.com", lastid:6 } }; +const users={}; +const tickets={}; +const camps={}; + +function InitDatabase() { + for (const key in users ) delete users[key]; + for (const key in tickets) delete tickets[key]; + for (const key in camps ) delete camps[key]; + users["teppy@egenesis.com" ]= { password: hashPW("x6321872X.1"), superuser:true, linksalt:"" }; + users["fallsonfire@gmail.com"]= { password: hashPW("indiantreeonfire"), superuser:false, linksalt:"" }; + } +InitDatabase(); // Middleware setup app.use(bodyParser.urlencoded({ extended: true })); @@ -164,7 +191,6 @@ app.get('/camps',requireSuperUser, (req,res) => { }) app.post("/camps",requireSuperUser,(req,res) => { - console.log("New camp: "); const campname=req.body.campname; const leader=req.body.leader; const qty=Number(req.body.qty); @@ -173,23 +199,11 @@ app.post("/camps",requireSuperUser,(req,res) => { camps[campname].lastid+=1; // LOG tickets[campname+'-'+camps[campname].lastid]={ owner: "", offered: leader, status:"i" }; // LOG } + EmailTickets(leader); return res.redirect("/camps"); }) -app.get('/camplead', requireLogin, (req,res) => { - let username=req.session.username; - const edit={ username:username, superuser:req.session.superuser, tickets: {} }; - for (const t in tickets) if (tickets[t].owner==username && tickets[t].status!='r') { - edit.tickets[t]={}; - edit.tickets[t].owner=tickets[t].owner; - edit.tickets[t].offered=tickets[t].offered; - edit.tickets[t].used=tickets[t].status=='u'; - } - return res.render("camplead",edit); - }) - - app.get('/editcamp', requireSuperUser, (req,res) => { let campname=req.query.campname; const edit={ username:req.session.username, superuser:req.session.superuser, campname:campname, leader:camps[campname].leader, tickets: {} }; @@ -210,7 +224,6 @@ app.get('/editcamp', requireSuperUser, (req,res) => { app.post('/moretickets', requireSuperUser, (req,res) => { const qty=Number(req.body.qty); const campname=req.body.campname; - console.log("More tickets! ",campname in camps,qty); if (campname in camps) for (let i=0; i { app.get('/mytickets',requireLogin, async (req,res)=> { - console.log("In mytickets req.session is ",req.session); let username=req.session.username; let claimed=0; let owned=0; @@ -247,43 +259,46 @@ app.get('/mytickets',requireLogin, async (req,res)=> { edit.tickets[t].owner=tickets[t].owner; edit.tickets[t].offered=tickets[t].offered; } - } - if (owned==0) return res.render("message",{ username:username, superuser:req.session.superuser, message:"You have no unused tickets" }); + } + let message=""; + if (claimed>0) message="You have claimed "+claimed+" tickets."; + edit.message=message; + if (owned==0) return res.render("zerotickets",{ username:username, superuser:req.session.superuser, message:message }); else if (owned==1) { const hash0=crypto.createHash('sha256'); const hash1=hash0.update(theticket+QRSalt); const hash=hash1.digest("base64").slice(0,6); - const dataURL=await QRCode.toDataURL('localhost:3000/useticket?t='+theticket+'&h='+hashQR(theticket,username)); - return res.render("oneticket",{ username:username, superuser:req.session.superuser, ticket:theticket, offered:tickets[theticket].offered, qrcode:dataURL }); + const useurl='/useticket?t='+theticket+'&h='+hashQR(theticket,username); + const dataURL=await QRCode.toDataURL(useurl); + return res.render("oneticket",{ username:username, superuser:req.session.superuser, message:message, ticket:theticket, offered:tickets[theticket].offered, qrcode:dataURL, useurl:useurl }); } else return res.render("manytickets",edit); }); -app.post('/mytickets',requireLogin, async (req,res)=> { +app.post('/oneticket',requireLogin, async (req,res)=> { let username=req.session.username; let theticket=req.body.ticket; let offered=req.body.offered; + let message=""; if (tickets[theticket].owner==username && tickets[theticket].status=="i") { tickets[theticket].offered=offered; // LOG } const hash0=crypto.createHash('sha256'); const hash1=hash0.update(theticket+QRSalt); const hash=hash1.digest("base64").slice(0,6); - const dataURL=await QRCode.toDataURL('localhost:3000/useticket?t='+theticket+'&h='+hashQR(theticket,username)); - return res.render("oneticket", { username:username, superuser:req.session.superuser, ticket:theticket, offered:tickets[theticket].offered, qrcode:dataURL }); + const dataURL=await QRCode.toDataURL('/useticket?t='+theticket+'&h='+hashQR(theticket,username)); + return res.render("oneticket", { username:username, superuser:req.session.superuser, message:message, magiclink:GetMagicLink(username), ticket:theticket, offered:tickets[theticket].offered, qrcode:dataURL }); }); app.post("/changestatus", requireSuperUser, (req,res) => { const ticket=req.body.ticket; - console.log("Changestatus ",ticket); tickets[ticket].status=req.body.status; // LOG res.json({ message: 'Status received', status: req.body.status }); }) app.post("/updateoffered", requireLogin, (req,res) => { - console.log("UpdateOffered being rubn"); const ticket=req.body.ticket; const offered=req.body.offered; if (tickets[ticket].owner!=req.session.username) res.status(500).send("Ticket "+ticket+" owned by someone else"); @@ -295,6 +310,20 @@ app.post("/updateoffered", requireLogin, (req,res) => { } }) +app.post("/updateoffered2", requireLogin, (req,res) => { + const changes={}; + for (ticket in req.body) if (tickets[ticket] && tickets[ticket].owner==req.session.username && req.body[ticket]!=tickets[ticket].offered) { + if (!(req.body[ticket] in changes)) changes[req.body[ticket]]=0; + changes[req.body[ticket]]++; + } + // Ok to bail here if we need to + for (ticket in req.body) if (tickets[ticket] && tickets[ticket].owner==req.session.username && req.body[ticket]!=tickets[ticket].offered) { + tickets[ticket].offered=req.body[ticket]; + } + for (email in changes) EmailTickets(email); + return res.redirect("manytickets"); + }) + app.post("/updateticketsu", requireSuperUser, (req,res) => { const ticket=req.body.ticket; @@ -307,43 +336,30 @@ app.post("/updateticketsu", requireSuperUser, (req,res) => { 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].status!="i") res.status(500).send("Ticket "+ticket+" has already been used."); - else { - tickets[ticket].status="u"; // LOG - res.send("

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

"); - } + let ticket=req.query.t; + let hash=req.query.h; + if (!tickets[ticket]) return res.status(500).send("Ticket "+ticket+" not found."); + if (hashQR(ticket,tickets[ticket].owner)!=hash) return res.status(500).send("Ticket "+ticket+" was transferred to "+tickets[ticket].owner); + if (tickets[ticket].status!="i") return res.status(500).send("Ticket "+ticket+" has already been used."); + tickets[ticket].status="u"; // LOG + return res.send("

Welcome, "+tickets[ticket].owner+" 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"); -}) - +async function EmailTickets(email) { + let offered=0; + for (const ticket in tickets) if (tickets[ticket].offered==email) offered++; + if (offered==0) return; + const textbody="You have been offered "+offered+" tickets to Falls On Fire! To claim them, visit this link:\n"+GetMagicLink(email); + const htmlbody="You have been offered "+offered+" tickets to Falls On Fire! To claim them, click here."; + await client.sendEmail({ From: "tickets@fallsonfire.net", + To: email, + Subject: "Falls on Fire: You've Got Tickets!", + TextBody: textbody, + HTMLBody: htmlbody + }); + } app.get('/testemail', (req,res) => { @@ -369,10 +385,42 @@ app.get('/', (req, res) => { return res.render("login", { superuser:false }); }); + +function HasPW(username) { + if (!(username in users)) return false; + if (!users[username]) return false; + if (!users[username].password) return false; + if (users[username].password==0) return false; + if (users[username].password=="") return false; + return true; + } + app.get('/login',(req,res) => { - return res.render("login",{ superuser:false }); + const username=req.query.u; + const hash=req.query.h; + if (MagicLinkValid(req.query.u,req.query.h)) return res.redirect("/mytickets"); + return res.render("login",{ message: "Normal Login Required." }); }); +app.post('/login', (req, res) => { + const { username, password, superuser } = req.body; + if (users[username] && users[username].password === hashPW(password)) { + req.session.username = username; + req.session.superuser = users[username].superuser; + const redir=req.session.returnTo; + delete req.session.returnTo; + return res.redirect(redir || "/mytickets"); + } + res.send('Invalid username or password. Try again'); +}); + +app.get('/logout', (req, res) => { + req.session.destroy(() => { + res.redirect('/'); + }); +}); + + app.get('/signup', (req, res) => { res.send(`

Sign Up

@@ -423,55 +471,35 @@ app.post('/changepassword', (req, res) => { }); -app.post('/login', (req, res) => { - const { username, password, superuser } = req.body; - if (users[username] && users[username].password === hashPW(password)) { - req.session.username = username; - req.session.superuser = users[username].superuser; - console.log("In post /login",req.session); - const redir=req.session.returnTo; - delete req.session.returnTo; - return res.redirect(redir || "/mytickets"); - } - res.send('Invalid username or password. Try again'); -}); - -app.get('/logout', (req, res) => { - req.session.destroy(() => { - res.redirect('/'); - }); -}); - app.post('/qrcode',requireLogin,async (req,res) => { const username=req.session.username; const ticket=req.body.ticket; if (tickets[ticket].owner!=username) return res.status(500).send("Only a ticket owner can generate a QR code"); - const URL=await QRCode.toDataURL('localhost:3000/useticket?t='+ticket+'&h='+hashQR(ticket,username)); - return res.send({ owner:tickets[ticket].owner, qrcode: URL }); + const URL=await QRCode.toDataURL('/useticket?t='+ticket+'&h='+hashQR(ticket,username)); + return res.send({ owner:username, qrcode: URL, magiclink:GetMagicLink(username) }); }) app.post('/qrcodesu',requireSuperUser,async (req,res) => { - const username=req.session.username; const ticket=req.body.ticket; - const URL=await QRCode.toDataURL('localhost:3000/useticket?t='+ticket+'&h='+hashQR(ticket,username)); - return res.send({ owner:tickets[ticket].owner, qrcode: URL }); + const username=tickets[ticket].owner; + const URL=await QRCode.toDataURL('/useticket?t='+ticket+'&h='+hashQR(ticket,username)); + return res.send({ owner:username, qrcode: URL, magiclink:GetMagicLink(username) }); }) + app.get('/settings',requireSuperUser, (req,res) => { res.render('settings',{ username:req.session.username, superuser:req.session.superuser, message: "" }) }); app.post('/wipedb',requireSuperUser, (req,res) => { - for (const key in users ) delete users[key]; - for (const key in tickets) delete tickets[key]; - for (const key in camps ) delete camps[key]; + InitDatabase(); res.render('settings',{ username:req.session.username, superuser:req.session.superuser, message: "Wiped the database, but not the logfile." }) }); app.post('/purge',requireSuperUser, (req,res) => { let count=0; for (const t in tickets) if (tickets[t].status=='r') { count++; delete tickets[t]; } - res.render('message',{ username:req.session.username, superuser:req.session.superuser, message: "Purged "+count+" revoked tickets" }) + res.render('settings',{ username:req.session.username, superuser:req.session.superuser, message: "Purged "+count+" revoked tickets" }) }); diff --git a/public/styles.css b/public/styles.css index 94b1716..c21cf32 100644 --- a/public/styles.css +++ b/public/styles.css @@ -24,7 +24,6 @@ .qrcode-image { object-fit: contain; - grid-column:2; } /* Close button */ @@ -36,63 +35,30 @@ cursor: pointer; } -.grid { -} - -/* General Reset */ body { - margin: 0; - font-family: Arial, sans-serif; display: grid; - grid-template-columns: 1fr 3fr; - height: 100vh; /* Full viewport height */ + grid-template-columns: 240px auto; + grid-template-rows: 40px auto; + font-family: Arial, sans-serif; } -/* Sidebar (Nav Links) */ -.nav-links { - background-color: #f4f4f4; /* Light gray background */ - padding: 20px; - box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1); /* Add a slight shadow for separation */ +.nav { grid-column: 1; - min-height: 100vh; /* Stretch to full viewport height */ + grid-row: 1 / 3; + background-color: #FFC0C0; } -.nav-links nav ul { - list-style: none; /* Remove bullet points */ - padding: 0; -} - -.nav-links nav ul li { - margin: 10px 0; /* Add spacing between links */ -} - -.nav-links nav ul li a { - text-decoration: none; /* Remove underline */ - color: #333; -} - -.nav-links nav ul li a:hover { - color: #007BFF; /* Change link color on hover */ +.message { + grid-column: 2; + grid-row: 1; + background-color: #C0FFC0; + align-content: center; + justify-items: center; } .content { grid-column: 2; - flex-grow: 1; /* Allow content to fill the remaining space */ - padding: 20px; + grid-row: 2; + background-color: #C0C0FF; } - -.menu-icon { - cursor: pointer; - display: inline-block; -} - -.menu-icon span { - display: block; - width: 25px; - height: 3px; - margin: 5px 0; - background: #fff; - transition: 0.4s; -} - diff --git a/views/manytickets.ejs b/views/manytickets.ejs index 2da1aeb..6a04351 100644 --- a/views/manytickets.ejs +++ b/views/manytickets.ejs @@ -13,7 +13,7 @@
-
+
Server Ready
Tickets owned by <%=username%> @@ -25,13 +25,14 @@ <% for (const t in tickets) { %> - + <% } %>
<%=t%>
+
diff --git a/views/oneticket.ejs b/views/oneticket.ejs index 77854e7..7955f5e 100644 --- a/views/oneticket.ejs +++ b/views/oneticket.ejs @@ -8,8 +8,9 @@ <%- include('partials/nav') %>
To use <%=ticket%>, scan this QR Code:
- QR Code -
+ QR Code
+ Or visit this URL: "<%=useurl%>" + Or transfer <%=ticket%> to:
diff --git a/views/partials/nav.ejs b/views/partials/nav.ejs index 6a935ca..9475e3f 100644 --- a/views/partials/nav.ejs +++ b/views/partials/nav.ejs @@ -1,14 +1,15 @@ - + Log Out
+
+
+<% if (typeof message !== 'undefined') { %> +

<%= message %>

+<% } %> +
diff --git a/views/zerotickets.ejs b/views/zerotickets.ejs new file mode 100644 index 0000000..ef79613 --- /dev/null +++ b/views/zerotickets.ejs @@ -0,0 +1,14 @@ + + + + No Tickets + + + + <%- include('partials/nav') %> +
+ You don't have any tickets assigned to you. +
+ + +