From ca754b1d04e6b1b3e92112708d1d9e9119983237 Mon Sep 17 00:00:00 2001 From: Vivek Chand Date: Sun, 28 Jul 2019 05:32:12 +0530 Subject: [PATCH] [component] Add login component #18 Closes #18 --- .eslintrc.json | 65 ++-- client/assets/default/index.css | 1 + client/components/login/index.css | 213 ++++++++++ client/components/login/index.js | 18 + client/components/login/login.js | 363 ++++++++++++++++++ .../components/organization-wrapper/index.js | 14 +- .../organization-wrapper.js | 14 +- client/constants/index.js | 1 + client/index.js | 9 +- client/utils/render-additional-info.js | 96 +++++ org-configurations/default-configuration.yml | 65 +++- package-lock.json | 100 +++-- package.json | 6 + 13 files changed, 871 insertions(+), 94 deletions(-) create mode 100644 client/assets/default/index.css create mode 100644 client/components/login/index.css create mode 100644 client/components/login/index.js create mode 100644 client/components/login/login.js create mode 100644 client/utils/render-additional-info.js diff --git a/.eslintrc.json b/.eslintrc.json index 3e9ce49a7..cfe7863e5 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,34 +1,35 @@ { - "extends": [ - "airbnb", - "prettier", - "prettier/react", - "jest-enzyme", - "plugin:jest/recommended" - ], - "parser": "babel-eslint", - "plugins": ["prettier", "jest"], - "parserOptions": { - "ecmaVersion": 2016, - "sourceType": "module", - "ecmaFeatures": { - "jsx": true - } - }, - "env": { - "es6": true, - "browser": true, - "node": true - }, - "rules": { - "array-bracket-spacing": [2, "never"], - "semi": [2, "always"], - "react/prefer-stateless-function": [0], - "react/jsx-filename-extension": [ - 1, - { - "extensions": [".js", ".jsx"] - } - ] - } + "extends": [ + "airbnb", + "prettier", + "prettier/react", + "jest-enzyme", + "plugin:jest/recommended" + ], + "parser": "babel-eslint", + "plugins": ["prettier", "jest"], + "parserOptions": { + "ecmaVersion": 2016, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "env": { + "es6": true, + "browser": true, + "node": true + }, + "rules": { + "array-bracket-spacing": [2, "never"], + "semi": [2, "always"], + "jsx-a11y/label-has-for": 0, + "react/prefer-stateless-function": [0], + "react/jsx-filename-extension": [ + 1, + { + "extensions": [".js", ".jsx"] + } + ] + } } diff --git a/client/assets/default/index.css b/client/assets/default/index.css new file mode 100644 index 000000000..16c2c3224 --- /dev/null +++ b/client/assets/default/index.css @@ -0,0 +1 @@ +/* */ diff --git a/client/components/login/index.css b/client/components/login/index.css new file mode 100644 index 000000000..350d3a3d1 --- /dev/null +++ b/client/components/login/index.css @@ -0,0 +1,213 @@ +.owisp-login-container { + flex-grow: 1; + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; + background: #f2f2f2; + width: 100%; +} +.owisp-login-form { + font-family: sans-serif; + background: #ffffff; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 24px 50px; +} +.owisp-login-form.success .owisp-login-input:invalid { + box-shadow: none; +} +.owisp-login-header-content { + text-transform: capitalize; + color: #444; + font-size: 28px; + margin-bottom: 4px; +} +.owisp-login-fieldset { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + align-self: flex-start; +} +.owisp-login-label { + margin-top: 15px; + margin-bottom: 5px; +} +.owisp-login-label-text { + color: #6b6b6b; + text-transform: capitalize; +} +.owisp-login-input { + padding: 10px; + border-radius: 3px; + font-size: 18px; + border: 2px solid #e7e7e7; + margin-top: 5px; + width: 380px; + box-sizing: border-box; +} +.owisp-login-input:focus { + border-color: #a7a7a7; +} +.owisp-login-input.error { + border-color: #d93025; +} +.owisp-login-form-btn { + width: 100%; + font-size: 18px; + height: 50px; + background: #293b52; + color: #ffffff; + text-transform: capitalize; + border: 0; + border-radius: 3px; + cursor: pointer; +} +.owisp-login-form-btn:focus { + outline: dotted 1px #444; + outline-offset: 0.5px; +} +.owisp-login-login-btn { + margin-top: 4px; +} +.owisp-login-form-btn:hover { + background: #223144; +} +.owisp-login-error { + font-size: 12px; + color: #d93025; + width: 324px; + display: flex; + flex-direction: row; + justify-content: flex-start; + flex-wrap: nowrap; +} +.owisp-login-error-icon { + display: inline-block; + background: #d93025; + color: #ffffff; + min-width: 16px; + align-self: center; + height: 16px; + border-radius: 50%; + text-align: center; + margin-right: 8px; + font-weight: 600; +} +.owisp-login-register-link { + text-decoration: none; + width: 100%; +} +.owisp-login-add-info { + margin-top: 15px; + color: #444; + max-width: 380px; +} +.owisp-login-additional-link { + color: #444; +} +.owisp-login-additional-link:hover, +.owisp-login-additional-link:focus { + outline: dotted 1px #444; + outline-offset: 0.5px; +} +.owisp-login-social-hr { + outline: 0; + border: 0; + text-align: center; + height: 1.5em; + line-height: 1em; + position: relative; + width: 100%; +} +.owisp-login-social-hr:before { + content: ""; + background: #e6e6e6; + position: absolute; + left: 0; + top: 48%; + width: 100%; + height: 2px; +} +.owisp-login-social-hr:after { + content: attr(data-content); + position: relative; + display: inline-block; + padding: 0 0.5em; + line-height: 1.5em; + color: #798993; + background-color: #fcfcfa; +} +.owisp-login-divider-description { + color: #798993; + text-transform: uppercase; + font-size: 0.9rem; + width: 100%; + text-align: center; +} +.owisp-login-social-links-div { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + flex-wrap: wrap; + margin-top: 8px; +} +.owisp-login-social-link { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + padding: 4px 8px; + align-items: center; + text-decoration: none; + color: #798993; + border-radius: 3px; + margin-right: 12px; + box-shadow: 1px 1px 1px 1px hsla(0, 0%, 53%, 0.33); +} +.owisp-login-social-link:hover { + background: #efefef; +} +.owisp-login-social-link:focus { + outline: dotted 1px #444; + outline-offset: 0.5px; +} +.owisp-login-social-link-icon { + width: 28px; + height: auto; +} +.owisp-login-social-link-text { + margin-left: 3px; + text-transform: capitalize; +} +.owisp-login-links-div { + display: flex; + flex-direction: column; + width: 100%; + justify-content: flex-start; + align-items: center; + margin-top: 20px; +} +.owisp-login-link { + line-height: 1.4; + text-decoration: none; + text-transform: capitalize; + color: #444; +} +.owisp-login-link:focus { + outline: dotted 1px #444; + outline-offset: 0.5px; +} +.owisp-login-link:hover { + text-decoration: underline; +} +.owisp-login-error-non-field { + box-sizing: border-box; + font-size: 16px; + width: 100%; + max-width: 380px; + margin-top: 15px; +} diff --git a/client/components/login/index.js b/client/components/login/index.js new file mode 100644 index 000000000..0094357b5 --- /dev/null +++ b/client/components/login/index.js @@ -0,0 +1,18 @@ +import {connect} from "react-redux"; + +import Component from "./login"; + +const mapStateToProps = state => { + return { + loginForm: state.organization.configuration.components.login_form, + privacyPolicy: state.organization.configuration.privacy_policy, + termsAndConditions: state.organization.configuration.terms_and_conditions, + language: state.language, + orgSlug: state.organization.configuration.slug, + }; +}; + +export default connect( + mapStateToProps, + null, +)(Component); diff --git a/client/components/login/login.js b/client/components/login/login.js new file mode 100644 index 000000000..67d8f18b6 --- /dev/null +++ b/client/components/login/login.js @@ -0,0 +1,363 @@ +/* eslint-disable camelcase */ +import "./index.css"; + +import axios from "axios"; +import PropTypes from "prop-types"; +import qs from "qs"; +import React from "react"; +import {Link} from "react-router-dom"; + +import {loginApiUrl} from "../../constants"; +import getAssetPath from "../../utils/get-asset-path"; +import getText from "../../utils/get-text"; +import renderAdditionalInfo from "../../utils/render-additional-info"; + +export default class Login extends React.Component { + constructor(props) { + super(props); + this.state = { + username: "", + password: "", + errors: {}, + }; + this.handleSubmit = this.handleSubmit.bind(this); + this.handleChange = this.handleChange.bind(this); + } + + handleChange(event) { + this.setState({[event.target.name]: event.target.value}); + } + + handleSubmit(event) { + event.preventDefault(); + const {orgSlug} = this.props; + const {username, password, errors} = this.state; + const url = loginApiUrl.replace("{orgSlug}", orgSlug); + this.setState({ + errors: {}, + }); + axios({ + method: "post", + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + url, + data: qs.stringify({ + username, + password, + }), + }) + .then(() => { + this.setState({ + errors: {}, + username: "", + password: "", + }); + }) + .catch(error => { + const {data} = error.response; + this.setState({ + errors: { + ...errors, + ...(data.non_field_errors + ? {nonField: data.non_field_errors[0]} + : {nonField: ""}), + ...(data.detail ? {nonField: data.detail} : {}), + ...(data.username ? {username: data.username} : {username: ""}), + ...(data.password ? {password: data.password} : {password: ""}), + }, + }); + }); + } + + render() { + const {errors, username, password} = this.state; + const { + language, + loginForm, + orgSlug, + termsAndConditions, + privacyPolicy, + } = this.props; + const { + links, + buttons, + input_fields, + social_login, + header, + additional_info_text, + } = loginForm; + return ( + +
+
+
+
+ {getText(header, language)} +
+
+
+ {errors.nonField && ( +
+ ! + + {errors.nonField} + +
+ )} + {input_fields.username ? ( + <> + + {errors.username && ( +
+ ! + + {errors.username} + +
+ )} + + ) : null} + {input_fields.password ? ( + <> + + {errors.password && ( +
+ ! + + {errors.password1} + +
+ )} + + ) : null} +
+ {additional_info_text ? ( +
+ {renderAdditionalInfo( + additional_info_text, + language, + termsAndConditions, + privacyPolicy, + orgSlug, + )} +
+ ) : null} + {buttons.login ? ( + <> + {buttons.login.label ? ( + + ) : null} + + + ) : null} + {buttons.register ? ( + <> + {buttons.register.label ? ( + + ) : null} + + + + + ) : null} + {social_login ? ( + <> + {social_login.divider_text ? ( +
+ ) : null} + {social_login.description ? ( +
+ {getText(social_login.description, language)} +
+ ) : null} + {social_login.links.length ? ( + <> +
+ {social_login.links.map(link => { + if (link.url) + return ( + + {link.icon ? ( + { + ) : null} + {link.text ? ( +
+ {getText(link.text, language)} +
+ ) : null} +
+ ); + return null; + })} +
+ + ) : null} + + ) : null} + {links ? ( +
+ {links.forget_password ? ( + + {getText(links.forget_password, language)} + + ) : null} + {links.register ? ( + + {getText(links.register, language)} + + ) : null} +
+ ) : null} +
+
+
+ ); + } +} + +Login.propTypes = { + loginForm: PropTypes.shape({ + header: PropTypes.object, + social_login: PropTypes.shape({ + divider_text: PropTypes.object, + description: PropTypes.object, + link: PropTypes.arrayOf(PropTypes.object), + }), + input_fields: PropTypes.shape({ + username: PropTypes.object, + password: PropTypes.object, + }), + additional_info_text: PropTypes.object, + buttons: PropTypes.object, + }).isRequired, + language: PropTypes.string.isRequired, + orgSlug: PropTypes.string.isRequired, + privacyPolicy: PropTypes.shape({ + title: PropTypes.object, + content: PropTypes.object, + }).isRequired, + termsAndConditions: PropTypes.shape({ + title: PropTypes.object, + content: PropTypes.object, + }).isRequired, +}; diff --git a/client/components/organization-wrapper/index.js b/client/components/organization-wrapper/index.js index 8c9997b5c..93f78e3fc 100644 --- a/client/components/organization-wrapper/index.js +++ b/client/components/organization-wrapper/index.js @@ -1,11 +1,13 @@ +import {withCookies} from "react-cookie"; import {connect} from "react-redux"; import setOrganization from "../../actions/set-organization"; import Component from "./organization-wrapper"; -const mapStateToProps = state => { +const mapStateToProps = (state, ownProps) => { return { organization: state.organization, + cookies: ownProps.cookies, }; }; @@ -16,7 +18,9 @@ const mapDispatchToProps = dispatch => { }, }; }; -export default connect( - mapStateToProps, - mapDispatchToProps, -)(Component); +export default withCookies( + connect( + mapStateToProps, + mapDispatchToProps, + )(Component), +); diff --git a/client/components/organization-wrapper/organization-wrapper.js b/client/components/organization-wrapper/organization-wrapper.js index c8aa31b10..d5c074246 100644 --- a/client/components/organization-wrapper/organization-wrapper.js +++ b/client/components/organization-wrapper/organization-wrapper.js @@ -9,6 +9,7 @@ import getAssetPath from "../../utils/get-asset-path"; import DoesNotExist from "../404"; import Footer from "../footer"; import Header from "../header"; +import Login from "../login"; import PasswordConfirm from "../password-confirm"; import PasswordReset from "../password-reset"; import Registration from "../registration"; @@ -16,14 +17,20 @@ import Registration from "../registration"; export default class OrganizationWrapper extends React.Component { constructor(props) { super(props); - const {match, setOrganization} = this.props; + const {match, setOrganization} = props; const organizationSlug = match.params.organization; if (organizationSlug) setOrganization(organizationSlug); } + componentDidUpdate(prevProps) { + const {setOrganization, match} = this.props; + if (prevProps.match.params.organization !== match.params.organization) { + if (match.params.organization) setOrganization(match.params.organization); + } + } + render() { - const {match} = this.props; - const {organization} = this.props; + const {organization, match} = this.props; const {title, favicon} = organization.configuration; const orgSlug = organization.configuration.slug; const cssPath = organization.configuration.css_path; @@ -46,6 +53,7 @@ export default class OrganizationWrapper extends React.Component { exact render={() => } /> + } />