Merge pull request #41 from EbookFoundation/feature/sitewide-keys
Feature/sitewide keyspull/42/head
commit
dc7b351bd8
|
@ -0,0 +1,75 @@
|
|||
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', 'name', 'whitelisted', 'verified', 'verification_key', 'created_at', 'updated_at']
|
||||
}).populate('user')
|
||||
return res.json(publishers)
|
||||
} catch (e) {
|
||||
return (new HttpError(500, e.message)).send(res)
|
||||
}
|
||||
},
|
||||
editUser: async function (req, res) {
|
||||
try {
|
||||
const id = req.param('id')
|
||||
const patchData = req.param('patch')
|
||||
const updated = await User.updateOne({ id }).set({
|
||||
...patchData
|
||||
})
|
||||
for (const key in updated) {
|
||||
if (patchData[key] === undefined && key !== 'id') delete updated[key]
|
||||
}
|
||||
return res.json(updated)
|
||||
} catch (e) {
|
||||
return (new HttpError(500, e.message)).send(res)
|
||||
}
|
||||
},
|
||||
editPublisher: async function (req, res) {
|
||||
try {
|
||||
const id = req.param('id')
|
||||
const patchData = req.param('patch')
|
||||
const updated = await PublishKey.updateOne({ id }).set({
|
||||
...patchData
|
||||
})
|
||||
for (const key in updated) {
|
||||
if (patchData[key] === undefined && key !== 'id') delete updated[key]
|
||||
}
|
||||
return res.json(updated)
|
||||
} catch (e) {
|
||||
return (new HttpError(500, e.message)).send(res)
|
||||
}
|
||||
},
|
||||
deleteUser: async function (req, res) {
|
||||
try {
|
||||
const id = req.param('id')
|
||||
await User.destroyOne({ id })
|
||||
return res.status(204).send()
|
||||
} catch (e) {
|
||||
return (new HttpError(500, e.message)).send(res)
|
||||
}
|
||||
},
|
||||
deletePublisher: async function (req, res) {
|
||||
try {
|
||||
const id = req.param('id')
|
||||
await PublishKey.destroyOne({ id })
|
||||
return res.status(204).send()
|
||||
} catch (e) {
|
||||
return (new HttpError(500, e.message)).send(res)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,19 @@
|
|||
const HttpError = require('../errors/HttpError')
|
||||
const request = require('request')
|
||||
const url = require('url')
|
||||
const uriRegex = /(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?/i
|
||||
|
||||
module.exports = {
|
||||
create: async function (req, res) {
|
||||
try {
|
||||
const name = req.param('name')
|
||||
const url = req.param('url')
|
||||
if (!name.length) throw new Error('Name cannot be blank')
|
||||
if (!url.length) throw new Error('URL cannot be blank')
|
||||
|
||||
if (!uriRegex.test(url)) throw new Error('Invalid URL')
|
||||
const created = await PublishKey.create({
|
||||
user: req.user.id,
|
||||
name,
|
||||
url
|
||||
}).fetch()
|
||||
return res.json(created)
|
||||
|
@ -17,7 +23,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)
|
||||
|
@ -40,5 +46,44 @@ module.exports = {
|
|||
} catch (e) {
|
||||
return (new HttpError(500, e.message)).send(res)
|
||||
}
|
||||
},
|
||||
verify: async function (req, res) {
|
||||
try {
|
||||
const id = req.param('id')
|
||||
const key = await PublishKey.findOne({ id })
|
||||
if (!key) throw new HttpError(404, 'Cannot find that key')
|
||||
if (key.verified) throw new HttpError(400, 'That key\'s domain has already been verified')
|
||||
|
||||
const verification = key.verification_key
|
||||
const _url = url.resolve(key.url, `${verification}.html`)
|
||||
|
||||
const { httpResp, body } = await requestAsync({
|
||||
url: _url,
|
||||
headers: { 'User-Agent': 'RoE-aggregator' }
|
||||
})
|
||||
|
||||
if (httpResp.statusCode !== 200 || body !== `${verification}.html`) throw new HttpError(404, `Could not find ${_url}`)
|
||||
|
||||
const updated = await PublishKey.updateOne({ id }).set({
|
||||
verified: true,
|
||||
verification_key: ''
|
||||
})
|
||||
return res.json(updated)
|
||||
} catch (e) {
|
||||
if (e instanceof HttpError) return e.send(res)
|
||||
else return (new HttpError(500, e.message)).send(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requestAsync (opts) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request.get(opts, function (err, httpResp, body) {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve({ httpResp, body })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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'))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -20,22 +20,25 @@ module.exports = {
|
|||
model: 'User',
|
||||
required: true
|
||||
},
|
||||
url: {
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
key: {
|
||||
type: 'string',
|
||||
required: true
|
||||
url: 'string',
|
||||
whitelisted: 'boolean',
|
||||
verified: 'boolean',
|
||||
verification_key: 'string',
|
||||
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 })
|
||||
key.verification_key = await generateToken({ bytes: 24, base: 'hex' })
|
||||
next()
|
||||
},
|
||||
beforeUpdate: async function (key, next) {
|
||||
|
|
|
@ -18,7 +18,8 @@ module.exports = {
|
|||
},
|
||||
email: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
admin: 'boolean'
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = async function (req, res, next) {
|
||||
if (process.env.NODE_ENV === 'development') return 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.' })
|
||||
}
|
|
@ -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.' })
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
* @docs :: http://sailsjs.org/#!documentation/policies
|
||||
*/
|
||||
module.exports = function (req, res, next) {
|
||||
if (req.session.authenticated) {
|
||||
return next()
|
||||
}
|
||||
if (process.env.NODE_ENV === 'development') return next()
|
||||
if (req.session.authenticated) return next()
|
||||
// res.status(403).json({ error: 'You are not permitted to perform this action.' })
|
||||
res.redirect('/login')
|
||||
}
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
'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',
|
||||
a_update_user: 'a_update_user',
|
||||
a_update_publisher: 'a_update_publisher'
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
export const patchUser = ({ id, ...data }) => async (dispatch, getState) => {
|
||||
dispatch(setWorking(true))
|
||||
try {
|
||||
const { data: user } = await Ajax.patch({
|
||||
url: getPath('/admin/api/users/' + id),
|
||||
data: {
|
||||
patch: data
|
||||
},
|
||||
noProcess: true
|
||||
})
|
||||
dispatch({
|
||||
type: ACTIONS.a_update_user,
|
||||
data: user
|
||||
})
|
||||
} catch (e) {
|
||||
dispatch({
|
||||
type: ACTIONS.error,
|
||||
data: e
|
||||
})
|
||||
} finally {
|
||||
dispatch(setWorking(false))
|
||||
}
|
||||
}
|
||||
|
||||
export const patchPublisher = ({ id, ...data }) => async (dispatch, getState) => {
|
||||
dispatch(setWorking(true))
|
||||
try {
|
||||
const { data: publisher } = await Ajax.patch({
|
||||
url: getPath('/admin/api/publishers/' + id),
|
||||
data: {
|
||||
patch: data
|
||||
},
|
||||
noProcess: true
|
||||
})
|
||||
dispatch({
|
||||
type: ACTIONS.a_update_publisher,
|
||||
data: publisher
|
||||
})
|
||||
} catch (e) {
|
||||
dispatch({
|
||||
type: ACTIONS.error,
|
||||
data: e
|
||||
})
|
||||
} finally {
|
||||
dispatch(setWorking(false))
|
||||
}
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
'use strict'
|
||||
|
||||
import Ajax from '../lib/Ajax'
|
||||
const FileSaver = require('file-saver')
|
||||
|
||||
const getPath = str => window.location.hostname === 'localhost' ? `http://localhost:3000${str}` : str
|
||||
|
||||
const ACTIONS = {
|
||||
set_working: 'set_working',
|
||||
|
@ -8,11 +11,14 @@ const ACTIONS = {
|
|||
edit_url: 'edit_url',
|
||||
delete_url: 'delete_url',
|
||||
list_url: 'list_url',
|
||||
set_editing: 'set_editing',
|
||||
set_editing_uri: 'set_editing_uri',
|
||||
set_editing_publisher: 'set_editing_publisher',
|
||||
error: 'error',
|
||||
set_user: 'set_user',
|
||||
add_publisher: 'add_publisher',
|
||||
delete_publisher: 'delete_publisher'
|
||||
delete_publisher: 'delete_publisher',
|
||||
set_publishers: 'set_publishers',
|
||||
update_publisher: 'update_publisher'
|
||||
}
|
||||
|
||||
export default ACTIONS
|
||||
|
@ -32,6 +38,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
|
||||
|
@ -42,8 +53,23 @@ export const addPublisher = url => ({
|
|||
data: url
|
||||
})
|
||||
|
||||
export const setEditing = id => ({
|
||||
type: ACTIONS.set_editing,
|
||||
export const updatePublisher = data => ({
|
||||
type: ACTIONS.update_publisher,
|
||||
data: data
|
||||
})
|
||||
|
||||
export const setEditing = () => (dispatch) => {
|
||||
dispatch(setEditingUri(null))
|
||||
dispatch(setEditingPublisher(null))
|
||||
}
|
||||
|
||||
export const setEditingUri = id => ({
|
||||
type: ACTIONS.set_editing_uri,
|
||||
data: id
|
||||
})
|
||||
|
||||
export const setEditingPublisher = id => ({
|
||||
type: ACTIONS.set_editing_publisher,
|
||||
data: id
|
||||
})
|
||||
|
||||
|
@ -60,7 +86,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 +106,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 +132,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 +149,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 +171,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 +182,7 @@ export const editUser = (user) => async (dispatch, getState) => {
|
|||
dispatch({
|
||||
type: ACTIONS.error,
|
||||
data: null
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
dispatch({
|
||||
type: ACTIONS.error,
|
||||
|
@ -162,12 +193,13 @@ export const editUser = (user) => async (dispatch, getState) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const createNewPublisher = (url) => async (dispatch, getState) => {
|
||||
export const createNewPublisher = ({ name, url }) => async (dispatch, getState) => {
|
||||
dispatch(setWorking(true))
|
||||
try {
|
||||
const { data } = await Ajax.post({
|
||||
url: '/api/keys',
|
||||
url: getPath('/api/keys'),
|
||||
data: {
|
||||
name,
|
||||
url
|
||||
}
|
||||
})
|
||||
|
@ -186,7 +218,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,
|
||||
|
@ -201,3 +233,26 @@ export const removePublisher = id => async (dispatch, getState) => {
|
|||
dispatch(setWorking(false))
|
||||
}
|
||||
}
|
||||
|
||||
export const verifyDomain = id => async (dispatch, getState) => {
|
||||
dispatch(setWorking(true))
|
||||
try {
|
||||
const { data } = await Ajax.post({
|
||||
url: getPath(`/api/keys/${id}/verify`)
|
||||
})
|
||||
dispatch(updatePublisher(data))
|
||||
dispatch(setEditingPublisher(null))
|
||||
} catch (e) {
|
||||
dispatch({
|
||||
type: ACTIONS.error,
|
||||
data: e
|
||||
})
|
||||
} finally {
|
||||
dispatch(setWorking(false))
|
||||
}
|
||||
}
|
||||
|
||||
export const saveFile = data => (dispatch) => {
|
||||
var blob = new Blob([data], { type: 'text/plain;charset=utf-8' })
|
||||
FileSaver.saveAs(blob, data)
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
@ -47,7 +49,7 @@ export const clearError = () => ({
|
|||
|
||||
export const setLoggedIn = (data) => (dispatch, getState) => {
|
||||
window.localStorage.setItem('roe-token', JSON.stringify(data))
|
||||
window.location.href = '/targets'
|
||||
window.location.href = '/keys'
|
||||
}
|
||||
|
||||
export const checkEmail = email => async (dispatch, getState) => {
|
||||
|
@ -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
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
'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, patchUser, patchPublisher } from './actions/admin'
|
||||
import Util from './lib/Util'
|
||||
import Icon from './components/Icon'
|
||||
|
||||
import '../styles/admin.scss'
|
||||
import './containers/listitem.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)
|
||||
|
||||
/*
|
||||
this.state.user = {
|
||||
...this.state.user,
|
||||
'created_at': 1551151466802,
|
||||
'updated_at': 1551151520134,
|
||||
'id': 1,
|
||||
'email': 'admin@tkluge.net',
|
||||
'admin': true
|
||||
}
|
||||
*/
|
||||
}
|
||||
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 (
|
||||
<li className='uri-list-item flex-container' key={`is-admin-${user.id}`}>
|
||||
<span className='flex'>{user.email}</span>
|
||||
<span className='flex'>
|
||||
<label for={`is-admin-${user.id}`} className='cb-label'>Admin?</label>
|
||||
<input className='checkbox' type='checkbox' checked={user.admin} onChange={() => this.dispatch(patchUser({ id: user.id, admin: !user.admin }))} id={`is-admin-${user.id}`} />
|
||||
<label htmlFor={`is-admin-${user.id}`} />
|
||||
</span>
|
||||
<div className='stack flex flex-container flex-vertical'>
|
||||
<span className='flex'><span className='key'>Created at:</span><span className='value'>{new Date(user.created_at).toLocaleString()}</span></span>
|
||||
<span className='flex'><span className='key'>Updated at:</span><span className='value'>{new Date(user.updated_at).toLocaleString()}</span></span>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
getRegisteredPublishers () {
|
||||
return this.state.publishers.map(pub => {
|
||||
return (
|
||||
<li className='uri-list-item flex-container flex-vertical' key={`is-whitelisted-${pub.id}`}>
|
||||
<header><h3>{pub.name}</h3></header>
|
||||
<div className='flex flex-container'>
|
||||
<div className='flex flex-container flex-vertical key-value'>
|
||||
<span className='flex'><span className='key'>Owner:</span><span className='value'>{pub.user.email}</span></span>
|
||||
<span className='flex'><span className='key'>App ID:</span><span className='value'>{pub.appid}</span></span>
|
||||
<span className='flex contains-icon'>
|
||||
<span className='key'>App domain:</span>
|
||||
<span className='value'>{pub.url}</span>
|
||||
<Icon icon={pub.verified ? 'shield-check' : 'alert-circle'} className={pub.verified ? 'verified' : 'unverified'} />
|
||||
</span>
|
||||
</div>
|
||||
<span className='flex'>
|
||||
<label htmlFor={`is-whitelisted-${pub.id}`} className='cb-label'>Whitelisted?</label>
|
||||
<input className='checkbox' type='checkbox' checked={pub.whitelisted} onChange={() => this.dispatch(patchPublisher({ id: pub.id, whitelisted: !pub.whitelisted }))} id={`is-whitelisted-${pub.id}`} />
|
||||
<label htmlFor={`is-whitelisted-${pub.id}`} />
|
||||
</span>
|
||||
<div className='stack flex flex-container flex-vertical'>
|
||||
<span className='flex'><span className='key'>Created at:</span><span className='value'>{new Date(pub.created_at).toLocaleString()}</span></span>
|
||||
<span className='flex'><span className='key'>Updated at:</span><span className='value'>{new Date(pub.updated_at).toLocaleString()}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<Router basename='/admin'>
|
||||
<div className='root-container flex-container admin-container'>
|
||||
<aside className='nav nav-left'>
|
||||
<header>
|
||||
<h1>RoE Admin</h1>
|
||||
<h2 className='flex-container'>
|
||||
<span className='flex'>{this.state.user.email}</span>
|
||||
<a href='/logout'>Log out</a>
|
||||
</h2>
|
||||
</header>
|
||||
<ul>
|
||||
<li><NavLink to='/users'>Users</NavLink></li>
|
||||
<li><NavLink to='/publishers'>Publishers</NavLink></li>
|
||||
<li><a href='/keys'>Exit admin</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
<section className={'content flex' + (this.state.working ? ' working' : '')}>
|
||||
<Progress bound />
|
||||
{this.state.error && <div className='error-box'>{this.state.error}</div>}
|
||||
<Switch>
|
||||
<Route path='/users' exact children={props => (
|
||||
<div>
|
||||
<header className='flex-container'>
|
||||
<div className='flex'>
|
||||
<h1>Site users</h1>
|
||||
<h2>Registered users on RoE</h2>
|
||||
</div>
|
||||
</header>
|
||||
<ul className='list'>
|
||||
{this.getRegisteredUsers()}
|
||||
</ul>
|
||||
</div>
|
||||
)} />
|
||||
|
||||
<Route path='/publishers' exact children={props => (
|
||||
<div>
|
||||
<header className='flex-container'>
|
||||
<div className='flex'>
|
||||
<h1>Publishers</h1>
|
||||
<h2>Whitelist sites who can publish books</h2>
|
||||
</div>
|
||||
</header>
|
||||
<ul className='list'>
|
||||
{this.getRegisteredPublishers()}
|
||||
</ul>
|
||||
</div>
|
||||
)} />
|
||||
|
||||
<Route path='/' render={() => <Redirect to='/users' />} />
|
||||
</Switch>
|
||||
</section>
|
||||
</div>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'))
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react'
|
||||
import './icon.scss'
|
||||
|
||||
function getSVG (icon) {
|
||||
switch (icon) {
|
||||
case 'delete': return '<path d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" />'
|
||||
case 'add': return '<path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />'
|
||||
case 'pound': return '<path d="M5.41,21L6.12,17H2.12L2.47,15H6.47L7.53,9H3.53L3.88,7H7.88L8.59,3H10.59L9.88,7H15.88L16.59,3H18.59L17.88,7H21.88L21.53,9H17.53L16.47,15H20.47L20.12,17H16.12L15.41,21H13.41L14.12,17H8.12L7.41,21H5.41M9.53,9L8.47,15H14.47L15.53,9H9.53Z" />'
|
||||
case 'filter-variant': return '<path d="M6,13H18V11H6M3,6V8H21V6M10,18H14V16H10V18Z" />'
|
||||
case 'close': return '<path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />'
|
||||
case 'check': return '<path d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />'
|
||||
case 'chevron-down': return '<path d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" />'
|
||||
case 'account-network': return '<path d="M13,17V19H14A1,1 0 0,1 15,20H22V22H15A1,1 0 0,1 14,23H10A1,1 0 0,1 9,22H2V20H9A1,1 0 0,1 10,19H11V17H5V15.5C5,13.57 8.13,12 12,12C15.87,12 19,13.57 19,15.5V17H13M12,3A3.5,3.5 0 0,1 15.5,6.5A3.5,3.5 0 0,1 12,10A3.5,3.5 0 0,1 8.5,6.5A3.5,3.5 0 0,1 12,3Z" />'
|
||||
case 'logout': return '<path d="M14.08,15.59L16.67,13H7V11H16.67L14.08,8.41L15.5,7L20.5,12L15.5,17L14.08,15.59M19,3A2,2 0 0,1 21,5V9.67L19,7.67V5H5V19H19V16.33L21,14.33V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H19Z" />'
|
||||
case 'square-edit-outline': return '<path d="M5,3C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19H5V5H12V3H5M17.78,4C17.61,4 17.43,4.07 17.3,4.2L16.08,5.41L18.58,7.91L19.8,6.7C20.06,6.44 20.06,6 19.8,5.75L18.25,4.2C18.12,4.07 17.95,4 17.78,4M15.37,6.12L8,13.5V16H10.5L17.87,8.62L15.37,6.12Z" />'
|
||||
case 'eye': return '<path d="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z" />'
|
||||
case 'eye-close': return ' <path d="M11.83,9L15,12.16C15,12.11 15,12.05 15,12A3,3 0 0,0 12,9C11.94,9 11.89,9 11.83,9M7.53,9.8L9.08,11.35C9.03,11.56 9,11.77 9,12A3,3 0 0,0 12,15C12.22,15 12.44,14.97 12.65,14.92L14.2,16.47C13.53,16.8 12.79,17 12,17A5,5 0 0,1 7,12C7,11.21 7.2,10.47 7.53,9.8M2,4.27L4.28,6.55L4.73,7C3.08,8.3 1.78,10 1,12C2.73,16.39 7,19.5 12,19.5C13.55,19.5 15.03,19.2 16.38,18.66L16.81,19.08L19.73,22L21,20.73L3.27,3M12,7A5,5 0 0,1 17,12C17,12.64 16.87,13.26 16.64,13.82L19.57,16.75C21.07,15.5 22.27,13.86 23,12C21.27,7.61 17,4.5 12,4.5C10.6,4.5 9.26,4.75 8,5.2L10.17,7.35C10.74,7.13 11.35,7 12,7Z" />'
|
||||
case 'shield-check': return '<path d="M10,17L6,13L7.41,11.59L10,14.17L16.59,7.58L18,9M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1Z" />'
|
||||
case 'alert-circle': return '<path d="M13,13H11V7H13M13,17H11V15H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />'
|
||||
default: return icon || 'missing icon prop'
|
||||
}
|
||||
}
|
||||
|
||||
function bsvg (icon) {
|
||||
return `<svg viewBox="0 0 24 24">${getSVG(icon)}</svg>`
|
||||
}
|
||||
|
||||
const Icon = props => {
|
||||
return (
|
||||
<span className={'icon' + (props.className ? ' ' + props.className : '')} dangerouslySetInnerHTML={{ __html: bsvg(props.icon) }} />
|
||||
)
|
||||
}
|
||||
|
||||
export default Icon
|
|
@ -1,18 +1,14 @@
|
|||
'use strict'
|
||||
|
||||
import React from 'react'
|
||||
import '../../styles/shared/iconbutton.scss'
|
||||
|
||||
function getSVG (icon) {
|
||||
switch (icon) {
|
||||
case 'delete': return '<svg viewBox="0 0 24 24"><path d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" /></svg>'
|
||||
default: return icon || 'missing icon prop'
|
||||
}
|
||||
}
|
||||
import Icon from './Icon'
|
||||
import './IconButton.scss'
|
||||
|
||||
const IconButton = props => {
|
||||
return (
|
||||
<button className='button icon' onClick={props.onClick} dangerouslySetInnerHTML={{ __html: getSVG(props.icon) }} />
|
||||
<button className={'button icon' + (props.className ? ' ' + props.className : '')} onClick={props.onClick}>
|
||||
<Icon icon={props.icon} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import React from 'react'
|
||||
|
||||
import '../../styles/shared/underlineinput.scss'
|
||||
import './underlineinput.scss'
|
||||
|
||||
const UnderlineInput = props => (
|
||||
<div className={'underlined-input ' + (props.className ? props.className : '')}>
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
@import '../../styles/lib/vars';
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
width: 40px;
|
||||
padding: 5px;
|
||||
background: transparent;
|
||||
border-radius: 50%;
|
||||
|
||||
svg {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
|
||||
path {
|
||||
fill: $white-2;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
@import '../lib/vars';
|
||||
@import '../../styles/lib/vars';
|
||||
|
||||
.button.icon {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
width: 40px;
|
||||
padding: 5px;
|
||||
// padding: 5px;
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
@ -17,7 +18,14 @@
|
|||
background: $black-4;
|
||||
box-shadow: $shadow-1;
|
||||
}
|
||||
path {
|
||||
fill: $black-2;
|
||||
.icon {
|
||||
padding: 0;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
width: 30px;
|
||||
|
||||
path {
|
||||
fill: $black-2;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
@import '../lib/vars';
|
||||
@import '../../styles/lib/vars';
|
||||
|
||||
.underlined-input,
|
||||
.underlined-input-readonly {
|
||||
|
@ -93,6 +93,12 @@
|
|||
|
||||
& + .underlined-input,
|
||||
& + .underlined-input-readonly {
|
||||
margin-top: 8px
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
& + .underlined-input.stack-h,
|
||||
& + .underlined-input-readonly.stack-h {
|
||||
margin-left: 14px;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
'use strict'
|
||||
|
||||
import React from 'react'
|
||||
import '../../styles/shared/carousel.scss'
|
||||
import './carousel.scss'
|
||||
|
||||
class Carousel extends React.Component {
|
||||
constructor () {
|
||||
|
@ -40,7 +40,7 @@ const CarouselItem = props => (
|
|||
{props.inputs}
|
||||
<span className='carousel-error'>{props.error}</span>
|
||||
<div className='button-row'>
|
||||
<a href='#' onClick={props.onSmallButtonClick}>{props.smallButton}</a>
|
||||
<a href='#' onClick={e => handleClick(e, props.onSmallButtonClick)}>{props.smallButton}</a>
|
||||
<button className='btn btn-primary' type='submit' >
|
||||
{props.button}
|
||||
</button>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react'
|
||||
import IconButton from '../components/IconButton'
|
||||
|
||||
export default class ConfirmIconButton extends React.Component {
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {
|
||||
confirmed: false
|
||||
}
|
||||
this.onClick = this.onClick.bind(this)
|
||||
}
|
||||
onClick (e) {
|
||||
if (this.state.confirmed) {
|
||||
this.props.onClick(e)
|
||||
} else {
|
||||
this.setState({ confirmed: true })
|
||||
}
|
||||
}
|
||||
render () {
|
||||
const { onClick, icon, ...rest } = this.props
|
||||
return (
|
||||
<IconButton
|
||||
{...rest}
|
||||
icon={this.state.confirmed ? 'check' : icon}
|
||||
className={this.state.confirmed ? 'confirm' : ''}
|
||||
onClick={this.onClick} />
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,29 +1,100 @@
|
|||
import React from 'react'
|
||||
import ConfirmIconButton from './ConfirmIconButton'
|
||||
import IconButton from '../components/IconButton'
|
||||
import UnderlineInput from '../components/UnderlineInput'
|
||||
import { removePublisher } from '../actions'
|
||||
import '../../styles/shared/listitem.scss'
|
||||
|
||||
const uriRegex = /(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i
|
||||
import Icon from '../components/Icon'
|
||||
import { removePublisher, setEditingPublisher, saveFile, verifyDomain } from '../actions'
|
||||
import './listitem.scss'
|
||||
|
||||
class PublisherListItem extends React.Component {
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {
|
||||
revealed: false
|
||||
}
|
||||
this.toggleReveal = this.toggleReveal.bind(this)
|
||||
this.getView = this.getView.bind(this)
|
||||
this.getEditing = this.getEditing.bind(this)
|
||||
this.cancelEvent = this.cancelEvent.bind(this)
|
||||
}
|
||||
toggleReveal (e) {
|
||||
e.stopPropagation()
|
||||
this.setState({
|
||||
revealed: !this.state.revealed
|
||||
})
|
||||
}
|
||||
cancelEvent (e, id) {
|
||||
e.stopPropagation()
|
||||
if (id === false) return
|
||||
this.props.dispatch(setEditingPublisher(id))
|
||||
}
|
||||
getView () {
|
||||
return (
|
||||
<li key={this.props.item.appid} className={'uri-list-item publisher-list-item flex-container flex-vertical' + (this.props.item.whitelisted ? ' whitelisted' : '')}>
|
||||
<header className='site-name flex-container'>
|
||||
<h3 className='flex'>{`${this.props.item.name}${this.props.item.whitelisted ? '' : ' (awaiting approval)'}`}</h3>
|
||||
<ConfirmIconButton icon='delete' onClick={() => this.props.dispatch(removePublisher(this.props.item.id))} />
|
||||
</header>
|
||||
<div className='flex flex-container'>
|
||||
<div className='col flex flex-container flex-vertical'>
|
||||
<div className='stack flex-container flex-vertical'>
|
||||
<span className='label'>AppID</span>
|
||||
<input className='value' value={this.props.item.appid} readOnly />
|
||||
</div>
|
||||
<div className='stack flex-container flex-vertical'>
|
||||
<span className='label'>Secret</span>
|
||||
<div className='flex-container'>
|
||||
<input className='value flex' type={this.state.revealed ? 'text' : 'password'} value={this.props.item.secret} readOnly />
|
||||
<IconButton className='tiny' onClick={this.toggleReveal} icon={this.state.revealed ? 'eye-close' : 'eye'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='col flex flex-container flex-vertical'>
|
||||
<div className='stack flex-container flex-vertical'>
|
||||
<span className='label'>Publisher domain</span>
|
||||
<input className='value' value={this.props.item.url} readOnly />
|
||||
</div>
|
||||
<div className='stack flex-container flex-vertical'>
|
||||
<span className='label'>Domain verification</span>
|
||||
<div className='verification'>
|
||||
<Icon icon={this.props.item.verified ? 'shield-check' : 'alert-circle'} className={this.props.item.verified ? 'verified' : 'unverified'} />
|
||||
{this.props.item.verified && <span className='verified'>Ownership verified</span>}
|
||||
{!this.props.item.verified && <button className='btn btn-clear' onClick={(e) => this.cancelEvent(e, this.props.item.id)}>Verify domain ownership</button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
getEditing () {
|
||||
return (
|
||||
<li key={this.props.item.appid} className='uri-list-item publisher-list-item flex-container flex-vertical editing' onClick={(e) => this.cancelEvent(e, false)}>
|
||||
<header className='site-name flex-container'>
|
||||
<h3 className='flex'>{this.props.item.name}</h3>
|
||||
<ConfirmIconButton icon='delete' onClick={() => this.props.dispatch(removePublisher(this.props.item.id))} />
|
||||
</header>
|
||||
<div className='flex flex-container'>
|
||||
<div className='col flex flex-container flex-vertical'>
|
||||
<p>
|
||||
Download <span className='name'>{this.props.item.verification_key}.html</span> and upload it to the root directory of your webserver. Then, click <strong>VERIFY</strong> to verify that you own and control <span className='name'>{this.props.item.url}</span>.
|
||||
</p>
|
||||
</div>
|
||||
<div className='col flex flex-container flex-vertical'>
|
||||
<div className='flex' />
|
||||
<button className='btn btn-clear' onClick={() => this.props.dispatch(saveFile(`${this.props.item.verification_key}.html`))}>Download file</button>
|
||||
<div className='flex' />
|
||||
<div className='flex-container buttons'>
|
||||
<span className='cancel flex' onClick={(e) => this.cancelEvent(e, null)}>Cancel</span>
|
||||
<button className='flex btn' onClick={(e) => this.props.dispatch(verifyDomain(this.props.item.id))}>Verify</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<li className='uri-list-item flex-container'>
|
||||
<div className='stack flex flex-container flex-vertical'>
|
||||
<span className='label'>Website URL</span>
|
||||
<span className='value'>{this.props.item.url}</span>
|
||||
</div>
|
||||
<div className='stack flex flex-container flex-vertical'>
|
||||
<span className='label'>Key</span>
|
||||
<input className='value' value={this.props.item.key} readOnly={true} />
|
||||
</div>
|
||||
<div className='stack flex flex-container flex-vertical'>
|
||||
<span className='label'>Secret</span>
|
||||
<input className='value' value={this.props.item.secret} readOnly={true} />
|
||||
</div>
|
||||
<IconButton icon='delete' onClick={() => this.props.dispatch(removePublisher(this.props.item.id))} />
|
||||
</li>
|
||||
this.props.editing ? this.getEditing() : this.getView()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
import React from 'react'
|
||||
import IconButton from '../components/IconButton'
|
||||
import UnderlineInput from '../components/UnderlineInput'
|
||||
import '../../styles/shared/listitem.scss'
|
||||
import { changeUrlField, setUrl, removeUrl, setEditing } from '../actions'
|
||||
import './listitem.scss'
|
||||
import { changeUrlField, setUrl, removeUrl, setEditingUri } from '../actions'
|
||||
|
||||
const uriRegex = /(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i
|
||||
const isbnRegex = /^(97(8|9))?\d{9}(\d|X)$/
|
||||
|
@ -19,7 +19,7 @@ class UriListItem extends React.Component {
|
|||
cancelEvent (e, id) {
|
||||
e.stopPropagation()
|
||||
if (id === false) return
|
||||
this.props.dispatch(setEditing(id))
|
||||
this.props.dispatch(setEditingUri(id))
|
||||
}
|
||||
getView () {
|
||||
return (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import '../lib/vars';
|
||||
@import '../../styles/lib/vars';
|
||||
|
||||
.carousel-container {
|
||||
position: relative;
|
|
@ -0,0 +1,184 @@
|
|||
@import '../../styles/lib/vars';
|
||||
|
||||
.uri-list-item {
|
||||
position: relative;
|
||||
min-height: 60px;
|
||||
line-height: 60px;
|
||||
max-width: 100%;
|
||||
background: white;
|
||||
box-shadow: $shadow-0;
|
||||
padding: 0 0 0 14px;
|
||||
cursor: pointer;
|
||||
transition: margin 0.15s $transition,
|
||||
padding 0.15s $transition,
|
||||
border-radius 0.15s $transition;
|
||||
|
||||
.button.icon {
|
||||
margin: 10px 5px 10px 5px;
|
||||
}
|
||||
|
||||
&.editing {
|
||||
margin: 24px 8px;
|
||||
line-height: initial;
|
||||
padding: 10px 0 10px 14px;
|
||||
border-radius: 3px;
|
||||
cursor: default;
|
||||
|
||||
header {
|
||||
h3 {
|
||||
display: inline-block;
|
||||
margin: 10px 0 0 0;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.settings {
|
||||
padding: 0 14px 0 0;
|
||||
}
|
||||
h4 {
|
||||
margin: 30px 0 0 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.stack {
|
||||
line-height: 25px;
|
||||
padding: 5px 0;
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
@include ellip;
|
||||
}
|
||||
span.label {
|
||||
font-size: 0.75rem;
|
||||
color: $black-2;
|
||||
}
|
||||
span.value {
|
||||
margin-top: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
&.publisher-list-item {
|
||||
cursor: default;
|
||||
min-height: 120px;
|
||||
padding: 10px 0 10px 14px;
|
||||
|
||||
.button.icon {
|
||||
margin: 0;
|
||||
|
||||
&.tiny {
|
||||
padding: 0;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
}
|
||||
.site-name {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
line-height: 40px;
|
||||
font-weight: normal;
|
||||
cursor: default;
|
||||
}
|
||||
.button.icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.stack {
|
||||
.label {
|
||||
display: inline-block;
|
||||
margin-bottom: -7px;
|
||||
}
|
||||
input {
|
||||
display: inline-block;
|
||||
line-height: 30px;
|
||||
background: transparent;
|
||||
}
|
||||
input.value {
|
||||
outline: none;
|
||||
border: none;
|
||||
font-family: monospace;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.col {
|
||||
margin-right: 10px;
|
||||
|
||||
& + .col {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.verification {
|
||||
line-height: 40px;
|
||||
height: 40px;
|
||||
|
||||
.icon {
|
||||
padding: 0;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
|
||||
&.verified {
|
||||
path {
|
||||
fill: $green;
|
||||
}
|
||||
}
|
||||
&.unverified {
|
||||
path {
|
||||
fill: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
vertical-align: middle;
|
||||
border: 1px solid $black-4;
|
||||
line-height: 30px;
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
span {
|
||||
color: $green;
|
||||
}
|
||||
}
|
||||
&.editing {
|
||||
.buttons {
|
||||
margin-top: 10px;
|
||||
|
||||
span.cancel {
|
||||
display: inline-block;
|
||||
line-height: 36px;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
color: $black-2;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
p {
|
||||
.name {
|
||||
color: $accent-3;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not(.whitelisted) {
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,11 +6,14 @@ 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'
|
||||
|
||||
import '../styles/index.scss'
|
||||
|
||||
const uriRegex = /^(https?:\/\/)(.+\.)*(.+\.).{1,}(:\d+)?/i
|
||||
|
||||
class App extends React.Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
@ -24,8 +27,9 @@ class App extends React.Component {
|
|||
},
|
||||
urls: [],
|
||||
publishers: [],
|
||||
newPublisherUrl: '',
|
||||
newPublisher: { name: '', url: '' },
|
||||
editingUrl: null,
|
||||
editingPublisher: 1,
|
||||
working: false
|
||||
}
|
||||
|
||||
|
@ -34,7 +38,7 @@ class App extends React.Component {
|
|||
this.setUserValue = this.setUserValue.bind(this)
|
||||
this.saveUser = this.saveUser.bind(this)
|
||||
this.getRegisteredPublishers = this.getRegisteredPublishers.bind(this)
|
||||
this.setPublisherUrl = this.setPublisherUrl.bind(this)
|
||||
this.setPublisherValue = this.setPublisherValue.bind(this)
|
||||
}
|
||||
dispatch (action) {
|
||||
if (!action) throw new Error('dispatch: missing action')
|
||||
|
@ -51,18 +55,12 @@ class App extends React.Component {
|
|||
componentDidMount () {
|
||||
this.dispatch(fetchData())
|
||||
}
|
||||
setPublisherUrl (e) {
|
||||
setPublisherValue (which, e) {
|
||||
this.setState({
|
||||
newPublisherUrl: e.target.value
|
||||
})
|
||||
}
|
||||
getRegisteredUris () {
|
||||
return this.state.urls.map((item, i) => {
|
||||
return (<UriListItem
|
||||
key={i}
|
||||
dispatch={this.dispatch}
|
||||
item={item}
|
||||
editing={this.state.editingUrl === item.id} />)
|
||||
newPublisher: {
|
||||
...this.state.newPublisher,
|
||||
[which]: e.target.value
|
||||
}
|
||||
})
|
||||
}
|
||||
setUserValue (which, e) {
|
||||
|
@ -84,10 +82,20 @@ class App extends React.Component {
|
|||
}
|
||||
})
|
||||
}
|
||||
getRegisteredUris () {
|
||||
return this.state.urls.map((item, i) => {
|
||||
return (<UriListItem
|
||||
key={i}
|
||||
dispatch={this.dispatch}
|
||||
item={item}
|
||||
editing={this.state.editingUrl === item.id} />)
|
||||
})
|
||||
}
|
||||
getRegisteredPublishers () {
|
||||
return this.state.publishers.map((item, i) => {
|
||||
return (<PublisherListItem
|
||||
key={i}
|
||||
editing={this.state.editingPublisher === item.id}
|
||||
dispatch={this.dispatch}
|
||||
item={item} />)
|
||||
})
|
||||
|
@ -105,9 +113,12 @@ class App extends React.Component {
|
|||
</h2>
|
||||
</header>
|
||||
<ul>
|
||||
<li><NavLink to='/keys'>Publishing keys</NavLink></li>
|
||||
<li><NavLink to='/targets'>Push URIs</NavLink></li>
|
||||
<li><NavLink to='/account'>My account</NavLink></li>
|
||||
<li><NavLink to='/keys'>Publishing keys</NavLink></li>
|
||||
{(this.state.user.id === 1 || this.state.user.admin) &&
|
||||
<li><a href='/admin'>Admin</a></li>
|
||||
}
|
||||
</ul>
|
||||
</aside>
|
||||
<section className={'content flex' + (this.state.working ? ' working' : '')}>
|
||||
|
@ -138,11 +149,18 @@ class App extends React.Component {
|
|||
</header>
|
||||
<div className='creator flex-container'>
|
||||
<UnderlineInput
|
||||
className='flex'
|
||||
placeholder='Site URL'
|
||||
value={this.state.newPublisherUrl}
|
||||
onChange={this.setPublisherUrl} />
|
||||
<button className='btn' onClick={() => this.dispatch(createNewPublisher(this.state.newPublisherUrl))}>Create keys</button>
|
||||
className='flex stack-h'
|
||||
placeholder='Website name'
|
||||
value={this.state.newPublisher.name}
|
||||
onChange={e => this.setPublisherValue('name', e)} />
|
||||
<UnderlineInput
|
||||
className='flex stack-h'
|
||||
type='text'
|
||||
placeholder='Website domain (starts with http or https)'
|
||||
value={this.state.newPublisher.url}
|
||||
pattern={uriRegex}
|
||||
onChange={(e) => this.setPublisherValue('url', e)} />
|
||||
<button className='btn' onClick={() => this.dispatch(createNewPublisher(this.state.newPublisher))}>Create keys</button>
|
||||
</div>
|
||||
<ul className='list'>
|
||||
{this.getRegisteredPublishers()}
|
||||
|
@ -181,7 +199,7 @@ class App extends React.Component {
|
|||
</div>
|
||||
)} />
|
||||
|
||||
<Route path='/' render={() => <Redirect to='/targets' />} />
|
||||
<Route path='/' render={() => <Redirect to='/keys' />} />
|
||||
</Switch>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
@ -59,12 +59,12 @@ export default class Ajax {
|
|||
|
||||
var fd = null
|
||||
var qs = ''
|
||||
if (opts.data && opts.method.toLowerCase() !== 'get') {
|
||||
if (!opts.noProcess && opts.data && opts.method.toLowerCase() !== 'get') {
|
||||
fd = new FormData()
|
||||
for (let key in opts.data) {
|
||||
fd.append(key, opts.data[key])
|
||||
}
|
||||
} else if (opts.data) {
|
||||
} else if (!opts.noProcess && opts.data) {
|
||||
qs += '?'
|
||||
let params = []
|
||||
for (let key in opts.data) {
|
||||
|
@ -73,6 +73,14 @@ export default class Ajax {
|
|||
qs += params.join('&')
|
||||
}
|
||||
|
||||
if (opts.noProcess) {
|
||||
opts.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...opts.headers
|
||||
}
|
||||
try { fd = JSON.stringify(opts.data) } catch (e) { console.warn(e) }
|
||||
}
|
||||
|
||||
xhr.onload = () => {
|
||||
if (!('' + xhr.status).startsWith('2')) { return xhr.onerror() }
|
||||
var data = xhr.response
|
||||
|
|
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
'use strict'
|
||||
|
||||
import Actions from '../actions/admin'
|
||||
|
||||
const reducer = (state = {}, action) => {
|
||||
const { type, data } = action
|
||||
let ind
|
||||
switch (type) {
|
||||
case Actions.set_working:
|
||||
return {
|
||||
working: data
|
||||
}
|
||||
case Actions.set_admin_data:
|
||||
return {
|
||||
user: data.user,
|
||||
users: data.users,
|
||||
publishers: data.publishers
|
||||
}
|
||||
case Actions.a_update_user:
|
||||
const modifiedUsers = [ ...state.users ]
|
||||
ind = modifiedUsers.findIndex(x => x.id === data.id)
|
||||
modifiedUsers[ind] = { ...modifiedUsers[ind], ...data }
|
||||
return {
|
||||
users: modifiedUsers
|
||||
}
|
||||
case Actions.a_update_publisher:
|
||||
const modifiedPublishers = [ ...state.publishers ]
|
||||
ind = modifiedPublishers.findIndex(x => x.id === data.id)
|
||||
modifiedPublishers[ind] = { ...modifiedPublishers[ind], ...data }
|
||||
return {
|
||||
publishers: modifiedPublishers
|
||||
}
|
||||
default: return {}
|
||||
}
|
||||
}
|
||||
|
||||
export default reducer
|
|
@ -4,7 +4,7 @@ import Actions from '../actions'
|
|||
|
||||
const reducer = (state = {}, action) => {
|
||||
const { type, data } = action
|
||||
let urls
|
||||
let urls, ind
|
||||
switch (type) {
|
||||
case Actions.set_working:
|
||||
return {
|
||||
|
@ -17,6 +17,10 @@ const reducer = (state = {}, action) => {
|
|||
...data
|
||||
}
|
||||
}
|
||||
case Actions.set_publishers:
|
||||
return {
|
||||
publishers: data
|
||||
}
|
||||
case Actions.list_url:
|
||||
return {
|
||||
urls: data || []
|
||||
|
@ -37,13 +41,17 @@ const reducer = (state = {}, action) => {
|
|||
return {
|
||||
urls: urls
|
||||
}
|
||||
case Actions.set_editing:
|
||||
case Actions.set_editing_uri:
|
||||
return {
|
||||
editingUrl: data
|
||||
}
|
||||
case Actions.set_editing_publisher:
|
||||
return {
|
||||
editingPublisher: data
|
||||
}
|
||||
case Actions.error:
|
||||
return {
|
||||
error: (data || {}).message || ''
|
||||
error: (data || {}).data ? (data || {}).data : (data || {}).message
|
||||
}
|
||||
case Actions.add_publisher:
|
||||
return {
|
||||
|
@ -55,6 +63,14 @@ const reducer = (state = {}, action) => {
|
|||
publishers: state.publishers.filter(x => x.id !== data),
|
||||
error: ''
|
||||
}
|
||||
case Actions.update_publisher:
|
||||
const modifiedPublishers = [ ...state.publishers ]
|
||||
ind = modifiedPublishers.findIndex(x => x.id === data.id)
|
||||
modifiedPublishers[ind] = { ...modifiedPublishers[ind], ...data }
|
||||
return {
|
||||
publishers: modifiedPublishers,
|
||||
error: ''
|
||||
}
|
||||
default: return {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
@import 'index';
|
||||
|
||||
.admin-container {
|
||||
.list {
|
||||
li {
|
||||
padding: 5px 0;
|
||||
min-height: 50px;
|
||||
line-height: 50px;
|
||||
padding: 0 14px;
|
||||
|
||||
header {
|
||||
h3 {
|
||||
font-weight: normal;
|
||||
line-height: 30px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.stack {
|
||||
line-height: 20px;
|
||||
}
|
||||
.cb-label {
|
||||
display: inline-block;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin-right: 14px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
.key-value {
|
||||
span {
|
||||
line-height: 30px;
|
||||
|
||||
&.contains-icon {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
padding: 0;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
svg {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
&.verified {
|
||||
path {
|
||||
fill: $green;
|
||||
}
|
||||
}
|
||||
&.unverified {
|
||||
path {
|
||||
fill: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.key {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.value {
|
||||
font-family: monospace;
|
||||
color: $black-3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,9 +4,10 @@
|
|||
.content {
|
||||
padding: 14px 0 42px 0;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
|
||||
.error-box {
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
line-height: 30px;
|
||||
background: $red;
|
||||
color: white;
|
||||
|
@ -28,11 +29,11 @@
|
|||
margin-top: 4px;
|
||||
color: $text-dark-2;
|
||||
text-shadow: 1px 1px 2px $black-4;
|
||||
|
||||
}
|
||||
.creator {
|
||||
padding: 0 14px;
|
||||
line-height: 60px;
|
||||
max-width: 500px;
|
||||
// max-width: 500px;
|
||||
|
||||
.btn {
|
||||
margin: 12px 0 12px 12px;
|
||||
|
|
|
@ -35,3 +35,32 @@ $accent-2: #18517c;
|
|||
$accent-3: #4f91b8;
|
||||
|
||||
$red: #FE4C52;
|
||||
$green: #4FE070;
|
||||
|
||||
$small-break: 640px;
|
||||
$medium-break: 800px;
|
||||
$large-break: 1200px;
|
||||
|
||||
$breakpoints: (
|
||||
'small': ( max-width: $small-break ),
|
||||
'medium': ( max-width: $medium-break ),
|
||||
'large': ( max-width: $large-break )
|
||||
) !default;
|
||||
|
||||
@mixin break($breakpoint) {
|
||||
@if map-has-key($breakpoints, $breakpoint) {
|
||||
@media #{inspect(map-get($breakpoints, $breakpoint))} {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@else {
|
||||
@warn "No value could be retrieved from `#{$breakpoint}`. "
|
||||
+ "Available breakpoints are: #{map-keys($breakpoints)}.";
|
||||
}
|
||||
}
|
||||
|
||||
@mixin ellip() {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
@import '../lib/vars';
|
||||
|
||||
.uri-list-item {
|
||||
min-height: 60px;
|
||||
line-height: 60px;
|
||||
background: white;
|
||||
box-shadow: $shadow-0;
|
||||
padding: 0 0 0 14px;
|
||||
cursor: pointer;
|
||||
transition: margin 0.15s $transition,
|
||||
padding 0.15s $transition,
|
||||
border-radius 0.15s $transition;
|
||||
|
||||
.button.icon {
|
||||
margin: 7px 5px 3px 5px;
|
||||
}
|
||||
|
||||
&.editing {
|
||||
margin: 24px 8px;
|
||||
line-height: initial;
|
||||
padding: 10px 0 10px 14px;
|
||||
border-radius: 3px;
|
||||
cursor: default;
|
||||
|
||||
header {
|
||||
h3 {
|
||||
display: inline-block;
|
||||
margin: 10px 0 0 0;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.settings {
|
||||
padding: 0 14px 0 0;
|
||||
}
|
||||
h4 {
|
||||
margin: 30px 0 0 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.stack {
|
||||
line-height: 25px;
|
||||
padding: 5px 0;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
span.label {
|
||||
font-size: 0.75rem;
|
||||
color: $black-2;
|
||||
}
|
||||
span.value {
|
||||
margin-top: -4px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,6 @@
|
|||
box-shadow: $shadow-1;
|
||||
|
||||
header {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
padding: 0 14px;
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<% var key, item %>
|
||||
<% htmlWebpackPlugin.options.links = htmlWebpackPlugin.options.links || [] %>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>River of Ebooks | admin</title>
|
||||
<base href="/">
|
||||
<% for (item of htmlWebpackPlugin.options.links) {
|
||||
if (typeof item === 'string' || item instanceof String) { item = { href: item, rel: 'stylesheet' } } %>
|
||||
<link<% for (key in item) { %> <%= key %>="<%= item[key] %>"<% } %> /><%
|
||||
} %>
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width, maximum-scale=1, minimum-scale=1, user-scalable=no" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -5,6 +5,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>River of Ebooks</title>
|
||||
<base href="/">
|
||||
<% for (item of htmlWebpackPlugin.options.links) {
|
||||
if (typeof item === 'string' || item instanceof String) { item = { href: item, rel: 'stylesheet' } } %>
|
||||
<link<% for (key in item) { %> <%= key %>="<%= item[key] %>"<% } %> /><%
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>RoE - Login</title>
|
||||
<title>River of Ebooks - Login</title>
|
||||
<base href="/">
|
||||
<% for (item of htmlWebpackPlugin.options.links) {
|
||||
if (typeof item === 'string' || item instanceof String) { item = { href: item, rel: 'stylesheet' } } %>
|
||||
<link<% for (key in item) { %> <%= key %>="<%= item[key] %>"<% } %> /><%
|
||||
|
|
|
@ -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
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
|
|
|
@ -34,7 +34,7 @@ module.exports.models = {
|
|||
* *
|
||||
***************************************************************************/
|
||||
|
||||
// schema: true,
|
||||
schema: true,
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
|
|
|
@ -42,5 +42,9 @@ module.exports.policies = {
|
|||
BooksController: {
|
||||
'*': true,
|
||||
publish: [ 'keyAuth' ]
|
||||
},
|
||||
|
||||
AdminController: {
|
||||
'*': [ 'sessionAuth', 'adminAuth' ]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,15 @@ 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',
|
||||
'POST /api/keys/:id/verify': 'PublishKeyController.verify',
|
||||
|
||||
'GET /admin/api/users': 'AdminController.listUsers',
|
||||
'GET /admin/api/publishers': 'AdminController.listPublishers',
|
||||
'PATCH /admin/api/users/:id': 'AdminController.editUser',
|
||||
'PATCH /admin/api/publishers/:id': 'AdminController.editPublisher',
|
||||
'DELETE /admin/api/users/:id': 'AdminController.deleteUser',
|
||||
'DELETE /admin/api/publishers/:id': 'AdminController.deletePublisher'
|
||||
|
||||
// ╦ ╦╔═╗╔╗ ╦ ╦╔═╗╔═╗╦╔═╔═╗
|
||||
// ║║║║╣ ╠╩╗╠═╣║ ║║ ║╠╩╗╚═╗
|
||||
|
|
10
docs/api.md
10
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: <api key>,
|
||||
roe-secret: <api 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
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
exports.up = function (knex, Promise) {
|
||||
return Promise.all([
|
||||
knex.schema.table('book', t => {
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
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('name')
|
||||
t.string('url')
|
||||
t.string('appid')
|
||||
t.string('secret')
|
||||
t.string('verification_key')
|
||||
t.boolean('whitelisted').defaultTo(false)
|
||||
t.boolean('verified').defaultTo(false)
|
||||
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')
|
||||
])
|
||||
}
|
|
@ -5,15 +5,16 @@
|
|||
"description": "a Sails application",
|
||||
"keywords": [],
|
||||
"scripts": {
|
||||
"start": "npm-run-all --parallel open:client lift",
|
||||
"start": "npm run forever",
|
||||
"start:dev": "npm-run-all --parallel open:client lift",
|
||||
"start:debug": "npm-run-all --parallel open:client debug",
|
||||
"start:prod": "npm-run-all --parallel build:prod lift",
|
||||
"open:client": "webpack-dev-server --mode development",
|
||||
"start:client": "webpack-dev-server --mode development",
|
||||
"lift": "sails lift",
|
||||
"build": "npm run build:prod",
|
||||
"build:dev": "webpack --mode development",
|
||||
"build:prod": "webpack --mode production",
|
||||
"clean": "rimraf .tmp && mkdirp .tmp/public",
|
||||
"lift": "sails lift",
|
||||
"forever": "sudo ./node_modules/.bin/pm2 start ecosystem.config.js --env production",
|
||||
"stop": "sudo ./node_modules/.bin/pm2 delete roe-base",
|
||||
"test": "npm run lint && npm run custom-tests && echo 'Done.'",
|
||||
|
@ -59,6 +60,7 @@
|
|||
"babel-eslint": "^10.0.1",
|
||||
"babel-loader": "^8.0.4",
|
||||
"css-loader": "^1.0.1",
|
||||
"file-saver": "^2.0.1",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"mini-css-extract-plugin": "^0.4.4",
|
||||
"mocha": "^5.2.0",
|
||||
|
|
1364
shrinkwrap.yaml
1364
shrinkwrap.yaml
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
<%- partial('../../.tmp/public/admin.html') %>
|
|
@ -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'
|
||||
}),
|
||||
|
|
Loading…
Reference in New Issue