Merge remote-tracking branch 'origin/develop' into 22-oauth-app-managment

This commit is contained in:
SleepWalker 2018-05-02 21:55:34 +03:00
commit bd77abcdd8
8 changed files with 162 additions and 106 deletions

View File

@ -1,9 +1,8 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import { FormattedMessage as Message } from 'react-intl'; import { FormattedMessage as Message } from 'react-intl';
import { IntlProvider } from 'components/i18n'; import { IntlProvider } from 'components/i18n';
import logger from 'services/logger';
import appInfo from 'components/auth/appInfo/AppInfo.intl.json'; import appInfo from 'components/auth/appInfo/AppInfo.intl.json';
import styles from './styles.scss'; import styles from './styles.scss';
@ -12,32 +11,64 @@ import messages from './BSoD.intl.json';
// TODO: probably it is better to render this view from the App view // TODO: probably it is better to render this view from the App view
// to remove dependencies from store and IntlProvider // to remove dependencies from store and IntlProvider
export default function BSoD({store}: {store: *}) { export default class BSoD extends React.Component<{
return ( store: Object
<IntlProvider store={store}> }, {
<div className={styles.body}> lastEventId?: string,
<canvas className={styles.canvas} }> {
ref={(el: ?HTMLCanvasElement) => el && new BoxesField(el)} state = {};
/>
<div className={styles.wrapper}> componentDidMount() {
<div className={styles.title}> // poll for event id
<Message {...appInfo.appName} /> const timer = setInterval(() => {
</div> if (!logger.getLastEventId()) {
<div className={styles.lineWithMargin}> return;
<Message {...messages.criticalErrorHappened} /> }
</div>
<div className={styles.line}> clearInterval(timer);
<Message {...messages.reloadPageOrContactUs} />
</div> this.setState({
<a href="mailto:support@ely.by" className={styles.support}> lastEventId: logger.getLastEventId()
support@ely.by });
</a> }, 500);
<div className={styles.easterEgg}> }
<Message {...messages.alsoYouCanInteractWithBackground}/>
render() {
const {store} = this.props;
const {lastEventId} = this.state;
let emailUrl = 'mailto:support@ely.by';
if (lastEventId) {
emailUrl += `?subject=Bug report for #${lastEventId}`;
}
return (
<IntlProvider store={store}>
<div className={styles.body}>
<canvas className={styles.canvas}
ref={(el: ?HTMLCanvasElement) => el && new BoxesField(el)}
/>
<div className={styles.wrapper}>
<div className={styles.title}>
<Message {...appInfo.appName} />
</div>
<div className={styles.lineWithMargin}>
<Message {...messages.criticalErrorHappened} />
</div>
<div className={styles.line}>
<Message {...messages.reloadPageOrContactUs} />
</div>
<a href={emailUrl} className={styles.support}>
support@ely.by
</a>
<div className={styles.easterEgg}>
<Message {...messages.alsoYouCanInteractWithBackground}/>
</div>
</div> </div>
</div> </div>
</div> </IntlProvider>
</IntlProvider> );
); }
} }

View File

@ -1,27 +1,24 @@
// @flow // @flow
import React from 'react'; import type { MessageDescriptor } from 'react-intl';
import type { ComponentType } from 'react'; import type { ComponentType } from 'react';
import type { Color } from 'components/ui';
import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import buttons from 'components/ui/buttons.scss'; import buttons from 'components/ui/buttons.scss';
import { COLOR_GREEN } from 'components/ui'; import { COLOR_GREEN } from 'components/ui';
import FormComponent from './FormComponent'; import FormComponent from './FormComponent';
import type { Color } from 'components/ui';
import type { MessageDescriptor } from 'react-intl';
export default class Button extends FormComponent<{ export default class Button extends FormComponent<{
label: string | MessageDescriptor, label: string | MessageDescriptor,
block?: bool, block?: bool,
small?: bool, small?: bool,
loading?: bool, loading?: bool,
className?: string, className?: string,
color?: Color, color: Color,
disabled?: bool, disabled?: bool,
component?: string | ComponentType<any>, component: string | ComponentType<any>,
} | HTMLButtonElement> { }> {
static defaultProps = { static defaultProps = {
color: COLOR_GREEN, color: COLOR_GREEN,
component: 'button', component: 'button',

View File

@ -1,35 +1,41 @@
import PropTypes from 'prop-types'; // @flow
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import type { CaptchaID } from 'services/captcha';
import type { Skin } from 'components/ui';
import captcha from 'services/captcha'; import captcha from 'services/captcha';
import logger from 'services/logger'; import logger from 'services/logger';
import { skins, SKIN_DARK } from 'components/ui';
import { ComponentLoader } from 'components/ui/loader'; import { ComponentLoader } from 'components/ui/loader';
import styles from './form.scss'; import styles from './form.scss';
import FormInputComponent from './FormInputComponent'; import FormInputComponent from './FormInputComponent';
export default class Captcha extends FormInputComponent { export default class Captcha extends FormInputComponent<{
static displayName = 'Captcha'; delay: number,
skin: Skin,
static propTypes = { }, {
skin: PropTypes.oneOf(skins), code: string,
delay: PropTypes.number }> {
}; el: ?HTMLDivElement;
captchaId: CaptchaID;
static defaultProps = { static defaultProps = {
skin: SKIN_DARK, skin: 'dark',
delay: 0 delay: 0
}; };
componentDidMount() { componentDidMount() {
setTimeout(() => { setTimeout(() => {
captcha.render(this.el, { this.el && captcha.render(this.el, {
skin: this.props.skin, skin: this.props.skin,
onSetCode: this.setCode onSetCode: this.setCode
}).then((captchaId) => this.captchaId = captchaId, (error) => logger.error('Error rendering captcha', { error })); })
.then((captchaId) => {this.captchaId = captchaId;})
.catch((error) => {
logger.error('Failed rendering captcha', {
error
});
});
}, this.props.delay); }, this.props.delay);
} }
@ -64,5 +70,5 @@ export default class Captcha extends FormInputComponent {
this.reset(); this.reset();
} }
setCode = (code) => this.setState({code}); setCode = (code: string) => this.setState({code});
} }

View File

@ -1,10 +1,9 @@
// @flow
import type { MessageDescriptor } from 'react-intl';
import { Component } from 'react'; import { Component } from 'react';
import { intlShape } from 'react-intl'; import { intlShape } from 'react-intl';
export default class FormComponent extends Component { export default class FormComponent<P, S = void> extends Component<P, S> {
static displayName = 'FormComponent';
static contextTypes = { static contextTypes = {
intl: intlShape.isRequired intl: intlShape.isRequired
}; };
@ -16,7 +15,7 @@ export default class FormComponent extends Component {
* *
* @return {string} * @return {string}
*/ */
formatMessage(message) { formatMessage(message: string | MessageDescriptor) {
if (message && message.id && this.context && this.context.intl) { if (message && message.id && this.context && this.context.intl) {
message = this.context.intl.formatMessage(message); message = this.context.intl.formatMessage(message);
} }

View File

@ -1,15 +1,18 @@
import PropTypes from 'prop-types'; // @flow
import type { MessageDescriptor } from 'react-intl';
import React from 'react'; import React from 'react';
import FormComponent from './FormComponent'; import FormComponent from './FormComponent';
import FormError from './FormError'; import FormError from './FormError';
export default class FormInputComponent extends FormComponent { type Error = string | MessageDescriptor;
static displayName = 'FormInputComponent';
static propTypes = { export default class FormInputComponent<P, S = void> extends FormComponent<P & {
error: PropTypes.string error?: Error,
}; }, S & {
error?: Error,
}> {
el: ?HTMLDivElement;
componentWillReceiveProps() { componentWillReceiveProps() {
if (this.state && this.state.error) { if (this.state && this.state.error) {
@ -19,7 +22,7 @@ export default class FormInputComponent extends FormComponent {
} }
} }
setEl = (el) => { setEl = (el: ?HTMLDivElement) => {
this.el = el; this.el = el;
}; };
@ -29,7 +32,7 @@ export default class FormInputComponent extends FormComponent {
return <FormError error={error} />; return <FormError error={error} />;
} }
setError(error) { setError(error: Error) {
this.setState({error}); this.setState({error});
} }
} }

View File

@ -32,7 +32,7 @@ export default class FormModel {
const props: Object = { const props: Object = {
name, name,
ref: (el: ?FormInputComponent) => { ref: (el: ?FormInputComponent<any>) => {
if (el) { if (el) {
if (!(el instanceof FormInputComponent)) { if (!(el instanceof FormInputComponent)) {
throw new Error('Expected FormInputComponent component'); throw new Error('Expected FormInputComponent component');

View File

@ -1,3 +1,4 @@
// @flow
import { loadScript } from 'functions'; import { loadScript } from 'functions';
import options from 'services/api/options'; import options from 'services/api/options';
@ -5,6 +6,8 @@ let readyPromise;
let lang = 'en'; let lang = 'en';
let sitekey; let sitekey;
export opaque type CaptchaID = string;
export default { export default {
/** /**
* @param {DOMNode|string} el - dom node or id of element where to render captcha * @param {DOMNode|string} el - dom node or id of element where to render captcha
@ -14,7 +17,10 @@ export default {
* *
* @return {Promise} - resolves to captchaId * @return {Promise} - resolves to captchaId
*/ */
render(el, {skin: theme, onSetCode: callback}) { render(el: HTMLElement, {skin: theme, onSetCode: callback}: {
skin: 'dark' | 'light',
onSetCode: (string) => void,
}): Promise<CaptchaID> {
return this.loadApi().then(() => return this.loadApi().then(() =>
window.grecaptcha.render(el, { window.grecaptcha.render(el, {
sitekey, sitekey,
@ -27,7 +33,7 @@ export default {
/** /**
* @param {string} captchaId - captcha id, returned from render promise * @param {string} captchaId - captcha id, returned from render promise
*/ */
reset(captchaId) { reset(captchaId: CaptchaID) {
this.loadApi().then(() => window.grecaptcha.reset(captchaId)); this.loadApi().then(() => window.grecaptcha.reset(captchaId));
}, },
@ -36,7 +42,7 @@ export default {
* *
* @see https://developers.google.com/recaptcha/docs/language * @see https://developers.google.com/recaptcha/docs/language
*/ */
setLang(newLang) { setLang(newLang: string) {
lang = newLang; lang = newLang;
}, },
@ -45,7 +51,7 @@ export default {
* *
* @see http://www.google.com/recaptcha/admin * @see http://www.google.com/recaptcha/admin
*/ */
setApiKey(apiKey) { setApiKey(apiKey: string) {
sitekey = apiKey; sitekey = apiKey;
}, },
@ -54,14 +60,14 @@ export default {
* *
* @return {Promise} * @return {Promise}
*/ */
loadApi() { loadApi(): Promise<void> {
if (!readyPromise) { if (!readyPromise) {
readyPromise = Promise.all([ readyPromise = Promise.all([
new Promise((resolve) => { new Promise((resolve) => {
window.onReCaptchaReady = resolve; window.onReCaptchaReady = resolve;
}), }),
options.get().then((resp) => this.setApiKey(resp.reCaptchaPublicKey)) options.get().then((resp) => this.setApiKey(resp.reCaptchaPublicKey))
]); ]).then(() => {});
loadScript(`https://recaptcha.net/recaptcha/api.js?onload=onReCaptchaReady&render=explicit&hl=${lang}`); loadScript(`https://recaptcha.net/recaptcha/api.js?onload=onReCaptchaReady&render=explicit&hl=${lang}`);
} }
@ -69,4 +75,3 @@ export default {
return readyPromise; return readyPromise;
} }
}; };

View File

@ -1,3 +1,5 @@
// @flow
import type {User} from 'components/user';
import Raven from 'raven-js'; import Raven from 'raven-js';
import abbreviate from './abbreviate'; import abbreviate from './abbreviate';
@ -5,8 +7,8 @@ import abbreviate from './abbreviate';
const isTest = process.env.__TEST__; // eslint-disable-line const isTest = process.env.__TEST__; // eslint-disable-line
const isProduction = process.env.__PROD__; // eslint-disable-line const isProduction = process.env.__PROD__; // eslint-disable-line
const logger = { class Logger {
init({sentryCdn}) { init({ sentryCdn }: { sentryCdn: string }) {
if (sentryCdn) { if (sentryCdn) {
Raven.config(sentryCdn, { Raven.config(sentryCdn, {
logger: 'accounts-js-app', logger: 'accounts-js-app',
@ -37,54 +39,67 @@ const logger = {
message = ''; message = '';
} }
logger.info(`Unhandled rejection${message}`, { this.info(`Unhandled rejection${message}`, {
error, error,
event event
}); });
}); });
} }
}, }
setUser(user) { setUser(user: User) {
Raven.setUserContext({ Raven.setUserContext({
username: user.username, username: user.username,
email: user.email, email: user.email,
id: user.id id: user.id
}); });
} }
};
[ error(message: string | Error, context: Object) {
// 'fatal', log('error', message, context);
'error', }
'warning',
'info',
'debug'
].forEach((level) => {
const method = level === 'warning' ? 'warn' : level;
logger[method] = (message, context) => { info(message: string | Error, context: Object) {
if (isTest) { log('info', message, context);
return; }
}
if (typeof context !== 'object') { warn(message: string | Error, context: Object) {
// it would better to always have an object here log('warning', message, context);
context = { }
message: context
};
}
prepareContext(context).then((context) => { getLastEventId(): string | void {
console[method](message, context); // eslint-disable-line return Raven.lastEventId();
}
}
Raven.captureException(message, { function log(
level, level: 'error' | 'warning' | 'info' | 'debug',
extra: context message: string | Error,
}); context: Object
) {
const method: 'error' | 'warn' | 'info' | 'debug' = level === 'warning' ? 'warn' : level;
if (isTest) {
return;
}
if (typeof context !== 'object') {
// it would better to always have an object here
context = {
message: context
};
}
prepareContext(context).then((context) => {
console[method](message, context); // eslint-disable-line
Raven.captureException(message, {
level,
extra: context,
...(typeof message === 'string' ? { fingerprint: [message] } : {}),
}); });
}; });
}); }
/** /**
* prepare data for JSON.stringify * prepare data for JSON.stringify
@ -93,7 +108,7 @@ const logger = {
* *
* @return {Promise} * @return {Promise}
*/ */
function prepareContext(context) { function prepareContext(context: any) {
if (context instanceof Response) { if (context instanceof Response) {
// TODO: rewrite abbreviate to use promises and recursively find Response // TODO: rewrite abbreviate to use promises and recursively find Response
return context.json() return context.json()
@ -120,4 +135,4 @@ function prepareContext(context) {
return Promise.resolve(abbreviate(context)); return Promise.resolve(abbreviate(context));
} }
export default logger; export default new Logger();