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("/");