Merge branch 'master' of github.com:jorilallo/atlas
This commit is contained in:
@ -3,48 +3,14 @@ import { toJS } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import PublishingInfo from 'components/PublishingInfo';
|
||||
import DocumentHtml from './components/DocumentHtml';
|
||||
|
||||
import styles from './Document.scss';
|
||||
|
||||
@observer
|
||||
class DocumentHtml extends React.Component {
|
||||
static propTypes = {
|
||||
html: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
this.setExternalLinks();
|
||||
}
|
||||
|
||||
componentDidUpdate = () => {
|
||||
this.setExternalLinks();
|
||||
}
|
||||
|
||||
setExternalLinks = () => {
|
||||
const links = this.refs.content.querySelectorAll('a');
|
||||
links.forEach(link => {
|
||||
if (link.hostname !== window.location.hostname) {
|
||||
link.target = '_blank'; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
ref="content"
|
||||
className={ styles.document }
|
||||
dangerouslySetInnerHTML={{ __html: this.props.html }}
|
||||
{ ...this.props }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
class Document extends React.Component {
|
||||
static propTypes = {
|
||||
document: React.PropTypes.object.isRequired,
|
||||
document: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -64,6 +30,3 @@ class Document extends React.Component {
|
||||
}
|
||||
|
||||
export default Document;
|
||||
export {
|
||||
DocumentHtml,
|
||||
};
|
||||
|
@ -4,87 +4,3 @@
|
||||
width: 100%;
|
||||
padding: 20px 20px 40px 20px;
|
||||
}
|
||||
|
||||
.document {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
:global {
|
||||
.anchor {
|
||||
visibility: hidden;
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
:global {
|
||||
.anchor {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 1.5em;
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// pre {
|
||||
// box-shadow: 1px 1px 1px #f5f5f5;
|
||||
// }
|
||||
|
||||
blockquote {
|
||||
font-style: italic;
|
||||
border-left: 2px solid $lightGray;
|
||||
padding-left: 0.8em;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
display: block;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead, tbody {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
thead {
|
||||
tr {
|
||||
border-bottom: 2px solid $lightGray;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border-bottom: 1px solid $lightGray;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: #fff;
|
||||
|
||||
// &:nth-child(2n) {
|
||||
// background-color: #f8f8f8;
|
||||
// }
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: left;
|
||||
border: 1px 0 solid $lightGray;
|
||||
padding: 5px 20px 5px 0;
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import styles from './DocumentHtml.scss';
|
||||
|
||||
@observer
|
||||
class DocumentHtml extends React.Component {
|
||||
static propTypes = {
|
||||
html: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
this.setExternalLinks();
|
||||
}
|
||||
|
||||
componentDidUpdate = () => {
|
||||
this.setExternalLinks();
|
||||
}
|
||||
|
||||
setExternalLinks = () => {
|
||||
const links = ReactDOM.findDOMNode(this).querySelectorAll('a');
|
||||
links.forEach(link => {
|
||||
if (link.hostname !== window.location.hostname) {
|
||||
link.target = '_blank'; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className={ styles.document }
|
||||
dangerouslySetInnerHTML={{ __html: this.props.html }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DocumentHtml;
|
@ -0,0 +1,85 @@
|
||||
@import '~styles/constants.scss';
|
||||
|
||||
.document {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
:global {
|
||||
.anchor {
|
||||
visibility: hidden;
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
:global {
|
||||
.anchor {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 1.5em;
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// pre {
|
||||
// box-shadow: 1px 1px 1px #f5f5f5;
|
||||
// }
|
||||
|
||||
blockquote {
|
||||
font-style: italic;
|
||||
border-left: 2px solid $lightGray;
|
||||
padding-left: 0.8em;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
display: block;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead, tbody {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
thead {
|
||||
tr {
|
||||
border-bottom: 2px solid $lightGray;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border-bottom: 1px solid $lightGray;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: #fff;
|
||||
|
||||
// &:nth-child(2n) {
|
||||
// background-color: #f8f8f8;
|
||||
// }
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: left;
|
||||
border: 1px 0 solid $lightGray;
|
||||
padding: 5px 20px 5px 0;
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import DocumentHtml from './DocumentHtml';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
const testHtml = `
|
||||
<h1>test document</h1>
|
||||
<p>Hello! <a href="/internal">internal link</a></p>
|
||||
<p>Aliens <a href="/external">external link</a></p>
|
||||
`;
|
||||
|
||||
test('renders', () => {
|
||||
const wrapper = shallow(
|
||||
<DocumentHtml
|
||||
html={ testHtml }
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('.document').length).toBe(1);
|
||||
});
|
@ -0,0 +1,2 @@
|
||||
import DocumentHtml from './DocumentHtml';
|
||||
export default DocumentHtml;
|
@ -1,4 +1,5 @@
|
||||
import Document, { DocumentHtml } from './Document';
|
||||
import Document from './Document';
|
||||
import DocumentHtml from './components/DocumentHtml';
|
||||
|
||||
export default Document;
|
||||
export {
|
||||
|
@ -73,7 +73,7 @@ class MarkdownEditor extends React.Component {
|
||||
formData.append('file', file);
|
||||
}
|
||||
|
||||
fetch(data.upload_url, {
|
||||
fetch(data.uploadUrl, {
|
||||
method: 'post',
|
||||
body: formData,
|
||||
})
|
||||
|
@ -10,7 +10,7 @@ import { convertToMarkdown } from 'utils/markdown';
|
||||
@observer
|
||||
class Flatpage extends React.Component {
|
||||
static propTypes = {
|
||||
route: PropTypes.object.isRequired,
|
||||
route: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -64,22 +64,14 @@ class ApiClient {
|
||||
|
||||
// Handle 401, log out user
|
||||
if (response.status === 401) {
|
||||
stores.user.logout();
|
||||
return stores.user.logout();
|
||||
}
|
||||
|
||||
// Handle failed responses
|
||||
let error;
|
||||
try {
|
||||
// Expect API to return JSON
|
||||
error = JSON.parse(response);
|
||||
} catch (e) {
|
||||
// Expect call to fail without JSON response
|
||||
error = { error: response.statusText };
|
||||
}
|
||||
|
||||
const error = {};
|
||||
error.statusCode = response.status;
|
||||
error.response = response;
|
||||
return reject(error);
|
||||
throw error;
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
@ -91,8 +83,12 @@ class ApiClient {
|
||||
}
|
||||
resolve(json);
|
||||
})
|
||||
.catch(() => {
|
||||
reject({ error: 'Unknown error' });
|
||||
.catch(error => {
|
||||
error.response.json()
|
||||
.then(json => {
|
||||
error.data = json;
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -150,6 +150,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-jest": "^15.0.0",
|
||||
"enzyme": "^2.4.1",
|
||||
"fetch-test-server": "^1.1.0",
|
||||
"fsevents": "1.0.14",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
@ -159,6 +160,7 @@
|
||||
"koa-webpack-hot-middleware": "1.0.3",
|
||||
"node-dev": "3.1.0",
|
||||
"nodemon": "1.9.1",
|
||||
"react-addons-test-utils": "^15.3.1",
|
||||
"react-test-renderer": "^15.3.1"
|
||||
}
|
||||
}
|
||||
|
@ -18,49 +18,63 @@ Object {
|
||||
|
||||
exports[`#auth.login should require either username or email 1`] = `
|
||||
Object {
|
||||
"error": "username or email is required",
|
||||
"ok": false
|
||||
"error": "validation_error",
|
||||
"message": "username/email is required",
|
||||
"ok": false,
|
||||
"status": 400
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#auth.login should require password 1`] = `
|
||||
Object {
|
||||
"error": "password is required",
|
||||
"ok": false
|
||||
"error": "validation_error",
|
||||
"message": "username/email is required",
|
||||
"ok": false,
|
||||
"status": 400
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#auth.login should validate password 1`] = `
|
||||
Object {
|
||||
"error": "Invalid password",
|
||||
"ok": false
|
||||
"error": "validation_error",
|
||||
"message": "username/email is required",
|
||||
"ok": false,
|
||||
"status": 400
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#auth.signup should require params 1`] = `
|
||||
Object {
|
||||
"error": "name is required",
|
||||
"ok": false
|
||||
"error": "validation_error",
|
||||
"message": "name is required",
|
||||
"ok": false,
|
||||
"status": 400
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#auth.signup should require unique email 1`] = `
|
||||
Object {
|
||||
"error": "User already exists with this email",
|
||||
"ok": false
|
||||
"error": "user_exists_with_email",
|
||||
"message": "User already exists with this email",
|
||||
"ok": false,
|
||||
"status": 400
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#auth.signup should require unique username 1`] = `
|
||||
Object {
|
||||
"error": "User already exists with this username",
|
||||
"ok": false
|
||||
"error": "user_exists_with_username",
|
||||
"message": "User already exists with this username",
|
||||
"ok": false,
|
||||
"status": 400
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#auth.signup should require valid email 1`] = `
|
||||
Object {
|
||||
"error": "email is invalid",
|
||||
"ok": false
|
||||
"error": "validation_error",
|
||||
"message": "email is invalid",
|
||||
"ok": false,
|
||||
"status": 400
|
||||
}
|
||||
`;
|
||||
|
@ -1,7 +1,9 @@
|
||||
exports[`#user.info should require authentication 1`] = `
|
||||
Object {
|
||||
"error": "Authentication required",
|
||||
"ok": false
|
||||
"error": "authentication_required",
|
||||
"message": "Authentication required",
|
||||
"ok": false,
|
||||
"status": 401
|
||||
}
|
||||
`;
|
||||
|
||||
@ -13,6 +15,7 @@ Object {
|
||||
"name": "User 1",
|
||||
"username": "user1"
|
||||
},
|
||||
"ok": true
|
||||
"ok": true,
|
||||
"status": 200
|
||||
}
|
||||
`;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Router from 'koa-router';
|
||||
import httpErrors from 'http-errors';
|
||||
import Sequelize from 'sequelize';
|
||||
import apiError, { httpErrors } from '../errors';
|
||||
import fetch from 'isomorphic-fetch';
|
||||
import querystring from 'querystring';
|
||||
|
||||
@ -18,11 +19,11 @@ router.post('auth.signup', async (ctx) => {
|
||||
ctx.assertPresent(password, 'password is required');
|
||||
|
||||
if (await User.findOne({ where: { email } })) {
|
||||
throw httpErrors.BadRequest('User already exists with this email');
|
||||
throw apiError(400, 'user_exists_with_email', 'User already exists with this email');
|
||||
}
|
||||
|
||||
if (await User.findOne({ where: { username } })) {
|
||||
throw httpErrors.BadRequest('User already exists with this username');
|
||||
throw apiError(400, 'user_exists_with_username', 'User already exists with this username');
|
||||
}
|
||||
|
||||
const user = await User.create({
|
||||
@ -39,21 +40,31 @@ router.post('auth.signup', async (ctx) => {
|
||||
});
|
||||
|
||||
router.post('auth.login', async (ctx) => {
|
||||
const { username, email, password } = ctx.request.body;
|
||||
const { username, password } = ctx.request.body;
|
||||
|
||||
ctx.assertPresent(username, 'username/email is required');
|
||||
ctx.assertPresent(password, 'password is required');
|
||||
|
||||
let user;
|
||||
if (username) {
|
||||
user = await User.findOne({ where: { username } });
|
||||
} else if (email) {
|
||||
user = await User.findOne({ where: { email } });
|
||||
user = await User.findOne({ where: Sequelize.or(
|
||||
{ email: username },
|
||||
{ username },
|
||||
) });
|
||||
} else {
|
||||
throw httpErrors.BadRequest('username or email is required');
|
||||
throw apiError(400, 'invalid_credentials', 'username or email is invalid');
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw apiError(400, 'username or email is invalid');
|
||||
}
|
||||
|
||||
if (!user.passwordDigest) {
|
||||
throw apiError(400, 'no_password', 'No password set');
|
||||
}
|
||||
|
||||
if (!await user.verifyPassword(password)) {
|
||||
throw httpErrors.BadRequest('Invalid password');
|
||||
throw apiError(400, 'invalid_password', 'Invalid password');
|
||||
}
|
||||
|
||||
ctx.body = { data: {
|
||||
@ -154,8 +165,6 @@ router.post('auth.slackCommands', async (ctx) => {
|
||||
}
|
||||
|
||||
if (!data.ok) throw httpErrors.BadRequest(data.error);
|
||||
|
||||
ctx.body = { success: true };
|
||||
});
|
||||
|
||||
|
||||
|
@ -91,7 +91,7 @@ describe('#auth.login', () => {
|
||||
await seed();
|
||||
const res = await server.post('/api/auth.login', {
|
||||
body: {
|
||||
email: 'user1@example.com',
|
||||
username: 'user1@example.com',
|
||||
password: 'test123!',
|
||||
},
|
||||
});
|
||||
|
@ -2,6 +2,7 @@ import bodyParser from 'koa-bodyparser';
|
||||
import Koa from 'koa';
|
||||
import Router from 'koa-router';
|
||||
import Sequelize from 'sequelize';
|
||||
import _ from 'lodash';
|
||||
|
||||
import auth from './auth';
|
||||
import user from './user';
|
||||
@ -41,7 +42,9 @@ api.use(async (ctx, next) => {
|
||||
|
||||
ctx.body = {
|
||||
ok: false,
|
||||
error: message,
|
||||
error: _.snakeCase(err.id || err.message),
|
||||
status: err.status,
|
||||
message,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ export default function apiWrapper(_options) {
|
||||
|
||||
ctx.body = {
|
||||
...ctx.body,
|
||||
status: ctx.status,
|
||||
ok,
|
||||
};
|
||||
};
|
||||
|
@ -1,23 +1,23 @@
|
||||
import httpErrors from 'http-errors';
|
||||
import apiError from '../../errors';
|
||||
import validator from 'validator';
|
||||
|
||||
export default function validation() {
|
||||
return function validationMiddleware(ctx, next) {
|
||||
ctx.assertPresent = function assertPresent(value, message) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
throw httpErrors.BadRequest(message);
|
||||
throw apiError(400, 'validation_error', message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertEmail = function assertEmail(value, message) {
|
||||
if (!validator.isEmail(value)) {
|
||||
throw httpErrors.BadRequest(message);
|
||||
throw apiError(400, 'validation_error', message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertUuid = function assertUuid(value, message) {
|
||||
if (!validator.isUUID(value)) {
|
||||
throw httpErrors.BadRequest(message);
|
||||
throw apiError(400, 'validation_error', message);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -25,7 +25,7 @@ router.post('user.s3Upload', auth(), async (ctx) => {
|
||||
const policy = makePolicy();
|
||||
|
||||
ctx.body = { data: {
|
||||
max_upload_size: process.env.AWS_S3_UPLOAD_MAX_SIZE,
|
||||
maxUploadSize: process.env.AWS_S3_UPLOAD_MAX_SIZE,
|
||||
upload_url: process.env.AWS_S3_UPLOAD_BUCKET_URL,
|
||||
form: {
|
||||
AWSAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||
@ -37,7 +37,7 @@ router.post('user.s3Upload', auth(), async (ctx) => {
|
||||
policy,
|
||||
},
|
||||
asset: {
|
||||
content_type: kind,
|
||||
contentType: kind,
|
||||
url: `${process.env.AWS_S3_UPLOAD_BUCKET_URL}${s3Key}/${filename}`,
|
||||
name: filename,
|
||||
size,
|
||||
|
10
server/errors.js
Normal file
10
server/errors.js
Normal file
@ -0,0 +1,10 @@
|
||||
import httpErrors from 'http-errors';
|
||||
|
||||
const apiError = (code, id, message) => {
|
||||
return httpErrors(code, message, { id });
|
||||
};
|
||||
|
||||
export default apiError;
|
||||
export {
|
||||
httpErrors,
|
||||
};
|
Reference in New Issue
Block a user