From 0481099098deefc98b94bc00664b2ee8d94877f9 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 24 Feb 2019 16:27:24 -0500 Subject: [PATCH] add admin pages for whitelist management --- api/controllers/AdminController.js | 27 ++++ api/controllers/PublishKeyController.js | 2 +- api/models/PublishKey.js | 19 ++- api/models/User.js | 3 +- api/policies/adminAuth.js | 4 + api/policies/keyAuth.js | 12 +- api/policies/sessionAuth.js | 4 +- assets/js/actions/admin.js | 51 ++++++ assets/js/actions/index.js | 33 ++-- assets/js/actions/login.js | 10 +- assets/js/admin.js | 149 ++++++++++++++++++ assets/js/containers/PublisherListItem.js | 38 +++-- assets/js/index.js | 8 +- assets/js/lib/Util.js | 11 ++ assets/js/reducers/admin.js | 22 +++ assets/js/reducers/index.js | 4 + assets/styles/admin.scss | 48 ++++++ assets/styles/index.scss | 2 +- assets/styles/shared/listitem.scss | 25 +++ assets/styles/shared/twopanels.scss | 1 - assets/templates/admin.html | 18 +++ assets/templates/index.html | 1 + assets/templates/login.html | 3 +- config/http.js | 12 +- config/models.js | 2 +- config/policies.js | 4 + config/routes.js | 11 +- docs/api.md | 10 +- .../20190220133443_add_srcHost_to_book.js | 1 - migrations/20190224022422_add_admin_users.js | 25 +++ shrinkwrap.yaml | 0 views/pages/admin.ejs | 1 + webpack.config.js | 16 +- 33 files changed, 515 insertions(+), 62 deletions(-) create mode 100644 api/controllers/AdminController.js create mode 100644 api/policies/adminAuth.js create mode 100644 assets/js/actions/admin.js create mode 100644 assets/js/admin.js create mode 100644 assets/js/lib/Util.js create mode 100644 assets/js/reducers/admin.js create mode 100644 assets/styles/admin.scss create mode 100644 assets/templates/admin.html create mode 100644 migrations/20190224022422_add_admin_users.js delete mode 100644 shrinkwrap.yaml create mode 100644 views/pages/admin.ejs diff --git a/api/controllers/AdminController.js b/api/controllers/AdminController.js new file mode 100644 index 0000000..4331178 --- /dev/null +++ b/api/controllers/AdminController.js @@ -0,0 +1,27 @@ +const HttpError = require('../errors/HttpError') + +module.exports = { + show: async function (req, res) { + res.view('pages/admin', { + email: req.user.email + }) + }, + listUsers: async function (req, res) { + try { + const users = await User.find({}) + return res.json(users) + } catch (e) { + return (new HttpError(500, e.message)).send(res) + } + }, + listPublishers: async function (req, res) { + try { + const publishers = await PublishKey.find({ + select: ['id', 'user', 'appid', 'url', 'created_at', 'updated_at'] + }).populate('user') + return res.json(publishers) + } catch (e) { + return (new HttpError(500, e.message)).send(res) + } + } +} diff --git a/api/controllers/PublishKeyController.js b/api/controllers/PublishKeyController.js index 5e768b0..e291170 100644 --- a/api/controllers/PublishKeyController.js +++ b/api/controllers/PublishKeyController.js @@ -17,7 +17,7 @@ module.exports = { }, list: async function (req, res) { try { - const keys = await PublishKey.find() + const keys = await PublishKey.find({ user: req.user.id }) return res.json(keys) } catch (e) { return (new HttpError(500, e.message)).send(res) diff --git a/api/models/PublishKey.js b/api/models/PublishKey.js index a03dfb0..8560124 100644 --- a/api/models/PublishKey.js +++ b/api/models/PublishKey.js @@ -1,10 +1,10 @@ const crypto = require('crypto') -function generateToken({ bytes, base }) { - return new Promise((res, rej) => { +function generateToken ({ bytes, base }) { + return new Promise((resolve, reject) => { crypto.randomBytes(bytes, (err, buf) => { - if (err) rej(err) - else res(buf.toString(base || 'base64')) + if (err) reject(err) + else resolve(buf.toString(base || 'base64')) }) }) } @@ -24,17 +24,16 @@ module.exports = { type: 'string', required: true }, - key: { - type: 'string', - required: true + whitelisted: 'boolean', + appid: { + type: 'string' }, secret: { - type: 'string', - required: true + type: 'string' } }, beforeCreate: async function (key, next) { - key.key = await generateToken({ bytes: 12 }) + key.appid = await generateToken({ bytes: 12 }) key.secret = await generateToken({ bytes: 48 }) next() }, diff --git a/api/models/User.js b/api/models/User.js index c8e0292..b417cd4 100644 --- a/api/models/User.js +++ b/api/models/User.js @@ -18,7 +18,8 @@ module.exports = { }, email: { type: 'string' - } + }, + admin: 'boolean' // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗ // ║╣ ║║║╠╩╗║╣ ║║╚═╗ diff --git a/api/policies/adminAuth.js b/api/policies/adminAuth.js new file mode 100644 index 0000000..aec17ab --- /dev/null +++ b/api/policies/adminAuth.js @@ -0,0 +1,4 @@ +module.exports = async function (req, res, next) { + if (req.user && (req.user.id === 1 || req.user.admin)) next() + else res.status(403).json({ error: 'You are not permitted to perform this action.' }) +} diff --git a/api/policies/keyAuth.js b/api/policies/keyAuth.js index c1963b9..e4180e3 100644 --- a/api/policies/keyAuth.js +++ b/api/policies/keyAuth.js @@ -1,11 +1,11 @@ module.exports = async function (req, res, next) { - const key = req.param('key') || req.header('x-roe-publish-key') - const secret = req.param('secret') || req.header('x-roe-publish-secret') - console.log(key) - console.log(secret) + const key = req.param('key') || req.headers['roe-key'] + const secret = req.param('secret') || req.headers['roe-secret'] - if (await PublishKey.findOne({ key, secret })) { - return next() + const pk = await PublishKey.findOne({ appid: key, secret }) + if (pk) { + if (pk.whitelisted) return next() + else res.status(403).json({ error: 'Your key has not been whitelisted yet. Please contact the site operator.' }) } res.status(403).json({ error: 'Invalid publishing key.' }) diff --git a/api/policies/sessionAuth.js b/api/policies/sessionAuth.js index 58bbde4..1d22cbd 100644 --- a/api/policies/sessionAuth.js +++ b/api/policies/sessionAuth.js @@ -9,6 +9,6 @@ module.exports = function (req, res, next) { if (req.session.authenticated) { return next() } - // res.status(403).json({ error: 'You are not permitted to perform this action.' }) - res.redirect('/login') + res.status(403).json({ error: 'You are not permitted to perform this action.' }) + // res.redirect('/login') } diff --git a/assets/js/actions/admin.js b/assets/js/actions/admin.js new file mode 100644 index 0000000..a20b7a4 --- /dev/null +++ b/assets/js/actions/admin.js @@ -0,0 +1,51 @@ +'use strict' + +import Ajax from '../lib/Ajax' + +const getPath = str => window.location.hostname === 'localhost' ? `http://localhost:3000${str}` : str + +const ACTIONS = { + set_working: 'set_working', + error: 'error', + set_admin_data: 'set_admin_data' +} + +export default ACTIONS + +export const setWorking = working => ({ + type: ACTIONS.set_working, + data: working +}) + +export const fetchAdminData = () => async (dispatch, getState) => { + dispatch(setWorking(true)) + try { + const { data: user } = await Ajax.get({ + url: getPath('/api/me') + }) + + const { data: users } = await Ajax.get({ + url: getPath('/admin/api/users') + }) + + const { data: publishers } = await Ajax.get({ + url: getPath('/admin/api/publishers') + }) + + dispatch({ + type: ACTIONS.set_admin_data, + data: { + user, + users, + publishers + } + }) + } catch (e) { + dispatch({ + type: ACTIONS.error, + data: e + }) + } finally { + dispatch(setWorking(false)) + } +} diff --git a/assets/js/actions/index.js b/assets/js/actions/index.js index 9500e21..c2e2794 100644 --- a/assets/js/actions/index.js +++ b/assets/js/actions/index.js @@ -2,6 +2,8 @@ import Ajax from '../lib/Ajax' +const getPath = str => window.location.hostname === 'localhost' ? `http://localhost:3000${str}` : str + const ACTIONS = { set_working: 'set_working', add_url: 'add_url', @@ -12,7 +14,8 @@ const ACTIONS = { error: 'error', set_user: 'set_user', add_publisher: 'add_publisher', - delete_publisher: 'delete_publisher' + delete_publisher: 'delete_publisher', + set_publishers: 'set_publishers' } export default ACTIONS @@ -32,6 +35,11 @@ export const setUser = user => ({ data: user }) +export const setPublishers = user => ({ + type: ACTIONS.set_publishers, + data: user +}) + export const addUrl = url => ({ type: ACTIONS.add_url, data: url @@ -60,7 +68,7 @@ export const removeUrl = id => async (dispatch, getState) => { dispatch(setWorking(true)) try { await Ajax.delete({ - url: '/api/targets/' + id + url: getPath('/api/targets/' + id) }) dispatch({ type: ACTIONS.delete_url, @@ -80,13 +88,18 @@ export const fetchData = () => async (dispatch, getState) => { dispatch(setWorking(true)) try { const { data: user } = await Ajax.get({ - url: '/api/me' + url: getPath('/api/me') }) dispatch(setUser(user)) const { data: urls } = await Ajax.get({ - url: '/api/targets' + url: getPath('/api/targets') }) dispatch(setUrls(urls)) + + const { data: publishers } = await Ajax.get({ + url: getPath('/api/keys') + }) + dispatch(setPublishers(publishers)) } catch (e) { dispatch({ type: ACTIONS.error, @@ -101,7 +114,7 @@ export const createNewUrl = () => async (dispatch, getState) => { dispatch(setWorking(true)) try { const { data } = await Ajax.post({ - url: '/api/targets' + url: getPath('/api/targets') }) dispatch(addUrl(data)) } catch (e) { @@ -118,7 +131,7 @@ export const setUrl = (value) => async (dispatch, getState) => { dispatch(setWorking(true)) try { await Ajax.patch({ - url: '/api/targets/' + value.id, + url: getPath('/api/targets/' + value.id), data: { ...value, id: undefined @@ -140,7 +153,7 @@ export const editUser = (user) => async (dispatch, getState) => { try { // if (!user.currentPassword) throw new Error('Please enter your current password.') await Ajax.patch({ - url: '/api/me', + url: getPath('/api/me'), data: { id: user.id, email: user.email, @@ -151,7 +164,7 @@ export const editUser = (user) => async (dispatch, getState) => { dispatch({ type: ACTIONS.error, data: null - }) + }) } catch (e) { dispatch({ type: ACTIONS.error, @@ -166,7 +179,7 @@ export const createNewPublisher = (url) => async (dispatch, getState) => { dispatch(setWorking(true)) try { const { data } = await Ajax.post({ - url: '/api/keys', + url: getPath('/api/keys'), data: { url } @@ -186,7 +199,7 @@ export const removePublisher = id => async (dispatch, getState) => { dispatch(setWorking(true)) try { await Ajax.delete({ - url: '/api/keys/' + id + url: getPath('/api/keys/' + id) }) dispatch({ type: ACTIONS.delete_publisher, diff --git a/assets/js/actions/login.js b/assets/js/actions/login.js index 26347a5..47612c9 100644 --- a/assets/js/actions/login.js +++ b/assets/js/actions/login.js @@ -2,6 +2,8 @@ import Ajax from '../lib/Ajax' +const getPath = str => window.location.hostname === 'localhost' ? `http://localhost:3000${str}` : str + const ACTIONS = { set_working: 'set_working', set_user: 'set_user', @@ -56,7 +58,7 @@ export const checkEmail = email => async (dispatch, getState) => { if (/^([a-zA-Z0-9_\-.]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,5})$/.test(email)) { try { await Ajax.post({ - url: '/auth/email_exists', + url: getPath('/auth/email_exists'), data: { email } @@ -83,7 +85,7 @@ export const checkPassword = (email, password) => async (dispatch, getState) => // do email + password check try { const res = await Ajax.post({ - url: '/auth/local', + url: getPath('/auth/local'), data: { identifier: email, password @@ -106,13 +108,13 @@ export const signup = (email, password) => async (dispatch, getState) => { if (/^([a-zA-Z0-9_\-.]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,5})$/.test(email)) { try { await Ajax.post({ - url: '/auth/email_available', + url: getPath('/auth/email_available'), data: { email } }) await Ajax.post({ - url: '/register', + url: getPath('/register'), data: { email, password diff --git a/assets/js/admin.js b/assets/js/admin.js new file mode 100644 index 0000000..ec62d9b --- /dev/null +++ b/assets/js/admin.js @@ -0,0 +1,149 @@ +'use strict' + +import React from 'react' +import ReactDOM from 'react-dom' +import { BrowserRouter as Router, Route, NavLink, Switch, Redirect } from 'react-router-dom' +import Progress from './components/Progress' +import appReducer from './reducers' +import adminReducer from './reducers/admin' +import { fetchAdminData } from './actions/admin' +import Util from './lib/Util' + +import '../styles/admin.scss' + +const reducer = Util.combineReducers(appReducer, adminReducer) + +class App extends React.Component { + constructor () { + super() + this.state = { + error: '', + user: { + id: '', + email: '', + password: '', + currentPassword: '' + }, + users: [], + publishers: [], + working: false + } + + this.dispatch = this.dispatch.bind(this) + this.getRegisteredUsers = this.getRegisteredUsers.bind(this) + this.getRegisteredPublishers = this.getRegisteredPublishers.bind(this) + } + dispatch (action) { + if (!action) throw new Error('dispatch: missing action') + if (action instanceof Function) { + action(this.dispatch, () => this.state) + } else { + const changes = reducer(this.state, action) + if (!changes || !Object.keys(changes).length) return + this.setState({ + ...changes + }) + } + } + componentDidMount () { + this.dispatch(fetchAdminData()) + } + getRegisteredUsers () { + return this.state.users.map(user => { + return ( +
  • + {user.email} + + + + +
    + {user.created_at} + {user.updated_at} +
    +
  • + ) + }) + } + getRegisteredPublishers () { + return this.state.publishers.map(pub => { + return ( +
  • +
    + {pub.url}{pub.appid} + {pub.user.email} +
    + + + + +
    + {pub.created_at} + {pub.updated_at} +
    +
  • + ) + }) + } + render () { + return ( + +
    + +
    + + {this.state.error &&
    {this.state.error}
    } + + ( +
    +
    +
    +

    Site users

    +

    Registered users on RoE

    +
    +
    +
      + {this.getRegisteredUsers()} +
    +
    + )} /> + + ( +
    +
    +
    +

    Publishers

    +

    Whitelist sites who can publish books

    +
    +
    +
      + {this.getRegisteredPublishers()} +
    +
    + )} /> + + } /> +
    +
    +
    +
    + ) + } +} + +ReactDOM.render(, document.getElementById('root')) diff --git a/assets/js/containers/PublisherListItem.js b/assets/js/containers/PublisherListItem.js index 798b92c..f09407a 100644 --- a/assets/js/containers/PublisherListItem.js +++ b/assets/js/containers/PublisherListItem.js @@ -1,26 +1,38 @@ import React from 'react' import IconButton from '../components/IconButton' -import UnderlineInput from '../components/UnderlineInput' import { removePublisher } from '../actions' import '../../styles/shared/listitem.scss' -const uriRegex = /(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i - class PublisherListItem extends React.Component { + constructor () { + super() + this.state = { + revealed: false + } + this.toggleReveal = this.toggleReveal.bind(this) + } + toggleReveal () { + this.setState({ + revealed: !this.state.revealed + }) + } render () { return ( -
  • -
    - Website URL +
  • +
    + Website name {this.props.item.url}
    -
    - Key - -
    -
    - Secret - +
    +
    + AppID + +
    +
    + Secret + +
    +
    this.props.dispatch(removePublisher(this.props.item.id))} />
  • diff --git a/assets/js/index.js b/assets/js/index.js index 313bb75..48cb1cd 100644 --- a/assets/js/index.js +++ b/assets/js/index.js @@ -6,6 +6,7 @@ import { BrowserRouter as Router, Route, NavLink, Switch, Redirect } from 'react import Progress from './components/Progress' import UnderlineInput from './components/UnderlineInput' import UriListItem from './containers/UriListItem' +import PublisherListItem from './containers/PublisherListItem' import reducer from './reducers' import { fetchData, createNewUrl, setEditing, editUser, createNewPublisher } from './actions' @@ -105,9 +106,12 @@ class App extends React.Component {
    @@ -139,7 +143,7 @@ class App extends React.Component {
    diff --git a/assets/js/lib/Util.js b/assets/js/lib/Util.js new file mode 100644 index 0000000..eea79e1 --- /dev/null +++ b/assets/js/lib/Util.js @@ -0,0 +1,11 @@ +export default class Util { + static combineReducers (...reducers) { + return function (...reducerParams) { + for (const reduce of reducers) { + const changes = reduce(...reducerParams) + if (changes && Object.keys(changes).length) return changes + } + return {} + } + } +} diff --git a/assets/js/reducers/admin.js b/assets/js/reducers/admin.js new file mode 100644 index 0000000..e40b442 --- /dev/null +++ b/assets/js/reducers/admin.js @@ -0,0 +1,22 @@ +'use strict' + +import Actions from '../actions/admin' + +const reducer = (state = {}, action) => { + const { type, data } = action + switch (type) { + case Actions.set_working: + return { + working: data + } + case Actions.set_admin_data: + return { + user: data.user, + users: data.users, + publishers: data.publishers + } + default: return {} + } +} + +export default reducer diff --git a/assets/js/reducers/index.js b/assets/js/reducers/index.js index 1bee852..bddda7f 100644 --- a/assets/js/reducers/index.js +++ b/assets/js/reducers/index.js @@ -17,6 +17,10 @@ const reducer = (state = {}, action) => { ...data } } + case Actions.set_publishers: + return { + publishers: data + } case Actions.list_url: return { urls: data || [] diff --git a/assets/styles/admin.scss b/assets/styles/admin.scss new file mode 100644 index 0000000..76c3a70 --- /dev/null +++ b/assets/styles/admin.scss @@ -0,0 +1,48 @@ +@import 'index'; + +.admin-container { + .list { + li { + padding: 5px 0; + height: 50px; + line-height: 40px; + + .stack { + line-height: 20px; + } + .cb-label { + display: inline-block; + height: 40px; + line-height: 40px; + margin-right: 14px; + cursor: pointer; + } + input.checkbox[type=checkbox] { + display: none; + + + label { + display: inline-block; + position: relative; + height: 20px; + width: 20px; + margin: 8px 0 12px 0; + vertical-align: middle; + border: 2px solid $black-1; + cursor: pointer; + } + + &:checked + label { + &:before { + position: absolute; + content: ''; + background: $accent-2; + height: 14px; + width: 14px; + left: 1px; + top: 1px; + } + } + } + } + } +} diff --git a/assets/styles/index.scss b/assets/styles/index.scss index a3440b4..8e18d05 100644 --- a/assets/styles/index.scss +++ b/assets/styles/index.scss @@ -28,7 +28,7 @@ margin-top: 4px; color: $text-dark-2; text-shadow: 1px 1px 2px $black-4; - + } .creator { padding: 0 14px; line-height: 60px; diff --git a/assets/styles/shared/listitem.scss b/assets/styles/shared/listitem.scss index 3a91dfe..2072486 100644 --- a/assets/styles/shared/listitem.scss +++ b/assets/styles/shared/listitem.scss @@ -55,4 +55,29 @@ margin-top: -4px; } } + + &.publisher-list-item { + cursor: default; + + .stack { + margin-right: 14px; + + &.site-name { + min-width: 400px; + flex: 0.5; + } + + input.value { + outline: none; + border: none; + font-family: monospace; + } + } + .btn-view { + margin: 18px 14px; + min-width: 90px; + height: 24px; + line-height: 24px; + } + } } diff --git a/assets/styles/shared/twopanels.scss b/assets/styles/shared/twopanels.scss index 8c72e21..b53c1a1 100644 --- a/assets/styles/shared/twopanels.scss +++ b/assets/styles/shared/twopanels.scss @@ -6,7 +6,6 @@ box-shadow: $shadow-1; header { - height: 50px; line-height: 50px; padding: 0 14px; diff --git a/assets/templates/admin.html b/assets/templates/admin.html new file mode 100644 index 0000000..be6453b --- /dev/null +++ b/assets/templates/admin.html @@ -0,0 +1,18 @@ +<% var key, item %> +<% htmlWebpackPlugin.options.links = htmlWebpackPlugin.options.links || [] %> + + + + + River of Ebooks | admin + + <% for (item of htmlWebpackPlugin.options.links) { + if (typeof item === 'string' || item instanceof String) { item = { href: item, rel: 'stylesheet' } } %> + <%= key %>="<%= item[key] %>"<% } %> /><% + } %> + + + +
    + + diff --git a/assets/templates/index.html b/assets/templates/index.html index 15e0a22..7dae7b5 100644 --- a/assets/templates/index.html +++ b/assets/templates/index.html @@ -5,6 +5,7 @@ River of Ebooks + <% for (item of htmlWebpackPlugin.options.links) { if (typeof item === 'string' || item instanceof String) { item = { href: item, rel: 'stylesheet' } } %> <%= key %>="<%= item[key] %>"<% } %> /><% diff --git a/assets/templates/login.html b/assets/templates/login.html index a3e9c5e..a17ff3f 100644 --- a/assets/templates/login.html +++ b/assets/templates/login.html @@ -4,7 +4,8 @@ - RoE - Login + River of Ebooks - Login + <% for (item of htmlWebpackPlugin.options.links) { if (typeof item === 'string' || item instanceof String) { item = { href: item, rel: 'stylesheet' } } %> <%= key %>="<%= item[key] %>"<% } %> /><% diff --git a/config/http.js b/config/http.js index b955ef3..7228ecf 100644 --- a/config/http.js +++ b/config/http.js @@ -26,6 +26,14 @@ const publishLimiter = rateLimit({ } }) +const allowCrossDomain = function (req, res, next) { + res.header('Access-Control-Allow-Origin', 'http://localhost:8080') + res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,PATCH,DELETE') + res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization') + res.header('Access-Control-Allow-Credentials', 'true') + next() +} + module.exports.http = { /**************************************************************************** @@ -47,6 +55,7 @@ module.exports.http = { ***************************************************************************/ order: [ + 'allowCrossDomain', 'rateLimit', 'publishLimit', 'cookieParser', @@ -63,7 +72,8 @@ module.exports.http = { rateLimit: rateLimiter, publishLimit: publishLimiter, passportInit: require('passport').initialize(), - passportSession: require('passport').session() + passportSession: require('passport').session(), + allowCrossDomain: allowCrossDomain /*************************************************************************** * * diff --git a/config/models.js b/config/models.js index ec41415..bc02ebf 100644 --- a/config/models.js +++ b/config/models.js @@ -34,7 +34,7 @@ module.exports.models = { * * ***************************************************************************/ - // schema: true, + schema: true, /*************************************************************************** * * diff --git a/config/policies.js b/config/policies.js index ea17841..efb0226 100644 --- a/config/policies.js +++ b/config/policies.js @@ -42,5 +42,9 @@ module.exports.policies = { BooksController: { '*': true, publish: [ 'keyAuth' ] + }, + + AdminController: { + '*': [ 'adminAuth' ] } } diff --git a/config/routes.js b/config/routes.js index 246cc74..a04894b 100644 --- a/config/routes.js +++ b/config/routes.js @@ -34,6 +34,12 @@ module.exports.routes = { // figure out why proper clientside routing breaks the backend session 'GET /account': 'TargetController.show', 'GET /targets': 'TargetController.show', + 'GET /keys': 'TargetController.show', + 'GET /admin': 'AdminController.show', + 'GET /admin/*': { + action: 'admin/show', + skipAssets: true + }, /*************************************************************************** * * @@ -78,7 +84,10 @@ module.exports.routes = { 'POST /api/keys': 'PublishKeyController.create', 'GET /api/keys': 'PublishKeyController.list', 'PATCH /api/keys/:id': 'PublishKeyController.refresh', - 'DELETE /api/keys/:id': 'PublishKeyController.delete' + 'DELETE /api/keys/:id': 'PublishKeyController.delete', + + 'GET /admin/api/users': 'AdminController.listUsers', + 'GET /admin/api/publishers': 'AdminController.listPublishers' // ╦ ╦╔═╗╔╗ ╦ ╦╔═╗╔═╗╦╔═╔═╗ // ║║║║╣ ╠╩╗╠═╣║ ║║ ║╠╩╗╚═╗ diff --git a/docs/api.md b/docs/api.md index 0820c60..d5aca4b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -4,7 +4,13 @@ ### Publishing a book ``` -POST to /api/publish containing the body: +POST to /api/publish containing headers: +{ + roe-key: , + roe-secret: +} + +and body: { title: The book's title, @@ -37,7 +43,7 @@ The server will respond with either: or ``` -400 BAD REQUEST +400 BAD REQUEST / 403 UNAUTHORIZED { "error": string, "hint": string diff --git a/migrations/20190220133443_add_srcHost_to_book.js b/migrations/20190220133443_add_srcHost_to_book.js index 77d47f6..238aa34 100644 --- a/migrations/20190220133443_add_srcHost_to_book.js +++ b/migrations/20190220133443_add_srcHost_to_book.js @@ -1,4 +1,3 @@ - exports.up = function (knex, Promise) { return Promise.all([ knex.schema.table('book', t => { diff --git a/migrations/20190224022422_add_admin_users.js b/migrations/20190224022422_add_admin_users.js new file mode 100644 index 0000000..c200cf0 --- /dev/null +++ b/migrations/20190224022422_add_admin_users.js @@ -0,0 +1,25 @@ +exports.up = function (knex, Promise) { + return Promise.all([ + knex.schema.table('user', t => { + t.boolean('admin').defaultTo(false) + }), + knex.schema.createTable('publishkey', t => { + t.increments('id').primary() + t.integer('user').notNullable().references('user.id').onDelete('CASCADE').onUpdate('CASCADE') + t.string('appid') + t.string('secret') + t.boolean('whitelisted') + t.integer('created_at') + t.integer('updated_at') + }) + ]) +} + +exports.down = function (knex, Promise) { + return Promise.all([ + knex.schema.table('user', t => { + t.dropColumns('admin') + }), + knex.schema.dropTable('publishkey') + ]) +} diff --git a/shrinkwrap.yaml b/shrinkwrap.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/views/pages/admin.ejs b/views/pages/admin.ejs new file mode 100644 index 0000000..dad9dfe --- /dev/null +++ b/views/pages/admin.ejs @@ -0,0 +1 @@ +<%- partial('../../.tmp/public/admin.html') %> diff --git a/webpack.config.js b/webpack.config.js index 90fb3ad..62f1181 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,11 +10,13 @@ module.exports = (env, argv) => { mode: mode || 'development', entry: { login: './assets/js/login.js', - index: './assets/js/index.js' + index: './assets/js/index.js', + admin: './assets/js/admin.js' }, output: { path: path.join(__dirname, '/.tmp/public'), - filename: '[name].bundle.js' + filename: '[name].bundle.js', + publicPath: '/' }, module: { rules: [ @@ -36,16 +38,22 @@ module.exports = (env, argv) => { plugins: [ new HtmlWebpackPlugin({ template: 'assets/templates/login.html', - links: mode === 'production' ? [{ rel: 'stylesheet', type: 'text/css', href: 'login.css' }] : [], + // links: mode === 'production' ? [{ rel: 'stylesheet', type: 'text/css', href: '/login.css' }] : [], filename: path.join(__dirname, '/.tmp/public/login.html'), chunks: ['login'] }), new HtmlWebpackPlugin({ template: 'assets/templates/index.html', - links: mode === 'production' ? [{ rel: 'stylesheet', type: 'text/css', href: 'index.css' }] : [], + // links: mode === 'production' ? [{ rel: 'stylesheet', type: 'text/css', href: '/index.css' }] : [], filename: path.join(__dirname, '/.tmp/public/index.html'), chunks: ['index'] }), + new HtmlWebpackPlugin({ + template: 'assets/templates/admin.html', + // links: mode === 'production' ? [{ rel: 'stylesheet', type: 'text/css', href: '/admin.css' }] : [], + filename: path.join(__dirname, '/.tmp/public/admin.html'), + chunks: ['admin'] + }), new MiniCssExtractPlugin({ filename: '[name].css' }),