Security Definitions component

This commit is contained in:
Roman Hotsiy 2017-11-22 23:38:38 +02:00
parent ed13bb0528
commit cec37df7aa
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
11 changed files with 267 additions and 25 deletions

View File

@ -3,6 +3,7 @@ import styled, { css } from '../styled-components';
const headerFontSize = { const headerFontSize = {
'1': '1.85714em', '1': '1.85714em',
'2': '1.57143em', '2': '1.57143em',
'3': '1.27em',
}; };
export const headerCommonMixin = level => css` export const headerCommonMixin = level => css`
@ -22,6 +23,11 @@ export const H2 = styled.h2`
color: black; color: black;
`; `;
export const H3 = styled.h2`
${headerCommonMixin(3)};
color: black;
`;
export const UnderlinedHeader = styled.h5` export const UnderlinedHeader = styled.h5`
border-bottom: 1px solid rgba(38, 50, 56, 0.3); border-bottom: 1px solid rgba(38, 50, 56, 0.3);
margin: 1em 0 1em 0; margin: 1em 0 1em 0;

View File

@ -1,12 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Markdown } from '../Markdown/Markdown'; import { AppStore } from '../../services/AppStore';
import { OpenAPIExternalDocumentation } from '../../types';
import { ApiInfoModel } from '../../services/models';
import { SecurityDefs } from '../SecurityDefs/SecurityDefs'; import { SecurityDefs } from '../SecurityDefs/SecurityDefs';
import { Markdown } from '../Markdown/Markdown';
import { MiddlePanel, DarkRightPanel, Row } from '../../common-elements/'; import { MiddlePanel, DarkRightPanel, Row } from '../../common-elements/';
import { import {
@ -18,14 +16,14 @@ import {
} from './styled.elements'; } from './styled.elements';
interface ApiInfoProps { interface ApiInfoProps {
info: ApiInfoModel; store: AppStore;
externalDocs?: OpenAPIExternalDocumentation;
} }
@observer @observer
export class ApiInfo extends React.Component<ApiInfoProps> { export class ApiInfo extends React.Component<ApiInfoProps> {
render() { render() {
const { info, externalDocs } = this.props; const { store } = this.props;
const { info, externalDocs } = store.spec;
const downloadFilename = info.downloadFileName; const downloadFilename = info.downloadFileName;
const downloadLink = info.downloadLink; const downloadLink = info.downloadLink;
@ -100,7 +98,15 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
<Markdown <Markdown
source={info.description || ''} source={info.description || ''}
raw={false} raw={false}
components={{ 'security-definitions': SecurityDefs }} components={{
'security-definitions': {
component: SecurityDefs,
propsSelector: store => ({
securitySchemes: store!.spec.security,
}),
},
}}
store={store}
/> />
</div> </div>
</MiddlePanel> </MiddlePanel>

View File

@ -1,20 +1,27 @@
import * as React from 'react'; import * as React from 'react';
import styled from '../../styled-components'; import styled from '../../styled-components';
import { MarkdownRenderer } from '../../services'; import { AppStore, MarkdownRenderer } from '../../services';
import { ComponentWithOptions } from '../OptionsProvider'; import { ComponentWithOptions } from '../OptionsProvider';
import * as DOMPurify from 'dompurify'; import * as DOMPurify from 'dompurify';
import { markdownCss } from './styles'; import { markdownCss } from './styles';
interface MarkdownProps { export type MDComponent = {
component: React.ComponentClass;
propsSelector: (store?: AppStore) => any;
attrs?: object;
};
export interface MarkdownProps {
source: string; source: string;
dense?: boolean; dense?: boolean;
inline?: boolean; inline?: boolean;
className?: string; className?: string;
raw?: boolean; raw?: boolean;
components?: { [name: string]: React.ComponentClass }; components?: Dict<MDComponent>;
sanitize?: boolean; sanitize?: boolean;
store?: AppStore;
} }
class InternalMarkdown extends ComponentWithOptions<MarkdownProps> { class InternalMarkdown extends ComponentWithOptions<MarkdownProps> {
@ -27,6 +34,12 @@ class InternalMarkdown extends ComponentWithOptions<MarkdownProps> {
} }
render() { render() {
const { source, raw, className, components, inline, dense, store } = this.props;
if (components && !store) {
throw new Error('When using components with Markdwon in ReDoc, store prop must be provided');
}
let sanitize: (string) => string; let sanitize: (string) => string;
if (this.props.sanitize || this.options.untrustedSpec) { if (this.props.sanitize || this.options.untrustedSpec) {
@ -36,7 +49,6 @@ class InternalMarkdown extends ComponentWithOptions<MarkdownProps> {
} }
const renderer = new MarkdownRenderer(); const renderer = new MarkdownRenderer();
const { source, raw, className, components, inline, dense } = this.props;
const parts = components const parts = components
? renderer.renderMdWithComponents(source, components, raw) ? renderer.renderMdWithComponents(source, components, raw)
: [renderer.renderMd(source, raw)]; : [renderer.renderMd(source, raw)];
@ -63,7 +75,7 @@ class InternalMarkdown extends ComponentWithOptions<MarkdownProps> {
typeof part === 'string' ? ( typeof part === 'string' ? (
<div key={idx} dangerouslySetInnerHTML={{ __html: sanitize(part) }} /> <div key={idx} dangerouslySetInnerHTML={{ __html: sanitize(part) }} />
) : ( ) : (
<part.component key={idx} {...part.attrs} /> <part.component key={idx} {...{ ...part.attrs, ...part.propsSelector(store) }} />
), ),
)} )}
</div> </div>

View File

@ -88,8 +88,8 @@ export const markdownCss = css`
word-break: keep-all; word-break: keep-all;
border-collapse: collapse; border-collapse: collapse;
border-spacing: 0; border-spacing: 0;
margin-top: 0.5em; margin-top: 1.5em;
margin-bottom: 0.5em; margin-bottom: 1.5em;
} }
table tr { table tr {

View File

@ -27,6 +27,7 @@ export class Redoc extends React.Component<RedocProps> {
render() { render() {
const { store: { spec, menu, options } } = this.props; const { store: { spec, menu, options } } = this.props;
const store = this.props.store;
return ( return (
<ThemeProvider theme={options.theme}> <ThemeProvider theme={options.theme}>
<OptionsProvider options={options}> <OptionsProvider options={options}>
@ -36,7 +37,7 @@ export class Redoc extends React.Component<RedocProps> {
<SideMenu menu={menu} /> <SideMenu menu={menu} />
</StickySidebar> </StickySidebar>
<ApiContent className="api-content"> <ApiContent className="api-content">
<ApiInfo info={spec.info} externalDocs={spec.externalDocs} /> <ApiInfo store={store} />
<ContentItems items={menu.items as any} /> <ContentItems items={menu.items as any} />
</ApiContent> </ApiContent>
</RedocWrap> </RedocWrap>

View File

@ -1,7 +1,126 @@
import * as React from 'react'; import * as React from 'react';
export class SecurityDefs extends React.PureComponent { import { SecuritySchemesModel } from '../../services/models/';
import styled from '../../styled-components';
import { H2 } from '../../common-elements';
import { Markdown } from '../Markdown/Markdown';
import { OpenAPISecurityScheme } from '../../types';
const AUTH_TYPES = {
oauth2: 'OAuth2',
apiKey: 'API Key',
basic: 'Basic Authorization',
openIdConnect: 'Open ID Connect',
};
export interface OAuthFlowProps {
type: string;
flow: OpenAPISecurityScheme['flows'][keyof OpenAPISecurityScheme['flows']];
}
const AuthTable = styled.table`
ul > li {
margin: 0.5em 0 !important;
}
`;
export class OAuthFlow extends React.PureComponent<OAuthFlowProps> {
render() { render() {
return <h1> Security Definitions here </h1>; const { type, flow } = this.props;
return (
<tr>
<th> {type} OAuth Flow </th>
<td>
{type === 'implicit' || type === 'authorizationCode' ? (
<div>
<strong> Authorization URL: </strong>
{(flow as any).authorizationUrl}
</div>
) : null}
{type === 'password' || type === 'clientCredentials' || type === 'authorizationCode' ? (
<div>
<strong> Token URL: </strong>
{(flow as any).tokenUrl}
</div>
) : null}
{flow!.refreshUrl && (
<div>
<strong> Refresh URL: </strong>
{flow!.refreshUrl}
</div>
)}
<div>
<strong> Scopes: </strong>
</div>
<ul>
{Object.keys(flow!.scopes).map(scope => (
<li>
<code>{scope}</code> - {flow!.scopes[scope]}
</li>
))}
</ul>
</td>
</tr>
);
}
}
export interface SecurityDefsProps {
securitySchemes?: SecuritySchemesModel;
}
export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
render() {
if (!this.props.securitySchemes) return null;
return (
<div>
{this.props.securitySchemes.schemes.map(scheme => (
<div key={scheme.id}>
<H2>{scheme.id}</H2>
<Markdown source={scheme.description || ''} />
<AuthTable className="security-details">
<tbody>
<tr>
<th> Security scheme type: </th>
<td> {AUTH_TYPES[scheme.type]} </td>
</tr>
{scheme.apiKey ? (
<tr>
<th> {scheme.apiKey.in} parameter name:</th>
<td> {scheme.apiKey.name} </td>
</tr>
) : scheme.http ? (
[
<tr>
<th> HTTP Authorization Scheme </th>
<th> {scheme.http.scheme} </th>
</tr>,
<tr>
<th> Bearer format </th>
<th> "{scheme.http.bearerFormat}" </th>
</tr>,
]
) : scheme.openId ? (
<tr>
<th> Connect URL </th>
<td>
<a target="_blank" href={scheme.openId.connectUrl}>
{scheme.openId.connectUrl}
</a>
</td>
</tr>
) : scheme.flows ? (
Object.keys(scheme.flows).map(type => (
<OAuthFlow key={type} type={type} flow={scheme.flows[type]} />
))
) : null}
</tbody>
</AuthTable>
</div>
))}
</div>
);
} }
} }

View File

@ -1,4 +1,6 @@
import * as Remarkable from 'remarkable'; import * as Remarkable from 'remarkable';
import { MDComponent } from '../components/Markdown/Markdown';
import { IMenuItem, SECTION_ATTR } from './MenuStore'; import { IMenuItem, SECTION_ATTR } from './MenuStore';
import { GroupModel } from './models'; import { GroupModel } from './models';
import { highlight } from '../utils'; import { highlight } from '../utils';
@ -145,9 +147,9 @@ export class MarkdownRenderer {
renderMdWithComponents( renderMdWithComponents(
rawText: string, rawText: string,
components: { [name: string]: React.ComponentClass }, components: Dict<MDComponent>,
raw: boolean = true, raw: boolean = true,
): (string | { component: React.ComponentClass; attrs: any })[] { ): (string | MDComponent)[] {
let componentDefs: string[] = []; let componentDefs: string[] = [];
let match; let match;
let anyCompRegexp = new RegExp(COMPONENT_REGEXP.replace('{component}', '(.*?)'), 'gmi'); let anyCompRegexp = new RegExp(COMPONENT_REGEXP.replace('{component}', '(.*?)'), 'gmi');
@ -165,8 +167,9 @@ export class MarkdownRenderer {
} }
if (componentDefs[i]) { if (componentDefs[i]) {
const { componentName, attrs } = parseComponent(componentDefs[i]); const { componentName, attrs } = parseComponent(componentDefs[i]);
if (!componentName) continue;
res.push({ res.push({
component: componentName && components[componentName], ...components[componentName],
attrs: attrs, attrs: attrs,
}); });
} }

View File

@ -7,7 +7,7 @@ import { MenuBuilder } from './MenuBuilder';
import { OpenAPIParser } from './OpenAPIParser'; import { OpenAPIParser } from './OpenAPIParser';
import { ApiInfoModel } from './models/ApiInfo'; import { ApiInfoModel } from './models/ApiInfo';
import { RedocNormalizedOptions } from './RedocNormalizedOptions'; import { RedocNormalizedOptions } from './RedocNormalizedOptions';
import { SecuritySchemesModel } from './models/SecuritySchemes';
/** /**
* Store that containts all the specification related information in the form of tree * Store that containts all the specification related information in the form of tree
*/ */
@ -39,7 +39,7 @@ export class SpecStore {
@computed @computed
get security() { get security() {
// TODO: implement security const schemes = this.parser.spec.components && this.parser.spec.components.securitySchemes;
throw new Error('Not implemented'); return schemes && new SecuritySchemesModel(this.parser);
} }
} }

View File

@ -0,0 +1,63 @@
import { OpenAPISecurityScheme, Referenced } from '../../types';
import { OpenAPIParser } from '../OpenAPIParser';
export class SecuritySchemeModel {
id: string;
type: OpenAPISecurityScheme['type'];
description: string;
apiKey?: {
name: string;
in: OpenAPISecurityScheme['in'];
};
http?: {
scheme: string;
bearerFormat?: string;
};
flows: OpenAPISecurityScheme['flows'];
openId?: {
connectUrl: string;
};
constructor(parser: OpenAPIParser, id: string, scheme: Referenced<OpenAPISecurityScheme>) {
const info = parser.deref(scheme);
this.id = id;
this.type = info.type;
this.description = info.description || '';
if (info.type === 'apiKey') {
this.apiKey = {
name: info.name!,
in: info.in,
};
}
if (info.type === 'http') {
this.http = {
scheme: info.scheme!,
bearerFormat: info.bearerFormat,
};
}
if (info.type === 'openIdConnect') {
this.openId = {
connectUrl: info.openIdConnectUrl!,
};
}
if (info.type === 'oauth2' && info.flows) {
this.flows = info.flows;
}
}
}
export class SecuritySchemesModel {
schemes: SecuritySchemeModel[];
constructor(public parser: OpenAPIParser) {
const schemes = (parser.spec.components && parser.spec.components.securitySchemes) || {};
this.schemes = Object.keys(schemes).map(
name => new SecuritySchemeModel(parser, name, schemes[name]),
);
}
}

View File

@ -10,3 +10,4 @@ export * from './Schema';
export * from './Field'; export * from './Field';
export * from './ApiInfo'; export * from './ApiInfo';
export * from './types'; export * from './types';
export * from './SecuritySchemes';

View File

@ -201,7 +201,38 @@ export type OpenAPIComponents = {
}; };
export type OpenAPISecurityRequirement = {}; export type OpenAPISecurityRequirement = {};
export type OpenAPISecurityScheme = {};
export type OpenAPISecurityScheme = {
type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect';
description?: string;
name?: string;
in?: 'query' | 'header' | 'cookie';
scheme?: string;
bearerFormat: string;
flows: {
implicit?: {
refreshUrl?: string;
scopes: Dict<string>;
authorizationUrl: string;
};
password?: {
refreshUrl?: string;
scopes: Dict<string>;
tokenUrl: string;
};
clientCredentials?: {
refreshUrl?: string;
scopes: Dict<string>;
tokenUrl: string;
};
authorizationCode?: {
refreshUrl?: string;
scopes: Dict<string>;
tokenUrl: string;
};
};
openIdConnectUrl?: string;
};
export type OpenAPITag = { export type OpenAPITag = {
name: string; name: string;