mirror of
https://github.com/Wolvan/poll.horse.git
synced 2024-11-22 04:58:00 +01:00
Allow voting via form
This commit is contained in:
parent
3204695c90
commit
76b8d0bbc5
2 changed files with 79 additions and 26 deletions
102
src/backend.ts
102
src/backend.ts
|
@ -1,6 +1,6 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import { Router } from "express";
|
import { CookieOptions, Router } from "express";
|
||||||
import persist from "node-persist";
|
import persist from "node-persist";
|
||||||
import { program } from "commander";
|
import { program } from "commander";
|
||||||
import { resolve } from "path";
|
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) => {
|
router.post("/vote/:id", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const id = req.params.id;
|
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" });
|
|
||||||
|
|
||||||
if (poll.dupeCheckMode === "ip") {
|
const error = await voteOnPoll(id, req.body.votes, {
|
||||||
const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress || "";
|
ip: req.headers["x-forwarded-for"] as string || req.socket.remoteAddress || "",
|
||||||
if (Array.isArray(poll.dupeData) && poll.dupeData.includes(ip as string)) return res.status(200).json({ status: "ok", id });
|
setCookie: res.cookie.bind(res),
|
||||||
if (Array.isArray(poll.dupeData)) poll.dupeData.push(ip as string);
|
cookies: req.cookies
|
||||||
} 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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
votes.filter(i => i && possibleVotes.includes(i)).forEach(vote => poll.options[vote]++);
|
if (error) res.status(error.statusCode).json({
|
||||||
await polls.setItem(id, poll);
|
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"]);
|
||||||
|
|
||||||
res.json({ status: "ok", id });
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error instanceof Error) res.status(500).json({
|
if (error instanceof Error) res.status(500).json({
|
||||||
|
|
|
@ -116,6 +116,7 @@ export default function init(router: Router): void {
|
||||||
});
|
});
|
||||||
router.get("/:id", async (req, res) => {
|
router.get("/:id", async (req, res) => {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
|
const options = (typeof req.query.options === "string" ? req.query.options.split("\uFFFE") : []).filter(i => i);
|
||||||
try {
|
try {
|
||||||
const poll: Poll = await fetch(
|
const poll: Poll = await fetch(
|
||||||
(program.opts().backendBaseUrl || "http://localhost:" + program.opts().port) + "/_backend/poll/" + id
|
(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 =>
|
const pollOptions = poll.options.map(option =>
|
||||||
`<div class="poll-option">
|
`<div class="poll-option">
|
||||||
<div class="input-container">
|
<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><div class="text">${option}</div>
|
||||||
</div>`
|
</div>`
|
||||||
).join("");
|
).join("");
|
||||||
|
|
Loading…
Reference in a new issue