This commit is contained in:
2024-12-09 15:11:22 -05:00
parent 581cedbd5a
commit 6f0bd26228
4 changed files with 133 additions and 40 deletions

View File

@@ -3,6 +3,7 @@ const bodyParser = require('body-parser');
const session = require('express-session');
const QRCode=require('qrcode');
const crypto=require('crypto');
const path=require('path');
const app = express();
app.set('view engine','ejs');
@@ -16,8 +17,8 @@ const QRSalt="!SaltyMagic5392370662";
// + Login with Password
// + Login with Token
// + Email Link
// Signup
// Set Password
// + Signup
// Change 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
@@ -50,8 +51,8 @@ function hashQR(t,username) {
//
// 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 users = { "teppy@egenesis.com" : { password: hashPW("x6321872X.1"), superuser:true },
"fallsonfire@gmail.com" : { password: hashPW("indiantreeonfire"), superuser:false }
};
const tickets = { "habitat-1" : { owner: "teppy@egenesis.com" , offered: "", paid: 0.00, used: false },
@@ -62,6 +63,8 @@ const tickets = { "habitat-1" : { owner: "teppy@egenesis.com" , offered: "", pa
"habitat-6" : { owner: "teppy@egenesis.com" , offered: "", paid: 0.00 },
};
const camps = { "habitat": 0 };
const tokens = { "abc" : { username: "teppy@egenesis.com", expires: 0 }
};
const tokensu = { "teppy@egenesis.com" : "abc"
@@ -77,14 +80,21 @@ app.use(session({
// Middleware to protect routes
function requireLogin(req, res, next) {
if (!req.session.username) {
return res.redirect('/login');
}
next();
}
if (!req.session.username) return res.redirect('/login');
next();
}
function requireSuperUser(req,res,next) {
console.log(req.session);
if (!req.session.superuser) return res.status(403).send(JSON.stringify( { error: "Forbidden without superuser flag" } ));
else next();
}
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';
@@ -123,7 +133,7 @@ app.get('/logintoken', (req, res) => {
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;
req.session.superuser= users[tokenData.username].superuser;
res.redirect('/transfer');
});
@@ -163,7 +173,6 @@ function categorizeTickets(username) {
}
// 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.
@@ -205,8 +214,7 @@ app.get('/transfer', requireLogin, async (req,res) => {
if (cat=="complex") return res.render('transfer',simpledata);
})
app.post("/toggle", (req,res) => {
console.log("Here is Tickets...",tickets);
app.post("/toggle", requireSuperUser, (req,res) => {
const ticket=req.body.ticket;
const isChecked = req.body.checked;
tickets[ticket].used=isChecked;
@@ -214,6 +222,17 @@ app.post("/toggle", (req,res) => {
res.json({ message: 'Checkbox state received', checked: isChecked });
})
app.post("/updateoffered", requireLogin, (req,res) => {
const ticket=req.body.ticket;
const offered=req.body.offered;
console.log("Ticket: ",ticket);
if (tickets[ticket].owner!=req.session.username) res.status(500).send("Ticket "+ticket+" owned by someone else");
else if (tickets[ticket].used) res.status(500).send("Ticket "+ticket+" has already been used");
else {
tickets[ticket].offered=offered;
res.json({ message: 'Updated owner of '+ticket+' to '+offered });
}
})
app.get("/useticket",(req,res) => {
let ticket=req.t;
@@ -254,7 +273,6 @@ app.post('emaillink',(req,res) => {
})
app.get('/testemail',
(req,res) => {
client.sendEmail({ From: 'tickets@fallsonfire.net',
@@ -331,7 +349,8 @@ app.post('/signup', (req, res) => {
if (users[username]) {
return res.send('User already exists. <a href="/signup">Try again</a>');
}
users[username] = { password };
users[username] = { password: hashPW(password) };
console.log("Created new account:",username);
res.redirect('/login');
});
@@ -372,10 +391,11 @@ app.get('/login', (req, res) => {
});
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');
const { username, password, superuser } = req.body;
if (users[username] && users[username].password === hashPW(password)) {
req.session.username = username;
req.session.superuser = users[username].superuser;
return res.redirect('/transfer');
}
res.send('Invalid username or password. <a href="/login">Try again</a>');
});

31
public/styles.css Normal file
View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
/* Modal container */
.modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1000; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
background-color: rgba(0, 0, 0, 0.5); /* Black with opacity */
}
/* Modal content */
.modal-content {
background-color: white;
margin: 15% auto; /* Center the modal */
padding: 20px;
border: 1px solid #888;
width: 80%; /* Adjust as needed */
}
/* Close button */
.close {
color: black;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}

9
views/superuser.ejs Normal file
View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>Error: Not Superuser</title>
</head>
<body>
<h1>Error: Not Superuser</h1>
</body>
</html>

View File

@@ -2,13 +2,15 @@
<html>
<head>
<title>Your Tickets</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Welcome, <%= username %>!</h1>
<form id="editor">
<div id="server-response">Server Ready</div>
<table border="1">
<tr>
<th>Ticket</th>
<th>Ticket#</th>
<th>Owner</th>
<th>Offered To</th>
<th>Used</th>
@@ -17,10 +19,10 @@
<% for (const t in tickets) { %>
<tr>
<td><%=t%></td>
<td><%=tickets[t].owner%></td>
<td><input type="edit" value="<%=tickets[t].owner%>" id="<%=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>
<td><input type="checkbox" id="<%=t%>-used"<%=tickets[t].used ? ' checked' : ''%>></td>
<td><button id="<%=t%>-action" type="button"> QRCode </button></td>
</tr>
<% } %>
<tr>
@@ -29,37 +31,68 @@
</form>
<script>
// JavaScript to change the form element
const EditOffer = document.getElementById('habitat-1-offered');
const ActionButton = document.getElementById('habitat-1-action');
const ResponseDisplay = document.getElementById("server-response");
let ResponseStack = 0;
let ResponseError="";
let ooEdits={}; // Owner or Offer has been edited
function UpdateSR(delta) {
ResponseStack+=delta;
if (ResponseError!="") ResponseDisplay.textContent=ResponseError;
else if (ResponseStack>0) ResponseDisplay.textContent="Waiting for Server";
else ResponseDisplay.textContent="Server Ready";
}
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 };
const fetchtable={ method:'POST', headers: { 'Content-Type': 'application/json' }, body: js };
console.log(fetchtable);
UpdateSR(1);
fetch('/toggle',fetchtable)
.then( response => response.json() )
.then( data => console.log('Server Response: ',data) )
.catch( error => console.error('Error: ',error));
.then( response => { if (!response.ok) throw new Error(`Server responded with status ${response.status}`); else return response.json(); } )
.then( data => UpdateSR(-1) )
.catch( error => { console.log("Here is the error!"+error); ResponseError=error; UpdateSR(-1); })
}
const elements = document.querySelectorAll("[id$=-used]");
document.body.addEventListener("click", event => {
const id=event.target.id;
if (id.endsWith("-action")) {
const id0=id.slice(0,-7);
if (ooEdits[id0]) {
UpdateSR(1);
const offeredEdit=document.getElementById(id0+"-offered");
const js=JSON.stringify( { ticket: id0, offered: offeredEdit.value } );
const fetchtable={ method:'POST', headers: { 'Content-Type': 'application/json' }, body: js };
fetch('/updateoffered',fetchtable)
.then( response => { if (!response.ok) throw new Error(`Server responded with status ${response.status}`); else return response.json(); } )
.then( data => { UpdateSR(-1); event.target.textContent="QRCode"; } )
.catch( error => { console.log("Here is the error!"+error); ResponseError=error; UpdateSR(-1); })
} else {
console.log("QRCode ",id0);
}
}
});
elements.forEach((el) => {
const checkboxes = document.querySelectorAll("[id$=-used]");
checkboxes.forEach(el => {
const UsedCheckbox = document.getElementById(el.id);
UsedCheckbox.addEventListener("change",toggleUsed);
});
EditOffer.addEventListener('input', () => {
ActionButton.textContent = "Update";
});
const owners = document.querySelectorAll("[id$=-owner]");
owners.forEach(el => {
const id0=el.id.slice(0,-6);
const OwnerEdit = document.getElementById(id0+"-owner" );
const OfferedEdit = document.getElementById(id0+"-offered");
const ActionButton = document.getElementById(id0+"-action" );
OwnerEdit .addEventListener('input', () => { ActionButton.textContent = "Update"; ooEdits[id0]=true; console.log("Changed OfferedEdit:",OfferedEdit.value); });
OfferedEdit.addEventListener('input', () => { ActionButton.textContent = "Update"; ooEdits[id0]=true; console.log("Changed OfferedEdit:",OfferedEdit.value); });
});
</script>
</body>