Skip to content

Commit

Permalink
feat: Implement new password verification flow front (#29322)
Browse files Browse the repository at this point in the history
  • Loading branch information
rique223 authored and hugocostadev committed Jun 26, 2023
1 parent 89d4638 commit 2db69a7
Show file tree
Hide file tree
Showing 16 changed files with 282 additions and 158 deletions.
4 changes: 3 additions & 1 deletion apps/meteor/app/api/server/v1/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { getUserInfo } from '../helpers/getUserInfo';
import { getPaginationItems } from '../helpers/getPaginationItems';
import { getUserFromParams } from '../helpers/getUserFromParams';
import { i18n } from '../../../../server/lib/i18n';
import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';

/**
* @openapi
Expand Down Expand Up @@ -392,7 +393,7 @@ API.v1.addRoute(
API.v1.addRoute(
'pw.getPolicy',
{
authRequired: true,
authRequired: false,
},
{
get() {
Expand All @@ -409,6 +410,7 @@ API.v1.addRoute(
},
{
async get() {
apiDeprecationLogger.endpoint(this.request.route, '7.0.0', this.response, ' Use pw.getPolicy instead.');
check(
this.queryParams,
Match.ObjectIncluding({
Expand Down
175 changes: 76 additions & 99 deletions apps/meteor/client/views/account/profile/AccountProfileForm.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { IUser } from '@rocket.chat/core-typings';
import {
Field,
FieldGroup,
TextInput,
TextAreaInput,
Box,
Icon,
AnimatedVisibility,
PasswordInput,
Button,
Grid,
Margins,
} from '@rocket.chat/fuselage';
import { Field, FieldGroup, TextInput, TextAreaInput, Box, Icon, PasswordInput, Button } from '@rocket.chat/fuselage';
import { useDebouncedCallback, useSafely } from '@rocket.chat/fuselage-hooks';
import { PasswordVerifier } from '@rocket.chat/ui-client';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
import { useVerifyPassword, useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
import type { Dispatch, ReactElement, SetStateAction } from 'react';
import React, { useCallback, useMemo, useEffect, useState } from 'react';

Expand Down Expand Up @@ -70,6 +59,8 @@ const AccountProfileForm = ({ values, handlers, user, settings, onSaveStateChang
const { realname, email, username, password, confirmationPassword, statusText, bio, statusType, customFields, nickname } =
values as AccountFormValues;

const passwordVerifications = useVerifyPassword(password);

const {
handleRealname,
handleEmail,
Expand Down Expand Up @@ -295,92 +286,78 @@ const AccountProfileForm = ({ values, handlers, user, settings, onSaveStateChang
),
[bio, handleBio, bioError, t],
)}
<Field>
<Grid>
<Grid.Item>
<FieldGroup display='flex' flexDirection='column' flexGrow={1} flexShrink={0}>
{useMemo(
() => (
<Field>
<Field.Label>{t('Email')}</Field.Label>
<Field.Row>
<TextInput
flexGrow={1}
value={email}
error={emailError}
onChange={handleEmail}
addon={<Icon name={verified ? 'circle-check' : 'mail'} size='x20' />}
disabled={!allowEmailChange}
/>
</Field.Row>
{!allowEmailChange && <Field.Hint>{t('Email_Change_Disabled')}</Field.Hint>}
<Field.Error>{t(emailError as TranslationKey)}</Field.Error>
</Field>
),
[t, email, handleEmail, verified, allowEmailChange, emailError],
)}
{useMemo(
() =>
!verified && (
<Field>
<Margins blockEnd='x28'>
<Button disabled={email !== previousEmail} onClick={handleSendConfirmationEmail}>
{t('Resend_verification_email')}
</Button>
</Margins>
</Field>
),
[verified, t, email, previousEmail, handleSendConfirmationEmail],
)}
</FieldGroup>
</Grid.Item>
<Grid.Item>
<FieldGroup display='flex' flexDirection='column' flexGrow={1} flexShrink={0}>
{useMemo(
() => (
<Field>
<Field.Label>{t('New_password')}</Field.Label>
<Field.Row>
<PasswordInput
autoComplete='off'
disabled={!allowPasswordChange}
error={showPasswordError ? passwordError : undefined}
flexGrow={1}
value={password}
onChange={handlePassword}
addon={<Icon name='key' size='x20' />}
/>
</Field.Row>
{!allowPasswordChange && <Field.Hint>{t('Password_Change_Disabled')}</Field.Hint>}
</Field>
),
[t, password, handlePassword, passwordError, allowPasswordChange, showPasswordError],
)}
{useMemo(
() => (
<Field>
<AnimatedVisibility visibility={password ? AnimatedVisibility.VISIBLE : AnimatedVisibility.HIDDEN}>
<Field.Label>{t('Confirm_password')}</Field.Label>
<Field.Row>
<PasswordInput
autoComplete='off'
error={showPasswordError ? passwordError : undefined}
flexGrow={1}
value={confirmationPassword}
onChange={handleConfirmationPassword}
addon={<Icon name='key' size='x20' />}
/>
</Field.Row>
{passwordError && <Field.Error>{showPasswordError ? passwordError : undefined}</Field.Error>}
</AnimatedVisibility>
</Field>
),
[t, confirmationPassword, handleConfirmationPassword, password, passwordError, showPasswordError],
{useMemo(
() => (
<Field>
<Field.Label>{t('Email')}</Field.Label>
<Field.Row display='flex' flexDirection='row' justifyContent='space-between'>
<TextInput
flexGrow={1}
value={email}
error={emailError}
onChange={handleEmail}
addon={<Icon name={verified ? 'circle-check' : 'mail'} size='x20' />}
disabled={!allowEmailChange}
/>
{!verified && (
<Button disabled={email !== previousEmail} onClick={handleSendConfirmationEmail} mis='x24'>
{t('Resend_verification_email')}
</Button>
)}
</FieldGroup>
</Grid.Item>
</Grid>
</Field>
</Field.Row>
{!allowEmailChange && <Field.Hint>{t('Email_Change_Disabled')}</Field.Hint>}
<Field.Error>{t(emailError as TranslationKey)}</Field.Error>
</Field>
),
[t, email, emailError, handleEmail, verified, allowEmailChange, previousEmail, handleSendConfirmationEmail],
)}
{useMemo(
() => (
<Field>
<Field.Label>{t('New_password')}</Field.Label>
<Field.Row mbe='x4'>
<PasswordInput
autoComplete='off'
disabled={!allowPasswordChange}
error={showPasswordError ? passwordError : undefined}
flexGrow={1}
value={password}
onChange={handlePassword}
addon={<Icon name='key' size='x20' />}
placeholder={t('Create_a_password')}
/>
</Field.Row>
<Field.Row mbs='x4'>
<PasswordInput
autoComplete='off'
error={showPasswordError ? passwordError : undefined}
flexGrow={1}
value={confirmationPassword}
onChange={handleConfirmationPassword}
addon={<Icon name='key' size='x20' />}
placeholder={t('Confirm_password')}
disabled={!allowPasswordChange}
/>
</Field.Row>
{!allowPasswordChange && <Field.Hint>{t('Password_Change_Disabled')}</Field.Hint>}
{passwordError && <Field.Error>{showPasswordError ? passwordError : undefined}</Field.Error>}
{passwordVerifications && allowPasswordChange && (
<PasswordVerifier password={password} passwordVerifications={passwordVerifications} />
)}
</Field>
),
[
t,
allowPasswordChange,
showPasswordError,
passwordError,
password,
handlePassword,
confirmationPassword,
handleConfirmationPassword,
passwordVerifications,
],
)}
<CustomFieldsForm jsonCustomFields={undefined} customFieldsData={customFields} setCustomFieldsData={handleCustomFields} />
</FieldGroup>
);
Expand Down
13 changes: 11 additions & 2 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1073,7 +1073,8 @@
"Confirm_new_encryption_password": "Confirm new encryption password",
"Confirm_new_password": "Confirm New Password",
"Confirm_New_Password_Placeholder": "Please re-enter new password...",
"Confirm_password": "Confirm your password",
"Confirm_password": "Confirm password",
"Confirm_your_password": "Confirm your password",
"Confirmation": "Confirmation",
"Configure_video_conference": "Configure conference call",
"Connect": "Connect",
Expand Down Expand Up @@ -2391,6 +2392,13 @@
"get-password-policy-mustContainAtLeastOneNumber": "The password should contain at least one number",
"get-password-policy-mustContainAtLeastOneSpecialCharacter": "The password should contain at least one special character",
"get-password-policy-mustContainAtLeastOneUppercase": "The password should contain at least one uppercase letter",
"get-password-policy-minLength-label": "At least {{limit}} characters",
"get-password-policy-maxLength-label": "At most {{limit}} characters",
"get-password-policy-forbidRepeatingCharactersCount-label": "Max. {{limit}} repeating characters",
"get-password-policy-mustContainAtLeastOneLowercase-label": "At least one lowercase letter",
"get-password-policy-mustContainAtLeastOneUppercase-label": "At least one uppercase letter",
"get-password-policy-mustContainAtLeastOneNumber-label": "At least one number",
"get-password-policy-mustContainAtLeastOneSpecialCharacter-label": "At least one symbol",
"get-server-info": "Get Server Info",
"get-server-info_description": "Permission to get server info",
"github_no_public_email": "You don't have any email as public email in your GitHub account",
Expand Down Expand Up @@ -3839,6 +3847,7 @@
"Password_History": "Password History",
"Password_History_Amount": "Password History Length",
"Password_History_Amount_Description": "Amount of most recently used passwords to prevent users from reusing.",
"Password_must_have": "Password must have:",
"Password_Policy": "Password Policy",
"Password_to_access": "Password to access",
"Passwords_do_not_match": "Passwords do not match",
Expand Down Expand Up @@ -5089,7 +5098,6 @@
"Type_your_job_title": "Type your job title",
"Type_your_message": "Type your message",
"Type_your_name": "Type your name",
"Type_your_new_password": "Type your new password",
"Type_your_password": "Type your password",
"Type_your_username": "Type your username",
"UI_Allow_room_names_with_special_chars": "Allow Special Characters in Room Names",
Expand Down Expand Up @@ -5806,6 +5814,7 @@
"Community_Private_apps_limit_exceeded": "Community edition app limit has been exceeded.",
"Theme_match_system": "Match system",
"Join_your_team": "Join your team",
"Create_a_password": "Create a password",
"Create_an_account": "Create an account",
"Get_all_apps": "Get all the apps your team needs",
"Workspaces_on_community_edition_trial_on": "Workspaces on Community Edition can have up to 5 marketplace apps and 3 private apps enabled. Start a free Enterprise trial to remove these limits today!",
Expand Down
12 changes: 0 additions & 12 deletions apps/meteor/tests/end-to-end/api/00-miscellaneous.js
Original file line number Diff line number Diff line change
Expand Up @@ -675,18 +675,6 @@ describe('miscellaneous', function () {
});

describe('/pw.getPolicy', () => {
it('should fail if not logged in', (done) => {
request
.get(api('pw.getPolicy'))
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
})
.end(done);
});

it('should return policies', (done) => {
request
.get(api('pw.getPolicy'))
Expand Down
2 changes: 1 addition & 1 deletion packages/rest-typings/src/v1/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export type MiscEndpoints = {
'/v1/pw.getPolicy': {
GET: () => {
enabled: boolean;
policy: [name: string, options?: Record<string, unknown>][];
policy: [name: string, value?: Record<string, number>][];
};
};

Expand Down
56 changes: 56 additions & 0 deletions packages/ui-client/src/components/PasswordVerifier.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Box, Icon } from '@rocket.chat/fuselage';
import type { useVerifyPassword } from '@rocket.chat/ui-contexts';
import { useTranslation } from 'react-i18next';

type PasswordVerifierProps = {
password: string;
passwordVerifications: ReturnType<typeof useVerifyPassword>;
};

export const PasswordVerifier = ({ password, passwordVerifications }: PasswordVerifierProps) => {
const { t } = useTranslation();

const handleRenderPasswordVerification = (passwordVerifications: ReturnType<typeof useVerifyPassword>) => {
const verifications = [];

if (!passwordVerifications) return null;

for (const verification in passwordVerifications) {
if (passwordVerifications[verification]) {
const { isValid, limit } = passwordVerifications[verification];
verifications.push(
<Box
display='flex'
flexBasis='50%'
alignItems='center'
mbe='x8'
fontScale='c1'
key={verification}
color={isValid && password.length !== 0 ? 'status-font-on-success' : 'status-font-on-danger'}
>
<Icon
name={isValid && password.length !== 0 ? 'success-circle' : 'error-circle'}
color={isValid && password.length !== 0 ? 'status-font-on-success' : 'status-font-on-danger'}
size='x16'
mie='x4'
/>
{t(`${verification}-label`, { limit })}
</Box>,
);
}
}

return verifications;
};

return (
<Box display='flex' flexDirection='column' mbs='x8'>
<Box mbe='x8' fontScale='c2'>
{t('Password_must_have')}
</Box>
<Box display='flex' flexWrap='wrap'>
{handleRenderPasswordVerification(passwordVerifications)}
</Box>
</Box>
);
};
1 change: 1 addition & 0 deletions packages/ui-client/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './EmojiPicker';
export * from './ExternalLink';
export * from './DotLeader';
export * from './PasswordVerifier';
export { default as TextSeparator } from './TextSeparator';
export * from './TooltipComponent';
export * as UserStatus from './UserStatus';
Expand Down
9 changes: 9 additions & 0 deletions packages/ui-contexts/src/hooks/usePasswordPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useQuery } from '@tanstack/react-query';

import { useEndpoint } from './useEndpoint';

export const usePasswordPolicy = () => {
const getPasswordPolicy = useEndpoint('GET', '/v1/pw.getPolicy');

return useQuery(['login', 'password-policy'], async () => getPasswordPolicy());
};
Loading

0 comments on commit 2db69a7

Please sign in to comment.