From cfa150cc4278506cf02a0b6baff73dd2639182e9 Mon Sep 17 00:00:00 2001 From: Wolvan Date: Wed, 12 Jan 2022 21:07:55 +0100 Subject: [PATCH] Move API and document it The API has been moved to a /api path to distinguish it from the form submission path and make it clear it is a callable API. The API also has been rudimentarily documented. --- API.md | 187 ++++++++++++++++++++++++++++++++ README.md | 2 + frontend/static/js/result.js | 2 +- src/backend.ts | 203 ++++++++++++++++++----------------- src/frontend.ts | 4 +- 5 files changed, 294 insertions(+), 104 deletions(-) create mode 100644 API.md diff --git a/API.md b/API.md new file mode 100644 index 0000000..3245c96 --- /dev/null +++ b/API.md @@ -0,0 +1,187 @@ +# API +## API URL +The base URL for all API calls is `/_backend/api.` + +
+
+ +## Get poll information +---- +Fetch JSON data of a single poll defined by the poll ID string. + +* **URL** + + /poll/:id + +* **Method:** + + `GET` + +* **URL Params** + + **Required:** + + `id=[alphanumeric]` + +* **Data Params** + + None + +* **Success Response:** + + * **Code:** 200
+ **Content:** + ```json + { + "id": "abcd1234", + "title": "An API requested Poll", + "dupeCheckMode": "ip", + "multiSelect": true, + "creationTime": "2022-01-12T19:26:37.262Z", + "options": [ + "Option A", + "Option B", + "Option C" + ] + } + ``` + +* **Error Response:** + + * **Code:** 404 NOT FOUND
+ **Content:** + ```json + { + "error" : "Poll not found" + } + ``` + + OR + + * **Code:** 500 INTERNAL SERVER ERROR
+ **Content:** + ```json + { + "error" : "" + } + ``` + +## Get poll result +---- +Fetch JSON data of the result of a single poll defined by the poll ID string. + +* **URL** + + /poll-result/:id + +* **Method:** + + `GET` + +* **URL Params** + + **Required:** + + `id=[alphanumeric]` + +* **Data Params** + + None + +* **Success Response:** + + * **Code:** 200
+ **Content:** + ```json + { + "title": "An API requested Poll", + "options": { + "Option A": 0, + "Option B": 3, + "Option C": 1 + } + } + ``` + +* **Error Response:** + + * **Code:** 404 NOT FOUND
+ **Content:** + ```json + { + "error" : "Poll not found" + } + ``` + + OR + + * **Code:** 500 INTERNAL SERVER ERROR
+ **Content:** + ```json + { + "error" : "" + } + ``` + +## Create a poll +---- +Create a new poll and return its id + +* **URL** + + /poll + +* **Method:** + + `POST` + +* **URL Params** + + None + +* **Data Params** + + **Required:** + + `options=[string][]` + + **Optional:** + + `title=[alphanumeric]` + + `dupeCheckMode="ip" | "cookie" | "none"` + + `multiSelect=[boolean]` + +* **Success Response:** + + * **Code:** 200
+ **Content:** + ```json + { + "id": "abcd1234" + } + ``` + +* **Error Response:** + + * **Code:** 400 BAD REQUEST
+ **Content:** + ```json + { + "error" : "" + } + ``` + **Possible Error Messages:** + - `Options must be an array and have at least 2 different entries` + - `Only <> options are allowed` + + OR + + * **Code:** 500 INTERNAL SERVER ERROR
+ **Content:** + ```json + { + "error" : "" + } + ``` \ No newline at end of file diff --git a/README.md b/README.md index 9f81927..7f53610 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ With strawpoll being somewhat very broken I decided to implement my own. Let's g ## What is this If you have never used strawpoll, in short this is a website to easily make small polls without a fuss. +## API +This service offers an API to create and get the status of polls. The API Docs can be found [here](API.md). ## Contributing The core is written in TypeScript, a typed superset to Javascript and executed with NodeJS. Pull Requests welcome. diff --git a/frontend/static/js/result.js b/frontend/static/js/result.js index 62a480a..e62af22 100644 --- a/frontend/static/js/result.js +++ b/frontend/static/js/result.js @@ -48,7 +48,7 @@ function domLoaded() { let prevResult = null; async function fetchNewestResults() { try { - const response = await fetch(POLL_BACKEND_URL + "/_backend/poll-result/" + POLL_ID); + const response = await fetch(POLL_BACKEND_URL + "/_backend/api/poll-result/" + POLL_ID); const json = await response.json(); if (json.error) throw new Error(json.error); const votes = json.votes; diff --git a/src/backend.ts b/src/backend.ts index de9acdb..d0231f3 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -23,47 +23,7 @@ function unxss(str: string) { .replace(/'/g, "'"); } -export default async function init(router: Router, polls: Storage): Promise { - router.get("/poll/: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" }); - res.json(Object.assign({}, poll, { - options: Object.keys(poll.options), - dupeData: null - })); - } catch (error) { - console.error(error); - if (error instanceof Error) res.status(500).json({ - error: error.message - }); - else res.status(500).json({ - error: error - }); - } - }); - - router.get("/poll-result/: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" }); - res.json({ - title: poll.title, - votes: poll.options - }); - } catch (error) { - console.error(error); - if (error instanceof Error) res.status(500).json({ - error: error.message - }); - else res.status(500).json({ - error: error - }); - } - }); - +export default async function init(router: Router, polls: Storage): Promise { async function createPoll(pollData: { title: string, options: string[], @@ -109,66 +69,6 @@ export default async function init(router: Router, polls: Storage): Promise { - try { - const poll = await createPoll({ - title: (req.body.title || "").trim().slice(0, MAX_CHARACTER_LENGTH), - options: req.body.options, - dupeCheckMode: req.body.dupeCheckMode, - multiSelect: req.body.multiSelect || false, - captcha: req.body.captcha || false, - }); - if (typeof poll !== "string") res.json({ - id: poll.id - }); - else res.status(400).json({ - error: poll - }); - } 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("/poll-form", async (req, res) => { - try { - const poll = await createPoll({ - title: (req.body["poll-title"] || "").trim().slice(0, MAX_CHARACTER_LENGTH), - options: req.body["poll-option"], - dupeCheckMode: req.body["dupe-check"], - multiSelect: req.body["multi-select"] === "on", - captcha: req.body["captcha"] === "on", - }); - if (typeof poll !== "string") res.redirect("/" + poll.id); - else res.redirect(`/?error=${ - encodeURIComponent(poll) - }&title=${ - encodeURIComponent(req.body["poll-title"]) - }&options=${ - encodeURIComponent((req.body["poll-option"] || []).slice(0, MAX_POLL_OPTIONS).join("\uFFFE")) - }&dupecheck=${ - encodeURIComponent(req.body["dupe-check"]) - }&multiselect=${ - req.body["multi-select"] === "on" - }&captcha=${ - req.body["captcha"] === "on" - }`); - } catch (error) { - console.error(error); - if (error instanceof Error) res.status(500).json({ - error: error.message - }); - else res.status(500).json({ - error: error - }); - } - }); - async function voteOnPoll(pollId: string, votes: string[], { ip, setCookie, cookies }: { ip: string, setCookie: (name: string, value: string, options?: CookieOptions) => void, @@ -209,6 +109,106 @@ export default async function init(router: Router, polls: Storage): Promise { + 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" }); + res.json(Object.assign({}, poll, { + options: Object.keys(poll.options), + dupeData: null + })); + } catch (error) { + console.error(error); + if (error instanceof Error) res.status(500).json({ + error: error.message + }); + else res.status(500).json({ + error: error + }); + } + }); + router.get("/api/poll-result/: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" }); + res.json({ + title: poll.title, + votes: poll.options + }); + } 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("/api/poll", async (req, res) => { + try { + const poll = await createPoll({ + title: (req.body.title || "").trim().slice(0, MAX_CHARACTER_LENGTH), + options: req.body.options, + dupeCheckMode: req.body.dupeCheckMode, + multiSelect: req.body.multiSelect || false, + captcha: req.body.captcha || false, + }); + if (typeof poll !== "string") res.json({ + id: poll.id + }); + else res.status(400).json({ + error: poll + }); + } catch (error) { + console.error(error); + if (error instanceof Error) res.status(500).json({ + error: error.message + }); + else res.status(500).json({ + error: error + }); + } + }); + // #endregion API + // #region Website Form Endpoints + router.post("/poll-form", async (req, res) => { + try { + const poll = await createPoll({ + title: (req.body["poll-title"] || "").trim().slice(0, MAX_CHARACTER_LENGTH), + options: req.body["poll-option"], + dupeCheckMode: req.body["dupe-check"], + multiSelect: req.body["multi-select"] === "on", + captcha: req.body["captcha"] === "on", + }); + if (typeof poll !== "string") res.redirect("/" + poll.id); + else res.redirect(`/?error=${ + encodeURIComponent(poll) + }&title=${ + encodeURIComponent(req.body["poll-title"]) + }&options=${ + encodeURIComponent((req.body["poll-option"] || []).slice(0, MAX_POLL_OPTIONS).join("\uFFFE")) + }&dupecheck=${ + encodeURIComponent(req.body["dupe-check"]) + }&multiselect=${ + req.body["multi-select"] === "on" + }&captcha=${ + req.body["captcha"] === "on" + }`); + } 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; @@ -250,4 +250,5 @@ export default async function init(router: Router, polls: Storage): Promise r.json()) as PollResult; if (!poll || poll.error) return res.redirect("/"); const totalVotes = Object.values(poll.votes).reduce((acc, cur) => acc + cur, 0); @@ -155,7 +155,7 @@ export default function init(router: Router): void { 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 + (program.opts().backendBaseUrl || "http://localhost:" + program.opts().port) + "/_backend/api/poll/" + id ).then(r => r.json()) as Poll; if (!poll || poll.error) return res.redirect("/");