Allow voting via form

This commit is contained in:
Wolvan 2022-01-06 15:31:17 +01:00
parent 3204695c90
commit 76b8d0bbc5
2 changed files with 79 additions and 26 deletions

View file

@ -1,6 +1,6 @@
"use strict";
import { Router } from "express";
import { CookieOptions, Router } from "express";
import persist from "node-persist";
import { program } from "commander";
import { resolve } from "path";
@ -164,36 +164,88 @@ export default async function init(router: Router): Promise<void> {
}
});
async function voteOnPoll(pollId: string, votes: string[], { ip, setCookie, cookies }: {
ip: string,
setCookie: (name: string, value: string, options?: CookieOptions) => void,
cookies: { [key: string]: string }
}): Promise<null|{
error: string,
statusCode: number
}> {
const poll: (Poll | undefined) = await polls.getItem(pollId);
if (!poll) return {
error: "Poll not found",
statusCode: 404
};
const possibleVotes = Object.keys(poll.options);
if (!Array.isArray(votes) || votes.filter(i => i && possibleVotes.includes(i)).length < 1) return {
error: "Votes must be an array and have at least 1 entry",
statusCode: 400
};
if (!poll.multiSelect && votes.filter(i => i && possibleVotes.includes(i)).length > 1) return {
error: "Single-select polls can only have one vote",
statusCode: 400
};
if (poll.dupeCheckMode === "ip") {
if (Array.isArray(poll.dupeData) && poll.dupeData.includes(ip as string)) return null;
if (Array.isArray(poll.dupeData)) poll.dupeData.push(ip as string);
} else if (poll.dupeCheckMode === "cookie") {
const cookie = cookies[poll.dupeData as string];
if (cookie) return null;
setCookie(poll.dupeData as string, "1", {
httpOnly: true,
maxAge: (1000 * 60 * 60 * 24 * 365) / 2
});
}
votes.filter(i => i && possibleVotes.includes(i)).forEach(vote => poll.options[vote]++);
await polls.setItem(pollId, poll);
return null;
}
router.post("/vote/:id", async (req, res) => {
try {
const id = req.params.id;
const poll: (Poll | undefined) = await polls.getItem(id);
if (!poll) return res.status(404).json({ error: "Poll not found" });
const votes = req.body.votes;
const possibleVotes = Object.keys(poll.options);
if (!Array.isArray(votes) || votes.filter(i => i && possibleVotes.includes(i)).length < 1)
return res.status(400).json({ error: "Votes must be an array and have at least 1 entry" });
if (!poll.multiSelect && votes.filter(i => i && possibleVotes.includes(i)).length > 1)
return res.status(400).json({ error: "Single-select polls can only have one vote" });
const error = await voteOnPoll(id, req.body.votes, {
ip: req.headers["x-forwarded-for"] as string || req.socket.remoteAddress || "",
setCookie: res.cookie.bind(res),
cookies: req.cookies
});
if (poll.dupeCheckMode === "ip") {
const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress || "";
if (Array.isArray(poll.dupeData) && poll.dupeData.includes(ip as string)) return res.status(200).json({ status: "ok", id });
if (Array.isArray(poll.dupeData)) poll.dupeData.push(ip as string);
} else if (poll.dupeCheckMode === "cookie") {
const cookie = req.cookies[poll.dupeData as string];
if (cookie) return res.status(200).json({ status: "ok", id });
res.cookie(poll.dupeData as string, "1", {
httpOnly: true,
maxAge: (1000 * 60 * 60 * 24 * 365) / 2
});
}
if (error) res.status(error.statusCode).json({
error: error.error
});
else res.json({ status: "ok", id });
} catch (error) {
console.error(error);
if (error instanceof Error) res.status(500).json({
error: error.message
});
else res.status(500).json({
error: error
});
}
});
router.post("/vote-form/:id", async (req, res) => {
try {
const id = req.params.id;
const votes = [].concat(req.body["poll-option"]);
votes.filter(i => i && possibleVotes.includes(i)).forEach(vote => poll.options[vote]++);
await polls.setItem(id, poll);
const error = await voteOnPoll(id, votes, {
ip: req.headers["x-forwarded-for"] as string || req.socket.remoteAddress || "",
setCookie: res.cookie.bind(res),
cookies: req.cookies
});
res.json({ status: "ok", id });
if (!error) return res.redirect("/" + id + "/r");
if (error.statusCode === 404) return res.redirect("/");
res.redirect(`/${id}?error=${
encodeURIComponent(error.error)
}&options=${
encodeURIComponent(votes.slice(0, MAX_POLL_OPTIONS).join("\uFFFE"))
}`);
} catch (error) {
console.error(error);
if (error instanceof Error) res.status(500).json({

View file

@ -116,6 +116,7 @@ export default function init(router: Router): void {
});
router.get("/:id", async (req, res) => {
const id = req.params.id;
const options = (typeof req.query.options === "string" ? req.query.options.split("\uFFFE") : []).filter(i => i);
try {
const poll: Poll = await fetch(
(program.opts().backendBaseUrl || "http://localhost:" + program.opts().port) + "/_backend/poll/" + id
@ -125,7 +126,7 @@ export default function init(router: Router): void {
const pollOptions = poll.options.map(option =>
`<div class="poll-option">
<div class="input-container">
<input type="${poll.multiSelect ? "checkbox" : "radio"}" name="poll-option" value="${option}"" /><div class="checkmark"><svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path class="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/></svg></div>
<input type="${poll.multiSelect ? "checkbox" : "radio"}" name="poll-option" value="${option}"" ${options.includes(option) ? "checked" : ""}/><div class="checkmark"><svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path class="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/></svg></div>
</div><div class="text">${option}</div>
</div>`
).join("");