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.
This commit is contained in:
Wolvan 2022-01-12 21:07:55 +01:00
parent 5260cfb7da
commit cfa150cc42
5 changed files with 294 additions and 104 deletions

187
API.md Normal file
View file

@ -0,0 +1,187 @@
# API
## API URL
The base URL for all API calls is `/_backend/api.`
<br>
<br>
## 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 <br />
**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 <br />
**Content:**
```json
{
"error" : "Poll not found"
}
```
OR
* **Code:** 500 INTERNAL SERVER ERROR <br />
**Content:**
```json
{
"error" : "<ERROR_MESSAGE_STRING>"
}
```
## 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 <br />
**Content:**
```json
{
"title": "An API requested Poll",
"options": {
"Option A": 0,
"Option B": 3,
"Option C": 1
}
}
```
* **Error Response:**
* **Code:** 404 NOT FOUND <br />
**Content:**
```json
{
"error" : "Poll not found"
}
```
OR
* **Code:** 500 INTERNAL SERVER ERROR <br />
**Content:**
```json
{
"error" : "<ERROR_MESSAGE_STRING>"
}
```
## 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 <br />
**Content:**
```json
{
"id": "abcd1234"
}
```
* **Error Response:**
* **Code:** 400 BAD REQUEST <br />
**Content:**
```json
{
"error" : "<ERROR_MESSAGE_STRING>"
}
```
**Possible Error Messages:**
- `Options must be an array and have at least 2 different entries`
- `Only <<MAX_POLL_OPTIONS>> options are allowed`
OR
* **Code:** 500 INTERNAL SERVER ERROR <br />
**Content:**
```json
{
"error" : "<ERROR_MESSAGE_STRING>"
}
```

View file

@ -5,6 +5,8 @@ With strawpoll being somewhat very broken I decided to implement my own. Let's g
## What is this ## What is this
If you have never used strawpoll, in short this is a website to easily make small polls without a fuss. 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 ## Contributing
The core is written in TypeScript, a typed superset to Javascript and executed with NodeJS. Pull Requests welcome. The core is written in TypeScript, a typed superset to Javascript and executed with NodeJS. Pull Requests welcome.

View file

@ -48,7 +48,7 @@ function domLoaded() {
let prevResult = null; let prevResult = null;
async function fetchNewestResults() { async function fetchNewestResults() {
try { 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(); const json = await response.json();
if (json.error) throw new Error(json.error); if (json.error) throw new Error(json.error);
const votes = json.votes; const votes = json.votes;

View file

@ -23,47 +23,7 @@ function unxss(str: string) {
.replace(/&#039;/g, "'"); .replace(/&#039;/g, "'");
} }
export default async function init(router: Router, polls: Storage): Promise<void> { export default async function init(router: Router, polls: Storage): Promise<void> {
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
});
}
});
async function createPoll(pollData: { async function createPoll(pollData: {
title: string, title: string,
options: string[], options: string[],
@ -109,66 +69,6 @@ export default async function init(router: Router, polls: Storage): Promise<void
await polls.setItem(id, poll); await polls.setItem(id, poll);
return poll; return poll;
} }
router.post("/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
});
}
});
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 }: { async function voteOnPoll(pollId: string, votes: string[], { ip, setCookie, cookies }: {
ip: string, ip: string,
setCookie: (name: string, value: string, options?: CookieOptions) => void, setCookie: (name: string, value: string, options?: CookieOptions) => void,
@ -209,6 +109,106 @@ export default async function init(router: Router, polls: Storage): Promise<void
return null; return null;
} }
// #region API
router.get("/api/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("/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) => { router.post("/vote-form/:id", async (req, res) => {
try { try {
const id = req.params.id; const id = req.params.id;
@ -250,4 +250,5 @@ export default async function init(router: Router, polls: Storage): Promise<void
}); });
} }
}); });
// #endregion Website Form Endpoints
} }

View file

@ -120,7 +120,7 @@ export default function init(router: Router): void {
const id = req.params.id; const id = req.params.id;
try { try {
const poll: PollResult = await fetch( const poll: PollResult = await fetch(
(program.opts().backendBaseUrl || "http://localhost:" + program.opts().port) + "/_backend/poll-result/" + id (program.opts().backendBaseUrl || "http://localhost:" + program.opts().port) + "/_backend/api/poll-result/" + id
).then(r => r.json()) as PollResult; ).then(r => r.json()) as PollResult;
if (!poll || poll.error) return res.redirect("/"); if (!poll || poll.error) return res.redirect("/");
const totalVotes = Object.values(poll.votes).reduce((acc, cur) => acc + cur, 0); 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); 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/api/poll/" + id
).then(r => r.json()) as Poll; ).then(r => r.json()) as Poll;
if (!poll || poll.error) return res.redirect("/"); if (!poll || poll.error) return res.redirect("/");