Ghost/ghost/members-auth-pages/index.js
Fabien O'Carroll 07b6e73bd8 Added members lib module (#10260)
* Added members library inc. gateway

refs #10213

* Added the auth pages and build steps for them

refs #10213

* Cleaned up logs

* Updated gruntfile to run yarn for member auth

* Design refinements on members popups

* UI refinements

* Updated backend call to trigger only if frontend validation passes

* Design refinements for error messages

* Added error message for email failure

* Updated request-password-reset to not attempt to send headers twice

* Updated preact publicPath to relative path

* Build auth pages on init
2019-05-07 17:15:50 +02:00

396 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import './styles/members.css';
import {IconEmail, IconLock, IconName, IconClose, IconError} from './components/icons';
import { Component } from 'preact';
const origin = new URL(window.location).origin;
const membersApi = location.pathname.replace(/\/members\/auth\/?$/, '/ghost/api/v2/members');
const storage = window.localStorage;
var layer0 = require('./layer0');
function getFreshState() {
const [hash, formType, query] = window.location.hash.match(/^#([^?]+)\??(.*)$/) || ['#signin?', 'signin', ''];
return {
formData: {},
query,
formType,
showError: false,
submitFail: false
};
}
export default class App extends Component {
constructor() {
super();
this.state = getFreshState();
this.gatewayFrame = '';
window.addEventListener("hashchange", () => this.onHashChange(), false);
}
loadGateway() {
const blogUrl = window.location.href.substring(0, window.location.href.indexOf('/members/auth'));
const frame = window.document.createElement('iframe');
frame.id = 'member-gateway';
frame.style.display = 'none';
frame.src = `${blogUrl}/members/gateway`;
frame.onload = () => {
this.gatewayFrame = layer0(frame);
};
document.body.appendChild(frame);
}
componentDidMount() {
this.loadGateway();
}
onHashChange() {
this.setState(getFreshState());
}
onInputChange(e, name) {
let value = e.target.value;
this.setState({
formData: {
...this.state.formData,
[name]: value
}
});
}
submitForm(e) {
e.preventDefault();
if (this.hasFrontendError(this.state.formType)) {
return false;
}
switch (this.state.formType) {
case 'signin':
this.signin(this.state.formData);
break;
case 'signup':
this.signup(this.state.formData);
break;
case 'request-password-reset':
this.requestPasswordReset(this.state.formData);
break;
case 'password-reset-sent':
this.resendPasswordResetEmail(this.state.formData)
break;
case 'reset-password':
this.resetPassword(this.state.formData)
break;
}
return false;
}
signin({ email, password }) {
this.gatewayFrame.call('signin', {email, password}, (err, successful) => {
if (err || !successful) {
this.setState({
submitFail: true
});
}
});
}
signup({ name, email, password }) {
this.gatewayFrame.call('signup', { name, email, password }, (err, successful) => {
if (err || !successful) {
this.setState({
submitFail: true
});
}
});
}
requestPasswordReset({ email }) {
this.gatewayFrame.call('request-password-reset', {email}, (err, successful) => {
if (err || !successful) {
this.setState({
submitFail: true
});
} else {
window.location.hash = 'password-reset-sent';
}
});
}
resendPasswordResetEmail({ email }) {
this.gatewayFrame.call('request-password-reset', {email}, (err, successful) => {
if (err || !successful) {
this.setState({
submitFail: true
});
} else {
window.location.hash = 'password-reset-sent';
}
});
}
resetPassword({ password }) {
const queryParams = new URLSearchParams(this.state.query);
const token = queryParams.get('token') || '';
this.gatewayFrame.call('reset-password', {password, token}, (err, successful) => {
if (err || !successful) {
this.setState({
submitFail: true
});
}
});
}
hasFrontendError(formType = this.state.formType) {
switch(formType) {
case 'signin':
return (
this.hasError({errorType: 'no-input', data: 'email'}) ||
this.hasError({errorType: 'no-input', data: 'password'})
);
case 'signup':
return (
this.hasError({errorType: 'no-input', data: 'email'}) ||
this.hasError({errorType: 'no-input', data: 'password'}) ||
this.hasError({errorType: 'no-input', data: 'name'})
);
}
return false;
}
hasError({errorType, data}) {
if (!this.state.showError) {
return false;
}
let value = '';
switch(errorType) {
case 'no-input':
value = this.state.formData[data];
return (!value);
case 'form-submit':
return this.state.submitFail;
}
}
renderError({error, formType}) {
if (this.hasError(error)) {
let errorLabel = '';
switch(error.errorType) {
case 'no-input':
errorLabel = `Enter ${error.data}`;
break;
case 'form-submit':
switch(formType) {
case 'signin':
errorLabel = "Wrong email or password";
break;
case 'signup':
errorLabel = "Email already registered"
break;
case 'request-password-reset':
errorLabel = "Unable to send email"
break;
case 'password-reset-sent':
errorLabel = "Unable to send email"
break;
}
}
return (
<span>{ errorLabel }</span>
)
}
return null;
}
renderFormHeaders(formType) {
let mainTitle = '';
let ctaTitle = '';
let ctaLabel = '';
let hash = '';
switch (formType) {
case 'signup':
mainTitle = 'Sign Up';
ctaTitle = 'Already a member?';
ctaLabel = 'Log in';
hash = 'signin';
break;
case 'signin':
mainTitle = 'Log In';
ctaTitle = 'Not a member?';
ctaLabel = 'Sign up';
hash = 'signup';
break;
case 'request-password-reset':
mainTitle = 'Reset password';
ctaTitle = '';
ctaLabel = 'Log in';
hash = 'signin';
break;
case 'password-reset-sent':
mainTitle = 'Reset password';
ctaTitle = '';
ctaLabel = 'Log in';
hash = 'signin';
break;
case 'reset-password':
mainTitle = 'Reset password';
ctaTitle = '';
ctaLabel = 'Log in';
hash = 'signin';
break;
}
let formError = this.renderError({ error: {errorType: "form-submit"}, formType });
return (
<div className="flex flex-column">
<div className="gm-logo"></div>
<div className="gm-auth-header">
<h1>{ mainTitle }</h1>
<div className="flex items-baseline">
<h4>{ ctaTitle }</h4>
<a href="javascript:;"
onClick={(e) => {window.location.hash = hash}}
>
{ctaLabel}
</a>
</div>
</div>
{(formError ? <div class="gm-form-errortext"><i>{ IconError }</i> { formError }</div> : "")}
</div>
)
}
renderFormInput({type, name, label, icon, placeholder, required, formType}) {
let value = this.state.formData[name];
let className = "";
let forgot = (type === 'password' && formType === 'signin');
let inputError = this.renderError({ error: {errorType: 'no-input', data: name}, formType });
className += (value ? "gm-input-filled" : "") + (forgot ? " gm-forgot-input" : "") + (inputError ? " gm-error" : "");
return (
<div className="mt8">
<div className="gm-floating-input">
<input
type={ type }
name={ name }
key={ name }
placeholder={ placeholder }
value={ value || '' }
onInput={ (e) => this.onInputChange(e, name) }
required = {required}
className={ className }
/>
<label for={ name }> { label }</label>
<i>{ icon }</i>
{ (forgot ? <a href="javascript:;" className="gm-forgot-link" onClick={(e) => {window.location.hash = 'request-password-reset'}}>Forgot</a> : "") }
</div>
<div class="gm-input-errortext">{ inputError }</div>
</div>
)
}
renderFormText({formType}) {
return (
<div className="mt8">
<p>Weve sent a recovery email to your inbox. Follow the link in the email to reset your password.</p>
</div>
)
}
onSubmitClick(e) {
this.setState({
showError: true,
submitFail: false
});
}
renderFormSubmit({buttonLabel, formType}) {
return (
<div className="mt8">
<button type="submit" name={ formType } className="gm-btn-blue" onClick={(e) => this.onSubmitClick(e)}>{ buttonLabel }</button>
</div>
)
}
renderFormSection(formType) {
const emailInput = this.renderFormInput({
type: 'email',
name: 'email',
label: 'Email',
icon: IconEmail,
placeholder: 'Email...',
required: true,
formType: formType
});
const passwordInput = this.renderFormInput({
type: 'password',
name: 'password',
label: 'Password',
icon: IconLock,
placeholder: 'Password...',
required: true,
formType: formType
});
const nameInput = this.renderFormInput({
type: 'text',
name: 'name',
label: 'Name',
icon: IconName,
placeholder: 'Name...',
required: true,
formType: formType
});
const formText = this.renderFormText({formType});
let formElements = [];
let buttonLabel = '';
switch (formType) {
case 'signin':
buttonLabel = 'Log in';
formElements = [emailInput, passwordInput, this.renderFormSubmit({formType, buttonLabel})];
break;
case 'signup':
buttonLabel = 'Sign up';
formElements = [nameInput, emailInput, passwordInput, this.renderFormSubmit({formType, buttonLabel})];
break;
case 'request-password-reset':
buttonLabel = 'Send reset password instructions';
formElements = [emailInput, this.renderFormSubmit({formType, buttonLabel})];
break;
case 'password-reset-sent':
buttonLabel = 'Resend instructions';
formElements = [formText, this.renderFormSubmit({formType, buttonLabel})];
break;
case 'reset-password':
buttonLabel = 'Set password';
formElements = [passwordInput, this.renderFormSubmit({formType, buttonLabel})];
break;
}
return (
<div className="flex flex-column nt1">
<form className={ `gm-` + formType + `-form` } onSubmit={(e) => this.submitForm(e)} noValidate>
{ formElements }
</form>
</div>
)
}
renderFormComponent(formType = this.state.formType) {
return (
<div className="gm-modal-container">
<div className="gm-modal gm-auth-modal" onClick={(e) => e.stopPropagation()}>
<a className="gm-modal-close" onClick={ (e) => this.close(e)}>{ IconClose }</a>
{this.renderFormHeaders(formType)}
{this.renderFormSection(formType)}
</div>
</div>
);
}
render() {
return (
<div className="gm-page-overlay" onClick={(e) => this.close(e)}>
{this.renderFormComponent()}
</div>
);
}
close(event) {
window.parent.postMessage('pls-close-auth-popup', '*');
}
}