mirror of
https://github.com/Wolvan/poll.horse.git
synced 2024-11-21 20:47:59 +01:00
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:
parent
5260cfb7da
commit
cfa150cc42
5 changed files with 294 additions and 104 deletions
187
API.md
Normal file
187
API.md
Normal 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>"
|
||||
}
|
||||
```
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
201
src/backend.ts
201
src/backend.ts
|
@ -24,46 +24,6 @@ function unxss(str: string) {
|
|||
}
|
||||
|
||||
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: {
|
||||
title: string,
|
||||
options: string[],
|
||||
|
@ -109,66 +69,6 @@ export default async function init(router: Router, polls: Storage): Promise<void
|
|||
await polls.setItem(id, 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 }: {
|
||||
ip: string,
|
||||
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;
|
||||
}
|
||||
|
||||
// #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) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
|
@ -250,4 +250,5 @@ export default async function init(router: Router, polls: Storage): Promise<void
|
|||
});
|
||||
}
|
||||
});
|
||||
// #endregion Website Form Endpoints
|
||||
}
|
|
@ -120,7 +120,7 @@ export default function init(router: Router): void {
|
|||
const id = req.params.id;
|
||||
try {
|
||||
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;
|
||||
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("/");
|
||||
|
||||
|
|
Loading…
Reference in a new issue