updated format and lint scripts and applied them
This commit is contained in:
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.next
|
||||||
|
styles
|
||||||
|
config
|
||||||
@@ -1,49 +1,49 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react'
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux'
|
||||||
import fetch from 'isomorphic-unfetch';
|
import fetch from 'isomorphic-unfetch'
|
||||||
import mapUser from '../util/mapUser';
|
import mapUser from '../util/mapUser'
|
||||||
import getUrl from '../util/getUrl';
|
import getUrl from '../util/getUrl'
|
||||||
import getJwt from '../util/getJwt';
|
import getJwt from '../util/getJwt'
|
||||||
|
|
||||||
const getDoc = async (id, req) => {
|
const getDoc = async (id, req) => {
|
||||||
let found, doc;
|
let found, doc
|
||||||
const jwt = getJwt(req);
|
const jwt = getJwt(req)
|
||||||
if(!jwt) return { found, doc, id };
|
if (!jwt) return { found, doc, id }
|
||||||
const docRes = await fetch(getUrl('docs/' + id, Boolean(req)), {
|
const docRes = await fetch(getUrl('docs/' + id, Boolean(req)), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { Authorization: jwt }
|
headers: { Authorization: jwt },
|
||||||
});
|
})
|
||||||
if (docRes.ok) {
|
if (docRes.ok) {
|
||||||
doc = await docRes.json();
|
doc = await docRes.json()
|
||||||
found = true;
|
found = true
|
||||||
|
}
|
||||||
|
return { found, doc, id }
|
||||||
}
|
}
|
||||||
return { found, doc, id };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ComposedComponent => {
|
export default ComposedComponent => {
|
||||||
class DocComp extends Component {
|
class DocComp extends Component {
|
||||||
state = {
|
state = {
|
||||||
found: false,
|
found: false,
|
||||||
id: null,
|
id: null,
|
||||||
doc: {}
|
doc: {},
|
||||||
}
|
}
|
||||||
static async getInitialProps({ query, req }) {
|
static async getInitialProps({ query, req }) {
|
||||||
return await getDoc(query.id, req);
|
return await getDoc(query.id, req)
|
||||||
}
|
}
|
||||||
static getDerivedStateFromProps(nextProps, state) {
|
static getDerivedStateFromProps(nextProps, state) {
|
||||||
const { found, id, doc } = nextProps;
|
const { found, id, doc } = nextProps
|
||||||
if(state.found !== found) return { found, id, doc };
|
if (state.found !== found) return { found, id, doc }
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
async componentDidUpdate(prevProps) {
|
async componentDidUpdate(prevProps) {
|
||||||
const { user, found, id } = this.props;
|
const { user, found, id } = this.props
|
||||||
if(prevProps.user.email === user.email || found) return;
|
if (prevProps.user.email === user.email || found) return
|
||||||
if(!user.email) return;
|
if (!user.email) return
|
||||||
this.setState(await getDoc(id));
|
this.setState(await getDoc(id))
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
return <ComposedComponent {...this.state} />;
|
return <ComposedComponent {...this.state} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return connect(mapUser)(DocComp);
|
return connect(mapUser)(DocComp)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,65 +1,65 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react'
|
||||||
import cm from 'codemirror';
|
import cm from 'codemirror'
|
||||||
import { getKey, isCtrlKey } from '../util/keys';
|
import { getKey, isCtrlKey } from '../util/keys'
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
require('codemirror/mode/markdown/markdown');
|
require('codemirror/mode/markdown/markdown')
|
||||||
}
|
}
|
||||||
export default class CodeMirror extends Component {
|
export default class CodeMirror extends Component {
|
||||||
handleChange = () => {
|
handleChange = () => {
|
||||||
if(!this.editor) return;
|
if (!this.editor) return
|
||||||
const value = this.editor.getValue();
|
const value = this.editor.getValue()
|
||||||
if (value !== this.props.value) {
|
if (value !== this.props.value) {
|
||||||
this.props.onChange && this.props.onChange(value);
|
this.props.onChange && this.props.onChange(value)
|
||||||
if (this.editor.getValue() !== this.props.value) {
|
if (this.editor.getValue() !== this.props.value) {
|
||||||
if (this.state.isControlled) {
|
if (this.state.isControlled) {
|
||||||
this.editor.setValue(this.props.value);
|
this.editor.setValue(this.props.value)
|
||||||
} else {
|
} else {
|
||||||
this.props.value = value;
|
this.props.value = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
checkSubmit = (cm, e) => {
|
checkSubmit = (cm, e) => {
|
||||||
const key = getKey(e);
|
const key = getKey(e)
|
||||||
if (isCtrlKey(key)) {
|
if (isCtrlKey(key)) {
|
||||||
this.ctrlKey = true;
|
this.ctrlKey = true
|
||||||
} else if (key === 13 && this.ctrlKey) {
|
} else if (key === 13 && this.ctrlKey) {
|
||||||
this.props.onSubmit();
|
this.props.onSubmit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleKeyUp = (cm, e) => {
|
handleKeyUp = (cm, e) => {
|
||||||
if(isCtrlKey(getKey(e))) this.ctrlKey = false;
|
if (isCtrlKey(getKey(e))) this.ctrlKey = false
|
||||||
}
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if(typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return
|
||||||
this.editor = cm.fromTextArea(this.textarea, this.props.options);
|
this.editor = cm.fromTextArea(this.textarea, this.props.options)
|
||||||
this.editor.on('change', this.handleChange);
|
this.editor.on('change', this.handleChange)
|
||||||
if (typeof this.props.onSubmit === 'function') {
|
if (typeof this.props.onSubmit === 'function') {
|
||||||
this.editor.on('keydown', this.checkSubmit);
|
this.editor.on('keydown', this.checkSubmit)
|
||||||
this.editor.on('keyup', this.handleKeyUp);
|
this.editor.on('keyup', this.handleKeyUp)
|
||||||
this.setupSubmitKey = true;
|
this.setupSubmitKey = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
if (this.setupSubmitKey) {
|
if (this.setupSubmitKey) {
|
||||||
this.editor.off('keydown', this.checkSubmit);
|
this.editor.off('keydown', this.checkSubmit)
|
||||||
this.editor.off('keyup', this.handleKeyUp);
|
this.editor.off('keyup', this.handleKeyUp)
|
||||||
this.setupSubmitKey = false;
|
this.setupSubmitKey = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
if(!this.editor || !this.props.value) return;
|
if (!this.editor || !this.props.value) return
|
||||||
if (this.editor.getValue() !== this.props.value) {
|
if (this.editor.getValue() !== this.props.value) {
|
||||||
this.editor.setValue(this.props.value);
|
this.editor.setValue(this.props.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const { value, className, onChange } = this.props;
|
const { value, className, onChange } = this.props
|
||||||
return (
|
return (
|
||||||
<div {...{ className }}>
|
<div {...{ className }}>
|
||||||
<textarea {...{value, onChange}} ref={el => this.textarea = el} />
|
<textarea {...{ value, onChange }} ref={el => (this.textarea = el)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,26 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link'
|
||||||
import getUrl from '../util/getUrl';
|
import getUrl from '../util/getUrl'
|
||||||
|
|
||||||
const DocItem = ({ id, name, dir, updated }) => {
|
const DocItem = ({ id, name, dir, updated }) => {
|
||||||
name = dir + (dir.length > 0 ? '/' : '') + name;
|
name = dir + (dir.length > 0 ? '/' : '') + name
|
||||||
const as = getUrl('k/' + id);
|
const as = getUrl('k/' + id)
|
||||||
const href = { pathname: '/k', query: { id } };
|
const href = { pathname: '/k', query: { id } }
|
||||||
return (<tr>
|
return (
|
||||||
<td><Link {...{href, as}}>
|
<tr>
|
||||||
<a><p className='noMargin'>
|
<td>
|
||||||
|
<Link {...{ href, as }}>
|
||||||
|
<a>
|
||||||
|
<p className="noMargin">
|
||||||
{name}
|
{name}
|
||||||
<span className='float-right'>
|
<span className="float-right">
|
||||||
{new Date(updated).toLocaleDateString('en-US')}
|
{new Date(updated).toLocaleDateString('en-US')}
|
||||||
</span>
|
</span>
|
||||||
</p></a>
|
</p>
|
||||||
</Link></td>
|
</a>
|
||||||
</tr>);
|
</Link>
|
||||||
};
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default DocItem;
|
export default DocItem
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
const Footer = () => (
|
const Footer = () => (
|
||||||
<footer className='footer'>
|
<footer className="footer">
|
||||||
<p>
|
<p>
|
||||||
Powered by{' '}
|
Powered by{' '}
|
||||||
<a href='//github.com/ijjk/mykb' target='_blank' rel='noopener noreferrer'>MYKB</a>
|
<a
|
||||||
|
href="//github.com/ijjk/mykb"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
MYKB
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default Footer;
|
export default Footer
|
||||||
|
|||||||
@@ -1,75 +1,79 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react'
|
||||||
import { withRouter } from 'next/router';
|
import { withRouter } from 'next/router'
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux'
|
||||||
import { doLogout } from '../redux/actions/userAct';
|
import { doLogout } from '../redux/actions/userAct'
|
||||||
import Link from 'next/link';
|
import Link from 'next/link'
|
||||||
import getUrl from '../util/getUrl';
|
import getUrl from '../util/getUrl'
|
||||||
import mapUser from '../util/mapUser';
|
import mapUser from '../util/mapUser'
|
||||||
|
|
||||||
const NavLink = ({ children, href, active }) => {
|
const NavLink = ({ children, href, active }) => {
|
||||||
const activeClass = active ? ' active' : '';
|
const activeClass = active ? ' active' : ''
|
||||||
return (
|
return (
|
||||||
<Link href={href} as={getUrl(href)}>
|
<Link href={href} as={getUrl(href)}>
|
||||||
<a className={activeClass}>{children}</a>
|
<a className={activeClass}>{children}</a>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
const navItems = [
|
const navItems = [['/', 'Home'], ['/new', 'New Doc'], ['/settings', 'Settings']]
|
||||||
['/', 'Home'],
|
|
||||||
['/new', 'New Doc'],
|
|
||||||
['/settings', 'Settings']
|
|
||||||
];
|
|
||||||
|
|
||||||
class Header extends Component {
|
class Header extends Component {
|
||||||
state = {
|
state = {
|
||||||
open: false
|
open: false,
|
||||||
}
|
}
|
||||||
hideNav = () => this.setState({ open: false });
|
hideNav = () => this.setState({ open: false })
|
||||||
toggleNav = () => this.setState({
|
toggleNav = () =>
|
||||||
open: !this.state.open
|
this.setState({
|
||||||
});
|
open: !this.state.open,
|
||||||
isActive = url => (
|
})
|
||||||
getUrl(this.props.router.pathname) === getUrl(url)
|
isActive = url => getUrl(this.props.router.pathname) === getUrl(url)
|
||||||
);
|
|
||||||
logout = e => {
|
logout = e => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
this.hideNav();
|
this.hideNav()
|
||||||
doLogout();
|
doLogout()
|
||||||
};
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const expandClass = this.state.open ? ' active' : '';
|
const expandClass = this.state.open ? ' active' : ''
|
||||||
const { user } = this.props;
|
const { user } = this.props
|
||||||
return (
|
return (
|
||||||
<nav className='navbar' role='navigation' aria-label='main navigation'>
|
<nav className="navbar" role="navigation" aria-label="main navigation">
|
||||||
<div className='navbar-brand'>
|
<div className="navbar-brand">
|
||||||
<NavLink href='/'>
|
<NavLink href="/">
|
||||||
<h3 onClick={this.hideNav}>MYKB</h3>
|
<h3 onClick={this.hideNav}>MYKB</h3>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!user.email ? null
|
{!user.email
|
||||||
|
? null
|
||||||
: [
|
: [
|
||||||
<div className={'navbar-burger ' + expandClass}
|
<div
|
||||||
onClick={this.toggleNav} key='burger'
|
className={'navbar-burger ' + expandClass}
|
||||||
|
onClick={this.toggleNav}
|
||||||
|
key="burger"
|
||||||
>
|
>
|
||||||
<div /><div /><div />
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
</div>,
|
</div>,
|
||||||
<div className={'navbar-items' + expandClass} key='items'>
|
<div className={'navbar-items' + expandClass} key="items">
|
||||||
{navItems.map(item => (
|
{navItems.map(item => (
|
||||||
<NavLink key={item[0]} href={item[0]}
|
<NavLink
|
||||||
|
key={item[0]}
|
||||||
|
href={item[0]}
|
||||||
active={this.isActive(item[0])}
|
active={this.isActive(item[0])}
|
||||||
>
|
>
|
||||||
<p className='item' onClick={this.hideNav}>{item[1]}</p>
|
<p className="item" onClick={this.hideNav}>
|
||||||
|
{item[1]}
|
||||||
|
</p>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
<a href='/logout' onClick={this.logout}>
|
<a href="/logout" onClick={this.logout}>
|
||||||
<p className='item'>Logout</p>
|
<p className="item">Logout</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>,
|
||||||
]}
|
]}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default withRouter(connect(mapUser)(Header));
|
export default withRouter(connect(mapUser)(Header))
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Component } from 'react';
|
import { Component } from 'react'
|
||||||
import Router from 'next/router';
|
import Router from 'next/router'
|
||||||
import getUrl from '../util/getUrl';
|
import getUrl from '../util/getUrl'
|
||||||
import { getKey } from '../util/keys';
|
import { getKey } from '../util/keys'
|
||||||
import { doLogout } from '../redux/actions/userAct';
|
import { doLogout } from '../redux/actions/userAct'
|
||||||
|
|
||||||
/* - keyboard shortcuts
|
/* - keyboard shortcuts
|
||||||
g then h -> navigate home
|
g then h -> navigate home
|
||||||
@@ -16,53 +16,56 @@ import { doLogout } from '../redux/actions/userAct';
|
|||||||
const keyToUrl = {
|
const keyToUrl = {
|
||||||
72: '/',
|
72: '/',
|
||||||
78: '/new',
|
78: '/new',
|
||||||
83: '/settings'
|
83: '/settings',
|
||||||
};
|
}
|
||||||
const keyToEl = {
|
const keyToEl = {
|
||||||
69: { sel: '#edit', func: 'click' },
|
69: { sel: '#edit', func: 'click' },
|
||||||
191: { sel: '.search', func: 'focus' }
|
191: { sel: '.search', func: 'focus' },
|
||||||
};
|
}
|
||||||
|
|
||||||
class KeyShortcuts extends Component {
|
class KeyShortcuts extends Component {
|
||||||
handleDown = e => {
|
handleDown = e => {
|
||||||
const tag = e.target.tagName;
|
const tag = e.target.tagName
|
||||||
if(tag === 'INPUT' || tag === 'TEXTAREA') return;
|
if (tag === 'INPUT' || tag === 'TEXTAREA') return
|
||||||
const key = getKey(e);
|
const key = getKey(e)
|
||||||
if(this.prevKey === 71) { // prev key was g
|
if (this.prevKey === 71) {
|
||||||
|
// prev key was g
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 72:
|
case 72:
|
||||||
case 78:
|
case 78:
|
||||||
case 83: {
|
case 83: {
|
||||||
const url = keyToUrl[key];
|
const url = keyToUrl[key]
|
||||||
Router.push(url, getUrl(url));
|
Router.push(url, getUrl(url))
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
case 76: {
|
case 76: {
|
||||||
setTimeout(doLogout, 1);
|
setTimeout(doLogout, 1)
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
default: break;
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 69:
|
case 69:
|
||||||
case 191: {
|
case 191: {
|
||||||
const { sel, func } = keyToEl[key];
|
const { sel, func } = keyToEl[key]
|
||||||
const el = document.querySelector(sel);
|
const el = document.querySelector(sel)
|
||||||
if(el) setTimeout(() => el[func](), 1);
|
if (el) setTimeout(() => el[func](), 1)
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
default: break;
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
this.prevKey = key
|
||||||
}
|
}
|
||||||
this.prevKey = key;
|
|
||||||
};
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
window.addEventListener('keydown', this.handleDown);
|
window.addEventListener('keydown', this.handleDown)
|
||||||
}
|
}
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener('keydown', this.handleDown);
|
window.removeEventListener('keydown', this.handleDown)
|
||||||
}
|
}
|
||||||
render = () => null
|
render = () => null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KeyShortcuts;
|
export default KeyShortcuts
|
||||||
|
|||||||
@@ -1,57 +1,66 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react'
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux'
|
||||||
import { doLogin } from '../redux/actions/userAct';
|
import { doLogin } from '../redux/actions/userAct'
|
||||||
import Spinner from './Spinner';
|
import Spinner from './Spinner'
|
||||||
import PaddedRow from './PaddedRow';
|
import PaddedRow from './PaddedRow'
|
||||||
import mapUser from '../util/mapUser';
|
import mapUser from '../util/mapUser'
|
||||||
|
|
||||||
class Login extends Component {
|
class Login extends Component {
|
||||||
state = {
|
state = {
|
||||||
email: '',
|
email: '',
|
||||||
pass: ''
|
pass: '',
|
||||||
};
|
}
|
||||||
|
|
||||||
updVal = e => {
|
updVal = e => {
|
||||||
const el = e.target;
|
const el = e.target
|
||||||
const val = el.value;
|
const val = el.value
|
||||||
if (el.getAttribute('type') === 'email') {
|
if (el.getAttribute('type') === 'email') {
|
||||||
return this.setState({ email: val });
|
return this.setState({ email: val })
|
||||||
}
|
}
|
||||||
this.setState({ pass: val });
|
this.setState({ pass: val })
|
||||||
}
|
}
|
||||||
|
|
||||||
submit = e => {
|
submit = e => {
|
||||||
const { pending } = this.props.user;
|
const { pending } = this.props.user
|
||||||
let { email, pass } = this.state;
|
let { email, pass } = this.state
|
||||||
email = email.trim();
|
email = email.trim()
|
||||||
pass = pass.trim();
|
pass = pass.trim()
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
if (pending || email.length === 0 || pass.length == 0) {
|
if (pending || email.length === 0 || pass.length == 0) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
doLogin({ email, password: pass });
|
doLogin({ email, password: pass })
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { pending, error } = this.props.user;
|
const { pending, error } = this.props.user
|
||||||
return (
|
return (
|
||||||
<div className='container content'>
|
<div className="container content">
|
||||||
<PaddedRow amount={25} vCenter>
|
<PaddedRow amount={25} vCenter>
|
||||||
<h4>Please login to continue</h4>
|
<h4>Please login to continue</h4>
|
||||||
<form noValidate>
|
<form noValidate>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label htmlFor='email'>Email:</label>
|
<label htmlFor="email">Email:</label>
|
||||||
<input type='email' id='email' name='email'
|
<input
|
||||||
autoFocus placeholder='John@deux.com'
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
autoFocus
|
||||||
|
placeholder="John@deux.com"
|
||||||
onChange={this.updVal}
|
onChange={this.updVal}
|
||||||
/>
|
/>
|
||||||
<label htmlFor='pass'>Pass:</label>
|
<label htmlFor="pass">Pass:</label>
|
||||||
<input type='password' id='pass' name='password'
|
<input
|
||||||
placeholder='Super secret password...' onChange={this.updVal}
|
type="password"
|
||||||
|
id="pass"
|
||||||
|
name="password"
|
||||||
|
placeholder="Super secret password..."
|
||||||
|
onChange={this.updVal}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<button className={'float-right' + (pending ? ' disabled' : '')}
|
<button
|
||||||
|
className={'float-right' + (pending ? ' disabled' : '')}
|
||||||
onClick={this.submit}
|
onClick={this.submit}
|
||||||
>
|
>
|
||||||
{pending ? <Spinner /> : 'Submit'}
|
{pending ? <Spinner /> : 'Submit'}
|
||||||
@@ -61,7 +70,7 @@ class Login extends Component {
|
|||||||
</form>
|
</form>
|
||||||
</PaddedRow>
|
</PaddedRow>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default connect(mapUser)(Login);
|
export default connect(mapUser)(Login)
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic'
|
||||||
import freezeSSR from '../util/freezeSSR';
|
import freezeSSR from '../util/freezeSSR'
|
||||||
|
|
||||||
const Markdown = dynamic(import('react-markdown'), freezeSSR('.Markdown'));
|
const Markdown = dynamic(import('react-markdown'), freezeSSR('.Markdown'))
|
||||||
const link = props => (
|
const link = props => <a {...props} target="_blank" rel="noopener noreferrer" />
|
||||||
<a {...props}
|
const renderers = { link }
|
||||||
target='_blank'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
const renderers = { link };
|
|
||||||
const AddRenderers = ({ className, source }) => (
|
const AddRenderers = ({ className, source }) => (
|
||||||
<Markdown {...{ className, source, renderers }} />
|
<Markdown {...{ className, source, renderers }} />
|
||||||
);
|
)
|
||||||
|
|
||||||
export default AddRenderers;
|
export default AddRenderers
|
||||||
|
|||||||
178
comps/MngDoc.js
178
comps/MngDoc.js
@@ -1,150 +1,170 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react'
|
||||||
import Router from 'next/router';
|
import Router from 'next/router'
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic'
|
||||||
import Page from '../comps/Page';
|
import Page from '../comps/Page'
|
||||||
import Markdown from '../comps/Markdown';
|
import Markdown from '../comps/Markdown'
|
||||||
import { checkDir, checkName } from '../util/checkDirParts';
|
import { checkDir, checkName } from '../util/checkDirParts'
|
||||||
import updStateFromId from '../util/updStateFromId';
|
import updStateFromId from '../util/updStateFromId'
|
||||||
import getUrl from '../util/getUrl';
|
import getUrl from '../util/getUrl'
|
||||||
import getJwt from '../util/getJwt';
|
import getJwt from '../util/getJwt'
|
||||||
|
|
||||||
const CodeMirrorSkel = () => (
|
const CodeMirrorSkel = () => (
|
||||||
<div className='column'>
|
<div className="column">
|
||||||
<textarea style={{ height: 'calc(300px - 1.2rem)', margin: 0 }} />
|
<textarea style={{ height: 'calc(300px - 1.2rem)', margin: 0 }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
const CodeMirror = dynamic(import('../comps/CodeMirror'), {
|
const CodeMirror = dynamic(import('../comps/CodeMirror'), {
|
||||||
loading: CodeMirrorSkel, ssr: false
|
loading: CodeMirrorSkel,
|
||||||
});
|
ssr: false,
|
||||||
|
})
|
||||||
const initState = {
|
const initState = {
|
||||||
name: '',
|
name: '',
|
||||||
dir: '',
|
dir: '',
|
||||||
md: '## New Document!!',
|
md: '## New Document!!',
|
||||||
editMode: false,
|
editMode: false,
|
||||||
error: null,
|
error: null,
|
||||||
pending: false
|
pending: false,
|
||||||
};
|
}
|
||||||
|
|
||||||
export default class MngDoc extends Component {
|
export default class MngDoc extends Component {
|
||||||
state = initState;
|
state = initState
|
||||||
updVal = updStateFromId.bind(this);
|
updVal = updStateFromId.bind(this)
|
||||||
updMd = md => this.setState({ md });
|
updMd = md => this.setState({ md })
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
let { name, md, dir, editMode } = this.state;
|
let { name, md, dir, editMode } = this.state
|
||||||
let data = {
|
let data = {
|
||||||
name: checkName(name),
|
name: checkName(name),
|
||||||
dir: checkDir(dir),
|
dir: checkDir(dir),
|
||||||
md
|
md,
|
||||||
};
|
}
|
||||||
const doErr = error => this.setState({ pending: false, error });
|
const doErr = error => this.setState({ pending: false, error })
|
||||||
const dirErr = 'can only contain A-Z, a-z, 0-9, -, or . and not start or end with .';
|
const dirErr =
|
||||||
|
'can only contain A-Z, a-z, 0-9, -, or . and not start or end with .'
|
||||||
|
|
||||||
if(!data.name) return doErr(
|
if (!data.name)
|
||||||
|
return doErr(
|
||||||
'Document name ' + (data.name === 0 ? 'can not be empty' : dirErr)
|
'Document name ' + (data.name === 0 ? 'can not be empty' : dirErr)
|
||||||
);
|
)
|
||||||
if (!data.dir && data.dir !== 0) {
|
if (!data.dir && data.dir !== 0) {
|
||||||
return doErr('Directory ' + dirErr);
|
return doErr('Directory ' + dirErr)
|
||||||
} else if (data.dir === 0) {
|
} else if (data.dir === 0) {
|
||||||
data.dir = '';
|
data.dir = ''
|
||||||
}
|
}
|
||||||
if (data.md.trim().length === 0) {
|
if (data.md.trim().length === 0) {
|
||||||
return doErr('Content can not be empty');
|
return doErr('Content can not be empty')
|
||||||
}
|
}
|
||||||
let url = getUrl('docs'),
|
let url = getUrl('docs'),
|
||||||
method = 'POST',
|
method = 'POST',
|
||||||
headers = { Authorization: getJwt(),
|
headers = {
|
||||||
'Content-Type': 'application/json'
|
Authorization: getJwt(),
|
||||||
};
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
if (editMode) {
|
if (editMode) {
|
||||||
let numRemoved = 0;
|
let numRemoved = 0
|
||||||
const dataKeys = Object.keys(data);
|
const dataKeys = Object.keys(data)
|
||||||
dataKeys.forEach(k => {
|
dataKeys.forEach(k => {
|
||||||
if (data[k] === this.props.doc[k]) {
|
if (data[k] === this.props.doc[k]) {
|
||||||
delete data[k];
|
delete data[k]
|
||||||
numRemoved++;
|
numRemoved++
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
if(dataKeys.length === numRemoved) return;
|
if (dataKeys.length === numRemoved) return
|
||||||
url = getUrl('docs/' + this.props.doc.id);
|
url = getUrl('docs/' + this.props.doc.id)
|
||||||
method = 'PATCH';
|
method = 'PATCH'
|
||||||
}
|
}
|
||||||
this.setState({ error: null, pending: true });
|
this.setState({ error: null, pending: true })
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
headers, method,
|
headers,
|
||||||
body: JSON.stringify(data)
|
method,
|
||||||
}).catch(doErr);
|
body: JSON.stringify(data),
|
||||||
|
}).catch(doErr)
|
||||||
try {
|
try {
|
||||||
data = await res.json();
|
data = await res.json()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
data = { message: 'An error occurred submitting doc' };
|
data = { message: 'An error occurred submitting doc' }
|
||||||
}
|
}
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const { id } = data;
|
const { id } = data
|
||||||
return Router.push({
|
return Router.push(
|
||||||
|
{
|
||||||
pathname: '/k',
|
pathname: '/k',
|
||||||
query: { id }
|
query: { id },
|
||||||
}, getUrl(`k/${id}`));
|
},
|
||||||
|
getUrl(`k/${id}`)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
doErr(data.message);
|
doErr(data.message)
|
||||||
}
|
}
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
static getDerivedStateFromProps(nextProps, prevState) {
|
||||||
const { doc } = nextProps;
|
const { doc } = nextProps
|
||||||
if (doc) {
|
if (doc) {
|
||||||
const { name, dir, md } = doc;
|
const { name, dir, md } = doc
|
||||||
return { name, md, dir, editMode: true };
|
return { name, md, dir, editMode: true }
|
||||||
} else if (prevState.id) {
|
} else if (prevState.id) {
|
||||||
return initState;
|
return initState
|
||||||
}
|
}
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const { md, dir, name, error, pending } = this.state;
|
const { md, dir, name, error, pending } = this.state
|
||||||
const rowStyle = { paddingTop: 10 };
|
const rowStyle = { paddingTop: 10 }
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className='row fill' style={rowStyle}>
|
<div className="row fill" style={rowStyle}>
|
||||||
<div className='column column-50'>
|
<div className="column column-50">
|
||||||
<Markdown className='fill Markdown' source={md} />
|
<Markdown className="fill Markdown" source={md} />
|
||||||
</div>
|
</div>
|
||||||
<div className='column column-50'>
|
<div className="column column-50">
|
||||||
<div className='row'>
|
<div className="row">
|
||||||
<div className='column column-60'>
|
<div className="column column-60">
|
||||||
<input type='text' maxLength={250}
|
<input
|
||||||
placeholder='New document name' id='name'
|
type="text"
|
||||||
value={name} onChange={this.updVal}
|
maxLength={250}
|
||||||
|
placeholder="New document name"
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
onChange={this.updVal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='column'>
|
<div className="column">
|
||||||
<input type='text' maxLength={1024}
|
<input
|
||||||
placeholder='Subdirectory (optional)' id='dir'
|
type="text"
|
||||||
value={dir} onChange={this.updVal}
|
maxLength={1024}
|
||||||
|
placeholder="Subdirectory (optional)"
|
||||||
|
id="dir"
|
||||||
|
value={dir}
|
||||||
|
onChange={this.updVal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='row'>
|
<div className="row">
|
||||||
<CodeMirror value={md}
|
<CodeMirror
|
||||||
className='column WrapCodeMirror'
|
value={md}
|
||||||
|
className="column WrapCodeMirror"
|
||||||
onChange={this.updMd}
|
onChange={this.updMd}
|
||||||
onSubmit={this.submit}
|
onSubmit={this.submit}
|
||||||
options={{
|
options={{
|
||||||
theme: 'monokai',
|
theme: 'monokai',
|
||||||
mode: 'markdown',
|
mode: 'markdown',
|
||||||
lineWrapping: true
|
lineWrapping: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='row' style={{ marginTop: 5 }}>
|
<div className="row" style={{ marginTop: 5 }}>
|
||||||
<div className='column'>
|
<div className="column">
|
||||||
<span>{error}</span>
|
<span>{error}</span>
|
||||||
<button className='float-right'
|
<button
|
||||||
|
className="float-right"
|
||||||
style={{ marginTop: 5 }}
|
style={{ marginTop: 5 }}
|
||||||
onClick={pending ? null : this.submit}
|
onClick={pending ? null : this.submit}
|
||||||
>Submit</button>
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
const PaddedRow = ({ children, amount, vCenter }) => {
|
const PaddedRow = ({ children, amount, vCenter }) => {
|
||||||
amount = amount || 20;
|
amount = amount || 20
|
||||||
const PadItem = () => (
|
const PadItem = () => <div className={'column column-' + amount + ' nomob'} />
|
||||||
<div className={'column column-' + amount + ' nomob'} />
|
let rowProps = { className: 'row' }
|
||||||
);
|
if (vCenter) rowProps = { className: 'row v-center' }
|
||||||
let rowProps = {className: 'row'};
|
else rowProps = { ...rowProps, style: { paddingTop: amount } }
|
||||||
if(vCenter) rowProps = {className: 'row v-center'};
|
|
||||||
else rowProps = {...rowProps, style: {paddingTop: amount}};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...rowProps}>
|
<div {...rowProps}>
|
||||||
<PadItem />
|
<PadItem />
|
||||||
<div className='column'>{ children }</div>
|
<div className="column">{children}</div>
|
||||||
<PadItem />
|
<PadItem />
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
export default PaddedRow;
|
export default PaddedRow
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux'
|
||||||
import Header from './Header';
|
import Header from './Header'
|
||||||
import KeyShortcuts from './KeyShortcuts';
|
import KeyShortcuts from './KeyShortcuts'
|
||||||
import Footer from './Footer';
|
import Footer from './Footer'
|
||||||
import Login from './Login';
|
import Login from './Login'
|
||||||
import Setup from './Setup';
|
import Setup from './Setup'
|
||||||
import mapUser from '../util/mapUser';
|
import mapUser from '../util/mapUser'
|
||||||
|
|
||||||
const Page = ({ user, children }) => {
|
const Page = ({ user, children }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header />
|
<Header />
|
||||||
<KeyShortcuts />
|
<KeyShortcuts />
|
||||||
{user.email
|
{user.email ? (
|
||||||
? <div className='container content'>
|
<div className="container content">{children}</div>
|
||||||
{ children }
|
) : user.setup ? (
|
||||||
</div>
|
<Setup />
|
||||||
: user.setup
|
) : (
|
||||||
? <Setup /> : <Login {...{user}} />
|
<Login {...{ user }} />
|
||||||
}
|
)}
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
export default connect(mapUser)(Page);
|
export default connect(mapUser)(Page)
|
||||||
|
|||||||
108
comps/Setup.js
108
comps/Setup.js
@@ -1,8 +1,8 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react'
|
||||||
import { doLogin } from '../redux/actions/userAct';
|
import { doLogin } from '../redux/actions/userAct'
|
||||||
import PaddedRow from './PaddedRow';
|
import PaddedRow from './PaddedRow'
|
||||||
import Spinner from './Spinner';
|
import Spinner from './Spinner'
|
||||||
import getUrl from '../util/getUrl';
|
import getUrl from '../util/getUrl'
|
||||||
|
|
||||||
export default class Setup extends Component {
|
export default class Setup extends Component {
|
||||||
state = {
|
state = {
|
||||||
@@ -10,88 +10,96 @@ export default class Setup extends Component {
|
|||||||
password: '',
|
password: '',
|
||||||
confirmPass: '',
|
confirmPass: '',
|
||||||
pending: false,
|
pending: false,
|
||||||
error: null
|
error: null,
|
||||||
}
|
}
|
||||||
updVal = e => {
|
updVal = e => {
|
||||||
const el = e.target;
|
const el = e.target
|
||||||
let key = 'email';
|
let key = 'email'
|
||||||
if(el.id === 'pass') key = 'password';
|
if (el.id === 'pass') key = 'password'
|
||||||
else if(el.id === 'pass2') key = 'confirmPass';
|
else if (el.id === 'pass2') key = 'confirmPass'
|
||||||
const obj = {};
|
const obj = {}
|
||||||
obj[key] = el.value;
|
obj[key] = el.value
|
||||||
this.setState(obj);
|
this.setState(obj)
|
||||||
}
|
}
|
||||||
submit = e => {
|
submit = e => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
let { email, password, confirmPass, pending } = this.state;
|
let { email, password, confirmPass, pending } = this.state
|
||||||
if(pending) return;
|
if (pending) return
|
||||||
email = email.trim();
|
email = email.trim()
|
||||||
password = password.trim();
|
password = password.trim()
|
||||||
confirmPass = confirmPass.trim();
|
confirmPass = confirmPass.trim()
|
||||||
const hasEmpty = [ email, password, confirmPass ]
|
const hasEmpty = [email, password, confirmPass].some(
|
||||||
.some(val => val.length === 0);
|
val => val.length === 0
|
||||||
|
)
|
||||||
|
|
||||||
if(hasEmpty) return;
|
if (hasEmpty) return
|
||||||
if (password.toLowerCase() !== confirmPass.toLowerCase()) {
|
if (password.toLowerCase() !== confirmPass.toLowerCase()) {
|
||||||
return this.setState({ error: 'Passwords don\'t match' });
|
return this.setState({ error: "Passwords don't match" })
|
||||||
}
|
}
|
||||||
this.setState({ pending: true, error: null });
|
this.setState({ pending: true, error: null })
|
||||||
const defaultErr = 'Could not create account';
|
const defaultErr = 'Could not create account'
|
||||||
|
|
||||||
fetch(getUrl('users'), {
|
fetch(getUrl('users'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ email, password, admin: true })
|
body: JSON.stringify({ email, password, admin: true }),
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return doLogin({ email, password }, null, true);
|
return doLogin({ email, password }, null, true)
|
||||||
}
|
}
|
||||||
res.json().then(({ message }) => {
|
res.json().then(({ message }) => {
|
||||||
const error = message || defaultErr;
|
const error = message || defaultErr
|
||||||
this.setState({ pending: false, error });
|
this.setState({ pending: false, error })
|
||||||
});
|
})
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
const error = err.message || defaultErr;
|
const error = err.message || defaultErr
|
||||||
this.setState({ pending: false, error });
|
this.setState({ pending: false, error })
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const { pending, error } = this.state;
|
const { pending, error } = this.state
|
||||||
return (
|
return (
|
||||||
<div className='container content'>
|
<div className="container content">
|
||||||
<PaddedRow amount={25} vCenter>
|
<PaddedRow amount={25} vCenter>
|
||||||
<div className='column'>
|
<div className="column">
|
||||||
<h3>Setup account</h3>
|
<h3>Setup account</h3>
|
||||||
<form noValidate>
|
<form noValidate>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label htmlFor='email'>Email:</label>
|
<label htmlFor="email">Email:</label>
|
||||||
<input type='email' autoFocus id='email'
|
<input
|
||||||
placeholder={'Your email (does\'t have to be actual email)'}
|
type="email"
|
||||||
|
autoFocus
|
||||||
|
id="email"
|
||||||
|
placeholder={"Your email (does't have to be actual email)"}
|
||||||
onChange={this.updVal}
|
onChange={this.updVal}
|
||||||
/>
|
/>
|
||||||
<label htmlFor='pass'>Password:</label>
|
<label htmlFor="pass">Password:</label>
|
||||||
<input type='password' id='pass' maxLength={512}
|
<input
|
||||||
placeholder='A super secret password'
|
type="password"
|
||||||
|
id="pass"
|
||||||
|
maxLength={512}
|
||||||
|
placeholder="A super secret password"
|
||||||
onChange={this.updVal}
|
onChange={this.updVal}
|
||||||
/>
|
/>
|
||||||
<label htmlFor='pass2'>Confirm Password:</label>
|
<label htmlFor="pass2">Confirm Password:</label>
|
||||||
<input type='password' id='pass2' maxLength={512}
|
<input
|
||||||
placeholder='Confirm your super secret password'
|
type="password"
|
||||||
|
id="pass2"
|
||||||
|
maxLength={512}
|
||||||
|
placeholder="Confirm your super secret password"
|
||||||
onChange={this.updVal}
|
onChange={this.updVal}
|
||||||
/>
|
/>
|
||||||
<button className='float-right'
|
<button className="float-right" onClick={this.submit}>
|
||||||
onClick={this.submit}
|
|
||||||
>
|
|
||||||
{pending ? <Spinner /> : 'Submit'}
|
{pending ? <Spinner /> : 'Submit'}
|
||||||
</button>
|
</button>
|
||||||
{!error ? null : <p className='danger'>{error}</p>}
|
{!error ? null : <p className="danger">{error}</p>}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</PaddedRow>
|
</PaddedRow>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,2 @@
|
|||||||
const Spinner = (props) => (
|
const Spinner = props => <div className="spinner" {...props} />
|
||||||
<div className='spinner' {...props}></div>
|
export default Spinner
|
||||||
);
|
|
||||||
export default Spinner;
|
|
||||||
|
|||||||
33
genSecret.js
33
genSecret.js
@@ -1,24 +1,23 @@
|
|||||||
const fs = require('fs');
|
/* eslint-disable no-console */
|
||||||
const path = require('path');
|
const fs = require('fs')
|
||||||
const crypto = require('crypto');
|
const path = require('path')
|
||||||
const secret = crypto.randomBytes(256).toString('hex');
|
const crypto = require('crypto')
|
||||||
const { NODE_ENV } = process.env;
|
const secret = crypto.randomBytes(256).toString('hex')
|
||||||
let confFile = 'default.json';
|
const { NODE_ENV } = process.env
|
||||||
|
let confFile = 'default.json'
|
||||||
|
|
||||||
if (NODE_ENV && NODE_ENV.toLowerCase() === 'production') {
|
if (NODE_ENV && NODE_ENV.toLowerCase() === 'production') {
|
||||||
confFile = 'production.json';
|
confFile = 'production.json'
|
||||||
}
|
}
|
||||||
let config = require('./config/' + confFile);
|
let config = require('./config/' + confFile)
|
||||||
let configPath = path.join(__dirname, 'config', confFile);
|
let configPath = path.join(__dirname, 'config', confFile)
|
||||||
|
|
||||||
if (!config.authentication) {
|
if (!config.authentication) {
|
||||||
config.authentication = { secret };
|
config.authentication = { secret }
|
||||||
} else {
|
} else {
|
||||||
config.authentication.secret = secret;
|
config.authentication.secret = secret
|
||||||
}
|
}
|
||||||
fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n',
|
fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', err => {
|
||||||
err => {
|
if (err) return console.error(err)
|
||||||
if(err) return console.error(err);
|
console.log('\nAuthentication secret successfully updated in', confFile)
|
||||||
console.log('\nAuthentication secret successfully updated in', confFile);
|
})
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
const withSass = require('@zeit/next-sass');
|
const withSass = require('@zeit/next-sass')
|
||||||
const { ANALYZE } = process.env
|
const { ANALYZE } = process.env
|
||||||
let AnalyzerPlugin;
|
let AnalyzerPlugin
|
||||||
if (ANALYZE) {
|
if (ANALYZE) {
|
||||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
|
||||||
AnalyzerPlugin = BundleAnalyzerPlugin;
|
AnalyzerPlugin = BundleAnalyzerPlugin
|
||||||
}
|
}
|
||||||
module.exports = withSass({
|
module.exports = withSass({
|
||||||
poweredByHeader: false,
|
poweredByHeader: false,
|
||||||
webpack: function(config, { isServer }) {
|
webpack: function(config, { isServer }) {
|
||||||
if (ANALYZE) {
|
if (ANALYZE) {
|
||||||
config.plugins.push(new AnalyzerPlugin({
|
config.plugins.push(
|
||||||
|
new AnalyzerPlugin({
|
||||||
analyzerMode: 'server',
|
analyzerMode: 'server',
|
||||||
analyzerPort: isServer ? 8888 : 8889,
|
analyzerPort: isServer ? 8888 : 8889,
|
||||||
openAnalyzer: true
|
openAnalyzer: true,
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}
|
},
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
"yarn": ">= 0.18.0"
|
"yarn": ">= 0.18.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write '**/*.js'",
|
"format": "prettier --ignore-path .eslintignore --write '**/*.js'",
|
||||||
"lint": "eslint ./src ./test ./pages ./redux ./util ./comps --config .eslintrc.json",
|
"lint": "eslint . --config .eslintrc.json",
|
||||||
"mocha": "cross-env NODE_ENV=production mocha test/ --recursive --exit",
|
"mocha": "cross-env NODE_ENV=production mocha test/ --recursive --exit",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"analyze": "cross-env ANALYZE=true next build",
|
"analyze": "cross-env ANALYZE=true next build",
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
import App, { Container } from 'next/app';
|
import App, { Container } from 'next/app'
|
||||||
import store from '../redux/store';
|
import store from '../redux/store'
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux'
|
||||||
import { setUser, doLogin } from '../redux/actions/userAct';
|
import { setUser, doLogin } from '../redux/actions/userAct'
|
||||||
import '../styles/style.sass';
|
import '../styles/style.sass'
|
||||||
const ssr = typeof window === 'undefined';
|
const ssr = typeof window === 'undefined'
|
||||||
|
|
||||||
export default class MyApp extends App {
|
export default class MyApp extends App {
|
||||||
static async getInitialProps({ Component, ctx }) {
|
static async getInitialProps({ Component, ctx }) {
|
||||||
let user = {};
|
let user = {}
|
||||||
let setup = false;
|
let setup = false
|
||||||
if (ssr) {
|
if (ssr) {
|
||||||
user = ctx.req.user || {};
|
user = ctx.req.user || {}
|
||||||
setup = ctx.req.doSetup || false;
|
setup = ctx.req.doSetup || false
|
||||||
}
|
}
|
||||||
let pageProps = {};
|
let pageProps = {}
|
||||||
if (Component.getInitialProps) {
|
if (Component.getInitialProps) {
|
||||||
pageProps = await Component.getInitialProps(ctx);
|
pageProps = await Component.getInitialProps(ctx)
|
||||||
}
|
}
|
||||||
return { Component, pageProps, user, setup };
|
return { Component, pageProps, user, setup }
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
const { user, setup } = this.props;
|
const { user, setup } = this.props
|
||||||
setUser({...user, setup});
|
setUser({ ...user, setup })
|
||||||
if (!ssr && !user.email) {
|
if (!ssr && !user.email) {
|
||||||
const { jwt } = window.localStorage;
|
const { jwt } = window.localStorage
|
||||||
if(jwt) doLogin(null, jwt, true);
|
if (jwt) doLogin(null, jwt, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let { Component, pageProps } = this.props;
|
let { Component, pageProps } = this.props
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Container>
|
<Container>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</Container>
|
</Container>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
import Document, { Head, Main, NextScript } from 'next/document';
|
import Document, { Head, Main, NextScript } from 'next/document'
|
||||||
import getUrl from '../util/getUrl';
|
import getUrl from '../util/getUrl'
|
||||||
|
|
||||||
export default class MyDocument extends Document {
|
export default class MyDocument extends Document {
|
||||||
render() {
|
render() {
|
||||||
const favicon = getUrl('favicon.ico');
|
const favicon = getUrl('favicon.ico')
|
||||||
return (
|
return (
|
||||||
<html>
|
<html>
|
||||||
<Head>
|
<Head>
|
||||||
<meta charSet='utf-8'/>
|
<meta charSet="utf-8" />
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'/>
|
<meta
|
||||||
<link rel='shortcut icon' href={favicon} type='image/x-icon'/>
|
name="viewport"
|
||||||
<link rel='icon' href={favicon} type='image/x-icon'/>
|
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||||
<link rel='stylesheet' href={getUrl('/_next/static/style.css')} />
|
/>
|
||||||
|
<link rel="shortcut icon" href={favicon} type="image/x-icon" />
|
||||||
|
<link rel="icon" href={favicon} type="image/x-icon" />
|
||||||
|
<link rel="stylesheet" href={getUrl('/_next/static/style.css')} />
|
||||||
<title>My Knowledge Base</title>
|
<title>My Knowledge Base</title>
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<body>
|
||||||
@@ -19,6 +22,6 @@ export default class MyDocument extends Document {
|
|||||||
<NextScript />
|
<NextScript />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react'
|
||||||
import Page from '../comps/Page';
|
import Page from '../comps/Page'
|
||||||
import MngDoc from '../comps/MngDoc';
|
import MngDoc from '../comps/MngDoc'
|
||||||
import AddDoc from '../comps/AddDoc';
|
import AddDoc from '../comps/AddDoc'
|
||||||
|
|
||||||
class Edit extends Component {
|
class Edit extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { found, doc } = this.props;
|
const { found, doc } = this.props
|
||||||
if(!found) return (
|
if (!found)
|
||||||
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<h3>Doc not found...</h3>
|
<h3>Doc not found...</h3>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
)
|
||||||
return <MngDoc {...{doc}} />;
|
return <MngDoc {...{ doc }} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default AddDoc(Edit);
|
export default AddDoc(Edit)
|
||||||
|
|||||||
184
pages/index.js
184
pages/index.js
@@ -1,14 +1,14 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react'
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux'
|
||||||
import Router from 'next/router';
|
import Router from 'next/router'
|
||||||
import Paginate from 'react-paginate';
|
import Paginate from 'react-paginate'
|
||||||
import Page from '../comps/Page';
|
import Page from '../comps/Page'
|
||||||
import PaddedRow from '../comps/PaddedRow';
|
import PaddedRow from '../comps/PaddedRow'
|
||||||
import Spinner from '../comps/Spinner';
|
import Spinner from '../comps/Spinner'
|
||||||
import DocItem from '../comps/DocItem';
|
import DocItem from '../comps/DocItem'
|
||||||
import { $limit, getDocs, buildQ } from '../util/getDocs';
|
import { $limit, getDocs, buildQ } from '../util/getDocs'
|
||||||
import getJwt from '../util/getJwt';
|
import getJwt from '../util/getJwt'
|
||||||
import mapUser from '../util/mapUser';
|
import mapUser from '../util/mapUser'
|
||||||
|
|
||||||
class Index extends Component {
|
class Index extends Component {
|
||||||
state = {
|
state = {
|
||||||
@@ -18,95 +18,104 @@ class Index extends Component {
|
|||||||
pending: false,
|
pending: false,
|
||||||
error: null,
|
error: null,
|
||||||
total: 0,
|
total: 0,
|
||||||
docs: []
|
docs: [],
|
||||||
}
|
}
|
||||||
static async getInitialProps({ req, query }) {
|
static async getInitialProps({ req, query }) {
|
||||||
let page = 1, $search = '';
|
let page = 1,
|
||||||
|
$search = ''
|
||||||
if (query) {
|
if (query) {
|
||||||
page = query.page || page;
|
page = query.page || page
|
||||||
$search = query.search || $search;
|
$search = query.search || $search
|
||||||
}
|
}
|
||||||
const jwt = getJwt(req);
|
const jwt = getJwt(req)
|
||||||
if(!jwt) return { total: 0, docs: [] };
|
if (!jwt) return { total: 0, docs: [] }
|
||||||
const q = buildQ({ $search, $skip: page });
|
const q = buildQ({ $search, $skip: page })
|
||||||
const data = await getDocs(q, req ? jwt : false);
|
const data = await getDocs(q, req ? jwt : false)
|
||||||
return { ...data, page, $search };
|
return { ...data, page, $search }
|
||||||
}
|
}
|
||||||
updDocs = (time, doSearch) => {
|
updDocs = (time, doSearch) => {
|
||||||
clearTimeout(this.docsTime);
|
clearTimeout(this.docsTime)
|
||||||
this.docsTime = setTimeout(async () => {
|
this.docsTime = setTimeout(async () => {
|
||||||
let { $sort, $search, page } = this.state;
|
let { $sort, $search, page } = this.state
|
||||||
if (doSearch) {
|
if (doSearch) {
|
||||||
const query = { search: $search };
|
const query = { search: $search }
|
||||||
if(!$search) delete query.search;
|
if (!$search) delete query.search
|
||||||
Router.push({ pathname: '/', query });
|
Router.push({ pathname: '/', query })
|
||||||
}
|
}
|
||||||
this.setState({ error: null });
|
this.setState({ error: null })
|
||||||
this.docsTime = setTimeout(() => {
|
this.docsTime = setTimeout(() => {
|
||||||
this.setState({ pending: true });
|
this.setState({ pending: true })
|
||||||
}, 125);
|
}, 125)
|
||||||
|
|
||||||
const q = buildQ({ $search, $sort, $skip: page });
|
const q = buildQ({ $search, $sort, $skip: page })
|
||||||
const data = await getDocs(q);
|
const data = await getDocs(q)
|
||||||
clearTimeout(this.docsTime);
|
clearTimeout(this.docsTime)
|
||||||
this.setState({ ...data, pending: false });
|
this.setState({ ...data, pending: false })
|
||||||
}, time || 275);
|
}, time || 275)
|
||||||
}
|
}
|
||||||
updQuery = e => {
|
updQuery = e => {
|
||||||
this.setState({ [e.target.id]: e.target.value });
|
this.setState({ [e.target.id]: e.target.value })
|
||||||
this.updDocs(0, e.target.id === '$search');
|
this.updDocs(0, e.target.id === '$search')
|
||||||
}
|
}
|
||||||
handlePage = ({ selected }) => {
|
handlePage = ({ selected }) => {
|
||||||
const { $search } = this.state;
|
const { $search } = this.state
|
||||||
const page = selected + 1;
|
const page = selected + 1
|
||||||
const query = {};
|
const query = {}
|
||||||
this.setState({ page });
|
this.setState({ page })
|
||||||
if(page > 1) query.page = page;
|
if (page > 1) query.page = page
|
||||||
if($search) query.search = $search;
|
if ($search) query.search = $search
|
||||||
Router.push({ pathname: '/', query });
|
Router.push({ pathname: '/', query })
|
||||||
this.updDocs(1);
|
this.updDocs(1)
|
||||||
}
|
}
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
static getDerivedStateFromProps(nextProps, prevState) {
|
||||||
let { docs, total, page, $search } = nextProps;
|
let { docs, total, page, $search } = nextProps
|
||||||
if(docs.length !== prevState.docs.length ||
|
if (
|
||||||
page !== prevState.page || $search !== prevState.$search) {
|
docs.length !== prevState.docs.length ||
|
||||||
return { total, docs, page, $search, pending: false };
|
page !== prevState.page ||
|
||||||
|
$search !== prevState.$search
|
||||||
|
) {
|
||||||
|
return { total, docs, page, $search, pending: false }
|
||||||
}
|
}
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { user, docs } = this.props;
|
const { user, docs } = this.props
|
||||||
if(prevProps.user.email === user.email) return;
|
if (prevProps.user.email === user.email) return
|
||||||
if(user.email && docs.length === 0) this.updDocs(1);
|
if (user.email && docs.length === 0) this.updDocs(1)
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { $sort, $search, pending, error, docs, total, page } = this.state
|
||||||
$sort, $search, pending,
|
const pages = Math.ceil(total / $limit)
|
||||||
error, docs, total, page
|
|
||||||
} = this.state;
|
|
||||||
const pages = Math.ceil(total / $limit);
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PaddedRow>
|
<PaddedRow>
|
||||||
<input type='text' placeholder='Search knowledge base...'
|
<input
|
||||||
maxLength={128} value={$search} className='search'
|
type="text"
|
||||||
id='$search' onChange={this.updQuery}
|
placeholder="Search knowledge base..."
|
||||||
|
maxLength={128}
|
||||||
|
value={$search}
|
||||||
|
className="search"
|
||||||
|
id="$search"
|
||||||
|
onChange={this.updQuery}
|
||||||
/>
|
/>
|
||||||
</PaddedRow>
|
</PaddedRow>
|
||||||
<PaddedRow>
|
<PaddedRow>
|
||||||
<div className='inline' style={{ width: '100%' }}>
|
<div className="inline" style={{ width: '100%' }}>
|
||||||
<h4 className='noMargin'>Docs</h4>
|
<h4 className="noMargin">Docs</h4>
|
||||||
<div className='float-right inline'>
|
<div className="float-right inline">
|
||||||
<label htmlFor='sort'>Sort: </label>
|
<label htmlFor="sort">Sort: </label>
|
||||||
<select id='$sort' value={$sort}
|
<select
|
||||||
onChange={this.updQuery} style={{ width: 150 }}
|
id="$sort"
|
||||||
|
value={$sort}
|
||||||
|
onChange={this.updQuery}
|
||||||
|
style={{ width: 150 }}
|
||||||
>
|
>
|
||||||
<option value='updated:-1'>{'Updated (new -> old)'}</option>
|
<option value="updated:-1">{'Updated (new -> old)'}</option>
|
||||||
<option value='updated:1'>{'Updated (old -> new)'}</option>
|
<option value="updated:1">{'Updated (old -> new)'}</option>
|
||||||
<option value='created:-1'>{'Created (new -> old)'}</option>
|
<option value="created:-1">{'Created (new -> old)'}</option>
|
||||||
<option value='created:1'>{'Created (old -> new)'}</option>
|
<option value="created:1">{'Created (old -> new)'}</option>
|
||||||
<option value='dirName:1'>{'Name (A -> Z)'}</option>
|
<option value="dirName:1">{'Name (A -> Z)'}</option>
|
||||||
<option value='dirName:-1'>{'Name (Z -> A)'}</option>
|
<option value="dirName:-1">{'Name (Z -> A)'}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,32 +123,37 @@ class Index extends Component {
|
|||||||
<PaddedRow>
|
<PaddedRow>
|
||||||
{docs.length > 0 || error || pending ? null : <p>No docs found...</p>}
|
{docs.length > 0 || error || pending ? null : <p>No docs found...</p>}
|
||||||
{!error ? null : <p>{error}</p>}
|
{!error ? null : <p>{error}</p>}
|
||||||
{!pending || error ? null : <Spinner style={{ margin: '25px auto 0' }} />}
|
{!pending || error ? null : (
|
||||||
{docs.length < 1 || pending || error ? null
|
<Spinner style={{ margin: '25px auto 0' }} />
|
||||||
: <div>
|
)}
|
||||||
|
{docs.length < 1 || pending || error ? null : (
|
||||||
|
<div>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>
|
<tr>
|
||||||
Doc <span className='float-right'>Modified</span>
|
<th>
|
||||||
</th></tr>
|
Doc <span className="float-right">Modified</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{docs.map(doc => <DocItem {...doc} key={doc.id} />)}
|
{docs.map(doc => <DocItem {...doc} key={doc.id} />)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{pages < 2 ? null
|
{pages < 2 ? null : (
|
||||||
: <Paginate pageCount={pages}
|
<Paginate
|
||||||
containerClassName='paginate'
|
pageCount={pages}
|
||||||
activeClassName='active'
|
containerClassName="paginate"
|
||||||
|
activeClassName="active"
|
||||||
onPageChange={this.handlePage}
|
onPageChange={this.handlePage}
|
||||||
forcePage={page - 1}
|
forcePage={page - 1}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</PaddedRow>
|
</PaddedRow>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default connect(mapUser)(Index);
|
export default connect(mapUser)(Index)
|
||||||
|
|||||||
67
pages/k.js
67
pages/k.js
@@ -1,53 +1,64 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react'
|
||||||
import Link from 'next/link';
|
import Link from 'next/link'
|
||||||
import Router from 'next/router';
|
import Router from 'next/router'
|
||||||
import fetch from 'isomorphic-unfetch';
|
import fetch from 'isomorphic-unfetch'
|
||||||
import Page from '../comps/Page';
|
import Page from '../comps/Page'
|
||||||
import Markdown from '../comps/Markdown';
|
import Markdown from '../comps/Markdown'
|
||||||
import AddDoc from '../comps/AddDoc';
|
import AddDoc from '../comps/AddDoc'
|
||||||
import getUrl from '../util/getUrl';
|
import getUrl from '../util/getUrl'
|
||||||
import getJwt from '../util/getJwt';
|
import getJwt from '../util/getJwt'
|
||||||
|
|
||||||
class k extends Component {
|
class k extends Component {
|
||||||
delete = async () => {
|
delete = async () => {
|
||||||
const sure = window.confirm('Are you sure you want to delete this doc? This can not be undone.');
|
const sure = window.confirm(
|
||||||
if(!sure) return;
|
'Are you sure you want to delete this doc? This can not be undone.'
|
||||||
|
)
|
||||||
|
if (!sure) return
|
||||||
const del = await fetch(getUrl('docs/' + this.props.id), {
|
const del = await fetch(getUrl('docs/' + this.props.id), {
|
||||||
headers: { Authorization: getJwt() },
|
headers: { Authorization: getJwt() },
|
||||||
method: 'DELETE'
|
method: 'DELETE',
|
||||||
}).catch(({ message }) => ({ ok: false, message }));
|
}).catch(({ message }) => ({ ok: false, message }))
|
||||||
if(del.ok) Router.push('/');
|
if (del.ok) Router.push('/')
|
||||||
else {
|
else {
|
||||||
if (!del.message) {
|
if (!del.message) {
|
||||||
const data = await del.json();
|
const data = await del.json()
|
||||||
del.message = data.message;
|
del.message = data.message
|
||||||
}
|
}
|
||||||
window.alert(`Could not delete doc, ${del.message}`);
|
window.alert(`Could not delete doc, ${del.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const { found, id, doc } = this.props;
|
const { found, id, doc } = this.props
|
||||||
if(!found) return (
|
if (!found)
|
||||||
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<h3>Doc not found...</h3>
|
<h3>Doc not found...</h3>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
)
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<h5 style={{ marginBottom: '1rem' }}>
|
<h5 style={{ marginBottom: '1rem' }}>
|
||||||
{doc.dir}{doc.dir.length > 0 ? '/' : ''}{doc.name}{' - '}
|
{doc.dir}
|
||||||
<Link as={getUrl('edit/' + id)}
|
{doc.dir.length > 0 ? '/' : ''}
|
||||||
|
{doc.name}
|
||||||
|
{' - '}
|
||||||
|
<Link
|
||||||
|
as={getUrl('edit/' + id)}
|
||||||
href={{ pathname: '/edit', query: { id } }}
|
href={{ pathname: '/edit', query: { id } }}
|
||||||
>
|
>
|
||||||
<a id='edit'>edit</a>
|
<a id="edit">edit</a>
|
||||||
</Link>
|
</Link>
|
||||||
<button className='float-right' onClick={this.delete}
|
<button
|
||||||
|
className="float-right"
|
||||||
|
onClick={this.delete}
|
||||||
style={{ margin: '5px 0 0' }}
|
style={{ margin: '5px 0 0' }}
|
||||||
>Delete</button>
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
</h5>
|
</h5>
|
||||||
<Markdown source={doc.md} className='Markdown' />
|
<Markdown source={doc.md} className="Markdown" />
|
||||||
</Page>
|
</Page>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default AddDoc(k);
|
export default AddDoc(k)
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
import MngDoc from '../comps/MngDoc';
|
import MngDoc from '../comps/MngDoc'
|
||||||
export default MngDoc;
|
export default MngDoc
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react'
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux'
|
||||||
import fetch from 'isomorphic-unfetch';
|
import fetch from 'isomorphic-unfetch'
|
||||||
import Page from '../comps/Page';
|
import Page from '../comps/Page'
|
||||||
import PaddedRow from '../comps/PaddedRow';
|
import PaddedRow from '../comps/PaddedRow'
|
||||||
import Spinner from '../comps/Spinner';
|
import Spinner from '../comps/Spinner'
|
||||||
import updStateFromId from '../util/updStateFromId';
|
import updStateFromId from '../util/updStateFromId'
|
||||||
import mapUser from '../util/mapUser';
|
import mapUser from '../util/mapUser'
|
||||||
import getUrl from '../util/getUrl';
|
import getUrl from '../util/getUrl'
|
||||||
import getJwt from '../util/getJwt';
|
import getJwt from '../util/getJwt'
|
||||||
|
|
||||||
class Settings extends Component {
|
class Settings extends Component {
|
||||||
state = {
|
state = {
|
||||||
@@ -15,53 +15,55 @@ class Settings extends Component {
|
|||||||
passErr: null,
|
passErr: null,
|
||||||
curPass: '',
|
curPass: '',
|
||||||
newPass: '',
|
newPass: '',
|
||||||
confPass: ''
|
confPass: '',
|
||||||
}
|
}
|
||||||
updVal = updStateFromId.bind(this);
|
updVal = updStateFromId.bind(this)
|
||||||
submit = async e => {
|
submit = async e => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
const { pending, curPass, newPass, confPass } = this.state;
|
const { pending, curPass, newPass, confPass } = this.state
|
||||||
const { email, _id } = this.props.user;
|
const { email, _id } = this.props.user
|
||||||
if(pending) return;
|
if (pending) return
|
||||||
const doErr = passErr => this.setState({ pending: false, passErr });
|
const doErr = passErr => this.setState({ pending: false, passErr })
|
||||||
const vals = {
|
const vals = {
|
||||||
'Current password': curPass,
|
'Current password': curPass,
|
||||||
'New password': newPass,
|
'New password': newPass,
|
||||||
'Confirm new password': confPass
|
'Confirm new password': confPass,
|
||||||
};
|
|
||||||
const keys = Object.keys(vals);
|
|
||||||
for(let i = 0; i < keys.length; i++) {
|
|
||||||
let key = keys[i], val = vals[key];
|
|
||||||
if(val.length === 0) return doErr(`${key} is required`);
|
|
||||||
}
|
}
|
||||||
if(newPass !== confPass) return doErr('New passwords don\'t match');
|
const keys = Object.keys(vals)
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
let key = keys[i],
|
||||||
|
val = vals[key]
|
||||||
|
if (val.length === 0) return doErr(`${key} is required`)
|
||||||
|
}
|
||||||
|
if (newPass !== confPass) return doErr("New passwords don't match")
|
||||||
|
|
||||||
this.setState({ passErr: null, pending: true });
|
this.setState({ passErr: null, pending: true })
|
||||||
const updRes = await fetch(getUrl('users/' + _id), {
|
const updRes = await fetch(getUrl('users/' + _id), {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json', Authorization: getJwt() },
|
headers: { 'Content-Type': 'application/json', Authorization: getJwt() },
|
||||||
body: JSON.stringify({ email, password: curPass, newPassword: newPass })
|
body: JSON.stringify({ email, password: curPass, newPassword: newPass }),
|
||||||
}).catch(doErr);
|
}).catch(doErr)
|
||||||
if (updRes.ok) {
|
if (updRes.ok) {
|
||||||
this.setState({
|
this.setState({
|
||||||
curPass: '', newPass: '', confPass: '',
|
curPass: '',
|
||||||
|
newPass: '',
|
||||||
|
confPass: '',
|
||||||
passErr: 'Password updated successfully',
|
passErr: 'Password updated successfully',
|
||||||
pending: false
|
pending: false,
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
let message = 'failed to update password';
|
let message = 'failed to update password'
|
||||||
try {
|
try {
|
||||||
const data = await updRes.json();
|
const data = await updRes.json()
|
||||||
message = data.message || message;
|
message = data.message || message
|
||||||
} catch (err) { doErr(err.message); }
|
} catch (err) {
|
||||||
doErr(message);
|
doErr(err.message)
|
||||||
|
}
|
||||||
|
doErr(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { pending, passErr, curPass, newPass, confPass } = this.state
|
||||||
pending, passErr, curPass,
|
|
||||||
newPass, confPass
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
@@ -71,20 +73,33 @@ class Settings extends Component {
|
|||||||
<form noValidate style={{ padding: '0 0 45px' }}>
|
<form noValidate style={{ padding: '0 0 45px' }}>
|
||||||
<h4>Change password</h4>
|
<h4>Change password</h4>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label htmlFor='curPass'>Current Password</label>
|
<label htmlFor="curPass">Current Password</label>
|
||||||
<input type='password' id='curPass' onChange={this.updVal}
|
<input
|
||||||
placeholder='Current super secret password...' value={curPass}
|
type="password"
|
||||||
|
id="curPass"
|
||||||
|
onChange={this.updVal}
|
||||||
|
placeholder="Current super secret password..."
|
||||||
|
value={curPass}
|
||||||
/>
|
/>
|
||||||
<label htmlFor='newPass'>New Password</label>
|
<label htmlFor="newPass">New Password</label>
|
||||||
<input type='password' id='newPass' onChange={this.updVal}
|
<input
|
||||||
placeholder='New super secret password...' value={newPass}
|
type="password"
|
||||||
|
id="newPass"
|
||||||
|
onChange={this.updVal}
|
||||||
|
placeholder="New super secret password..."
|
||||||
|
value={newPass}
|
||||||
/>
|
/>
|
||||||
<label htmlFor='confPass'>Confirm New Password</label>
|
<label htmlFor="confPass">Confirm New Password</label>
|
||||||
<input type='password' id='confPass' onChange={this.updVal}
|
<input
|
||||||
placeholder='Confirm new super secret password...' value={confPass}
|
type="password"
|
||||||
|
id="confPass"
|
||||||
|
onChange={this.updVal}
|
||||||
|
placeholder="Confirm new super secret password..."
|
||||||
|
value={confPass}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<button onClick={this.submit}
|
<button
|
||||||
|
onClick={this.submit}
|
||||||
className={'float-right' + (pending ? ' disabled' : '')}
|
className={'float-right' + (pending ? ' disabled' : '')}
|
||||||
>
|
>
|
||||||
{pending ? <Spinner /> : 'Submit'}
|
{pending ? <Spinner /> : 'Submit'}
|
||||||
@@ -93,7 +108,7 @@ class Settings extends Component {
|
|||||||
</form>
|
</form>
|
||||||
</PaddedRow>
|
</PaddedRow>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default connect(mapUser)(Settings);
|
export default connect(mapUser)(Settings)
|
||||||
|
|||||||
@@ -1,74 +1,82 @@
|
|||||||
import fetch from 'isomorphic-unfetch';
|
import fetch from 'isomorphic-unfetch'
|
||||||
import store from '../store';
|
import store from '../store'
|
||||||
import getUrl from '../../util/getUrl';
|
import getUrl from '../../util/getUrl'
|
||||||
|
|
||||||
// define action types
|
// define action types
|
||||||
export const SET_USER = 'SET_USER';
|
export const SET_USER = 'SET_USER'
|
||||||
export const LOGIN_PENDING = 'LOGIN_PENDING';
|
export const LOGIN_PENDING = 'LOGIN_PENDING'
|
||||||
export const LOGIN_FAILED = 'LOGIN_FAILED';
|
export const LOGIN_FAILED = 'LOGIN_FAILED'
|
||||||
export const LOGOUT = 'LOGOUT';
|
export const LOGOUT = 'LOGOUT'
|
||||||
|
|
||||||
export const setUser = user => {
|
export const setUser = user => {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: SET_USER,
|
type: SET_USER,
|
||||||
data: user
|
data: user,
|
||||||
});
|
})
|
||||||
}; // setUser
|
} // setUser
|
||||||
|
|
||||||
export const doLogout = () => {
|
export const doLogout = () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.localStorage.removeItem('jwt');
|
window.localStorage.removeItem('jwt')
|
||||||
document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;';
|
document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;'
|
||||||
}
|
}
|
||||||
store.dispatch({ type: LOGOUT });
|
store.dispatch({ type: LOGOUT })
|
||||||
}; // doLogout
|
} // doLogout
|
||||||
|
|
||||||
export async function doLogin(creds, jwt, noPend) {
|
export async function doLogin(creds, jwt, noPend) {
|
||||||
!noPend && store.dispatch({ type: LOGIN_PENDING });
|
!noPend && store.dispatch({ type: LOGIN_PENDING })
|
||||||
const authReqOpts = { method: 'POST', credentials: 'include' };
|
const authReqOpts = { method: 'POST', credentials: 'include' }
|
||||||
const authReqHead = { headers: jwt ? { Authorization: jwt } : {
|
const authReqHead = {
|
||||||
'Content-Type': 'application/json' }
|
headers: jwt
|
||||||
};
|
? { Authorization: jwt }
|
||||||
const authReqBody = jwt ? null : {
|
: {
|
||||||
body: JSON.stringify({...creds, strategy: 'local'})
|
'Content-Type': 'application/json',
|
||||||
};
|
},
|
||||||
const authReq = new Request(getUrl('auth'), {
|
|
||||||
...authReqOpts, ...authReqHead, ...authReqBody
|
|
||||||
});
|
|
||||||
const authRes = await fetch(authReq).catch(err => {
|
|
||||||
store.dispatch({ type: LOGIN_FAILED, data: err.message });
|
|
||||||
});
|
|
||||||
if(!authRes.ok) {
|
|
||||||
let error;
|
|
||||||
try {
|
|
||||||
error = await authRes.json();
|
|
||||||
error = error.message;
|
|
||||||
}
|
}
|
||||||
catch (err) {
|
const authReqBody = jwt
|
||||||
error = authRes.status === 429
|
? null
|
||||||
|
: {
|
||||||
|
body: JSON.stringify({ ...creds, strategy: 'local' }),
|
||||||
|
}
|
||||||
|
const authReq = new Request(getUrl('auth'), {
|
||||||
|
...authReqOpts,
|
||||||
|
...authReqHead,
|
||||||
|
...authReqBody,
|
||||||
|
})
|
||||||
|
const authRes = await fetch(authReq).catch(err => {
|
||||||
|
store.dispatch({ type: LOGIN_FAILED, data: err.message })
|
||||||
|
})
|
||||||
|
if (!authRes.ok) {
|
||||||
|
let error
|
||||||
|
try {
|
||||||
|
error = await authRes.json()
|
||||||
|
error = error.message
|
||||||
|
} catch (err) {
|
||||||
|
error =
|
||||||
|
authRes.status === 429
|
||||||
? 'Max login attempts reached'
|
? 'Max login attempts reached'
|
||||||
: 'An error occurred during login';
|
: 'An error occurred during login'
|
||||||
}
|
}
|
||||||
return store.dispatch({
|
return store.dispatch({
|
||||||
type: LOGIN_FAILED,
|
type: LOGIN_FAILED,
|
||||||
data: error
|
data: error,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
const { accessToken } = await authRes.json();
|
const { accessToken } = await authRes.json()
|
||||||
const payload = accessToken.split('.')[1];
|
const payload = accessToken.split('.')[1]
|
||||||
const { userId } = JSON.parse(atob(payload));
|
const { userId } = JSON.parse(atob(payload))
|
||||||
const userReq = new Request(getUrl(`/users/${userId}`), {
|
const userReq = new Request(getUrl(`/users/${userId}`), {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: accessToken
|
Authorization: accessToken,
|
||||||
}
|
},
|
||||||
});
|
})
|
||||||
const userRes = await fetch(userReq);
|
const userRes = await fetch(userReq)
|
||||||
if (!userRes.ok) {
|
if (!userRes.ok) {
|
||||||
return store.dispatch({
|
return store.dispatch({
|
||||||
type: LOGIN_FAILED,
|
type: LOGIN_FAILED,
|
||||||
data: 'failed to get user'
|
data: 'failed to get user',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
window.localStorage.setItem('jwt', accessToken);
|
window.localStorage.setItem('jwt', accessToken)
|
||||||
setUser(await userRes.json());
|
setUser(await userRes.json())
|
||||||
} // doLogin
|
} // doLogin
|
||||||
@@ -2,8 +2,8 @@ import {
|
|||||||
SET_USER,
|
SET_USER,
|
||||||
LOGIN_PENDING,
|
LOGIN_PENDING,
|
||||||
LOGIN_FAILED,
|
LOGIN_FAILED,
|
||||||
LOGOUT
|
LOGOUT,
|
||||||
} from '../actions/userAct';
|
} from '../actions/userAct'
|
||||||
|
|
||||||
const initState = {
|
const initState = {
|
||||||
setup: false,
|
setup: false,
|
||||||
@@ -11,35 +11,36 @@ const initState = {
|
|||||||
email: null,
|
email: null,
|
||||||
admin: null,
|
admin: null,
|
||||||
pending: false,
|
pending: false,
|
||||||
error: null
|
error: null,
|
||||||
};
|
}
|
||||||
|
|
||||||
function user(state = initState, action) {
|
function user(state = initState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SET_USER: {
|
case SET_USER: {
|
||||||
return {
|
return {
|
||||||
...initState,
|
...initState,
|
||||||
...action.data
|
...action.data,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
case LOGIN_PENDING: {
|
case LOGIN_PENDING: {
|
||||||
return {
|
return {
|
||||||
...initState,
|
...initState,
|
||||||
pending: true
|
pending: true,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
case LOGIN_FAILED: {
|
case LOGIN_FAILED: {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
pending: false,
|
pending: false,
|
||||||
error: action.data
|
error: action.data,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
case LOGOUT: {
|
case LOGOUT: {
|
||||||
return initState;
|
return initState
|
||||||
}
|
}
|
||||||
default: return state;
|
default:
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default user;
|
export default user
|
||||||
|
|||||||
@@ -1,23 +1,19 @@
|
|||||||
import {
|
import { applyMiddleware, combineReducers, createStore } from 'redux'
|
||||||
applyMiddleware,
|
|
||||||
combineReducers,
|
|
||||||
createStore
|
|
||||||
} from 'redux';
|
|
||||||
|
|
||||||
import user from './reducers/userRed';
|
import user from './reducers/userRed'
|
||||||
|
|
||||||
let middleware;
|
let middleware
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
const logger = require('redux-logger').default;
|
const logger = require('redux-logger').default
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
middleware = applyMiddleware(logger);
|
middleware = applyMiddleware(logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reducers = combineReducers({
|
const reducers = combineReducers({
|
||||||
user
|
user,
|
||||||
});
|
})
|
||||||
|
|
||||||
export default middleware
|
export default (middleware
|
||||||
? createStore(reducers, middleware)
|
? createStore(reducers, middleware)
|
||||||
: createStore(reducers);
|
: createStore(reducers))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Application hooks that run for every service
|
// Application hooks that run for every service
|
||||||
const logger = require('./hooks/logger');
|
const logger = require('./hooks/logger')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
before: {
|
before: {
|
||||||
@@ -9,7 +9,7 @@ module.exports = {
|
|||||||
create: [],
|
create: [],
|
||||||
update: [],
|
update: [],
|
||||||
patch: [],
|
patch: [],
|
||||||
remove: []
|
remove: [],
|
||||||
},
|
},
|
||||||
|
|
||||||
after: {
|
after: {
|
||||||
@@ -19,7 +19,7 @@ module.exports = {
|
|||||||
create: [],
|
create: [],
|
||||||
update: [],
|
update: [],
|
||||||
patch: [],
|
patch: [],
|
||||||
remove: []
|
remove: [],
|
||||||
},
|
},
|
||||||
|
|
||||||
error: {
|
error: {
|
||||||
@@ -29,6 +29,6 @@ module.exports = {
|
|||||||
create: [],
|
create: [],
|
||||||
update: [],
|
update: [],
|
||||||
patch: [],
|
patch: [],
|
||||||
remove: []
|
remove: [],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|||||||
181
src/app.js
181
src/app.js
@@ -1,131 +1,126 @@
|
|||||||
const favicon = require('serve-favicon');
|
const favicon = require('serve-favicon')
|
||||||
const fs = require('fs');
|
const fs = require('fs')
|
||||||
const path = require('path');
|
const path = require('path')
|
||||||
const compress = require('compression');
|
const compress = require('compression')
|
||||||
const cors = require('cors');
|
const cors = require('cors')
|
||||||
const helmet = require('helmet');
|
const helmet = require('helmet')
|
||||||
const cookieParser = require('cookie-parser')();
|
const cookieParser = require('cookie-parser')()
|
||||||
const RateLimit = require('express-rate-limit');
|
const RateLimit = require('express-rate-limit')
|
||||||
const trustIPs = require('./trustIPs');
|
const trustIPs = require('./trustIPs')
|
||||||
const logger = require('winston');
|
const logger = require('winston')
|
||||||
|
|
||||||
const feathers = require('@feathersjs/feathers');
|
const feathers = require('@feathersjs/feathers')
|
||||||
const express = require('@feathersjs/express');
|
const express = require('@feathersjs/express')
|
||||||
const configuration = require('@feathersjs/configuration');
|
const configuration = require('@feathersjs/configuration')
|
||||||
const hostConfig = require('../config/host.json');
|
const hostConfig = require('../config/host.json')
|
||||||
|
|
||||||
const middleware = require('./middleware');
|
const middleware = require('./middleware')
|
||||||
const services = require('./services');
|
const services = require('./services')
|
||||||
const appHooks = require('./app.hooks');
|
const appHooks = require('./app.hooks')
|
||||||
const channels = require('./channels');
|
const channels = require('./channels')
|
||||||
const authentication = require('./authentication');
|
const authentication = require('./authentication')
|
||||||
|
|
||||||
const dev = process.env.NODE_ENV !== 'production';
|
const dev = process.env.NODE_ENV !== 'production'
|
||||||
const basePath = require('../util/basePath');
|
const basePath = require('../util/basePath')
|
||||||
const stripBase = require('../util/stripBase');
|
const stripBase = require('../util/stripBase')
|
||||||
const getUrl = require('../util/getUrl');
|
const getUrl = require('../util/getUrl')
|
||||||
const { parse } = require('url');
|
const { parse } = require('url')
|
||||||
const nxt = require('next')({ dev, quiet: true });
|
const nxt = require('next')({ dev, quiet: true })
|
||||||
const nxtHandler = nxt.getRequestHandler();
|
const nxtHandler = nxt.getRequestHandler()
|
||||||
|
|
||||||
const app = express(feathers());
|
const app = express(feathers())
|
||||||
|
|
||||||
app.run = async port => {
|
app.run = async port => {
|
||||||
const server = app.listen(port);
|
const server = app.listen(port)
|
||||||
await nxt.prepare();
|
await nxt.prepare()
|
||||||
|
|
||||||
if (dev) {
|
if (dev) {
|
||||||
server.on('upgrade', (req, socket) => {
|
server.on('upgrade', (req, socket) => {
|
||||||
nxtHandler(req, socket, parse(stripBase(req.url), true));
|
nxtHandler(req, socket, parse(stripBase(req.url), true))
|
||||||
});
|
})
|
||||||
|
}
|
||||||
|
return server
|
||||||
}
|
}
|
||||||
return server;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load app configuration
|
// Load app configuration
|
||||||
app.configure(configuration());
|
app.configure(configuration())
|
||||||
|
|
||||||
// load host config
|
// load host config
|
||||||
Object.keys(hostConfig).forEach(key => (
|
Object.keys(hostConfig).forEach(key => app.set(key, hostConfig[key]))
|
||||||
app.set(key, hostConfig[key])
|
app.set('didSetup', false)
|
||||||
));
|
|
||||||
app.set('didSetup', false);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.statSync(path.join(__dirname, '..', 'db', '.didSetup'));
|
fs.statSync(path.join(__dirname, '..', 'db', '.didSetup'))
|
||||||
app.set('didSetup', true);
|
app.set('didSetup', true)
|
||||||
}
|
} catch (err) {
|
||||||
catch(err) {
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
req.doSetup = !app.get('didSetup');
|
req.doSetup = !app.get('didSetup')
|
||||||
next();
|
next()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const authLimit = new RateLimit({
|
const authLimit = new RateLimit({
|
||||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||||
max: 5, // 5 attempts then block
|
max: 5, // 5 attempts then block
|
||||||
delayAfter: 3, // slow down after 3 fails
|
delayAfter: 3, // slow down after 3 fails
|
||||||
delayMs: 2 * 1000
|
delayMs: 2 * 1000,
|
||||||
});
|
})
|
||||||
app.authLimit = authLimit;
|
app.authLimit = authLimit
|
||||||
app.use(getUrl('auth'), authLimit);
|
app.use(getUrl('auth'), authLimit)
|
||||||
app.patch(getUrl('users/*'), authLimit);
|
app.patch(getUrl('users/*'), authLimit)
|
||||||
|
|
||||||
// Enable CORS, security, compression, favicon and body parsing
|
// Enable CORS, security, compression, favicon and body parsing
|
||||||
trustIPs(app);
|
trustIPs(app)
|
||||||
app.use(cors());
|
app.use(cors())
|
||||||
app.use(helmet({
|
app.use(
|
||||||
hidePoweredBy: { setTo: 'hamsters' }
|
helmet({
|
||||||
}));
|
hidePoweredBy: { setTo: 'hamsters' },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
if(!dev) app.use(compress());
|
if (!dev) app.use(compress())
|
||||||
app.use(express.json()); // use { limit } option to increase max post size
|
app.use(express.json()) // use { limit } option to increase max post size
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }))
|
||||||
app.use(getUrl('/'), favicon('favicon.ico'));
|
app.use(getUrl('/'), favicon('favicon.ico'))
|
||||||
app.configure(express.rest()); // Set up Plugins and providers
|
app.configure(express.rest()) // Set up Plugins and providers
|
||||||
app.configure(middleware); // middleware/index.js
|
app.configure(middleware) // middleware/index.js
|
||||||
app.configure(authentication); // Set up authentication
|
app.configure(authentication) // Set up authentication
|
||||||
app.configure(services); // Set up our services (see `services/index.js`)
|
app.configure(services) // Set up our services (see `services/index.js`)
|
||||||
app.configure(channels); // Set up event channels (see channels.js)
|
app.configure(channels) // Set up event channels (see channels.js)
|
||||||
|
|
||||||
const checkJWT = async (req, res, next) => {
|
const checkJWT = async (req, res, next) => {
|
||||||
const result = await req.app.authenticate('jwt', {})(req);
|
const result = await req.app.authenticate('jwt', {})(req)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
req.jwt = req.cookies.jwt;
|
req.jwt = req.cookies.jwt
|
||||||
delete result.data.user.password;
|
delete result.data.user.password
|
||||||
req.user = result.data.user;
|
req.user = result.data.user
|
||||||
}
|
}
|
||||||
next();
|
next()
|
||||||
};
|
}
|
||||||
nxt.setAssetPrefix(basePath); // setup next.js routes
|
nxt.setAssetPrefix(basePath) // setup next.js routes
|
||||||
|
;['/', '/logout', '/new', '/settings'].forEach(route => {
|
||||||
[ '/', '/logout', '/new', '/settings' ]
|
|
||||||
.forEach(route => {
|
|
||||||
app.get(getUrl(route), cookieParser, checkJWT, (req, res) => {
|
app.get(getUrl(route), cookieParser, checkJWT, (req, res) => {
|
||||||
const { query } = parse(req.url, true);
|
const { query } = parse(req.url, true)
|
||||||
nxt.render(req, res, route, query);
|
nxt.render(req, res, route, query)
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
;['/k', '/edit'].forEach(route => {
|
||||||
[ '/k', '/edit' ]
|
|
||||||
.forEach(route => {
|
|
||||||
app.get(getUrl(route + '/:id'), cookieParser, checkJWT, (req, res) => {
|
app.get(getUrl(route + '/:id'), cookieParser, checkJWT, (req, res) => {
|
||||||
nxt.render(req, res, route, { id: req.params.id });
|
nxt.render(req, res, route, { id: req.params.id })
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
const notFound = express.notFound();
|
const notFound = express.notFound()
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
let accept = req.get('accept');
|
let accept = req.get('accept')
|
||||||
if (accept && accept.toLowerCase() === 'application/json')
|
if (accept && accept.toLowerCase() === 'application/json')
|
||||||
return notFound(req, res, next);
|
return notFound(req, res, next)
|
||||||
if (req.url.substr(0, basePath.length) !== basePath)
|
if (req.url.substr(0, basePath.length) !== basePath)
|
||||||
return nxt.render404(req, res);
|
return nxt.render404(req, res)
|
||||||
nxtHandler(req, res, parse(stripBase(req.url), true));
|
nxtHandler(req, res, parse(stripBase(req.url), true))
|
||||||
});
|
})
|
||||||
|
|
||||||
app.use(express.errorHandler({ logger }));
|
app.use(express.errorHandler({ logger }))
|
||||||
app.hooks(appHooks);
|
app.hooks(appHooks)
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
const authentication = require('@feathersjs/authentication');
|
const authentication = require('@feathersjs/authentication')
|
||||||
const jwt = require('@feathersjs/authentication-jwt');
|
const jwt = require('@feathersjs/authentication-jwt')
|
||||||
const local = require('@feathersjs/authentication-local');
|
const local = require('@feathersjs/authentication-local')
|
||||||
const getUrl = require('../util/getUrl');
|
const getUrl = require('../util/getUrl')
|
||||||
|
|
||||||
module.exports = function(app) {
|
module.exports = function(app) {
|
||||||
const config = app.get('authentication');
|
const config = app.get('authentication')
|
||||||
config.path = getUrl(config.path);
|
config.path = getUrl(config.path)
|
||||||
config.service = getUrl('users');
|
config.service = getUrl('users')
|
||||||
|
|
||||||
// Set up authentication with the secret
|
// Set up authentication with the secret
|
||||||
app.configure(authentication(
|
app.configure(
|
||||||
|
authentication(
|
||||||
Object.assign({}, config, {
|
Object.assign({}, config, {
|
||||||
cookie: {
|
cookie: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
secure: false,
|
secure: false,
|
||||||
name: 'jwt',
|
name: 'jwt',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
));
|
)
|
||||||
app.configure(jwt());
|
)
|
||||||
app.configure(local());
|
app.configure(jwt())
|
||||||
|
app.configure(local())
|
||||||
|
|
||||||
// The `authentication` service is used to create a JWT.
|
// The `authentication` service is used to create a JWT.
|
||||||
// The before `create` hook registers strategies that can be used
|
// The before `create` hook registers strategies that can be used
|
||||||
@@ -30,13 +32,11 @@ module.exports = function (app) {
|
|||||||
create: [
|
create: [
|
||||||
authentication.hooks.authenticate(config.strategies),
|
authentication.hooks.authenticate(config.strategies),
|
||||||
ctx => {
|
ctx => {
|
||||||
ctx.app.authLimit.resetKey(ctx.params.ip);
|
ctx.app.authLimit.resetKey(ctx.params.ip)
|
||||||
return ctx;
|
return ctx
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
remove: [
|
remove: [authentication.hooks.authenticate('jwt')],
|
||||||
authentication.hooks.authenticate('jwt')
|
},
|
||||||
]
|
})
|
||||||
}
|
}
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
module.exports = function(app) {
|
module.exports = function(app) {
|
||||||
if (typeof app.channel !== 'function') {
|
if (typeof app.channel !== 'function') {
|
||||||
// If no real-time functionality has been configured just return
|
// If no real-time functionality has been configured just return
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.on('connection', connection => {
|
app.on('connection', connection => {
|
||||||
// On a new real-time connection, add it to the anonymous channel
|
// On a new real-time connection, add it to the anonymous channel
|
||||||
app.channel('anonymous').join(connection);
|
app.channel('anonymous').join(connection)
|
||||||
});
|
})
|
||||||
|
|
||||||
app.on('login', (authResult, { connection }) => {
|
app.on('login', (authResult, { connection }) => {
|
||||||
// connection can be undefined if there is no
|
// connection can be undefined if there is no
|
||||||
@@ -17,10 +17,10 @@ module.exports = function(app) {
|
|||||||
// const user = connection.user;
|
// const user = connection.user;
|
||||||
|
|
||||||
// The connection is no longer anonymous, remove it
|
// The connection is no longer anonymous, remove it
|
||||||
app.channel('anonymous').leave(connection);
|
app.channel('anonymous').leave(connection)
|
||||||
|
|
||||||
// Add it to the authenticated user channel
|
// Add it to the authenticated user channel
|
||||||
app.channel('authenticated').join(connection);
|
app.channel('authenticated').join(connection)
|
||||||
|
|
||||||
// Channels can be named anything and joined on any condition
|
// Channels can be named anything and joined on any condition
|
||||||
|
|
||||||
@@ -34,18 +34,21 @@ module.exports = function(app) {
|
|||||||
// app.channel(`emails/${user.email}`).join(channel);
|
// app.channel(`emails/${user.email}`).join(channel);
|
||||||
// app.channel(`userIds/$(user.id}`).join(channel);
|
// app.channel(`userIds/$(user.id}`).join(channel);
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
app.publish((data, hook) => {
|
app.publish((data, hook) => {
|
||||||
// Here you can add event publishers to channels set up in `channels.js`
|
// Here you can add event publishers to channels set up in `channels.js`
|
||||||
// To publish only for a specific event use `app.publish(eventname, () => {})`
|
// To publish only for a specific event use `app.publish(eventname, () => {})`
|
||||||
|
|
||||||
console.log('Publishing all events to all authenticated users. See `channels.js` and https://docs.feathersjs.com/api/channels.html for more information.'); // eslint-disable-line
|
// eslint-disable-next-line
|
||||||
|
console.log(
|
||||||
|
'Publishing all events to all authenticated users. See `channels.js` and https://docs.feathersjs.com/api/channels.html for more information.'
|
||||||
|
)
|
||||||
|
|
||||||
// e.g. to publish all service events to all authenticated users use
|
// e.g. to publish all service events to all authenticated users use
|
||||||
return app.channel('authenticated');
|
return app.channel('authenticated')
|
||||||
});
|
})
|
||||||
|
|
||||||
// Here you can also add service specific event publishers
|
// Here you can also add service specific event publishers
|
||||||
// e..g the publish the `users` service `created` event to the `admins` channel
|
// e..g the publish the `users` service `created` event to the `admins` channel
|
||||||
@@ -58,4 +61,4 @@ module.exports = function(app) {
|
|||||||
// app.channel(`emails/${data.recipientEmail}`)
|
// app.channel(`emails/${data.recipientEmail}`)
|
||||||
// ];
|
// ];
|
||||||
// });
|
// });
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// A hook that logs service method before, after and error
|
// A hook that logs service method before, after and error
|
||||||
// See https://github.com/winstonjs/winston for documentation
|
// See https://github.com/winstonjs/winston for documentation
|
||||||
// about the logger.
|
// about the logger.
|
||||||
const logger = require('winston');
|
const logger = require('winston')
|
||||||
|
|
||||||
// To see more detailed messages, uncomment the following line
|
// To see more detailed messages, uncomment the following line
|
||||||
// logger.level = 'debug';
|
// logger.level = 'debug';
|
||||||
@@ -10,14 +10,16 @@ module.exports = function () {
|
|||||||
return context => {
|
return context => {
|
||||||
// This debugs the service call and a stringified version of the hook context
|
// This debugs the service call and a stringified version of the hook context
|
||||||
// You can customize the mssage (and logger) to your needs
|
// You can customize the mssage (and logger) to your needs
|
||||||
logger.debug(`${context.type} app.service('${context.path}').${context.method}()`);
|
logger.debug(
|
||||||
|
`${context.type} app.service('${context.path}').${context.method}()`
|
||||||
|
)
|
||||||
|
|
||||||
if (typeof context.toJSON === 'function') {
|
if (typeof context.toJSON === 'function') {
|
||||||
logger.debug('Hook Context', JSON.stringify(context, null, ' '));
|
logger.debug('Hook Context', JSON.stringify(context, null, ' '))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.error) {
|
if (context.error) {
|
||||||
logger.error(context.error);
|
logger.error(context.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const logger = require('winston');
|
const logger = require('winston')
|
||||||
const app = require('./app');
|
const app = require('./app')
|
||||||
const port = app.get('port');
|
const port = app.get('port')
|
||||||
|
|
||||||
app.run(port).then(() => {
|
app.run(port).then(() => {
|
||||||
logger.info('MYKB listening at http://%s:%d', app.get('host'), port)
|
logger.info('MYKB listening at http://%s:%d', app.get('host'), port)
|
||||||
@@ -8,4 +8,4 @@ app.run(port).then(() => {
|
|||||||
|
|
||||||
process.on('unhandledRejection', (reason, p) =>
|
process.on('unhandledRejection', (reason, p) =>
|
||||||
logger.error('Unhandled Rejection at: Promise ', p, reason)
|
logger.error('Unhandled Rejection at: Promise ', p, reason)
|
||||||
);
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ module.exports = function (app) {
|
|||||||
|
|
||||||
// add req.ip to feathers
|
// add req.ip to feathers
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
req.feathers.ip = req.ip;
|
req.feathers.ip = req.ip
|
||||||
next();
|
next()
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
const NeDB = require('nedb');
|
const NeDB = require('nedb')
|
||||||
const path = require('path');
|
const path = require('path')
|
||||||
|
|
||||||
module.exports = function(app) {
|
module.exports = function(app) {
|
||||||
const dbPath = app.get('nedb');
|
const dbPath = app.get('nedb')
|
||||||
const Model = new NeDB({
|
const Model = new NeDB({
|
||||||
filename: path.join(dbPath, 'users.db'),
|
filename: path.join(dbPath, 'users.db'),
|
||||||
autoload: true
|
autoload: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
Model.ensureIndex({ fieldName: 'email', unique: true });
|
Model.ensureIndex({ fieldName: 'email', unique: true })
|
||||||
|
|
||||||
return Model;
|
return Model
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,225 +1,233 @@
|
|||||||
const errors = require('@feathersjs/errors');
|
const errors = require('@feathersjs/errors')
|
||||||
const gitP = require('simple-git/promise');
|
const gitP = require('simple-git/promise')
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra')
|
||||||
const path = require('path');
|
const path = require('path')
|
||||||
const loadDocs = require('./loadDocs');
|
const loadDocs = require('./loadDocs')
|
||||||
const comparableFields = [ 'name', 'dir' ];
|
const comparableFields = ['name', 'dir']
|
||||||
let git;
|
let git
|
||||||
|
|
||||||
class Service {
|
class Service {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.options = options || {};
|
this.options = options || {}
|
||||||
this.docsDir = this.options.docsDir;
|
this.docsDir = this.options.docsDir
|
||||||
this.$limit = this.options.paginate.default;
|
this.$limit = this.options.paginate.default
|
||||||
this.useGit = this.options.useGit;
|
this.useGit = this.options.useGit
|
||||||
this.maxDocSize = 1024 * 100; // 100 kb (don't try sending if bigger)
|
this.maxDocSize = 1024 * 100 // 100 kb (don't try reading if bigger)
|
||||||
this.docs = {};
|
this.docs = {}
|
||||||
this.updTimeouts = {};
|
this.updTimeouts = {}
|
||||||
loadDocs.bind(this)();
|
loadDocs.bind(this)()
|
||||||
|
|
||||||
if (this.useGit) {
|
if (this.useGit) {
|
||||||
git = gitP(this.docsDir);
|
git = gitP(this.docsDir)
|
||||||
fs.stat(path.join(this.docsDir, '.git'), async err => {
|
fs.stat(path.join(this.docsDir, '.git'), async err => {
|
||||||
if (err && err.code === 'ENOENT') {
|
if (err && err.code === 'ENOENT') {
|
||||||
git.init().then(() => {
|
git.init().then(() => {
|
||||||
if(this.numInitDocs === 0) return;
|
if (this.numInitDocs === 0) return
|
||||||
git.add('./*').then(() => git.commit('initial commit'));
|
git.add('./*').then(() => git.commit('initial commit'))
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMd(id) {
|
async getMd(id) {
|
||||||
const doc = this.docs[id];
|
const doc = this.docs[id]
|
||||||
if(doc.md) return doc.md;
|
if (doc.md) return doc.md
|
||||||
const docPath = path.join(this.docsDir, doc.dir, doc.name);
|
const docPath = path.join(this.docsDir, doc.dir, doc.name)
|
||||||
const { size } = await fs.stat(docPath);
|
const { size } = await fs.stat(docPath)
|
||||||
if(size > this.maxDocSize) return 'Document is too big to display...';
|
if (size > this.maxDocSize) return 'Document is too big to display...'
|
||||||
const buff = await fs.readFile(docPath);
|
const buff = await fs.readFile(docPath)
|
||||||
return buff.toString();
|
return buff.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
async isMatch(id, $search) {
|
async isMatch(id, $search) {
|
||||||
const doc = this.docs[id];
|
const doc = this.docs[id]
|
||||||
const name = doc.name.toLowerCase();
|
const name = doc.name.toLowerCase()
|
||||||
|
|
||||||
if(name.indexOf($search) > -1) return true;
|
if (name.indexOf($search) > -1) return true
|
||||||
const dir = doc.dir.toLowerCase();
|
const dir = doc.dir.toLowerCase()
|
||||||
|
|
||||||
if(dir.indexOf($search) > -1) return true;
|
if (dir.indexOf($search) > -1) return true
|
||||||
const relPath = dir + (dir.length > 0 ? '/' : '') + name;
|
const relPath = dir + (dir.length > 0 ? '/' : '') + name
|
||||||
|
|
||||||
if(relPath.toLowerCase().indexOf($search) > -1) return true;
|
if (relPath.toLowerCase().indexOf($search) > -1) return true
|
||||||
let md = await this.getMd(id);
|
let md = await this.getMd(id)
|
||||||
md = md.toLowerCase();
|
md = md.toLowerCase()
|
||||||
|
|
||||||
if(md.indexOf($search) > -1) return true;
|
if (md.indexOf($search) > -1) return true
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkRmDir(dir) {
|
async checkRmDir(dir) {
|
||||||
if(dir === '') return;
|
if (dir === '') return
|
||||||
const parts = dir.split('/');
|
const parts = dir.split('/')
|
||||||
const n = parts.length;
|
const n = parts.length
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const dir = parts.filter((p, pIdx) => pIdx < (n - i)).join('/');
|
const dir = parts.filter((p, pIdx) => pIdx < n - i).join('/')
|
||||||
const docsInDir = await this.find({ query: { dir } });
|
const docsInDir = await this.find({ query: { dir } })
|
||||||
if (docsInDir.total === 0) {
|
if (docsInDir.total === 0) {
|
||||||
try {
|
try {
|
||||||
await fs.rmdir(path.join(this.docsDir, dir));
|
await fs.rmdir(path.join(this.docsDir, dir))
|
||||||
} catch(err) { return; }
|
} catch (err) {
|
||||||
} else return;
|
return
|
||||||
|
}
|
||||||
|
} else return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async find(params) {
|
async find(params) {
|
||||||
const { query } = params;
|
const { query } = params
|
||||||
let { $limit, $skip, $search, $select, $sort } = query;
|
let { $limit, $skip, $search, $select, $sort } = query
|
||||||
if($skip < 0) $skip = 0;
|
if ($skip < 0) $skip = 0
|
||||||
$limit = $limit || this.$limit;
|
$limit = $limit || this.$limit
|
||||||
if ($search) {
|
if ($search) {
|
||||||
$search = $search.toLowerCase().trim();
|
$search = $search.toLowerCase().trim()
|
||||||
}
|
}
|
||||||
let ids = Object.keys(this.docs);
|
let ids = Object.keys(this.docs)
|
||||||
const data = [];
|
const data = []
|
||||||
const toComp = comparableFields.filter(f => typeof query[f] !== 'undefined');
|
const toComp = comparableFields.filter(f => typeof query[f] !== 'undefined')
|
||||||
if (toComp.length > 0) {
|
if (toComp.length > 0) {
|
||||||
ids = ids.filter(id => {
|
ids = ids.filter(id => {
|
||||||
const doc = this.docs[id];
|
const doc = this.docs[id]
|
||||||
return !toComp.some(f => doc[f] !== query[f]);
|
return !toComp.some(f => doc[f] !== query[f])
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
if ($search) {
|
if ($search) {
|
||||||
ids = await Promise.all(
|
ids = await Promise.all(
|
||||||
ids.map(async id => await this.isMatch(id, $search) ? id : null)
|
ids.map(async id => ((await this.isMatch(id, $search)) ? id : null))
|
||||||
);
|
)
|
||||||
ids = ids.filter(Boolean);
|
ids = ids.filter(Boolean)
|
||||||
}
|
}
|
||||||
if(ids.length === 0) return { total: 0, data };
|
if (ids.length === 0) return { total: 0, data }
|
||||||
if ($sort) {
|
if ($sort) {
|
||||||
const sortKey = Object.keys($sort).pop();
|
const sortKey = Object.keys($sort).pop()
|
||||||
const ascDesc = $sort[sortKey];
|
const ascDesc = $sort[sortKey]
|
||||||
if(this.docs[ids[0]][sortKey] || sortKey === 'dirName') { // make sure valid key
|
|
||||||
|
if (this.docs[ids[0]][sortKey] || sortKey === 'dirName') {
|
||||||
ids.sort((a, b) => {
|
ids.sort((a, b) => {
|
||||||
let val1, val2, parseVals;
|
a = this.docs[a]
|
||||||
const docA = this.docs[a],
|
b = this.docs[b]
|
||||||
docB = this.docs[b];
|
let parseVal
|
||||||
if (sortKey === 'dirName') {
|
if (sortKey === 'dirName') {
|
||||||
parseVals = doc => path.join(doc.dir, doc.name);
|
parseVal = doc => path.join(doc.dir, doc.name)
|
||||||
|
} else {
|
||||||
|
parseVal = doc => doc[sortKey]
|
||||||
}
|
}
|
||||||
else {
|
a = parseVal(a)
|
||||||
parseVals = doc => doc[sortKey];
|
b = parseVal(b)
|
||||||
}
|
let c = 0
|
||||||
[val1, val2] = [docA, docB].map(parseVals);
|
if (a < b) c = -1
|
||||||
let c = 0;
|
else if (a > b) c = 1
|
||||||
if(val1 < val2) c = -1;
|
return c * ascDesc
|
||||||
else if(val1 > val2) c = 1;
|
})
|
||||||
return c * ascDesc;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let i = $skip || 0; i < ids.length && data.length < $limit; i++) {
|
for (let i = $skip || 0; i < ids.length && data.length < $limit; i++) {
|
||||||
const id = ids[i];
|
const id = ids[i]
|
||||||
let doc = $select && !$select.md ? this.docs[id] : this.get(id);
|
let doc = $select && !$select.md ? this.docs[id] : this.get(id)
|
||||||
if ($select) {
|
if ($select) {
|
||||||
let _doc = {};
|
let _doc = {}
|
||||||
$select.forEach(k => _doc[k] = doc[k]);
|
$select.forEach(k => (_doc[k] = doc[k]))
|
||||||
doc = _doc;
|
doc = _doc
|
||||||
}
|
}
|
||||||
data.push(doc);
|
data.push(doc)
|
||||||
}
|
}
|
||||||
return { total: ids.length, data };
|
return { total: ids.length, data }
|
||||||
}
|
}
|
||||||
|
|
||||||
async get (id, params) { // eslint-disable-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const doc = this.docs[id];
|
async get(id, params) {
|
||||||
if(!doc) throw new errors.NotFound(`No record found for id '${id}'`);
|
const doc = this.docs[id]
|
||||||
if(!doc.md) doc.md = await this.getMd(id);
|
if (!doc) throw new errors.NotFound(`No record found for id '${id}'`)
|
||||||
return this.docs[id];
|
if (!doc.md) doc.md = await this.getMd(id)
|
||||||
|
return this.docs[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
async create (data, params) { // eslint-disable-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
async create(data, params) {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
return await Promise.all(data.map(current => this.create(current)));
|
return await Promise.all(data.map(current => this.create(current)))
|
||||||
}
|
}
|
||||||
const { name, dir, md } = data;
|
const { name, dir, md } = data
|
||||||
try {
|
try {
|
||||||
const rPath = path.join(dir, name);
|
const rPath = path.join(dir, name)
|
||||||
const docPath = path.join(this.docsDir, rPath);
|
const docPath = path.join(this.docsDir, rPath)
|
||||||
await fs.outputFile(docPath, md);
|
await fs.outputFile(docPath, md)
|
||||||
|
|
||||||
if (this.useGit) {
|
if (this.useGit) {
|
||||||
git.add(rPath).then(() => git.commit(`added doc ${rPath}`));
|
git.add(rPath).then(() => git.commit(`added doc ${rPath}`))
|
||||||
}
|
}
|
||||||
const ts = new Date().toJSON();
|
const ts = new Date().toJSON()
|
||||||
return this.setDoc(path.join(dir, name), ts, md, ts);
|
return this.setDoc(path.join(dir, name), ts, md, ts)
|
||||||
}
|
} catch (err) {
|
||||||
catch (err) {
|
throw new errors.GeneralError('could not create doc')
|
||||||
throw new errors.GeneralError('could not create doc');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async update (id, data, params) { // eslint-disable-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
throw new errors.MethodNotAllowed('can not update on docs service');
|
async update(id, data, params) {
|
||||||
|
throw new errors.MethodNotAllowed('can not update on docs service')
|
||||||
}
|
}
|
||||||
|
|
||||||
async patch (id, data, params) { // eslint-disable-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const doc = this.docs[id];
|
async patch(id, data, params) {
|
||||||
if(!doc) throw new errors.NotFound(`No record found for id '${id}'`);
|
const doc = this.docs[id]
|
||||||
let { name, dir, md } = data;
|
if (!doc) throw new errors.NotFound(`No record found for id '${id}'`)
|
||||||
let diffDir = Boolean(doc.dir !== dir);
|
let { name, dir, md } = data
|
||||||
if(!name) name = doc.name;
|
let diffDir = Boolean(doc.dir !== dir)
|
||||||
|
if (!name) name = doc.name
|
||||||
if (typeof dir !== 'string') {
|
if (typeof dir !== 'string') {
|
||||||
dir = doc.dir;
|
dir = doc.dir
|
||||||
diffDir = false;
|
diffDir = false
|
||||||
}
|
}
|
||||||
const rPath = path.join(dir, name);
|
const rPath = path.join(dir, name)
|
||||||
const docPath = path.join(this.docsDir, rPath);
|
const docPath = path.join(this.docsDir, rPath)
|
||||||
if (name !== doc.name || diffDir) {
|
if (name !== doc.name || diffDir) {
|
||||||
const oldRPath = path.join(doc.dir, doc.name);
|
const oldRPath = path.join(doc.dir, doc.name)
|
||||||
const oldPath = path.join(this.docsDir, oldRPath);
|
const oldPath = path.join(this.docsDir, oldRPath)
|
||||||
await fs.ensureDir(path.join(this.docsDir, dir));
|
await fs.ensureDir(path.join(this.docsDir, dir))
|
||||||
await fs.move(oldPath, docPath);
|
await fs.move(oldPath, docPath)
|
||||||
|
|
||||||
if (this.useGit) {
|
if (this.useGit) {
|
||||||
git.rm(oldRPath)
|
git
|
||||||
|
.rm(oldRPath)
|
||||||
.then(() => git.add(rPath))
|
.then(() => git.add(rPath))
|
||||||
.then(() => git.commit(`renamed doc ${oldRPath} ${rPath}`));
|
.then(() => git.commit(`renamed doc ${oldRPath} ${rPath}`))
|
||||||
}
|
}
|
||||||
id = this.getId(path.join(dir, name));
|
id = this.getId(path.join(dir, name))
|
||||||
this.docs[id] = Object.assign({}, doc, { id, name, dir });
|
this.docs[id] = Object.assign({}, doc, { id, name, dir })
|
||||||
delete this.docs[doc.id];
|
delete this.docs[doc.id]
|
||||||
if(diffDir) this.checkRmDir(doc.dir);
|
if (diffDir) this.checkRmDir(doc.dir)
|
||||||
}
|
}
|
||||||
if (md) {
|
if (md) {
|
||||||
await fs.writeFile(docPath, md);
|
await fs.writeFile(docPath, md)
|
||||||
if (this.useGit) {
|
if (this.useGit) {
|
||||||
git.add(rPath).then(() => git.commit(`updated doc ${rPath}`));
|
git.add(rPath).then(() => git.commit(`updated doc ${rPath}`))
|
||||||
}
|
}
|
||||||
this.docs[id].md = md;
|
this.docs[id].md = md
|
||||||
}
|
}
|
||||||
return this.docs[id];
|
return this.docs[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove (id, params) { // eslint-disable-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const doc = this.docs[id];
|
async remove(id, params) {
|
||||||
if(!id) throw new errors.NotFound(`No record found for id '${id}'`);
|
const doc = this.docs[id]
|
||||||
const rPath = path.join(doc.dir, doc.name);
|
if (!id) throw new errors.NotFound(`No record found for id '${id}'`)
|
||||||
const docPath = path.join(this.docsDir, rPath);
|
const rPath = path.join(doc.dir, doc.name)
|
||||||
await fs.unlink(docPath);
|
const docPath = path.join(this.docsDir, rPath)
|
||||||
|
await fs.unlink(docPath)
|
||||||
|
|
||||||
if (this.useGit) {
|
if (this.useGit) {
|
||||||
git.rm(rPath).then(() => git.commit(`removed doc ${rPath}`));
|
git.rm(rPath).then(() => git.commit(`removed doc ${rPath}`))
|
||||||
}
|
}
|
||||||
delete this.docs[id];
|
delete this.docs[id]
|
||||||
this.checkRmDir(doc.dir);
|
this.checkRmDir(doc.dir)
|
||||||
return doc;
|
return doc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function(options) {
|
module.exports = function(options) {
|
||||||
return new Service(options);
|
return new Service(options)
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports.Service = Service;
|
module.exports.Service = Service
|
||||||
|
|||||||
@@ -1,72 +1,81 @@
|
|||||||
const { authenticate } = require('@feathersjs/authentication').hooks;
|
const { authenticate } = require('@feathersjs/authentication').hooks
|
||||||
const { checkDir, checkName } = require('../../../util/checkDirParts');
|
const { checkDir, checkName } = require('../../../util/checkDirParts')
|
||||||
const { disable, invalid, adminOnly } = require('../hooksUtil');
|
const { disable, invalid, adminOnly } = require('../hooksUtil')
|
||||||
const getUrl = require('../../../util/getUrl');
|
const getUrl = require('../../../util/getUrl')
|
||||||
const nameIsValid = name => {
|
const nameIsValid = name => {
|
||||||
name = checkName(name);
|
name = checkName(name)
|
||||||
if(!name) return invalid('name');
|
if (!name) return invalid('name')
|
||||||
if (name.substr(name.length - 3).toLowerCase() !== '.md') {
|
if (name.substr(name.length - 3).toLowerCase() !== '.md') {
|
||||||
name += '.md';
|
name += '.md'
|
||||||
|
}
|
||||||
|
return name
|
||||||
}
|
}
|
||||||
return name;
|
|
||||||
};
|
|
||||||
const dirIsValid = dir => {
|
const dirIsValid = dir => {
|
||||||
dir = checkDir(dir);
|
dir = checkDir(dir)
|
||||||
if(!dir && dir !== 0) return invalid('dir');
|
if (!dir && dir !== 0) return invalid('dir')
|
||||||
else if(dir === 0) return '';
|
else if (dir === 0) return ''
|
||||||
return dir;
|
return dir
|
||||||
};
|
}
|
||||||
const mdIsValid = md => {
|
const mdIsValid = md => {
|
||||||
if (typeof md !== 'string' || md.trim().length === 0) {
|
if (typeof md !== 'string' || md.trim().length === 0) {
|
||||||
return invalid('md');
|
return invalid('md')
|
||||||
|
}
|
||||||
|
return md
|
||||||
}
|
}
|
||||||
return md;
|
|
||||||
};
|
|
||||||
const pathTaken = async (name, dir, app) => {
|
const pathTaken = async (name, dir, app) => {
|
||||||
const matches = await app.service(getUrl('docs')).find({ query: { name, dir } });
|
const matches = await app
|
||||||
|
.service(getUrl('docs'))
|
||||||
|
.find({ query: { name, dir } })
|
||||||
if (matches.total > 0) {
|
if (matches.total > 0) {
|
||||||
return invalid(null, 'filename is taken');
|
return invalid(null, 'filename is taken')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
before: {
|
before: {
|
||||||
all: [authenticate('jwt')],
|
all: [authenticate('jwt')],
|
||||||
find: [],
|
find: [],
|
||||||
get: [],
|
get: [],
|
||||||
create: [ async ctx => {
|
create: [
|
||||||
const { app, data } = ctx;
|
async ctx => {
|
||||||
let { name, dir, md } = data;
|
const { app, data } = ctx
|
||||||
const k = {};
|
let { name, dir, md } = data
|
||||||
k.name = nameIsValid(name);
|
const k = {}
|
||||||
k.dir = dirIsValid(dir);
|
k.name = nameIsValid(name)
|
||||||
k.md = mdIsValid(md);
|
k.dir = dirIsValid(dir)
|
||||||
await pathTaken(k.name, k.dir, app);
|
k.md = mdIsValid(md)
|
||||||
ctx.data = k;
|
await pathTaken(k.name, k.dir, app)
|
||||||
return ctx;
|
ctx.data = k
|
||||||
}],
|
return ctx
|
||||||
|
},
|
||||||
|
],
|
||||||
update: [disable],
|
update: [disable],
|
||||||
patch: [ async ctx => {
|
patch: [
|
||||||
const { data, app } = ctx;
|
async ctx => {
|
||||||
const { name, dir, md } = data;
|
const { data, app } = ctx
|
||||||
const k = {};
|
const { name, dir, md } = data
|
||||||
if(name) k.name = nameIsValid(name);
|
const k = {}
|
||||||
if(typeof dir === 'string') k.dir = dirIsValid(dir); // allow empty string
|
if (name) k.name = nameIsValid(name)
|
||||||
|
if (typeof dir === 'string') k.dir = dirIsValid(dir) // allow empty string
|
||||||
if (name || typeof dir === 'string') {
|
if (name || typeof dir === 'string') {
|
||||||
let checkName, checkDir;
|
let checkName, checkDir
|
||||||
if (!name || typeof dir !== 'string') {
|
if (!name || typeof dir !== 'string') {
|
||||||
const doc = await app.service(getUrl('docs')).get(ctx.id);
|
const doc = await app.service(getUrl('docs')).get(ctx.id)
|
||||||
if(!name) checkName = doc.name;
|
if (!name) checkName = doc.name
|
||||||
if(typeof dir !== 'string') checkDir = doc.dir;
|
if (typeof dir !== 'string') checkDir = doc.dir
|
||||||
}
|
}
|
||||||
await pathTaken(k.name || checkName,
|
await pathTaken(
|
||||||
typeof k.dir === 'string' ? k.dir : checkDir, app);
|
k.name || checkName,
|
||||||
|
typeof k.dir === 'string' ? k.dir : checkDir,
|
||||||
|
app
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if(md) k.md = mdIsValid(md);
|
if (md) k.md = mdIsValid(md)
|
||||||
if(Object.keys(k).length === 0) invalid(null, 'nothing to update');
|
if (Object.keys(k).length === 0) invalid(null, 'nothing to update')
|
||||||
ctx.data = k;
|
ctx.data = k
|
||||||
return ctx;
|
return ctx
|
||||||
}],
|
},
|
||||||
remove: [ adminOnly ]
|
],
|
||||||
|
remove: [adminOnly],
|
||||||
},
|
},
|
||||||
|
|
||||||
after: {
|
after: {
|
||||||
@@ -76,7 +85,7 @@ module.exports = {
|
|||||||
create: [],
|
create: [],
|
||||||
update: [],
|
update: [],
|
||||||
patch: [],
|
patch: [],
|
||||||
remove: []
|
remove: [],
|
||||||
},
|
},
|
||||||
|
|
||||||
error: {
|
error: {
|
||||||
@@ -86,6 +95,6 @@ module.exports = {
|
|||||||
create: [],
|
create: [],
|
||||||
update: [],
|
update: [],
|
||||||
patch: [],
|
patch: [],
|
||||||
remove: []
|
remove: [],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
// Initializes the `docs` service on path `/docs`
|
// Initializes the `docs` service on path `/docs`
|
||||||
const createService = require('./docs.class.js');
|
const createService = require('./docs.class.js')
|
||||||
const hooks = require('./docs.hooks');
|
const hooks = require('./docs.hooks')
|
||||||
const getUrl = require('../../../util/getUrl');
|
const getUrl = require('../../../util/getUrl')
|
||||||
|
|
||||||
module.exports = function(app) {
|
module.exports = function(app) {
|
||||||
|
const paginate = app.get('paginate')
|
||||||
const paginate = app.get('paginate');
|
const docsDir = app.get('docsDir')
|
||||||
const docsDir = app.get('docsDir');
|
const cacheSize = app.get('cacheSize')
|
||||||
const cacheSize = app.get('cacheSize');
|
const useGit = app.get('useGit')
|
||||||
const useGit = app.get('useGit');
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
name: 'docs',
|
name: 'docs',
|
||||||
paginate,
|
paginate,
|
||||||
docsDir,
|
docsDir,
|
||||||
cacheSize,
|
cacheSize,
|
||||||
useGit
|
useGit,
|
||||||
};
|
}
|
||||||
const url = getUrl('docs');
|
const url = getUrl('docs')
|
||||||
|
|
||||||
// Initialize our service with any options it requires
|
// Initialize our service with any options it requires
|
||||||
app.use(url, createService(options));
|
app.use(url, createService(options))
|
||||||
|
|
||||||
// Get our initialized service so that we can register hooks and filters
|
// Get our initialized service so that we can register hooks and filters
|
||||||
const service = app.service(url);
|
const service = app.service(url)
|
||||||
|
|
||||||
service.hooks(hooks);
|
service.hooks(hooks)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,64 +1,78 @@
|
|||||||
const chokidar = require('chokidar');
|
const chokidar = require('chokidar')
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra')
|
||||||
const path = require('path');
|
const path = require('path')
|
||||||
const glob = require('glob');
|
const glob = require('glob')
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto')
|
||||||
|
|
||||||
async function loadDocs() {
|
async function loadDocs() {
|
||||||
const { docsDir, cacheSize } = this.options;
|
const { docsDir, cacheSize } = this.options
|
||||||
this.numInitDocs = glob.sync(path.join(docsDir, '**/*.md')).length;
|
this.numInitDocs = glob.sync(path.join(docsDir, '**/*.md')).length
|
||||||
this.cached = 0;
|
this.cached = 0
|
||||||
this.loaded = false;
|
this.loaded = false
|
||||||
|
|
||||||
const getId = relPath => (crypto.createHash('sha1')
|
const getId = relPath =>
|
||||||
.update(relPath).digest().toString('base64')
|
crypto
|
||||||
.substr(0, 16).split('/').join('_')
|
.createHash('sha1')
|
||||||
);
|
.update(relPath)
|
||||||
this.getId = getId;
|
.digest()
|
||||||
|
.toString('base64')
|
||||||
|
.substr(0, 16)
|
||||||
|
.split('/')
|
||||||
|
.join('_')
|
||||||
|
this.getId = getId
|
||||||
|
|
||||||
const setDoc = (relPath, created, md, updated) => {
|
const setDoc = (relPath, created, md, updated) => {
|
||||||
const id = getId(relPath);
|
const id = getId(relPath)
|
||||||
let dir = relPath.split('/');
|
let dir = relPath.split('/')
|
||||||
const name = dir.pop();
|
const name = dir.pop()
|
||||||
dir = dir.join('/');
|
dir = dir.join('/')
|
||||||
const doc = {
|
const doc = {
|
||||||
id, name, dir, created, md, updated
|
id,
|
||||||
};
|
name,
|
||||||
this.docs[id] = doc;
|
dir,
|
||||||
return doc;
|
created,
|
||||||
};
|
md,
|
||||||
this.setDoc = setDoc;
|
updated,
|
||||||
|
}
|
||||||
|
this.docs[id] = doc
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
this.setDoc = setDoc
|
||||||
|
|
||||||
const watcher = chokidar.watch(path.join(docsDir, '/**/*.md'), { persistent: true });
|
const watcher = chokidar.watch(path.join(docsDir, '/**/*.md'), {
|
||||||
|
persistent: true,
|
||||||
|
})
|
||||||
const relPath = path => {
|
const relPath = path => {
|
||||||
path = path.split(docsDir).pop();
|
path = path.split(docsDir).pop()
|
||||||
if(path.substr(0, 1) === '/') path = path.substr(1);
|
if (path.substr(0, 1) === '/') path = path.substr(1)
|
||||||
return path;
|
return path
|
||||||
};
|
}
|
||||||
const handleDoc = async (path, stats) => {
|
const handleDoc = async (path, stats) => {
|
||||||
const rPath = relPath(path);
|
const rPath = relPath(path)
|
||||||
if(!stats) stats = await fs.stat(path);
|
if (!stats) stats = await fs.stat(path)
|
||||||
else { // stats was set so it's a change event
|
else {
|
||||||
const changedDoc = this.docs[getId(rPath)];
|
// stats was set so it's a change event
|
||||||
if(changedDoc && changedDoc.md) this.cached -= changedDoc.md.length;
|
const changedDoc = this.docs[getId(rPath)]
|
||||||
|
if (changedDoc && changedDoc.md) this.cached -= changedDoc.md.length
|
||||||
}
|
}
|
||||||
const { birthtime, size, mtime } = stats;
|
const { birthtime, size, mtime } = stats
|
||||||
let md = null;
|
let md = null
|
||||||
if(size < this.maxDocSize && (this.cached + size) < cacheSize) {
|
if (size < this.maxDocSize && this.cached + size < cacheSize) {
|
||||||
md = await fs.readFile(path);
|
md = await fs.readFile(path)
|
||||||
md = md.toString();
|
md = md.toString()
|
||||||
this.cached += md.length;
|
this.cached += md.length
|
||||||
}
|
}
|
||||||
setDoc(rPath, birthtime, md, mtime);
|
setDoc(rPath, birthtime, md, mtime)
|
||||||
};
|
}
|
||||||
watcher.on('add', handleDoc); // file added
|
watcher.on('add', handleDoc) // file added
|
||||||
watcher.on('change', handleDoc); // file changed (rename triggers unlink then add)
|
watcher.on('change', handleDoc) // file changed (rename triggers unlink then add)
|
||||||
watcher.on('unlink', path => { // file removed
|
watcher.on('unlink', path => {
|
||||||
const id = getId(relPath(path));
|
// file removed
|
||||||
|
const id = getId(relPath(path))
|
||||||
if (this.docs[id] && this.docs[id].md) {
|
if (this.docs[id] && this.docs[id].md) {
|
||||||
this.cached -= this.docs[id].md.length;
|
this.cached -= this.docs[id].md.length
|
||||||
}
|
}
|
||||||
delete this.docs[id];
|
delete this.docs[id]
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
module.exports = loadDocs;
|
module.exports = loadDocs
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
const { BadRequest, Forbidden } = require('@feathersjs/errors');
|
const { BadRequest, Forbidden } = require('@feathersjs/errors')
|
||||||
|
|
||||||
const isAdmin = ctx => {
|
const isAdmin = ctx => {
|
||||||
const { params } = ctx;
|
const { params } = ctx
|
||||||
return Boolean(params.user && params.user.admin);
|
return Boolean(params.user && params.user.admin)
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
disable: () => {
|
disable: () => {
|
||||||
throw new BadRequest('method not allowed');
|
throw new BadRequest('method not allowed')
|
||||||
},
|
},
|
||||||
|
|
||||||
invalid: (field, msg) => {
|
invalid: (field, msg) => {
|
||||||
throw new BadRequest(msg || `invalid ${field} value`);
|
throw new BadRequest(msg || `invalid ${field} value`)
|
||||||
},
|
},
|
||||||
|
|
||||||
isAdmin: isAdmin,
|
isAdmin: isAdmin,
|
||||||
|
|
||||||
adminOnly: ctx => {
|
adminOnly: ctx => {
|
||||||
if(!isAdmin(ctx)) throw new Forbidden('invalid permission');
|
if (!isAdmin(ctx)) throw new Forbidden('invalid permission')
|
||||||
return ctx;
|
return ctx
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
const users = require('./users/users.service.js');
|
const users = require('./users/users.service.js')
|
||||||
const docs = require('./docs/docs.service.js');
|
const docs = require('./docs/docs.service.js')
|
||||||
module.exports = function(app) {
|
module.exports = function(app) {
|
||||||
app.configure(users);
|
app.configure(users)
|
||||||
app.configure(docs);
|
app.configure(docs)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
const { authenticate } = require('@feathersjs/authentication').hooks;
|
const { authenticate } = require('@feathersjs/authentication').hooks
|
||||||
const { Forbidden } = require('@feathersjs/errors');
|
const { Forbidden } = require('@feathersjs/errors')
|
||||||
const fs = require('fs');
|
const fs = require('fs')
|
||||||
const path = require('path');
|
const path = require('path')
|
||||||
const { disable, invalid, isAdmin, adminOnly } = require('../hooksUtil');
|
const { disable, invalid, isAdmin, adminOnly } = require('../hooksUtil')
|
||||||
const {
|
const {
|
||||||
hashPassword, protect
|
hashPassword,
|
||||||
} = require('@feathersjs/authentication-local').hooks;
|
protect,
|
||||||
|
} = require('@feathersjs/authentication-local').hooks
|
||||||
|
|
||||||
const invalidStr = str => Boolean(
|
const invalidStr = str =>
|
||||||
typeof str !== 'string' || str.trim().length === 0);
|
Boolean(typeof str !== 'string' || str.trim().length === 0)
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
before: {
|
before: {
|
||||||
@@ -17,55 +18,59 @@ module.exports = {
|
|||||||
get: [authenticate('jwt')],
|
get: [authenticate('jwt')],
|
||||||
create: [
|
create: [
|
||||||
async ctx => {
|
async ctx => {
|
||||||
const { data, app } = ctx;
|
const { data, app } = ctx
|
||||||
if (app.get('didSetup') && !isAdmin(ctx)) {
|
if (app.get('didSetup') && !isAdmin(ctx)) {
|
||||||
throw new Forbidden('invalid permission');
|
throw new Forbidden('invalid permission')
|
||||||
}
|
}
|
||||||
const { email, password, admin } = data;
|
const { email, password, admin } = data
|
||||||
if(invalidStr(email)) invalid('email');
|
if (invalidStr(email)) invalid('email')
|
||||||
if(invalidStr(password)) invalid('password');
|
if (invalidStr(password)) invalid('password')
|
||||||
if(typeof admin !== 'boolean') invalid('admin');
|
if (typeof admin !== 'boolean') invalid('admin')
|
||||||
|
|
||||||
ctx.data = { email, password, admin };
|
ctx.data = { email, password, admin }
|
||||||
return ctx;
|
return ctx
|
||||||
},
|
},
|
||||||
hashPassword()
|
hashPassword(),
|
||||||
],
|
],
|
||||||
update: [disable],
|
update: [disable],
|
||||||
patch: [ authenticate('local'),
|
patch: [
|
||||||
|
authenticate('local'),
|
||||||
async ctx => {
|
async ctx => {
|
||||||
const { newPassword } = ctx.data;
|
const { newPassword } = ctx.data
|
||||||
if(invalidStr(newPassword)) invalid('newPassword');
|
if (invalidStr(newPassword)) invalid('newPassword')
|
||||||
ctx.data = { password: newPassword };
|
ctx.data = { password: newPassword }
|
||||||
await hashPassword()(ctx);
|
await hashPassword()(ctx)
|
||||||
ctx.app.authLimit.resetKey(ctx.params.ip);
|
ctx.app.authLimit.resetKey(ctx.params.ip)
|
||||||
return ctx;
|
return ctx
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
remove: [ authenticate('jwt'), adminOnly ]
|
remove: [authenticate('jwt'), adminOnly],
|
||||||
},
|
},
|
||||||
|
|
||||||
after: {
|
after: {
|
||||||
all: [
|
all: [
|
||||||
// Make sure the password field is never sent to the client
|
// Make sure the password field is never sent to the client
|
||||||
// Always must be the last hook
|
// Always must be the last hook
|
||||||
protect('password')
|
protect('password'),
|
||||||
],
|
],
|
||||||
find: [],
|
find: [],
|
||||||
get: [],
|
get: [],
|
||||||
create: [
|
create: [
|
||||||
async ctx => {
|
async ctx => {
|
||||||
const { app } = ctx;
|
const { app } = ctx
|
||||||
if(app.get('didSetup')) return ctx;
|
if (app.get('didSetup')) return ctx
|
||||||
app.set('didSetup', true);
|
app.set('didSetup', true)
|
||||||
fs.writeFileSync( // create empty file so we cant stat it
|
fs.writeFileSync(
|
||||||
path.join(__dirname, '..', '..', '..', 'db', '.didSetup'), '');
|
// create empty file so we cant stat it
|
||||||
return ctx;
|
path.join(__dirname, '..', '..', '..', 'db', '.didSetup'),
|
||||||
}
|
''
|
||||||
|
)
|
||||||
|
return ctx
|
||||||
|
},
|
||||||
],
|
],
|
||||||
update: [],
|
update: [],
|
||||||
patch: [],
|
patch: [],
|
||||||
remove: []
|
remove: [],
|
||||||
},
|
},
|
||||||
|
|
||||||
error: {
|
error: {
|
||||||
@@ -75,6 +80,6 @@ module.exports = {
|
|||||||
create: [],
|
create: [],
|
||||||
update: [],
|
update: [],
|
||||||
patch: [],
|
patch: [],
|
||||||
remove: []
|
remove: [],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
// Initializes the `users` service on path `/users`
|
// Initializes the `users` service on path `/users`
|
||||||
const createService = require('feathers-nedb');
|
const createService = require('feathers-nedb')
|
||||||
const createModel = require('../../models/users.model');
|
const createModel = require('../../models/users.model')
|
||||||
const hooks = require('./users.hooks');
|
const hooks = require('./users.hooks')
|
||||||
const getUrl = require('../../../util/getUrl');
|
const getUrl = require('../../../util/getUrl')
|
||||||
|
|
||||||
module.exports = function(app) {
|
module.exports = function(app) {
|
||||||
const Model = createModel(app);
|
const Model = createModel(app)
|
||||||
const paginate = app.get('paginate');
|
const paginate = app.get('paginate')
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
name: 'users',
|
name: 'users',
|
||||||
Model,
|
Model,
|
||||||
paginate
|
paginate,
|
||||||
};
|
}
|
||||||
const url = getUrl('users');
|
const url = getUrl('users')
|
||||||
|
|
||||||
// Initialize our service with any options it requires
|
// Initialize our service with any options it requires
|
||||||
app.use(url, createService(options));
|
app.use(url, createService(options))
|
||||||
|
|
||||||
// Get our initialized service so that we can register hooks and filters
|
// Get our initialized service so that we can register hooks and filters
|
||||||
const service = app.service(url);
|
const service = app.service(url)
|
||||||
|
|
||||||
service.hooks(hooks);
|
service.hooks(hooks)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,50 +1,49 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
const fs = require('fs');
|
const fs = require('fs')
|
||||||
const path = require('path');
|
const path = require('path')
|
||||||
const fetch = require('isomorphic-unfetch');
|
const fetch = require('isomorphic-unfetch')
|
||||||
const ips = require('../config/trustIPs.json');
|
const ips = require('../config/trustIPs.json')
|
||||||
ips.push('loopback');
|
ips.push('loopback')
|
||||||
|
|
||||||
const cfv4 = 'https://www.cloudflare.com/ips-v4';
|
const cfv4 = 'https://www.cloudflare.com/ips-v4'
|
||||||
const cfv6 = 'https://www.cloudflare.com/ips-v6';
|
const cfv6 = 'https://www.cloudflare.com/ips-v6'
|
||||||
const cfConf = path.join(__dirname, '../config/cfIPs.json');
|
const cfConf = path.join(__dirname, '../config/cfIPs.json')
|
||||||
const refreshInterval = 24 * 60 * 60 * 1000;
|
const refreshInterval = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
const getIps = str => {
|
const getIps = str => {
|
||||||
str = str.split('\n').map(ip => ip.trim());
|
str = str.split('\n').map(ip => ip.trim())
|
||||||
str = str.filter(ip => ip.length !== 0);
|
str = str.filter(ip => ip.length !== 0)
|
||||||
return str;
|
return str
|
||||||
};
|
}
|
||||||
|
|
||||||
const getCfIps = async app => {
|
const getCfIps = async app => {
|
||||||
const cfIps = [];
|
const cfIps = []
|
||||||
let res = await fetch(cfv4);
|
let res = await fetch(cfv4)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
cfIps.push(...getIps(await res.text()));
|
cfIps.push(...getIps(await res.text()))
|
||||||
}
|
}
|
||||||
res = await fetch(cfv6);
|
res = await fetch(cfv6)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
cfIps.push(...getIps(await res.text()));
|
cfIps.push(...getIps(await res.text()))
|
||||||
}
|
}
|
||||||
fs.writeFile(cfConf, JSON.stringify(cfIps, null, 2), err => {
|
fs.writeFile(cfConf, JSON.stringify(cfIps, null, 2), err => {
|
||||||
if(err) console.error(err);
|
if (err) console.error(err)
|
||||||
});
|
})
|
||||||
app.set('trust proxy', [...ips, ...cfIps]);
|
app.set('trust proxy', [...ips, ...cfIps])
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports = app => {
|
module.exports = app => {
|
||||||
if (!app.get('trustCloudflare')) {
|
if (!app.get('trustCloudflare')) {
|
||||||
return app.set('trust proxy', ips);
|
return app.set('trust proxy', ips)
|
||||||
}
|
}
|
||||||
fs.readFile(cfConf, async (err, buff) => {
|
fs.readFile(cfConf, async (err, buff) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
if(err.code === 'ENOENT') getCfIps(app);
|
if (err.code === 'ENOENT') getCfIps(app)
|
||||||
else return console.error(err);
|
else return console.error(err)
|
||||||
|
} else {
|
||||||
|
const cfIps = JSON.parse(buff.toString())
|
||||||
|
app.set('trust proxy', [...ips, ...cfIps])
|
||||||
}
|
}
|
||||||
else {
|
setInterval(() => getCfIps(app), refreshInterval)
|
||||||
const cfIps = JSON.parse(buff.toString());
|
})
|
||||||
app.set('trust proxy', [...ips, ...cfIps]);
|
|
||||||
}
|
}
|
||||||
setInterval(() => getCfIps(app), refreshInterval);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,55 +1,56 @@
|
|||||||
const assert = require('assert');
|
const assert = require('assert')
|
||||||
const rp = require('request-promise');
|
const rp = require('request-promise')
|
||||||
const url = require('url');
|
const url = require('url')
|
||||||
const app = require('../src/app');
|
const app = require('../src/app')
|
||||||
const getUrlPath = require('../util/getUrl');
|
const getUrlPath = require('../util/getUrl')
|
||||||
|
|
||||||
const port = app.get('port') || 3030;
|
const port = app.get('port') || 3030
|
||||||
const getUrl = pathname => url.format({
|
const getUrl = pathname =>
|
||||||
|
url.format({
|
||||||
hostname: app.get('host') || 'localhost',
|
hostname: app.get('host') || 'localhost',
|
||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
port,
|
port,
|
||||||
pathname: getUrlPath(pathname)
|
pathname: getUrlPath(pathname),
|
||||||
});
|
})
|
||||||
|
|
||||||
describe('Feathers application tests', () => {
|
describe('Feathers application tests', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
this.server = await app.run(port);
|
this.server = await app.run(port)
|
||||||
});
|
})
|
||||||
|
|
||||||
after(done => {
|
after(done => {
|
||||||
this.server.close(done);
|
this.server.close(done)
|
||||||
});
|
})
|
||||||
|
|
||||||
it('starts and shows the index page', () => {
|
it('starts and shows the index page', () => {
|
||||||
return rp(getUrl('/')).then(body =>
|
return rp(getUrl('/')).then(body =>
|
||||||
assert.ok(body.indexOf('<html>') !== -1)
|
assert.ok(body.indexOf('<html>') !== -1)
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
describe('404', function() {
|
describe('404', function() {
|
||||||
it('shows a 404 HTML page', () => {
|
it('shows a 404 HTML page', () => {
|
||||||
return rp({
|
return rp({
|
||||||
url: getUrl('path/to/nowhere'),
|
url: getUrl('path/to/nowhere'),
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'text/html'
|
Accept: 'text/html',
|
||||||
}
|
},
|
||||||
}).catch(res => {
|
}).catch(res => {
|
||||||
assert.equal(res.statusCode, 404);
|
assert.equal(res.statusCode, 404)
|
||||||
assert.ok(res.error.indexOf('<html>') !== -1);
|
assert.ok(res.error.indexOf('<html>') !== -1)
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
it('shows a 404 JSON error without stack trace', () => {
|
it('shows a 404 JSON error without stack trace', () => {
|
||||||
return rp({
|
return rp({
|
||||||
url: getUrl('path/to/nowhere'),
|
url: getUrl('path/to/nowhere'),
|
||||||
json: true
|
json: true,
|
||||||
}).catch(res => {
|
}).catch(res => {
|
||||||
assert.equal(res.statusCode, 404);
|
assert.equal(res.statusCode, 404)
|
||||||
assert.equal(res.error.code, 404);
|
assert.equal(res.error.code, 404)
|
||||||
assert.equal(res.error.message, 'Page not found');
|
assert.equal(res.error.message, 'Page not found')
|
||||||
assert.equal(res.error.name, 'NotFound');
|
assert.equal(res.error.name, 'NotFound')
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
const assert = require('assert');
|
const assert = require('assert')
|
||||||
const app = require('../../src/app');
|
const app = require('../../src/app')
|
||||||
const getUrl = require('../../util/getUrl');
|
const getUrl = require('../../util/getUrl')
|
||||||
|
|
||||||
describe('\'docs\' service', () => {
|
describe("'docs' service", () => {
|
||||||
it('registered the service', () => {
|
it('registered the service', () => {
|
||||||
const service = app.service(getUrl('docs'));
|
const service = app.service(getUrl('docs'))
|
||||||
assert.ok(service, 'Registered the service');
|
assert.ok(service, 'Registered the service')
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
const assert = require('assert');
|
const assert = require('assert')
|
||||||
const app = require('../../src/app');
|
const app = require('../../src/app')
|
||||||
const getUrl = require('../../util/getUrl');
|
const getUrl = require('../../util/getUrl')
|
||||||
|
|
||||||
describe('\'users\' service', () => {
|
describe("'users' service", () => {
|
||||||
it('registered the service', () => {
|
it('registered the service', () => {
|
||||||
const service = app.service(getUrl('users'));
|
const service = app.service(getUrl('users'))
|
||||||
assert.ok(service, 'Registered the service');
|
assert.ok(service, 'Registered the service')
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// make sure basePath doesn't end with /
|
// make sure basePath doesn't end with /
|
||||||
let { basePath } = require('../config/host.json');
|
let { basePath } = require('../config/host.json')
|
||||||
const urlChars = basePath.split('');
|
const urlChars = basePath.split('')
|
||||||
|
|
||||||
if (basePath.length > 1 && urlChars.pop() === '/') {
|
if (basePath.length > 1 && urlChars.pop() === '/') {
|
||||||
basePath = urlChars.join('');
|
basePath = urlChars.join('')
|
||||||
}
|
}
|
||||||
module.exports = basePath;
|
module.exports = basePath
|
||||||
|
|||||||
@@ -1,48 +1,52 @@
|
|||||||
const isOkDirPart = str => {
|
const isOkDirPart = str => {
|
||||||
if(str.length > 255 || str.length === 0) return false;
|
if (str.length > 255 || str.length === 0) return false
|
||||||
const end = str.length - 1;
|
const end = str.length - 1
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
const c = str.charCodeAt(i);
|
const c = str.charCodeAt(i)
|
||||||
if(!(c > 47 && c < 58) && // 0-9
|
if (
|
||||||
|
!(c > 47 && c < 58) && // 0-9
|
||||||
!(c > 64 && c < 91) && // A-Z
|
!(c > 64 && c < 91) && // A-Z
|
||||||
!(c > 96 && c < 123) && // a-z
|
!(c > 96 && c < 123) && // a-z
|
||||||
!(c === 95) && !(c === 45) && // _ and -
|
!(c === 95) &&
|
||||||
!((c === 46 || c === 32) && // period or space if not first or last
|
!(c === 45) && // _ and -
|
||||||
i !== 0 && i !== end)) {
|
!(
|
||||||
return false;
|
(c === 46 || c === 32) && // period or space if not first or last
|
||||||
|
i !== 0 &&
|
||||||
|
i !== end
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
||||||
checkDir: dir => {
|
checkDir: dir => {
|
||||||
if(typeof dir !== 'string') return false;
|
if (typeof dir !== 'string') return false
|
||||||
dir = dir.trim();
|
dir = dir.trim()
|
||||||
if(dir.length === 0) return 0;
|
if (dir.length === 0) return 0
|
||||||
if (dir.indexOf('/') > -1) {
|
if (dir.indexOf('/') > -1) {
|
||||||
dir = dir.split('/').filter(p => p.length !== 0);
|
dir = dir.split('/').filter(p => p.length !== 0)
|
||||||
if (dir.length === 1) {
|
if (dir.length === 1) {
|
||||||
if(!isOkDirPart(dir[0])) false;
|
if (!isOkDirPart(dir[0])) false
|
||||||
dir = dir[0];
|
dir = dir[0]
|
||||||
} else if (dir.length === 0) {
|
} else if (dir.length === 0) {
|
||||||
dir = '';
|
dir = ''
|
||||||
} else if (dir.some(part => !isOkDirPart(part))) {
|
} else if (dir.some(part => !isOkDirPart(part))) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
} else if (!isOkDirPart(dir)) {
|
} else if (!isOkDirPart(dir)) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
return Array.isArray(dir) ? dir.join('/') : dir;
|
return Array.isArray(dir) ? dir.join('/') : dir
|
||||||
},
|
},
|
||||||
|
|
||||||
checkName: name => {
|
checkName: name => {
|
||||||
if(typeof name !== 'string') return false;
|
if (typeof name !== 'string') return false
|
||||||
name = name.trim();
|
name = name.trim()
|
||||||
if(name.length === 0) return 0;
|
if (name.length === 0) return 0
|
||||||
if(!isOkDirPart(name)) return false;
|
if (!isOkDirPart(name)) return false
|
||||||
return name;
|
return name
|
||||||
},
|
},
|
||||||
|
}
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
const freezeSSR = selector => {
|
const freezeSSR = selector => {
|
||||||
const FrozenSSR = () => {
|
const FrozenSSR = () => {
|
||||||
let __html = '';
|
let __html = ''
|
||||||
let props = {};
|
let props = {}
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== 'undefined') {
|
||||||
let el = document.querySelector(selector);
|
let el = document.querySelector(selector)
|
||||||
if (el) {
|
if (el) {
|
||||||
__html = el.innerHTML;
|
__html = el.innerHTML
|
||||||
el.getAttributeNames().forEach(attr => {
|
el.getAttributeNames().forEach(attr => {
|
||||||
const attrKey = attr === 'class' ? 'className' : attr;
|
const attrKey = attr === 'class' ? 'className' : attr
|
||||||
props[attrKey] = el.getAttribute(attr);
|
props[attrKey] = el.getAttribute(attr)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return <div {...props} dangerouslySetInnerHTML={{ __html }} />;
|
return <div {...props} dangerouslySetInnerHTML={{ __html }} />
|
||||||
};
|
}
|
||||||
|
|
||||||
return { loading: FrozenSSR };
|
return { loading: FrozenSSR }
|
||||||
};
|
}
|
||||||
|
|
||||||
export default freezeSSR;
|
export default freezeSSR
|
||||||
|
|||||||
@@ -1,36 +1,39 @@
|
|||||||
import fetch from 'isomorphic-unfetch';
|
import fetch from 'isomorphic-unfetch'
|
||||||
import parseSort from './parseSort';
|
import parseSort from './parseSort'
|
||||||
import getUrl from './getUrl';
|
import getUrl from './getUrl'
|
||||||
import getJwt from './getJwt';
|
import getJwt from './getJwt'
|
||||||
|
|
||||||
export const $limit = 12; // number of docs per page
|
export const $limit = 12 // number of docs per page
|
||||||
export const select = ['id', 'name', 'updated', 'dir']
|
export const select = ['id', 'name', 'updated', 'dir'].map((f, i) => ({
|
||||||
.map((f, i) => ({ [`$select[${i}]`]: f }));
|
[`$select[${i}]`]: f,
|
||||||
|
}))
|
||||||
|
|
||||||
export const getDocs = async (q, jwt) => {
|
export const getDocs = async (q, jwt) => {
|
||||||
const docsRes = await fetch(getUrl('docs', Boolean(jwt)) + q, {
|
const docsRes = await fetch(getUrl('docs', Boolean(jwt)) + q, {
|
||||||
headers: { Authorization: jwt || getJwt() }
|
headers: { Authorization: jwt || getJwt() },
|
||||||
}).catch(({ message }) => ({ ok: false, error: message }));
|
}).catch(({ message }) => ({ ok: false, error: message }))
|
||||||
if (docsRes.ok) {
|
if (docsRes.ok) {
|
||||||
const res = await docsRes.json();
|
const res = await docsRes.json()
|
||||||
const total = res.total || 0;
|
const total = res.total || 0
|
||||||
const docs = res.data || [];
|
const docs = res.data || []
|
||||||
return { docs, total };
|
return { docs, total }
|
||||||
|
}
|
||||||
|
return { total: 0, docs: [], error: docsRes.message }
|
||||||
}
|
}
|
||||||
return { total: 0, docs: [], error: docsRes.message };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const buildQ = q => {
|
export const buildQ = q => {
|
||||||
if(!q.$search) delete q.$search;
|
if (!q.$search) delete q.$search
|
||||||
if(!q.$skip) delete q.$skip;
|
if (!q.$skip) delete q.$skip
|
||||||
else {
|
else {
|
||||||
q.$skip = (q.$skip - 1) * $limit;
|
q.$skip = (q.$skip - 1) * $limit
|
||||||
|
}
|
||||||
|
const $sort = parseSort(q.$sort ? q.$sort : 'updated:-1')
|
||||||
|
delete q.$sort
|
||||||
|
select.forEach(sel => (q = { ...q, ...sel }))
|
||||||
|
q = { $limit, ...q }
|
||||||
|
let url = Object.keys(q)
|
||||||
|
.map(k => `${k}=${encodeURIComponent(q[k])}`)
|
||||||
|
.join('&')
|
||||||
|
url = `?${url}&${$sort}`
|
||||||
|
return url
|
||||||
}
|
}
|
||||||
const $sort = parseSort(q.$sort ? q.$sort : 'updated:-1');
|
|
||||||
delete q.$sort;
|
|
||||||
select.forEach(sel => q = {...q, ...sel});
|
|
||||||
q = { $limit, ...q };
|
|
||||||
let url = Object.keys(q).map(k => `${k}=${encodeURIComponent(q[k])}`).join('&');
|
|
||||||
url = `?${url}&${$sort}`;
|
|
||||||
return url;
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export default req => {
|
export default req => {
|
||||||
if(req) return req.jwt;
|
if (req) return req.jwt
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
return window.localStorage.getItem('jwt');
|
return window.localStorage.getItem('jwt')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
const url = require('url');
|
const url = require('url')
|
||||||
const urljoin = require('url-join');
|
const urljoin = require('url-join')
|
||||||
const basePath = require('./basePath');
|
const basePath = require('./basePath')
|
||||||
const { host, port, protocol } = require('../config/host.json');
|
const { host, port, protocol } = require('../config/host.json')
|
||||||
|
|
||||||
module.exports = (path, absolute) => {
|
module.exports = (path, absolute) => {
|
||||||
path = urljoin(basePath, path);
|
path = urljoin(basePath, path)
|
||||||
if(!absolute) return path;
|
if (!absolute) return path
|
||||||
return url.format({
|
return url.format({
|
||||||
hostname: host,
|
hostname: host,
|
||||||
port,
|
port,
|
||||||
protocol,
|
protocol,
|
||||||
pathname: path,
|
pathname: path,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
getKey: e => e.which || e.keyCode,
|
getKey: e => e.which || e.keyCode,
|
||||||
isCtrlKey: key => key === 91 || key === 93 || key === 17
|
isCtrlKey: key => key === 91 || key === 93 || key === 17,
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export default ({ user }) => {
|
export default ({ user }) => {
|
||||||
return { user };
|
return { user }
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
export default sort => {
|
export default sort => {
|
||||||
let key, ascDesc;
|
let key, ascDesc
|
||||||
switch (typeof sort) {
|
switch (typeof sort) {
|
||||||
case 'object': {
|
case 'object': {
|
||||||
key = Object.keys(sort).pop();
|
key = Object.keys(sort).pop()
|
||||||
ascDesc = sort[key];
|
ascDesc = sort[key]
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
case 'string': {
|
case 'string': {
|
||||||
const parts = sort.split(':');
|
const parts = sort.split(':')
|
||||||
key = parts[0];
|
key = parts[0]
|
||||||
ascDesc = parts[1];
|
ascDesc = parts[1]
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
default: break;
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return `$sort[${key}]=${ascDesc}`
|
||||||
}
|
}
|
||||||
return `$sort[${key}]=${ascDesc}`;
|
|
||||||
};
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
const basePath = require('./basePath');
|
const basePath = require('./basePath')
|
||||||
|
|
||||||
module.exports = url => {
|
module.exports = url => {
|
||||||
if (basePath !== '/') {
|
if (basePath !== '/') {
|
||||||
url = url.split(basePath).join('');
|
url = url.split(basePath).join('')
|
||||||
|
}
|
||||||
|
return url
|
||||||
}
|
}
|
||||||
return url;
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export default function updateStateFromId(e) {
|
export default function updateStateFromId(e) {
|
||||||
const el = e.target;
|
const el = e.target
|
||||||
this.setState({ [el.id]: el.value });
|
this.setState({ [el.id]: el.value })
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user