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

-
- - titleElement = e } /> -
- -
- - authorElement = e } /> -
- -
- -