Change prettier rules

This commit is contained in:
ErickSkrauch
2020-05-24 02:08:24 +03:00
parent 73f0c37a6a
commit f85b9d8d35
382 changed files with 24137 additions and 26046 deletions

View File

@@ -3,14 +3,14 @@ import { Helmet } from 'react-helmet-async';
import { FormattedMessage as Message, MessageDescriptor } from 'react-intl';
export default function AuthTitle({ title }: { title: MessageDescriptor }) {
return (
<Message {...title}>
{(msg) => (
<span>
{msg}
<Helmet title={msg as string} />
</span>
)}
</Message>
);
return (
<Message {...title}>
{(msg) => (
<span>
{msg}
<Helmet title={msg as string} />
</span>
)}
</Message>
);
}

View File

@@ -11,63 +11,63 @@ import Context, { AuthContext } from './Context';
*/
class BaseAuthBody extends React.Component<
// TODO: this may be converted to generic type RouteComponentProps<T>
RouteComponentProps<Record<string, any>>
// TODO: this may be converted to generic type RouteComponentProps<T>
RouteComponentProps<Record<string, any>>
> {
static contextType = Context;
/* TODO: use declare */ context: React.ContextType<typeof Context>;
prevErrors: AuthContext['auth']['error'];
static contextType = Context;
/* TODO: use declare */ context: React.ContextType<typeof Context>;
prevErrors: AuthContext['auth']['error'];
autoFocusField: string | null = '';
autoFocusField: string | null = '';
componentDidMount() {
this.prevErrors = this.context.auth.error;
}
componentDidUpdate() {
if (this.context.auth.error !== this.prevErrors) {
this.form.setErrors(this.context.auth.error || {});
this.context.requestRedraw();
componentDidMount() {
this.prevErrors = this.context.auth.error;
}
this.prevErrors = this.context.auth.error;
}
componentDidUpdate() {
if (this.context.auth.error !== this.prevErrors) {
this.form.setErrors(this.context.auth.error || {});
this.context.requestRedraw();
}
renderErrors(): ReactNode {
const error = this.form.getFirstError();
if (error === null) {
return null;
this.prevErrors = this.context.auth.error;
}
return <AuthError error={error} onClose={this.onClearErrors} />;
}
renderErrors(): ReactNode {
const error = this.form.getFirstError();
onFormSubmit() {
this.context.resolve(this.serialize());
}
if (error === null) {
return null;
}
onClearErrors = () => this.context.clearErrors();
form = new FormModel({
renderErrors: false,
});
bindField(name: string) {
return this.form.bindField(name);
}
serialize() {
return this.form.serialize();
}
autoFocus() {
const fieldId = this.autoFocusField;
if (fieldId && this.form.hasField(fieldId)) {
this.form.focus(fieldId);
return <AuthError error={error} onClose={this.onClearErrors} />;
}
onFormSubmit() {
this.context.resolve(this.serialize());
}
onClearErrors = () => this.context.clearErrors();
form = new FormModel({
renderErrors: false,
});
bindField(name: string) {
return this.form.bindField(name);
}
serialize() {
return this.form.serialize();
}
autoFocus() {
const fieldId = this.autoFocusField;
if (fieldId && this.form.hasField(fieldId)) {
this.form.focus(fieldId);
}
}
}
}
export default BaseAuthBody;

View File

@@ -4,28 +4,28 @@ import { User } from 'app/components/user';
import { State as AuthState } from './reducer';
export interface AuthContext {
auth: AuthState;
user: User;
requestRedraw: () => Promise<void>;
clearErrors: () => void;
resolve: (payload: { [key: string]: any } | undefined) => void;
reject: (payload: { [key: string]: any } | undefined) => void;
auth: AuthState;
user: User;
requestRedraw: () => Promise<void>;
clearErrors: () => void;
resolve: (payload: { [key: string]: any } | undefined) => void;
reject: (payload: { [key: string]: any } | undefined) => void;
}
const Context = React.createContext<AuthContext>({
auth: {
error: null,
login: '',
scopes: [],
} as any,
user: {
id: null,
isGuest: true,
} as any,
async requestRedraw() {},
clearErrors() {},
resolve() {},
reject() {},
auth: {
error: null,
login: '',
scopes: [],
} as any,
user: {
id: null,
isGuest: true,
} as any,
async requestRedraw() {},
clearErrors() {},
resolve() {},
reject() {},
});
Context.displayName = 'AuthContext';

File diff suppressed because it is too large Load Diff

View File

@@ -2,16 +2,13 @@
To add new panel you need to:
- create panel component at `components/auth/[panelId]`
- add new context in `components/auth/PanelTransition`
- connect component to router in `pages/auth/AuthPage`
- add new state to `services/authFlow` and coresponding test to
`tests/services/authFlow`
- connect state to `authFlow`. Update `services/authFlow/AuthFlow.test` and
`services/authFlow/AuthFlow.functional.test` (the last one for some complex
flow)
- add new actions to `components/auth/actions` and api endpoints to
`services/api`
- whatever else you need
- create panel component at `components/auth/[panelId]`
- add new context in `components/auth/PanelTransition`
- connect component to router in `pages/auth/AuthPage`
- add new state to `services/authFlow` and coresponding test to `tests/services/authFlow`
- connect state to `authFlow`. Update `services/authFlow/AuthFlow.test` and
`services/authFlow/AuthFlow.functional.test` (the last one for some complex flow)
- add new actions to `components/auth/actions` and api endpoints to `services/api`
- whatever else you need
Commit id with example implementation: f4d315c

View File

@@ -4,36 +4,32 @@ import { FormattedMessage as Message, MessageDescriptor } from 'react-intl';
import Context, { AuthContext } from './Context';
interface Props {
isAvailable?: (context: AuthContext) => boolean;
payload?: Record<string, any>;
label: MessageDescriptor;
isAvailable?: (context: AuthContext) => boolean;
payload?: Record<string, any>;
label: MessageDescriptor;
}
const RejectionLink: ComponentType<Props> = ({
isAvailable,
payload,
label,
}) => {
const context = useContext(Context);
const RejectionLink: ComponentType<Props> = ({ isAvailable, payload, label }) => {
const context = useContext(Context);
if (isAvailable && !isAvailable(context)) {
// TODO: if want to properly support multiple links, we should control
// the dividers ' | ' rendered from factory too
return null;
}
if (isAvailable && !isAvailable(context)) {
// TODO: if want to properly support multiple links, we should control
// the dividers ' | ' rendered from factory too
return null;
}
return (
<a
href="#"
onClick={(event) => {
event.preventDefault();
return (
<a
href="#"
onClick={(event) => {
event.preventDefault();
context.reject(payload);
}}
>
<Message {...label} />
</a>
);
context.reject(payload);
}}
>
<Message {...label} />
</a>
);
};
export default RejectionLink;

View File

@@ -1,8 +1,8 @@
{
"title": "User Agreement",
"accept": "Accept",
"declineAndLogout": "Decline and logout",
"description1": "We have updated our {link}.",
"termsOfService": "terms of service",
"description2": "In order to continue using {name} service, you need to accept them."
"title": "User Agreement",
"accept": "Accept",
"declineAndLogout": "Decline and logout",
"description1": "We have updated our {link}.",
"termsOfService": "terms of service",
"description2": "In order to continue using {name} service, you need to accept them."
}

View File

@@ -3,14 +3,14 @@ import Body from './AcceptRulesBody';
import messages from './AcceptRules.intl.json';
export default factory({
title: messages.title,
body: Body,
footer: {
color: 'darkBlue',
autoFocus: true,
label: messages.accept,
},
links: {
label: messages.declineAndLogout,
},
title: messages.title,
body: Body,
footer: {
color: 'darkBlue',
autoFocus: true,
label: messages.accept,
},
links: {
label: messages.declineAndLogout,
},
});

View File

@@ -11,38 +11,38 @@ import styles from './acceptRules.scss';
import messages from './AcceptRules.intl.json';
export default class AcceptRulesBody extends BaseAuthBody {
static displayName = 'AcceptRulesBody';
static panelId = 'acceptRules';
static displayName = 'AcceptRulesBody';
static panelId = 'acceptRules';
render() {
return (
<div>
{this.renderErrors()}
render() {
return (
<div>
{this.renderErrors()}
<div className={styles.security}>
<span className={icons.lock} />
</div>
<div className={styles.security}>
<span className={icons.lock} />
</div>
<p className={styles.descriptionText}>
<Message
{...messages.description1}
values={{
link: (
<Link to="/rules" target="_blank">
<Message {...messages.termsOfService} />
</Link>
),
}}
/>
<br />
<Message
{...messages.description2}
values={{
name: <Message {...appInfo.appName} />,
}}
/>
</p>
</div>
);
}
<p className={styles.descriptionText}>
<Message
{...messages.description1}
values={{
link: (
<Link to="/rules" target="_blank">
<Message {...messages.termsOfService} />
</Link>
),
}}
/>
<br />
<Message
{...messages.description2}
values={{
name: <Message {...appInfo.appName} />,
}}
/>
</p>
</div>
);
}
}

View File

@@ -1,16 +1,16 @@
@import '~app/components/ui/colors.scss';
.descriptionText {
font-size: 15px;
line-height: 1.4;
padding-bottom: 8px;
color: #aaa;
font-size: 15px;
line-height: 1.4;
padding-bottom: 8px;
color: #aaa;
}
// TODO: вынести иконки такого типа в какую-то внешнюю структуру?
.security {
color: #fff;
font-size: 90px;
line-height: 1;
margin-bottom: 15px;
color: #fff;
font-size: 90px;
line-height: 1;
margin-bottom: 15px;
}

View File

@@ -5,200 +5,190 @@ import expect from 'app/test/unexpected';
import request from 'app/services/request';
import {
setLoadingState,
oAuthValidate,
oAuthComplete,
setClient,
setOAuthRequest,
setScopes,
setOAuthCode,
requirePermissionsAccept,
login,
setLogin,
setLoadingState,
oAuthValidate,
oAuthComplete,
setClient,
setOAuthRequest,
setScopes,
setOAuthCode,
requirePermissionsAccept,
login,
setLogin,
} from 'app/components/auth/actions';
import { OauthData, OAuthValidateResponse } from '../../services/api/oauth';
const oauthData: OauthData = {
clientId: '',
redirectUrl: '',
responseType: '',
scope: '',
state: '',
prompt: 'none',
clientId: '',
redirectUrl: '',
responseType: '',
scope: '',
state: '',
prompt: 'none',
};
describe('components/auth/actions', () => {
const dispatch = sinon.stub().named('store.dispatch');
const getState = sinon.stub().named('store.getState');
const dispatch = sinon.stub().named('store.dispatch');
const getState = sinon.stub().named('store.getState');
function callThunk<A extends Array<any>, F extends (...args: A) => any>(
fn: F,
...args: A
): Promise<void> {
const thunk = fn(...args);
function callThunk<A extends Array<any>, F extends (...args: A) => any>(fn: F, ...args: A): Promise<void> {
const thunk = fn(...args);
return thunk(dispatch, getState);
}
return thunk(dispatch, getState);
}
function expectDispatchCalls(calls: Array<Array<ReduxAction>>) {
expect(dispatch, 'to have calls satisfying', [
[setLoadingState(true)],
...calls,
[setLoadingState(false)],
]);
}
beforeEach(() => {
dispatch.reset();
getState.reset();
getState.returns({});
sinon.stub(request, 'get').named('request.get');
sinon.stub(request, 'post').named('request.post');
});
afterEach(() => {
(request.get as any).restore();
(request.post as any).restore();
});
describe('#oAuthValidate()', () => {
let resp: OAuthValidateResponse;
function expectDispatchCalls(calls: Array<Array<ReduxAction>>) {
expect(dispatch, 'to have calls satisfying', [[setLoadingState(true)], ...calls, [setLoadingState(false)]]);
}
beforeEach(() => {
resp = {
client: {
id: '123',
name: '',
description: '',
},
oAuth: {
state: 123,
},
session: {
scopes: ['account_info'],
},
};
(request.get as any).returns(Promise.resolve(resp));
dispatch.reset();
getState.reset();
getState.returns({});
sinon.stub(request, 'get').named('request.get');
sinon.stub(request, 'post').named('request.post');
});
it('should send get request to an api', () =>
callThunk(oAuthValidate, oauthData).then(() => {
expect(request.get, 'to have a call satisfying', [
'/api/oauth2/v1/validate',
{},
]);
}));
it('should dispatch setClient, setOAuthRequest and setScopes', () =>
callThunk(oAuthValidate, oauthData).then(() => {
expectDispatchCalls([
[setClient(resp.client)],
[
setOAuthRequest({
...resp.oAuth,
prompt: 'none',
loginHint: undefined,
}),
],
[setScopes(resp.session.scopes)],
]);
}));
});
describe('#oAuthComplete()', () => {
beforeEach(() => {
getState.returns({
auth: {
oauth: oauthData,
},
});
afterEach(() => {
(request.get as any).restore();
(request.post as any).restore();
});
it('should post to api/oauth2/complete', () => {
(request.post as any).returns(
Promise.resolve({
redirectUri: '',
}),
);
describe('#oAuthValidate()', () => {
let resp: OAuthValidateResponse;
return callThunk(oAuthComplete).then(() => {
expect(request.post, 'to have a call satisfying', [
'/api/oauth2/v1/complete?client_id=&redirect_uri=&response_type=&description=&scope=&prompt=none&login_hint=&state=',
{},
]);
});
beforeEach(() => {
resp = {
client: {
id: '123',
name: '',
description: '',
},
oAuth: {
state: 123,
},
session: {
scopes: ['account_info'],
},
};
(request.get as any).returns(Promise.resolve(resp));
});
it('should send get request to an api', () =>
callThunk(oAuthValidate, oauthData).then(() => {
expect(request.get, 'to have a call satisfying', ['/api/oauth2/v1/validate', {}]);
}));
it('should dispatch setClient, setOAuthRequest and setScopes', () =>
callThunk(oAuthValidate, oauthData).then(() => {
expectDispatchCalls([
[setClient(resp.client)],
[
setOAuthRequest({
...resp.oAuth,
prompt: 'none',
loginHint: undefined,
}),
],
[setScopes(resp.session.scopes)],
]);
}));
});
it('should dispatch setOAuthCode for static_page redirect', () => {
const resp = {
success: true,
redirectUri: 'static_page?code=123&state=',
};
describe('#oAuthComplete()', () => {
beforeEach(() => {
getState.returns({
auth: {
oauth: oauthData,
},
});
});
(request.post as any).returns(Promise.resolve(resp));
it('should post to api/oauth2/complete', () => {
(request.post as any).returns(
Promise.resolve({
redirectUri: '',
}),
);
return callThunk(oAuthComplete).then(() => {
expectDispatchCalls([
[
setOAuthCode({
success: true,
code: '123',
displayCode: false,
}),
],
]);
});
return callThunk(oAuthComplete).then(() => {
expect(request.post, 'to have a call satisfying', [
'/api/oauth2/v1/complete?client_id=&redirect_uri=&response_type=&description=&scope=&prompt=none&login_hint=&state=',
{},
]);
});
});
it('should dispatch setOAuthCode for static_page redirect', () => {
const resp = {
success: true,
redirectUri: 'static_page?code=123&state=',
};
(request.post as any).returns(Promise.resolve(resp));
return callThunk(oAuthComplete).then(() => {
expectDispatchCalls([
[
setOAuthCode({
success: true,
code: '123',
displayCode: false,
}),
],
]);
});
});
it('should resolve to with success false and redirectUri for access_denied', async () => {
const resp = {
statusCode: 401,
error: 'access_denied',
redirectUri: 'redirectUri',
};
(request.post as any).returns(Promise.reject(resp));
const data = await callThunk(oAuthComplete);
expect(data, 'to equal', {
success: false,
redirectUri: 'redirectUri',
});
});
it('should dispatch requirePermissionsAccept if accept_required', () => {
const resp = {
statusCode: 401,
error: 'accept_required',
};
(request.post as any).returns(Promise.reject(resp));
return callThunk(oAuthComplete).catch((error) => {
expect(error.acceptRequired, 'to be true');
expectDispatchCalls([[requirePermissionsAccept()]]);
});
});
});
it('should resolve to with success false and redirectUri for access_denied', async () => {
const resp = {
statusCode: 401,
error: 'access_denied',
redirectUri: 'redirectUri',
};
describe('#login()', () => {
describe('when correct login was entered', () => {
beforeEach(() => {
(request.post as any).returns(
Promise.reject({
errors: {
password: 'error.password_required',
},
}),
);
});
(request.post as any).returns(Promise.reject(resp));
const data = await callThunk(oAuthComplete);
expect(data, 'to equal', {
success: false,
redirectUri: 'redirectUri',
});
it('should set login', () =>
callThunk(login, { login: 'foo' }).then(() => {
expectDispatchCalls([[setLogin('foo')]]);
}));
});
});
it('should dispatch requirePermissionsAccept if accept_required', () => {
const resp = {
statusCode: 401,
error: 'accept_required',
};
(request.post as any).returns(Promise.reject(resp));
return callThunk(oAuthComplete).catch((error) => {
expect(error.acceptRequired, 'to be true');
expectDispatchCalls([[requirePermissionsAccept()]]);
});
});
});
describe('#login()', () => {
describe('when correct login was entered', () => {
beforeEach(() => {
(request.post as any).returns(
Promise.reject({
errors: {
password: 'error.password_required',
},
}),
);
});
it('should set login', () =>
callThunk(login, { login: 'foo' }).then(() => {
expectDispatchCalls([[setLogin('foo')]]);
}));
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
{
"accountActivationTitle": "Account activation",
"activationMailWasSent": "Please check {email} for the message with further instructions",
"activationMailWasSentNoEmail": "Please check your Email for the message with further instructions",
"confirmEmail": "Confirm Email",
"didNotReceivedEmail": "Did not received Email?",
"enterTheCode": "Enter the code from Email here"
"accountActivationTitle": "Account activation",
"activationMailWasSent": "Please check {email} for the message with further instructions",
"activationMailWasSentNoEmail": "Please check your Email for the message with further instructions",
"confirmEmail": "Confirm Email",
"didNotReceivedEmail": "Did not received Email?",
"enterTheCode": "Enter the code from Email here"
}

View File

@@ -3,13 +3,13 @@ import messages from './Activation.intl.json';
import Body from './ActivationBody';
export default factory({
title: messages.accountActivationTitle,
body: Body,
footer: {
color: 'blue',
label: messages.confirmEmail,
},
links: {
label: messages.didNotReceivedEmail,
},
title: messages.accountActivationTitle,
body: Body,
footer: {
color: 'blue',
label: messages.confirmEmail,
},
links: {
label: messages.didNotReceivedEmail,
},
});

View File

@@ -9,49 +9,48 @@ import styles from './activation.scss';
import messages from './Activation.intl.json';
export default class ActivationBody extends BaseAuthBody {
static displayName = 'ActivationBody';
static panelId = 'activation';
static displayName = 'ActivationBody';
static panelId = 'activation';
autoFocusField =
this.props.match.params && this.props.match.params.key ? null : 'key';
autoFocusField = this.props.match.params && this.props.match.params.key ? null : 'key';
render() {
const { key } = this.props.match.params;
const { email } = this.context.user;
render() {
const { key } = this.props.match.params;
const { email } = this.context.user;
return (
<div>
{this.renderErrors()}
return (
<div>
{this.renderErrors()}
<div className={styles.description}>
<div className={styles.descriptionImage} />
<div className={styles.description}>
<div className={styles.descriptionImage} />
<div className={styles.descriptionText}>
{email ? (
<Message
{...messages.activationMailWasSent}
values={{
email: <b>{email}</b>,
}}
/>
) : (
<Message {...messages.activationMailWasSentNoEmail} />
)}
</div>
</div>
<div className={styles.formRow}>
<Input
{...this.bindField('key')}
color="blue"
center
required
value={key}
readOnly={!!key}
autoComplete="off"
placeholder={messages.enterTheCode}
/>
</div>
</div>
);
}
<div className={styles.descriptionText}>
{email ? (
<Message
{...messages.activationMailWasSent}
values={{
email: <b>{email}</b>,
}}
/>
) : (
<Message {...messages.activationMailWasSentNoEmail} />
)}
</div>
</div>
<div className={styles.formRow}>
<Input
{...this.bindField('key')}
color="blue"
center
required
value={key}
readOnly={!!key}
autoComplete="off"
placeholder={messages.enterTheCode}
/>
</div>
</div>
);
}
}

View File

@@ -5,15 +5,15 @@
}
.descriptionImage {
composes: envelope from '~app/components/ui/icons.scss';
composes: envelope from '~app/components/ui/icons.scss';
font-size: 100px;
color: $blue;
font-size: 100px;
color: $blue;
}
.descriptionText {
font-family: $font-family-title;
margin: 5px 0 19px;
line-height: 1.4;
font-size: 16px;
font-family: $font-family-title;
margin: 5px 0 19px;
line-height: 1.4;
font-size: 16px;
}

View File

@@ -1,7 +1,7 @@
{
"appName": "Ely Accounts",
"goToAuth": "Go to auth",
"appDescription": "You are on the Ely.by authorization service, that allows you to safely perform any operations on your account. This single entry point for websites and desktop software, including game launchers.",
"useItYourself": "Visit our {link}, to learn how to use this service in you projects.",
"documentation": "documentation"
"appName": "Ely Accounts",
"goToAuth": "Go to auth",
"appDescription": "You are on the Ely.by authorization service, that allows you to safely perform any operations on your account. This single entry point for websites and desktop software, including game launchers.",
"useItYourself": "Visit our {link}, to learn how to use this service in you projects.",
"documentation": "documentation"
}

View File

@@ -7,51 +7,49 @@ import styles from './appInfo.scss';
import messages from './AppInfo.intl.json';
export default class AppInfo extends React.Component<{
name?: string;
description?: string;
onGoToAuth: () => void;
name?: string;
description?: string;
onGoToAuth: () => void;
}> {
render() {
const { name, description, onGoToAuth } = this.props;
render() {
const { name, description, onGoToAuth } = this.props;
return (
<div className={styles.appInfo}>
<div className={styles.logoContainer}>
<h2 className={styles.logo}>
{name ? name : <Message {...messages.appName} />}
</h2>
</div>
<div className={styles.descriptionContainer}>
{description ? (
<p className={styles.description}>{description}</p>
) : (
<div>
<p className={styles.description}>
<Message {...messages.appDescription} />
</p>
<p className={styles.description}>
<Message
{...messages.useItYourself}
values={{
link: (
<a href="http://docs.ely.by/oauth.html">
<Message {...messages.documentation} />
</a>
),
}}
/>
</p>
return (
<div className={styles.appInfo}>
<div className={styles.logoContainer}>
<h2 className={styles.logo}>{name ? name : <Message {...messages.appName} />}</h2>
</div>
<div className={styles.descriptionContainer}>
{description ? (
<p className={styles.description}>{description}</p>
) : (
<div>
<p className={styles.description}>
<Message {...messages.appDescription} />
</p>
<p className={styles.description}>
<Message
{...messages.useItYourself}
values={{
link: (
<a href="http://docs.ely.by/oauth.html">
<Message {...messages.documentation} />
</a>
),
}}
/>
</p>
</div>
)}
</div>
<div className={styles.goToAuth}>
<Button onClick={onGoToAuth} label={messages.goToAuth} />
</div>
<div className={styles.footer}>
<FooterMenu />
</div>
</div>
)}
</div>
<div className={styles.goToAuth}>
<Button onClick={onGoToAuth} label={messages.goToAuth} />
</div>
<div className={styles.footer}>
<FooterMenu />
</div>
</div>
);
}
);
}
}

View File

@@ -2,71 +2,71 @@
@import '~app/components/ui/fonts.scss';
.appInfo {
max-width: 270px;
margin: 0 auto;
padding: 55px 25px;
max-width: 270px;
margin: 0 auto;
padding: 55px 25px;
}
.logoContainer {
position: relative;
padding: 15px 0;
position: relative;
padding: 15px 0;
&:after {
content: '';
display: block;
&:after {
content: '';
display: block;
position: absolute;
left: 0;
bottom: 0;
height: 3px;
width: 40px;
position: absolute;
left: 0;
bottom: 0;
height: 3px;
width: 40px;
background: $green;
}
background: $green;
}
}
.logo {
font-family: $font-family-title;
color: #fff;
font-size: 36px;
font-family: $font-family-title;
color: #fff;
font-size: 36px;
}
.descriptionContainer {
margin: 20px 0;
margin: 20px 0;
}
.description {
$font-color: #ccc;
font-family: $font-family-base;
color: $font-color;
font-size: 13px;
line-height: 1.7;
margin-top: 7px;
$font-color: #ccc;
font-family: $font-family-base;
color: $font-color;
font-size: 13px;
line-height: 1.7;
margin-top: 7px;
a {
color: lighten($font-color, 10%);
border-bottom-color: #666;
a {
color: lighten($font-color, 10%);
border-bottom-color: #666;
&:hover {
color: $font-color;
&:hover {
color: $font-color;
}
}
}
}
.goToAuth {
}
@media (min-width: 720px) {
.goToAuth {
display: none;
}
.goToAuth {
display: none;
}
}
.footer {
position: absolute;
bottom: 10px;
left: 0;
right: 0;
text-align: center;
line-height: 1.5;
position: absolute;
bottom: 10px;
left: 0;
right: 0;
text-align: center;
line-height: 1.5;
}

View File

@@ -1,3 +1,3 @@
.checkboxInput {
margin-top: 15px;
margin-top: 15px;
}

View File

@@ -5,41 +5,36 @@ import { PanelBodyHeader } from 'app/components/ui/Panel';
import { ValidationError } from 'app/components/ui/form/FormModel';
interface Props {
error: ValidationError;
onClose?: () => void;
error: ValidationError;
onClose?: () => void;
}
let autoHideTimer: number | null = null;
function resetTimeout(): void {
if (autoHideTimer) {
clearTimeout(autoHideTimer);
autoHideTimer = null;
}
if (autoHideTimer) {
clearTimeout(autoHideTimer);
autoHideTimer = null;
}
}
const AuthError: ComponentType<Props> = ({ error, onClose }) => {
useEffect(() => {
resetTimeout();
useEffect(() => {
resetTimeout();
if (
onClose &&
typeof error !== 'string' &&
error.payload &&
error.payload.canRepeatIn
) {
const msLeft = error.payload.canRepeatIn * 1000;
// 1500 to let the user see, that time is elapsed
setTimeout(onClose, msLeft - Date.now() + 1500);
}
if (onClose && typeof error !== 'string' && error.payload && error.payload.canRepeatIn) {
const msLeft = error.payload.canRepeatIn * 1000;
// 1500 to let the user see, that time is elapsed
setTimeout(onClose, msLeft - Date.now() + 1500);
}
return resetTimeout;
}, [error, onClose]);
return resetTimeout;
}, [error, onClose]);
return (
<PanelBodyHeader type="error" onClose={onClose}>
{resolveError(error)}
</PanelBodyHeader>
);
return (
<PanelBodyHeader type="error" onClose={onClose}>
{resolveError(error)}
</PanelBodyHeader>
);
};
export default AuthError;

View File

@@ -1,7 +1,7 @@
{
"chooseAccountTitle": "Choose an account",
"addAccount": "Log into another account",
"logoutAll": "Log out from all accounts",
"pleaseChooseAccount": "Please select an account you're willing to use",
"pleaseChooseAccountForApp": "Please select an account that you want to use to authorize {appName}"
"chooseAccountTitle": "Choose an account",
"addAccount": "Log into another account",
"logoutAll": "Log out from all accounts",
"pleaseChooseAccount": "Please select an account you're willing to use",
"pleaseChooseAccountForApp": "Please select an account that you want to use to authorize {appName}"
}

View File

@@ -3,14 +3,14 @@ import messages from './ChooseAccount.intl.json';
import Body from './ChooseAccountBody';
export default factory({
title: messages.chooseAccountTitle,
body: Body,
footer: {
label: messages.addAccount,
},
links: [
{
label: messages.logoutAll,
title: messages.chooseAccountTitle,
body: Body,
footer: {
label: messages.addAccount,
},
],
links: [
{
label: messages.logoutAll,
},
],
});

View File

@@ -10,44 +10,44 @@ import styles from './chooseAccount.scss';
import messages from './ChooseAccount.intl.json';
export default class ChooseAccountBody extends BaseAuthBody {
static displayName = 'ChooseAccountBody';
static panelId = 'chooseAccount';
static displayName = 'ChooseAccountBody';
static panelId = 'chooseAccount';
render() {
const { client } = this.context.auth;
render() {
const { client } = this.context.auth;
return (
<div>
{this.renderErrors()}
return (
<div>
{this.renderErrors()}
<div className={styles.description}>
{client ? (
<Message
{...messages.pleaseChooseAccountForApp}
values={{
appName: <span className={styles.appName}>{client.name}</span>,
}}
/>
) : (
<div className={styles.description}>
<Message {...messages.pleaseChooseAccount} />
<div className={styles.description}>
{client ? (
<Message
{...messages.pleaseChooseAccountForApp}
values={{
appName: <span className={styles.appName}>{client.name}</span>,
}}
/>
) : (
<div className={styles.description}>
<Message {...messages.pleaseChooseAccount} />
</div>
)}
</div>
<div className={styles.accountSwitcherContainer}>
<AccountSwitcher
allowAdd={false}
allowLogout={false}
highlightActiveAccount={false}
onSwitch={this.onSwitch}
/>
</div>
</div>
)}
</div>
);
}
<div className={styles.accountSwitcherContainer}>
<AccountSwitcher
allowAdd={false}
allowLogout={false}
highlightActiveAccount={false}
onSwitch={this.onSwitch}
/>
</div>
</div>
);
}
onSwitch = (account: Account): void => {
this.context.resolve(account);
};
onSwitch = (account: Account): void => {
this.context.resolve(account);
};
}

View File

@@ -2,17 +2,17 @@
@import '~app/components/ui/fonts.scss';
.accountSwitcherContainer {
margin-left: -$bodyLeftRightPadding;
margin-right: -$bodyLeftRightPadding;
margin-left: -$bodyLeftRightPadding;
margin-right: -$bodyLeftRightPadding;
}
.description {
font-family: $font-family-title;
margin: 5px 0 19px;
line-height: 1.4;
font-size: 16px;
font-family: $font-family-title;
margin: 5px 0 19px;
line-height: 1.4;
font-size: 16px;
}
.appName {
color: #fff;
color: #fff;
}

View File

@@ -7,44 +7,36 @@ import { Color } from 'app/components/ui';
import BaseAuthBody from './BaseAuthBody';
export type Factory = () => {
Title: ComponentType;
Body: typeof BaseAuthBody;
Footer: ComponentType;
Links: ComponentType;
Title: ComponentType;
Body: typeof BaseAuthBody;
Footer: ComponentType;
Links: ComponentType;
};
type RejectionLinkProps = ComponentProps<typeof RejectionLink>;
interface FactoryParams {
title: MessageDescriptor;
body: typeof BaseAuthBody;
footer: {
color?: Color;
label: string | MessageDescriptor;
autoFocus?: boolean;
};
links?: RejectionLinkProps | Array<RejectionLinkProps>;
title: MessageDescriptor;
body: typeof BaseAuthBody;
footer: {
color?: Color;
label: string | MessageDescriptor;
autoFocus?: boolean;
};
links?: RejectionLinkProps | Array<RejectionLinkProps>;
}
export default function ({
title,
body,
footer,
links,
}: FactoryParams): Factory {
return () => ({
Title: () => <AuthTitle title={title} />,
Body: body,
Footer: () => <Button type="submit" {...footer} />,
Links: () =>
links ? (
<span>
{([] as Array<RejectionLinkProps>)
.concat(links)
.map((link, index) => [
index ? ' | ' : '',
<RejectionLink {...link} key={index} />,
])}
</span>
) : null,
});
export default function ({ title, body, footer, links }: FactoryParams): Factory {
return () => ({
Title: () => <AuthTitle title={title} />,
Body: body,
Footer: () => <Button type="submit" {...footer} />,
Links: () =>
links ? (
<span>
{([] as Array<RejectionLinkProps>)
.concat(links)
.map((link, index) => [index ? ' | ' : '', <RejectionLink {...link} key={index} />])}
</span>
) : null,
});
}

View File

@@ -1,7 +1,7 @@
{
"authForAppSuccessful": "Authorization for {appName} was successfully completed",
"authForAppFailed": "Authorization for {appName} was failed",
"waitAppReaction": "Please, wait till your application response",
"passCodeToApp": "To complete authorization process, please, provide the following code to {appName}",
"copy": "Copy"
"authForAppSuccessful": "Authorization for {appName} was successfully completed",
"authForAppFailed": "Authorization for {appName} was failed",
"waitAppReaction": "Please, wait till your application response",
"passCodeToApp": "To complete authorization process, please, provide the following code to {appName}",
"copy": "Copy"
}

View File

@@ -10,100 +10,95 @@ import messages from './Finish.intl.json';
import styles from './finish.scss';
interface Props {
appName: string;
code?: string;
state: string;
displayCode?: boolean;
success?: boolean;
appName: string;
code?: string;
state: string;
displayCode?: boolean;
success?: boolean;
}
class Finish extends React.Component<Props> {
render() {
const { appName, code, state, displayCode, success } = this.props;
const authData = JSON.stringify({
auth_code: code,
state,
});
render() {
const { appName, code, state, displayCode, success } = this.props;
const authData = JSON.stringify({
auth_code: code,
state,
});
history.pushState(null, document.title, `#${authData}`);
history.pushState(null, document.title, `#${authData}`);
return (
<div className={styles.finishPage}>
<Helmet title={authData} />
return (
<div className={styles.finishPage}>
<Helmet title={authData} />
{success ? (
<div>
<div className={styles.successBackground} />
<div className={styles.greenTitle}>
<Message
{...messages.authForAppSuccessful}
values={{
appName: <span className={styles.appName}>{appName}</span>,
}}
/>
{success ? (
<div>
<div className={styles.successBackground} />
<div className={styles.greenTitle}>
<Message
{...messages.authForAppSuccessful}
values={{
appName: <span className={styles.appName}>{appName}</span>,
}}
/>
</div>
{displayCode ? (
<div data-testid="oauth-code-container">
<div className={styles.description}>
<Message {...messages.passCodeToApp} values={{ appName }} />
</div>
<div className={styles.codeContainer}>
<div className={styles.code}>{code}</div>
</div>
<Button color="green" small label={messages.copy} onClick={this.onCopyClick} />
</div>
) : (
<div className={styles.description}>
<Message {...messages.waitAppReaction} />
</div>
)}
</div>
) : (
<div>
<div className={styles.failBackground} />
<div className={styles.redTitle}>
<Message
{...messages.authForAppFailed}
values={{
appName: <span className={styles.appName}>{appName}</span>,
}}
/>
</div>
<div className={styles.description}>
<Message {...messages.waitAppReaction} />
</div>
</div>
)}
</div>
{displayCode ? (
<div data-testid="oauth-code-container">
<div className={styles.description}>
<Message {...messages.passCodeToApp} values={{ appName }} />
</div>
<div className={styles.codeContainer}>
<div className={styles.code}>{code}</div>
</div>
<Button
color="green"
small
label={messages.copy}
onClick={this.onCopyClick}
/>
</div>
) : (
<div className={styles.description}>
<Message {...messages.waitAppReaction} />
</div>
)}
</div>
) : (
<div>
<div className={styles.failBackground} />
<div className={styles.redTitle}>
<Message
{...messages.authForAppFailed}
values={{
appName: <span className={styles.appName}>{appName}</span>,
}}
/>
</div>
<div className={styles.description}>
<Message {...messages.waitAppReaction} />
</div>
</div>
)}
</div>
);
}
onCopyClick: MouseEventHandler = (event) => {
event.preventDefault();
const { code } = this.props;
if (code) {
copy(code);
);
}
};
onCopyClick: MouseEventHandler = (event) => {
event.preventDefault();
const { code } = this.props;
if (code) {
copy(code);
}
};
}
export default connect(({ auth }: RootState) => {
if (!auth || !auth.client || !auth.oauth) {
throw new Error('Can not connect Finish component. No auth data in state');
}
if (!auth || !auth.client || !auth.oauth) {
throw new Error('Can not connect Finish component. No auth data in state');
}
return {
appName: auth.client.name,
code: auth.oauth.code,
displayCode: auth.oauth.displayCode,
state: auth.oauth.state,
success: auth.oauth.success,
};
return {
appName: auth.client.name,
code: auth.oauth.code,
displayCode: auth.oauth.displayCode,
state: auth.oauth.state,
success: auth.oauth.success,
};
})(Finish);

View File

@@ -2,75 +2,75 @@
@import '~app/components/ui/fonts.scss';
.finishPage {
font-family: $font-family-title;
position: relative;
max-width: 515px;
padding-top: 40px;
margin: 0 auto;
text-align: center;
font-family: $font-family-title;
position: relative;
max-width: 515px;
padding-top: 40px;
margin: 0 auto;
text-align: center;
}
.iconBackground {
position: absolute;
top: -15px;
transform: translateX(-50%);
font-size: 200px;
color: #e0d9cf;
z-index: -1;
position: absolute;
top: -15px;
transform: translateX(-50%);
font-size: 200px;
color: #e0d9cf;
z-index: -1;
}
.successBackground {
composes: checkmark from '~app/components/ui/icons.scss';
@extend .iconBackground;
composes: checkmark from '~app/components/ui/icons.scss';
@extend .iconBackground;
}
.failBackground {
composes: close from '~app/components/ui/icons.scss';
@extend .iconBackground;
composes: close from '~app/components/ui/icons.scss';
@extend .iconBackground;
}
.title {
font-size: 22px;
margin-bottom: 10px;
font-size: 22px;
margin-bottom: 10px;
}
.greenTitle {
composes: title;
composes: title;
color: $green;
color: $green;
.appName {
color: darker($green);
}
.appName {
color: darker($green);
}
}
.redTitle {
composes: title;
composes: title;
color: $red;
color: $red;
.appName {
color: darker($red);
}
.appName {
color: darker($red);
}
}
.description {
font-size: 18px;
margin-bottom: 10px;
font-size: 18px;
margin-bottom: 10px;
}
.codeContainer {
margin-bottom: 5px;
margin-top: 35px;
margin-bottom: 5px;
margin-top: 35px;
}
.code {
$border: 5px solid darker($green);
$border: 5px solid darker($green);
display: inline-block;
border-right: $border;
border-left: $border;
padding: 5px 10px;
word-break: break-all;
text-align: center;
display: inline-block;
border-right: $border;
border-left: $border;
padding: 5px 10px;
word-break: break-all;
text-align: center;
}

View File

@@ -1,7 +1,7 @@
{
"title": "Forgot password",
"sendMail": "Send mail",
"specifyEmail": "Specify the registration Email address or last used username for your account and we will send an Email with instructions for further password recovery.",
"pleasePressButton": "Please press the button bellow to get an Email with password recovery code.",
"alreadyHaveCode": "Already have a code"
"title": "Forgot password",
"sendMail": "Send mail",
"specifyEmail": "Specify the registration Email address or last used username for your account and we will send an Email with instructions for further password recovery.",
"pleasePressButton": "Please press the button bellow to get an Email with password recovery code.",
"alreadyHaveCode": "Already have a code"
}

View File

@@ -3,14 +3,14 @@ import messages from './ForgotPassword.intl.json';
import Body from './ForgotPasswordBody';
export default factory({
title: messages.title,
body: Body,
footer: {
color: 'lightViolet',
autoFocus: true,
label: messages.sendMail,
},
links: {
label: messages.alreadyHaveCode,
},
title: messages.title,
body: Body,
footer: {
color: 'lightViolet',
autoFocus: true,
label: messages.sendMail,
},
links: {
label: messages.alreadyHaveCode,
},
});

View File

@@ -11,87 +11,83 @@ import styles from './forgotPassword.scss';
import messages from './ForgotPassword.intl.json';
export default class ForgotPasswordBody extends BaseAuthBody {
static displayName = 'ForgotPasswordBody';
static panelId = 'forgotPassword';
static hasGoBack = true;
static displayName = 'ForgotPasswordBody';
static panelId = 'forgotPassword';
static hasGoBack = true;
state = {
isLoginEdit: false,
};
state = {
isLoginEdit: false,
};
autoFocusField = 'login';
autoFocusField = 'login';
render() {
const { isLoginEdit } = this.state;
render() {
const { isLoginEdit } = this.state;
const login = this.getLogin();
const isLoginEditShown = isLoginEdit || !login;
const login = this.getLogin();
const isLoginEditShown = isLoginEdit || !login;
return (
<div>
{this.renderErrors()}
return (
<div>
{this.renderErrors()}
<PanelIcon icon="lock" />
<PanelIcon icon="lock" />
{isLoginEditShown ? (
<div>
<p className={styles.descriptionText}>
<Message {...messages.specifyEmail} />
</p>
<Input
{...this.bindField('login')}
icon="envelope"
color="lightViolet"
required
placeholder={messages.accountEmail}
defaultValue={login}
/>
</div>
) : (
<div data-testid="forgot-password-login">
<div className={styles.login}>
{login}
<span
className={styles.editLogin}
onClick={this.onClickEdit}
data-testid="edit-login"
/>
{isLoginEditShown ? (
<div>
<p className={styles.descriptionText}>
<Message {...messages.specifyEmail} />
</p>
<Input
{...this.bindField('login')}
icon="envelope"
color="lightViolet"
required
placeholder={messages.accountEmail}
defaultValue={login}
/>
</div>
) : (
<div data-testid="forgot-password-login">
<div className={styles.login}>
{login}
<span className={styles.editLogin} onClick={this.onClickEdit} data-testid="edit-login" />
</div>
<p className={styles.descriptionText}>
<Message {...messages.pleasePressButton} />
</p>
</div>
)}
<Captcha {...this.bindField('captcha')} delay={600} />
</div>
<p className={styles.descriptionText}>
<Message {...messages.pleasePressButton} />
</p>
</div>
)}
<Captcha {...this.bindField('captcha')} delay={600} />
</div>
);
}
serialize() {
const data = super.serialize();
if (!data.login) {
data.login = this.getLogin();
);
}
return data;
}
serialize() {
const data = super.serialize();
getLogin() {
const login = getLogin(this.context);
const { user } = this.context;
if (!data.login) {
data.login = this.getLogin();
}
return login || user.username || user.email || '';
}
return data;
}
onClickEdit = async () => {
this.setState({
isLoginEdit: true,
});
getLogin() {
const login = getLogin(this.context);
const { user } = this.context;
await this.context.requestRedraw();
return login || user.username || user.email || '';
}
this.form.focus('login');
};
onClickEdit = async () => {
this.setState({
isLoginEdit: true,
});
await this.context.requestRedraw();
this.form.focus('login');
};
}

View File

@@ -1,31 +1,31 @@
@import '~app/components/ui/colors.scss';
.descriptionText {
font-size: 15px;
line-height: 1.4;
padding-bottom: 8px;
color: #aaa;
font-size: 15px;
line-height: 1.4;
padding-bottom: 8px;
color: #aaa;
}
.login {
composes: email from '~app/components/auth/password/password.scss';
composes: email from '~app/components/auth/password/password.scss';
}
.editLogin {
composes: pencil from '~app/components/ui/icons.scss';
composes: pencil from '~app/components/ui/icons.scss';
position: relative;
bottom: 1px;
padding-left: 3px;
position: relative;
bottom: 1px;
padding-left: 3px;
color: #666666;
font-size: 10px;
color: #666666;
font-size: 10px;
transition: color 0.3s;
transition: color 0.3s;
cursor: pointer;
cursor: pointer;
&:hover {
color: #ccc;
}
&:hover {
color: #ccc;
}
}

View File

@@ -1,9 +1,9 @@
.helpLinks {
margin: 8px 0;
position: relative;
height: 20px;
margin: 8px 0;
position: relative;
height: 20px;
color: #444;
text-align: center;
font-size: 16px;
color: #444;
text-align: center;
font-size: 16px;
}

View File

@@ -1,6 +1,6 @@
{
"createNewAccount": "Create new account",
"loginTitle": "Sign in",
"emailOrUsername": "Email or username",
"next": "Next"
"createNewAccount": "Create new account",
"loginTitle": "Sign in",
"emailOrUsername": "Email or username",
"next": "Next"
}

View File

@@ -3,14 +3,14 @@ import Body from './LoginBody';
import messages from './Login.intl.json';
export default factory({
title: messages.loginTitle,
body: Body,
footer: {
color: 'green',
label: messages.next,
},
links: {
isAvailable: (context) => !context.user.isGuest,
label: messages.createNewAccount,
},
title: messages.loginTitle,
body: Body,
footer: {
color: 'green',
label: messages.next,
},
links: {
isAvailable: (context) => !context.user.isGuest,
label: messages.createNewAccount,
},
});

View File

@@ -7,26 +7,21 @@ import { User } from 'app/components/user/reducer';
import messages from './Login.intl.json';
export default class LoginBody extends BaseAuthBody {
static displayName = 'LoginBody';
static panelId = 'login';
static hasGoBack = (state: { user: User }) => {
return !state.user.isGuest;
};
static displayName = 'LoginBody';
static panelId = 'login';
static hasGoBack = (state: { user: User }) => {
return !state.user.isGuest;
};
autoFocusField = 'login';
autoFocusField = 'login';
render() {
return (
<div>
{this.renderErrors()}
render() {
return (
<div>
{this.renderErrors()}
<Input
{...this.bindField('login')}
icon="envelope"
required
placeholder={messages.emailOrUsername}
/>
</div>
);
}
<Input {...this.bindField('login')} icon="envelope" required placeholder={messages.emailOrUsername} />
</div>
);
}
}

View File

@@ -1,4 +1,4 @@
{
"enterTotp": "Enter code",
"description": "In order to sign in this account, you need to enter a one-time password from mobile application"
"enterTotp": "Enter code",
"description": "In order to sign in this account, you need to enter a one-time password from mobile application"
}

View File

@@ -4,10 +4,10 @@ import messages from './Mfa.intl.json';
import passwordMessages from '../password/Password.intl.json';
export default factory({
title: messages.enterTotp,
body: Body,
footer: {
color: 'green',
label: passwordMessages.signInButton,
},
title: messages.enterTotp,
body: Body,
footer: {
color: 'green',
label: passwordMessages.signInButton,
},
});

View File

@@ -8,30 +8,30 @@ import styles from './mfa.scss';
import messages from './Mfa.intl.json';
export default class MfaBody extends BaseAuthBody {
static panelId = 'mfa';
static hasGoBack = true;
static panelId = 'mfa';
static hasGoBack = true;
autoFocusField = 'totp';
autoFocusField = 'totp';
render() {
return (
<div>
{this.renderErrors()}
render() {
return (
<div>
{this.renderErrors()}
<PanelIcon icon="lock" />
<PanelIcon icon="lock" />
<p className={styles.descriptionText}>
<Message {...messages.description} />
</p>
<p className={styles.descriptionText}>
<Message {...messages.description} />
</p>
<Input
{...this.bindField('totp')}
icon="key"
required
placeholder={messages.enterTotp}
autoComplete="off"
/>
</div>
);
}
<Input
{...this.bindField('totp')}
icon="key"
required
placeholder={messages.enterTotp}
autoComplete="off"
/>
</div>
);
}
}

View File

@@ -1,6 +1,6 @@
.descriptionText {
font-size: 15px;
line-height: 1.4;
padding-bottom: 8px;
color: #aaa;
font-size: 15px;
line-height: 1.4;
padding-bottom: 8px;
color: #aaa;
}

View File

@@ -1,7 +1,7 @@
{
"passwordTitle": "Enter password",
"signInButton": "Sign in",
"forgotPassword": "Forgot password",
"accountPassword": "Account password",
"rememberMe": "Remember me on this device"
"passwordTitle": "Enter password",
"signInButton": "Sign in",
"forgotPassword": "Forgot password",
"accountPassword": "Account password",
"rememberMe": "Remember me on this device"
}

View File

@@ -3,13 +3,13 @@ import Body from './PasswordBody';
import messages from './Password.intl.json';
export default factory({
title: messages.passwordTitle,
body: Body,
footer: {
color: 'green',
label: messages.signInButton,
},
links: {
label: messages.forgotPassword,
},
title: messages.passwordTitle,
body: Body,
footer: {
color: 'green',
label: messages.signInButton,
},
links: {
label: messages.forgotPassword,
},
});

View File

@@ -8,46 +8,38 @@ import styles from './password.scss';
import messages from './Password.intl.json';
export default class PasswordBody extends BaseAuthBody {
static displayName = 'PasswordBody';
static panelId = 'password';
static hasGoBack = true;
static displayName = 'PasswordBody';
static panelId = 'password';
static hasGoBack = true;
autoFocusField = 'password';
autoFocusField = 'password';
render() {
const { user } = this.context;
render() {
const { user } = this.context;
return (
<div>
{this.renderErrors()}
return (
<div>
{this.renderErrors()}
<div className={styles.miniProfile}>
<div className={styles.avatar}>
{user.avatar ? (
<img src={user.avatar} />
) : (
<span className={icons.user} />
)}
</div>
<div className={styles.email}>{user.email || user.username}</div>
</div>
<div className={styles.miniProfile}>
<div className={styles.avatar}>
{user.avatar ? <img src={user.avatar} /> : <span className={icons.user} />}
</div>
<div className={styles.email}>{user.email || user.username}</div>
</div>
<Input
{...this.bindField('password')}
icon="key"
type="password"
required
placeholder={messages.accountPassword}
/>
<Input
{...this.bindField('password')}
icon="key"
type="password"
required
placeholder={messages.accountPassword}
/>
<div className={authStyles.checkboxInput}>
<Checkbox
{...this.bindField('rememberMe')}
defaultChecked
label={messages.rememberMe}
/>
</div>
</div>
);
}
<div className={authStyles.checkboxInput}>
<Checkbox {...this.bindField('rememberMe')} defaultChecked label={messages.rememberMe} />
</div>
</div>
);
}
}

View File

@@ -1,22 +1,22 @@
@import '~app/components/ui/fonts.scss';
.avatar {
width: 90px;
height: 90px;
font-size: 90px;
line-height: 1;
margin: 0 auto;
width: 90px;
height: 90px;
font-size: 90px;
line-height: 1;
margin: 0 auto;
img {
width: 100%;
}
img {
width: 100%;
}
}
.email {
font-family: $font-family-title;
font-size: 18px;
color: #fff;
font-family: $font-family-title;
font-size: 18px;
color: #fff;
margin-bottom: 15px;
margin-top: 10px;
margin-bottom: 15px;
margin-top: 10px;
}

View File

@@ -1,12 +1,12 @@
{
"permissionsTitle": "Application permissions",
"youAuthorizedAs": "You authorized as:",
"theAppNeedsAccess1": "This application needs access",
"theAppNeedsAccess2": "to your data",
"decline": "Decline",
"approve": "Approve",
"scope_minecraft_server_session": "Authorization data for minecraft server",
"scope_offline_access": "Access to your profile data, when you offline",
"scope_account_info": "Access to your profile data (except Email)",
"scope_account_email": "Access to your Email address"
"permissionsTitle": "Application permissions",
"youAuthorizedAs": "You authorized as:",
"theAppNeedsAccess1": "This application needs access",
"theAppNeedsAccess2": "to your data",
"decline": "Decline",
"approve": "Approve",
"scope_minecraft_server_session": "Authorization data for minecraft server",
"scope_offline_access": "Access to your profile data, when you offline",
"scope_account_info": "Access to your profile data (except Email)",
"scope_account_email": "Access to your Email address"
}

View File

@@ -3,14 +3,14 @@ import messages from './Permissions.intl.json';
import Body from './PermissionsBody';
export default factory({
title: messages.permissionsTitle,
body: Body,
footer: {
color: 'orange',
autoFocus: true,
label: messages.approve,
},
links: {
label: messages.decline,
},
title: messages.permissionsTitle,
body: Body,
footer: {
color: 'orange',
autoFocus: true,
label: messages.approve,
},
links: {
label: messages.decline,
},
});

View File

@@ -8,58 +8,52 @@ import styles from './permissions.scss';
import messages from './Permissions.intl.json';
export default class PermissionsBody extends BaseAuthBody {
static displayName = 'PermissionsBody';
static panelId = 'permissions';
static displayName = 'PermissionsBody';
static panelId = 'permissions';
render() {
const { user } = this.context;
const { scopes } = this.context.auth;
render() {
const { user } = this.context;
const { scopes } = this.context.auth;
return (
<div>
{this.renderErrors()}
return (
<div>
{this.renderErrors()}
<PanelBodyHeader>
<div className={styles.authInfo}>
<div className={styles.authInfoAvatar}>
{user.avatar ? (
<img src={user.avatar} />
) : (
<span className={icons.user} />
)}
<PanelBodyHeader>
<div className={styles.authInfo}>
<div className={styles.authInfoAvatar}>
{user.avatar ? <img src={user.avatar} /> : <span className={icons.user} />}
</div>
<div className={styles.authInfoTitle}>
<Message {...messages.youAuthorizedAs} />
</div>
<div className={styles.authInfoEmail}>{user.username}</div>
</div>
</PanelBodyHeader>
<div className={styles.permissionsContainer}>
<div className={styles.permissionsTitle}>
<Message {...messages.theAppNeedsAccess1} />
<br />
<Message {...messages.theAppNeedsAccess2} />
</div>
<ul className={styles.permissionsList}>
{scopes.map((scope) => {
const key = `scope_${scope}`;
const message = messages[key];
return (
<li key={key}>
{message ? (
<Message {...message} />
) : (
scope.replace(/^\w|_/g, (match) => match.replace('_', ' ').toUpperCase())
)}
</li>
);
})}
</ul>
</div>
</div>
<div className={styles.authInfoTitle}>
<Message {...messages.youAuthorizedAs} />
</div>
<div className={styles.authInfoEmail}>{user.username}</div>
</div>
</PanelBodyHeader>
<div className={styles.permissionsContainer}>
<div className={styles.permissionsTitle}>
<Message {...messages.theAppNeedsAccess1} />
<br />
<Message {...messages.theAppNeedsAccess2} />
</div>
<ul className={styles.permissionsList}>
{scopes.map((scope) => {
const key = `scope_${scope}`;
const message = messages[key];
return (
<li key={key}>
{message ? (
<Message {...message} />
) : (
scope.replace(/^\w|_/g, (match) =>
match.replace('_', ' ').toUpperCase(),
)
)}
</li>
);
})}
</ul>
</div>
</div>
);
}
);
}
}

View File

@@ -2,76 +2,76 @@
@import '~app/components/ui/fonts.scss';
.authInfo {
// Отступы сверху и снизу разные т.к. мы ужимаем высоту линии строки с логином на 2 пикселя и из-за этого теряем отступ снизу
padding: 5px 20px 7px;
text-align: left;
// Отступы сверху и снизу разные т.к. мы ужимаем высоту линии строки с логином на 2 пикселя и из-за этого теряем отступ снизу
padding: 5px 20px 7px;
text-align: left;
}
.authInfoAvatar {
$size: 30px;
$size: 30px;
float: left;
height: $size;
width: $size;
font-size: $size;
line-height: 1;
margin-right: 10px;
margin-top: 2px;
color: #aaa;
float: left;
height: $size;
width: $size;
font-size: $size;
line-height: 1;
margin-right: 10px;
margin-top: 2px;
color: #aaa;
img {
width: 100%;
}
img {
width: 100%;
}
}
.authInfoTitle {
font-size: 14px;
color: #666;
font-size: 14px;
color: #666;
}
.authInfoEmail {
font-family: $font-family-title;
font-size: 20px;
line-height: 16px;
color: #fff;
font-family: $font-family-title;
font-size: 20px;
line-height: 16px;
color: #fff;
}
.permissionsContainer {
padding: 15px 12px;
text-align: left;
padding: 15px 12px;
text-align: left;
}
.permissionsTitle {
font-family: $font-family-title;
font-size: 18px;
color: #dd8650;
padding-bottom: 6px;
font-family: $font-family-title;
font-size: 18px;
color: #dd8650;
padding-bottom: 6px;
}
.permissionsList {
list-style: none;
margin-top: 10px;
list-style: none;
margin-top: 10px;
li {
color: #a9a9a9;
font-size: 14px;
line-height: 1.4;
padding-bottom: 4px;
padding-left: 17px;
position: relative;
li {
color: #a9a9a9;
font-size: 14px;
line-height: 1.4;
padding-bottom: 4px;
padding-left: 17px;
position: relative;
&:last-of-type {
padding-bottom: 0;
&:last-of-type {
padding-bottom: 0;
}
&:before {
content: '';
color: lighter($light_violet);
font-size: 39px; // ~ 9px
line-height: 9px;
position: absolute;
top: 6px;
left: -4px;
}
}
&:before {
content: '';
color: lighter($light_violet);
font-size: 39px; // ~ 9px
line-height: 9px;
position: absolute;
top: 6px;
left: -4px;
}
}
}

View File

@@ -1,12 +1,12 @@
{
"title": "Restore password",
"contactSupport": "Contact support",
"messageWasSent": "The recovery code was sent to your account Email.",
"messageWasSentTo": "The recovery code was sent to your Email {email}.",
"enterCodeBelow": "Please enter the code received into the field below:",
"enterNewPasswordBelow": "Enter and repeat new password below:",
"change": "Change password",
"newPassword": "Enter new password",
"newRePassword": "Repeat new password",
"enterTheCode": "Enter confirmation code"
"title": "Restore password",
"contactSupport": "Contact support",
"messageWasSent": "The recovery code was sent to your account Email.",
"messageWasSentTo": "The recovery code was sent to your Email {email}.",
"enterCodeBelow": "Please enter the code received into the field below:",
"enterNewPasswordBelow": "Enter and repeat new password below:",
"change": "Change password",
"newPassword": "Enter new password",
"newRePassword": "Repeat new password",
"enterTheCode": "Enter confirmation code"
}

View File

@@ -3,13 +3,13 @@ import messages from './RecoverPassword.intl.json';
import Body from './RecoverPasswordBody';
export default factory({
title: messages.title,
body: Body,
footer: {
color: 'lightViolet',
label: messages.change,
},
links: {
label: messages.contactSupport,
},
title: messages.title,
body: Body,
footer: {
color: 'lightViolet',
label: messages.change,
},
links: {
label: messages.contactSupport,
},
});

View File

@@ -11,70 +11,67 @@ import messages from './RecoverPassword.intl.json';
// TODO: activation code field may be decoupled into common component and reused here and in activation panel
export default class RecoverPasswordBody extends BaseAuthBody {
static displayName = 'RecoverPasswordBody';
static panelId = 'recoverPassword';
static hasGoBack = true;
static displayName = 'RecoverPasswordBody';
static panelId = 'recoverPassword';
static hasGoBack = true;
autoFocusField =
this.props.match.params && this.props.match.params.key
? 'newPassword'
: 'key';
autoFocusField = this.props.match.params && this.props.match.params.key ? 'newPassword' : 'key';
render() {
const { user } = this.context;
const { key } = this.props.match.params;
render() {
const { user } = this.context;
const { key } = this.props.match.params;
return (
<div>
{this.renderErrors()}
return (
<div>
{this.renderErrors()}
<p className={styles.descriptionText}>
{user.maskedEmail ? (
<Message
{...messages.messageWasSentTo}
values={{
email: <b>{user.maskedEmail}</b>,
}}
/>
) : (
<Message {...messages.messageWasSent} />
)}{' '}
<Message {...messages.enterCodeBelow} />
</p>
<p className={styles.descriptionText}>
{user.maskedEmail ? (
<Message
{...messages.messageWasSentTo}
values={{
email: <b>{user.maskedEmail}</b>,
}}
/>
) : (
<Message {...messages.messageWasSent} />
)}{' '}
<Message {...messages.enterCodeBelow} />
</p>
<Input
{...this.bindField('key')}
color="lightViolet"
center
required
value={key}
readOnly={!!key}
autoComplete="off"
placeholder={messages.enterTheCode}
/>
<Input
{...this.bindField('key')}
color="lightViolet"
center
required
value={key}
readOnly={!!key}
autoComplete="off"
placeholder={messages.enterTheCode}
/>
<p className={styles.descriptionText}>
<Message {...messages.enterNewPasswordBelow} />
</p>
<p className={styles.descriptionText}>
<Message {...messages.enterNewPasswordBelow} />
</p>
<Input
{...this.bindField('newPassword')}
icon="key"
color="lightViolet"
type="password"
required
placeholder={messages.newPassword}
/>
<Input
{...this.bindField('newPassword')}
icon="key"
color="lightViolet"
type="password"
required
placeholder={messages.newPassword}
/>
<Input
{...this.bindField('newRePassword')}
icon="key"
color="lightViolet"
type="password"
required
placeholder={messages.newRePassword}
/>
</div>
);
}
<Input
{...this.bindField('newRePassword')}
icon="key"
color="lightViolet"
type="password"
required
placeholder={messages.newRePassword}
/>
</div>
);
}
}

View File

@@ -1,8 +1,8 @@
@import '~app/components/ui/colors.scss';
.descriptionText {
font-size: 15px;
line-height: 1.4;
margin-bottom: 8px;
color: #aaa;
font-size: 15px;
line-height: 1.4;
margin-bottom: 8px;
color: #aaa;
}

View File

@@ -3,40 +3,36 @@ import auth from './reducer';
import { setLogin, setAccountSwitcher } from './actions';
describe('components/auth/reducer', () => {
describe('auth:setCredentials', () => {
it('should set login', () => {
const expectedLogin = 'foo';
describe('auth:setCredentials', () => {
it('should set login', () => {
const expectedLogin = 'foo';
expect(
auth(undefined, setLogin(expectedLogin)).credentials,
'to satisfy',
{
login: expectedLogin,
},
);
});
});
describe('auth:setAccountSwitcher', () => {
it('should be enabled by default', () =>
expect(auth(undefined, {} as any), 'to satisfy', {
isSwitcherEnabled: true,
}));
it('should enable switcher', () => {
const expectedValue = true;
expect(auth(undefined, setAccountSwitcher(expectedValue)), 'to satisfy', {
isSwitcherEnabled: expectedValue,
});
expect(auth(undefined, setLogin(expectedLogin)).credentials, 'to satisfy', {
login: expectedLogin,
});
});
});
it('should disable switcher', () => {
const expectedValue = false;
describe('auth:setAccountSwitcher', () => {
it('should be enabled by default', () =>
expect(auth(undefined, {} as any), 'to satisfy', {
isSwitcherEnabled: true,
}));
expect(auth(undefined, setAccountSwitcher(expectedValue)), 'to satisfy', {
isSwitcherEnabled: expectedValue,
});
it('should enable switcher', () => {
const expectedValue = true;
expect(auth(undefined, setAccountSwitcher(expectedValue)), 'to satisfy', {
isSwitcherEnabled: expectedValue,
});
});
it('should disable switcher', () => {
const expectedValue = false;
expect(auth(undefined, setAccountSwitcher(expectedValue)), 'to satisfy', {
isSwitcherEnabled: expectedValue,
});
});
});
});
});

View File

@@ -3,173 +3,156 @@ import { RootState } from 'app/reducers';
import { Scope } from '../../services/api/oauth';
import {
ErrorAction,
CredentialsAction,
AccountSwitcherAction,
LoadingAction,
ClientAction,
OAuthAction,
ScopesAction,
ErrorAction,
CredentialsAction,
AccountSwitcherAction,
LoadingAction,
ClientAction,
OAuthAction,
ScopesAction,
} from './actions';
export interface Credentials {
login?: string | null; // By some reasons there is can be null value. Need to investigate.
password?: string;
rememberMe?: boolean;
returnUrl?: string;
isRelogin?: boolean;
isTotpRequired?: boolean;
login?: string | null; // By some reasons there is can be null value. Need to investigate.
password?: string;
rememberMe?: boolean;
returnUrl?: string;
isRelogin?: boolean;
isTotpRequired?: boolean;
}
type Error = Record<
string,
| string
| {
type: string;
payload: Record<string, any>;
}
string,
| string
| {
type: string;
payload: Record<string, any>;
}
> | null;
export interface Client {
id: string;
name: string;
description: string;
id: string;
name: string;
description: string;
}
export interface OAuthState {
clientId: string;
redirectUrl: string;
responseType: string;
description?: string;
scope: string;
prompt: string;
loginHint: string;
state: string;
success?: boolean;
code?: string;
displayCode?: boolean;
acceptRequired?: boolean;
clientId: string;
redirectUrl: string;
responseType: string;
description?: string;
scope: string;
prompt: string;
loginHint: string;
state: string;
success?: boolean;
code?: string;
displayCode?: boolean;
acceptRequired?: boolean;
}
type Scopes = Array<Scope>;
export interface State {
credentials: Credentials;
error: Error;
isLoading: boolean;
isSwitcherEnabled: boolean;
client: Client | null;
oauth: OAuthState | null;
scopes: Scopes;
credentials: Credentials;
error: Error;
isLoading: boolean;
isSwitcherEnabled: boolean;
client: Client | null;
oauth: OAuthState | null;
scopes: Scopes;
}
const error: Reducer<State['error'], ErrorAction> = (
state = null,
{ type, payload },
) => {
if (type === 'auth:error') {
return payload;
}
return state;
};
const credentials: Reducer<State['credentials'], CredentialsAction> = (
state = {},
{ type, payload },
) => {
if (type === 'auth:setCredentials') {
if (payload) {
return {
...payload,
};
const error: Reducer<State['error'], ErrorAction> = (state = null, { type, payload }) => {
if (type === 'auth:error') {
return payload;
}
return {};
}
return state;
return state;
};
const isSwitcherEnabled: Reducer<
State['isSwitcherEnabled'],
AccountSwitcherAction
> = (state = true, { type, payload }) => {
if (type === 'auth:setAccountSwitcher') {
return payload;
}
const credentials: Reducer<State['credentials'], CredentialsAction> = (state = {}, { type, payload }) => {
if (type === 'auth:setCredentials') {
if (payload) {
return {
...payload,
};
}
return state;
return {};
}
return state;
};
const isLoading: Reducer<State['isLoading'], LoadingAction> = (
state = false,
{ type, payload },
const isSwitcherEnabled: Reducer<State['isSwitcherEnabled'], AccountSwitcherAction> = (
state = true,
{ type, payload },
) => {
if (type === 'set_loading_state') {
return payload;
}
if (type === 'auth:setAccountSwitcher') {
return payload;
}
return state;
return state;
};
const client: Reducer<State['client'], ClientAction> = (
state = null,
{ type, payload },
) => {
if (type === 'set_client') {
return payload;
}
const isLoading: Reducer<State['isLoading'], LoadingAction> = (state = false, { type, payload }) => {
if (type === 'set_loading_state') {
return payload;
}
return state;
return state;
};
const client: Reducer<State['client'], ClientAction> = (state = null, { type, payload }) => {
if (type === 'set_client') {
return payload;
}
return state;
};
const oauth: Reducer<State['oauth'], OAuthAction> = (state = null, action) => {
switch (action.type) {
case 'set_oauth':
return action.payload;
case 'set_oauth_result':
return {
...(state as OAuthState),
...action.payload,
};
case 'require_permissions_accept':
return {
...(state as OAuthState),
acceptRequired: true,
};
default:
return state;
}
switch (action.type) {
case 'set_oauth':
return action.payload;
case 'set_oauth_result':
return {
...(state as OAuthState),
...action.payload,
};
case 'require_permissions_accept':
return {
...(state as OAuthState),
acceptRequired: true,
};
default:
return state;
}
};
const scopes: Reducer<State['scopes'], ScopesAction> = (
state = [],
{ type, payload },
) => {
if (type === 'set_scopes') {
return payload;
}
const scopes: Reducer<State['scopes'], ScopesAction> = (state = [], { type, payload }) => {
if (type === 'set_scopes') {
return payload;
}
return state;
return state;
};
export default combineReducers<State>({
credentials,
error,
isLoading,
isSwitcherEnabled,
client,
oauth,
scopes,
credentials,
error,
isLoading,
isSwitcherEnabled,
client,
oauth,
scopes,
});
export function getLogin(
state: RootState | Pick<RootState, 'auth'>,
): string | null {
return state.auth.credentials.login || null;
export function getLogin(state: RootState | Pick<RootState, 'auth'>): string | null {
return state.auth.credentials.login || null;
}
export function getCredentials(state: RootState): Credentials {
return state.auth.credentials;
return state.auth.credentials;
}

View File

@@ -1,10 +1,10 @@
{
"registerTitle": "Sign Up",
"yourNickname": "Your nickname",
"yourEmail": "Your Email",
"accountPassword": "Account password",
"repeatPassword": "Repeat password",
"signUpButton": "Register",
"acceptRules": "I agree with {link}",
"termsOfService": "terms of service"
"registerTitle": "Sign Up",
"yourNickname": "Your nickname",
"yourEmail": "Your Email",
"accountPassword": "Account password",
"repeatPassword": "Repeat password",
"signUpButton": "Register",
"acceptRules": "I agree with {link}",
"termsOfService": "terms of service"
}

View File

@@ -5,19 +5,19 @@ import messages from './Register.intl.json';
import Body from './RegisterBody';
export default factory({
title: messages.registerTitle,
body: Body,
footer: {
color: 'blue',
label: messages.signUpButton,
},
links: [
{
label: activationMessages.didNotReceivedEmail,
payload: { requestEmail: true },
title: messages.registerTitle,
body: Body,
footer: {
color: 'blue',
label: messages.signUpButton,
},
{
label: forgotPasswordMessages.alreadyHaveCode,
},
],
links: [
{
label: activationMessages.didNotReceivedEmail,
payload: { requestEmail: true },
},
{
label: forgotPasswordMessages.alreadyHaveCode,
},
],
});

View File

@@ -11,73 +11,73 @@ import messages from './Register.intl.json';
// TODO: password and username can be validate for length and sameness
export default class RegisterBody extends BaseAuthBody {
static panelId = 'register';
static panelId = 'register';
autoFocusField = 'username';
autoFocusField = 'username';
render() {
return (
<div>
{this.renderErrors()}
render() {
return (
<div>
{this.renderErrors()}
<Input
{...this.bindField('username')}
icon="user"
color="blue"
type="text"
required
placeholder={messages.yourNickname}
/>
<Input
{...this.bindField('username')}
icon="user"
color="blue"
type="text"
required
placeholder={messages.yourNickname}
/>
<Input
{...this.bindField('email')}
icon="envelope"
color="blue"
type="email"
required
placeholder={messages.yourEmail}
/>
<Input
{...this.bindField('email')}
icon="envelope"
color="blue"
type="email"
required
placeholder={messages.yourEmail}
/>
<Input
{...this.bindField('password')}
icon="key"
color="blue"
type="password"
required
placeholder={passwordMessages.accountPassword}
/>
<Input
{...this.bindField('password')}
icon="key"
color="blue"
type="password"
required
placeholder={passwordMessages.accountPassword}
/>
<Input
{...this.bindField('rePassword')}
icon="key"
color="blue"
type="password"
required
placeholder={messages.repeatPassword}
/>
<Input
{...this.bindField('rePassword')}
icon="key"
color="blue"
type="password"
required
placeholder={messages.repeatPassword}
/>
<Captcha {...this.bindField('captcha')} delay={600} />
<Captcha {...this.bindField('captcha')} delay={600} />
<div className={styles.checkboxInput}>
<Checkbox
{...this.bindField('rulesAgreement')}
color="blue"
required
label={
<Message
{...messages.acceptRules}
values={{
link: (
<Link to="/rules" target="_blank">
<Message {...messages.termsOfService} />
</Link>
),
}}
/>
}
/>
</div>
</div>
);
}
<div className={styles.checkboxInput}>
<Checkbox
{...this.bindField('rulesAgreement')}
color="blue"
required
label={
<Message
{...messages.acceptRules}
values={{
link: (
<Link to="/rules" target="_blank">
<Message {...messages.termsOfService} />
</Link>
),
}}
/>
}
/>
</div>
</div>
);
}
}

View File

@@ -1,5 +1,5 @@
{
"title": "Did not received an Email",
"specifyYourEmail": "Please, enter an Email you've registered with and we will send you new activation code",
"sendNewEmail": "Send new Email"
"title": "Did not received an Email",
"specifyYourEmail": "Please, enter an Email you've registered with and we will send you new activation code",
"sendNewEmail": "Send new Email"
}

View File

@@ -4,13 +4,13 @@ import messages from './ResendActivation.intl.json';
import Body from './ResendActivationBody';
export default factory({
title: messages.title,
body: Body,
footer: {
color: 'blue',
label: messages.sendNewEmail,
},
links: {
label: forgotPasswordMessages.alreadyHaveCode,
},
title: messages.title,
body: Body,
footer: {
color: 'blue',
label: messages.sendNewEmail,
},
links: {
label: forgotPasswordMessages.alreadyHaveCode,
},
});

View File

@@ -8,33 +8,33 @@ import styles from './resendActivation.scss';
import messages from './ResendActivation.intl.json';
export default class ResendActivation extends BaseAuthBody {
static displayName = 'ResendActivation';
static panelId = 'resendActivation';
static hasGoBack = true;
static displayName = 'ResendActivation';
static panelId = 'resendActivation';
static hasGoBack = true;
autoFocusField = 'email';
autoFocusField = 'email';
render() {
return (
<div>
{this.renderErrors()}
render() {
return (
<div>
{this.renderErrors()}
<div className={styles.description}>
<Message {...messages.specifyYourEmail} />
</div>
<div className={styles.description}>
<Message {...messages.specifyYourEmail} />
</div>
<Input
{...this.bindField('email')}
icon="envelope"
color="blue"
type="email"
required
placeholder={registerMessages.yourEmail}
defaultValue={this.context.user.email}
/>
<Input
{...this.bindField('email')}
icon="envelope"
color="blue"
type="email"
required
placeholder={registerMessages.yourEmail}
defaultValue={this.context.user.email}
/>
<Captcha {...this.bindField('captcha')} delay={600} />
</div>
);
}
<Captcha {...this.bindField('captcha')} delay={600} />
</div>
);
}
}

View File

@@ -1,8 +1,8 @@
@import '~app/components/ui/fonts.scss';
.description {
font-family: $font-family-title;
margin: 5px 0 19px;
line-height: 1.4;
font-size: 16px;
font-family: $font-family-title;
margin: 5px 0 19px;
line-height: 1.4;
font-size: 16px;
}