diff --git a/frontend/scenes/Application.js b/frontend/scenes/Application.js
index ac5e2e65..785a3dfb 100644
--- a/frontend/scenes/Application.js
+++ b/frontend/scenes/Application.js
@@ -2,25 +2,70 @@ import React from 'react';
import { observer } from 'mobx-react';
import Helmet from 'react-helmet';
-const Application = observer((props) => {
- return (
-
-
- { props.children }
-
- );
-});
+@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 (
+
+
+ { this.props.children }
+
+ );
+ }
+}
export default Application;
diff --git a/frontend/scenes/Settings/Settings.js b/frontend/scenes/Settings/Settings.js
index 369f65e3..dda64d68 100644
--- a/frontend/scenes/Settings/Settings.js
+++ b/frontend/scenes/Settings/Settings.js
@@ -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 = (
@@ -34,19 +40,57 @@ class Settings extends React.Component {
loading={ this.store.isFetching }
>
- Slack
+
+
Slack
+
+ Connect Atlas to your Slack to instantly search for your documents
+ using /atlas
command.
+
-
- Connect Atlas to your Slack to instantly search for your documents
- using /atlas
command.
-
-
-
-
-
+
+
+
+
+
+
+
API access
+
+ Create API tokens to hack on your Atlas.
+ Learn more in API documentation.
+
+
+ { this.store.apiKeys && (
+
+ { this.store.apiKeys.map(key => (
+
+ { key.name } |
+ { key.secret } |
+ {/*
+ Delete
+ | */}
+
+ )) }
+
+ ) }
+
+
+
+
+
+
);
diff --git a/frontend/scenes/Settings/Settings.scss b/frontend/scenes/Settings/Settings.scss
index 8074d9f0..64f268ed 100644
--- a/frontend/scenes/Settings/Settings.scss
+++ b/frontend/scenes/Settings/Settings.scss
@@ -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;
+}
diff --git a/frontend/scenes/Settings/SettingsStore.js b/frontend/scenes/Settings/SettingsStore.js
index 1b0502eb..67b9940c 100644
--- a/frontend/scenes/Settings/SettingsStore.js
+++ b/frontend/scenes/Settings/SettingsStore.js
@@ -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;
diff --git a/frontend/styles/constants.scss b/frontend/styles/constants.scss
index 7910a5f5..8e565739 100644
--- a/frontend/styles/constants.scss
+++ b/frontend/styles/constants.scss
@@ -1,7 +1,7 @@
$lightGray: #eee;
$textColor: #171B35;
-$actionColor: #2da9e1;
+$actionColor: #3AA3E3;
$darkGray: #ccc;
$lightGray: #eee;
diff --git a/server/api/apiKeys.js b/server/api/apiKeys.js
new file mode 100644
index 00000000..efafe616
--- /dev/null
+++ b/server/api/apiKeys.js
@@ -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;
diff --git a/server/api/index.js b/server/api/index.js
index ac92836d..c5000d39 100644
--- a/server/api/index.js
+++ b/server/api/index.js
@@ -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.
diff --git a/server/migrations/20160824061730-add-apikeys.js b/server/migrations/20160824061730-add-apikeys.js
new file mode 100644
index 00000000..7cff90f6
--- /dev/null
+++ b/server/migrations/20160824061730-add-apikeys.js
@@ -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');
+ },
+};
diff --git a/server/migrations/20160824062457-add-apikey-indeces.js b/server/migrations/20160824062457-add-apikey-indeces.js
new file mode 100644
index 00000000..1ed8ae22
--- /dev/null
+++ b/server/migrations/20160824062457-add-apikey-indeces.js
@@ -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']);
+ },
+};
diff --git a/server/models/ApiKey.js b/server/models/ApiKey.js
new file mode 100644
index 00000000..f54636a9
--- /dev/null
+++ b/server/models/ApiKey.js
@@ -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;
diff --git a/server/models/index.js b/server/models/index.js
index 9a238c61..db1d5bc0 100644
--- a/server/models/index.js
+++ b/server/models/index.js
@@ -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,
};
diff --git a/server/presenters.js b/server/presenters.js
index 0eb99a51..d5d24705 100644
--- a/server/presenters.js
+++ b/server/presenters.js
@@ -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,
+ };
+}