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
|
## 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.
|
||||||
|
|
|
@ -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;
|
||||||
|
|
203
src/backend.ts
203
src/backend.ts
|
@ -23,47 +23,7 @@ function unxss(str: string) {
|
||||||
.replace(/'/g, "'");
|
.replace(/'/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
|
||||||
}
|
}
|
|
@ -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("/");
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue