mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 19:27:52 +01:00
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:
committed by
GitHub
parent
3bda6dee08
commit
4c73b398e8
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
43
app/javascript/components/common/CopyToClipboardButton.tsx
Normal file
43
app/javascript/components/common/CopyToClipboardButton.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user