mirror of
https://github.com/outline/outline.git
synced 2025-03-28 14:34:35 +00:00
Implemented some of api keys
This commit is contained in:
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -1,7 +1,7 @@
|
||||
$lightGray: #eee;
|
||||
|
||||
$textColor: #171B35;
|
||||
$actionColor: #2da9e1;
|
||||
$actionColor: #3AA3E3;
|
||||
|
||||
$darkGray: #ccc;
|
||||
$lightGray: #eee;
|
||||
|
53
server/api/apiKeys.js
Normal file
53
server/api/apiKeys.js
Normal 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;
|
@ -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.
|
||||
|
46
server/migrations/20160824061730-add-apikeys.js
Normal file
46
server/migrations/20160824061730-add-apikeys.js
Normal 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');
|
||||
},
|
||||
};
|
13
server/migrations/20160824062457-add-apikey-indeces.js
Normal file
13
server/migrations/20160824062457-add-apikey-indeces.js
Normal 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
28
server/models/ApiKey.js
Normal 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;
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user