add email auth ui
This commit is contained in:
@@ -12,8 +12,27 @@ const __loginAnonymously = (username: string): Promise<User | null> => {
|
||||
return fetcher.get<User>({ url, withCredentials: true, overriddenApiBase: '' });
|
||||
};
|
||||
|
||||
const __loginViaEmail = (token: string): Promise<User | null> => {
|
||||
const url = `/auth/email/login?token=${token}`;
|
||||
return fetcher.get<User>({ url, withCredentials: true, overriddenApiBase: '' });
|
||||
};
|
||||
|
||||
/**
|
||||
* First step of two of `email` authorization
|
||||
*
|
||||
* @param username userrname
|
||||
* @param address email address
|
||||
*/
|
||||
export const sendEmailVerificationRequest = (username: string, address: string): Promise<void> => {
|
||||
const url = `/auth/email/login?id=${siteId}&user=${encodeURIComponent(username)}&address=${encodeURIComponent(
|
||||
address
|
||||
)}`;
|
||||
return fetcher.get({ url, withCredentials: true, overriddenApiBase: '' });
|
||||
};
|
||||
|
||||
export const logIn = (provider: AuthProvider): Promise<User | null> => {
|
||||
if (provider.name === 'anonymous') return __loginAnonymously(provider.username);
|
||||
if (provider.name === 'email') return __loginViaEmail(provider.token);
|
||||
|
||||
return new Promise<User | null>((resolve, reject) => {
|
||||
const url = `${BASE_URL}/auth/${provider.name}/login?from=${encodeURIComponent(
|
||||
|
||||
@@ -22,6 +22,7 @@ export const PROVIDER_NAMES: { [P in AuthProvider['name']]: string } = {
|
||||
yandex: 'Yandex',
|
||||
dev: 'Dev',
|
||||
anonymous: 'Anonymous',
|
||||
email: 'Email',
|
||||
};
|
||||
|
||||
/** locastorage key for collapsed comments */
|
||||
|
||||
@@ -119,7 +119,8 @@ export type AuthProvider =
|
||||
| { name: 'github' }
|
||||
| { name: 'yandex' }
|
||||
| { name: 'dev' }
|
||||
| { name: 'anonymous'; username: string };
|
||||
| { name: 'anonymous'; username: string }
|
||||
| { name: 'email'; token: string };
|
||||
|
||||
export type BlockTTL = 'permanently' | '43200m' | '10080m' | '1440m';
|
||||
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
.auth-panel-email-login-form {
|
||||
padding: 0.4em 0.6em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.auth-panel-email-login-form__input {
|
||||
width: 15em;
|
||||
margin: 0.1em;
|
||||
}
|
||||
|
||||
.auth-panel-email-login-form__submit {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
padding: 0.1em;
|
||||
margin-top: 0.2em;
|
||||
color: currentColor;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auth-panel-email-login-form__submit:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.auth-panel-email-login-form__back-button {
|
||||
color: #259c9a;
|
||||
cursor: pointer;
|
||||
margin-left: 0.1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-panel-email-login-form__error {
|
||||
color: #9a0000;
|
||||
text-align: center;
|
||||
margin-top: 1em;
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
/** @jsx h */
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
import { Theme, User } from '@app/common/types';
|
||||
import { sendEmailVerificationRequest } from '@app/common/api';
|
||||
import { extractErrorMessageFromResponse } from '@app/utils/errorUtils';
|
||||
import { connect } from 'preact-redux';
|
||||
import { getHandleClickProps } from '@app/common/accessibility';
|
||||
|
||||
const mapStateToProps = () => ({
|
||||
sendEmailVerification: sendEmailVerificationRequest,
|
||||
});
|
||||
|
||||
type Props = {
|
||||
onSignIn(token: string): Promise<User | null>;
|
||||
onSuccess?(user: User): Promise<void>;
|
||||
theme: Theme;
|
||||
className?: string;
|
||||
} & ReturnType<typeof mapStateToProps>;
|
||||
|
||||
interface State {
|
||||
usernameValue: string;
|
||||
addressValue: string;
|
||||
tokenValue: string;
|
||||
verificationSent: boolean;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export class EmailLoginForm extends Component<Props, State> {
|
||||
static usernameRegex = /^[a-zA-Z][\w ]+$/;
|
||||
static emailRegex = /[^@]+@[^.]+\..+/;
|
||||
|
||||
inputRef?: HTMLInputElement;
|
||||
tokenRef?: HTMLInputElement;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
usernameValue: '',
|
||||
addressValue: '',
|
||||
tokenValue: '',
|
||||
verificationSent: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
this.onVerificationSubmit = this.onVerificationSubmit.bind(this);
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.onUsernameChange = this.onUsernameChange.bind(this);
|
||||
this.onAddressChange = this.onAddressChange.bind(this);
|
||||
this.onTokenChange = this.onTokenChange.bind(this);
|
||||
this.goBack = this.goBack.bind(this);
|
||||
}
|
||||
|
||||
async onVerificationSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
this.setState({ loading: true });
|
||||
try {
|
||||
await this.props.sendEmailVerification(this.state.usernameValue, this.state.addressValue);
|
||||
this.setState({ verificationSent: true });
|
||||
setTimeout(() => {
|
||||
this.tokenRef && this.tokenRef.focus();
|
||||
}, 100);
|
||||
} catch (e) {
|
||||
this.setState({ error: extractErrorMessageFromResponse(e) });
|
||||
} finally {
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
}
|
||||
|
||||
async onSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
this.setState({ loading: true });
|
||||
const user = await this.props.onSignIn(this.state.tokenValue);
|
||||
if (!user) {
|
||||
this.setState({ error: 'No user was found' });
|
||||
return;
|
||||
}
|
||||
this.props.onSuccess && this.props.onSuccess(user);
|
||||
} catch (e) {
|
||||
this.setState({ error: extractErrorMessageFromResponse(e) });
|
||||
} finally {
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
}
|
||||
|
||||
onUsernameChange(e: Event) {
|
||||
this.setState({ error: null, usernameValue: (e.target as HTMLInputElement).value });
|
||||
}
|
||||
|
||||
onAddressChange(e: Event) {
|
||||
this.setState({ error: null, addressValue: (e.target as HTMLInputElement).value });
|
||||
}
|
||||
|
||||
onTokenChange(e: Event) {
|
||||
this.setState({ error: null, tokenValue: (e.target as HTMLInputElement).value });
|
||||
}
|
||||
|
||||
goBack() {
|
||||
this.setState({
|
||||
tokenValue: '',
|
||||
error: null,
|
||||
verificationSent: false,
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.inputRef && this.inputRef.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
getForm1InvalidReason(): string | null {
|
||||
if (this.state.loading) return 'Loading...';
|
||||
const username = this.state.usernameValue;
|
||||
if (username.length < 3) return 'Username must be at least 3 characters long';
|
||||
if (!EmailLoginForm.usernameRegex.test(username))
|
||||
return 'Username must start from the letter and contain only latin letters, numbers, underscores, and spaces';
|
||||
if (!EmailLoginForm.emailRegex.test(this.state.addressValue)) return 'Address should be valid email address';
|
||||
return null;
|
||||
}
|
||||
|
||||
getForm2InvalidReason(): string | null {
|
||||
if (this.state.loading) return 'Loading...';
|
||||
if (this.state.tokenValue.length === 0) return 'Token field must not be empty';
|
||||
return null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
setTimeout(() => {
|
||||
this.inputRef && this.inputRef.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
render(props: RenderableProps<Props>) {
|
||||
// TODO: will be great to `b` to accept `string | undefined | (string|undefined)[]` as classname
|
||||
let className = b('auth-panel-email-login-form', {}, { theme: props.theme });
|
||||
if (props.className) {
|
||||
className += ' ' + b('auth-panel-email-login-form', {}, { theme: props.theme });
|
||||
}
|
||||
|
||||
const form1InvalidReason = this.getForm1InvalidReason();
|
||||
|
||||
if (!this.state.verificationSent)
|
||||
return (
|
||||
<form className={className} onSubmit={this.onVerificationSubmit}>
|
||||
{/*
|
||||
* We adding hidden span element to bear with DropDown's onOutSideClick handler.
|
||||
* This function checks if element that was clicked is a children of it's root component.
|
||||
* And the problem is that by the time handler gets executed our target element is not a
|
||||
* part of a dom, so handler suggests that we clicked somewhere outside and hides dropdown
|
||||
*/}
|
||||
<span
|
||||
className="auth-panel-email-login-form__back-button"
|
||||
role="button"
|
||||
{...getHandleClickProps(this.goBack)}
|
||||
style={{ display: 'none' }}
|
||||
>
|
||||
{'< Back'}
|
||||
</span>
|
||||
<input
|
||||
className="auth-panel-email-login-form__input"
|
||||
ref={ref => (this.inputRef = ref)}
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={this.state.usernameValue}
|
||||
onInput={this.onUsernameChange}
|
||||
/>
|
||||
<input
|
||||
className="auth-panel-email-login-form__input"
|
||||
type="text"
|
||||
placeholder="Email Address"
|
||||
value={this.state.addressValue}
|
||||
onInput={this.onAddressChange}
|
||||
/>
|
||||
<input
|
||||
className="auth-panel-email-login-form__submit"
|
||||
type="submit"
|
||||
value="Send Verification"
|
||||
title={form1InvalidReason || ''}
|
||||
disabled={form1InvalidReason !== null}
|
||||
/>
|
||||
{this.state.error && <div class="auth-panel-email-login-form__error">{this.state.error}</div>}
|
||||
</form>
|
||||
);
|
||||
|
||||
const form2InvalidReason = this.getForm2InvalidReason();
|
||||
|
||||
return (
|
||||
<form className={className} onSubmit={this.onSubmit}>
|
||||
<span className="auth-panel-email-login-form__back-button" role="button" {...getHandleClickProps(this.goBack)}>
|
||||
{'< Back'}
|
||||
</span>
|
||||
<input
|
||||
className="auth-panel-email-login-form__input"
|
||||
ref={ref => (this.tokenRef = ref)}
|
||||
type="text"
|
||||
placeholder="Token"
|
||||
value={this.state.tokenValue}
|
||||
onInput={this.onTokenChange}
|
||||
/>
|
||||
<input
|
||||
className="auth-panel-email-login-form__submit"
|
||||
type="submit"
|
||||
value="Confirm"
|
||||
title={form2InvalidReason || ''}
|
||||
disabled={form2InvalidReason !== null}
|
||||
/>
|
||||
{this.state.error && <div class="auth-panel-email-login-form__error">{this.state.error}</div>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const EmailLoginFormConnected = connect(mapStateToProps)(EmailLoginForm);
|
||||
@@ -0,0 +1,3 @@
|
||||
export { EmailLoginForm, EmailLoginFormConnected } from './auth-panel__email-login-form';
|
||||
|
||||
require('./auth-panel__email-login-form.scss');
|
||||
@@ -11,6 +11,7 @@ import Dropdown, { DropdownItem } from '@app/components/dropdown';
|
||||
import { Button } from '@app/components/button';
|
||||
import { UserID } from './__user-id';
|
||||
import { AnonymousLoginForm } from './__anonymous-login-form';
|
||||
import { EmailLoginFormConnected as EmailLoginForm } from './__email-login-form';
|
||||
import { StoreState } from '@app/store';
|
||||
|
||||
export interface Props {
|
||||
@@ -49,6 +50,7 @@ export class AuthPanel extends Component<Props, State> {
|
||||
this.toggleCommentsAvailability = this.toggleCommentsAvailability.bind(this);
|
||||
this.onSortChange = this.onSortChange.bind(this);
|
||||
this.onSignIn = this.onSignIn.bind(this);
|
||||
this.onEmailSignIn = this.onEmailSignIn.bind(this);
|
||||
this.handleAnonymousLoginFormSubmut = this.handleAnonymousLoginFormSubmut.bind(this);
|
||||
this.handleOAuthLogin = this.handleOAuthLogin.bind(this);
|
||||
this.toggleUserInfoVisibility = this.toggleUserInfoVisibility.bind(this);
|
||||
@@ -94,6 +96,10 @@ export class AuthPanel extends Component<Props, State> {
|
||||
this.props.onSignIn(provider);
|
||||
}
|
||||
|
||||
onEmailSignIn(token: string) {
|
||||
return this.props.onSignIn({ name: 'email', token });
|
||||
}
|
||||
|
||||
async handleAnonymousLoginFormSubmut(username: string) {
|
||||
this.onSignIn({ name: 'anonymous', username });
|
||||
}
|
||||
@@ -173,6 +179,27 @@ export class AuthPanel extends Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
if (provider === 'email') {
|
||||
return (
|
||||
<span>
|
||||
{comma}{' '}
|
||||
<Dropdown
|
||||
title={PROVIDER_NAMES[provider]}
|
||||
titleClass="auth-panel__pseudo-link"
|
||||
theme={this.props.theme}
|
||||
>
|
||||
<DropdownItem>
|
||||
<EmailLoginForm
|
||||
onSignIn={this.onEmailSignIn}
|
||||
theme={this.props.theme}
|
||||
className="auth-panel__email-login-form"
|
||||
/>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{comma}
|
||||
|
||||
Reference in New Issue
Block a user