const express = require('express'); const bodyParser = require('body-parser'); const session = require('express-session'); const cookieParser = require('cookie-parser'); const QRCode=require('qrcode'); const crypto=require('crypto'); const path=require('path'); const fs = require('fs'); const multer = require("multer"); const upload = multer(); const csvParse = require("csv-parse"); require('dotenv').config(); const port=process.env.PORT||3000; const base_url = process.env.BASE_URL; const stripe=require('stripe')(process.env.STRIPE_SECRET_KEY); const app = express(); app.set('view engine','ejs'); app.use(express.json()); app.use(express.static('public')); app.use(cookieParser()); app.use(session({ secret: 'supersecretkey', resave: false, saveUninitialized: false, })); const PORT = 3000; const MainURL ="http://localhost:3000"; const PWSalt ="!SaltyMagic7283715374"; const EmailSalt="!SaltyMagic3562196239"; const QRSalt ="!SaltyMagic5392370662"; // // ToDo, Actions: // + Login with Password // + Login with Token // + Email Link // + Display Many Tickets (User) // + Display One Ticket (User) // + Camp Editor // + Use Ticket // + Claim Any Tickets // + Purge Revoked Function (Admin) // + Issue Tickets from Camp Admin Page // + Send email when new tickets are issued/offered // + Make one Update button on editcamp (Admin) // + Make one Update button on manytickets (User) // + Turn ticket use on/off from Settings (Admin) // + Turn email on/off from Settings (Admin) // + Magic-link Login System // + Convert all the routes to use common.(user,superuser,etc) // + Display messages for all GET routes? // + Setting to deactivate transfers globally // + Mass-import of individual tickets // + Cookie based QR code functionality // + Create Account (User) // + Change Password (User) // + Change most POST routes to end in redirect instead of render. // + See how much each camp/ticket has paid (Admin) // + Purchase individual open camping tickets // + Deactivate individual magic links (Admin) // + Get it working again online // Make all HTML look nice // If you only have a magic link, supress "Change Password" // Once a ticket is used, disallow transfer (oneticket) // Once a ticket is used, disallow transfer (manytickets) // Why does the live version redirect back to localhost? // Make sure open camping tickets actually send email // Don't hardcode MainURL and PORT // Limit number of Open Camping tickets // + Make sure subsequent FB imports don't add duplicate tickets // Maybe: // Deactivate individual magic links (User) // Option to "Email me my QR Code" // // ToDo, Later // + Use a templating engine // + Store password hashed and salted // + Stripe Integration // Logging and Replay system(?) // More efficent data structure: TicketsByCamp, TicketsByOffered, TicketsByOwner // // // Login workflow: // There is always an implicit magic link that allows anyone to log in with their email address and hash(GlobalSalt+PerUserSalty+Email), except if the user sets a password for himself. // We store a per-user salt, initially the empty string, in case we need to invalidate particular users' magic links. // // // Commands // ISSUE campname email // USE ticket // STATUS ticket i/u/r // CLAIM ticket email // // // Stripe Integration: // The Stripe CLI is configured for Andrew Tepper with account id acct_1QZlMSCHHjDgpHos // Login on stripe.com is tickets@fallsonfire.net Password is x65195241X.1 // function base64ToBase64Url(base64) { return base64 .replace(/\+/g, '-') // Replace '+' with '-' .replace(/\//g, '_') // Replace '/' with '_' .replace(/=+$/, ''); // Remove trailing '=' } function hashEmail(email) { const hash0=crypto.createHash('sha256'); const usersalt=email in users ? (users[email].linksalt ? users[email].linksalt : "") : ""; const hash1=hash0.update(email+EmailSalt+usersalt); const hash=hash1.digest("base64"); return base64ToBase64Url(hash); } function hashPW(pw) { const hash0=crypto.createHash('sha256'); const hash1=hash0.update(pw+PWSalt); const hash=hash1.digest("base64"); return(hash); } function hashQR(t,ownername) { const hash0=crypto.createHash('sha256'); const hash1=hash0.update(t+QRSalt+ownername); const hash=base64ToBase64Url(hash1.digest("base64")).slice(0,6); return(hash); } function GetMagicLink(email) { return MainURL+"/login?u="+email+"&h="+hashEmail(email); } function MagicLinkValid(email,hash) { if (HasPW(email)) return false; return hash==hashEmail(email); } app.use((req, res, next) => { res.locals.commonData = { username: req.username, // Attach user info if available superuser: req.superuser, settings: settings, error: req.session && req.session.error || null, // Flash error messages message: req.session && req.session.message || null, // Flash success messages }; // Clear session-based flash messages after use if (req.session) { delete req.session.error; delete req.session.message; } next(); }); // // In-memory data structures // // There are two ways to log in: with a username/password or with a token. // //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: "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 } }; let users={}; let tickets={}; let camps={}; let settings={ "enable-transfer":true, "open-limit":0 }; 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["ginachen94@gmail.com" ]= { password: hashPW("indiantreeonfire"),superuser:true, linksalt:"" }; camps["open"]={ leader:"", lastid:0 }; } InitDatabase(); function SerializeAll() { const tables={ users, tickets, camps, settings }; fs.writeFileSync('foftickets.json', JSON.stringify(tables, null, 2), 'utf8'); } function DeserializeAll() { const data = fs.readFileSync('foftickets.json', 'utf8'); const tables = JSON.parse(data); users=tables.users; tickets=tables.tickets; camps=tables.camps; settings=tables.settings; } // Middleware setup app.use(bodyParser.urlencoded({ extended: true })); // Middleware to protect routes function requireLogin(req, res, next) { if (req.session.username) return next(); req.session.returnTo=req.originalUrl; return res.redirect('/login'); } function requireSuperUser(req,res,next) { if (req.session.superuser) return next(); req.session.returnTo=req.originalUrl; return res.redirect('/login'); } app.get('/styles.css', (req, res) => { res.setHeader('Content-Type', 'text/css'); res.sendFile(path.join(__dirname, 'public', 'styles.css')); }); 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('/emaillink',(req,res) => { res.send(`

Email me a login link

` ) }); app.get('/camps',requireSuperUser, (req,res) => { const camplist={}; for (const c in camps) { camplist[c]={ leader:camps[c].leader, issued:0, claimed:0, used:0, npaid:0, paid:0 }; } for (const t in tickets) { const parts=t.split("-"); const campname=parts[0]; const ticketnum=Number(parts[1]); if (tickets[t].status!="r") { camplist[campname].issued+=1; if (tickets[t].owner!="") camplist[campname].claimed+=1; if (tickets[t].status=="u") camplist[campname].used+=1; if (tickets[t].paid>0) camplist[campname].npaid+=1; camplist[campname].paid+=tickets[t].paid; } } return res.render("camps",{ username:req.session.username, superuser:req.session.superuser, camps:camplist }); }) app.post("/camps",requireSuperUser,(req,res) => { const campname=req.body.campname; const leader=req.body.leader; const qty=Number(req.body.qty); camps[campname]??={ leader:leader, lastid:0 }; for (let i=0; i { let campname=req.query.campname; if (!camps[campname]) return res.redirect("/"); const edit={ username:req.session.username, superuser:req.session.superuser, campname:campname, leader:camps[campname].leader, tickets: {} }; for (const t in tickets) { const parts=t.split("-"); const cname=parts[0]; const tnum=Number(parts[1]); if (cname==campname) { edit.tickets[t]={}; edit.tickets[t].owner=tickets[t].owner; edit.tickets[t].offered=tickets[t].offered; edit.tickets[t].status=tickets[t].status; edit.tickets[t].paid=tickets[t].paid; } } return res.render("editcamp",edit); }) app.post('/moretickets', requireSuperUser, (req,res) => { const qty=Number(req.body.qty); const campname=req.body.campname; if (campname in camps) for (let i=0; i { let username=req.session.username; const edit={ username:req.session.username, superuser:req.session.superuser, tickets: {}, settings:settings }; for (const t in tickets) if (tickets[t].owner==username && tickets[t].status=="i") { edit.tickets[t]={}; edit.tickets[t].offered=tickets[t].offered; edit.tickets[t].paid=tickets[t].paid; } return res.render("manytickets",edit); }) async function renderOneOrManyTickets(req,res) { let username=req.session.username; let claimed=0; let owned=0; let theticket=""; const edit={ username:req.session.username, superuser:req.session.superuser, tickets: {}, settings:settings }; for (const t in tickets) { if (tickets[t].status=="i" && tickets[t].offered==username) { claimed++; tickets[t].owner=username; tickets[t].offered=""; } // LOG if (tickets[t].status=="i" && tickets[t].owner==username) { owned++; if (owned==1) theticket=t; else theticket=""; edit.tickets[t]={}; edit.tickets[t].owner=tickets[t].owner; edit.tickets[t].offered=tickets[t].offered; edit.tickets[t].paid=tickets[t].paid; } } 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 useurl=MainURL+'/useticket?t='+theticket+'&h='+hashQR(theticket,username); const dataURL=await QRCode.toDataURL(useurl); const data={ username:username, superuser:req.session.superuser, message:message, magiclink:GetMagicLink(username), ticket:theticket, offered:tickets[theticket].offered, paid:tickets[theticket].paid, qrcode:dataURL, useurl:useurl, settings: settings } return res.render("oneticket",data); } else return res.render("manytickets",edit); } app.get('/mytickets',requireLogin, renderOneOrManyTickets); app.get("/oneticket",requireLogin, async (req,res) => { let username=req.session.username; let ticket=req.query.t; if (!tickets[ticket]) return res.render("error",{ username:username, superuser:req.session.superuser, message: "Ticket not found: "+ticket }); if (req.session.superuser) username=tickets[ticket].owner; else if (tickets[ticket].owner!=username) return res.render("error", { username:username, superuser:req.session.superuser, message: "You are not the owner of ticket "+ticket }); let offered=tickets[ticket].offered; let message=""; const hash0=crypto.createHash('sha256'); const hash1=hash0.update(ticket+QRSalt); const hash=hash1.digest("base64").slice(0,6); const useurl=MainURL+'/useticket?t='+ticket+'&h='+hashQR(ticket,username); const dataURL=await QRCode.toDataURL(MainURL+'/useticket?t='+ticket+'&h='+hashQR(ticket,username)); return res.render("oneticket", { username:username, superuser:req.session.superuser, message:message, magiclink:GetMagicLink(username), ticket:ticket, offered:tickets[ticket].offered, paid:tickets[ticket].paid, qrcode:dataURL, useurl:useurl, settings:settings }); }); app.post('/oneticket',requireLogin, async (req,res) => { // Make sure the ticket is owned by the logged in user! let username=req.session.username; let ticket=req.body.ticket; if (!tickets[ticket] || tickets[ticket].owner!=username) return res.render("error", { username:username, superuser:req.session.superuser, message: "You are not the owner of ticket "+ticket }); let offered=req.body.offered; console.log("Trying to transfer ",ticket); let message=""; if (req.session.cantransfer && tickets[ticket].owner==username && tickets[ticket].status=="i") { tickets[ticket].offered=offered; // LOG EmailTickets(offered); } const hash0=crypto.createHash('sha256'); const hash1=hash0.update(ticket+QRSalt); const hash=hash1.digest("base64").slice(0,6); const dataURL=await QRCode.toDataURL(MainURL+'/useticket?t='+ticket+'&h='+hashQR(ticket,username)); const useurl=MainURL+'/useticket?t='+ticket+'&h='+hashQR(ticket,username); return res.redirect("/"); }); app.post("/changestatus", requireSuperUser, (req,res) => { const ticket=req.body.ticket; tickets[ticket].status=req.body.status; // LOG res.json({ message: 'Status received', status: req.body.status }); }) app.post("/updateoffered2", requireLogin, (req,res) => { if (!settings["enable-transfer"]) { return res.render("error",{ message: "Transfer functionality has been disabled by the admin." }); } 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("/updateoffered2su", requireSuperUser, (req,res) => { const emaillist={}; for (name in req.body) { let ticket=0; if (name.endsWith("-owner" )) { ticket=name.slice(0,-6); tickets[ticket].owner =req.body[name]; } else if (name.endsWith("-status" )) { ticket=name.slice(0,-7); tickets[ticket].status =req.body[name]; } else if (name.endsWith("-offered")) { ticket=name.slice(0,-8); if (tickets[ticket].offered!=req.body[name]) { tickets[ticket].offered=req.body[name]; emaillist[req.body[name]]=1; } } } for (email in emaillist) EmailTickets(email); const referer = req.get('Referer'); if (referer) return res.redirect(referer); else return res.redirect('/'); }) app.post("/updateticketsu", requireSuperUser, (req,res) => { const ticket=req.body.ticket; const owner=req.body.owner; const offered=req.body.offered; tickets[ticket].owner=req.body.owner; // LOG tickets[ticket].offered=req.body.offered; // LOG res.json({ message: 'Updated '+ticket }); }) app.get("/useticket",(req,res) => { let ticket=req.query.t; let hash=req.query.h; if (!tickets[ticket]) return res.render("error", { message: "Ticket "+ticket+" not found." }); if (tickets[ticket].status!="i") return res.render("error", { message: "Ticket "+ticket+" has already been used." }); if (hashQR(ticket,tickets[ticket].owner)!=hash) return res.render("error", { message: "Ticket "+ticket+" was transferred to "+tickets[ticket].owner }); let paid_message=tickets[ticket].paid; if (tickets[ticket].paid==0) paid_message="
This ticket has not been paid for. Encourage a donation at the gate."; if (!settings['enable-ticket-use']) return res.render("message", { message: "Your ticket is good, "+tickets[ticket].owner+". The server is not in Event Mode, so Ticket "+ticket+" is still valid."+paid_message }); if (req.cookies["fof_scanqr"]!="on") return res.render("message", { message: "Ticket "+ticket+" owned by "+tickets[ticket].owner+" is good, but scanning on this device has not been enabled."+paid_message }); tickets[ticket].status="u"; // LOG return res.render("message", { message: "Welcome, "+tickets[ticket].owner+" to Falls on Fire! Ticket "+ticket+" has now been used."+paid_message }); }) async function EmailTickets_fof(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."; if (!settings['enable-email']) { console.log("Email disabled. Would have sent to "+email+": "+textbody); return; } await client.sendEmail({ From: "tickets@fallsonfire.net", To: email, Subject: "Falls on Fire: You've Got Tickets!", TextBody: textbody, HTMLBody: htmlbody }); } 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 the Frostburn Decompression! To claim them, visit this link:\n"+GetMagicLink(email); const htmlbody="You have been offered "+offered+" tickets to the Frostburn Decompression! To claim them, click here."; if (!settings['enable-email']) { console.log("Email disabled. Would have sent to "+email+": "+textbody); return; } await client.sendEmail({ From: "tickets@fallsonfire.net", To: email, Subject: "Frostburn Decompression: You've Got Tickets!", TextBody: textbody, HTMLBody: htmlbody }); } 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) return renderOneOrManyTickets(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) => { const username=req.query.u; const hash=req.query.h; if (MagicLinkValid(username,hash)) { req.session.username=username; 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"); } req.session.error="Invalid username or password."; return res.redirect("/login"); }); app.get('/logout', (req, res) => { req.session.destroy(() => { res.redirect('/'); }); }); app.get('/create', (req, res) => { return res.render("create"); }); app.post('/create', async (req, res) => { const { username, password1, password2 } = req.body; if (password1!=password2) { req.session.message="Passwords do not match."; return res.redirect('/'); } if (users[username] && !users[username].needsconfirm) { req.session.error="Email (username) already exists."; return res.redirect('/'); } if (users[username] && users[username].needsconfirm) { await client.sendEmail({ From: "tickets@fallsonfire.net", To: username, Subject: "Confirm Account Creation", TextBody: "Click here to confirm creation of account "+username, HTMLBody: "Click here to confirm creation of account "+username }); req.session.message="Email has not yet been confirmed. Resent confirm link."; return res.redirect('/'); } users[username] = { password: hashPW(password1), needsconfirm:false }; if (users[username].needsconfirm) req.session.message="Check email to confirm account creation."; else req.session.message="Account created. You may now log in."; return res.redirect('/'); }); app.get("/scanqron", (req,res) => { res.cookie("fof_scanqr","on",{ maxAge: 7 * 24 * 60 * 60 * 1000 }); return res.redirect("/checkscanqr"); }); app.get("/scanqroff", (req,res) => { res.cookie("fof_scanqr","off"); return res.redirect("/checkscanqr"); }); app.get("/checkscanqr", (req,res) => { const scan=req.cookies["fof_scanqr"]; return res.render("message",{ message: "QR Code Scanning is "+(scan=="on" ? "On" : "Off") }); }); app.get('/changepassword', requireLogin,(req, res) => { return res.render("changepassword",{ username:req.session.username, superuser:req.session.superuser, settings:settings, message: "" }); }); app.post('/changepassword', requireLogin,(req, res) => { const { password0, password1, password2 } = req.body; if (users[req.session.username].password!=hashPW(password0)) { req.session.error="Old Password is not correct."; return res.redirect('/changepassword'); } if (password1!=password2) { req.session.error="Passwords do not match"; return res.redirect('/changepassword'); } users[req.session.username].password=hashPW(password1); req.session.message="Password Changed."; return 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(MainURL+'/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 ticket=req.body.ticket; const username=tickets[ticket].owner; const URL=await QRCode.toDataURL(MainURL+'/useticket?t='+ticket+'&h='+hashQR(ticket,username)); return res.send({ owner:username, qrcode: URL, magiclink:GetMagicLink(username) }); }) function opencount() { let rval=0; for (t in tickets) if (t.startsWith("open-")) if (tickets[t].status!='r') rval++; return rval; } app.get('/settings',requireSuperUser, (req,res) => { res.render('settings',{ username:req.session.username, superuser:req.session.superuser, settings:settings, opencount:opencount(), message: "" }) }); app.post('/addopen',requireSuperUser,(req,res) => { settings['open-limit']+=Number(req.body.addopen); return res.redirect('/settings'); }); app.post('/importfb',requireSuperUser,upload.single("file"),(req,res) => { console.log("File name:", req.file.originalname); let emails={}; for (t in tickets) { if (tickets[t].offered) emails[tickets[t].offered]=1; if (tickets[t].owner ) emails[tickets[t].owner ]=1; } const contents=req.file.buffer.toString(); csvParse.parse(contents, { columns: true, trim: true }, (err, records) => { if (err) { console.log("CSV Parsing Error:", err); req.session.error="The CVS file did not parse correctly. Check console."; return res.redirect("/settings"); } let count=0; for (const item of records) if (item.email in emails) { if (!camps[item.camp]) camps[item.camp]={ leader:"", lastid:0 }; camps[item.camp].lastid++; const ticket=item.camp+"-"+camps[item.camp].lastid; tickets[ticket]={ owner:"", offered: item.email, paid:0.00, status:"i" }; console.log("Offered ",ticket," to ",item.email); count++; emails[item.email]=1; } req.session.message="Imported "+count+" Frostburn-style records."; return res.redirect("/settings"); }); }); app.post('/wipedb',requireSuperUser, (req,res) => { InitDatabase(); res.redirect("/"); }); app.post('/serialize',requireSuperUser, async (req,res) => { SerializeAll(); return res.redirect("/settings"); }); app.post('/deserialize',requireSuperUser, (req,res) => { DeserializeAll(); return res.redirect("/settings"); // Since we may be overwriting session }); app.post('/purge',requireSuperUser, (req,res) => { let count=0; for (const t in tickets) if (tickets[t].status=='r') { count++; delete tickets[t]; } return res.redirect("/settings"); }); app.post("/killmagiclink",requireSuperUser,(req,res) => { if (!users[req.body.email]) users[req.body.email]={ }; users[req.body.email].linksalt=generateSecureToken(); req.session.message="Deactivated any previous Magic Links for "+req.body.email; return res.redirect("/settings"); }); app.post('/update-setting', requireSuperUser, (req, res) => { settings[req.body.name]=req.body.checked; res.json({ success: true, message: 'Checkbox state updated successfully' }); }); app.post('/pay0',requireLogin,(req,res) => { return res.render("pay",{ username:req.session.username, superuser:req.session.superuser, payforwhat: { kind:"existing", ticket:req.body.ticket, amount:+req.body.amount, total:+req.body.amount, qty:1 }, }); }); function check_payforwhat(payforwhat,req) { if (payforwhat.kind=="existing") { let ticket=tickets[payforwhat.ticket]; if (!ticket || ticket.status=="r" || ticket.owner!=req.session.username) return false; } else if (payforwhat.kind=="new") { if (payforwhat.qty<=0 || payforwhat.qty>6 || payforwhat.amounteach<=0 || payforwhat.amounteach>100000) return false; } return true; } function assure_camp_exists(camp) { if (!camps[camp]) camps[camp]={ leader:"", lastid:0 }; } function do_payforwhat(payforwhat) { if (payforwhat.kind=="existing") { tickets[payforwhat.ticket].paid=100.0*payforwhat.amount; } else if (payforwhat.kind=="new") { assure_camp_exists(payforwhat.camp); for (let i=0; i { return res.render("buy",{ username:req.session.username, superuser:req.session.superuser, settings:settings, message: "" }); }); app.post('/buy',requireLogin,(req,res) => { return res.render("pay",{ username:req.session.username, superuser:req.session.superuser, payforwhat: { kind:"new", camp:"open", email:req.body.email, qty:req.body.qty, amounteach:req.body.amounteach, total:req.body.qty*req.body.amounteach }}); }); app.post('/charge', requireLogin, async (req, res) => { const payforwhat=req.body.payforwhat; if (!check_payforwhat(payforwhat,req)) return res.json({ error: 'Invalid PayForWhat' }); try { // Token or Payment Method ID from the client const paymentMethodId = req.body.paymentMethodId; // Create a PaymentIntent on the server let pennies=0; if (payforwhat.kind=="new") pennies=Math.round(parseFloat(payforwhat.amounteach))*parseInt(payforwhat.qty) * 100; else if (payforwhat.kind=="existing") pennies=Math.round(parseFloat(payforwhat.amount))*100; const return_url=base_url+'/mytickets'; const paymentIntent = await stripe.paymentIntents.create({ amount: pennies, // Amount in cents currency: 'usd', payment_method: paymentMethodId, confirmation_method: 'automatic', confirm: true, // Attempt to confirm the payment immediately return_url: return_url, }); // Check status of payment intent if (paymentIntent.status === 'requires_action') { // Additional action is required (e.g. 3D Secure) res.json({ requiresAction: true, paymentIntentClientSecret: paymentIntent.client_secret, }); } else if (paymentIntent.status === 'succeeded') { // Payment is complete do_payforwhat(payforwhat); res.json({ success: true, redirect_url: return_url }); } else { res.json({ error: 'Invalid PaymentIntent status' }); } } catch (error) { console.error('Payment error:', error); res.json({ error: error.message }); } }); // Start the server app.listen(PORT, () => { console.log(`Server is running at http://localhost:${PORT}`); });