Skip to content
This repository was archived by the owner on Dec 8, 2022. It is now read-only.

Commit 9305d0e

Browse files
authored
Merge branch 'develop' into 27-create-automated-tests
2 parents ed67a57 + 554b98c commit 9305d0e

15 files changed

Lines changed: 241 additions & 51 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Create Error UserAlreadyExistsError
2+
export default class UserAlreadyExistsError extends Error {
3+
constructor(message: string) {
4+
super(message);
5+
this.name = 'UserAlreadyExistsError';
6+
Object.setPrototypeOf(this, UserAlreadyExistsError.prototype);
7+
}
8+
}

server/src/routes/users.routes.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import ResetUserPasswordService from '../services/ResetUserPasswordService';
77
import DeleteUserService from '../services/DeleteUserService';
88
import ensureAuthenticated from '../middlewares/ensureAuthenticated';
99
import Queue from '../services/Queue';
10-
import SendConfirmAccountMail from '../jobs/SendConfirmAccountMail';
1110
import SendResetPasswordMail from '../jobs/SendResetPasswordMail';
1211
import authConfig from '../config/auth';
13-
import User from '../models/User';
1412
import SendDataExportMail from '../jobs/SendDataExportMail';
1513
import Notification from '../models/Notification';
14+
import User from '../models/User';
15+
import UserAlreadyExistsError from '../models/exceptions/UserAlreadyExistsError';
16+
import sendConfirmationMail from '../utils/sendConfirmationEmail';
1617

1718
const usersRouter = Router();
1819

@@ -131,26 +132,31 @@ usersRouter.post('/', async (request, response, next) => {
131132
const createUser = new CreateUserService();
132133
const user = await createUser.execute({ email, password });
133134

134-
// Create confirmation token so that the user can confirm their account
135-
const EMAIL_SECRET = authConfig.jwt.secret;
136-
const emailToken = sign(
137-
{
138-
user,
139-
},
140-
EMAIL_SECRET as string,
141-
{
142-
expiresIn: authConfig.jwt.expiresIn,
143-
},
144-
);
145-
146-
// Add confirmation email task to the queue, so that it will be sent to the user
147-
await Queue.add(SendConfirmAccountMail.key, {
148-
token: emailToken,
149-
user,
150-
});
135+
if (user) {
136+
sendConfirmationMail(user);
137+
}
151138

152139
return response.json(user);
153140
} catch (err) {
141+
if (err instanceof UserAlreadyExistsError) {
142+
const { email } = request.body;
143+
const user = await getRepository(User).findOne({
144+
where: { email },
145+
});
146+
147+
if (user && !user.confirmed) {
148+
sendConfirmationMail(user);
149+
150+
return response
151+
.status(400)
152+
.json({ error: `${err.message} Confirmation email resent.` });
153+
}
154+
155+
return response.status(400).json({
156+
error: `${err.message} User is already confirmed, so confirmation email was not resent.`,
157+
});
158+
}
159+
154160
if (err instanceof Error) {
155161
return response.status(400).json({ error: err.message });
156162
}

server/src/services/CreateUserService.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getRepository } from 'typeorm';
22
import { hash } from 'bcryptjs';
33
import User from '../models/User';
44
import { UserOptionalPassword } from '../@types/alertadotesouro';
5+
import UserAlreadyExistsError from '../models/exceptions/UserAlreadyExistsError';
56

67
/**
78
* Interface for the request object for creating a new User.
@@ -25,7 +26,7 @@ class CreateUserService {
2526
});
2627

2728
if (findUser) {
28-
throw new Error('E-mail address is already being used.');
29+
throw new UserAlreadyExistsError('E-mail address is already being used.');
2930
}
3031

3132
const hashedPassword = await hash(password, 8);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { sign } from 'jsonwebtoken';
2+
import SendConfirmAccountMail from '../jobs/SendConfirmAccountMail';
3+
import User from '../models/User';
4+
import Queue from '../services/Queue';
5+
import authConfig from '../config/auth';
6+
7+
export default function sendConfirmationMail(user: User): void {
8+
const EMAIL_SECRET = authConfig.jwt.secret;
9+
const emailToken = sign(
10+
{
11+
user,
12+
},
13+
EMAIL_SECRET as string,
14+
{
15+
expiresIn: authConfig.jwt.expiresIn,
16+
},
17+
);
18+
19+
// Add confirmation email task to the queue, so that it will be sent to the user
20+
Queue.add(SendConfirmAccountMail.key, {
21+
token: emailToken,
22+
user,
23+
});
24+
}

web/.eslintrc.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ settings:
2424
react:
2525
version: detect
2626
rules:
27+
no-alert: off
28+
jsx-a11y/label-has-associated-control: off
2729
no-underscore-dangle: off
2830
no-console: off
2931
no-unused-vars: off

web/src/components/Input/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,17 @@ export default function Input({ name, icon: Icon, ...props }: InputProps) {
4343
setIsFilled(!!inputRef.current?.value);
4444
}, []);
4545

46+
const focusInput = useCallback(() => {
47+
inputRef.current?.focus();
48+
}, []);
49+
4650
return (
47-
<Container isFocused={isFocused} isFilled={isFilled} isErrored={!!error}>
51+
<Container
52+
isFocused={isFocused}
53+
isFilled={isFilled}
54+
isErrored={!!error}
55+
onClick={focusInput}
56+
>
4857
<div id="svg-div">{Icon && <Icon size={20} />}</div>
4958
<input
5059
onFocus={handleInputFocus}

web/src/pages/Account/index.tsx

Lines changed: 139 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,31 @@ import { useRef, useCallback } from 'react';
33
import { FormHandles } from '@unform/core';
44
import * as Yup from 'yup';
55
import { Form } from '@unform/web';
6-
import { FiAtSign, FiCheck, FiLock, FiPlus } from 'react-icons/fi';
6+
import {
7+
FiAtSign,
8+
FiCheck,
9+
FiFileText,
10+
FiLock,
11+
FiLogOut,
12+
FiPlus,
13+
FiRefreshCcw,
14+
FiUserX,
15+
} from 'react-icons/fi';
16+
import { useNavigate } from 'react-router';
717
import getValidationErrors from '../../utils/getValidationErrors';
818
import Input from '../../components/Input';
919
import { Container, AnimationContainer } from './styles';
1020
import api from '../../services/api';
1121

22+
interface PasswordUpdateFormData {
23+
oldPassword: string;
24+
newPassword: string;
25+
newPasswordConfirmation: string;
26+
}
27+
1228
export default function Account() {
1329
const formRef = useRef<FormHandles>(null);
30+
const navigate = useNavigate();
1431

1532
const handleDataExport = async () => {
1633
// GET request + bearer token to data export endpoint
@@ -51,21 +68,108 @@ export default function Account() {
5168
),
5269
});
5370

54-
await schema.validate(data, {
55-
abortEarly: false,
56-
});
57-
} catch (err) {
58-
if (err instanceof Yup.ValidationError) {
59-
const errors = getValidationErrors(err);
60-
formRef.current?.setErrors(errors);
61-
} else throw err;
71+
// Delete user info from localStorage and redirect user to login page
72+
const handleLogout = () => {
73+
// Delete user token from localStorage
74+
localStorage.removeItem('@AlertaDoTesouro:token');
75+
localStorage.removeItem('@AlertaDoTesouro:user');
76+
77+
78+
// Redirect user to login page
79+
navigate('/login');
80+
};
81+
82+
const handleDeleteAccount = async () => {
83+
if (
84+
window.confirm(
85+
'Você realmente quer deletar sua conta? Esta ação é irreversível!',
86+
)
87+
) {
88+
// Send DELETE request to API to delete user account, along with bearer token
89+
const userToken: string | null = localStorage.getItem(
90+
'@AlertaDoTesouro:token',
91+
);
92+
if (userToken) {
93+
await api.delete('/users', {
94+
headers: {
95+
Authorization: `Bearer ${userToken}`,
96+
},
97+
});
98+
99+
// Simulate logout
100+
handleLogout();
101+
}
62102
}
63-
}, []);
103+
};
104+
105+
const getUserEmail = (): string | undefined => {
106+
try {
107+
const userDataString: string | null = localStorage.getItem(
108+
'@AlertaDoTesouro:user',
109+
);
110+
if (userDataString) {
111+
const user: Record<string, string> = JSON.parse(
112+
userDataString,
113+
) as Record<string, string>;
114+
if (user) {
115+
const userEmail: string = user.email;
116+
return userEmail;
117+
}
118+
}
119+
} catch (error) {
120+
if (error instanceof Error) {
121+
console.log(error);
122+
}
123+
}
124+
125+
return '';
126+
};
127+
128+
const handlePasswordUpdate = useCallback(
129+
async (data: PasswordUpdateFormData) => {
130+
try {
131+
formRef.current?.setErrors({});
132+
133+
// Redundant validation, but surely is a validation.
134+
const schema = Yup.object().shape({
135+
oldPassword: Yup.string().min(8, 'Mínimo de 8 caracteres'),
136+
newPassword: Yup.string().min(8, 'Mínimo de 8 caracteres'),
137+
newPasswordConfirmation: Yup.string().when(
138+
'password',
139+
(password: string, field: Yup.StringSchema) =>
140+
password
141+
? field
142+
.required('Senhas devem ser iguais')
143+
.oneOf([Yup.ref('password')], 'Senhas devem ser iguais')
144+
: field,
145+
),
146+
});
147+
148+
await schema.validate(data, {
149+
abortEarly: false,
150+
});
151+
152+
await api.put('/users', data);
153+
154+
console.log('Senha atualizada com sucesso.');
155+
} catch (err) {
156+
if (err instanceof Yup.ValidationError) {
157+
const errors = getValidationErrors(err);
158+
formRef.current?.setErrors(errors);
159+
} else {
160+
console.log(
161+
'Ocorreu um erro ao atualizar a senha. Tente novamente mais tarde.',
162+
);
163+
}
164+
}
165+
},
166+
[],
167+
);
64168

65169
return (
66170
<Container>
67171
<AnimationContainer>
68-
<Form ref={formRef} onSubmit={handleSubmit}>
172+
<Form ref={formRef} onSubmit={handlePasswordUpdate}>
69173
<div id="form-header">
70174
<h1>SUAS INFORMAÇÕES</h1>
71175
</div>
@@ -77,6 +181,7 @@ export default function Account() {
77181
icon={FiAtSign}
78182
name="email"
79183
placeholder="turing@inf.ufes.br"
184+
defaultValue={getUserEmail()}
80185
/>
81186

82187
<div id="input-header">
@@ -101,20 +206,37 @@ export default function Account() {
101206
placeholder="Confirmação de sua nova senha"
102207
/>
103208

104-
<button type="submit">Atualizar dados</button>
105-
<button id="sair" type="submit">
106-
Sair
107-
</button>
108-
<button id="deletar-conta" type="submit">
109-
Deletar conta
209+
{/* 'Form' Submit */}
210+
<button id="atualizar-senha" type="submit">
211+
<FiRefreshCcw />
212+
Atualizar senha
110213
</button>
214+
215+
{/* Data export */}
111216
<button
112217
id="exportar-dados"
113218
type="button"
114219
onClick={() => handleDataExport()}
115220
>
221+
<FiFileText />
116222
Exportar dados
117223
</button>
224+
225+
{/* Logout */}
226+
<button id="sair" type="button" onClick={() => handleLogout()}>
227+
<FiLogOut />
228+
Sair
229+
</button>
230+
231+
{/* Account deletion */}
232+
<button
233+
id="deletar-conta"
234+
type="button"
235+
onClick={() => handleDeleteAccount()}
236+
>
237+
<FiUserX />
238+
Deletar conta
239+
</button>
118240
</Form>
119241
</AnimationContainer>
120242
</Container>

web/src/pages/ForgotPassword/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,12 @@ export default function ForgotPassword() {
7272
<h3>Informe o e-mail de sua conta para redefinir sua senha:</h3>
7373
</div>
7474
<Input
75+
autoFocus
7576
icon={FiAtSign}
7677
name="email"
77-
placeholder="Ex: alan@turing.com"
78+
placeholder="turing@inf.ufes.br"
7879
/>
7980

80-
{/* TODO: Improve icon spacing */}
8181
<button type="submit">
8282
<FiMail /> Redefinir senha
8383
</button>

web/src/pages/ResetPassword/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export default function ResetPassword() {
9595
</div>
9696

9797
<Input
98+
autoFocus
9899
icon={FiLock}
99100
name="newPassword"
100101
type="password"

web/src/pages/SignIn/index.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useRef, useCallback, useContext } from 'react';
22
import { useNavigate } from 'react-router';
33
import { Link } from 'react-router-dom';
44

5-
import { FiKey, FiLock, FiAtSign } from 'react-icons/fi';
5+
import { FiKey, FiLock, FiAtSign, FiLogIn } from 'react-icons/fi';
66
import { Form } from '@unform/web';
77
import * as Yup from 'yup';
88
import { FormHandles } from '@unform/core';
@@ -92,7 +92,10 @@ export default function SignIn() {
9292
placeholder="Sua senha"
9393
/>
9494

95-
<button type="submit">Entrar</button>
95+
<button type="submit">
96+
<FiLogIn />
97+
Entrar
98+
</button>
9699
</Form>
97100

98101
<Link to="/registrar">

0 commit comments

Comments
 (0)