Commit 9e3772b9 authored by Sam Gluck's avatar Sam Gluck
Browse files

Add validation for the login form;

parent 7551810d
......@@ -44,7 +44,7 @@ interface MenuState {
}
export default class extends React.Component<MenuProps, MenuState> {
static state = {
state = {
openMenuName: null,
open: false
};
......
mutation SetUserQuery($user: User!) {
updateUser(user: $user) @client {
mutation SetUserQuery($isAuthenticated: Boolean, $data: User!) {
updateUser(isAuthenticated: $isAuthenticated, data: $data) @client {
isAuthenticated,
data
}
......
import * as React from 'react';
import compose from 'recompose/compose';
import { graphql, OperationOption } from 'react-apollo';
import { Route, Redirect, RouteComponentProps } from 'react-router-dom';
import { Grid, Row, Col } from '@zendeskgarden/react-grid';
import { Redirect, Route, RouteComponentProps } from 'react-router-dom';
import { Col, Grid, Row } from '@zendeskgarden/react-grid';
import { withTheme } from '@zendeskgarden/react-theming';
import styled from '../../themes/styled';
import styled, { ThemeInterface } from '../../themes/styled';
import Logo from '../../components/brand/Logo/Logo';
import LanguageSelect from '../../components/inputs/LanguageSelect/LanguageSelect';
import Body from '../../components/chrome/Body/Body';
......@@ -13,7 +13,7 @@ import Button from '../../components/elements/Button/Button';
import H6 from '../../components/typography/H6/H6';
import P from '../../components/typography/P/P';
import LoginForm from './LoginForm';
import { ThemeInterface } from '../../themes/styled';
import { ValidationField, ValidationObject, ValidationType } from './types';
const { GetUserQuery } = require('../../graphql/GET_USER.client.graphql');
const { SetUserQuery } = require('../../graphql/SET_USER.client.graphql');
......@@ -75,47 +75,111 @@ interface LoginProps extends RouteComponentProps {
interface LoginState {
redirectTo: string | null;
authenticating: boolean;
validation: ValidationObject[];
}
type CredentialsObject = {
username: string;
password: string;
};
const DEMO_CREDENTIALS = {
username: 'moodle',
password: 'moodle'
};
class Login extends React.Component<LoginProps, LoginState> {
state = {
redirectTo: null,
authenticating: false
authenticating: false,
validation: []
};
static validateCredentials(credentials: CredentialsObject) {
const validation: ValidationObject[] = [];
if (!credentials.username.length) {
validation.push({
field: ValidationField.username,
type: ValidationType.error,
message: 'The username field cannot be empty'
} as ValidationObject);
}
if (!credentials.password.length) {
validation.push({
field: ValidationField.password,
type: ValidationType.error,
message: 'The password field cannot be empty'
} as ValidationObject);
}
return validation;
}
constructor(props) {
super(props);
this.onLoginFormSubmit = this.onLoginFormSubmit.bind(this);
this.onLoginFormInputChange = this.onLoginFormInputChange.bind(this);
}
/**
* Submit the login form credentials to authenticate the user.
* TODO implement real auth + validation when we know what the backend looks like
* @param e {Event}
* @param credentials {Object}
*/
async onLoginFormSubmit(e) {
e.preventDefault();
async onLoginFormSubmit(credentials) {
const validation = Login.validateCredentials(credentials);
if (validation.length) {
this.setState({ validation });
return;
}
this.setState({
authenticating: true
});
// TODO implement real auth when we know what the backend looks like
setTimeout(async () => {
if (
credentials.username !== DEMO_CREDENTIALS.username ||
credentials.password !== DEMO_CREDENTIALS.password
) {
this.setState({
authenticating: false,
validation: [
{
field: null,
type: ValidationType.warning,
message:
'Could not log in. Please check your credentials or use the link below to reset your password.'
} as ValidationObject
]
});
return;
}
if (this.props.updateUser) {
await this.props.updateUser({
variables: {
isAuthenticated: true,
user: {}
data: {}
}
});
this.setState({
redirectTo: '/'
});
}
}, 1000);
}
/** Clear the validation messages for a field and also generic validations when its value changes. */
onLoginFormInputChange(field: ValidationField) {
this.setState({
validation: this.state.validation.filter(
(validation: ValidationObject) => {
return validation.field !== field && validation.field !== null;
}
)
});
}
render() {
if (this.state.redirectTo) {
return <Redirect to={this.state.redirectTo as any} />;
......@@ -189,7 +253,9 @@ class Login extends React.Component<LoginProps, LoginState> {
———
</P>
<LoginForm
onLoginFormSubmit={this.onLoginFormSubmit}
validation={this.state.validation}
onSubmit={this.onLoginFormSubmit}
onInputChange={this.onLoginFormInputChange}
authenticating={this.state.authenticating}
/>
</Col>
......@@ -247,7 +313,7 @@ export interface Args {
// get the user auth object from local cache
const withUser = graphql<{}, Args>(GetUserQuery);
// get user mutation resolver so we can set the user in the local cache
// get user mutation so we can set the user in the local cache
const withUserAuthentication = graphql<{}, Args>(SetUserQuery, {
name: 'updateUser'
// TODO enforce proper types for OperationOption
......
import * as React from 'react';
import { Link } from 'react-router-dom';
import { Row, Col } from '@zendeskgarden/react-grid';
import { TextField, Label } from '@zendeskgarden/react-textfields';
import { Col, Row } from '@zendeskgarden/react-grid';
import { Label, Message, TextField } from '@zendeskgarden/react-textfields';
import styled from '../../themes/styled';
import TextInput from '../../components/inputs/Text/Text';
import Button from '../../components/elements/Button/Button';
import Loader from '../../components/elements/Loader/Loader';
import { ValidationField, ValidationObject, ValidationType } from './types';
type SubmitColProps = {
alignRight?: boolean;
......@@ -29,36 +30,137 @@ const Spacer = styled.div`
height: 10px;
`;
export default ({ onLoginFormSubmit, authenticating }) => (
<LoginForm onSubmit={onLoginFormSubmit}>
<Row>
<Col>
<TextField>
<Label>Username:</Label>
<TextInput required placeholder="Enter your username" />
</TextField>
<Spacer />
<TextField>
<Label>Password:</Label>
<TextInput
required
type="password"
placeholder="Enter your password"
/>
</TextField>
</Col>
</Row>
<Row>
<SubmitCol>
<Link to="/reset-password" title="Forgotten password">
Forgotten password?
</Link>
</SubmitCol>
<SubmitCol alignRight>
<Button disabled={authenticating} type="submit">
{authenticating ? <Loader /> : 'Sign in'}
</Button>
</SubmitCol>
</Row>
</LoginForm>
);
type LoginFormProps = {
onSubmit: Function;
onInputChange: Function;
authenticating: boolean;
validation: ValidationObject[];
};
type LoginFormState = {
username: string;
password: string;
};
export default class extends React.Component<LoginFormProps, LoginFormState> {
state = {
username: '',
password: ''
};
constructor(props) {
super(props);
this.getValidation = this.getValidation.bind(this);
this.getValidationMessage = this.getValidationMessage.bind(this);
}
getValidation(field: ValidationField | null): ValidationType | null {
const validation = this.props.validation.find(
(validation: ValidationObject) => {
return validation.field === field;
}
);
if (validation) {
return validation.type;
}
return null;
}
getValidationMessage(field: ValidationField | null): String {
return this.props.validation.reduce(
(message: string, validation: ValidationObject) => {
if (validation.field === field) {
if (message.length) {
return (message += ', ' + validation.message);
} else {
return validation.message;
}
}
return message;
},
''
);
}
render() {
const { onInputChange, onSubmit, authenticating } = this.props;
return (
<LoginForm
onSubmit={evt => {
evt.preventDefault();
onSubmit(this.state);
}}
>
<Row>
<Col>
<TextField>
<Label>Username:</Label>
<TextInput
placeholder="Enter your username"
value={this.state.username}
validation={this.getValidation(ValidationField.username)}
onChange={evt => {
this.setState({
username: evt.target.value
});
onInputChange(ValidationField.username, evt.target.value);
}}
/>
<Message
validation={this.getValidation(ValidationField.username)}
>
{this.getValidationMessage(ValidationField.username)}
</Message>
</TextField>
<Spacer />
<TextField>
<Label>Password:</Label>
<TextInput
type="password"
placeholder="Enter your password"
value={this.state.password}
validation={this.getValidation(ValidationField.password)}
onChange={evt => {
this.setState({
password: evt.target.value
});
onInputChange(ValidationField.password, evt.target.value);
}}
/>
<Message
validation={this.getValidation(ValidationField.password)}
>
{this.getValidationMessage(ValidationField.password)}
</Message>
</TextField>
</Col>
</Row>
{this.getValidationMessage(null) ? (
<Row>
<Col>
<Message
style={{ margin: '10px 0' }}
validation={this.getValidation(null)}
>
{this.getValidationMessage(null)}
</Message>
</Col>
</Row>
) : null}
<Row>
<SubmitCol>
<Link to="/reset-password" title="Forgotten password">
Forgotten password?
</Link>
</SubmitCol>
<SubmitCol alignRight>
<Button disabled={authenticating} type="submit">
{authenticating ? <Loader /> : 'Sign in'}
</Button>
</SubmitCol>
</Row>
</LoginForm>
);
}
}
// as per Zendesk Garden TextField validation types
export enum ValidationType {
error = 'error',
warning = 'warning',
success = 'success'
}
export enum ValidationField {
username,
password
}
export type ValidationObject = {
type: ValidationType;
field: ValidationField | null;
message: string;
};
src/static/img/social-icons/google.png

2.75 KB | W: | H:

src/static/img/social-icons/google.png

2.28 KB | W: | H:

src/static/img/social-icons/google.png
src/static/img/social-icons/google.png
src/static/img/social-icons/google.png
src/static/img/social-icons/google.png
  • 2-up
  • Swipe
  • Onion skin
src/static/img/social-icons/twitter.png

2.63 KB | W: | H:

src/static/img/social-icons/twitter.png

1.98 KB | W: | H:

src/static/img/social-icons/twitter.png
src/static/img/social-icons/twitter.png
src/static/img/social-icons/twitter.png
src/static/img/social-icons/twitter.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -19,3 +19,9 @@ i.facebook {
i.google {
background-image: url(../static/img/social-icons/google.png);
}
*:hover > i.twitter,
*:hover > i.facebook,
*:hover > i.google {
background-position: left top;
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment