changes
This commit is contained in:
415
foftickets.js
Normal file
415
foftickets.js
Normal file
@@ -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(`<h1>Sign Up</h1>
|
||||
<form method="POST" action="/loginpassword">
|
||||
<label>Username: <input type="text" name="username" required></label><br>
|
||||
<label>Password: <input type="password" name="password" required></label><br>
|
||||
<button type="submit">Log In</button>
|
||||
</form>`);
|
||||
})
|
||||
|
||||
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(`
|
||||
<h1>Email me a login link</h1>
|
||||
<form method="POST" action="/emaillink">
|
||||
<label>Email Address: <input name="email"></label>
|
||||
<button type="submit">Submit</button>`
|
||||
)
|
||||
})
|
||||
|
||||
// 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("<h1>Welcome, "+tickets[ticket].username+" to Falls on Fire! Ticket "+ticket+" has now been used.</h1>");
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
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: '<p>Your Login Link: </p>',
|
||||
}).then(() => {
|
||||
console.log('Email sent');
|
||||
res.send("<h1>Email sent</h1>");
|
||||
}).catch((error) => {
|
||||
console.error('Error sending email:', error);
|
||||
res.send("<h1>Email failed</h1>");
|
||||
});
|
||||
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: '<p>This is a test email.</p>',
|
||||
}).then(() => {
|
||||
console.log('Email sent');
|
||||
res.send("<h1>Email sent</h1>");
|
||||
}).catch((error) => {
|
||||
console.error('Error sending email:', error);
|
||||
res.send("<h1>Email failed</h1>");
|
||||
})});
|
||||
|
||||
|
||||
|
||||
|
||||
// Routes
|
||||
app.get('/', (req, res) => {
|
||||
if (req.session.username) {
|
||||
res.send(`
|
||||
<h1>Welcome, ${req.session.username}</h1>
|
||||
<a href="/products">Products</a><br>
|
||||
<a href="/faq">FAQ</a><br>
|
||||
<a href="/logout">Log Out</a>
|
||||
`);
|
||||
} else {
|
||||
res.send(`
|
||||
<h1>Welcome to our website!</h1>
|
||||
<a href="/login">Log In</a><br>
|
||||
<a href="/signup">Sign Up</a>
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/signup', (req, res) => {
|
||||
res.send(`
|
||||
<h1>Sign Up</h1>
|
||||
<form method="POST" action="/signup">
|
||||
<label>Username: <input type="text" name="username" required></label><br>
|
||||
<label>Password: <input type="password" name="password" required></label><br>
|
||||
<button type="submit">Sign Up</button>
|
||||
</form>
|
||||
<a href="/login">Log In</a>
|
||||
`);
|
||||
});
|
||||
|
||||
|
||||
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. <a href="/signup">Try again</a>');
|
||||
}
|
||||
users[username] = { password };
|
||||
res.redirect('/login');
|
||||
});
|
||||
|
||||
app.get('/changepassword', (req, res) => {
|
||||
res.send(`
|
||||
<h1>Change Password</h1>
|
||||
<form method="POST" action="/changepassword">
|
||||
<label>Password: <input type="password" name="password1" required></label><br>
|
||||
<label>Again: <input type="password" name="password2" required></label><br>
|
||||
<button type="submit">Sign Up</button>
|
||||
</form>
|
||||
<a href="/">Home</a>
|
||||
`);
|
||||
});
|
||||
|
||||
app.post('/changepassword', (req, res) => {
|
||||
const { password1, password2 } = req.body;
|
||||
if (!req.session.username) {
|
||||
return res.send('You are not logged in<a href="/">Back</a>');
|
||||
}
|
||||
if (password1!=password2) {
|
||||
return res.send('Passwords do not match<a href="/">Back</a>');
|
||||
}
|
||||
users[req.session.username].password=hashPW(password1);
|
||||
res.redirect('/login');
|
||||
});
|
||||
|
||||
app.get('/login', (req, res) => {
|
||||
res.send(`
|
||||
<h1>Log In</h1>
|
||||
<form method="POST" action="/login">
|
||||
<label>Username: <input type="text" name="username" required></label><br>
|
||||
<label>Password: <input type="password" name="password" required></label><br>
|
||||
<button type="submit">Log In</button>
|
||||
</form>
|
||||
<a href="/signup">Sign Up</a>
|
||||
`);
|
||||
});
|
||||
|
||||
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. <a href="/login">Try again</a>');
|
||||
});
|
||||
|
||||
app.get('/logout', (req, res) => {
|
||||
req.session.destroy(() => {
|
||||
res.redirect('/');
|
||||
});
|
||||
});
|
||||
|
||||
// Protected routes
|
||||
app.get('/products', requireLogin, (req, res) => {
|
||||
res.send(`
|
||||
<h1>Products Page</h1>
|
||||
<p>Here is a list of products!</p>
|
||||
<a href="/">Home</a>
|
||||
`);
|
||||
});
|
||||
|
||||
app.get('/tickets', requireLogin, (req, res) => {
|
||||
res.write(`<h1>Tickets for `+req.session.username+`</h1>`);
|
||||
res.write(`<a href="/">Home</a>`);
|
||||
res.end();
|
||||
});
|
||||
|
||||
app.get('/faq', requireLogin, (req, res) => {
|
||||
res.send(`
|
||||
<h1>FAQ Page</h1>
|
||||
<p>Here are some frequently asked questions.</p>
|
||||
<a href="/">Home</a>
|
||||
`);
|
||||
});
|
||||
|
||||
// Start the server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running at http://localhost:${PORT}`);
|
||||
});
|
||||
9
views/error.ejs
Normal file
9
views/error.ejs
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Error Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Some kind of error</h1>
|
||||
</body>
|
||||
</html>
|
||||
9
views/error.ejs~
Normal file
9
views/error.ejs~
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>You don't have any tickets</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>No tickets for you!!</h1>
|
||||
</body>
|
||||
</html>
|
||||
9
views/notickets.ejs
Normal file
9
views/notickets.ejs
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>You don't have any tickets</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>No tickets for you!!</h1>
|
||||
</body>
|
||||
</html>
|
||||
27
views/notickets.ejs~
Normal file
27
views/notickets.ejs~
Normal file
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Your Tickets</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome, <%= username %>!</h1>
|
||||
<table border="1">
|
||||
<tr>
|
||||
<th>Ticket</th>
|
||||
<th>Owner</th>
|
||||
<th>Offered To</th>
|
||||
<th>QRCode</th>
|
||||
</tr>
|
||||
<% for (const t in tlist) { %>
|
||||
<tr>
|
||||
<td> <%= t %> </td>
|
||||
<td> <%= tlist[t].owner %> </td>
|
||||
<td> <%= tlist[t].offered %> </td>
|
||||
<td> <%= QRCode.toDataURL("https://abc.com", function(err,url) { if (err) { console.error(err); return; } console.log(url); }); %> </td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<tr>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
11
views/simpleowner.ejs
Normal file
11
views/simpleowner.ejs
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>You have a Ticket!</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome, <%= username %>!</h1>
|
||||
Your ticket is: <img src="<%= qrcode %>" alt="QR Code">
|
||||
<%= ticket %>
|
||||
</body>
|
||||
</html>
|
||||
28
views/simpleowner.ejs~
Normal file
28
views/simpleowner.ejs~
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Your Tickets</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome, <%= username %>!</h1>
|
||||
<table border="1">
|
||||
<tr>
|
||||
<th>Ticket</th>
|
||||
<th>Owner</th>
|
||||
<th>Offered To</th>
|
||||
<th>QRCode</th>
|
||||
</tr>
|
||||
<% for (const t in tlist) { %>
|
||||
<tr>
|
||||
<td> <%= t %> </td>
|
||||
<td> <%= tlist[t].owner %> </td>
|
||||
<td> <%= tlist[t].offered %> </td>
|
||||
<td> <%= simpledata %> </td>
|
||||
<td> <%= QRCode.toDataURL("https://abc.com", function(err,url) { if (err) { console.error(err); return; } console.log(url); }); %> </td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<tr>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
66
views/transfer.ejs
Normal file
66
views/transfer.ejs
Normal file
@@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Your Tickets</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome, <%= username %>!</h1>
|
||||
<form id="editor">
|
||||
<table border="1">
|
||||
<tr>
|
||||
<th>Ticket</th>
|
||||
<th>Owner</th>
|
||||
<th>Offered To</th>
|
||||
<th>Used</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
<% for (const t in tickets) { %>
|
||||
<tr>
|
||||
<td><%=t%></td>
|
||||
<td><%=tickets[t].owner%></td>
|
||||
<td><input type="edit" value="<%=tickets[t].offered%>" id="<%=t%>-offered"></td>
|
||||
<td><input type="checkbox" id="<%=t%>-used"<%=tickets[t].used ? ' checked' : ''%>></td>
|
||||
<td><button id="<%=t%>-action"> QRCode </button></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<tr>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
<script>
|
||||
// JavaScript to change the form element
|
||||
const EditOffer = document.getElementById('habitat-1-offered');
|
||||
const ActionButton = document.getElementById('habitat-1-action');
|
||||
|
||||
|
||||
function toggleUsed(el) {
|
||||
const id=el.target.id.slice(0,-5);
|
||||
const isChecked=this.checked;
|
||||
const js=JSON.stringify( { ticket: id, checked: isChecked } );
|
||||
const fetchtable={ method:'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: js };
|
||||
console.log(fetchtable);
|
||||
fetch('/toggle',fetchtable)
|
||||
.then( response => response.json() )
|
||||
.then( data => console.log('Server Response: ',data) )
|
||||
.catch( error => console.error('Error: ',error));
|
||||
}
|
||||
|
||||
const elements = document.querySelectorAll("[id$=-used]");
|
||||
|
||||
|
||||
elements.forEach((el) => {
|
||||
const UsedCheckbox = document.getElementById(el.id);
|
||||
UsedCheckbox.addEventListener("change",toggleUsed);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
EditOffer.addEventListener('input', () => {
|
||||
ActionButton.textContent = "Update";
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
28
views/transfer.ejs~
Normal file
28
views/transfer.ejs~
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Your Tickets</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome, <%= username %>!</h1>
|
||||
<table border="1">
|
||||
<tr>
|
||||
<th>Ticket</th>
|
||||
<th>Owner</th>
|
||||
<th>Offered To</th>
|
||||
<th>QRCode</th>
|
||||
</tr>
|
||||
<% for (const t in tlist) { %>
|
||||
<tr>
|
||||
<td> <%= t %> </td>
|
||||
<td> <%= tlist[t].owner %> </td>
|
||||
<td> <%= tlist[t].offered %> </td>
|
||||
<td> <%= simpledata %> </td>
|
||||
<td> <%= QRCode.toDataURL("https://abc.com", function(err,url) { if (err) { console.error(err); return; } console.log(url); }); %> </td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<tr>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user