Skip to content

Commit

Permalink
Merge pull request #8 from fga-eps-mds/9-us04-recuperacao-de-senha
Browse files Browse the repository at this point in the history
9 us04 recuperacao de senha
  • Loading branch information
Matheusafonsouza authored Dec 9, 2024
2 parents b8d8e75 + b5f67b1 commit 42e897d
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 0 deletions.
4 changes: 4 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import Loans from "./pages/Loans"
import { Toaster } from "./components/ui/toaster"
import PrivateRoute from './PrivateRoute';
import SignIn from "./pages/SignIn"
import RecoverPassword from "./pages/RecoverPassword"
import ChangePassword from "./pages/ChangePassword"

function App() {
return (
Expand All @@ -22,6 +24,8 @@ function App() {
<Route path="*" element={<NotFound />} />
<Route path="/login" element={<SignIn />} />
<Route path="/cadastro" element={<SignUp />} />
<Route path="/recuperar-senha" element={<RecoverPassword />} />
<Route path="/alterar-senha" element={<ChangePassword />} />
<Route path="/inicio" element={<PrivateRoute><Home /></PrivateRoute>} />
<Route path="/perfil" element={<PrivateRoute><Profile /></PrivateRoute>} />
<Route path="/perfil/editar" element={<PrivateRoute><ProfileEdit /></PrivateRoute>} />
Expand Down
20 changes: 20 additions & 0 deletions src/hooks/useApi/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@ const useApi = () => {
.catch((err) => resolve(getDefaultErrorUseAPIMessage(err)));
});
},
recoverPassword: (email: string): Promise<{ data: any }> => {
return new Promise((resolve) => {
api
.post(`/auth/recover-password/${email}`)
.then((res) => resolve(res))
.catch((err) => resolve(getDefaultErrorUseAPIMessage(err)));
});
},
changePassword: (password: string, token: string): Promise<{ data: any }> => {
return new Promise((resolve) => {
api
.post(`/auth/change-password`, { password }, {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((res) => resolve(res))
.catch((err) => resolve(getDefaultErrorUseAPIMessage(err)));
});
},
editProfile: async (id: string, data: {
firstName: string;
lastName: string;
Expand Down
40 changes: 40 additions & 0 deletions src/hooks/useAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ type AuthContextType = {
signUp: (userToSignUp: SignUpParams) => Promise<boolean>;
signIn: (userToSignIn: SignInParams) => Promise<boolean>;
editProfile: (id: string, profileToEdit: EditProfileParams) => Promise<boolean>;
recoverPassword: (email: string) => Promise<boolean>;
changePassword: (password: string, token: string) => Promise<boolean>;
};

const AuthContext = createContext({} as AuthContextType);
Expand All @@ -43,6 +45,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
signUp: authSignUp,
signIn: authSignIn,
editProfile: authEditProfile,
recoverPassword: authRecoverPassword,
changePassword: authChangePassword,
} = useApi();

const localToken =
Expand Down Expand Up @@ -87,6 +91,40 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
return true;
}

async function recoverPassword(email: string): Promise<boolean> {
const { data } = await authRecoverPassword(email);
if (data.id) {
toaster.create({
title: 'E-mail enviado para recuperação de senha!',
type: 'success',
})
return true;
};
toaster.create({
title: 'Erro ao requisitar recuperação de senha',
description: 'Verifique os campos e tente novamente.',
type: 'error',
})
return false;
}

async function changePassword(password: string, token: string): Promise<boolean> {
const { data } = await authChangePassword(password, token);
if (data.id) {
toaster.create({
title: 'Senha alterada com sucesso! Você será redirecionado para o login...',
type: 'success',
})
return true;
};
toaster.create({
title: 'Erro ao alterar a senha',
description: 'Verifique os campos e tente novamente.',
type: 'error',
})
return false;
}

async function editProfile(id: string, profileToEdit: EditProfileParams): Promise<boolean> {
const { data } = await authEditProfile(id, profileToEdit);
if (data.id) {
Expand Down Expand Up @@ -120,6 +158,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
signUp,
editProfile,
signIn,
recoverPassword,
changePassword,
}}
>
{children}
Expand Down
75 changes: 75 additions & 0 deletions src/pages/ChangePassword/ChangePasswordForm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useState } from 'react';
import { useAuth } from '../../../hooks/useAuth';
import { Stack } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { Button } from '../../../components/ui/button';
import { Field } from '../../../components/ui/field';
import { PasswordInput } from '../../../components/ui/password-input';
import { useLocation } from 'react-router';

interface FormValues {
password: string;
passwordConfirmation: string;
}

function useQuery() {
return new URLSearchParams(useLocation().search);
}

function ChangePasswordForm() {
const query = useQuery();
const [loading, setLoading] = useState(false);

const token = query.get('token');

const {
register,
handleSubmit,
formState: { errors, isValid },
} = useForm<FormValues>();

const { changePassword } = useAuth();

const onSubmit = handleSubmit(async (data: FormValues) => {
if (!token) return;
setLoading(true);
await changePassword(data.password, token);
setLoading(false);
})

return (
<form onSubmit={onSubmit}>
<Stack gap={'40px'}>
<Stack gap={'10px'}>
<Field invalid={!!errors.password} errorText={errors.password?.message}>
<PasswordInput
size={'2xl'}
placeholder={'Senha'}
{...register('password', { required: "Campo obrigatório." })}
/>
</Field>
<Field invalid={!!errors.passwordConfirmation} errorText={errors.passwordConfirmation?.message}>
<PasswordInput
size={'2xl'}
placeholder={'Confirmar senha'}
{...register('passwordConfirmation', { required: "Campo obrigatório." })}
/>
</Field>
</Stack>
<Button
loading={loading}
type="submit"
width={'100%'}
size={'2xl'}
bg={'green.100'}
fontWeight={'semibold'}
disabled={!isValid}
>
Alterar senha
</Button>
</Stack>
</form>
);
}

export default ChangePasswordForm
12 changes: 12 additions & 0 deletions src/pages/ChangePassword/ChangePasswordHeader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Stack, Text } from '@chakra-ui/react';

function RecoverPasswordHeader() {
return (
<Stack gap={'5px'}>
<Text textStyle={'3xl'} fontWeight={'semibold'} color={'blue.100'}>Nova senha</Text>
<Text color={'blue.100'}>Deseja criar uma nova senha? Insira sua senha e confirme-a.</Text>
</Stack>
);
}

export default RecoverPasswordHeader
18 changes: 18 additions & 0 deletions src/pages/ChangePassword/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Box, Center, Stack } from '@chakra-ui/react';
import ChangePasswordForm from './ChangePasswordForm';
import ChangePasswordHeader from './ChangePasswordHeader';

function ChangePassword() {
return (
<Box padding='40px'>
<Center>
<Stack gap={'40px'}>
<ChangePasswordHeader />
<ChangePasswordForm />
</Stack>
</Center>
</Box>
);
}

export default ChangePassword
63 changes: 63 additions & 0 deletions src/pages/RecoverPassword/RecoverPasswordForm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useState } from 'react';
import { useAuth } from '../../../hooks/useAuth';
import { Input, Stack } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { Button } from '../../../components/ui/button';
import { Field } from '../../../components/ui/field';

interface FormValues {
email: string;
}

function RecoverPasswordForm() {
const [loading, setLoading] = useState(false);

const {
register,
handleSubmit,
formState: { errors, isValid },
} = useForm<FormValues>();

const { recoverPassword } = useAuth();

const onSubmit = handleSubmit(async (data: FormValues) => {
setLoading(true);
await recoverPassword(data.email);
setLoading(false);
})

return (
<form onSubmit={onSubmit}>
<Stack gap={'40px'}>
<Stack gap={'10px'}>
<Field invalid={!!errors.email} errorText={errors.email?.message}>
<Input
size={'2xl'}
placeholder={'E-mail'}
{...register('email', {
required: "Campo obrigatório.",
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
message: "E-mail inválido."
}
})}
/>
</Field>
</Stack>
<Button
loading={loading}
type="submit"
width={'100%'}
size={'2xl'}
bg={'green.100'}
fontWeight={'semibold'}
disabled={!isValid}
>
Solicitar nova senha
</Button>
</Stack>
</form>
);
}

export default RecoverPasswordForm
12 changes: 12 additions & 0 deletions src/pages/RecoverPassword/RecoverPasswordHeader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Stack, Text } from '@chakra-ui/react';

function RecoverPasswordHeader() {
return (
<Stack gap={'5px'}>
<Text textStyle={'3xl'} fontWeight={'semibold'} color={'blue.100'}>Recuperação de Senha</Text>
<Text color={'blue.100'}>Para recuperar o acesso a sua conta, vamos enviar um código para seu e-mail.</Text>
</Stack>
);
}

export default RecoverPasswordHeader
18 changes: 18 additions & 0 deletions src/pages/RecoverPassword/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Box, Center, Stack } from '@chakra-ui/react';
import RecoverPasswordForm from './RecoverPasswordForm';
import RecoverPasswordHeader from './RecoverPasswordHeader';

function RecoverPassword() {
return (
<Box padding='40px'>
<Center>
<Stack gap={'40px'}>
<RecoverPasswordHeader />
<RecoverPasswordForm />
</Stack>
</Center>
</Box>
);
}

export default RecoverPassword

0 comments on commit 42e897d

Please sign in to comment.