Skip to content

Commit

Permalink
Update credentials view
Browse files Browse the repository at this point in the history
  • Loading branch information
peterMuriuki committed Feb 6, 2025
1 parent d06bc71 commit b8ca521
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { Dictionary } from '@onaio/utils';
import { sendSuccessNotification, sendErrorNotification } from '@opensrp/notifications';
import type { TFunction } from '@opensrp/i18n';
import { PasswordStrengthMeter } from './passwordStrengthMeter';

reducerRegistry.register(keycloakUsersReducerName, keycloakUsersReducer);

Expand Down Expand Up @@ -129,7 +130,8 @@ const UserCredentials: React.FC<CredentialsPropsTypes> = (props: CredentialsProp
},
};
const history = useHistory();
const heading = `${t('User Credentials')} | ${username}`;
const heading = t(`Reset password | {{username}}`, { username });

const headerProps = {
pageHeaderProps: {
title: heading,
Expand All @@ -148,7 +150,7 @@ const UserCredentials: React.FC<CredentialsPropsTypes> = (props: CredentialsProp
submitForm(values, userId, serviceClass, keycloakBaseURL, t)
}
>
<CredentialsFieldsRender />
<CredentialsFieldsRender isReset={true} />
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit" className="reset-password">
{t('Set password')}
Expand All @@ -169,13 +171,18 @@ UserCredentials.defaultProps = defaultCredentialsProps;

export { UserCredentials };

export const CredentialsFieldsRender = () => {
export interface CredentialsFieldsRenderProps {
isReset: boolean;
}

export const CredentialsFieldsRender = (props: CredentialsFieldsRenderProps) => {
const { isReset } = props;
const { t } = useTranslation();
return (
<>
<Form.Item
name={passwordField}
label={t('Password')}
label={isReset ? t('New Password') : t('Password')}
rules={[
{
required: true,
Expand All @@ -184,7 +191,7 @@ export const CredentialsFieldsRender = () => {
]}
hasFeedback
>
<Input.Password />
<PasswordStrengthMeter />
</Form.Item>

<Form.Item
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Input, Progress } from 'antd';
import { InputProps } from 'antd/lib/input';
import React, { useState } from 'react';

// 00-24 poor password
// 25-49 weak password
// 50-74 good password
// 75-100 strong password
// entropy source: https://www.baeldung.com/cs/password-entropy
export const calculateEntropy = (password: string) => {
let charSetSize = 0;
if (/[a-z]/.test(password)) charSetSize += 26;
if (/[A-Z]/.test(password)) charSetSize += 26;
if (/[0-9]/.test(password)) charSetSize += 10;
if (/\W|_/.test(password)) charSetSize += 32;
return password.length > 0 ? Math.log2(charSetSize ** password.length) : 0;
};

export const PasswordStrengthMeter = (props: InputProps) => {
const [strength, setStrength] = useState(0);

const getStrengthLevel = (entropy: number) => {
if (entropy < 25) return 'poor';
if (entropy < 50) return 'weak';
if (entropy < 75) return 'good';
return 'strong';
};

const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newPassword = e.target.value;
setStrength(calculateEntropy(newPassword));
props.onChange?.(e);
};

const strengthLevel = getStrengthLevel(strength);
const strengthPercent = Math.min((strength / 100) * 100, 100);
const inputStrokeColor =
strengthLevel === 'poor'
? '#ff4d4f'
: strengthLevel === 'weak'
? '#faad14'
: strengthLevel === 'good'
? '#52c41a'
: '#1890ff';

return (
<div>
<Input.Password {...props} onChange={handlePasswordChange} />
<Progress
data-testid={`level-${strengthLevel}`}
percent={strengthPercent}
status={strengthLevel === 'poor' ? 'exception' : 'active'}
strokeColor={inputStrokeColor}
showInfo={false}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { PasswordStrengthMeter } from '../passwordStrengthMeter';

const props = {
placeholder: 'Enter your password',
};

describe('PasswordStrengthMeter', () => {
test('renders input field and progress bar', () => {
render(<PasswordStrengthMeter {...props} />);
expect(screen.getByPlaceholderText('Enter your password')).toBeInTheDocument();
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});

test('updates progress bar when password is typed', () => {
render(<PasswordStrengthMeter {...props} />);
const input = screen.getByPlaceholderText('Enter your password');

fireEvent.change(input, { target: { value: 'password123' } });
// confirm test id has the correct value.
screen.getByTestId('level-good');
});

test('changes progress bar color based on password strength', () => {
render(<PasswordStrengthMeter {...props} />);
const input = screen.getByPlaceholderText('Enter your password');

fireEvent.change(input, { target: { value: 'weak' } });
screen.getByTestId('level-poor');

fireEvent.change(input, { target: { value: 'Stronger1!' } });
screen.getByTestId('level-good');
});

test('calls onChange when password is entered', () => {
const handleChange = jest.fn();
render(<PasswordStrengthMeter {...props} onChange={handleChange} />);
const input = screen.getByPlaceholderText('Enter your password');

fireEvent.change(input, { target: { value: 'testing123' } });
expect(handleChange).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const TableActions = (props: Props): JSX.Element => {
data-testid="credentials"
onClick={() => history.push(`${URL_USER_CREDENTIALS}/${record.id}/${record.username}`)}
>
{t('Credentials')}
{t('Reset Password')}
</Button>
),
},
Expand Down

0 comments on commit b8ca521

Please sign in to comment.