Skip to content

Commit

Permalink
feat(clerk-js,localizations,types): Make fname, lname & username read…
Browse files Browse the repository at this point in the history
…only if user has SAML accounts
  • Loading branch information
Mark Pitsilos authored and yourtallness committed Jun 8, 2023
1 parent 5871146 commit 6018fb1
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 5 deletions.
7 changes: 7 additions & 0 deletions .changeset/old-stingrays-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/localizations': patch
'@clerk/clerk-js': patch
'@clerk/types': patch
---

Make first name, last name & password readonly for users with active SAML accounts
38 changes: 35 additions & 3 deletions packages/clerk-js/src/ui/components/UserProfile/PasswordPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@ import { useRef } from 'react';
import { useWizard, Wizard } from '../../common';
import { useCoreSession, useCoreUser, useEnvironment } from '../../contexts';
import { localizationKeys } from '../../customizables';
import { ContentPage, Form, FormButtons, SuccessPage, useCardState, withCardStateProvider } from '../../elements';
import {
ContentPage,
Form,
FormButtonContainer,
FormButtons,
InformationBox,
SuccessPage,
useCardState,
useNavigateToFlowStart,
withCardStateProvider,
} from '../../elements';
import { useConfirmPassword, usePasswordComplexity } from '../../hooks';
import { handleError, useFormControl } from '../../utils';
import { UserProfileBreadcrumbs } from './UserProfileNavbar';
Expand Down Expand Up @@ -32,6 +42,9 @@ export const PasswordPage = withCardStateProvider(() => {
: localizationKeys('userProfile.passwordPage.title');
const card = useCardState();
const wizard = useWizard();
const { navigateToFlowStart } = useNavigateToFlowStart();

const canEditPassword = user.samlAccounts.length == 0;

// Ensure that messages will not use the updated state of User after a password has been set or changed
const successPagePropsRef = useRef<Parameters<typeof SuccessPage>[0]>({
Expand All @@ -58,6 +71,7 @@ export const PasswordPage = withCardStateProvider(() => {
strengthMeter: true,
informationText: failedValidationsText,
});

const confirmField = useFormControl('confirmPassword', '', {
type: 'password',
label: localizationKeys('formFieldLabel__confirmPassword'),
Expand Down Expand Up @@ -113,6 +127,8 @@ export const PasswordPage = withCardStateProvider(() => {
headerTitle={title}
Breadcrumbs={UserProfileBreadcrumbs}
>
{!canEditPassword && <InformationBox message={localizationKeys('userProfile.passwordPage.readonly')} />}

<Form.Root
onSubmit={updatePassword}
onBlur={validateForm}
Expand All @@ -132,6 +148,7 @@ export const PasswordPage = withCardStateProvider(() => {
minLength={6}
required
autoFocus
isDisabled={!canEditPassword}
/>
</Form.ControlRow>
)}
Expand All @@ -141,6 +158,7 @@ export const PasswordPage = withCardStateProvider(() => {
minLength={6}
required
autoFocus={!user.passwordEnabled}
isDisabled={!canEditPassword}
/>
</Form.ControlRow>
<Form.ControlRow elementId={confirmField.id}>
Expand All @@ -150,12 +168,26 @@ export const PasswordPage = withCardStateProvider(() => {
displayConfirmPasswordFeedback(e.target.value);
return confirmField.props.onChange(e);
}}
isDisabled={!canEditPassword}
/>
</Form.ControlRow>
<Form.ControlRow elementId={sessionsField.id}>
<Form.Control {...sessionsField.props} />
<Form.Control
{...sessionsField.props}
isDisabled={!canEditPassword}
/>
</Form.ControlRow>
<FormButtons isDisabled={!canSubmit} />
{canEditPassword ? (
<FormButtons isDisabled={!canSubmit} />
) : (
<FormButtonContainer>
<Form.ResetButton
localizationKey={localizationKeys('userProfile.formButtonReset')}
block={false}
onClick={navigateToFlowStart}
/>
</FormButtonContainer>
)}
</Form.Root>
</ContentPage>

Expand Down
16 changes: 15 additions & 1 deletion packages/clerk-js/src/ui/components/UserProfile/ProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import { isDefaultImage } from '../../../utils';
import { useWizard, Wizard } from '../../common';
import { useCoreUser, useEnvironment } from '../../contexts';
import { localizationKeys } from '../../customizables';
import { ContentPage, Form, FormButtons, SuccessPage, useCardState, withCardStateProvider } from '../../elements';
import {
ContentPage,
Form,
FormButtons,
InformationBox,
SuccessPage,
useCardState,
withCardStateProvider,
} from '../../elements';
import { handleError, useFormControl } from '../../utils';
import { UserProfileAvatarUploader } from './UserProfileAvatarUploader';
import { UserProfileBreadcrumbs } from './UserProfileNavbar';
Expand Down Expand Up @@ -41,6 +49,8 @@ export const ProfilePage = withCardStateProvider(() => {
const requiredFieldsFilled =
hasRequiredFields && !!lastNameField.value && !!firstNameField.value && optionalFieldsChanged;

const canEditName = user.samlAccounts.length == 0;

const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();

Expand Down Expand Up @@ -83,6 +93,8 @@ export const ProfilePage = withCardStateProvider(() => {
headerTitle={title}
Breadcrumbs={UserProfileBreadcrumbs}
>
{!canEditName && <InformationBox message={localizationKeys('userProfile.profilePage.readonly')} />}

<Form.Root onSubmit={onSubmit}>
<UserProfileAvatarUploader
user={user}
Expand All @@ -95,6 +107,7 @@ export const ProfilePage = withCardStateProvider(() => {
autoFocus
{...firstNameField.props}
required={first_name.required}
isDisabled={!canEditName}
/>
</Form.ControlRow>
)}
Expand All @@ -103,6 +116,7 @@ export const ProfilePage = withCardStateProvider(() => {
<Form.Control
{...lastNameField.props}
required={last_name.required}
isDisabled={!canEditName}
/>
</Form.ControlRow>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,72 @@ describe('PasswordPage', () => {
expect(identifierField.value).toBe(identifier);
});

describe('with SAML', () => {
it('prevents adding a password if user has enterprise connections', async () => {
const emailAddress = 'george@jungle.com';

const config = createFixtures.config(f => {
f.withEmailAddress();
f.withSaml();
f.withUser({
email_addresses: [emailAddress],
saml_accounts: [
{
id: 'samlacc_foo',
provider: 'saml_okta',
email_address: emailAddress,
},
],
});
});

const { wrapper } = await createFixtures(config);

render(<PasswordPage />, { wrapper });

expect(screen.getByLabelText(/new password/i)).toBeDisabled();
expect(screen.getByLabelText(/confirm password/i)).toBeDisabled();
expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).toBeDisabled();

screen.getByText(
'Your password can currently not be edited because you can sign in only via the enterprise connection.',
);
});

it('prevents changing a password if user has enterprise connections', async () => {
const emailAddress = 'george@jungle.com';

const config = createFixtures.config(f => {
f.withEmailAddress();
f.withSaml();
f.withUser({
password_enabled: true,
email_addresses: [emailAddress],
saml_accounts: [
{
id: 'samlacc_foo',
provider: 'saml_okta',
email_address: emailAddress,
},
],
});
});

const { wrapper } = await createFixtures(config);

render(<PasswordPage />, { wrapper });

expect(screen.getByLabelText(/current password/i)).toBeDisabled();
expect(screen.getByLabelText(/new password/i)).toBeDisabled();
expect(screen.getByLabelText(/confirm password/i)).toBeDisabled();
expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).toBeDisabled();

screen.getByText(
'Your password can currently not be edited because you can sign in only via the enterprise connection.',
);
});
});

describe('Actions', () => {
it('calls the appropriate function upon pressing continue and finish', async () => {
const { wrapper, fixtures } = await createFixtures(initConfig);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,40 @@ describe('ProfilePage', () => {
});
});

describe('with SAML', () => {
it('disables the first & last name inputs if user has enterprise connections', async () => {
const emailAddress = 'george@jungle.com';
const firstName = 'George';
const lastName = 'Clerk';

const config = createFixtures.config(f => {
f.withEmailAddress();
f.withSaml();
f.withName();
f.withUser({
first_name: firstName,
last_name: lastName,
email_addresses: [emailAddress],
saml_accounts: [
{
id: 'samlacc_foo',
provider: 'saml_okta',
email_address: emailAddress,
},
],
});
});

const { wrapper } = await createFixtures(config);

render(<ProfilePage />, { wrapper });

expect(screen.getByRole('textbox', { name: 'First name' })).toBeDisabled();
expect(screen.getByRole('textbox', { name: 'Last name' })).toBeDisabled();
screen.getByText('Your profile information has been provided by the enterprise connection and cannot be edited.');
});
});

describe('Profile image', () => {
it('shows the image', async () => {
const { wrapper } = await createFixtures(f => {
Expand Down
29 changes: 29 additions & 0 deletions packages/clerk-js/src/ui/elements/InformationBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { LocalizationKey } from '../customizables';
import { Flex, Icon, Text } from '../customizables';
import { InformationCircle } from '../icons';

type InformationBoxProps = {
message: LocalizationKey | string;
};

export function InformationBox(props: InformationBoxProps) {
return (
<Flex
sx={t => ({
gap: t.space.$2,
padding: `${t.space.$3} ${t.space.$4}`,
backgroundColor: t.colors.$blackAlpha50,
borderRadius: t.radii.$md,
})}
>
<Icon
icon={InformationCircle}
sx={t => ({ opacity: t.opacity.$disabled })}
/>
<Text
localizationKey={props.message}
sx={t => ({ color: t.colors.$blackAlpha700 })}
/>
</Flex>
);
}
1 change: 1 addition & 0 deletions packages/clerk-js/src/ui/elements/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,5 @@ export * from './IconCircle';
export * from './Popover';
export * from './Section';
export * from './PreviewButton';
export * from './InformationBox';
export * from './withAvatarShimmer';
1 change: 1 addition & 0 deletions packages/clerk-js/src/ui/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { default as CheckCircle } from './check-circle.svg';
export { default as ExclamationTriangle } from './exclamation-triangle.svg';
export { default as ExclamationCircle } from './exclamation-circle.svg';
export { default as EyeSlash } from './eye-slash.svg';
export { default as InformationCircle } from './information-circle.svg';
export { default as MagnifyingGlass } from './magnifying-glass.svg';
export { default as Caret } from './caret.svg';
export { default as PencilEdit } from './pencil-edit.svg';
Expand Down
5 changes: 5 additions & 0 deletions packages/clerk-js/src/ui/icons/information-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const enUS: LocalizationResource = {
formFieldLabel__phoneNumber_username: 'phone number or username',
formFieldLabel__emailAddress_phoneNumber_username: 'Email address, phone number or username',
formFieldLabel__password: 'Password',
formFieldLabel__currentPassword: 'Current Password',
formFieldLabel__currentPassword: 'Current password',
formFieldLabel__newPassword: 'New password',
formFieldLabel__confirmPassword: 'Confirm password',
formFieldLabel__signOutOfOtherSessions: 'Sign out of all other devices',
Expand Down Expand Up @@ -363,6 +363,7 @@ export const enUS: LocalizationResource = {
fileDropAreaTitle: 'Drag file here, or...',
fileDropAreaAction: 'Select file',
fileDropAreaHint: 'Upload a JPG, PNG, GIF, or WEBP image smaller than 10 MB',
readonly: 'Your profile information has been provided by the enterprise connection and cannot be edited.',
successMessage: 'Your profile has been updated.',
},
usernamePage: {
Expand Down Expand Up @@ -433,6 +434,7 @@ export const enUS: LocalizationResource = {
passwordPage: {
title: 'Set password',
changePasswordTitle: 'Change password',
readonly: 'Your password can currently not be edited because you can sign in only via the enterprise connection.',
successMessage: 'Your password has been set.',
changePasswordSuccessMessage: 'Your password has been updated.',
sessionsSignedOutSuccessMessage: 'All other devices have been signed out.',
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ type _LocalizationResource = {
fileDropAreaTitle: LocalizationValue;
fileDropAreaAction: LocalizationValue;
fileDropAreaHint: LocalizationValue;
readonly: LocalizationValue;
successMessage: LocalizationValue;
};
usernamePage: {
Expand Down Expand Up @@ -462,6 +463,7 @@ type _LocalizationResource = {
};
passwordPage: {
title: LocalizationValue;
readonly: LocalizationValue;
successMessage: LocalizationValue;
changePasswordTitle: LocalizationValue;
changePasswordSuccessMessage: LocalizationValue;
Expand Down

0 comments on commit 6018fb1

Please sign in to comment.