diff --git a/.envdev b/.envdev
index 4cab1e7..ba4445e 100644
--- a/.envdev
+++ b/.envdev
@@ -7,4 +7,5 @@ DB_PASSWORD=charizard
DB_TIMEZONE=Australia/Sydney
QUERY_LIMIT=10
-QUERY_KEY=key
\ No newline at end of file
+
+SECRET_ACCESS=access
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 8a8628d..2888e94 100644
--- a/.gitignore
+++ b/.gitignore
@@ -104,4 +104,7 @@ dist
.tern-port
# Docker generated files and folders
-data/
+letsencrypt/
+mysql/
+docker-compose.yml
+startup.sql
diff --git a/README.md b/README.md
index 647da45..5cc848f 100644
--- a/README.md
+++ b/README.md
@@ -4,12 +4,24 @@ An API centric news server. Uses Sequelize and mariaDB by default.
# Setup
-This currently runs in docker. It might need to run twice the first time.
+There are multiple ways to run this app - it can run on it's own via `npm start` (for production) or `npm run dev` (for development). it can also run inside docker using `docker-compose up --build` - run `node configure-script.js` to generate docker-compose.yml.
+
+To generate an authorization token, use [auth-server](https://github.com/krgamestudios/auth-server). A public-facing development auth-server is available here (tokens are 10 minutes):
+
+```
+POST https://dev-auth.eggtrainer.com/auth/login HTTP/1.1
+Content-Type: application/json
+
+{
+ "email": "kayneruse@gmail.com",
+ "password": "helloworld"
+}
+```
# API
```
-//NOTE: GET will return null if a specific article can't be found
+//NOTE: GET will return an empty array if a specific article can't be found
//NOTE: you can add a "limit" query parameter to change the default limit
GET /news?limit=10
@@ -33,11 +45,11 @@ GET /news/archive/:id
...
]
-//get the latest titles, up to a default limit, or specify the index "id"
-GET /news/titles/:id
+//get the latest metadata, up to a default limit, or specify the index "id"
+GET /news/metadata/:id
-//get the titles starting from the beginning, up to a default limit, or specify the index "id"
-GET /news/archive/titles/:id
+//get the metadata starting from the beginning, up to a default limit, or specify the index "id"
+GET /news/archive/metadata/:id
//result (if only a single article is specified, returns just that article rather than an array):
[
@@ -54,50 +66,36 @@ GET /news/archive/titles/:id
//send a formatted JSON object, returns new index on success, or error on failure
POST /news
+Authorization: Bearer XXX
//arguments:
{
- "key": key //the whitelist key, allows access to the POST routes
"title": title //title of the article
"author": author //author of the article
"body": body //body of the article
}
-//result:
+//result (status 200 on success, otherwise an error status):
{
- "ok": ok //true on success, otherwise false
- "index": index //new index of the article, or undefined
- "error": error //error encountered, or undefined
+ "index": index //new index of the article
}
//similar to `POST /news`, but allows overwriting an existing article
PATCH /news/:id
+Authorization: Bearer XXX
//arguments:
{
- "key": key //the whitelist key, allows access to the PATCH routes
- "title": title //title of the article
- "author": author //author of the article
- "body": body //body of the article
+ "title": title //title of the article, optional
+ "author": author //author of the article, optional
+ "body": body //body of the article, optional
}
-//result:
-{
- "ok": ok //true on success, otherwise false
- "error": error //error encountered, or undefined
-}
+status 200 on success, otherwise an error status
//remove an article from the news feed
DELETE /news/:id
+Authorization: Bearer XXX
-//arguments:
-{
- "key": key //the whitelist key, allows access to the DELETE routes
-}
-
-//result:
-{
- "ok": ok //true on success, otherwise false
- "error": error //error encountered, or undefined
-}
+status 200 on success, otherwise an error status
```
diff --git a/configure-script.js b/configure-script.js
new file mode 100644
index 0000000..99e4d09
--- /dev/null
+++ b/configure-script.js
@@ -0,0 +1,134 @@
+//setup
+const readline = require('readline');
+const fs = require('fs');
+const crypto = require("crypto");
+
+const uuid = (bytes = 16) => crypto.randomBytes(bytes).toString("hex");
+
+const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ terminal: false
+});
+
+//manually promisify this (util didn't work)
+const question = (prompt, def = null) => {
+ return new Promise((resolve, reject) => {
+ rl.question(`${prompt}${def ? ` (${def})` : ''}: `, answer => {
+ //loop on required
+ if (def === null && !answer) {
+ return resolve(question(prompt, def));
+ }
+
+ return resolve(answer || def);
+ });
+ });
+};
+
+//questions
+(async () => {
+ //project configuration
+ const appName = await question('App Name', 'news');
+ const appWebAddress = await question('Web Addr', `${appName}.example.com`);
+ const appPort = await question('App Port', '3100');
+
+ const appDBUser = await question('DB User', appName);
+ const appDBPass = await question('DB Pass', uuid());
+ const dbRootPass = await question('DB Root Pass');
+
+ const appSecretAccess = await question('Access Token Secret', uuid(32));
+
+ const supportEmail = await question('Support Email', 'example@example.com');
+
+ //generate the files
+ const ymlfile = `
+version: '3'
+
+services:
+ ${appName}:
+ build:
+ context: .
+ ports:
+ - "${appPort}"
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.${appName}router.rule=Host(\`${appWebAddress}\`)"
+ - "traefik.http.routers.${appName}router.entrypoints=websecure"
+ - "traefik.http.routers.${appName}router.tls.certresolver=myresolver"
+ - "traefik.http.routers.${appName}router.service=${appName}service@docker"
+ - "traefik.http.services.${appName}service.loadbalancer.server.port=${appPort}"
+ environment:
+ - WEB_PORT=${appPort}
+ - DB_HOSTNAME=database
+ - DB_DATABASE=${appName}
+ - DB_USERNAME=${appDBUser}
+ - DB_PASSWORD=${appDBPass}
+ - DB_TIMEZONE=Australia/Sydney
+ - QUERY_LIMIT=10
+ - SECRET_ACCESS=${appSecretAccess}
+ networks:
+ - app-network
+ depends_on:
+ - database
+ database:
+ image: mariadb:latest
+ environment:
+ - MYSQL_DATABASE: ${appName}
+ - MYSQL_USER: ${appDBUser}
+ - MYSQL_PASSWORD: ${appDBPass}
+ - MYSQL_ROOT_PASSWORD: ${dbRootPass}
+ networks:
+ - app-network
+ volumes:
+ - ./mysql:/var/lib/mysql
+ - ./startup.sql:/docker-entrypoint-initdb.d/startup.sql:ro
+ traefik:
+ image: "traefik:v2.4"
+ container_name: "traefik"
+ command:
+ - "--log.level=ERROR"
+ - "--api.insecure=false"
+ - "--providers.docker=true"
+ - "--providers.docker.exposedbydefault=false"
+ - "--entrypoints.websecure.address=:443"
+ - "--certificatesresolvers.myresolver.acme.tlschallenge=true"
+ - "--certificatesresolvers.myresolver.acme.email=${supportEmail}"
+ - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - "./letsencrypt:/letsencrypt"
+ - "/var/run/docker.sock:/var/run/docker.sock:ro"
+ networks:
+ - app-network
+networks:
+ app-network:
+ driver: bridge
+`;
+
+ const dockerfile = `
+FROM node:15
+WORKDIR "/app"
+COPY package*.json ./
+RUN npm install --production
+COPY . /app
+EXPOSE ${appPort}
+USER node
+ENTRYPOINT ["bash", "-c"]
+CMD ["sleep 10 && npm start"]
+`;
+
+ const sqlfile = `
+CREATE DATABASE IF NOT EXISTS ${appName};
+CREATE USER IF NOT EXISTS '${appDBUser}'@'%' IDENTIFIED BY '${appDBPass}';
+GRANT ALL PRIVILEGES ON ${appName}.* TO '${appDBUser}'@'%';
+`;
+
+ fs.writeFileSync('docker-compose.yml', ymlfile);
+ fs.writeFileSync('Dockerfile', dockerfile);
+ fs.writeFileSync('startup.sql', sqlfile);
+})()
+ .then(() => rl.close())
+ .catch(e => console.error(e))
+;
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index 1d606fd..0000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-version: '3'
-
-services:
-
- app:
- build:
- context: .
- environment:
- WEB_PORT: 3100
- DB_HOSTNAME: database
- DB_DATABASE: news
- DB_USERNAME: news
- DB_PASSWORD: charizard
- DB_TIMEZONE: Australia/Sydney
- QUERY_LIMIT: 10
- QUERY_KEY: key
- networks:
- - app-network
- ports:
- - "3100:3100"
- depends_on:
- - database
-
- database:
- image: mariadb:latest
- environment:
- MYSQL_DATABASE: news
- MYSQL_USER: news
- MYSQL_PASSWORD: charizard
- MYSQL_ROOT_PASSWORD: root
- networks:
- - app-network
- volumes:
- - ./data:/var/lib/mysql
-
-networks:
- app-network:
- driver: bridge
diff --git a/front-ends/react/news-editor.jsx b/front-ends/react/news-editor.jsx
deleted file mode 100644
index 2bd699d..0000000
--- a/front-ends/react/news-editor.jsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import React, { useState } from 'react';
-import Select from 'react-dropdown-select';
-
-//DOCS: props.uri is the address of a live news-server
-//DOCS: props.newsKey is the key of the live news-server
-const NewsEditor = props => {
- let titleElement, authorElement, bodyElement;
- const [articles, setArticles] = useState(null);
- const [index, setIndex] = useState(null);
-
- if (!articles) {
- fetch(`${props.uri}/titles?limit=999`, { method: 'GET' })
- .then(a => {
- if (!a.ok) {
- throw `Network error ${a.status}: ${a.statusText} ${a.url}`;
- }
- return a.json();
- })
- .then(a => setArticles(a))
- .catch(e => console.error(e))
- ;
- }
-
- return (
-
-
News Editor
-
- Article:
- { return { label: article.title, value: article.index }; })}
- onChange={values => setIndex(fetchSelection(values[0].value, titleElement, authorElement, bodyElement, props.uri))}
- />
-
-
-
- );
-};
-
-const fetchSelection = (index, titleElement, authorElement, bodyElement, uri) => {
- fetch(`${uri}/archive/${index}`, {
- 'Content-Type': 'application/json',
- 'Access-Control-Allow-Origin': '*'
- })
- .then(blob => blob.json())
- .then(article => {
- titleElement.value = article.title;
- authorElement.value = article.author;
- bodyElement.value = article.body;
- })
- .catch(e => console.error(e))
- ;
-
- return index; //this is admittedly odd
-};
-
-const handleSubmit = async (index, title, author, body, uri, newsKey) => {
- title = title.trim();
- author = author.trim();
- body = body.trim();
- uri = uri.trim();
- newsKey = newsKey.trim();
-
- //fetch POST json data
- const raw = await fetch(
- `${uri}/${index}`,
- {
- method: 'PATCH',
- headers: {
- 'Content-Type': 'application/json',
- 'Access-Control-Allow-Origin': '*'
- },
- body: JSON.stringify({ title: title, author: author, body: body, key: newsKey })
- }
- );
-
- if (raw.ok) {
- const result = await raw.json();
-
- if (result.ok) {
- alert(`Updated article index ${index}`);
- } else {
- alert(result.error);
- }
- } else {
- alert(raw.statusText);
- }
-};
-
-export default NewsEditor;
\ No newline at end of file
diff --git a/front-ends/react/news-feed.jsx b/front-ends/react/news-feed.jsx
deleted file mode 100644
index 6a68c1f..0000000
--- a/front-ends/react/news-feed.jsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import React, { useState } from 'react';
-import dateFormat from 'dateformat';
-
-//DOCS: props.uri is the address of a live news-server
-const NewsFeed = props => {
- const [articles, setArticles] = useState(null);
-
- if (!articles) {
- fetch(props.uri, { method: 'GET' })
- .then(a => {
- if (!a.ok) {
- throw `Network error ${a.status}: ${a.statusText} ${a.url}`;
- }
- return a.json();
- })
- .then(a => setArticles(a))
- .catch(e => console.error(e))
- ;
- }
-
- return (
-
-
News Feed
- {(articles || []).map((article, index) => {
- return (
-
-
-
{article.title}
-
Written by {article.author} , {
- article.edits > 0 ?
- Last Updated {dateFormat(articles.updatedAt, 'fullDate')} ({`${article.edits} edit${article.edits > 1 ? 's': ''}`}) :
- Published {dateFormat(articles.createdAt, 'fullDate')}
- }
-
{article.body}
-
- );
- })}
-
- );
-};
-
-export default NewsFeed;
\ No newline at end of file
diff --git a/front-ends/react/news-publisher.jsx b/front-ends/react/news-publisher.jsx
deleted file mode 100644
index 9bb1969..0000000
--- a/front-ends/react/news-publisher.jsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import React from 'react';
-
-//DOCS: props.uri is the address of a live news-server
-//DOCS: props.newsKey is the key of the live news-server
-const NewsPublisher = props => {
- let titleElement, authorElement, bodyElement;
-
- return (
-
-
News Publisher
-
{
- e.preventDefault();
- await handleSubmit(titleElement.value, authorElement.value, bodyElement.value, props.uri, props.newsKey);
- titleElement.value = authorElement.value = bodyElement.value = '';
- }}>
-
- Title:
- titleElement = e } />
-
-
-
- Author:
- authorElement = e } />
-
-
-
- Body:
- bodyElement = e } />
-
-
- Publish
-
-
- );
-};
-
-const handleSubmit = async (title, author, body, uri, newsKey) => {
- title = title.trim();
- author = author.trim();
- body = body.trim();
- uri = uri.trim();
- newsKey = newsKey.trim();
-
- //fetch POST json data
- const raw = await fetch(
- uri,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Access-Control-Allow-Origin': '*'
- },
- body: JSON.stringify({ title: title, author: author, body: body, key: newsKey })
- }
- );
-
- if (raw.ok) {
- const result = await raw.json();
-
- if (result.ok) {
- alert(`Published article index ${result.index}`);
- } else {
- alert(result.error);
- }
- } else {
- alert(raw.statusText);
- }
-};
-
-export default NewsPublisher;
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 330f9b5..4a12871 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
+ "jsonwebtoken": "^8.5.1",
"mariadb": "^2.5.2",
"sequelize": "^6.5.0"
},
@@ -253,6 +254,11 @@
"node": ">=8"
}
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
+ },
"node_modules/bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@@ -576,6 +582,14 @@
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=",
"dev": true
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -1017,6 +1031,59 @@
"integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=",
"dev": true
},
+ "node_modules/jsonwebtoken": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
+ "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^5.6.0"
+ },
+ "engines": {
+ "node": ">=4",
+ "npm": ">=1.4.28"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/jsonwebtoken/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "dependencies": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/keyv": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
@@ -1043,6 +1110,41 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
+ },
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
@@ -2217,6 +2319,11 @@
"fill-range": "^7.0.1"
}
},
+ "buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
+ },
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@@ -2466,6 +2573,14 @@
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=",
"dev": true
},
+ "ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "requires": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -2804,6 +2919,54 @@
"integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=",
"dev": true
},
+ "jsonwebtoken": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
+ "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
+ "requires": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^5.6.0"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+ }
+ }
+ },
+ "jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "requires": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "requires": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"keyv": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
@@ -2827,6 +2990,41 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
+ "lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
+ },
+ "lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
+ },
+ "lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
+ },
+ "lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
+ },
+ "lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
+ },
+ "lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
+ },
+ "lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
+ },
"long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
diff --git a/package.json b/package.json
index 7976ad7..942f410 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
+ "jsonwebtoken": "^8.5.1",
"mariadb": "^2.5.2",
"sequelize": "^6.5.0"
},
diff --git a/server/database/index.js b/server/database/index.js
index 56439e8..8667adf 100644
--- a/server/database/index.js
+++ b/server/database/index.js
@@ -4,7 +4,7 @@ const sequelize = new Sequelize(process.env.DB_DATABASE, process.env.DB_USERNAME
host: process.env.DB_HOSTNAME,
dialect: 'mariadb',
timezone: process.env.DB_TIMEZONE,
-// logging: false
+ logging: false
});
module.exports = sequelize;
\ No newline at end of file
diff --git a/server/database/models/articles.js b/server/database/models/articles.js
index c352426..a7bc948 100644
--- a/server/database/models/articles.js
+++ b/server/database/models/articles.js
@@ -1,7 +1,7 @@
const Sequelize = require('sequelize');
const sequelize = require('..');
-module.exports = sequelize.define('articles', {
+const articles = sequelize.define('articles', {
index: {
type: Sequelize.INTEGER(11),
allowNull: false,
@@ -30,3 +30,7 @@ module.exports = sequelize.define('articles', {
defaultValue: 0
}
});
+
+sequelize.sync();
+
+module.exports = articles;
\ No newline at end of file
diff --git a/server/database/models/revisions.js b/server/database/models/revisions.js
index 4bb5fa3..1ab45d7 100644
--- a/server/database/models/revisions.js
+++ b/server/database/models/revisions.js
@@ -3,8 +3,6 @@ const sequelize = require('..');
const articles = require('./articles');
-sequelize.sync();
-
const revisions = sequelize.define('revisions', {
title: {
type: Sequelize.TEXT,
diff --git a/server/news/edit.js b/server/news/edit.js
index ad9af11..fe8bd1d 100644
--- a/server/news/edit.js
+++ b/server/news/edit.js
@@ -2,11 +2,6 @@ const { Op } = require('sequelize');
const { articles, revisions } = require('../database/models');
const route = async (req, res) => {
- //check the key
- if (req.body.key != process.env.QUERY_KEY) {
- return res.status(401).json({ ok: false, error: 'invalid key' });
- }
-
//get the existing record
const record = await articles.findOne({
where: {
@@ -17,7 +12,7 @@ const route = async (req, res) => {
});
if (!record) {
- return res.status(500).json({ ok: false, error: 'failed to update non-existing record' });
+ return res.status(500).send('Failed to update non-existing record');
}
//store the revision
@@ -40,9 +35,7 @@ const route = async (req, res) => {
}
});
- return res.status(200).json({
- ok: true
- });
+ return res.status(200).end();
};
module.exports = route;
\ No newline at end of file
diff --git a/server/news/index.js b/server/news/index.js
index d563e4b..2d1969d 100644
--- a/server/news/index.js
+++ b/server/news/index.js
@@ -1,26 +1,38 @@
const express = require('express');
const router = express.Router();
+//middleware
+const authToken = require('../utilities/token-auth');
+
//the routes
const query = require('./query');
const publish = require('./publish');
const edit = require('./edit');
const remove = require('./remove');
-//basic route management
+//basic route management (all query possibilities)
router.get('/', query(false, false));
router.get('/:id(\\d+)', query(false, false));
router.get('/archive', query(true, false));
router.get('/archive/:id(\\d+)', query(true, false));
-router.get('/titles', query(false, true));
-router.get('/titles/:id(\\d+)', query(false, true));
-router.get('/archive/titles', query(true, true));
-router.get('/archive/titles/:id(\\d+)', query(true, true));
+router.get('/metadata', query(false, true));
+router.get('/metadata/:id(\\d+)', query(false, true));
+router.get('/archive/metadata', query(true, true));
+router.get('/archive/metadata/:id(\\d+)', query(true, true));
+//use middleware to authenticate the rest of the routes
+router.use(authToken);
+router.use((req, res, next) => {
+ if (req.user.privilege == 'administrator') {
+ next();
+ } else {
+ res.status(403).end();
+ }
+});
+
+//authenticated routes
router.post('/', publish);
-
router.patch('/:id(\\d+)', edit);
-
router.delete('/:id(\\d+)', remove);
module.exports = router;
diff --git a/server/news/publish.js b/server/news/publish.js
index 51d03e8..36750e2 100644
--- a/server/news/publish.js
+++ b/server/news/publish.js
@@ -1,11 +1,6 @@
const { articles } = require('../database/models');
const route = async (req, res) => {
- //check the key
- if (req.body.key != process.env.QUERY_KEY) {
- return res.status(401).json({ ok: false, error: 'invalid key' });
- }
-
//upsert the data
const [instance, created] = await articles.upsert({
title: req.body.title,
@@ -14,10 +9,10 @@ const route = async (req, res) => {
});
if (!created) {
- return res.status(500).json({ ok: false, error: 'failed to create record' });
+ return res.status(500).send('Failed to create record');
}
- //BUGFIX
+ //BUGFIX: instance doesn't have the index for some reason
const result = await articles.findOne({
order: [
['index', 'DESC']
@@ -25,7 +20,6 @@ const route = async (req, res) => {
});
return res.status(200).json({
- ok: true,
// index: instance.get('index')
index: result.index
});
diff --git a/server/news/query.js b/server/news/query.js
index 5ecf741..30ffbf4 100644
--- a/server/news/query.js
+++ b/server/news/query.js
@@ -2,12 +2,12 @@ const { Op } = require('sequelize');
const { articles } = require('../database/models');
//the query function that can be reused
-const query = (ascending, titlesOnly) => async (req, res) => {
- //specific search
+const query = (ascending, metadataOnly) => async (req, res) => {
+ //specific search (id is defined)
if (req.params.id && typeof(parseInt(req.params.id)) === 'number') {
const result = await articles.findOne({
attributes: [
- 'index', 'title', 'author', 'edits', 'createdAt', 'updatedAt', ...(!titlesOnly ? ['body'] : [])
+ 'index', 'title', 'author', 'edits', 'createdAt', 'updatedAt', ...(!metadataOnly ? ['body'] : [])
],
where: {
index: {
@@ -16,15 +16,15 @@ const query = (ascending, titlesOnly) => async (req, res) => {
}
});
- //returns null if failed to find
- return res.status(200).json(result);
+ //result is null if failed to find
+ return res.status(200).json(result || []);
}
//default search
else {
const result = await articles.findAndCountAll({
attributes: [
- 'index', 'title', 'author', 'edits', 'createdAt', 'updatedAt', ...(!titlesOnly ? ['body'] : [])
+ 'index', 'title', 'author', 'edits', 'createdAt', 'updatedAt', ...(!metadataOnly ? ['body'] : [])
],
order: [
['index', ascending ? 'ASC' : 'DESC']
diff --git a/server/news/remove.js b/server/news/remove.js
index 605e04b..0660577 100644
--- a/server/news/remove.js
+++ b/server/news/remove.js
@@ -2,11 +2,6 @@ const { Op } = require('sequelize');
const { articles, revisions } = require('../database/models');
const route = async (req, res) => {
- //check the key
- if (req.body.key != process.env.QUERY_KEY) {
- return res.status(401).json({ ok: false, error: 'invalid key' });
- }
-
//get the existing record
const record = await articles.findOne({
where: {
@@ -17,7 +12,7 @@ const route = async (req, res) => {
});
if (!record) {
- return res.status(500).json({ ok: false, error: 'failed to remove non-existing record' });
+ return res.status(500).json('Failed to remove non-existing record');
}
//store the revision
@@ -35,9 +30,7 @@ const route = async (req, res) => {
}
});
- return res.status(200).json({
- ok: true
- });
+ return res.status(200).end();
};
module.exports = route;
\ No newline at end of file
diff --git a/server/utilities/token-auth.js b/server/utilities/token-auth.js
new file mode 100644
index 0000000..0231b31
--- /dev/null
+++ b/server/utilities/token-auth.js
@@ -0,0 +1,21 @@
+const jwt = require('jsonwebtoken');
+
+//middleware to authenticate the JWT token
+module.exports = (req, res, next) => {
+ const authHeader = req.headers['authorization'];
+ const token = authHeader?.split (' ')[1]; //'Bearer token'
+
+ if (!token) {
+ return res.status(401).end();
+ }
+
+ jwt.verify(token, process.env.SECRET_ACCESS, (err, user) => {
+ if (err) {
+ return res.status(403).end();
+ }
+
+ req.user = user;
+
+ next();
+ });
+};
\ No newline at end of file
diff --git a/test/requests.rest b/test/requests.rest
new file mode 100644
index 0000000..7563224
--- /dev/null
+++ b/test/requests.rest
@@ -0,0 +1,54 @@
+#Login to the auth-server
+POST http://127.0.0.1:3200/auth/login HTTP/1.1
+Content-Type: application/json
+
+{
+ "email": "kayneruse@gmail.com",
+ "password": "helloworld"
+}
+
+###
+
+#Refresh from the auth-server
+POST http://127.0.0.1:3200/auth/token HTTP/1.1
+Content-Type: application/json
+
+{
+ "token": ""
+}
+
+###
+
+#Query
+GET http://127.0.0.1:3100/news HTTP/1.1
+
+###
+
+#Publish
+POST http://127.0.0.1:3100/news HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer
+
+{
+ "title": "Hello World",
+ "author": "Kayne Ruse",
+ "body": "Lorem ipsum dolor sit amet..."
+}
+
+###
+
+#Edit
+PATCH http://127.0.0.1:3100/news/5 HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer
+
+{
+ "title": "Goodnight World"
+}
+
+###
+
+#Delete
+DELETE http://127.0.0.1:3100/news/4 HTTP/1.1
+Content-Type: application/json
+Authorization: Bearer