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,7 +2,55 @@ import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
const Application = observer((props) => { @observer
class Application extends React.Component {
static childContextTypes = {
rebass: React.PropTypes.object,
}
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 ( return (
<div style={{ width: '100%', height: '100%', display: 'flex', flex: 1 }}> <div style={{ width: '100%', height: '100%', display: 'flex', flex: 1 }}>
<Helmet <Helmet
@ -14,13 +62,10 @@ const Application = observer((props) => {
}, },
] } ] }
/> />
{ props.children } { this.props.children }
</div> </div>
); );
}); }
}
Application.propTypes = {
children: React.PropTypes.node.isRequired,
};
export default Application; export default Application;

View File

@ -2,6 +2,7 @@ import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Flex } from 'reflexbox'; import { Flex } from 'reflexbox';
import { Input, ButtonOutline, InlineForm } from 'rebass';
import Layout, { Title } from 'components/Layout'; import Layout, { Title } from 'components/Layout';
import CenteredContent from 'components/CenteredContent'; import CenteredContent from 'components/CenteredContent';
import SlackAuthLink from 'components/SlackAuthLink'; import SlackAuthLink from 'components/SlackAuthLink';
@ -19,6 +20,11 @@ class Settings extends React.Component {
this.store = new SettingsStore(); this.store = new SettingsStore();
} }
onKeyCreate = (e) => {
e.preventDefault();
this.store.createApiKey();
}
render() { render() {
const title = ( const title = (
<Title> <Title>
@ -34,8 +40,8 @@ class Settings extends React.Component {
loading={ this.store.isFetching } loading={ this.store.isFetching }
> >
<CenteredContent> <CenteredContent>
<div className={ styles.section }>
<h2 className={ styles.sectionHeader }>Slack</h2> <h2 className={ styles.sectionHeader }>Slack</h2>
<p> <p>
Connect Atlas to your Slack to instantly search for your documents Connect Atlas to your Slack to instantly search for your documents
using <code>/atlas</code> command. using <code>/atlas</code> command.
@ -47,6 +53,44 @@ class Settings extends React.Component {
> >
<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" /> <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>
</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> </CenteredContent>
</Layout> </Layout>
); );

View File

@ -1,4 +1,25 @@
@import '~styles/constants.scss';
.section {
margin-bottom: 40px;
}
.sectionHeader { .sectionHeader {
border-bottom: 1px solid #eee; 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 { observable, action, runInAction, toJS } from 'mobx';
// import { client } from 'utils/ApiClient'; import { client } from 'utils/ApiClient';
class SearchStore { 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; export default SearchStore;

View File

@ -1,7 +1,7 @@
$lightGray: #eee; $lightGray: #eee;
$textColor: #171B35; $textColor: #171B35;
$actionColor: #2da9e1; $actionColor: #3AA3E3;
$darkGray: #ccc; $darkGray: #ccc;
$lightGray: #eee; $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 collections from './collections';
import documents from './documents'; import documents from './documents';
import hooks from './hooks'; import hooks from './hooks';
import apiKeys from './apiKeys';
import validation from './validation'; import validation from './validation';
import methodOverride from '../middlewares/methodOverride'; import methodOverride from '../middlewares/methodOverride';
@ -52,6 +53,7 @@ router.use('/', user.routes());
router.use('/', collections.routes()); router.use('/', collections.routes());
router.use('/', documents.routes()); router.use('/', documents.routes());
router.use('/', hooks.routes()); router.use('/', hooks.routes());
router.use('/', apiKeys.routes());
// Router is embedded in a Koa application wrapper, because koa-router does not // Router is embedded in a Koa application wrapper, because koa-router does not
// allow middleware to catch any routes which were not explicitly defined. // 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 Atlas from './Atlas';
import Document from './Document'; import Document from './Document';
import Revision from './Revision'; import Revision from './Revision';
import ApiKey from './ApiKey';
export { export {
User, User,
@ -10,4 +11,5 @@ export {
Atlas, Atlas,
Document, Document,
Revision, Revision,
ApiKey,
}; };

View File

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