Add the possibility to enable/disable default OAuths (#303)

This commit is contained in:
Riccardo Graziosi
2024-03-05 18:13:16 +01:00
committed by GitHub
parent 719f1ad4e9
commit 32d19cbe7c
31 changed files with 508 additions and 131 deletions

View File

@@ -15,6 +15,7 @@ interface Props {
submitError: string;
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
handleToggleEnabledDefaultOAuth(id: number, enabled: boolean): void;
handleDeleteOAuth(id: number): void;
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
@@ -27,6 +28,7 @@ const AuthenticationIndexPage = ({
submitError,
handleToggleEnabledOAuth,
handleToggleEnabledDefaultOAuth,
handleDeleteOAuth,
setPage,
@@ -48,6 +50,7 @@ const AuthenticationIndexPage = ({
<OAuthProvidersList
oAuths={oAuths.items}
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
handleToggleEnabledDefaultOAuth={handleToggleEnabledDefaultOAuth}
handleDeleteOAuth={handleDeleteOAuth}
setPage={setPage}
setSelectedOAuth={setSelectedOAuth}

View File

@@ -16,6 +16,7 @@ interface Props {
onSubmitOAuth(oAuth: IOAuth, authenticityToken: string): Promise<any>;
onUpdateOAuth(id: number, form: ISiteSettingsOAuthForm, authenticityToken: string): Promise<any>;
onToggleEnabledOAuth(id: number, isEnabled: boolean, authenticityToken: string): void;
onToggleEnabledDefaultOAuth(id: number, isEnabled: boolean, authenticityToken: string): void;
onDeleteOAuth(id: number, authenticityToken: string): void;
isSubmitting: boolean;
@@ -32,6 +33,7 @@ const AuthenticationSiteSettingsP = ({
onSubmitOAuth,
onUpdateOAuth,
onToggleEnabledOAuth,
onToggleEnabledDefaultOAuth,
onDeleteOAuth,
isSubmitting,
submitError,
@@ -58,6 +60,10 @@ const AuthenticationSiteSettingsP = ({
onToggleEnabledOAuth(id, enabled, authenticityToken);
};
const handleToggleEnabledDefaultOAuth = (id: number, enabled: boolean) => {
onToggleEnabledDefaultOAuth(id, enabled, authenticityToken);
};
const handleDeleteOAuth = (id: number) => {
onDeleteOAuth(id, authenticityToken);
};
@@ -67,6 +73,7 @@ const AuthenticationSiteSettingsP = ({
<AuthenticationIndexPage
oAuths={oAuths}
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
handleToggleEnabledDefaultOAuth={handleToggleEnabledDefaultOAuth}
handleDeleteOAuth={handleDeleteOAuth}
setPage={setPage}
setSelectedOAuth={setSelectedOAuth}

View File

@@ -12,6 +12,7 @@ import { MutedText } from '../../common/CustomTexts';
interface Props {
oAuth: IOAuth;
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
handleToggleEnabledDefaultOAuth(id: number, enabled: boolean): void;
handleDeleteOAuth(id: number): void;
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
setSelectedOAuth: React.Dispatch<React.SetStateAction<number>>;
@@ -20,6 +21,7 @@ interface Props {
const OAuthProviderItem = ({
oAuth,
handleToggleEnabledOAuth,
handleToggleEnabledDefaultOAuth,
handleDeleteOAuth,
setPage,
setSelectedOAuth,
@@ -41,48 +43,59 @@ const OAuthProviderItem = ({
/>
</div>
:
<div><MutedText>{I18n.t('site_settings.authentication.default_oauth')}</MutedText></div>
<div className="oAuthIsEnabled">
<Switch
label={I18n.t(`common.${oAuth.isEnabled ? 'enabled' : 'disabled'}`)}
onClick={() => handleToggleEnabledDefaultOAuth(oAuth.id, !oAuth.defaultOAuthIsEnabled)}
checked={oAuth.defaultOAuthIsEnabled}
htmlId={`oAuth${oAuth.name}EnabledSwitch`}
/>
</div>
}
</div>
</div>
{
oAuth.tenantId &&
<div className="oAuthActions">
<CopyToClipboardButton
label={I18n.t('site_settings.authentication.copy_url')}
textToCopy={oAuth.callbackUrl}
/>
<ActionLink
onClick={() =>
window.open(`/o_auths/${oAuth.id}/start?reason=test`, '', 'width=640, height=640')
}
icon={<TestIcon />}
customClass='testAction'
>
{I18n.t('common.buttons.test')}
</ActionLink>
<ActionLink
onClick={() => {
setSelectedOAuth(oAuth.id);
setPage('edit');
}}
icon={<EditIcon />}
customClass='editAction'
>
{I18n.t('common.buttons.edit')}
</ActionLink>
<ActionLink
onClick={() => confirm(I18n.t('common.confirmation')) && handleDeleteOAuth(oAuth.id)}
icon={<DeleteIcon />}
customClass='deleteAction'
>
{I18n.t('common.buttons.delete')}
</ActionLink>
</div>
oAuth.tenantId ?
<div className="oAuthActions">
<CopyToClipboardButton
label={I18n.t('site_settings.authentication.copy_url')}
textToCopy={oAuth.callbackUrl}
/>
<ActionLink
onClick={() =>
window.open(`/o_auths/${oAuth.id}/start?reason=test`, '', 'width=640, height=640')
}
icon={<TestIcon />}
customClass='testAction'
>
{I18n.t('common.buttons.test')}
</ActionLink>
<ActionLink
onClick={() => {
setSelectedOAuth(oAuth.id);
setPage('edit');
}}
icon={<EditIcon />}
customClass='editAction'
>
{I18n.t('common.buttons.edit')}
</ActionLink>
<ActionLink
onClick={() => confirm(I18n.t('common.confirmation')) && handleDeleteOAuth(oAuth.id)}
icon={<DeleteIcon />}
customClass='deleteAction'
>
{I18n.t('common.buttons.delete')}
</ActionLink>
</div>
:
<div className="defaultOAuthDiv">
<span className="defaultOAuthLabel"><MutedText>{I18n.t('site_settings.authentication.default_oauth')}</MutedText></span>
</div>
}
</li>
);

View File

@@ -9,6 +9,7 @@ import OAuthProviderItem from './OAuthProviderItem';
interface Props {
oAuths: Array<IOAuth>;
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
handleToggleEnabledDefaultOAuth(id: number, enabled: boolean): void;
handleDeleteOAuth(id: number): void;
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
setSelectedOAuth: React.Dispatch<React.SetStateAction<number>>;
@@ -17,6 +18,7 @@ interface Props {
const OAuthProvidersList = ({
oAuths,
handleToggleEnabledOAuth,
handleToggleEnabledDefaultOAuth,
handleDeleteOAuth,
setPage,
setSelectedOAuth,
@@ -35,6 +37,7 @@ const OAuthProvidersList = ({
<OAuthProviderItem
oAuth={oAuth}
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
handleToggleEnabledDefaultOAuth={handleToggleEnabledDefaultOAuth}
handleDeleteOAuth={handleDeleteOAuth}
setPage={setPage}
setSelectedOAuth={setSelectedOAuth}

View File

@@ -45,6 +45,8 @@ export interface ITenantSignUpTenantForm {
subdomain: string;
}
export type AuthMethod = 'none' | 'email' | 'oauth';
const TenantSignUpP = ({
oAuths,
oAuthLoginCompleted,
@@ -58,9 +60,12 @@ const TenantSignUpP = ({
baseUrl,
authenticityToken
}: Props) => {
// authMethod is either 'none', 'email' or 'oauth'
const [authMethod, setAuthMethod] = useState<AuthMethod>(oAuthLoginCompleted ? 'oauth' : 'none');
const [userData, setUserData] = useState({
fullName: '',
email: '',
fullName: oAuthLoginCompleted ? oauthUserName : '',
email: oAuthLoginCompleted ? oauthUserEmail : '',
password: '',
passwordConfirmation: '',
});
@@ -72,20 +77,18 @@ const TenantSignUpP = ({
const [currentStep, setCurrentStep] = useState(oAuthLoginCompleted ? 2 : 1);
const [emailAuth, setEmailAuth] = useState(false);
const handleSignUpSubmit = (siteName: string, subdomain: string) => {
handleSubmit(
oAuthLoginCompleted ? oauthUserName : userData.fullName,
oAuthLoginCompleted ? oauthUserEmail : userData.email,
userData.fullName,
userData.email,
userData.password,
siteName,
subdomain,
oAuthLoginCompleted,
authMethod == 'oauth',
authenticityToken,
).then(res => {
if (res?.status !== HttpStatus.Created) return;
if (oAuthLoginCompleted) {
if (authMethod == 'oauth') {
let redirectUrl = new URL(baseUrl);
redirectUrl.hostname = `${subdomain}.${redirectUrl.hostname}`;
window.location.href = `${redirectUrl.toString()}users/sign_in`;
@@ -107,12 +110,9 @@ const TenantSignUpP = ({
<UserSignUpForm
currentStep={currentStep}
setCurrentStep={setCurrentStep}
emailAuth={emailAuth}
setEmailAuth={setEmailAuth}
authMethod={authMethod}
setAuthMethod={setAuthMethod}
oAuths={oAuths}
oAuthLoginCompleted={oAuthLoginCompleted}
oauthUserEmail={oauthUserEmail}
oauthUserName={oauthUserName}
userData={userData}
setUserData={setUserData}
/>

View File

@@ -5,7 +5,7 @@ import I18n from 'i18n-js';
import Box from '../common/Box';
import Button from '../common/Button';
import OAuthProviderLink from '../common/OAuthProviderLink';
import { ITenantSignUpUserForm } from './TenantSignUpP';
import { AuthMethod, ITenantSignUpUserForm } from './TenantSignUpP';
import { DangerText } from '../common/CustomTexts';
import { getLabel, getValidationMessage } from '../../helpers/formUtils';
import { EMAIL_REGEX } from '../../constants/regex';
@@ -16,12 +16,9 @@ import { BackIcon, EditIcon } from '../common/Icons';
interface Props {
currentStep: number;
setCurrentStep(step: number): void;
emailAuth: boolean;
setEmailAuth(enabled: boolean): void;
authMethod: AuthMethod;
setAuthMethod(method: AuthMethod): void;
oAuths: Array<IOAuth>;
oAuthLoginCompleted: boolean;
oauthUserEmail?: string;
oauthUserName?: string;
userData: ITenantSignUpUserForm;
setUserData({}: ITenantSignUpUserForm): void;
}
@@ -29,12 +26,9 @@ interface Props {
const UserSignUpForm = ({
currentStep,
setCurrentStep,
emailAuth,
setEmailAuth,
authMethod,
setAuthMethod,
oAuths,
oAuthLoginCompleted,
oauthUserEmail,
oauthUserName,
userData,
setUserData,
}: Props) => {
@@ -44,7 +38,15 @@ const UserSignUpForm = ({
setError,
getValues,
formState: { errors }
} = useForm<ITenantSignUpUserForm>();
} = useForm<ITenantSignUpUserForm>({
defaultValues: {
fullName: userData.fullName,
email: userData.email,
password: userData.password,
passwordConfirmation: userData.passwordConfirmation,
}
});
const onSubmit: SubmitHandler<ITenantSignUpUserForm> = data => {
if (data.password !== data.passwordConfirmation) {
setError('passwordConfirmation', I18n.t('common.validations.password_mismatch'));
@@ -60,36 +62,40 @@ const UserSignUpForm = ({
<h3>Create user account</h3>
{
currentStep === 1 && !emailAuth &&
currentStep === 1 && authMethod == 'none' &&
<>
<Button className="emailAuth" onClick={() => setEmailAuth(true)}>
<Button className="emailAuth" onClick={() => setAuthMethod('email')}>
Sign up with email
</Button>
{
oAuths.filter(oAuth => oAuth.isEnabled).map((oAuth, i) =>
<OAuthProviderLink
oAuthId={oAuth.id}
oAuthName={oAuth.name}
oAuthLogo={oAuth.logo}
oAuthReason='tenantsignup'
isSignUp
key={i}
/>
)
}
{ oAuths.length > 0 && <hr /> }
<div className="oauthProviderList">
{
oAuths.filter(oAuth => oAuth.isEnabled).map((oAuth, i) =>
<OAuthProviderLink
oAuthId={oAuth.id}
oAuthName={oAuth.name}
oAuthLogo={oAuth.logo}
oAuthReason='tenantsignup'
isSignUp
key={i}
/>
)
}
</div>
</>
}
{
currentStep === 1 && emailAuth &&
currentStep === 1 && (authMethod == 'email' || authMethod == 'oauth') &&
<form onSubmit={handleSubmit(onSubmit)}>
<ActionLink
onClick={() => setEmailAuth(false)}
onClick={() => setAuthMethod('none')}
icon={<BackIcon />}
customClass="backButton"
>
{I18n.t('common.buttons.back')}
Use another method
</ActionLink>
<div className="formRow">
@@ -106,6 +112,7 @@ const UserSignUpForm = ({
<div className="formRow">
<input
{...register('email', { required: true, pattern: EMAIL_REGEX })}
disabled={authMethod == 'oauth'}
type="email"
placeholder={getLabel('user', 'email')}
id="userEmail"
@@ -117,29 +124,32 @@ const UserSignUpForm = ({
</DangerText>
</div>
<div className="formRow">
<div className="userPasswordDiv">
<input
{...register('password', { required: true, minLength: 6, maxLength: 128 })}
type="password"
placeholder={getLabel('user', 'password')}
id="userPassword"
className="formControl"
/>
<DangerText>{ errors.password && I18n.t('common.validations.password', { n: 6 }) }</DangerText>
</div>
{
authMethod == 'email' &&
<div className="formRow">
<div className="userPasswordDiv">
<input
{...register('password', { required: true, minLength: 6, maxLength: 128 })}
type="password"
placeholder={getLabel('user', 'password')}
id="userPassword"
className="formControl"
/>
<DangerText>{ errors.password && I18n.t('common.validations.password', { n: 6 }) }</DangerText>
</div>
<div className="userPasswordConfirmationDiv">
<input
{...register('passwordConfirmation')}
type="password"
placeholder={getLabel('user', 'password_confirmation')}
id="userPasswordConfirmation"
className="formControl"
/>
<DangerText>{ errors.passwordConfirmation && I18n.t('common.validations.password_mismatch') }</DangerText>
<div className="userPasswordConfirmationDiv">
<input
{...register('passwordConfirmation')}
type="password"
placeholder={getLabel('user', 'password_confirmation')}
id="userPasswordConfirmation"
className="formControl"
/>
<DangerText>{ errors.passwordConfirmation && I18n.t('common.validations.password_mismatch') }</DangerText>
</div>
</div>
</div>
}
<Button
onClick={() => null}
@@ -151,9 +161,9 @@ const UserSignUpForm = ({
}
{
currentStep === 2 && !oAuthLoginCompleted &&
currentStep === 2 &&
<p className="userRecap">
<b>{oAuthLoginCompleted ? oauthUserName : userData.fullName}</b> ({oAuthLoginCompleted ? oauthUserEmail : userData.email})
<b>{userData.fullName}</b> ({userData.email})
<ActionLink onClick={() => setCurrentStep(currentStep-1)} icon={<EditIcon />} customClass="editUser">Edit</ActionLink>
</p>
}

View File

@@ -3,6 +3,7 @@ import I18n from 'i18n-js';
import { useState } from 'react';
import ActionLink from './ActionLink';
import { CopyIcon, DoneIcon } from './Icons';
import { SuccessText } from './CustomTexts';
interface Props {
label: string;
@@ -40,7 +41,7 @@ const CopyToClipboardButton = ({
</ActionLink>
:
<span style={{display: 'flex', marginRight: 12}}>
{copiedLabel}
<SuccessText>{copiedLabel}</SuccessText>
</span>
);
};