Skip to content

Commit

Permalink
[DEV-997] Change password from user profile (#487)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremygordillo authored Jan 2, 2024
1 parent 0de4416 commit c0fbcba
Show file tree
Hide file tree
Showing 8 changed files with 428 additions and 37 deletions.
114 changes: 95 additions & 19 deletions apps/nextjs-website/src/app/profile/personal-data/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
'use client';
import { Stack, Typography } from '@mui/material';
import { Box, Divider, Stack, Typography } from '@mui/material';
import { Auth } from 'aws-amplify';
import { useTranslations } from 'next-intl';
import { useUser } from '@/helpers/user.helper';
import { InfoCardItemProps } from '@/components/atoms/InfoCardItem/InfoCardItem';
import { InfoCard } from '@/components/molecules/InfoCard/InfoCard';
import DeleteSection from '@/components/molecules/DeleteSection/DeleteSection';
import React, { useState } from 'react';

import { translations } from '@/_contents/translations';
import {
InfoCardItem,
InfoCardItemProps,
} from '@/components/atoms/InfoCardItem/InfoCardItem';
import DeleteSection from '@/components/molecules/DeleteSection/DeleteSection';
import { InfoCard } from '@/components/molecules/InfoCard/InfoCard';
import { ProfileInfoCard } from '@/components/organisms/Auth/ProfileInfoCard';
import { useUser } from '@/helpers/user.helper';
import { useRouter } from 'next/navigation';
import ConfirmationModal from '@/components/atoms/ConfirmationModal/ConfirmationModal';
import PasswordFormWrapper from '@/components/organisms/Auth/PasswordFormWrapper';

const PersonalData = () => {
const {
Expand All @@ -14,22 +24,37 @@ const PersonalData = () => {
},
} = translations;
const t = useTranslations('profile');
const router = useRouter();
const { user } = useUser();
const [editItem, setEditItem] = useState<InfoCardItemProps | null>(null);
const [showModal, setShowModal] = useState(false);

async function handleChangePassword(
oldPassword: string,
newPassword: string
) {
await Auth.changePassword(user, oldPassword, newPassword);
setShowModal(true);
}

const dataSectionItems: InfoCardItemProps[] = [
{
name: 'name',
title: t('personalData.fields.name'),
value: user?.attributes.given_name,
},
{
name: 'surname',
title: t('personalData.fields.surname'),
value: user?.attributes.family_name,
},
{
name: 'role',
title: t('personalData.fields.role'),
value: user?.attributes['custom:job_role'],
},
{
name: 'sector',
title: t('personalData.fields.sector'),
value: companyRoles.find(
(role) => role.value === user?.attributes['custom:company_type']
Expand All @@ -39,31 +64,82 @@ const PersonalData = () => {

const accountSectionItems: InfoCardItemProps[] = [
{
name: 'email',
title: t('personalData.fields.email'),
value: user?.attributes.email,
},
{
name: 'password',
title: t('personalData.fields.password'),
value: '••••••••••••',
editable: true,
},
];

const renderItem = (
item: InfoCardItemProps,
index: number,
items: InfoCardItemProps[]
) => {
const isPasswordItem: boolean = item.name === 'password';
const isEmailItem: boolean = item.name === 'email';
const showDivider = index !== items.length - 1;

const isEditing = editItem?.name === item.name;

return (
<Box key={index}>
{isPasswordItem && (
<PasswordFormWrapper
item={item}
isEditing={isEditing}
onCancel={() => setEditItem(null)}
onSave={handleChangePassword}
onEdit={() => setEditItem(item)}
/>
)}
{isEmailItem && (
<InfoCardItem {...item} onEdit={() => setEditItem(item)} />
)}
{showDivider && <Divider />}
</Box>
);
};

return (
<Stack
gap={5}
sx={{ padding: { xs: '40px 24px', md: '80px 40px' }, width: '100%' }}
>
<Typography variant='h4'>{t('personalData.title')}</Typography>
<InfoCard
cardTitle={t('personalData.dataSection')}
items={dataSectionItems}
/>
<InfoCard
cardTitle={t('personalData.accountSection')}
items={accountSectionItems}
<>
<ConfirmationModal
setOpen={() => null}
open={showModal}
title={t('changePassword.dialog.title')}
text={t('changePassword.dialog.text')}
confirmCta={{
label: t('changePassword.dialog.confirmLabel'),
onClick: () => {
Auth.signOut().then(() => {
router.replace('/auth/login');
});
return null;
},
}}
/>
<DeleteSection user={user} />
</Stack>
<Stack
gap={5}
sx={{ padding: { xs: '40px 24px', md: '80px 40px' }, width: '100%' }}
>
<Typography variant='h4'>{t('personalData.title')}</Typography>
<InfoCard
cardTitle={t('personalData.dataSection')}
items={dataSectionItems}
/>
<ProfileInfoCard
cardTitle={t('personalData.accountSection')}
items={accountSectionItems}
renderItem={renderItem}
/>
<DeleteSection user={user} />
</Stack>
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { useTranslations } from 'next-intl';
import EditIcon from '@mui/icons-material/Edit';
import { ButtonNaked } from '@pagopa/mui-italia';

const InfoCardEditButton = ({ onClick }: { onClick?: () => null }) => {
type InforCardEditButtonProps = {
// eslint-disable-next-line functional/no-return-void
onClick?: () => void;
};

const InfoCardEditButton = ({ onClick }: InforCardEditButtonProps) => {
const t = useTranslations('shared');

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,60 @@
'use client';

import { Stack, Typography } from '@mui/material';
import { ReactNode } from 'react';
import { ReactNode, useCallback } from 'react';
import InfoCardEditButton from '../InfoCardEditButton/InfoCardEditButton';

export type InfoCardItemProps = {
name: string;
editable?: boolean;
title: string;
value?: string;
valueFallback?: ReactNode;
// eslint-disable-next-line functional/no-return-void
onEdit?: () => void;
};

export const InfoCardItem = ({
editable = false,
title,
value,
valueFallback,
onEdit,
}: InfoCardItemProps) => {
const handleClick = useCallback(() => {
if (onEdit) {
onEdit();
}
}, [onEdit]);

const editButton = editable ? (
<InfoCardEditButton onClick={handleClick} />
) : null;

const valueComponent = value ? (
<Typography minHeight='24px' fontSize={16} flexGrow={1} fontWeight={700}>
{value}
</Typography>
) : (
valueFallback
);

return (
<Stack my={{ xs: 1, md: 3 }} flexDirection={{ xs: 'column', md: 'row' }}>
<Stack
my={{ xs: 1, md: 3 }}
flexDirection={{ xs: 'column', md: 'row' }}
alignItems={{ xs: 'flex-start', md: 'center' }}
gap={2}
>
<Typography
variant='body2'
fontSize={16}
minWidth={{ xs: 'auto', md: '200px' }}
minWidth={{ xs: 'auto', md: '170px' }}
>
{title}
</Typography>
{value ? (
<Typography
minHeight={'24px'}
fontSize={16}
flexGrow={1}
fontWeight={700}
>
{value}
</Typography>
) : (
valueFallback
)}
{valueComponent}
{editButton}
</Stack>
);
};
136 changes: 136 additions & 0 deletions apps/nextjs-website/src/components/organisms/Auth/EditPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { ButtonNaked } from '@pagopa/mui-italia';
import { Stack, Typography } from '@mui/material';
import { useCallback, useState } from 'react';

import { PasswordTextField } from './PasswordTextField';
import { useTranslations } from 'next-intl';
import { passwordMatcher } from '@/helpers/auth.helpers';

type EditPasswordFormProps = {
onSave: (oldPassword: string, newPassword: string) => Promise<void>;
// eslint-disable-next-line functional/no-return-void
onCancel: () => void;
};

type Passwords = {
current_password: string;
new_password: string;
password_confirm: string;
};

export const EditPasswordForm = ({
onSave,
onCancel,
}: EditPasswordFormProps) => {
const t = useTranslations('profile');
const [errors, setErrors] = useState<Partial<Passwords>>({});
const [passwords, setPasswords] = useState<Passwords>({
current_password: '',
new_password: '',
password_confirm: '',
});

const validateForm = useCallback(() => {
const { current_password, new_password, password_confirm } = passwords;
// eslint-disable-next-line functional/no-let
let err = {};

if (!current_password) {
err = { current_password: t('changePassword.requiredCurrentPassword') };
}

if (!passwordMatcher.test(new_password)) {
err = { ...err, new_password: t('changePassword.passwordPolicy') };
} else if (new_password !== password_confirm) {
err = { ...err, new_password: t('changePassword.passwordsNotMatch') };
}

setErrors(err);
const hasErrors = Object.keys(err).length > 0;
return !hasErrors;
}, [passwords, t]);

const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const {
target: { value, name },
} = event;

setPasswords((prev) => ({ ...prev, [name]: value }));
};

const handleSave = () => {
if (!validateForm()) return;
onSave(passwords.current_password, passwords.new_password).catch(
(error) => {
if (error.code === 'NotAuthorizedException') {
setErrors({ current_password: t('changePassword.wrongPassword') });
} else {
console.error(error);
}
}
);
};

const actions = (
<>
<ButtonNaked
color='primary'
sx={{ paddingLeft: 0, paddingRight: 0 }}
onClick={onCancel}
>
{t('changePassword.cancel')}
</ButtonNaked>
<ButtonNaked variant='contained' color='primary' onClick={handleSave}>
{t('changePassword.save')}
</ButtonNaked>
</>
);

return (
<Stack gap={3}>
<Stack
alignItems={{ xs: 'flex-start', md: 'center' }}
flexDirection={{ xs: 'column', md: 'row' }}
gap={2}
mt={{ xs: 1, md: 3 }}
>
<Typography
variant='body2'
flexGrow={1}
fontSize={16}
minWidth={{ xs: 'auto', md: '170px' }}
>
{t('changePassword.title')}
</Typography>
<Stack flexDirection='row' gap={4} display={{ xs: 'none', md: 'flex' }}>
{actions}
</Stack>
</Stack>
<PasswordTextField
id='current_password'
label={t('changePassword.currentPassword')}
hasError={Reflect.has(errors, 'current_password')}
helperText={errors.current_password}
value={passwords.current_password}
onChange={handlePasswordChange}
/>
<PasswordTextField
id='new_password'
label={t('changePassword.newPassword')}
value={passwords.new_password}
hasError={Reflect.has(errors, 'new_password')}
helperText={errors.new_password}
onChange={handlePasswordChange}
/>
<PasswordTextField
id='password_confirm'
label={t('changePassword.confirmPassword')}
value={passwords.password_confirm}
onChange={handlePasswordChange}
/>
<Stack flexDirection='row' gap={4} display={{ xs: 'flex', md: 'none' }}>
{actions}
</Stack>
</Stack>
);
};
Loading

0 comments on commit c0fbcba

Please sign in to comment.