From 5420104ae16cd492f5e5bb4deaa076d4902a7c66 Mon Sep 17 00:00:00 2001 From: Ricardo Campos Date: Sat, 16 May 2026 00:19:32 +0200 Subject: [PATCH 1/3] feat: prevent user form login before conforming his email --- .../__test__/components/LoginForm.test.tsx | 22 +++++++ .../__test__/context/SidebarProvider.test.tsx | 2 +- client/src/components/LoginForm/index.tsx | 57 ++++++++++++------- client/src/constants/languageConstants.ts | 8 ++- client/src/constants/serverResponses.ts | 4 +- .../exception/EmailNotConfirmedException.java | 14 +++++ .../server/service/AuthService.java | 7 +++ .../server/service/AuthServiceTest.java | 25 ++++++++ 8 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 server/src/main/java/br/com/tasknoteapp/server/exception/EmailNotConfirmedException.java diff --git a/client/src/__test__/components/LoginForm.test.tsx b/client/src/__test__/components/LoginForm.test.tsx index cff8c5c5..aeeafff7 100644 --- a/client/src/__test__/components/LoginForm.test.tsx +++ b/client/src/__test__/components/LoginForm.test.tsx @@ -140,4 +140,26 @@ describe('LoginForm Component test', () => { vi.useRealTimers(); }); + + it('should display resend button when login fails with "Email not confirmed!"', async () => { + authContextMock.signIn.mockRejectedValue(new Error('Email not confirmed!')); + renderFn("login"); + + const emailInput = screen.getByTestId('login_email_input'); + const passwordInput = screen.getByTestId('account-password-login'); + const submitButton = screen.getByRole('button', { name: /login/i }); + + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); + fireEvent.change(passwordInput, { target: { value: 'password123' } }); + + await act(async () => { + fireEvent.click(submitButton); + }); + + const errorMessage = await screen.findByText(/email not confirmed/i); + expect(errorMessage).toBeDefined(); + + const resendButton = screen.getByRole('button', { name: /resend confirmation email/i }); + expect(resendButton).toBeDefined(); + }); }); diff --git a/client/src/__test__/context/SidebarProvider.test.tsx b/client/src/__test__/context/SidebarProvider.test.tsx index d29677bc..e548d0e1 100644 --- a/client/src/__test__/context/SidebarProvider.test.tsx +++ b/client/src/__test__/context/SidebarProvider.test.tsx @@ -60,7 +60,7 @@ describe('SidebarProvider', () => { }); await waitFor(() => - expect(getByTestId('page').textContent).toBe('/another') + expect(getByTestId('page').textContent).toBe('/home') ); }); }); diff --git a/client/src/components/LoginForm/index.tsx b/client/src/components/LoginForm/index.tsx index 5fc230b8..e2c852a6 100644 --- a/client/src/components/LoginForm/index.tsx +++ b/client/src/components/LoginForm/index.tsx @@ -34,6 +34,7 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { const [formInvalid, setFormInvalid] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [successMessage, setSuccessMessage] = useState(''); + const [showResend, setShowResend] = useState(false); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [passwordAgain, setPasswordAgain] = useState(''); @@ -82,6 +83,7 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { } setFormInvalid(false); + setShowResend(false); try { if (prefix === 'login') { await signIn(email, password); @@ -93,6 +95,7 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { setPassword(''); setPasswordAgain(''); setSuccessMessage(translateServerResponse('Please confirm your email before proceeding', i18n.language)); + setShowResend(true); setSecondsLeft(30); setIsResendEnabled(false); } @@ -109,13 +112,19 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { catch (e) { console.log(e); setFormInvalid(true); + let msg = ''; if (typeof e === 'string') { console.log('string'); - setErrorMessage(translateServerResponse(e, i18n.language)); + msg = e; } else if (e instanceof Error) { console.log(e.message); - setErrorMessage(translateServerResponse(e.message, i18n.language)); + msg = e.message; + } + + setErrorMessage(translateServerResponse(msg, i18n.language)); + if (msg === 'Email not confirmed!') { + setShowResend(true); } } }; @@ -124,7 +133,9 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { try { await api.postJSON(ApiConfig.resendConfirmUrl, { email }); - setSuccessMessage('Confirmation email re-sent! Please check the spam folder.'); + setSuccessMessage(translateServerResponse('Confirmation email re-sent! Please check the spam folder.', i18n.language)); + setFormInvalid(false); + setShowResend(true); setSecondsLeft(30); setIsResendEnabled(false); } @@ -166,34 +177,42 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { ? ( { errorMessage } + {showResend && ( +
+ +
+ )}
) : null} - {successMessage.length > 1 && prefix === 'register' && ( + {successMessage.length > 1 && ( <> { successMessage } -
- -
+ {showResend && ( +
+ +
+ )} )} - {successMessage.length > 1 && prefix !== 'register' && ( - - { successMessage } - - )} -
{prefix !== 'complete_reset' && ( = { WRONG_IDENTIFICATION_pt_br: 'Identificação incorreta ou faltando', WRONG_OR_MISSING_INFO_pt_br: 'Informação errada ou incompleta!', NOTHING_TO_UPDATE_pt_br: 'Nada para atualizar!', + EMAIL_NOT_CONFIRMED_pt_br: 'E-mail não confirmado!', + CONFIRMATION_RESENT_pt_br: 'E-mail de confirmação reenviado! Por favor, verifique a pasta de spam.', BAD_PASSWORD_8_es: 'Contraseña inválida: La contraseña debe tener al menos 8 caracteres', BAD_PASSWORD_7_es: 'Contraseña inválida: La contraseña debe tener al menos 8 caracteres, 1 carácter especial', @@ -117,6 +119,8 @@ export const serverResponsesTranslations: Record = { WRONG_IDENTIFICATION_es: 'Identificación incorrecta o faltante', WRONG_OR_MISSING_INFO_es: '¡Información incorrecta o incompleta!', NOTHING_TO_UPDATE_es: '¡Nada que actualizar!', + EMAIL_NOT_CONFIRMED_es: '¡Correo no confirmado!', + CONFIRMATION_RESENT_es: '¡Correo de confirmación reenviado! Por favor, revisa la carpeta de spam.', BAD_PASSWORD_8_ru: 'Неправильный пароль: Пароль должен содержать не менее 8 символов.', BAD_PASSWORD_7_ru: 'Неправильный пароль: Пароль должен содержать не менее 8 символов, 1 специальный символ.', @@ -143,5 +147,7 @@ export const serverResponsesTranslations: Record = { UNKNOWN_ru: 'Неизвестная ошибка', WRONG_IDENTIFICATION_ru: 'Неправильная или отсутствующая идентификация', WRONG_OR_MISSING_INFO_ru: 'Неверная или отсутствующая информация!', - NOTHING_TO_UPDATE_ru: 'Нечего обновлять!' + NOTHING_TO_UPDATE_ru: 'Нечего обновлять!', + EMAIL_NOT_CONFIRMED_ru: 'Электронная почта не подтверждена!', + CONFIRMATION_RESENT_ru: 'Письмо с подтверждением отправлено еще раз! Пожалуйста, проверьте папку со спамом.' }; diff --git a/client/src/constants/serverResponses.ts b/client/src/constants/serverResponses.ts index bc723c17..02b6472c 100644 --- a/client/src/constants/serverResponses.ts +++ b/client/src/constants/serverResponses.ts @@ -24,5 +24,7 @@ export const serverResponses: Record = { 'Wrong or missing identification': 'WRONG_IDENTIFICATION', 'Wrong or missing information!': 'WRONG_OR_MISSING_INFO', 'Invalid credentials': 'INVALID_CREDENTIALS', - 'Nothing to update!': 'NOTHING_TO_UPDATE' + 'Nothing to update!': 'NOTHING_TO_UPDATE', + 'Email not confirmed!': 'EMAIL_NOT_CONFIRMED', + 'Confirmation email re-sent! Please check the spam folder.': 'CONFIRMATION_RESENT' }; diff --git a/server/src/main/java/br/com/tasknoteapp/server/exception/EmailNotConfirmedException.java b/server/src/main/java/br/com/tasknoteapp/server/exception/EmailNotConfirmedException.java new file mode 100644 index 00000000..61556c83 --- /dev/null +++ b/server/src/main/java/br/com/tasknoteapp/server/exception/EmailNotConfirmedException.java @@ -0,0 +1,14 @@ +package br.com.tasknoteapp.server.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.server.ResponseStatusException; + +/** This class represents an Email Not Confirmed exception. */ +@ResponseStatus(code = HttpStatus.FORBIDDEN) +public class EmailNotConfirmedException extends ResponseStatusException { + + public EmailNotConfirmedException() { + super(HttpStatus.FORBIDDEN, "Email not confirmed!"); + } +} diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java b/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java index 38f1c4a5..1bd50946 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java @@ -6,6 +6,7 @@ import br.com.tasknoteapp.server.exception.BadPasswordException; import br.com.tasknoteapp.server.exception.BadUuidException; import br.com.tasknoteapp.server.exception.EmailAlreadyExistsException; +import br.com.tasknoteapp.server.exception.EmailNotConfirmedException; import br.com.tasknoteapp.server.exception.InvalidCredentialsException; import br.com.tasknoteapp.server.exception.MaxLoginLimitAttemptException; import br.com.tasknoteapp.server.exception.ResetExpiredException; @@ -189,6 +190,12 @@ public UserResponseWithToken signInUser(LoginRequest login) { checkLoginAttemptLimit(userOptional.get().getId()); UserEntity user = userOptional.get(); + + if (Objects.isNull(user.getEmailConfirmedAt())) { + logger.warn("User {} tried to login but email is not confirmed", user.getId()); + throw new EmailNotConfirmedException(); + } + user.setResetToken(null); user.setResetPasswordExpiration(null); diff --git a/server/src/test/java/br/com/tasknoteapp/server/service/AuthServiceTest.java b/server/src/test/java/br/com/tasknoteapp/server/service/AuthServiceTest.java index f7843bc8..bba793f2 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/service/AuthServiceTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/service/AuthServiceTest.java @@ -11,6 +11,7 @@ import br.com.tasknoteapp.server.exception.BadPasswordException; import br.com.tasknoteapp.server.exception.BadUuidException; import br.com.tasknoteapp.server.exception.EmailAlreadyExistsException; +import br.com.tasknoteapp.server.exception.EmailNotConfirmedException; import br.com.tasknoteapp.server.exception.InvalidCredentialsException; import br.com.tasknoteapp.server.exception.MaxLoginLimitAttemptException; import br.com.tasknoteapp.server.exception.ResetExpiredException; @@ -197,6 +198,7 @@ void signInUser_happyPath_shouldSucceed() { UserEntity existing = new UserEntity(); existing.setId(919L); existing.setEmail(request.email()); + existing.setEmailConfirmedAt(LocalDateTime.now()); when(userRepository.findByEmail(request.email())).thenReturn(Optional.of(existing)); when(userPwdLimitRepository.findTop3ByUser_idOrderByWhenHappenedDesc(existing.getId())) @@ -212,6 +214,27 @@ void signInUser_happyPath_shouldSucceed() { Assertions.assertEquals("a1b2c3", token.token()); } + @Test + @DisplayName("SignIn user not confirmed should fail") + void signInUser_notConfirmed_shouldFail() { + LoginRequest request = new LoginRequest("email@domain.com", "123456", "123456", "en"); + + UserEntity existing = new UserEntity(); + existing.setId(919L); + existing.setEmail(request.email()); + existing.setEmailConfirmedAt(null); + when(userRepository.findByEmail(request.email())).thenReturn(Optional.of(existing)); + + when(userPwdLimitRepository.findTop3ByUser_idOrderByWhenHappenedDesc(existing.getId())) + .thenReturn(List.of()); + + Assertions.assertThrows( + EmailNotConfirmedException.class, + () -> { + authService.signInUser(request); + }); + } + @Test @DisplayName("SignIn wrong user or password should fail") void signInUser_wrongUserOrPassword_shouldFail() { @@ -233,6 +256,7 @@ void signInUser_maxLoginAttempt_shouldFail() { UserEntity existing = new UserEntity(); existing.setId(919L); + existing.setEmailConfirmedAt(LocalDateTime.now()); when(userRepository.findByEmail(request.email())).thenReturn(Optional.of(existing)); UserPwdLimitEntity limit1 = new UserPwdLimitEntity(); @@ -256,6 +280,7 @@ void signInUser_badCredentials_shouldFail() { UserEntity existing = new UserEntity(); existing.setId(919L); + existing.setEmailConfirmedAt(LocalDateTime.now()); when(userRepository.findByEmail(request.email())).thenReturn(Optional.of(existing)); when(userPwdLimitRepository.findTop3ByUser_idOrderByWhenHappenedDesc(existing.getId())) From e84d117598e716e7cb4b77f5637af85bf71fd560 Mon Sep 17 00:00:00 2001 From: Ricardo Campos Date: Sat, 16 May 2026 18:26:38 -0300 Subject: [PATCH 2/3] chore: update files and improve things --- AGENTS.md | 14 +- client/package-lock.json | 510 ------------------ client/src/App.tsx | 4 +- .../__test__/components/AlertError.test.tsx | 2 +- client/src/components/AlertError/index.tsx | 6 +- client/src/components/FormInput/index.tsx | 4 +- client/src/components/LoginForm/index.tsx | 40 +- client/src/context/AuthProvider.tsx | 10 +- client/src/routes/ProtectedRoute/index.tsx | 2 +- client/src/types/SigninResponse.ts | 2 +- client/src/types/SummaryResponse.ts | 5 - client/src/views/Account/index.tsx | 6 +- client/src/views/EmailConfirmation/index.tsx | 2 +- client/src/views/Home/index.tsx | 9 +- client/src/views/Landing/index.tsx | 2 +- client/src/views/NoteAdd/index.tsx | 4 +- client/src/views/Register/index.tsx | 2 +- client/src/views/TaskAdd/index.tsx | 4 +- .../tasknoteapp/server/config/CorsConfig.java | 3 +- .../server/config/SecurityConfig.java | 7 +- .../server/controller/HealthController.java | 10 +- .../server/controller/NoteController.java | 10 +- .../controller/RestExceptionController.java | 8 + .../server/controller/TaskController.java | 10 +- .../controller/UserSessionController.java | 2 +- .../tasknoteapp/server/entity/NoteEntity.java | 2 +- .../tasknoteapp/server/entity/TaskEntity.java | 2 +- .../server/entity/TaskUrlEntityPk.java | 8 - .../tasknoteapp/server/entity/UserEntity.java | 32 +- .../exception/MailServiceException.java | 9 - .../server/filter/HeaderVersionFilter.java | 10 +- .../filter/JwtAuthenticationFilter.java | 15 +- .../hint/HttpServletRequestRuntimeHint.java | 3 +- .../repository/UserPwdLimitRepository.java | 3 - .../server/request/LoginRequest.java | 14 +- .../server/request/NotePatchRequest.java | 2 +- .../server/request/NoteRequest.java | 2 +- .../server/request/NoteUrlPatchRequest.java | 4 - .../request/ResendConfirmationRequest.java | 6 - .../server/request/TaskPatchRequest.java | 2 +- .../server/request/TaskRequest.java | 2 +- .../server/request/TaskUrlPatchRequest.java | 4 - .../server/response/NoteResponse.java | 6 +- .../server/response/TaskResponse.java | 4 +- .../server/response/UserResponse.java | 5 +- .../response/UserResponseWithToken.java | 5 +- .../response/ValidationExceptionResponse.java | 8 - .../server/service/AuthService.java | 42 +- .../server/service/JwtService.java | 12 +- .../server/service/MailgunEmailService.java | 29 +- .../server/service/NoteService.java | 42 +- .../server/service/TaskService.java | 84 ++- .../server/service/impl/JwtServiceImpl.java | 5 +- .../server/templates/MailgunTemplate.java | 8 +- .../MailgunTemplateEmailChanged.java | 13 +- .../templates/MailgunTemplateResetPwd.java | 3 +- .../MailgunTemplateResetPwdConfirm.java | 3 +- .../templates/MailgunTemplateSignUp.java | 5 +- .../tasknoteapp/server/util/TimeAgoUtil.java | 4 +- .../tasknoteapp/server/util/TokenUtil.java | 2 +- .../com/tasknoteapp/server/util/UuidUtil.java | 2 +- 61 files changed, 259 insertions(+), 821 deletions(-) delete mode 100644 client/src/types/SummaryResponse.ts delete mode 100644 server/src/main/java/br/com/tasknoteapp/server/exception/MailServiceException.java delete mode 100644 server/src/main/java/br/com/tasknoteapp/server/request/NoteUrlPatchRequest.java delete mode 100644 server/src/main/java/br/com/tasknoteapp/server/request/TaskUrlPatchRequest.java diff --git a/AGENTS.md b/AGENTS.md index 99aafc55..695b72f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,22 +9,27 @@ ## Architecture details that matter - Frontend route split is auth-state driven in `client/src/App.tsx` + `client/src/routes/ProtectedRoute/index.tsx`; private screens render through `client/src/layout/PrivateLayout/index.tsx`. - Home view (`client/src/views/Home/index.tsx`) is a key integration surface: loads tasks/notes/tags, applies client-side filtering, and drives note sharing (`/rest/notes/{id}/share` + `/public/notes/{token}`). +- Three React Contexts are active at runtime: `AuthProvider` (JWT lifecycle), `FilterProvider` (search text + option persisted to `localStorage` as `FILTER_TEXT`/`FILTER_OPTION`), and `SidebarProvider` (sidebar open/close state) — all under `client/src/context/`. - Backend follows controller -> service -> repository layering (`server/src/main/java/br/com/tasknoteapp/server/{controller,service,repository}`). +- `HomeController` (`/rest/home`) exposes aggregated data such as `/rest/home/tasks/tags`; it is separate from `TaskController` and `NoteController`. - Global API error shape comes from `server/src/main/java/br/com/tasknoteapp/server/controller/RestExceptionController.java`; frontend expects `message` or `fields[].fieldMessage` (`client/src/api-service/api.ts`). - Email and password-reset flows are Mailgun-backed (`server/src/main/java/br/com/tasknoteapp/server/service/MailgunEmailService.java`) and use templates under `server/src/main/java/br/com/tasknoteapp/server/templates/` + `mailgun-templates/`. +- Backend targets **Java 25** and **Spring Boot 4.x** (`server/pom.xml`). ## Developer workflows (use these first) - Frontend quality gate: `bash tools/check-frontend.sh` (runs `npm ci`, `lint:fix`, `build`, `test:no-watch`). - Backend quality gate: `bash tools/check-backend.sh` (runs checkstyle, compile, then `clean verify -P tests`). +- Backend dependency freshness check: `bash tools/check-be-deps.sh` (verifies that `failsafe`, `surefire`, `jacoco`, `checkstyle`, and Spring Boot plugin versions in `pom.xml` match the latest releases on Maven Central; must be run from the repo root or `server/`). - Important Maven default: tests/checkstyle/jacoco are skipped unless profile `-P tests` is enabled (`server/pom.xml`). - Dev stack via Docker Compose/Taskfile (`Taskfile.yml`, `docker-compose.dev.yml`): app `5000`, API `8585`, Java debug port `5005`, Postgres `5432`. - Frontend local dev: run from `client/` with `npm start`; backend local dev: run from `server/` with `./mvnw spring-boot:run`. ## CI/CD and release behavior -- PR workflows (`.github/workflows/client-ci.yml`, `.github/workflows/server-ci.yml`) run checks then push `:candidate` images to GHCR. -- Main workflows (`.github/workflows/main-client.yml`, `.github/workflows/main-server.yml`) push versioned tags + `latest`; backend workflow also increments `server/pom.xml` version. -- Deploy workflows (`.github/workflows/deploy-stg.yml`, `.github/workflows/deploy.yml`) apply Terraform in `terraform-stg/` and `terraform/` to Kubernetes. -- Infra wiring (secrets, services, ingress, image vars) is defined in `terraform/main.tf`. +- PR workflows (`.github/workflows/ci-pr-frontend.yml`, `.github/workflows/ci-pr-backend.yml`) run checks then push `:candidate` and `:pr-` images to GHCR. +- Main workflows (`.github/workflows/ci-main-frontend.yml`, `.github/workflows/ci-main-backend.yml`) push versioned tags (`app-v.` / `api-v`) + `latest`; backend workflow also increments `server/pom.xml` version. +- Staging deploy workflow (`.github/workflows/cd-pr.yml`) triggers on completion of either PR CI workflow and applies Terraform in `terraform-stg/` using a plan→apply split. +- Production deploy workflow (`.github/workflows/cd-main.yml`) triggers on completion of either Main CI workflow and applies Terraform in `terraform/` using a plan→apply split; `apply` can be skipped if there are no Terraform changes. +- Infra wiring (secrets, services, ingress, image vars) is defined in `terraform/main.tf`; an alternative GCP target is under `terraform-gcp/`. ## Project conventions to preserve - Frontend style is ESLint flat config + stylistic rules (2-space indent, single quotes, semicolons) in `client/eslint.config.mjs`. @@ -37,4 +42,3 @@ - Keep API contract compatibility with frontend `ApiConfig` paths; changing endpoint paths requires synchronized client updates. - When changing auth/session behavior, update both backend filters/controllers and frontend `AuthProvider` token lifecycle. - If you change CI/build/deploy commands, update both `README.md` and this `AGENTS.md` to keep workflows discoverable. - diff --git a/client/package-lock.json b/client/package-lock.json index badf20a9..9035b46c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -496,448 +496,6 @@ "node": ">=10" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", - "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", - "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", - "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", - "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", - "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", - "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", - "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", - "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", - "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", - "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", - "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", - "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", - "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", - "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", - "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", - "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", - "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", - "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", - "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", - "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", - "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", - "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", - "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", - "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", - "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", - "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -4943,49 +4501,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esbuild": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", - "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.1", - "@esbuild/android-arm": "0.27.1", - "@esbuild/android-arm64": "0.27.1", - "@esbuild/android-x64": "0.27.1", - "@esbuild/darwin-arm64": "0.27.1", - "@esbuild/darwin-x64": "0.27.1", - "@esbuild/freebsd-arm64": "0.27.1", - "@esbuild/freebsd-x64": "0.27.1", - "@esbuild/linux-arm": "0.27.1", - "@esbuild/linux-arm64": "0.27.1", - "@esbuild/linux-ia32": "0.27.1", - "@esbuild/linux-loong64": "0.27.1", - "@esbuild/linux-mips64el": "0.27.1", - "@esbuild/linux-ppc64": "0.27.1", - "@esbuild/linux-riscv64": "0.27.1", - "@esbuild/linux-s390x": "0.27.1", - "@esbuild/linux-x64": "0.27.1", - "@esbuild/netbsd-arm64": "0.27.1", - "@esbuild/netbsd-x64": "0.27.1", - "@esbuild/openbsd-arm64": "0.27.1", - "@esbuild/openbsd-x64": "0.27.1", - "@esbuild/openharmony-arm64": "0.27.1", - "@esbuild/sunos-x64": "0.27.1", - "@esbuild/win32-arm64": "0.27.1", - "@esbuild/win32-ia32": "0.27.1", - "@esbuild/win32-x64": "0.27.1" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -11149,31 +10664,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-declaration-location": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/ts-declaration-location/-/ts-declaration-location-1.0.7.tgz", - "integrity": "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==", - "dev": true, - "funding": [ - { - "type": "ko-fi", - "url": "https://ko-fi.com/rebeccastevens" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/ts-declaration-location" - } - ], - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "dependencies": { - "picomatch": "^4.0.2" - }, - "peerDependencies": { - "typescript": ">=4.0.0" - } - }, "node_modules/ts-toolbelt": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", diff --git a/client/src/App.tsx b/client/src/App.tsx index 7a951869..3709f5dc 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -103,7 +103,7 @@ function App(): React.ReactNode { /** * Determines the appropriate router based on the user's authentication status. - * @returns {Router} The configured router. + * @returns The configured router. */ const getBrowserRouter = () => { if (signed) { @@ -137,6 +137,6 @@ function App(): React.ReactNode { {!loading && } ); -}; +} export default App; diff --git a/client/src/__test__/components/AlertError.test.tsx b/client/src/__test__/components/AlertError.test.tsx index 02198203..c70d9c1f 100644 --- a/client/src/__test__/components/AlertError.test.tsx +++ b/client/src/__test__/components/AlertError.test.tsx @@ -7,7 +7,7 @@ describe('AlertError Component tests', () => { const dataTestId: string = "alert-error-msg-tests"; const renderAlert = (message?: string) => { return render( - + ); }; diff --git a/client/src/components/AlertError/index.tsx b/client/src/components/AlertError/index.tsx index 9e87ab7d..1510ebdf 100644 --- a/client/src/components/AlertError/index.tsx +++ b/client/src/components/AlertError/index.tsx @@ -3,7 +3,7 @@ import { Alert } from 'react-bootstrap'; type Props = { errorMessage?: string; - dataTestid?: string; + dataTestId?: string; onClose?: () => void; }; @@ -12,7 +12,7 @@ type Props = { * * @param {Props} props the AlertError props with the message to be displayed. * @param {string} [props.errorMessage] Optional error message. - * @param {string} [props.dataTestid] Optional data-testid property. + * @param {string} [props.dataTestId] Optional data-testId property. * @param {Function} [props.onClose] OnClose function to be called. * @returns {React.ReactNode} the AlertError rendered component. */ @@ -25,7 +25,7 @@ const AlertError: React.FC = (props: Props): React.ReactNode => { { props.errorMessage } diff --git a/client/src/components/FormInput/index.tsx b/client/src/components/FormInput/index.tsx index d66e316d..56599fbf 100644 --- a/client/src/components/FormInput/index.tsx +++ b/client/src/components/FormInput/index.tsx @@ -20,7 +20,7 @@ interface Props { valueDate?: Date | null; onChange?: (e: React.ChangeEvent) => void; onChangeDate?: (date: Date | null) => void; - data_testid?: string; + dataTestId?: string; pwdHideText?: string; pwdShowText?: string; pwdHelperTxt?: string; @@ -107,7 +107,7 @@ function FormInput(props: React.PropsWithChildren): React.ReactNode { placeholder={props.placeholder ? props.placeholder : ''} value={props?.value} onChange={props.onChange} - data-testid={props.data_testid} + data-testid={props.dataTestId} /> )} diff --git a/client/src/components/LoginForm/index.tsx b/client/src/components/LoginForm/index.tsx index e2c852a6..9eb7cd99 100644 --- a/client/src/components/LoginForm/index.tsx +++ b/client/src/components/LoginForm/index.tsx @@ -54,16 +54,16 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { /** * Handles the form submit button click event. * - * When clicked, checks if the form is valid. It it's not, raise an error - * message in an Alert. Otherwise, it call the signIn API and navigates to the home page. + * When clicked, checks if the form is valid. If it's not, raise an error + * message in an Alert. Otherwise, it calls the signIn API and navigates to the home page. */ - const handleSubmit = async (event: React.FormEvent): Promise => { + const handleSubmit = async (event: React.SubmitEvent): Promise => { event.preventDefault(); event.stopPropagation(); setValidated(true); const form = event.currentTarget; - if (form.checkValidity() === false) { + if (!form.checkValidity()) { setFormInvalid(true); if (prefix === 'complete_reset') { setErrorMessage(translateServerResponse('Please fill in the new password', i18n.language)); @@ -110,7 +110,6 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { } } catch (e) { - console.log(e); setFormInvalid(true); let msg = ''; if (typeof e === 'string') { @@ -118,7 +117,6 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { msg = e; } else if (e instanceof Error) { - console.log(e.message); msg = e.message; } @@ -175,8 +173,8 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { {formInvalid ? ( - - { errorMessage } + <> + {errorMessage} {showResend && (
)} -
+ ) : null} @@ -206,7 +206,9 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { onClick={handleResend} disabled={!isResendEnabled} > - {isResendEnabled ? `${t('register_resent_email')}` : `${t('register_resent_email_secs')} ${secondsLeft}`} + {isResendEnabled + ? `${t('register_resent_email')}` + : `${t('register_resent_email_secs')} ${secondsLeft}`} )} @@ -221,7 +223,7 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { required={true} name="email" placeholder={t(`${prefix}_email_placeholder`)} - data_testid={`${prefix}_email_input`} + dataTestId={`${prefix}_email_input`} value={email} onChange={(e: React.ChangeEvent) => { setEmail(e.target.value); @@ -244,7 +246,7 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { onChange={(e: React.ChangeEvent) => { setPassword(e.target.value); }} - data_testid="account-password-login" + dataTestId="account-password-login" /> )} @@ -263,7 +265,7 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { onChange={(e: React.ChangeEvent) => { setPasswordAgain(e.target.value); }} - data_testid={`account-repeat-password-${prefix}`} + dataTestId={`account-repeat-password-${prefix}`} /> )} @@ -278,7 +280,10 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode {
{t(`${prefix}_account`)} - + {t(`${prefix}_go_other`)}
@@ -290,7 +295,10 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode {
{prefix === 'login' && ( - + {t('login_forgot_password')} )} diff --git a/client/src/context/AuthProvider.tsx b/client/src/context/AuthProvider.tsx index fa92bbb0..afb36248 100644 --- a/client/src/context/AuthProvider.tsx +++ b/client/src/context/AuthProvider.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import AuthContext, { AuthContextData } from './AuthContext'; import { API_TOKEN, REDIRECT_PATH, USER_DATA } from '../app-constants/app-constants'; -import { SigninResponse } from '../types/SigninResponse'; +import { SignInResponse } from '../types/SigninResponse'; import api from '../api-service/api'; import ApiConfig from '../api-service/apiConfig'; import { UserResponse } from '../types/UserResponse'; @@ -17,13 +17,13 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro const [user, setUser] = useState(); const [isAdmin, setIsAdmin] = useState(false); - const fetchCurrentSession = async (pathname: string): Promise => { + const fetchCurrentSession = async (pathname: string): Promise => { const token = localStorage.getItem(API_TOKEN); if (!token) { return undefined; } try { - const bearerToken: SigninResponse = await api.getJSON(ApiConfig.refreshTokenUrl); + const bearerToken: SignInResponse = await api.getJSON(ApiConfig.refreshTokenUrl); setSigned(true); return bearerToken; } @@ -64,7 +64,7 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro }; const checkCurrentAuthUser = async (pathname: string): Promise => { - const bearerToken: SigninResponse | undefined = await fetchCurrentSession(pathname); + const bearerToken: SignInResponse | undefined = await fetchCurrentSession(pathname); if (bearerToken && bearerToken.token) { const userLocal = updateUserSession(null, bearerToken.token); if (userLocal) { @@ -89,7 +89,7 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro const signIn = async (email: string, password: string): Promise => { try { const payload = { email, password }; - const registerResponse: SigninResponse = await api.postJSON(ApiConfig.signInUrl, payload); + const registerResponse: SignInResponse = await api.postJSON(ApiConfig.signInUrl, payload); const currentUser: UserResponse = { userId: registerResponse.userId, name: registerResponse.name, diff --git a/client/src/routes/ProtectedRoute/index.tsx b/client/src/routes/ProtectedRoute/index.tsx index af5f3f1d..4f90bc7f 100644 --- a/client/src/routes/ProtectedRoute/index.tsx +++ b/client/src/routes/ProtectedRoute/index.tsx @@ -19,6 +19,6 @@ function ProtectedRoute(): React.ReactNode { return ; } return ; -}; +} export default ProtectedRoute; diff --git a/client/src/types/SigninResponse.ts b/client/src/types/SigninResponse.ts index 5efb5bcc..2a36bda9 100644 --- a/client/src/types/SigninResponse.ts +++ b/client/src/types/SigninResponse.ts @@ -1,4 +1,4 @@ -export type SigninResponse = { +export type SignInResponse = { userId: number; name: string; email: string; diff --git a/client/src/types/SummaryResponse.ts b/client/src/types/SummaryResponse.ts deleted file mode 100644 index 8bbd378f..00000000 --- a/client/src/types/SummaryResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type SummaryResponse = { - pendingTaskCount: number; - doneTaskCount: number; - notesCount: number; -}; diff --git a/client/src/views/Account/index.tsx b/client/src/views/Account/index.tsx index 1a4dcfdd..8c42a896 100644 --- a/client/src/views/Account/index.tsx +++ b/client/src/views/Account/index.tsx @@ -105,7 +105,7 @@ function Account(): React.ReactNode { await patchUserInfo(patchPayload); }; - const handleSubmit = async (event?: React.FormEvent): Promise => { + const handleSubmit = async (event?: React.SubmitEvent): Promise => { event?.preventDefault(); event?.stopPropagation(); setValidated(true); @@ -164,7 +164,7 @@ function Account(): React.ReactNode { /> - {/* User name */} + {/* Username */} ) => { setUserPassword(e.target.value); }} - data_testid="account-password-one" + dataTestId="account-password-one" /> {/* User password again */} diff --git a/client/src/views/EmailConfirmation/index.tsx b/client/src/views/EmailConfirmation/index.tsx index 3e81a95a..8f2b57e2 100644 --- a/client/src/views/EmailConfirmation/index.tsx +++ b/client/src/views/EmailConfirmation/index.tsx @@ -74,7 +74,7 @@ const EmailConfirmation: React.FC = () => {

{t('email_confirmation_success')}

{t('email_confirmation_success_text')}

- + {t('email_confirmation_btn')} diff --git a/client/src/views/Home/index.tsx b/client/src/views/Home/index.tsx index cb7c2c20..8623324e 100644 --- a/client/src/views/Home/index.tsx +++ b/client/src/views/Home/index.tsx @@ -82,7 +82,7 @@ function Home(): React.ReactNode { const markAsDone = async (task: TaskResponse): Promise => { try { await api.deleteNoContent(`${ApiConfig.tasksUrl}/${task.id}`); - loadAllTasks(); + await loadAllTasks(); } catch (e) { handleError(e); @@ -97,7 +97,7 @@ function Home(): React.ReactNode { const deleteNote = async (noteIdParam: number) => { try { await api.deleteNoContent(`${ApiConfig.notesUrl}/${noteIdParam}`); - loadAllNotes(); + await loadAllNotes(); } catch (e) { handleError(e); @@ -113,7 +113,7 @@ function Home(): React.ReactNode { try { const action = note.shared ? 'unshare' : 'share'; await api.putJSON(`${ApiConfig.notesUrl}/${note.id}/${action}`, {}); - loadAllNotes(); + await loadAllNotes(); } catch (e) { handleError(e); @@ -176,10 +176,9 @@ function Home(): React.ReactNode { } else { let filteredTasks = allTasks.filter((task: TaskResponse) => { - const shouldFilter = task.description.toLowerCase().includes(text.toLowerCase()) + return task.description.toLowerCase().includes(text.toLowerCase()) || task.tag.toLowerCase().includes(text.toLowerCase()) || task.urls.filter((url: string) => url.includes(text.toLowerCase())).length > 0; - return shouldFilter; }); if (tagToFilter === 'untagged') { diff --git a/client/src/views/Landing/index.tsx b/client/src/views/Landing/index.tsx index d7ed0497..0cc8df80 100644 --- a/client/src/views/Landing/index.tsx +++ b/client/src/views/Landing/index.tsx @@ -12,7 +12,7 @@ import './styles.scss'; * Landing page component. * * This component displays the landing page of the application, - * providing navigation to login or register. + * providing navigation to log in or register. * It also checks for an existing user session. * * @returns {React.ReactNode} The Landing page component. diff --git a/client/src/views/NoteAdd/index.tsx b/client/src/views/NoteAdd/index.tsx index 22af5e76..5299c0da 100644 --- a/client/src/views/NoteAdd/index.tsx +++ b/client/src/views/NoteAdd/index.tsx @@ -122,13 +122,13 @@ function NoteAdd(): React.ReactNode { * * @param {React.FormEvent} event - The form submission event. */ - const handleSubmit = async (event: React.FormEvent): Promise => { + const handleSubmit = async (event: React.SubmitEvent): Promise => { event.preventDefault(); event.stopPropagation(); setValidated(true); const form = event.currentTarget; - if (form.checkValidity() === false) { + if (!form.checkValidity()) { setErrorMessage(translateServerResponse('Please fill in all the fields', i18n.language)); return; } diff --git a/client/src/views/Register/index.tsx b/client/src/views/Register/index.tsx index bdc60d1a..944113d3 100644 --- a/client/src/views/Register/index.tsx +++ b/client/src/views/Register/index.tsx @@ -5,7 +5,7 @@ import LoginForm from '../../components/LoginForm'; * Register page component. * * This component displays the register page of the application, - * providing navigation to login or back to home. + * providing navigation to log in or back to home. * * @returns {React.ReactNode} The Register page component. */ diff --git a/client/src/views/TaskAdd/index.tsx b/client/src/views/TaskAdd/index.tsx index 0b522f81..633b89bf 100644 --- a/client/src/views/TaskAdd/index.tsx +++ b/client/src/views/TaskAdd/index.tsx @@ -125,13 +125,13 @@ function TaskAdd(): React.ReactNode { * * @param {React.FormEvent} event - The form submission event. */ - const handleSubmit = async (event: React.FormEvent): Promise => { + const handleSubmit = async (event: React.SubmitEvent): Promise => { event.preventDefault(); event.stopPropagation(); setValidated(true); const form = event.currentTarget; - if (form.checkValidity() === false) { + if (!form.checkValidity()) { setErrorMessage(translateServerResponse('Please fill in all the fields', i18n.language)); return; } diff --git a/server/src/main/java/br/com/tasknoteapp/server/config/CorsConfig.java b/server/src/main/java/br/com/tasknoteapp/server/config/CorsConfig.java index 019c7e1c..989b3caa 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/config/CorsConfig.java +++ b/server/src/main/java/br/com/tasknoteapp/server/config/CorsConfig.java @@ -1,6 +1,7 @@ package br.com.tasknoteapp.server.config; import java.util.Arrays; +import org.jspecify.annotations.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -22,7 +23,7 @@ public class CorsConfig implements WebMvcConfigurer { * * @param registry CorsRegistry instance. */ - public void addCorsMappings(CorsRegistry registry) { + public void addCorsMappings(@NonNull CorsRegistry registry) { if (allowedOrigins != null && allowedOrigins.length > 0) { logger.info("CORS policy allowed origins: {}", Arrays.asList(allowedOrigins)); diff --git a/server/src/main/java/br/com/tasknoteapp/server/config/SecurityConfig.java b/server/src/main/java/br/com/tasknoteapp/server/config/SecurityConfig.java index ba7f707c..a838c13e 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/config/SecurityConfig.java +++ b/server/src/main/java/br/com/tasknoteapp/server/config/SecurityConfig.java @@ -39,10 +39,9 @@ public SecurityConfig(UserService userService, JwtAuthenticationFilter jwtAuthen * * @param http instance of HttpSecurity containing the request. * @return SecurityFilterChain with allowed endpoints and all configuration. - * @throws Exception due to bad configuration possibilities. */ @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain filterChain(HttpSecurity http) { http.cors(Customizer.withDefaults()) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests( @@ -99,11 +98,9 @@ public AuthenticationProvider authenticationProvider() { * * @param config AuthenticationConfiguration instance * @return AuthenticationManager instance - * @throws Exception if issues occur. */ @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration config) - throws Exception { + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) { return config.getAuthenticationManager(); } } diff --git a/server/src/main/java/br/com/tasknoteapp/server/controller/HealthController.java b/server/src/main/java/br/com/tasknoteapp/server/controller/HealthController.java index a0c610dd..5ac40d30 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/controller/HealthController.java +++ b/server/src/main/java/br/com/tasknoteapp/server/controller/HealthController.java @@ -5,7 +5,6 @@ import java.util.HashMap; import java.util.Map; import javax.sql.DataSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -15,9 +14,14 @@ @RestController public class HealthController { - @Autowired private DataSource dataSource; + private final DataSource dataSource; - @Autowired private AppVersionService appVersionService; + private final AppVersionService appVersionService; + + HealthController(DataSource dataSource, AppVersionService appVersionService) { + this.dataSource = dataSource; + this.appVersionService = appVersionService; + } /** * Endpoint to check the health of the application and its database connection. diff --git a/server/src/main/java/br/com/tasknoteapp/server/controller/NoteController.java b/server/src/main/java/br/com/tasknoteapp/server/controller/NoteController.java index 2dc772fc..d861d3a7 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/controller/NoteController.java +++ b/server/src/main/java/br/com/tasknoteapp/server/controller/NoteController.java @@ -33,7 +33,7 @@ public NoteController(NoteService noteService) { /** * Get all notes. * - * @return List of NoteResponse with all found notes and its urls, if any. + * @return List of NoteResponse with all found notes and its URLs, if any. */ @GetMapping public List getAllNotes() { @@ -44,7 +44,7 @@ public List getAllNotes() { * Get a note by its ID. * * @param id Note identification. - * @return NoteResponse with note data and its urls, if any. + * @return NoteResponse with note data and its URLs, if any. * @throws NoteNotFoundException when note not found. */ @GetMapping("/{id}") @@ -56,7 +56,7 @@ public NoteResponse getTaskById(@PathVariable Long id) { * Patch a note. * * @param id The note id to be patched. - * @param noteRequest Note data to be patched, including optionally its urls. + * @param noteRequest Note data to be patched, including optionally its URLs. * @return NoteResponse containing data that was updated. * @throws NoteNotFoundException when note not found. */ @@ -70,8 +70,8 @@ public ResponseEntity patchNote( /** * Create a note. * - * @param noteRequest Note data to be created, including optionally its urls. Following RESTful - * API pattern from https://restfulapi.net/rest-put-vs-post/. + * @param noteRequest Note data to be created, including optionally its URLs. Following RESTful + * API pattern from docs. * @return NoteResponse containing data that was created. */ @PostMapping diff --git a/server/src/main/java/br/com/tasknoteapp/server/controller/RestExceptionController.java b/server/src/main/java/br/com/tasknoteapp/server/controller/RestExceptionController.java index 6403a8e4..85a539e0 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/controller/RestExceptionController.java +++ b/server/src/main/java/br/com/tasknoteapp/server/controller/RestExceptionController.java @@ -4,6 +4,7 @@ import br.com.tasknoteapp.server.exception.BaseNotFoundException; import br.com.tasknoteapp.server.exception.BaseServiceUnavailableException; import br.com.tasknoteapp.server.exception.EmailAlreadyExistsException; +import br.com.tasknoteapp.server.exception.EmailNotConfirmedException; import br.com.tasknoteapp.server.exception.InvalidCredentialsException; import br.com.tasknoteapp.server.exception.UserForbiddenException; import br.com.tasknoteapp.server.response.ValidationExceptionResponse; @@ -47,6 +48,13 @@ ResponseEntity handleUserForbiddenException( .body(new ValidationExceptionResponse("access", ex.getReason())); } + @ExceptionHandler(EmailNotConfirmedException.class) + ResponseEntity handleEmailNotConfirmedException( + EmailNotConfirmedException ex) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(new ValidationExceptionResponse("access", ex.getReason())); + } + /* 404 - Not Found */ @ExceptionHandler(BaseNotFoundException.class) ResponseEntity handleNotFoundException(BaseNotFoundException ex) { diff --git a/server/src/main/java/br/com/tasknoteapp/server/controller/TaskController.java b/server/src/main/java/br/com/tasknoteapp/server/controller/TaskController.java index 94d8daf1..a9729e7a 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/controller/TaskController.java +++ b/server/src/main/java/br/com/tasknoteapp/server/controller/TaskController.java @@ -32,7 +32,7 @@ public TaskController(TaskService taskService) { /** * Get all tasks. * - * @return List of TaskResponse with all found tasks and its urls, if any. + * @return List of TaskResponse with all found tasks and its URLs, if any. */ @GetMapping public List getAllTasks() { @@ -43,7 +43,7 @@ public List getAllTasks() { * Get a task by its ID. * * @param id Task identification. - * @return TaskResponse with task data and its urls, if any. + * @return TaskResponse with task data and its URLs, if any. * @throws TaskNotFoundException when task not found. */ @GetMapping("/{id}") @@ -55,7 +55,7 @@ public TaskResponse getTaskById(@PathVariable Long id) { * Patch a task. * * @param id The task id to be patched. - * @param taskRequest Task data to be patched, including optionally its urls. + * @param taskRequest Task data to be patched, including optionally its URLs. * @return TaskResponse containing data that was updated. * @throws TaskNotFoundException when task not found. */ @@ -68,8 +68,8 @@ public ResponseEntity patchTask( /** * Create a task. * - * @param taskRequest Task data to be created, including optionally its urls. Following RESTful - * API pattern from https://restfulapi.net/rest-put-vs-post/. + * @param taskRequest Task data to be created, including optionally its URLs. Following RESTful + * API pattern from docs. * @return TaskResponse containing data that was created. */ @PostMapping diff --git a/server/src/main/java/br/com/tasknoteapp/server/controller/UserSessionController.java b/server/src/main/java/br/com/tasknoteapp/server/controller/UserSessionController.java index b6c8f54e..81d934fa 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/controller/UserSessionController.java +++ b/server/src/main/java/br/com/tasknoteapp/server/controller/UserSessionController.java @@ -35,7 +35,7 @@ public JwtAuthenticationResponse refresh() { /** * Delete all the user data and information from the server. * - * @returns {@link UserResponse} with the user information. + * @return {@link UserResponse} with the user information. */ @DeleteMapping("/delete-account") public ResponseEntity deleteAccount() { diff --git a/server/src/main/java/br/com/tasknoteapp/server/entity/NoteEntity.java b/server/src/main/java/br/com/tasknoteapp/server/entity/NoteEntity.java index dd3d21b7..f9576697 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/entity/NoteEntity.java +++ b/server/src/main/java/br/com/tasknoteapp/server/entity/NoteEntity.java @@ -29,7 +29,7 @@ public class NoteEntity { @ManyToOne(fetch = FetchType.LAZY) private UserEntity user; - @Column(name = "tag", nullable = true, length = 30) + @Column(name = "tag", length = 30) private String tag; @Column(name = "last_update") diff --git a/server/src/main/java/br/com/tasknoteapp/server/entity/TaskEntity.java b/server/src/main/java/br/com/tasknoteapp/server/entity/TaskEntity.java index 4c972aed..312ccd57 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/entity/TaskEntity.java +++ b/server/src/main/java/br/com/tasknoteapp/server/entity/TaskEntity.java @@ -39,7 +39,7 @@ public class TaskEntity { @Column(name = "high_priority") private Boolean highPriority; - @Column(name = "tag", nullable = true, length = 30) + @Column(name = "tag", length = 30) private String tag; public Long getId() { diff --git a/server/src/main/java/br/com/tasknoteapp/server/entity/TaskUrlEntityPk.java b/server/src/main/java/br/com/tasknoteapp/server/entity/TaskUrlEntityPk.java index 01ca3d89..a3547af6 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/entity/TaskUrlEntityPk.java +++ b/server/src/main/java/br/com/tasknoteapp/server/entity/TaskUrlEntityPk.java @@ -17,14 +17,6 @@ public TaskUrlEntityPk(Long taskId, String url) { this.url = url; } - public Long getTaskId() { - return taskId; - } - - public void setTaskId(Long taskId) { - this.taskId = taskId; - } - public String getUrl() { return url; } diff --git a/server/src/main/java/br/com/tasknoteapp/server/entity/UserEntity.java b/server/src/main/java/br/com/tasknoteapp/server/entity/UserEntity.java index 1774b768..b942d7e5 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/entity/UserEntity.java +++ b/server/src/main/java/br/com/tasknoteapp/server/entity/UserEntity.java @@ -34,25 +34,25 @@ public class UserEntity implements UserDetails { @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; - @Column(name = "inactivated_at", nullable = true) + @Column(name = "inactivated_at") private LocalDateTime inactivatedAt; @Column(name = "name", length = 20) private String name; - @Column(name = "email_confirmed_at", nullable = true) + @Column(name = "email_confirmed_at") private LocalDateTime emailConfirmedAt; - @Column(name = "email_uuid", columnDefinition = "uuid", nullable = true, unique = true) + @Column(name = "email_uuid", columnDefinition = "uuid", unique = true) private UUID emailUuid; - @Column(name = "reset_password_expiration", nullable = true) + @Column(name = "reset_password_expiration") private LocalDateTime resetPasswordExpiration; - @Column(name = "reset_token", nullable = true, length = 35) + @Column(name = "reset_token", length = 35) private String resetToken; - @Column(name = "lang", nullable = true, length = 6) + @Column(name = "lang", length = 6) private String lang; @Column(name = "last_password_change", nullable = false) @@ -69,26 +69,6 @@ public String getUsername() { return email; } - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } - public Long getId() { return id; } diff --git a/server/src/main/java/br/com/tasknoteapp/server/exception/MailServiceException.java b/server/src/main/java/br/com/tasknoteapp/server/exception/MailServiceException.java deleted file mode 100644 index 359ca612..00000000 --- a/server/src/main/java/br/com/tasknoteapp/server/exception/MailServiceException.java +++ /dev/null @@ -1,9 +0,0 @@ -package br.com.tasknoteapp.server.exception; - -/** This exception represents an error when sending email messages. */ -public class MailServiceException extends BaseServiceUnavailableException { - - public MailServiceException(String error) { - super("mail", error); - } -} diff --git a/server/src/main/java/br/com/tasknoteapp/server/filter/HeaderVersionFilter.java b/server/src/main/java/br/com/tasknoteapp/server/filter/HeaderVersionFilter.java index da6dae66..279d3d1e 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/filter/HeaderVersionFilter.java +++ b/server/src/main/java/br/com/tasknoteapp/server/filter/HeaderVersionFilter.java @@ -6,7 +6,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import org.springframework.beans.factory.annotation.Autowired; +import org.jspecify.annotations.NonNull; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @@ -14,11 +14,15 @@ @Component public class HeaderVersionFilter extends OncePerRequestFilter { - @Autowired private AppVersionService appVersionService; + private final AppVersionService appVersionService; + + HeaderVersionFilter(AppVersionService appVersionService) { + this.appVersionService = appVersionService; + } @Override protected void doFilterInternal( - HttpServletRequest request, + @NonNull HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { diff --git a/server/src/main/java/br/com/tasknoteapp/server/filter/JwtAuthenticationFilter.java b/server/src/main/java/br/com/tasknoteapp/server/filter/JwtAuthenticationFilter.java index c949c84a..38931535 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/filter/JwtAuthenticationFilter.java +++ b/server/src/main/java/br/com/tasknoteapp/server/filter/JwtAuthenticationFilter.java @@ -8,7 +8,7 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Objects; -import org.springframework.beans.factory.annotation.Autowired; +import org.jspecify.annotations.NonNull; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -21,15 +21,20 @@ @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { - @Autowired private UserService userService; + private final UserService userService; - @Autowired private JwtService jwtService; + private final JwtService jwtService; + + JwtAuthenticationFilter(UserService userService, JwtService jwtService) { + this.userService = userService; + this.jwtService = jwtService; + } @Override protected void doFilterInternal( HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { final String authorizationHeader = request.getHeader("Authorization"); diff --git a/server/src/main/java/br/com/tasknoteapp/server/hint/HttpServletRequestRuntimeHint.java b/server/src/main/java/br/com/tasknoteapp/server/hint/HttpServletRequestRuntimeHint.java index 107132e4..0c53227a 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/hint/HttpServletRequestRuntimeHint.java +++ b/server/src/main/java/br/com/tasknoteapp/server/hint/HttpServletRequestRuntimeHint.java @@ -1,6 +1,7 @@ package br.com.tasknoteapp.server.hint; import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.NonNull; import org.springframework.aot.hint.ProxyHints; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -9,7 +10,7 @@ public class HttpServletRequestRuntimeHint implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(@NonNull RuntimeHints hints, ClassLoader classLoader) { try { ProxyHints proxies = hints.proxies(); proxies.registerJdkProxy(HttpServletRequest.class); diff --git a/server/src/main/java/br/com/tasknoteapp/server/repository/UserPwdLimitRepository.java b/server/src/main/java/br/com/tasknoteapp/server/repository/UserPwdLimitRepository.java index 3a8a28d7..5d596b10 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/repository/UserPwdLimitRepository.java +++ b/server/src/main/java/br/com/tasknoteapp/server/repository/UserPwdLimitRepository.java @@ -2,7 +2,6 @@ import br.com.tasknoteapp.server.entity.UserPwdLimitEntity; import java.util.List; -import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -10,8 +9,6 @@ /** This interface represents a user password limit repository, for database access. */ public interface UserPwdLimitRepository extends JpaRepository { - List findAllByUser_id(Long userId, Sort sort); - List findTop3ByUser_idOrderByWhenHappenedDesc(Long userId); @Modifying diff --git a/server/src/main/java/br/com/tasknoteapp/server/request/LoginRequest.java b/server/src/main/java/br/com/tasknoteapp/server/request/LoginRequest.java index 84534d55..e6e0ead2 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/request/LoginRequest.java +++ b/server/src/main/java/br/com/tasknoteapp/server/request/LoginRequest.java @@ -10,11 +10,9 @@ public class LoginRequest { @NotNull private String password; - private String passwordAgain; + private final String passwordAgain; - private String lang; - - public LoginRequest() {} + private final String lang; /** * Constructs a LoginRequest with the specified email and password. @@ -51,18 +49,10 @@ public String passwordAgain() { return passwordAgain; } - public void setPasswordAgain(String passwordAgain) { - this.passwordAgain = passwordAgain; - } - public String lang() { return lang; } - public void setLang(String lang) { - this.lang = lang; - } - @Override public boolean equals(Object o) { if (this == o) { diff --git a/server/src/main/java/br/com/tasknoteapp/server/request/NotePatchRequest.java b/server/src/main/java/br/com/tasknoteapp/server/request/NotePatchRequest.java index 222ee171..26cffb25 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/request/NotePatchRequest.java +++ b/server/src/main/java/br/com/tasknoteapp/server/request/NotePatchRequest.java @@ -8,6 +8,6 @@ public record NotePatchRequest( String description, @Pattern( regexp = "^(https?://.*|#.*)?$", - message = "URL must start with http://, https:// or #") + message = "URL must start with https:// or #") String url, String tag) {} diff --git a/server/src/main/java/br/com/tasknoteapp/server/request/NoteRequest.java b/server/src/main/java/br/com/tasknoteapp/server/request/NoteRequest.java index acf0c1c7..bd78f95d 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/request/NoteRequest.java +++ b/server/src/main/java/br/com/tasknoteapp/server/request/NoteRequest.java @@ -9,6 +9,6 @@ public record NoteRequest( @NotNull String description, @Pattern( regexp = "^(https?://.*|#.*)?$", - message = "URL must start with http://, https:// or #") + message = "URL must start with https:// or #") String url, String tag) {} diff --git a/server/src/main/java/br/com/tasknoteapp/server/request/NoteUrlPatchRequest.java b/server/src/main/java/br/com/tasknoteapp/server/request/NoteUrlPatchRequest.java deleted file mode 100644 index bc0b7870..00000000 --- a/server/src/main/java/br/com/tasknoteapp/server/request/NoteUrlPatchRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package br.com.tasknoteapp.server.request; - -/** This record represents a Note Url payload to be patched. */ -public record NoteUrlPatchRequest(Long id, String url) {} diff --git a/server/src/main/java/br/com/tasknoteapp/server/request/ResendConfirmationRequest.java b/server/src/main/java/br/com/tasknoteapp/server/request/ResendConfirmationRequest.java index 22dab706..651b2030 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/request/ResendConfirmationRequest.java +++ b/server/src/main/java/br/com/tasknoteapp/server/request/ResendConfirmationRequest.java @@ -8,12 +8,6 @@ public class ResendConfirmationRequest { @Email @NotNull private String email; - public ResendConfirmationRequest() {} - - public ResendConfirmationRequest(String email) { - this.email = email; - } - public String email() { return email.trim().toLowerCase(); } diff --git a/server/src/main/java/br/com/tasknoteapp/server/request/TaskPatchRequest.java b/server/src/main/java/br/com/tasknoteapp/server/request/TaskPatchRequest.java index ff5b5b74..12f977c6 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/request/TaskPatchRequest.java +++ b/server/src/main/java/br/com/tasknoteapp/server/request/TaskPatchRequest.java @@ -10,7 +10,7 @@ public record TaskPatchRequest( List< @Pattern( regexp = "^(https?://.*|#.*)?$", - message = "URL must start with http://, https:// or #") + message = "URL must start with https:// or #") String> urls, String dueDate, diff --git a/server/src/main/java/br/com/tasknoteapp/server/request/TaskRequest.java b/server/src/main/java/br/com/tasknoteapp/server/request/TaskRequest.java index 854adb94..bb277968 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/request/TaskRequest.java +++ b/server/src/main/java/br/com/tasknoteapp/server/request/TaskRequest.java @@ -11,7 +11,7 @@ public record TaskRequest( List< @Pattern( regexp = "^(https?://.*|#.*)?$", - message = "URL must start with http://, https:// or #") + message = "URL must start with https:// or #") String> urls, String dueDate, diff --git a/server/src/main/java/br/com/tasknoteapp/server/request/TaskUrlPatchRequest.java b/server/src/main/java/br/com/tasknoteapp/server/request/TaskUrlPatchRequest.java deleted file mode 100644 index 9ac1e128..00000000 --- a/server/src/main/java/br/com/tasknoteapp/server/request/TaskUrlPatchRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package br.com.tasknoteapp.server.request; - -/** This record represents a Task Url payload to be patched. */ -public record TaskUrlPatchRequest(Long id, String url) {} diff --git a/server/src/main/java/br/com/tasknoteapp/server/response/NoteResponse.java b/server/src/main/java/br/com/tasknoteapp/server/response/NoteResponse.java index 6f65ce0f..26db95be 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/response/NoteResponse.java +++ b/server/src/main/java/br/com/tasknoteapp/server/response/NoteResponse.java @@ -3,16 +3,16 @@ import br.com.tasknoteapp.server.entity.NoteEntity; import br.com.tasknoteapp.server.util.TimeAgoUtil; -/** This record represents a task and its urls object to be returned. */ +/** This record represents a task and its URLs object to be returned. */ public record NoteResponse( Long id, String title, String description, String url, String lastUpdate, String tag, boolean shared, String shareToken) { /** - * Creates a NoteResponse given a NoteEntity and its Urls. + * Creates a NoteResponse given a NoteEntity and its Urals. * * @param entity The NoteEntity source data. - * @return NoteResponse instance with all note data and urls, if any. + * @return NoteResponse instance with all note data and URLs, if any. */ public static NoteResponse fromEntity(NoteEntity entity, String url) { String timeAgoFmt = TimeAgoUtil.format(entity.getLastUpdate()); diff --git a/server/src/main/java/br/com/tasknoteapp/server/response/TaskResponse.java b/server/src/main/java/br/com/tasknoteapp/server/response/TaskResponse.java index 292c80bc..f82fbdc1 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/response/TaskResponse.java +++ b/server/src/main/java/br/com/tasknoteapp/server/response/TaskResponse.java @@ -18,10 +18,10 @@ public record TaskResponse( List urls) { /** - * Creates a TaskResponse given a TaskEntity and its Urls. + * Creates a TaskResponse given a TaskEntity and its URLs. * * @param entity The TaskEntity source data. - * @return TaskResponse instance with all task data and urls, if any. + * @return TaskResponse instance with all task data and URLs, if any. */ public static TaskResponse fromEntity(TaskEntity entity, List urls) { String timeAgoFmt = TimeAgoUtil.format(entity.getLastUpdate()); diff --git a/server/src/main/java/br/com/tasknoteapp/server/response/UserResponse.java b/server/src/main/java/br/com/tasknoteapp/server/response/UserResponse.java index 65d4b378..9f1d0711 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/response/UserResponse.java +++ b/server/src/main/java/br/com/tasknoteapp/server/response/UserResponse.java @@ -2,7 +2,6 @@ import br.com.tasknoteapp.server.entity.UserEntity; import java.time.LocalDateTime; -import java.util.Optional; /** This record represents a User Response object. */ public record UserResponse( @@ -20,7 +19,7 @@ public record UserResponse( * @param user The user entity instance with user info to be used as source. * @return UserResponse instance. */ - public static UserResponse fromEntity(UserEntity user, Optional gravatarUrl) { + public static UserResponse fromEntity(UserEntity user, String gravatarUrl) { return new UserResponse( user.getId(), user.getName(), @@ -28,6 +27,6 @@ public static UserResponse fromEntity(UserEntity user, Optional gravatar user.getAdmin(), user.getCreatedAt(), user.getInactivatedAt(), - gravatarUrl.orElse(null)); + gravatarUrl); } } diff --git a/server/src/main/java/br/com/tasknoteapp/server/response/UserResponseWithToken.java b/server/src/main/java/br/com/tasknoteapp/server/response/UserResponseWithToken.java index 012be6d7..5e28156a 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/response/UserResponseWithToken.java +++ b/server/src/main/java/br/com/tasknoteapp/server/response/UserResponseWithToken.java @@ -2,7 +2,6 @@ import br.com.tasknoteapp.server.entity.UserEntity; import java.time.LocalDateTime; -import java.util.Optional; /** This record represents a User Response object. */ public record UserResponseWithToken( @@ -24,7 +23,7 @@ public record UserResponseWithToken( * @return UserResponse instance. */ public static UserResponseWithToken fromEntity( - UserEntity user, String token, Optional gravatarUrl) { + UserEntity user, String token, String gravatarUrl) { return new UserResponseWithToken( user.getId(), user.getName(), @@ -32,7 +31,7 @@ public static UserResponseWithToken fromEntity( user.getAdmin(), user.getCreatedAt(), user.getInactivatedAt(), - gravatarUrl.orElse(null), + gravatarUrl, token, user.getLang()); } diff --git a/server/src/main/java/br/com/tasknoteapp/server/response/ValidationExceptionResponse.java b/server/src/main/java/br/com/tasknoteapp/server/response/ValidationExceptionResponse.java index 6615c5bc..624db44b 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/response/ValidationExceptionResponse.java +++ b/server/src/main/java/br/com/tasknoteapp/server/response/ValidationExceptionResponse.java @@ -29,12 +29,4 @@ public ValidationExceptionResponse(String field, String errorMessage) { this.fields = List.of(new FieldIssueResponse(field, errorMessage)); this.errorMessage = String.format(MESSAGE_TEMPLATE, fields.size()); } - - public String getErrorMessage() { - return errorMessage; - } - - public List getFields() { - return fields; - } } diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java b/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java index 1bd50946..237f3181 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java @@ -143,8 +143,9 @@ public UserResponseWithToken signUpNewUser(LoginRequest newUser) { mailgunEmailService.sendNewUser(user); } - logger.info("User created! ID " + user.getId()); - return UserResponseWithToken.fromEntity(user, null, getGravatarImageUrl(newUser.email())); + logger.info("User created! ID {}", user.getId()); + return UserResponseWithToken.fromEntity( + user, null, getGravatarImageUrl(newUser.email()).orElse(null)); } /** @@ -180,7 +181,7 @@ public User loadUserByUsername(String email) { */ @Transactional public UserResponseWithToken signInUser(LoginRequest login) { - logger.info("Signing in user: " + login.email()); + logger.info("Signing in user: {}", login.email()); Optional userOptional = findByEmail(login.email()); if (userOptional.isEmpty()) { @@ -205,11 +206,12 @@ public UserResponseWithToken signInUser(LoginRequest login) { String token = jwtService.generateToken(user); - logger.info("User authenticated! Token " + token); + logger.info("User authenticated! Token {}", token.substring(0, 6) + "..."); userPwdLimitRepository.deleteAllForUser(user.getId()); userRepository.save(user); - return UserResponseWithToken.fromEntity(user, token, getGravatarImageUrl(login.email())); + return UserResponseWithToken.fromEntity( + user, token, getGravatarImageUrl(login.email()).orElse(null)); } catch (BadCredentialsException e) { logger.error( "BadCredentialsException when logging in user {}: {}", user.getId(), e.getMessage()); @@ -253,7 +255,9 @@ public List getAllUsers() { List users = userRepository.findAll(); List usersResponse = new ArrayList<>(users.size()); users.forEach( - u -> usersResponse.add(UserResponse.fromEntity(u, getGravatarImageUrl(u.getEmail())))); + u -> + usersResponse.add( + UserResponse.fromEntity(u, getGravatarImageUrl(u.getEmail()).orElse(null)))); logger.info("{} user(s) found!", usersResponse.size()); return usersResponse; @@ -293,7 +297,7 @@ public UserResponse deleteUserAccount() { userPwdLimitRepository.deleteAllForUser(currentUser.getId()); userRepository.delete(currentUser); - return UserResponse.fromEntity(currentUser, getGravatarImageUrl(email)); + return UserResponse.fromEntity(currentUser, getGravatarImageUrl(email).orElse(null)); } /** @@ -345,17 +349,17 @@ public UserResponse patchUserInfo(UserPatchRequest patchRequest) { shouldUpdate = true; } - if (shouldUpdate && currentUser != null) { + if (shouldUpdate) { userRepository.save(currentUser); } if (emailChanged && hasValidMailgunApiKey()) { // send email to older and new account - logger.info("Email changed from " + email + " to " + patchRequest.email()); + logger.info("Email changed from {} to {}", email, patchRequest.email()); mailgunEmailService.sendEmailChangedNotification(currentUser, email); } - return UserResponse.fromEntity(currentUser, getGravatarImageUrl(email)); + return UserResponse.fromEntity(currentUser, getGravatarImageUrl(email).orElse(null)); } /** @@ -382,7 +386,7 @@ public Optional getCurrentUser() { @Transactional public void confirmUserAccount(String identification) { logger.info("Confirming user email account"); - UUID uuid = null; + UUID uuid; try { uuid = UUID.fromString(identification); @@ -399,11 +403,11 @@ public void confirmUserAccount(String identification) { user.setEmailConfirmedAt(LocalDateTime.now()); userRepository.save(user); - logger.info("User email address confirmed: " + identification); + logger.info("User email address confirmed: {}", identification); } /** - * Re-send the confirm email to the user. + * Re-send the confirmation email to the user. * * @param email The email to re-send. */ @@ -431,11 +435,11 @@ public void resendEmailConfirmation(String email) { */ @Transactional public void resetPasswordForUser(String email) { - logger.info("Requesting password reset for email " + email); + logger.info("Requesting password reset for email {}", email); Optional userOptional = userRepository.findByEmail(email); if (userOptional.isEmpty()) { - logger.info("No user found with this email " + email); + logger.info("No user found with email {}", email); return; } @@ -461,7 +465,7 @@ public void resetPasswordForUser(String email) { */ @Transactional public void confirmResetPasswordForUser(PasswordResetRequest request) { - logger.info("Saving new password for token " + request.token()); + logger.info("Saving new password for token {}", request.token()); Optional userOptional = userRepository.findByResetToken(request.token()); if (userOptional.isEmpty()) { @@ -495,12 +499,12 @@ public void confirmResetPasswordForUser(PasswordResetRequest request) { mailgunEmailService.sendPasswordResetConfirmation(user); } - logger.info("New password set for token " + request.token()); + logger.info("New password set for token {}", request.token()); } private Optional getGravatarImageUrl(String email) { email = email.toLowerCase().trim(); - logger.info("Current user email: " + email); + logger.info("Current user email: {}", email); try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); @@ -532,7 +536,7 @@ private void checkLoginAttemptLimit(Long userId) { // if it's more than 3 times in the last 10 minutes, raise timer of 3 hours. if (userPwdList.size() >= 3) { - UserPwdLimitEntity mostRecent = userPwdList.get(0); + UserPwdLimitEntity mostRecent = userPwdList.getFirst(); logger.warn("Oldest: {}", mostRecent.getWhenHappened()); Duration duration = Duration.between(mostRecent.getWhenHappened(), LocalDateTime.now()); if (duration.toMinutes() <= 3L) { diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/JwtService.java b/server/src/main/java/br/com/tasknoteapp/server/service/JwtService.java index 55881175..6c38e77d 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/JwtService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/JwtService.java @@ -8,15 +8,15 @@ /** This interface contains methods for handling user JWT tokens. */ public interface JwtService { - public String getEmailFromToken(String token); + String getEmailFromToken(String token); - public LocalDateTime extractExpiration(String token); + LocalDateTime extractExpiration(String token); - public String generateToken(UserEntity user); + String generateToken(UserEntity user); - public String createToken(Map claims, String email); + String createToken(Map claims, String email); - public boolean isTokenExpired(String token); + boolean isTokenExpired(String token); - public boolean validateTokenAndUser(String token, UserDetails user); + boolean validateTokenAndUser(String token, UserDetails user); } diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/MailgunEmailService.java b/server/src/main/java/br/com/tasknoteapp/server/service/MailgunEmailService.java index ae691bb8..83885601 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/MailgunEmailService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/MailgunEmailService.java @@ -26,7 +26,7 @@ public class MailgunEmailService { private final RestClient restClient; private final String targetEnv; private final String domain; - private String senderEmail; + private final String senderEmail; /** * Creates an instance of the Mail service class. @@ -56,16 +56,18 @@ public MailgunEmailService( logger.warn("Mailgun API Key is missing or too short!"); } - this.restClient = restClientBuilder - .baseUrl("https://api.mailgun.net/v3/" + domain) - .defaultStatusHandler(HttpStatusCode::isError, (request, response) -> { - logger.error( - "Mailgun API Error: {} {}", - response.getStatusCode(), - response.getStatusText()); - }) - .defaultHeaders(headers -> headers.setBasicAuth("api", apiKey)) - .build(); + this.restClient = + restClientBuilder + .baseUrl("https://api.mailgun.net/v3/" + domain) + .defaultStatusHandler( + HttpStatusCode::isError, + (request, response) -> + logger.error( + "Mailgun API Error: {} {}", + response.getStatusCode(), + response.getStatusText())) + .defaultHeaders(headers -> headers.setBasicAuth("api", apiKey)) + .build(); } /** @@ -148,7 +150,7 @@ public void sendEmailChangedNotification(UserEntity user, String oldEmail) { * * @param to The target email address. * @param subject The message subject. - * @param template The mailgun template. + * @param template The Mailgun template. */ private void sendEmail(String to, String subject, MailgunTemplate template) { String from = "TaskNote App <" + senderEmail + ">"; @@ -167,7 +169,8 @@ private void sendEmail(String to, String subject, MailgunTemplate template) { } try { - restClient.post() + restClient + .post() .uri("/messages") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .body(mailData) diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java b/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java index 941c10fc..0e056253 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java @@ -63,10 +63,10 @@ public NoteService( public List getAllNotes() { UserEntity user = getCurrentUser(); - logger.info("Get all notes to user " + user.getId()); + logger.info("Get all notes to user {}", user.getId()); List notes = noteRepository.findAllByUser_id(user.getId()); - logger.info(notes.size() + " notes found!"); + logger.info("{} notes found!", notes.size()); return getNotesUrl(notes); } @@ -79,7 +79,7 @@ public List getAllNotes() { */ public NoteResponse getNoteById(Long noteId) { UserEntity user = getCurrentUser(); - logger.info("Get note " + noteId + " to user " + user.getId()); + logger.info("Get note {} to user {}", noteId, user.getId()); Optional note = noteRepository.findById(noteId); if (note.isEmpty()) { @@ -90,7 +90,7 @@ public NoteResponse getNoteById(Long noteId) { throw new NoteNotFoundException(); } - logger.info("Note found! Id " + noteId); + logger.info("Note found! Id {}", noteId); return NoteResponse.fromEntity(note.get(), getNoteUrl(noteId)); } @@ -103,7 +103,7 @@ public NoteResponse getNoteById(Long noteId) { public NoteResponse createNote(NoteRequest noteRequest) { UserEntity user = getCurrentUser(); - logger.info("Creating note to user " + user.getId()); + logger.info("Creating note to user {}", user.getId()); NoteEntity note = new NoteEntity(); note.setTitle(noteRequest.title()); @@ -113,7 +113,7 @@ public NoteResponse createNote(NoteRequest noteRequest) { note.setUser(user); NoteEntity created = noteRepository.save(note); - logger.info("Note created! Id " + created.getId()); + logger.info("Note created! Id {}", created.getId()); String savedUrl = null; if (!Objects.isNull(noteRequest.url()) && !noteRequest.url().isEmpty()) { @@ -136,7 +136,7 @@ public NoteResponse createNote(NoteRequest noteRequest) { public NoteResponse patchNote(Long noteId, NotePatchRequest patch) { UserEntity user = getCurrentUser(); - logger.info("Patching task " + noteId + " to user " + user.getId()); + logger.info("Patching task {} to user {}", noteId, user.getId()); Optional note = noteRepository.findByIdAndUser_id(noteId, user.getId()); if (note.isEmpty()) { @@ -157,18 +157,18 @@ public NoteResponse patchNote(Long noteId, NotePatchRequest patch) { noteUrlRepository.deleteByNote_id(noteId); noteUrlRepository.flush(); - logger.info("URL deleted from task " + noteId); + logger.info("URL deleted from task {} during patch", noteId); if (!Objects.isNull(patch.url()) && !patch.url().isBlank()) { saveUrl(noteEntity, patch.url()); } else { - logger.info("No urls to patch for task " + noteId); + logger.info("No URLs to patch for task {}", noteId); } NoteEntity patchedNote = noteRepository.save(noteEntity); noteRepository.flush(); - logger.info("Note patched! Id " + patchedNote.getId()); + logger.info("Note patched! Id {}", patchedNote.getId()); return NoteResponse.fromEntity(patchedNote, getNoteUrl(patchedNote.getId())); } @@ -182,7 +182,7 @@ public NoteResponse patchNote(Long noteId, NotePatchRequest patch) { public void deleteNote(Long noteId) { UserEntity user = getCurrentUser(); - logger.info("Deleting note " + noteId + " to user " + user.getId()); + logger.info("Deleting note {} to user {}", noteId, user.getId()); Optional note = noteRepository.findByIdAndUser_id(noteId, user.getId()); if (note.isEmpty()) { @@ -192,11 +192,11 @@ public void deleteNote(Long noteId) { NoteEntity noteEntity = note.get(); noteUrlRepository.deleteByNote_id(noteId); - logger.info("URL deleted from task " + noteId); + logger.info("URL deleted from task {}", noteId); noteRepository.delete(noteEntity); - logger.info("Note deleted! Id " + noteId); + logger.info("Note deleted! Id {}", noteId); } /** @@ -208,11 +208,11 @@ public void deleteNote(Long noteId) { public List searchNotes(String searchTerm) { UserEntity user = getCurrentUser(); - logger.info("Searching notes to user " + user.getId()); + logger.info("Searching notes to user {}", user.getId()); List notes = noteRepository.findAllBySearchTerm(searchTerm.toUpperCase(), user.getId()); - logger.info(notes.size() + " tasks found!"); + logger.info("{} tasks found!", notes.size()); return getNotesUrl(notes); } @@ -225,7 +225,7 @@ public List searchNotes(String searchTerm) { @Transactional public NoteResponse shareNote(Long noteId) { UserEntity user = getCurrentUser(); - logger.info("Sharing note " + noteId + " for user " + user.getId()); + logger.info("Sharing note {} for user {}", noteId, user.getId()); Optional noteOpt = noteRepository.findByIdAndUser_id(noteId, user.getId()); if (noteOpt.isEmpty()) { @@ -238,7 +238,7 @@ public NoteResponse shareNote(Long noteId) { noteEntity.setShared(true); noteEntity.setShareToken(UUID.randomUUID().toString()); noteRepository.save(noteEntity); - logger.info("Note " + noteId + " shared with token " + noteEntity.getShareToken()); + logger.info("Note {} shared with token {}", noteId, noteEntity.getShareToken()); } return NoteResponse.fromEntity(noteEntity, getNoteUrl(noteEntity.getId())); @@ -252,7 +252,7 @@ public NoteResponse shareNote(Long noteId) { */ public NoteResponse unshareNote(Long noteId) { UserEntity user = getCurrentUser(); - logger.info("Unsharing note " + noteId + " for user " + user.getId()); + logger.info("Unsharing note {} for user {}", noteId, user.getId()); Optional noteOpt = noteRepository.findByIdAndUser_id(noteId, user.getId()); if (noteOpt.isEmpty()) { @@ -263,7 +263,7 @@ public NoteResponse unshareNote(Long noteId) { noteEntity.setShared(false); noteEntity.setShareToken(null); noteRepository.save(noteEntity); - logger.info("Note " + noteId + " unshared."); + logger.info("Note {} unshared", noteId); return NoteResponse.fromEntity(noteEntity, getNoteUrl(noteEntity.getId())); } @@ -275,7 +275,7 @@ public NoteResponse unshareNote(Long noteId) { * @return {@link NoteResponse} containing the shared note. */ public NoteResponse getSharedNote(String shareToken) { - logger.info("Fetching shared note with token " + shareToken); + logger.info("Fetching shared note with token {}", shareToken); Optional noteOpt = noteRepository.findByShareToken(shareToken); if (noteOpt.isEmpty() || !noteOpt.get().isShared()) { @@ -313,7 +313,7 @@ private NoteUrlEntity saveUrl(NoteEntity noteEntity, String url) { noteUrl.setNote(noteEntity); NoteUrlEntity savedUrl = noteUrlRepository.save(noteUrl); - logger.info("URL saved to note " + noteEntity.getId()); + logger.info("URL saved to note {}", noteEntity.getId()); return savedUrl; } diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/TaskService.java b/server/src/main/java/br/com/tasknoteapp/server/service/TaskService.java index fbd18db9..6375616b 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/TaskService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/TaskService.java @@ -63,10 +63,10 @@ public TaskService( */ public List getAllTasks() { UserEntity user = getCurrentUser(); - logger.info("Get all tasks to user " + user.getId()); + logger.info("Get all tasks to user ID {}", user.getId()); List tasks = taskRepository.findAllByUser_id(user.getId()); - logger.info(tasks.size() + " tasks found!"); + logger.info("{} tasks found in getAllTasks!", tasks.size()); return tasks.stream() .map((TaskEntity tr) -> TaskResponse.fromEntity(tr, getAllTasksUrls(tr.getId()))) @@ -81,14 +81,14 @@ public List getAllTasks() { */ public TaskResponse getTaskById(Long taskId) { UserEntity user = getCurrentUser(); - logger.info("Get task " + taskId + " to user " + user.getId()); + logger.info("Get task ID {} to user ID {}", taskId, user.getId()); Optional task = taskRepository.findById(taskId); if (task.isEmpty()) { throw new TaskNotFoundException(); } - logger.info("Task found! Id " + taskId); + logger.info("Task found! ID {}", taskId); return TaskResponse.fromEntity(task.get(), getAllTasksUrls(taskId)); } @@ -100,7 +100,7 @@ public TaskResponse getTaskById(Long taskId) { public TaskResponse createTask(TaskRequest taskRequest) { UserEntity user = getCurrentUser(); - logger.info("Creating task to user " + user.getId()); + logger.info("Creating task to user {}", user.getId()); TaskEntity task = new TaskEntity(); task.setDescription(taskRequest.description()); @@ -118,7 +118,7 @@ public TaskResponse createTask(TaskRequest taskRequest) { saveUrls(task, taskRequest.urls()); } - logger.info("Task created! Id " + created.getId()); + logger.info("Task created! Id {}", created.getId()); return TaskResponse.fromEntity(created, getAllTasksUrls(created.getId())); } @@ -133,7 +133,7 @@ public TaskResponse createTask(TaskRequest taskRequest) { public TaskResponse patchTask(Long taskId, TaskPatchRequest patch) { UserEntity user = getCurrentUser(); - logger.info("Patching task " + taskId + " to user " + user.getId()); + logger.info("Patching task {} to user {}", taskId, user.getId()); Optional task = taskRepository.findById(taskId); if (task.isEmpty()) { @@ -165,7 +165,7 @@ public TaskResponse patchTask(Long taskId, TaskPatchRequest patch) { TaskEntity patchedTask = taskRepository.save(taskEntity); - logger.info("Task patched! Id " + patchedTask.getId()); + logger.info("Task patched! Id {}", patchedTask.getId()); return TaskResponse.fromEntity(patchedTask, getAllTasksUrls(taskId)); } @@ -179,7 +179,7 @@ public TaskResponse patchTask(Long taskId, TaskPatchRequest patch) { public void deleteTask(Long taskId) { UserEntity user = getCurrentUser(); - logger.info("Deleting task " + taskId + " to user " + user.getId()); + logger.info("Deleting task {} to user {}", taskId, user.getId()); Optional task = taskRepository.findById(taskId); if (task.isEmpty()) { @@ -191,14 +191,14 @@ public void deleteTask(Long taskId) { List urlsToDelete = taskUrlRepository.findAllById_taskId(taskEntity.getId()); if (!urlsToDelete.isEmpty()) { taskUrlRepository.deleteAllById_taskId(taskId); - logger.info("Deleted " + urlsToDelete.size() + " urls from task " + taskId); + logger.info("Deleted {} URLs from task ID {} in deleteTask", urlsToDelete.size(), taskId); } else { - logger.info("No urls to delete for task " + taskId); + logger.info("No URLs to delete for task ID {} in deleteTask", taskId); } taskRepository.delete(taskEntity); - logger.info("Task deleted! Id " + taskId); + logger.info("Task deleted! Id {}", taskId); } /** @@ -210,7 +210,7 @@ public void deleteTask(Long taskId) { public List searchTasks(String searchTerm) { UserEntity user = getCurrentUser(); - logger.info("Searching tasks to user " + user.getId()); + logger.info("Searching tasks to user {}", user.getId()); if (Objects.isNull(searchTerm) || searchTerm.isBlank()) { return List.of(); @@ -218,7 +218,7 @@ public List searchTasks(String searchTerm) { List tasks = taskRepository.findAllBySearchTerm(searchTerm.toUpperCase(), user.getId()); - logger.info(tasks.size() + " tasks found!"); + logger.info("{} tasks found!", tasks.size()); return tasks.stream() .map((TaskEntity tr) -> TaskResponse.fromEntity(tr, getAllTasksUrls(tr.getId()))) @@ -242,30 +242,27 @@ public List getTasksByFilter(String filter) { return List.of(); } - if (filter.equals("all")) { - return allTasks.stream() - .map((TaskEntity tr) -> TaskResponse.fromEntity(tr, getAllTasksUrls(tr.getId()))) - .toList(); - } - - if (filter.equals("high")) { - return allTasks.stream() - .filter(TaskEntity::getHighPriority) - .map((TaskEntity tr) -> TaskResponse.fromEntity(tr, getAllTasksUrls(tr.getId()))) - .toList(); - } - - if (filter.equals("untagged")) { - return allTasks.stream() - .filter(t -> t.getTag() == null || t.getTag().isBlank()) - .map((TaskEntity tr) -> TaskResponse.fromEntity(tr, getAllTasksUrls(tr.getId()))) - .toList(); - } - - return allTasks.stream() - .filter(t -> t.getTag().equals(filter)) - .map((TaskEntity tr) -> TaskResponse.fromEntity(tr, getAllTasksUrls(tr.getId()))) - .toList(); + return switch (filter) { + case "all" -> + allTasks.stream() + .map((TaskEntity tr) -> TaskResponse.fromEntity(tr, getAllTasksUrls(tr.getId()))) + .toList(); + case "high" -> + allTasks.stream() + .filter(TaskEntity::getHighPriority) + .map((TaskEntity tr) -> TaskResponse.fromEntity(tr, getAllTasksUrls(tr.getId()))) + .toList(); + case "untagged" -> + allTasks.stream() + .filter(t -> t.getTag() == null || t.getTag().isBlank()) + .map((TaskEntity tr) -> TaskResponse.fromEntity(tr, getAllTasksUrls(tr.getId()))) + .toList(); + default -> + allTasks.stream() + .filter(t -> t.getTag().equals(filter)) + .map((TaskEntity tr) -> TaskResponse.fromEntity(tr, getAllTasksUrls(tr.getId()))) + .toList(); + }; } private UserEntity getCurrentUser() { @@ -289,7 +286,7 @@ private void saveUrls(TaskEntity taskEntity, List urls) { } taskUrlRepository.saveAll(tasksUrl); - logger.info("Added " + tasksUrl.size() + " urls from task " + taskEntity.getId()); + logger.info("Added {} URLs from task {}", tasksUrl.size(), taskEntity.getId()); } private void patchDueDate(TaskEntity taskEntity, TaskPatchRequest patch) { @@ -298,8 +295,7 @@ private void patchDueDate(TaskEntity taskEntity, TaskPatchRequest patch) { try { taskEntity.setDueDate(LocalDate.parse(patch.dueDate())); } catch (DateTimeParseException e) { - logger.error( - "Unable to parse the provided date: " + patch.dueDate() + ": " + e.getMessage()); + logger.error("Unable to parse the provided date: {}: {}", patch.dueDate(), e.getMessage()); } } } @@ -309,9 +305,9 @@ private void patchTaskUrl(TaskEntity taskEntity, TaskPatchRequest patch) { List urlsToDelete = taskUrlRepository.findAllById_taskId(taskId); if (!urlsToDelete.isEmpty()) { taskUrlRepository.deleteAllById_taskId(taskId); - logger.info("Deleted " + urlsToDelete.size() + " urls from task " + taskId); + logger.info("Deleted {} URLs from task {}", urlsToDelete.size(), taskId); } else { - logger.info("No urls to delete for task " + taskId); + logger.info("No URLs to delete for task {}", taskId); } if (!Objects.isNull(patch.urls())) { @@ -319,7 +315,7 @@ private void patchTaskUrl(TaskEntity taskEntity, TaskPatchRequest patch) { patch.urls().stream().filter(u -> !u.isBlank()).map(String::trim).toList(); saveUrls(taskEntity, urlListToAdd); } else { - logger.info("No urls to add for task " + taskId); + logger.info("No URLs to add for task ID {}", taskId); } } } diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/impl/JwtServiceImpl.java b/server/src/main/java/br/com/tasknoteapp/server/service/impl/JwtServiceImpl.java index 1bd3b102..04516fb3 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/impl/JwtServiceImpl.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/impl/JwtServiceImpl.java @@ -115,10 +115,7 @@ public boolean validateTokenAndUser(String token, UserDetails user) { private T extractClaim(String token, Function claimsResolver) { final Optional claims = extractAllClaims(token); - if (claims.isPresent()) { - return claimsResolver.apply(claims.get()); - } - return null; + return claims.map(claimsResolver).orElse(null); } private Optional extractAllClaims(String token) { diff --git a/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplate.java b/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplate.java index cfd85990..0264a817 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplate.java +++ b/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplate.java @@ -3,12 +3,12 @@ import java.util.Map; import java.util.Optional; -/** This interface represents a mailgun template structure. */ +/** This interface represents a Mailgun template structure. */ public interface MailgunTemplate { - static final String STRING_SCAPE = "\""; - static final String COLON = ":"; - static final String COMMA = ","; + String STRING_SCAPE = "\""; + String COLON = ":"; + String COMMA = ","; String getName(); diff --git a/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateEmailChanged.java b/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateEmailChanged.java index ffef8521..b97c0537 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateEmailChanged.java +++ b/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateEmailChanged.java @@ -7,9 +7,8 @@ /** This class represents a template for the email change workflow. */ public class MailgunTemplateEmailChanged implements MailgunTemplate { - private String templateName = "email_changed"; - private String carbonCopy; private final Map props; + private String carbonCopy; public MailgunTemplateEmailChanged() { this.props = new HashMap<>(); @@ -25,7 +24,7 @@ public void setEmailTo(String emailTo) { @Override public String getName() { - return templateName; + return "email_changed"; } @Override @@ -33,12 +32,12 @@ public Map getVariables() { return props; } - public void setCarbonCopy(String carbonCopy) { - this.carbonCopy = carbonCopy; - } - @Override public Optional getCarbonCopy() { return Optional.ofNullable(carbonCopy); } + + public void setCarbonCopy(String carbonCopy) { + this.carbonCopy = carbonCopy; + } } diff --git a/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateResetPwd.java b/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateResetPwd.java index b4032b13..edad7a27 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateResetPwd.java +++ b/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateResetPwd.java @@ -6,7 +6,6 @@ /** This class represents a template for the password reset workflow. */ public class MailgunTemplateResetPwd implements MailgunTemplate { - private String templateName = "password_reset"; private final Map props; public MailgunTemplateResetPwd() { @@ -19,7 +18,7 @@ public void setResetLink(String resetLink) { @Override public String getName() { - return templateName; + return "password_reset"; } @Override diff --git a/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateResetPwdConfirm.java b/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateResetPwdConfirm.java index c018e4b6..dd518c8f 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateResetPwdConfirm.java +++ b/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateResetPwdConfirm.java @@ -6,7 +6,6 @@ /** This class represents a template for the password reset confirmation workflow. */ public class MailgunTemplateResetPwdConfirm implements MailgunTemplate { - private String templateName = "password_change_confirmation"; private final Map props; public MailgunTemplateResetPwdConfirm() { @@ -15,7 +14,7 @@ public MailgunTemplateResetPwdConfirm() { @Override public String getName() { - return templateName; + return "password_change_confirmation"; } @Override diff --git a/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateSignUp.java b/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateSignUp.java index dd5af19c..10c47b89 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateSignUp.java +++ b/server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateSignUp.java @@ -3,10 +3,9 @@ import java.util.HashMap; import java.util.Map; -/** This class represents a template for the sign up workflow. */ +/** This class represents a template for the sign-up workflow. */ public class MailgunTemplateSignUp implements MailgunTemplate { - private String templateName = "sign_up_confirmation"; private final Map props; public MailgunTemplateSignUp() { @@ -19,7 +18,7 @@ public void setConfirmationLink(String confirmationLink) { @Override public String getName() { - return templateName; + return "sign_up_confirmation"; } @Override diff --git a/server/src/main/java/br/com/tasknoteapp/server/util/TimeAgoUtil.java b/server/src/main/java/br/com/tasknoteapp/server/util/TimeAgoUtil.java index a37d91f6..dcd5a552 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/util/TimeAgoUtil.java +++ b/server/src/main/java/br/com/tasknoteapp/server/util/TimeAgoUtil.java @@ -14,7 +14,7 @@ public class TimeAgoUtil { /** * Format a time in the time ago format. * - * @param pastTime The past time to be formatted. + * @param pastTime The pastime to be formatted. * @return String with formatted time. */ public static String format(LocalDateTime pastTime) { @@ -91,7 +91,7 @@ public static String formatDueDate(LocalDate futureDate) { int year = futureDate.getYear(); String dateFmt = String.format(" (%s %d%s, %s %d)", dayOfWeek, dayOfMonth, suffix, month, year); - return sb.toString() + dateFmt; + return sb + dateFmt; } private static String getDaySuffix(int day) { diff --git a/server/src/main/java/br/com/tasknoteapp/server/util/TokenUtil.java b/server/src/main/java/br/com/tasknoteapp/server/util/TokenUtil.java index 72ffeafc..7d6a7a07 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/util/TokenUtil.java +++ b/server/src/main/java/br/com/tasknoteapp/server/util/TokenUtil.java @@ -15,7 +15,7 @@ public class TokenUtil { */ public String generateToken() { // 24 bytes ~ 32 chars length after Base64 - // Base64 encoding expands that by a factor of 4/3. + // encoding expands that by a factor of 4/3. // 24 bytes * 4/3 = 32 characters byte[] randomBytes = new byte[24]; secureRandom.nextBytes(randomBytes); diff --git a/server/src/main/java/br/com/tasknoteapp/server/util/UuidUtil.java b/server/src/main/java/br/com/tasknoteapp/server/util/UuidUtil.java index 5d781884..f74336e4 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/util/UuidUtil.java +++ b/server/src/main/java/br/com/tasknoteapp/server/util/UuidUtil.java @@ -33,7 +33,7 @@ private UUID generateUuidFromName(UUID namespace, String name) { sha1[6] &= 0x0f; sha1[6] |= 0x50; sha1[8] &= 0x3f; - sha1[8] |= 0x80; + sha1[8] |= (byte) 0x80; return bytesToUuid(sha1); } From c7e0ceff9bff1f6634357d82bca0d01f34cb4ff1 Mon Sep 17 00:00:00 2001 From: Ricardo Campos Date: Sat, 16 May 2026 21:23:50 -0300 Subject: [PATCH 3/3] feat: fix errors issue and improve logs --- .../response/ValidationExceptionResponse.java | 19 +++++ .../server/service/AuthService.java | 10 ++- .../server/service/NoteService.java | 36 ++++----- .../server/service/TaskService.java | 20 ++--- .../server/service/UserSessionService.java | 14 +++- .../server/service/impl/UserServiceImpl.java | 3 +- .../tasknoteapp/server/util/SecurityUtil.java | 21 +++++ .../server/util/SecurityUtilTest.java | 78 +++++++++++++++++++ 8 files changed, 167 insertions(+), 34 deletions(-) create mode 100644 server/src/main/java/br/com/tasknoteapp/server/util/SecurityUtil.java create mode 100644 server/src/test/java/br/com/tasknoteapp/server/util/SecurityUtilTest.java diff --git a/server/src/main/java/br/com/tasknoteapp/server/response/ValidationExceptionResponse.java b/server/src/main/java/br/com/tasknoteapp/server/response/ValidationExceptionResponse.java index 624db44b..c5bb3d14 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/response/ValidationExceptionResponse.java +++ b/server/src/main/java/br/com/tasknoteapp/server/response/ValidationExceptionResponse.java @@ -29,4 +29,23 @@ public ValidationExceptionResponse(String field, String errorMessage) { this.fields = List.of(new FieldIssueResponse(field, errorMessage)); this.errorMessage = String.format(MESSAGE_TEMPLATE, fields.size()); } + + /** + * Method used by Spring internals to build the final JSON response. + * + * @return String describing the error message + */ + public String getErrorMessage() { + return errorMessage; + } + + /** + * Method used by Spring internals to build the final JSON response with failed fields. + * + * @return List of FieldIssueResponse containing the field name and the error message related to + * it + */ + public List getFields() { + return fields; + } } diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java b/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java index 237f3181..94bfc945 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java @@ -20,6 +20,7 @@ import br.com.tasknoteapp.server.response.UserResponse; import br.com.tasknoteapp.server.response.UserResponseWithToken; import br.com.tasknoteapp.server.util.AuthUtil; +import br.com.tasknoteapp.server.util.SecurityUtil; import br.com.tasknoteapp.server.util.TokenUtil; import br.com.tasknoteapp.server.util.UuidUtil; import jakarta.transaction.Transactional; @@ -106,7 +107,7 @@ public AuthService( */ @Transactional public UserResponseWithToken signUpNewUser(LoginRequest newUser) { - logger.info("Signing up new user: {}", newUser.email()); + logger.info("Signing up new user: {}", SecurityUtil.redactEmail(newUser.email())); if (findByEmail(newUser.email()).isPresent()) { throw new EmailAlreadyExistsException(); @@ -181,7 +182,7 @@ public User loadUserByUsername(String email) { */ @Transactional public UserResponseWithToken signInUser(LoginRequest login) { - logger.info("Signing in user: {}", login.email()); + logger.info("Signing in user: {}", SecurityUtil.redactEmail(login.email())); Optional userOptional = findByEmail(login.email()); if (userOptional.isEmpty()) { @@ -355,7 +356,8 @@ public UserResponse patchUserInfo(UserPatchRequest patchRequest) { if (emailChanged && hasValidMailgunApiKey()) { // send email to older and new account - logger.info("Email changed from {} to {}", email, patchRequest.email()); + logger.info( + "Email changed from {} to {}", email, SecurityUtil.redactEmail(patchRequest.email())); mailgunEmailService.sendEmailChangedNotification(currentUser, email); } @@ -504,7 +506,7 @@ public void confirmResetPasswordForUser(PasswordResetRequest request) { private Optional getGravatarImageUrl(String email) { email = email.toLowerCase().trim(); - logger.info("Current user email: {}", email); + logger.info("Current user email: {}", SecurityUtil.redactEmail(email)); try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java b/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java index 0e056253..064adbda 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java @@ -63,7 +63,7 @@ public NoteService( public List getAllNotes() { UserEntity user = getCurrentUser(); - logger.info("Get all notes to user {}", user.getId()); + logger.info("Get all notes to user ID {}", user.getId()); List notes = noteRepository.findAllByUser_id(user.getId()); logger.info("{} notes found!", notes.size()); @@ -79,7 +79,7 @@ public List getAllNotes() { */ public NoteResponse getNoteById(Long noteId) { UserEntity user = getCurrentUser(); - logger.info("Get note {} to user {}", noteId, user.getId()); + logger.info("Get note ID {} to user ID {}", noteId, user.getId()); Optional note = noteRepository.findById(noteId); if (note.isEmpty()) { @@ -90,7 +90,7 @@ public NoteResponse getNoteById(Long noteId) { throw new NoteNotFoundException(); } - logger.info("Note found! Id {}", noteId); + logger.info("Note found! ID {}", noteId); return NoteResponse.fromEntity(note.get(), getNoteUrl(noteId)); } @@ -103,7 +103,7 @@ public NoteResponse getNoteById(Long noteId) { public NoteResponse createNote(NoteRequest noteRequest) { UserEntity user = getCurrentUser(); - logger.info("Creating note to user {}", user.getId()); + logger.info("Creating note to user ID {}", user.getId()); NoteEntity note = new NoteEntity(); note.setTitle(noteRequest.title()); @@ -113,7 +113,7 @@ public NoteResponse createNote(NoteRequest noteRequest) { note.setUser(user); NoteEntity created = noteRepository.save(note); - logger.info("Note created! Id {}", created.getId()); + logger.info("Note created! ID {}", created.getId()); String savedUrl = null; if (!Objects.isNull(noteRequest.url()) && !noteRequest.url().isEmpty()) { @@ -136,7 +136,7 @@ public NoteResponse createNote(NoteRequest noteRequest) { public NoteResponse patchNote(Long noteId, NotePatchRequest patch) { UserEntity user = getCurrentUser(); - logger.info("Patching task {} to user {}", noteId, user.getId()); + logger.info("Patching task ID {} to user ID {}", noteId, user.getId()); Optional note = noteRepository.findByIdAndUser_id(noteId, user.getId()); if (note.isEmpty()) { @@ -157,18 +157,18 @@ public NoteResponse patchNote(Long noteId, NotePatchRequest patch) { noteUrlRepository.deleteByNote_id(noteId); noteUrlRepository.flush(); - logger.info("URL deleted from task {} during patch", noteId); + logger.info("URL deleted from note ID {} during patch", noteId); if (!Objects.isNull(patch.url()) && !patch.url().isBlank()) { saveUrl(noteEntity, patch.url()); } else { - logger.info("No URLs to patch for task {}", noteId); + logger.info("No URLs to patch for note ID {}", noteId); } NoteEntity patchedNote = noteRepository.save(noteEntity); noteRepository.flush(); - logger.info("Note patched! Id {}", patchedNote.getId()); + logger.info("Note patched! ID {}", patchedNote.getId()); return NoteResponse.fromEntity(patchedNote, getNoteUrl(patchedNote.getId())); } @@ -182,7 +182,7 @@ public NoteResponse patchNote(Long noteId, NotePatchRequest patch) { public void deleteNote(Long noteId) { UserEntity user = getCurrentUser(); - logger.info("Deleting note {} to user {}", noteId, user.getId()); + logger.info("Deleting note ID {} to user ID {}", noteId, user.getId()); Optional note = noteRepository.findByIdAndUser_id(noteId, user.getId()); if (note.isEmpty()) { @@ -192,11 +192,11 @@ public void deleteNote(Long noteId) { NoteEntity noteEntity = note.get(); noteUrlRepository.deleteByNote_id(noteId); - logger.info("URL deleted from task {}", noteId); + logger.info("URL deleted from note ID {}", noteId); noteRepository.delete(noteEntity); - logger.info("Note deleted! Id {}", noteId); + logger.info("Note deleted! ID {}", noteId); } /** @@ -208,7 +208,7 @@ public void deleteNote(Long noteId) { public List searchNotes(String searchTerm) { UserEntity user = getCurrentUser(); - logger.info("Searching notes to user {}", user.getId()); + logger.info("Searching notes for user ID {}", user.getId()); List notes = noteRepository.findAllBySearchTerm(searchTerm.toUpperCase(), user.getId()); @@ -225,7 +225,7 @@ public List searchNotes(String searchTerm) { @Transactional public NoteResponse shareNote(Long noteId) { UserEntity user = getCurrentUser(); - logger.info("Sharing note {} for user {}", noteId, user.getId()); + logger.info("Sharing note ID {} for user ID {}", noteId, user.getId()); Optional noteOpt = noteRepository.findByIdAndUser_id(noteId, user.getId()); if (noteOpt.isEmpty()) { @@ -238,7 +238,7 @@ public NoteResponse shareNote(Long noteId) { noteEntity.setShared(true); noteEntity.setShareToken(UUID.randomUUID().toString()); noteRepository.save(noteEntity); - logger.info("Note {} shared with token {}", noteId, noteEntity.getShareToken()); + logger.info("Note ID {} shared with token {}", noteId, noteEntity.getShareToken()); } return NoteResponse.fromEntity(noteEntity, getNoteUrl(noteEntity.getId())); @@ -252,7 +252,7 @@ public NoteResponse shareNote(Long noteId) { */ public NoteResponse unshareNote(Long noteId) { UserEntity user = getCurrentUser(); - logger.info("Unsharing note {} for user {}", noteId, user.getId()); + logger.info("Unsharing note ID {} for user ID {}", noteId, user.getId()); Optional noteOpt = noteRepository.findByIdAndUser_id(noteId, user.getId()); if (noteOpt.isEmpty()) { @@ -263,7 +263,7 @@ public NoteResponse unshareNote(Long noteId) { noteEntity.setShared(false); noteEntity.setShareToken(null); noteRepository.save(noteEntity); - logger.info("Note {} unshared", noteId); + logger.info("Note ID {} unshared", noteId); return NoteResponse.fromEntity(noteEntity, getNoteUrl(noteEntity.getId())); } @@ -313,7 +313,7 @@ private NoteUrlEntity saveUrl(NoteEntity noteEntity, String url) { noteUrl.setNote(noteEntity); NoteUrlEntity savedUrl = noteUrlRepository.save(noteUrl); - logger.info("URL saved to note {}", noteEntity.getId()); + logger.info("URL saved to note ID {}", noteEntity.getId()); return savedUrl; } diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/TaskService.java b/server/src/main/java/br/com/tasknoteapp/server/service/TaskService.java index 6375616b..9079f753 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/TaskService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/TaskService.java @@ -100,7 +100,7 @@ public TaskResponse getTaskById(Long taskId) { public TaskResponse createTask(TaskRequest taskRequest) { UserEntity user = getCurrentUser(); - logger.info("Creating task to user {}", user.getId()); + logger.info("Creating task to user ID {}", user.getId()); TaskEntity task = new TaskEntity(); task.setDescription(taskRequest.description()); @@ -118,7 +118,7 @@ public TaskResponse createTask(TaskRequest taskRequest) { saveUrls(task, taskRequest.urls()); } - logger.info("Task created! Id {}", created.getId()); + logger.info("Task created! ID {}", created.getId()); return TaskResponse.fromEntity(created, getAllTasksUrls(created.getId())); } @@ -133,7 +133,7 @@ public TaskResponse createTask(TaskRequest taskRequest) { public TaskResponse patchTask(Long taskId, TaskPatchRequest patch) { UserEntity user = getCurrentUser(); - logger.info("Patching task {} to user {}", taskId, user.getId()); + logger.info("Patching task ID {} to user ID {}", taskId, user.getId()); Optional task = taskRepository.findById(taskId); if (task.isEmpty()) { @@ -165,7 +165,7 @@ public TaskResponse patchTask(Long taskId, TaskPatchRequest patch) { TaskEntity patchedTask = taskRepository.save(taskEntity); - logger.info("Task patched! Id {}", patchedTask.getId()); + logger.info("Task patched! ID {}", patchedTask.getId()); return TaskResponse.fromEntity(patchedTask, getAllTasksUrls(taskId)); } @@ -179,7 +179,7 @@ public TaskResponse patchTask(Long taskId, TaskPatchRequest patch) { public void deleteTask(Long taskId) { UserEntity user = getCurrentUser(); - logger.info("Deleting task {} to user {}", taskId, user.getId()); + logger.info("Deleting task ID {} to user ID {}", taskId, user.getId()); Optional task = taskRepository.findById(taskId); if (task.isEmpty()) { @@ -198,7 +198,7 @@ public void deleteTask(Long taskId) { taskRepository.delete(taskEntity); - logger.info("Task deleted! Id {}", taskId); + logger.info("Task deleted! ID {}", taskId); } /** @@ -210,7 +210,7 @@ public void deleteTask(Long taskId) { public List searchTasks(String searchTerm) { UserEntity user = getCurrentUser(); - logger.info("Searching tasks to user {}", user.getId()); + logger.info("Searching tasks to user ID {}", user.getId()); if (Objects.isNull(searchTerm) || searchTerm.isBlank()) { return List.of(); @@ -286,7 +286,7 @@ private void saveUrls(TaskEntity taskEntity, List urls) { } taskUrlRepository.saveAll(tasksUrl); - logger.info("Added {} URLs from task {}", tasksUrl.size(), taskEntity.getId()); + logger.info("Added {} URLs from task ID {}", tasksUrl.size(), taskEntity.getId()); } private void patchDueDate(TaskEntity taskEntity, TaskPatchRequest patch) { @@ -305,9 +305,9 @@ private void patchTaskUrl(TaskEntity taskEntity, TaskPatchRequest patch) { List urlsToDelete = taskUrlRepository.findAllById_taskId(taskId); if (!urlsToDelete.isEmpty()) { taskUrlRepository.deleteAllById_taskId(taskId); - logger.info("Deleted {} URLs from task {}", urlsToDelete.size(), taskId); + logger.info("Deleted {} URLs from task ID {}", urlsToDelete.size(), taskId); } else { - logger.info("No URLs to delete for task {}", taskId); + logger.info("No URLs to delete for task ID {}", taskId); } if (!Objects.isNull(patch.urls())) { diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/UserSessionService.java b/server/src/main/java/br/com/tasknoteapp/server/service/UserSessionService.java index 371ff06a..6172550d 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/UserSessionService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/UserSessionService.java @@ -6,15 +6,20 @@ import br.com.tasknoteapp.server.response.NoteResponse; import br.com.tasknoteapp.server.response.TaskResponse; import br.com.tasknoteapp.server.response.UserResponse; +import br.com.tasknoteapp.server.util.SecurityUtil; import jakarta.transaction.Transactional; import java.util.List; import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; /** This class contains methods to handle user session and account deletion. */ @Service public class UserSessionService { + private static final Logger logger = LoggerFactory.getLogger(UserSessionService.class.getName()); + private final AuthService authService; private final TaskService taskService; @@ -57,6 +62,11 @@ public UserResponse deleteCurrentUserAccount() { throw new UserNotFoundException(); } + logger.info( + "Delete current user account for user ID {} email {}", + userOptional.get().getId(), + SecurityUtil.redactEmail(userOptional.get().getEmail())); + List tasks = taskService.getAllTasks(); for (TaskResponse task : tasks) { Long taskId = task != null ? task.id() : null; @@ -73,6 +83,8 @@ public UserResponse deleteCurrentUserAccount() { } } - return authService.deleteUserAccount(); + UserResponse response = authService.deleteUserAccount(); + logger.info("User account deleted for user ID {}", userOptional.get().getId()); + return response; } } diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/impl/UserServiceImpl.java b/server/src/main/java/br/com/tasknoteapp/server/service/impl/UserServiceImpl.java index d461e686..df79337a 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/impl/UserServiceImpl.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/impl/UserServiceImpl.java @@ -3,6 +3,7 @@ import br.com.tasknoteapp.server.entity.UserEntity; import br.com.tasknoteapp.server.repository.UserRepository; import br.com.tasknoteapp.server.service.UserService; +import br.com.tasknoteapp.server.util.SecurityUtil; import java.util.Optional; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Service; @@ -18,7 +19,7 @@ public UserServiceImpl(UserRepository userRepository) { email -> { Optional user = userRepository.findByEmail(email); if (user.isEmpty()) { - throw new RuntimeException("User not found: " + email); + throw new RuntimeException("User not found: " + SecurityUtil.redactEmail(email)); } return user.get(); }; diff --git a/server/src/main/java/br/com/tasknoteapp/server/util/SecurityUtil.java b/server/src/main/java/br/com/tasknoteapp/server/util/SecurityUtil.java new file mode 100644 index 00000000..a369f1e4 --- /dev/null +++ b/server/src/main/java/br/com/tasknoteapp/server/util/SecurityUtil.java @@ -0,0 +1,21 @@ +package br.com.tasknoteapp.server.util; + +/** This class contains useful methods to handle security routines. */ +public class SecurityUtil { + + /** + * Redacts an email address by replacing the characters before the "@" symbol with "...". If the + * email is null or blank, it returns the original email. For example, " + * + * @param email The email address + * @return Redacted email address + */ + public static String redactEmail(String email) { + if (email == null || email.isBlank()) { + return email; + } + int posAt = email.indexOf("@"); + int halfPos = posAt / 2; + return email.substring(0, halfPos) + "..." + email.substring(posAt); + } +} diff --git a/server/src/test/java/br/com/tasknoteapp/server/util/SecurityUtilTest.java b/server/src/test/java/br/com/tasknoteapp/server/util/SecurityUtilTest.java new file mode 100644 index 00000000..def579a0 --- /dev/null +++ b/server/src/test/java/br/com/tasknoteapp/server/util/SecurityUtilTest.java @@ -0,0 +1,78 @@ +package br.com.tasknoteapp.server.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class SecurityUtilTest { + + @Test + void redactEmailWithNullTest() { + String result = SecurityUtil.redactEmail(null); + + Assertions.assertNull(result); + } + + @Test + void redactEmailWithBlankTest() { + String result = SecurityUtil.redactEmail(""); + + Assertions.assertEquals("", result); + } + + @Test + void redactEmailWithWhitespaceTest() { + String result = SecurityUtil.redactEmail(" "); + + Assertions.assertEquals(" ", result); + } + + @Test + void redactEmailWithStandardEmailTest() { + String result = SecurityUtil.redactEmail("john.doe@example.com"); + + Assertions.assertEquals("john...@example.com", result); + } + + @Test + void redactEmailWithShortEmailTest() { + String result = SecurityUtil.redactEmail("ab@cd.com"); + + Assertions.assertEquals("a...@cd.com", result); + } + + @Test + void redactEmailWithSingleCharacterBeforeAtTest() { + String result = SecurityUtil.redactEmail("a@example.com"); + + Assertions.assertEquals("...@example.com", result); + } + + @Test + void redactEmailWithLongLocalPartTest() { + String result = SecurityUtil.redactEmail("verylongemailaddress@example.com"); + + Assertions.assertEquals("verylongem...@example.com", result); + } + + @Test + void redactEmailWithNumbersAndSymbolsTest() { + String result = SecurityUtil.redactEmail("user.name+tag123@example.co.uk"); + + Assertions.assertEquals("user.nam...@example.co.uk", result); + } + + @Test + void redactEmailWithMultipleDotsTest() { + String result = SecurityUtil.redactEmail("first.middle.last@example.com"); + + Assertions.assertEquals("first.mi...@example.com", result); + } + + @Test + void redactEmailWithUppercaseTest() { + String result = SecurityUtil.redactEmail("John.Doe@Example.COM"); + + Assertions.assertEquals("John...@Example.COM", result); + } +} +