New ClientCertificate model and ability to have private certificates (#555)

* Pull out client certificates

* Add test for the migration
This commit is contained in:
Gregory Schier
2017-10-31 19:05:35 +01:00
committed by GitHub
parent 9a0ed72af2
commit 52fe473063
15 changed files with 373 additions and 161 deletions

View File

@@ -0,0 +1,56 @@
import * as models from '../index';
import {globalBeforeEach} from '../../__jest__/before-each';
describe('migrate()', () => {
beforeEach(globalBeforeEach);
it('migrates client certificates properly', async () => {
const workspace = await models.workspace.create({
name: 'My Workspace',
certificates: [
{key: 'key', passphrase: 'mypass'},
{disabled: true, cert: 'cert'}
]
});
const migratedWorkspace = await models.workspace.migrate(workspace);
const certs = await models.clientCertificate.findByParentId(workspace._id);
// Delete modified and created so we can assert them
for (const cert of certs) {
expect(typeof cert.modified).toBe('number');
expect(typeof cert.created).toBe('number');
delete cert.modified;
delete cert.created;
}
expect(certs.length).toBe(2);
expect(certs.sort((c1, c2) => c1._id > c2._id ? -1 : 1)).toEqual([{
_id: 'crt_a262d22b5fa8491c9bd958fba03e301e',
cert: null,
disabled: false,
isPrivate: false,
key: 'key',
parentId: 'wrk_cc1dd2ca4275747aa88199e8efd42403',
passphrase: 'mypass',
pfx: null,
type: 'ClientCertificate'
}, {
_id: 'crt_2e7c268809ee44b8900d5cbbaa7d3a19',
cert: 'cert',
disabled: false,
isPrivate: false,
key: null,
parentId: 'wrk_cc1dd2ca4275747aa88199e8efd42403',
passphrase: null,
pfx: null,
type: 'ClientCertificate'
}]);
expect(migratedWorkspace.certificates).toBeUndefined();
// Make sure we don't create new certs if we migrate again
await models.workspace.migrate(migratedWorkspace);
const certsAgain = await models.clientCertificate.findByParentId(workspace._id);
expect(certsAgain.length).toBe(2);
});
});

View File

@@ -0,0 +1,68 @@
// @flow
import * as db from '../common/database';
import type {BaseModel} from './index';
export const name = 'Client Certificate';
export const type = 'ClientCertificate';
export const prefix = 'crt';
export const canDuplicate = true;
type BaseClientCertificate = {
parentId: string,
host: string,
passphrase: string | null,
cert: string | null,
key: string | null,
pfx: string | null,
disabled: boolean,
// For sync control
isPrivate: boolean
};
export type ClientCertificate = BaseModel & BaseClientCertificate;
export function init (): BaseClientCertificate {
return {
parentId: '',
host: '',
passphrase: null,
disabled: false,
cert: null,
key: null,
pfx: null,
isPrivate: false
};
}
export async function migrate (doc: ClientCertificate) {
return doc;
}
export function create (patch: Object = {}): Promise<ClientCertificate> {
if (!patch.parentId) {
throw new Error('New ClientCertificate missing `parentId`: ' + JSON.stringify(patch));
}
return db.docCreate(type, patch);
}
export function update (cert: ClientCertificate, patch: Object = {}): Promise<ClientCertificate> {
return db.docUpdate(cert, patch);
}
export function getById (id: string): Promise<ClientCertificate | null> {
return db.get(type, id);
}
export function findByParentId (parentId: string): Promise<Array<ClientCertificate>> {
return db.find(type, {parentId});
}
export function remove (cert: ClientCertificate): Promise<void> {
return db.remove(cert);
}
export function all (): Promise<Array<ClientCertificate>> {
return db.all(type);
}

View File

@@ -12,6 +12,8 @@ type BaseEnvironment = {
name: string,
data: Object,
color: string | null,
// For sync control
isPrivate: boolean
};

View File

@@ -12,6 +12,7 @@ import * as _requestVersion from './request-version';
import * as _requestMeta from './request-meta';
import * as _response from './response';
import * as _oAuth2Token from './o-auth-2-token';
import * as _clientCertificate from './client-certificate';
import {generateId} from '../common/misc';
export type BaseModel = {
@@ -36,6 +37,7 @@ export const requestVersion = _requestVersion;
export const requestMeta = _requestMeta;
export const response = _response;
export const oAuth2Token = _oAuth2Token;
export const clientCertificate = _clientCertificate;
export function all () {
return [
@@ -51,7 +53,8 @@ export function all () {
requestVersion,
requestMeta,
response,
oAuth2Token
oAuth2Token,
clientCertificate
];
}

View File

@@ -10,14 +10,7 @@ export const canDuplicate = true;
type BaseWorkspace = {
name: string,
description: string,
certificates: Array<{
host: string,
passphrase: string,
cert: string,
key: string,
pfx: string
}>
description: string
};
export type Workspace = BaseModel & BaseWorkspace;
@@ -25,10 +18,7 @@ export type Workspace = BaseModel & BaseWorkspace;
export function init () {
return {
name: 'New Workspace',
description: '',
certificates: [
// {host, port, cert, key, pfx, passphrase}
]
description: ''
};
}
@@ -41,6 +31,7 @@ export async function migrate (doc: Workspace): Promise<Workspace> {
}
await _ensureDependencies(doc);
doc = await _migrateExtractClientCertificates(doc);
return doc;
}
@@ -82,3 +73,29 @@ async function _ensureDependencies (workspace: Workspace) {
await models.cookieJar.getOrCreateForParentId(workspace._id);
await models.environment.getOrCreateForWorkspaceId(workspace._id);
}
async function _migrateExtractClientCertificates (workspace: Workspace) {
if (!Array.isArray(workspace.certificates)) {
// Already migrated
return workspace;
}
for (const cert of workspace.certificates) {
await models.clientCertificate.create({
parentId: workspace._id,
host: cert.host,
passphrase: cert.passphrase || null,
cert: cert.cert || null,
key: cert.key || null,
pfx: cert.pfx || null,
isPrivate: false
});
}
delete workspace.certificates;
// This will remove the now-missing `certificates` property
await update(workspace, {});
return workspace;
}

View File

@@ -75,7 +75,6 @@ describe('actuallySend()', () => {
ACCEPT_ENCODING: '',
COOKIEFILE: '',
FOLLOWLOCATION: true,
MAXREDIRS: -1,
HTTPHEADER: [
'Content-Type: application/json',
'Expect: ',
@@ -129,7 +128,6 @@ describe('actuallySend()', () => {
ACCEPT_ENCODING: '',
COOKIEFILE: '',
FOLLOWLOCATION: true,
MAXREDIRS: -1,
HTTPHEADER: [
'Content-Type: application/x-www-form-urlencoded',
'Expect: ',
@@ -208,7 +206,6 @@ describe('actuallySend()', () => {
CUSTOMREQUEST: 'GET',
ACCEPT_ENCODING: '',
FOLLOWLOCATION: true,
MAXREDIRS: -1,
HTTPHEADER: [
'Content-Type: application/json',
'Expect: ',
@@ -259,7 +256,6 @@ describe('actuallySend()', () => {
CUSTOMREQUEST: 'POST',
COOKIEFILE: '',
FOLLOWLOCATION: true,
MAXREDIRS: -1,
HTTPHEADER: [
'Content-Type: application/octet-stream',
'Expect: ',
@@ -316,7 +312,6 @@ describe('actuallySend()', () => {
ACCEPT_ENCODING: '',
COOKIEFILE: '',
FOLLOWLOCATION: true,
MAXREDIRS: -1,
CUSTOMREQUEST: 'POST',
HTTPHEADER: [
'Content-Type: multipart/form-data; boundary=X-INSOMNIA-BOUNDARY',
@@ -369,7 +364,6 @@ describe('actuallySend()', () => {
ACCEPT_ENCODING: '',
COOKIEFILE: '',
FOLLOWLOCATION: true,
MAXREDIRS: -1,
HTTPHEADER: ['content-type: '],
NOPROGRESS: false,
PROXY: '',
@@ -404,7 +398,6 @@ describe('actuallySend()', () => {
ACCEPT_ENCODING: '',
COOKIEFILE: '',
FOLLOWLOCATION: true,
MAXREDIRS: -1,
HTTPHEADER: ['content-type: '],
NOPROGRESS: false,
PROXY: '',
@@ -438,7 +431,6 @@ describe('actuallySend()', () => {
ACCEPT_ENCODING: '',
COOKIEFILE: '',
FOLLOWLOCATION: true,
MAXREDIRS: -1,
HTTPHEADER: ['content-type: '],
NOPROGRESS: false,
PROXY: '',
@@ -473,7 +465,6 @@ describe('actuallySend()', () => {
ACCEPT_ENCODING: '',
COOKIEFILE: '',
FOLLOWLOCATION: true,
MAXREDIRS: -1,
HTTPHEADER: ['content-type: '],
NOPROGRESS: false,
PROXY: '',

View File

@@ -132,12 +132,17 @@ export function _actuallySend (
// Set all the basic options
setOpt(Curl.option.FOLLOWLOCATION, settings.followRedirects);
setOpt(Curl.option.MAXREDIRS, settings.maxRedirects);
setOpt(Curl.option.TIMEOUT_MS, settings.timeout); // 0 for no timeout
setOpt(Curl.option.VERBOSE, true); // True so debug function works
setOpt(Curl.option.NOPROGRESS, false); // False so progress function works
setOpt(Curl.option.ACCEPT_ENCODING, ''); // Auto decode everything
// Set maximum amount of redirects allowed
// NOTE: Setting this to -1 breaks some versions of libcurl
if (settings.maxRedirects > 0) {
setOpt(Curl.option.MAXREDIRS, settings.maxRedirects);
}
// Only set CURLOPT_CUSTOMREQUEST if not HEAD or GET. This is because Curl
// See https://curl.haxx.se/libcurl/c/CURLOPT_CUSTOMREQUEST.html
switch (renderedRequest.method.toUpperCase()) {
@@ -327,7 +332,8 @@ export function _actuallySend (
}
// Set client certs if needed
for (const certificate of workspace.certificates) {
const clientCertificates = await models.clientCertificate.findByParentId(workspace._id);
for (const certificate of clientCertificates) {
if (certificate.disabled) {
continue;
}

View File

@@ -17,7 +17,8 @@ const WHITE_LIST = {
[models.request.type]: true,
[models.requestGroup.type]: true,
[models.environment.type]: true,
[models.cookieJar.type]: true
[models.cookieJar.type]: true,
[models.clientCertificate.type]: true
};
export const logger = new Logger();

View File

@@ -1,5 +1,5 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
// @flow
import * as React from 'react';
import autobind from 'autobind-decorator';
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs';
import DebouncedInput from '../base/debounced-input';
@@ -12,25 +12,56 @@ import PromptButton from '../base/prompt-button';
import * as models from '../../../models/index';
import {trackEvent} from '../../../analytics/index';
import MarkdownEditor from '../markdown-editor';
import type {Workspace} from '../../../models/workspace';
import type {ClientCertificate} from '../../../models/client-certificate';
type Props = {
clientCertificates: Array<ClientCertificate>,
workspace: Workspace,
editorFontSize: number,
editorIndentSize: number,
editorKeyMap: string,
editorLineWrapping: boolean,
nunjucksPowerUserMode: boolean,
handleRender: Function,
handleGetRenderContext: Function,
handleRemoveWorkspace: Function,
handleDuplicateWorkspace: Function
};
type State = {
showAddCertificateForm: boolean,
host: string,
crtPath: string,
keyPath: string,
pfxPath: string,
isPrivate: boolean,
passphrase: string,
showDescription: boolean,
defaultPreviewMode: boolean
};
@autobind
class WorkspaceSettingsModal extends PureComponent {
constructor (props) {
class WorkspaceSettingsModal extends React.PureComponent<Props, State> {
modal: Modal | null;
constructor (props: Props) {
super(props);
this.state = {
showAddCertificateForm: false,
host: '',
crtPath: '',
keyPath: '',
pfxPath: '',
host: '',
passphrase: '',
isPrivate: false,
showDescription: false,
defaultPreviewMode: false
};
}
_workspaceUpdate (patch) {
_workspaceUpdate (patch: Object) {
models.workspace.update(this.props.workspace, patch);
}
@@ -39,7 +70,7 @@ class WorkspaceSettingsModal extends PureComponent {
trackEvent('Workspace', 'Add Description');
}
_handleSetModalRef (n) {
_handleSetModalRef (n: ?Modal) {
this.modal = n;
}
@@ -55,14 +86,22 @@ class WorkspaceSettingsModal extends PureComponent {
}
_handleToggleCertificateForm () {
this.setState({showAddCertificateForm: !this.state.showAddCertificateForm});
this.setState(state => ({
showAddCertificateForm: !state.showAddCertificateForm,
crtPath: '',
keyPath: '',
pfxPath: '',
host: '',
passphrase: '',
isPrivate: false
}));
}
_handleRename (name) {
_handleRename (name: string) {
this._workspaceUpdate({name});
}
_handleDescriptionChange (description) {
_handleDescriptionChange (description: string) {
this._workspaceUpdate({description});
if (this.state.defaultPreviewMode !== false) {
@@ -70,84 +109,75 @@ class WorkspaceSettingsModal extends PureComponent {
}
}
_handleCreateHostChange (e) {
this.setState({host: e.target.value});
_handleCreateHostChange (e: SyntheticEvent<HTMLInputElement>) {
this.setState({host: e.currentTarget.value});
}
_handleCreatePfxChange (pfxPath) {
_handleCreatePfxChange (pfxPath: string) {
this.setState({pfxPath});
}
_handleCreateCrtChange (crtPath) {
_handleCreateCrtChange (crtPath: string) {
this.setState({crtPath});
}
_handleCreateKeyChange (keyPath) {
_handleCreateKeyChange (keyPath: string) {
this.setState({keyPath});
}
_handleCreatePassphraseChange (e) {
this.setState({passphrase: e.target.value});
_handleCreatePassphraseChange (e: SyntheticEvent<HTMLInputElement>) {
this.setState({passphrase: e.currentTarget.value});
}
async _handleSubmitCertificate (e) {
_handleCreateIsPrivateChange (e: SyntheticEvent<HTMLInputElement>) {
this.setState({isPrivate: e.currentTarget.checked});
}
async _handleCreateCertificate (e: SyntheticEvent<HTMLFormElement>) {
e.preventDefault();
const {workspace} = this.props;
const {pfxPath, crtPath, keyPath, host, passphrase} = this.state;
const {pfxPath, crtPath, keyPath, host, passphrase, isPrivate} = this.state;
const certificate = {
host,
passphrase,
cert: crtPath,
key: keyPath,
pfx: pfxPath,
disabled: false
isPrivate,
parentId: workspace._id,
passphrase: passphrase || null,
disabled: false,
cert: crtPath || null,
key: keyPath || null,
pfx: pfxPath || null
};
const certificates = [
...workspace.certificates.filter(c => c.host !== certificate.host),
certificate
];
await models.workspace.update(workspace, {certificates});
await models.clientCertificate.create(certificate);
this._handleToggleCertificateForm();
trackEvent('Certificates', 'Create');
}
_handleDeleteCertificate (certificate) {
const {workspace} = this.props;
const certificates = workspace.certificates.filter(c => c.host !== certificate.host);
models.workspace.update(workspace, {certificates});
async _handleDeleteCertificate (certificate: ClientCertificate) {
await models.clientCertificate.remove(certificate);
trackEvent('Certificates', 'Delete');
}
_handleToggleCertificate (certificate) {
const {workspace} = this.props;
const certificates = workspace.certificates.map(
c => c === certificate ? Object.assign({}, c, {disabled: !c.disabled}) : c
);
models.workspace.update(workspace, {certificates});
async _handleToggleCertificate (certificate: ClientCertificate) {
await models.clientCertificate.update(certificate, {disabled: !certificate.disabled});
trackEvent('Certificates', 'Toggle');
}
show () {
const hasDescription = !!this.props.workspace.description;
this.setState({
showAddCertificateForm: false,
crtPath: '',
keyPath: '',
pfxPath: '',
host: '',
passphrase: '',
showDescription: hasDescription,
defaultPreviewMode: hasDescription
defaultPreviewMode: hasDescription,
showAddCertificateForm: false
});
this.modal.show();
this.modal && this.modal.show();
}
hide () {
this.modal.hide();
this.modal && this.modal.hide();
}
renderModalHeader () {
@@ -163,8 +193,73 @@ class WorkspaceSettingsModal extends PureComponent {
);
}
renderCertificate (certificate: ClientCertificate) {
return (
<div key={certificate._id}>
<div className="row-spaced">
<div>
<span className="pad-right no-wrap">
<strong>PFX:</strong>
{' '}
{certificate.pfx
? <i className="fa fa-check"/>
: <i className="fa fa-remove"/>
}
</span>
<span className="pad-right no-wrap">
<strong>CRT:</strong>
{' '}
{certificate.cert
? <i className="fa fa-check"/>
: <i className="fa fa-remove"/>
}
</span>
<span className="pad-right no-wrap">
<strong>Key:</strong>
{' '}
{certificate.key
? <i className="fa fa-check"/>
: <i className="fa fa-remove"/>
}
</span>
<span className="pad-right no-wrap" title={certificate.passphrase || null}>
<strong>Passphrase:</strong>
{' '}
{certificate.passphrase
? <i className="fa fa-check"/>
: <i className="fa fa-remove"/>
}
</span>
<span className="pad-right">
<strong>Host:</strong>
{' '}
<span className="monospace selectable">{certificate.host}</span>
</span>
</div>
<div className="no-wrap">
<button className="btn btn--super-compact width-auto"
title="Enable or disable certificate"
onClick={() => this._handleToggleCertificate(certificate)}>
{certificate.disabled
? <i className="fa fa-square-o"/>
: <i className="fa fa-check-square-o"/>
}
</button>
<PromptButton className="btn btn--super-compact width-auto"
confirmMessage=" "
addIcon
onClick={() => this._handleDeleteCertificate(certificate)}>
<i className="fa fa-trash-o"/>
</PromptButton>
</div>
</div>
</div>
);
}
renderModalBody () {
const {
clientCertificates,
workspace,
editorLineWrapping,
editorFontSize,
@@ -175,10 +270,14 @@ class WorkspaceSettingsModal extends PureComponent {
nunjucksPowerUserMode
} = this.props;
const publicCertificates = clientCertificates.filter(c => !c.isPrivate);
const privateCertificates = clientCertificates.filter(c => c.isPrivate);
const {
pfxPath,
crtPath,
keyPath,
isPrivate,
showAddCertificateForm,
showDescription,
defaultPreviewMode
@@ -246,84 +345,38 @@ class WorkspaceSettingsModal extends PureComponent {
<TabPanel className="react-tabs__tab-panel pad scrollable">
{!showAddCertificateForm ? (
<div>
{workspace.certificates.length === 0 ? (
<p className="notice info margin-top-sm">
{clientCertificates.length === 0 ? (
<p className="notice surprise margin-top-sm">
You have not yet added any certificates
</p>
) : workspace.certificates.map(certificate => (
<div key={certificate.host}>
<p className="notice info no-margin-top">
Client certificates are an experimental feature
</p>
<div className="row-spaced">
<div>
<span className="pad-right no-wrap">
<strong>PFX:</strong>
{' '}
{certificate.pfx
? <i className="fa fa-check"/>
: <i className="fa fa-remove"/>
}
</span>
<span className="pad-right no-wrap">
<strong>CRT:</strong>
{' '}
{certificate.cert
? <i className="fa fa-check"/>
: <i className="fa fa-remove"/>
}
</span>
<span className="pad-right no-wrap">
<strong>Key:</strong>
{' '}
{certificate.key
? <i className="fa fa-check"/>
: <i className="fa fa-remove"/>
}
</span>
<span className="pad-right no-wrap" title={certificate.passphrase || null}>
<strong>Passphrase:</strong>
{' '}
{certificate.passphrase
? <i className="fa fa-check"/>
: <i className="fa fa-remove"/>
}
</span>
<span className="pad-right">
<strong>Host:</strong>
{' '}
<span className="monospace selectable">{certificate.host}</span>
</span>
</div>
<div className="no-wrap">
<button className="btn btn--super-compact width-auto"
title="Enable or disable certificate"
onClick={() => this._handleToggleCertificate(certificate)}>
{certificate.disabled
? <i className="fa fa-square-o"/>
: <i className="fa fa-check-square-o"/>
}
</button>
<PromptButton className="btn btn--super-compact width-auto"
confirmMessage=" "
addIcon
onClick={() => this._handleDeleteCertificate(certificate)}>
<i className="fa fa-trash-o"/>
</PromptButton>
</div>
</div>
) : null}
{publicCertificates.length > 0
? publicCertificates.map(this.renderCertificate)
: null
}
{privateCertificates.length > 0 ? (
<div>
<h2>
Private Certificates
<HelpTooltip position="right" className="space-left">
Private certificates will not by synced.
</HelpTooltip>
</h2>
{privateCertificates.map(this.renderCertificate)}
</div>
))}
) : null}
<hr className="hr--spaced"/>
<div className="text-center">
<button className="btn btn--clicky auto"
onClick={this._handleToggleCertificateForm}>
Add Certificate
Create Certificate
</button>
</div>
</div>
) : (
<form onSubmit={this._handleSubmitCertificate}>
<form onSubmit={this._handleCreateCertificate}>
<div className="form-control form-control--outlined no-pad-top">
<label>Host
<HelpTooltip position="right" className="space-left">
@@ -387,16 +440,27 @@ class WorkspaceSettingsModal extends PureComponent {
/>
</label>
</div>
<div className="form-control form-control--slim">
<label>Private
<HelpTooltip className="space-left">
Private certificates will not be synced
</HelpTooltip>
<input
type="checkbox"
value={isPrivate}
onChange={this._handleCreateIsPrivateChange}
/>
</label>
</div>
<br/>
<div className="pad-top text-right">
<button type="button"
className="btn btn--super-compact"
className="btn btn--super-compact space-right"
onClick={this._handleToggleCertificateForm}>
Cancel
</button>
{' '}
<button className="btn btn--clicky" type="submit">
Add Certificate
<button className="btn btn--clicky space-right" type="submit">
Create Certificate
</button>
</div>
</form>
@@ -418,17 +482,4 @@ class WorkspaceSettingsModal extends PureComponent {
}
}
WorkspaceSettingsModal.propTypes = {
workspace: PropTypes.object.isRequired,
editorFontSize: PropTypes.number.isRequired,
editorIndentSize: PropTypes.number.isRequired,
editorKeyMap: PropTypes.string.isRequired,
editorLineWrapping: PropTypes.bool.isRequired,
nunjucksPowerUserMode: PropTypes.bool.isRequired,
handleRender: PropTypes.func.isRequired,
handleGetRenderContext: PropTypes.func.isRequired,
handleRemoveWorkspace: PropTypes.func.isRequired,
handleDuplicateWorkspace: PropTypes.func.isRequired
};
export default WorkspaceSettingsModal;

View File

@@ -42,6 +42,7 @@ import * as importers from 'insomnia-importers';
import type {CookieJar} from '../../models/cookie-jar';
import type {Environment} from '../../models/environment';
import ErrorBoundary from './error-boundary';
import type {ClientCertificate} from '../../models/client-certificate';
type Props = {
// Helper Functions
@@ -102,6 +103,7 @@ type Props = {
activeWorkspace: Workspace,
activeCookieJar: CookieJar,
activeEnvironment: Environment | null,
activeWorkspaceClientCertificates: Array<ClientCertificate>,
// Optional
oAuth2Token: ?OAuth2Token,
@@ -339,6 +341,7 @@ class Wrapper extends React.PureComponent<Props, State> {
activeCookieJar,
activeRequestResponses,
activeResponse,
activeWorkspaceClientCertificates,
environments,
handleActivateRequest,
handleCreateRequest,
@@ -455,6 +458,7 @@ class Wrapper extends React.PureComponent<Props, State> {
<WorkspaceSettingsModal
ref={registerModal}
clientCertificates={activeWorkspaceClientCertificates}
workspace={activeWorkspace}
editorFontSize={settings.editorFontSize}
editorIndentSize={settings.editorIndentSize}

View File

@@ -21,7 +21,7 @@ import * as globalActions from '../redux/modules/global';
import * as db from '../../common/database';
import * as models from '../../models';
import {trackEvent} from '../../analytics';
import {selectActiveCookieJar, selectActiveOAuth2Token, selectActiveRequest, selectActiveRequestMeta, selectActiveRequestResponses, selectActiveResponse, selectActiveWorkspace, selectActiveWorkspaceMeta, selectEntitiesLists, selectSidebarChildren, selectUnseenWorkspaces, selectWorkspaceRequestsAndRequestGroups} from '../redux/selectors';
import {selectActiveCookieJar, selectActiveOAuth2Token, selectActiveRequest, selectActiveRequestMeta, selectActiveRequestResponses, selectActiveResponse, selectActiveWorkspace, selectActiveWorkspaceClientCertificates, selectActiveWorkspaceMeta, selectEntitiesLists, selectSidebarChildren, selectUnseenWorkspaces, selectWorkspaceRequestsAndRequestGroups} from '../redux/selectors';
import RequestCreateModal from '../components/modals/request-create-modal';
import GenerateCodeModal from '../components/modals/generate-code-modal';
import WorkspaceSettingsModal from '../components/modals/workspace-settings-modal';
@@ -818,6 +818,7 @@ function mapStateToProps (state, props) {
// Workspace stuff
const workspaceMeta = selectActiveWorkspaceMeta(state, props) || {};
const activeWorkspace = selectActiveWorkspace(state, props);
const activeWorkspaceClientCertificates = selectActiveWorkspaceClientCertificates(state, props);
const sidebarHidden = workspaceMeta.sidebarHidden || false;
const sidebarFilter = workspaceMeta.sidebarFilter || '';
const sidebarWidth = workspaceMeta.sidebarWidth || DEFAULT_SIDEBAR_WIDTH;
@@ -861,6 +862,7 @@ function mapStateToProps (state, props) {
isLoading,
loadStartTime,
activeWorkspace,
activeWorkspaceClientCertificates,
activeRequest,
activeRequestResponses,
activeResponse,

View File

@@ -38,7 +38,7 @@
@dropdown-min-width: round(@font-size * 12);
/* Modals */
@modal-width: round(@font-size * 50);
@modal-width: round(@font-size * 60);
@modal-width-wide: round(@font-size * 70);
/* Other */

View File

@@ -5,6 +5,7 @@ import {Provider} from 'react-redux';
import {DragDropContext} from 'react-dnd';
import App from './containers/app';
import * as models from '../models';
import * as db from '../common/database';
import {types as modelTypes} from '../models';
import {init as initStore} from './redux/modules';
import {init as initDB} from '../common/database';
@@ -53,4 +54,5 @@ import {isDevelopment} from '../common/constants';
// Export some useful things for dev
if (isDevelopment()) {
window.models = models;
window.db = db;
}

View File

@@ -49,7 +49,8 @@ async function getAllDocs () {
...await models.request.all(),
...await models.requestMeta.all(),
...await models.response.all(),
...await models.oAuth2Token.all()
...await models.oAuth2Token.all(),
...await models.clientCertificate.all()
];
return allDocs;

View File

@@ -28,6 +28,14 @@ export const selectActiveWorkspace = createSelector(
}
);
export const selectActiveWorkspaceClientCertificates = createSelector(
selectEntitiesLists,
selectActiveWorkspace,
(entities, activeWorkspace) => {
return entities.clientCertificates.filter(c => c.parentId === activeWorkspace._id);
}
);
export const selectActiveWorkspaceMeta = createSelector(
selectActiveWorkspace,
selectEntitiesLists,