Add OAuth2 authentication (#147)

- Added Site settings > Authentication section
- Create/edit/delete your custom oauth2 configurations
- Login or signup with oauth2
This commit is contained in:
Riccardo Graziosi
2022-08-05 18:15:17 +02:00
committed by GitHub
parent 3bda6dee08
commit 4c73b398e8
65 changed files with 2096 additions and 129 deletions

View File

@@ -0,0 +1,46 @@
import * as React from 'react';
import I18n from 'i18n-js';
import Box from '../../common/Box';
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
import OAuthForm, { ISiteSettingsOAuthForm } from './OAuthForm';
import Spinner from '../../common/Spinner';
import { DangerText } from '../../common/CustomTexts';
import { IOAuth } from '../../../interfaces/IOAuth';
interface Props {
handleSubmitOAuth(oAuth: IOAuth): void;
handleUpdateOAuth(id: number, form: ISiteSettingsOAuthForm): void;
isSubmitting: boolean;
submitError: string;
selectedOAuth: IOAuth;
page: AuthenticationPages;
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
}
const AuthenticationFormPage = ({
handleSubmitOAuth,
handleUpdateOAuth,
isSubmitting,
submitError,
selectedOAuth,
page,
setPage,
}: Props) => (
<>
<Box customClass="authenticationFormPage">
<OAuthForm
handleSubmitOAuth={handleSubmitOAuth}
handleUpdateOAuth={handleUpdateOAuth}
selectedOAuth={selectedOAuth}
page={page}
setPage={setPage}
/>
{ isSubmitting && <Spinner /> }
{ submitError && <DangerText>{submitError}</DangerText> }
</Box>
</>
);
export default AuthenticationFormPage;

View File

@@ -0,0 +1,53 @@
import * as React from 'react';
import I18n from 'i18n-js';
import Box from '../../common/Box';
import OAuthProvidersList from './OAuthProvidersList';
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
import { OAuthsState } from '../../../reducers/oAuthsReducer';
import SiteSettingsInfoBox from '../../common/SiteSettingsInfoBox';
interface Props {
oAuths: OAuthsState;
isSubmitting: boolean;
submitError: string;
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
handleDeleteOAuth(id: number): void;
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
setSelectedOAuth: React.Dispatch<React.SetStateAction<number>>;
}
const AuthenticationIndexPage = ({
oAuths,
isSubmitting,
submitError,
handleToggleEnabledOAuth,
handleDeleteOAuth,
setPage,
setSelectedOAuth,
}: Props) => (
<>
<Box customClass="authenticationIndexPage">
<h2>{ I18n.t('site_settings.authentication.title') }</h2>
<OAuthProvidersList
oAuths={oAuths.items}
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
handleDeleteOAuth={handleDeleteOAuth}
setPage={setPage}
setSelectedOAuth={setSelectedOAuth}
/>
</Box>
<SiteSettingsInfoBox
areUpdating={oAuths.areLoading || isSubmitting}
error={oAuths.error || submitError}
/>
</>
);
export default AuthenticationIndexPage;

View File

@@ -0,0 +1,89 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import HttpStatus from '../../../constants/http_status';
import { IOAuth } from '../../../interfaces/IOAuth';
import { OAuthsState } from '../../../reducers/oAuthsReducer';
import AuthenticationFormPage from './AuthenticationFormPage';
import AuthenticationIndexPage from './AuthenticationIndexPage';
import { ISiteSettingsOAuthForm } from './OAuthForm';
interface Props {
oAuths: OAuthsState;
requestOAuths(): void;
onSubmitOAuth(oAuth: IOAuth, authenticityToken: string): Promise<any>;
onUpdateOAuth(id: number, form: ISiteSettingsOAuthForm, authenticityToken: string): Promise<any>;
onToggleEnabledOAuth(id: number, isEnabled: boolean, authenticityToken: string): void;
onDeleteOAuth(id: number, authenticityToken: string): void;
isSubmitting: boolean;
submitError: string;
authenticityToken: string;
}
export type AuthenticationPages = 'index' | 'new' | 'edit';
const AuthenticationSiteSettingsP = ({
oAuths,
requestOAuths,
onSubmitOAuth,
onUpdateOAuth,
onToggleEnabledOAuth,
onDeleteOAuth,
isSubmitting,
submitError,
authenticityToken,
}: Props) => {
const [page, setPage] = useState<AuthenticationPages>('index');
const [selectedOAuth, setSelectedOAuth] = useState<number>(null);
useEffect(requestOAuths, []);
const handleSubmitOAuth = (oAuth: IOAuth) => {
onSubmitOAuth(oAuth, authenticityToken).then(res => {
if (res?.status === HttpStatus.Created) setPage('index');
});
};
const handleUpdateOAuth = (id: number, form: ISiteSettingsOAuthForm) => {
onUpdateOAuth(id, form, authenticityToken).then(res => {
if (res?.status === HttpStatus.OK) setPage('index');
});
};
const handleToggleEnabledOAuth = (id: number, enabled: boolean) => {
onToggleEnabledOAuth(id, enabled, authenticityToken);
};
const handleDeleteOAuth = (id: number) => {
onDeleteOAuth(id, authenticityToken);
};
return (
page === 'index' ?
<AuthenticationIndexPage
oAuths={oAuths}
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
handleDeleteOAuth={handleDeleteOAuth}
setPage={setPage}
setSelectedOAuth={setSelectedOAuth}
isSubmitting={isSubmitting}
submitError={submitError}
/>
:
<AuthenticationFormPage
handleSubmitOAuth={handleSubmitOAuth}
handleUpdateOAuth={handleUpdateOAuth}
isSubmitting={isSubmitting}
submitError={submitError}
selectedOAuth={oAuths.items.find(oAuth => oAuth.id === selectedOAuth)}
page={page}
setPage={setPage}
/>
);
};
export default AuthenticationSiteSettingsP;

View File

@@ -0,0 +1,247 @@
import * as React from 'react';
import I18n from 'i18n-js';
import { SubmitHandler, useForm } from 'react-hook-form';
import { DangerText } from '../../common/CustomTexts';
import { getLabel, getValidationMessage } from '../../../helpers/formUtils';
import Button from '../../common/Button';
import { URL_REGEX } from '../../../constants/regex';
import { IOAuth } from '../../../interfaces/IOAuth';
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
import { useState } from 'react';
import Separator from '../../common/Separator';
interface Props {
selectedOAuth: IOAuth;
page: AuthenticationPages;
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
handleSubmitOAuth(oAuth: IOAuth): void;
handleUpdateOAuth(id: number, form: ISiteSettingsOAuthForm): void;
}
export interface ISiteSettingsOAuthForm {
name: string;
logo: string;
clientId: string;
clientSecret: string;
authorizeUrl: string;
tokenUrl: string;
profileUrl: string;
scope: string;
jsonUserEmailPath: string;
jsonUserNamePath: string;
}
const OAuthForm = ({
selectedOAuth,
page,
setPage,
handleSubmitOAuth,
handleUpdateOAuth,
}: Props) => {
const [editClientSecret, setEditClientSecret] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty }
} = useForm<ISiteSettingsOAuthForm>({
defaultValues: page === 'new' ? {
name: '',
logo: '',
clientId: '',
clientSecret: '',
authorizeUrl: '',
tokenUrl: '',
profileUrl: '',
scope: '',
jsonUserEmailPath: '',
jsonUserNamePath: '',
} : {
name: selectedOAuth.name,
logo: selectedOAuth.logo,
clientId: selectedOAuth.clientId,
clientSecret: selectedOAuth.clientSecret,
authorizeUrl: selectedOAuth.authorizeUrl,
tokenUrl: selectedOAuth.tokenUrl,
profileUrl: selectedOAuth.profileUrl,
scope: selectedOAuth.scope,
jsonUserEmailPath: selectedOAuth.jsonUserEmailPath,
jsonUserNamePath: selectedOAuth.jsonUserNamePath,
},
});
const onSubmit: SubmitHandler<ISiteSettingsOAuthForm> = data => {
const oAuth = { ...data, isEnabled: false };
if (page === 'new') {
handleSubmitOAuth(oAuth);
} else if (page === 'edit') {
if (editClientSecret === false) {
delete oAuth.clientSecret;
}
handleUpdateOAuth(selectedOAuth.id, oAuth as ISiteSettingsOAuthForm);
}
};
return (
<>
<a
onClick={() => {
let confirmation = true;
if (isDirty)
confirmation = confirm(I18n.t('common.unsaved_changes') + ' ' + I18n.t('common.confirmation'));
if (confirmation) setPage('index');
}}
className="backButton link">
{ I18n.t('common.buttons.back') }
</a>
<h2>{ I18n.t(`site_settings.authentication.form.title_${page}`) }</h2>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="formRow">
<div className="formGroup col-6">
<label htmlFor="name">{ getLabel('o_auth', 'name') }</label>
<input
{...register('name', { required: true })}
id="name"
className="formControl"
/>
<DangerText>{errors.name && getValidationMessage(errors.name.type, 'o_auth', 'name')}</DangerText>
</div>
<div className="formGroup col-6">
<label htmlFor="logo">{ getLabel('o_auth', 'logo') }</label>
<input
{...register('logo')}
id="logo"
className="formControl"
/>
</div>
</div>
<h5>{ I18n.t('site_settings.authentication.form.subtitle_oauth_config') }</h5>
<div className="formRow">
<div className="formGroup col-6">
<label htmlFor="clientId">{ getLabel('o_auth', 'client_id') }</label>
<input
{...register('clientId', { required: true })}
id="clientId"
className="formControl"
/>
<DangerText>{errors.clientId && getValidationMessage(errors.clientId.type, 'o_auth', 'client_id')}</DangerText>
</div>
<div className="formGroup col-6">
<label htmlFor="clientSecret">{ getLabel('o_auth', 'client_secret') }</label>
<input
{...register('clientSecret', { required: page === 'new' || (page === 'edit' && editClientSecret) })}
id="clientSecret"
className="formControl"
disabled={page === 'edit' && editClientSecret === false}
/>
{
page === 'edit' &&
<>
<small>
{I18n.t('site_settings.authentication.form.client_secret_help') + "\t"}
</small>
<Separator />
{
editClientSecret ?
<a onClick={() => setEditClientSecret(false)} className="link">{I18n.t('common.buttons.cancel')}</a>
:
<a onClick={() => setEditClientSecret(true)} className="link">{I18n.t('common.buttons.edit')}</a>
}
<br />
</>
}
<DangerText>{errors.clientSecret && getValidationMessage(errors.clientSecret.type, 'o_auth', 'client_secret')}</DangerText>
</div>
</div>
<div className="formRow">
<div className="formGroup col-6">
<label htmlFor="authorizeUrl">{ getLabel('o_auth', 'authorize_url') }</label>
<input
{...register('authorizeUrl', { required: true, pattern: URL_REGEX })}
id="authorizeUrl"
className="formControl"
/>
<DangerText>{errors.authorizeUrl?.type === 'required' && getValidationMessage(errors.authorizeUrl.type, 'o_auth', 'authorize_url')}</DangerText>
<DangerText>{errors.authorizeUrl?.type === 'pattern' && I18n.t('common.validations.url')}</DangerText>
</div>
<div className="formGroup col-6">
<label htmlFor="tokenUrl">{ getLabel('o_auth', 'token_url') }</label>
<input
{...register('tokenUrl', { required: true, pattern: URL_REGEX })}
id="tokenUrl"
className="formControl"
/>
<DangerText>{errors.tokenUrl?.type === 'required' && getValidationMessage(errors.tokenUrl.type, 'o_auth', 'token_url')}</DangerText>
<DangerText>{errors.tokenUrl?.type === 'pattern' && I18n.t('common.validations.url')}</DangerText>
</div>
</div>
<div className="formGroup">
<label htmlFor="scope">{ getLabel('o_auth', 'scope') }</label>
<input
{...register('scope', { required: true })}
id="scope"
className="formControl"
/>
<DangerText>{errors.scope && getValidationMessage(errors.scope.type, 'o_auth', 'scope')}</DangerText>
</div>
<h5>{ I18n.t('site_settings.authentication.form.subtitle_user_profile_config') }</h5>
<div className="formGroup">
<label htmlFor="profileUrl">{ getLabel('o_auth', 'profile_url') }</label>
<input
{...register('profileUrl', { required: true, pattern: URL_REGEX })}
id="profileUrl"
className="formControl"
/>
<DangerText>{errors.profileUrl?.type === 'required' && getValidationMessage(errors.profileUrl.type, 'o_auth', 'profile_url')}</DangerText>
<DangerText>{errors.profileUrl?.type === 'pattern' && I18n.t('common.validations.url')}</DangerText>
</div>
<div className="formRow">
<div className="formGroup col-6">
<label htmlFor="jsonUserEmailPath">{ getLabel('o_auth', 'json_user_email_path') }</label>
<input
{...register('jsonUserEmailPath', { required: true })}
id="jsonUserEmailPath"
className="formControl"
/>
<DangerText>
{errors.jsonUserEmailPath && getValidationMessage(errors.jsonUserEmailPath.type, 'o_auth', 'json_user_email_path')}
</DangerText>
</div>
<div className="formGroup col-6">
<label htmlFor="jsonUserNamePath">{ getLabel('o_auth', 'json_user_name_path') }</label>
<input
{...register('jsonUserNamePath')}
id="jsonUserNamePath"
className="formControl"
/>
</div>
</div>
<Button onClick={() => null}>
{
page === 'new' ?
I18n.t('common.buttons.create')
:
I18n.t('common.buttons.update')
}
</Button>
</form>
</>
);
}
export default OAuthForm;

View File

@@ -0,0 +1,68 @@
import * as React from 'react';
import I18n from 'i18n-js';
import { IOAuth } from '../../../interfaces/IOAuth';
import Switch from '../../common/Switch';
import Separator from '../../common/Separator';
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
import CopyToClipboardButton from '../../common/CopyToClipboardButton';
interface Props {
oAuth: IOAuth;
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
handleDeleteOAuth(id: number): void;
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
setSelectedOAuth: React.Dispatch<React.SetStateAction<number>>;
}
const OAuthProviderItem = ({
oAuth,
handleToggleEnabledOAuth,
handleDeleteOAuth,
setPage,
setSelectedOAuth,
}: Props) => (
<li className="oAuthListItem">
<div className="oAuthInfo">
<img src={oAuth.logo} className="oAuthLogo" width={42} height={42} />
<div className="oAuthNameAndEnabled">
<span className="oAuthName">{oAuth.name}</span>
<div className="oAuthIsEnabled">
<Switch
label={I18n.t(`common.${oAuth.isEnabled ? 'enabled' : 'disabled'}`)}
onClick={() => handleToggleEnabledOAuth(oAuth.id, !oAuth.isEnabled)}
checked={oAuth.isEnabled}
htmlId={`oAuth${oAuth.name}EnabledSwitch`}
/>
</div>
</div>
</div>
<div className="oAuthActions">
<CopyToClipboardButton
label={I18n.t('site_settings.authentication.copy_url')}
textToCopy={oAuth.callbackUrl}
/>
<Separator />
<a onClick={() =>
window.open(`/o_auths/${oAuth.id}/start?reason=test`, '', 'width=640, height=640')
}>
Test
</a>
<Separator />
<a onClick={() => {
setSelectedOAuth(oAuth.id);
setPage('edit');
}}>
{ I18n.t('common.buttons.edit') }
</a>
<Separator />
<a onClick={() => confirm(I18n.t('common.confirmation')) && handleDeleteOAuth(oAuth.id)}>
{ I18n.t('common.buttons.delete') }
</a>
</div>
</li>
);
export default OAuthProviderItem;

View File

@@ -0,0 +1,49 @@
import * as React from 'react';
import I18n from 'i18n-js';
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
import Button from '../../common/Button';
import { IOAuth } from '../../../interfaces/IOAuth';
import OAuthProviderItem from './OAuthProviderItem';
interface Props {
oAuths: Array<IOAuth>;
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
handleDeleteOAuth(id: number): void;
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
setSelectedOAuth: React.Dispatch<React.SetStateAction<number>>;
}
const OAuthProvidersList = ({
oAuths,
handleToggleEnabledOAuth,
handleDeleteOAuth,
setPage,
setSelectedOAuth,
}: Props) => (
<>
<div className="oauthProvidersTitle">
<h3>{ I18n.t('site_settings.authentication.oauth_subtitle') }</h3>
<Button onClick={() => setPage('new')}>
{ I18n.t('common.buttons.new') }
</Button>
</div>
<ul className="oAuthsList">
{
oAuths.map((oAuth, i) => (
<OAuthProviderItem
oAuth={oAuth}
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
handleDeleteOAuth={handleDeleteOAuth}
setPage={setPage}
setSelectedOAuth={setSelectedOAuth}
key={i}
/>
))
}
</ul>
</>
);
export default OAuthProvidersList;

View File

@@ -0,0 +1,33 @@
import * as React from 'react';
import { Provider } from 'react-redux';
import { Store } from 'redux';
import AuthenticationSiteSettings from '../../../containers/AuthenticationSiteSettings';
import createStoreHelper from '../../../helpers/createStore';
import { State } from '../../../reducers/rootReducer';
interface Props {
authenticityToken: string;
}
class AuthenticationSiteSettingsRoot extends React.Component<Props> {
store: Store<State, any>;
constructor(props: Props) {
super(props);
this.store = createStoreHelper();
}
render() {
return (
<Provider store={this.store}>
<AuthenticationSiteSettings
authenticityToken={this.props.authenticityToken}
/>
</Provider>
);
}
}
export default AuthenticationSiteSettingsRoot;

View File

@@ -67,7 +67,7 @@ const BoardForm = ({
<input
{...register('name', { required: true })}
placeholder={I18n.t('site_settings.boards.form.name')}
autoFocus
autoFocus={mode === 'update'}
className="formControl"
/>

View File

@@ -13,6 +13,7 @@ import {
TENANT_BRAND_NONE,
} from '../../../interfaces/ITenant';
import { DangerText } from '../../common/CustomTexts';
import { getLabel, getValidationMessage } from '../../../helpers/formUtils';
export interface ISiteSettingsGeneralForm {
siteName: string;
@@ -79,17 +80,17 @@ const GeneralSiteSettingsP = ({
<form onSubmit={handleSubmit(onSubmit)}>
<div className="formRow">
<div className="formGroup col-4">
<label htmlFor="siteName">{ I18n.t('site_settings.general.site_name') }</label>
<label htmlFor="siteName">{ getLabel('tenant', 'site_name') }</label>
<input
{...register('siteName', { required: true })}
id="siteName"
className="formControl"
/>
<DangerText>{errors.siteName && I18n.t('site_settings.general.validations.site_name')}</DangerText>
<DangerText>{errors.siteName && getValidationMessage(errors.siteName.type, 'tenant', 'site_name')}</DangerText>
</div>
<div className="formGroup col-4">
<label htmlFor="siteLogo">{ I18n.t('site_settings.general.site_logo') }</label>
<label htmlFor="siteLogo">{ getLabel('tenant', 'site_logo') }</label>
<input
{...register('siteLogo')}
id="siteLogo"
@@ -98,7 +99,7 @@ const GeneralSiteSettingsP = ({
</div>
<div className="formGroup col-4">
<label htmlFor="brandSetting">{ I18n.t('site_settings.general.brand_setting') }</label>
<label htmlFor="brandSetting">{ getLabel('tenant', 'brand_setting') }</label>
<select
{...register('brandDisplaySetting')}
id="brandSetting"
@@ -121,7 +122,7 @@ const GeneralSiteSettingsP = ({
</div>
<div className="formGroup">
<label htmlFor="locale">{ I18n.t('site_settings.general.locale') }</label>
<label htmlFor="locale">{ getLabel('tenant', 'locale') }</label>
<select
{...register('locale')}
id="locale"

View File

@@ -71,7 +71,7 @@ const PostStatusForm = ({
<input
{...register('name', { required: true })}
placeholder={I18n.t('site_settings.post_statuses.form.name')}
autoFocus
autoFocus={mode === 'update'}
className="formControl"
/>

View File

@@ -8,6 +8,7 @@ import SiteSettingsInfoBox from '../../common/SiteSettingsInfoBox';
import { UsersState } from '../../../reducers/usersReducer';
import { UserRoles, USER_STATUS_ACTIVE, USER_STATUS_BLOCKED } from '../../../interfaces/IUser';
import HttpStatus from '../../../constants/http_status';
import Spinner from '../../common/Spinner';
interface Props {
users: UsersState;
@@ -79,17 +80,20 @@ class UsersSiteSettingsP extends React.Component<Props> {
<ul className="usersList">
{
users.items.map((user, i) => (
<UserEditable
user={user}
updateUserRole={this._handleUpdateUserRole}
updateUserStatus={this._handleUpdateUserStatus}
users.areLoading === false ?
users.items.map((user, i) => (
<UserEditable
user={user}
updateUserRole={this._handleUpdateUserRole}
updateUserStatus={this._handleUpdateUserStatus}
currentUserEmail={currentUserEmail}
currentUserRole={currentUserRole}
key={i}
/>
))
currentUserEmail={currentUserEmail}
currentUserRole={currentUserRole}
key={i}
/>
))
:
<Spinner />
}
</ul>
</Box>

View File

@@ -8,6 +8,7 @@ import Spinner from '../common/Spinner';
import { DangerText } from '../common/CustomTexts';
import { ITenantSignUpTenantForm } from './TenantSignUpP';
import HttpStatus from '../../constants/http_status';
import { getLabel, getValidationMessage } from '../../helpers/formUtils';
interface Props {
isSubmitting: boolean;
@@ -34,11 +35,11 @@ const TenantSignUpForm = ({
<input
{...register('siteName', { required: true })}
autoFocus
placeholder={I18n.t('signup.step2.site_name')}
placeholder={getLabel('tenant', 'site_name')}
id="tenantSiteName"
className="formControl"
/>
<DangerText>{errors.siteName && I18n.t('signup.step2.validations.site_name')}</DangerText>
<DangerText>{errors.siteName?.type === 'required' && getValidationMessage('required', 'tenant', 'site_name')}</DangerText>
</div>
<div className="formRow">
@@ -51,7 +52,7 @@ const TenantSignUpForm = ({
return res.status === HttpStatus.OK;
},
})}
placeholder={I18n.t('signup.step2.subdomain')}
placeholder={getLabel('tenant', 'subdomain')}
id="tenantSubdomain"
className="formControl"
/>
@@ -60,7 +61,7 @@ const TenantSignUpForm = ({
</div>
</div>
<DangerText>
{errors.subdomain?.type === 'required' && I18n.t('signup.step2.validations.subdomain')}
{errors.subdomain?.type === 'required' && getValidationMessage('required', 'tenant', 'subdomain')}
</DangerText>
<DangerText>
{errors.subdomain?.type === 'validate' && I18n.t('signup.step2.validations.subdomain_already_taken')}

View File

@@ -6,6 +6,8 @@ import Box from '../common/Box';
import Button from '../common/Button';
import { ITenantSignUpUserForm } from './TenantSignUpP';
import { DangerText } from '../common/CustomTexts';
import { getLabel, getValidationMessage } from '../../helpers/formUtils';
import { EMAIL_REGEX } from '../../constants/regex';
interface Props {
currentStep: number;
@@ -24,7 +26,13 @@ const UserSignUpForm = ({
userData,
setUserData,
}: Props) => {
const { register, handleSubmit, setError, formState: { errors } } = useForm<ITenantSignUpUserForm>();
const {
register,
handleSubmit,
setError,
getValues,
formState: { errors }
} = useForm<ITenantSignUpUserForm>();
const onSubmit: SubmitHandler<ITenantSignUpUserForm> = data => {
if (data.password !== data.passwordConfirmation) {
setError('passwordConfirmation', I18n.t('signup.step1.validations.password_mismatch'));
@@ -53,22 +61,25 @@ const UserSignUpForm = ({
<input
{...register('fullName', { required: true, minLength: 2 })}
autoFocus
placeholder={I18n.t('common.forms.auth.full_name')}
placeholder={getLabel('user', 'full_name')}
id="userFullName"
className="formControl"
/>
<DangerText>{ errors.fullName && I18n.t('signup.step1.validations.full_name') }</DangerText>
<DangerText>{errors.fullName && getValidationMessage('required', 'user', 'full_name')}</DangerText>
</div>
<div className="formRow">
<input
{...register('email', { required: true, pattern: /(.+)@(.+){2,}\.(.+){2,}/ })}
{...register('email', { required: true, pattern: EMAIL_REGEX })}
type="email"
placeholder={I18n.t('common.forms.auth.email')}
placeholder={getLabel('user', 'email')}
id="userEmail"
className="formControl"
/>
<DangerText>{ errors.email && I18n.t('signup.step1.validations.email') }</DangerText>
<DangerText>{errors.email?.type === 'required' && getValidationMessage('required', 'user', 'email')}</DangerText>
<DangerText>
{errors.email?.type === 'pattern' && I18n.t('common.validations.email')}
</DangerText>
</div>
<div className="formRow">
@@ -76,22 +87,22 @@ const UserSignUpForm = ({
<input
{...register('password', { required: true, minLength: 6, maxLength: 128 })}
type="password"
placeholder={I18n.t('common.forms.auth.password')}
placeholder={getLabel('user', 'password')}
id="userPassword"
className="formControl"
/>
<DangerText>{ errors.password && I18n.t('signup.step1.validations.password', { n: 6 }) }</DangerText>
<DangerText>{ errors.password && I18n.t('common.validations.password', { n: 6 }) }</DangerText>
</div>
<div className="formGroup col-6">
<input
{...register('passwordConfirmation')}
type="password"
placeholder={I18n.t('common.forms.auth.password_confirmation')}
placeholder={getLabel('user', 'password_confirmation')}
id="userPasswordConfirmation"
className="formControl"
/>
<DangerText>{ errors.passwordConfirmation && I18n.t('signup.step1.validations.password_mismatch') }</DangerText>
<DangerText>{ errors.passwordConfirmation && I18n.t('common.validations.password_mismatch') }</DangerText>
</div>
</div>

View File

@@ -0,0 +1,43 @@
import * as React from 'react';
import I18n from 'i18n-js';
import { useState } from 'react';
interface Props {
label: string;
textToCopy: string;
copiedLabel?: string;
}
const CopyToClipboardButton = ({
label,
textToCopy,
copiedLabel = I18n.t('common.copied')
}: Props) => {
const [ready, setReady] = useState(true);
const alertError = () =>
alert(`Error in automatically copying to clipboard. Please copy the callback url manually:\n\n${textToCopy}`);
return (
ready ?
<a
onClick={() => {
if (navigator.clipboard) {
navigator.clipboard.writeText(textToCopy).then(() => {
setReady(false);
setTimeout(() => setReady(true), 2000);
},
alertError);
} else {
alertError();
}
}}
>
{label}
</a>
:
<span>{copiedLabel}</span>
);
};
export default CopyToClipboardButton;