Implemented some of api keys

This commit is contained in:
Jori Lallo 2016-08-24 00:37:54 -07:00
parent 29665621b3
commit a2aea2abfc
12 changed files with 342 additions and 35 deletions

View File

@ -2,25 +2,70 @@ import React from 'react';
import { observer } from 'mobx-react';
import Helmet from 'react-helmet';
const Application = observer((props) => {
return (
<div style={{ width: '100%', height: '100%', display: 'flex', flex: 1 }}>
<Helmet
title="Atlas"
meta={ [
{
name: 'viewport',
content: 'width=device-width, initial-scale=1.0',
},
] }
/>
{ props.children }
</div>
);
});
@observer
class Application extends React.Component {
static childContextTypes = {
rebass: React.PropTypes.object,
}
Application.propTypes = {
children: React.PropTypes.node.isRequired,
};
propTypes = {
children: React.PropTypes.node.isRequired,
}
getChildContext() {
return {
rebass: {
colors: {
primary: '#171B35',
},
// color: '#eee',
// backgroundColor: '#fff',
borderRadius: 2,
borderColor: '#eee',
// fontSizes: [64, 48, 28, 20, 18, 16, 14],
bold: 500,
scale: [
0,
8,
18,
36,
72,
],
Input: {
// borderBottom: '1px solid #eee',
},
Button: {
// color: '#eee',
// backgroundColor: '#fff',
// border: '1px solid #ccc',
},
ButtonOutline: {
color: '#000',
},
InlineForm: {
},
},
};
}
render() {
return (
<div style={{ width: '100%', height: '100%', display: 'flex', flex: 1 }}>
<Helmet
title="Atlas"
meta={ [
{
name: 'viewport',
content: 'width=device-width, initial-scale=1.0',
},
] }
/>
{ this.props.children }
</div>
);
}
}
export default Application;

View File

@ -2,6 +2,7 @@ import React from 'react';
import { observer } from 'mobx-react';
import { Flex } from 'reflexbox';
import { Input, ButtonOutline, InlineForm } from 'rebass';
import Layout, { Title } from 'components/Layout';
import CenteredContent from 'components/CenteredContent';
import SlackAuthLink from 'components/SlackAuthLink';
@ -19,6 +20,11 @@ class Settings extends React.Component {
this.store = new SettingsStore();
}
onKeyCreate = (e) => {
e.preventDefault();
this.store.createApiKey();
}
render() {
const title = (
<Title>
@ -34,19 +40,57 @@ class Settings extends React.Component {
loading={ this.store.isFetching }
>
<CenteredContent>
<h2 className={ styles.sectionHeader }>Slack</h2>
<div className={ styles.section }>
<h2 className={ styles.sectionHeader }>Slack</h2>
<p>
Connect Atlas to your Slack to instantly search for your documents
using <code>/atlas</code> command.
</p>
<p>
Connect Atlas to your Slack to instantly search for your documents
using <code>/atlas</code> command.
</p>
<SlackAuthLink
scopes={ ['commands'] }
redirectUri={ `${URL}/auth/slack/commands` }
>
<img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcSet="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" />
</SlackAuthLink>
<SlackAuthLink
scopes={ ['commands'] }
redirectUri={ `${URL}/auth/slack/commands` }
>
<img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcSet="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" />
</SlackAuthLink>
</div>
<div className={ styles.section }>
<h2 className={ styles.sectionHeader }>API access</h2>
<p>
Create API tokens to hack on your Atlas.
Learn more in <a href>API documentation</a>.
</p>
{ this.store.apiKeys && (
<table className={ styles.apiKeyTable }>
{ this.store.apiKeys.map(key => (
<tr>
<td>{ key.name }</td>
<td><code>{ key.secret }</code></td>
{/* <td>
<span className={ styles.deleteAction }>Delete</span>
</td> */}
</tr>
)) }
</table>
) }
<div>
<InlineForm
placeholder="Token name"
buttonLabel="Create token"
label="InlineForm"
name="inline_form"
value={ this.store.keyName }
onChange={ this.store.setKeyName }
onClick={ this.onKeyCreate }
style={{ width: '100%' }}
disabled={ this.store.isFetching }
/>
</div>
</div>
</CenteredContent>
</Layout>
);

View File

@ -1,4 +1,25 @@
@import '~styles/constants.scss';
.section {
margin-bottom: 40px;
}
.sectionHeader {
border-bottom: 1px solid #eee;
}
.apiKeyTable {
margin-bottom: 20px;
width: 100%;
td {
margin-right: 20px;
color: #969696;
}
}
.deleteAction {
font-size: 14px;
cursor: pointer;
color: $textColor;
}

View File

@ -1,7 +1,52 @@
import { observable, action, runInAction } from 'mobx';
// import { client } from 'utils/ApiClient';
import { observable, action, runInAction, toJS } from 'mobx';
import { client } from 'utils/ApiClient';
class SearchStore {
};
@observable apiKeys = [];
@observable keyName;
@observable isFetching;
@action fetchApiKeys = async () => {
this.isFetching = true;
try {
const res = await client.post('/apiKeys.list');
const { data } = res;
runInAction('fetchApiKeys', () => {
this.apiKeys = data;
});
} catch (e) {
console.error("Something went wrong");
}
this.isFetching = false;
}
@action createApiKey = async () => {
this.isFetching = true;
try {
const res = await client.post('/apiKeys.create', {
name: `${this.keyName}` || 'Untitled key',
});
const { data } = res;
runInAction('createApiKey', () => {
this.apiKeys.push(data);
this.keyName = '';
});
} catch (e) {
console.error("Something went wrong");
}
this.isFetching = false;
}
@action setKeyName = (value) => {
this.keyName = value.target.value;
}
constructor() {
this.fetchApiKeys();
}
}
export default SearchStore;

View File

@ -1,7 +1,7 @@
$lightGray: #eee;
$textColor: #171B35;
$actionColor: #2da9e1;
$actionColor: #3AA3E3;
$darkGray: #ccc;
$lightGray: #eee;

53
server/api/apiKeys.js Normal file
View File

@ -0,0 +1,53 @@
import Router from 'koa-router';
import httpErrors from 'http-errors';
import _ from 'lodash';
import auth from './authentication';
import pagination from './middlewares/pagination';
import { presentApiKey } from '../presenters';
import { ApiKey } from '../models';
const router = new Router();
router.post('apiKeys.create', auth(), async (ctx) => {
const {
name,
} = ctx.body;
ctx.assertPresent(name, 'name is required');
const user = ctx.state.user;
const key = await ApiKey.create({
name,
userId: user.id,
});
ctx.body = {
data: presentApiKey(ctx, key),
};
});
router.post('apiKeys.list', auth(), pagination(), async (ctx) => {
const user = ctx.state.user;
const keys = await ApiKey.findAll({
where: {
userId: user.id,
},
order: [
['createdAt', 'DESC'],
],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
const data = keys.map(key => {
return presentApiKey(ctx, key);
});
ctx.body = {
pagination: ctx.state.pagination,
data,
};
});
export default router;

View File

@ -9,6 +9,7 @@ import user from './user';
import collections from './collections';
import documents from './documents';
import hooks from './hooks';
import apiKeys from './apiKeys';
import validation from './validation';
import methodOverride from '../middlewares/methodOverride';
@ -52,6 +53,7 @@ router.use('/', user.routes());
router.use('/', collections.routes());
router.use('/', documents.routes());
router.use('/', hooks.routes());
router.use('/', apiKeys.routes());
// Router is embedded in a Koa application wrapper, because koa-router does not
// allow middleware to catch any routes which were not explicitly defined.

View File

@ -0,0 +1,46 @@
'use strict';
module.exports = {
up: function (queryInterface, Sequelize) {
queryInterface.createTable('apiKeys', {
id: {
type: 'UUID',
allowNull: false,
primaryKey: true,
},
name: {
type: 'CHARACTER VARYING',
allowNull: true,
},
secret: {
type: 'CHARACTER VARYING',
allowNull: false,
unique: true,
},
userId: {
type: 'UUID',
allowNull: true,
// references: {
// model: 'users',
// key: 'id',
// },
},
createdAt: {
type: 'TIMESTAMP WITH TIME ZONE',
allowNull: false,
},
updatedAt: {
type: 'TIMESTAMP WITH TIME ZONE',
allowNull: false,
},
deletedAt: {
type: 'TIMESTAMP WITH TIME ZONE',
allowNull: true,
},
});
},
down: function (queryInterface, Sequelize) {
queryInterface.createTable('apiKeys');
},
};

View File

@ -0,0 +1,13 @@
'use strict';
module.exports = {
up: function (queryInterface, Sequelize) {
queryInterface.addIndex('apiKeys', ['secret', 'deletedAt']);
queryInterface.addIndex('apiKeys', ['userId', 'deletedAt']);
},
down: function (queryInterface, Sequelize) {
queryInterface.removeIndex('apiKeys', ['secret', 'deletedAt']);
queryInterface.removeIndex('apiKeys', ['userId', 'deletedAt']);
},
};

28
server/models/ApiKey.js Normal file
View File

@ -0,0 +1,28 @@
import {
DataTypes,
sequelize,
} from '../sequelize';
import randomstring from 'randomstring';
const Team = sequelize.define('team', {
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
name: DataTypes.STRING,
secret: { type: DataTypes.STRING, unique: true },
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
},
},
}, {
tableName: 'apiKeys',
paranoid: true,
hooks: {
beforeValidate: (key) => {
key.secret = randomstring.generate(38);
},
},
});
export default Team;

View File

@ -3,6 +3,7 @@ import Team from './Team';
import Atlas from './Atlas';
import Document from './Document';
import Revision from './Revision';
import ApiKey from './ApiKey';
export {
User,
@ -10,4 +11,5 @@ export {
Atlas,
Document,
Revision,
ApiKey,
};

View File

@ -129,3 +129,11 @@ export function presentCollection(ctx, collection, includeRecentDocuments=false)
resolve(data);
});
}
export function presentApiKey(ctx, key) {
return {
id: key.id,
name: key.name,
secret: key.secret,
};
}