diff --git a/client/src/context/AuthProvider.tsx b/client/src/context/AuthProvider.tsx index e32309a..d863dbf 100644 --- a/client/src/context/AuthProvider.tsx +++ b/client/src/context/AuthProvider.tsx @@ -127,6 +127,19 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro .finally(() => setLoading(false)); }, []); + useEffect(() => { + if (!signed) return; + const TWENTY_FIVE_MINUTES = 25 * 60 * 1000; + const intervalId = setInterval(() => { + checkCurrentAuthUser(window.location.pathname).catch(() => { + setSigned(false); + setUser(undefined); + localStorage.clear(); + }); + }, TWENTY_FIVE_MINUTES); + return () => clearInterval(intervalId); + }, [signed]); + const updateUser = (userUpdated: UserResponse): void => { setUser(userUpdated); localStorage.setItem(USER_DATA, JSON.stringify(userUpdated)); diff --git a/client/vite.config.ts b/client/vite.config.ts index 7be48bb..c9c0e7c 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -29,7 +29,7 @@ export default defineConfig(({ mode }: ConfigEnv) => { ], build: { outDir: 'dist', - sourcemap: true + sourcemap: mode === 'development' }, server: { port: 5000 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 17680f2..da2ba31 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -23,13 +23,13 @@ services: POSTGRES_DB: tasknote POSTGRES_HOST: tasknote-db POSTGRES_USER: tasknoteuser - POSTGRES_PASSWORD: default + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PORT: 5432 - CORS_ALLOWED_ORIGINS: http://tasknote-web:5000, http://localhost:5000, https://flattop-depth-dropper.ngrok-free.dev + CORS_ALLOWED_ORIGINS: http://tasknote-web:5000, http://localhost:5000 SERVER_SERVLET_CONTEXT_PATH: / TARGET_ENV: production - SECURITY_KEY: ${SECURITY_KEY:-default-security-key} - MAILGUN_APIKEY: ${MAILGUN_APIKEY:-default-mailgun-apikey} + SECURITY_KEY: ${SECURITY_KEY} + MAILGUN_APIKEY: ${MAILGUN_APIKEY} ports: ["8585:8585"] image: ghcr.io/rmcampos/tasknote/api:latest healthcheck: @@ -47,8 +47,7 @@ services: environment: POSTGRES_DB: tasknote POSTGRES_USER: tasknoteuser - POSTGRES_PASSWORD: default - ports: ["5432:5432"] + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} healthcheck: test: psql -q -U $${POSTGRES_USER} -d $${POSTGRES_DB} -c 'SELECT 1' interval: 1m30s 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 a838c13..21a0d91 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 @@ -56,7 +56,7 @@ public class SecurityConfig { .requestMatchers("/rest/**") .authenticated() .anyRequest() - .permitAll()) + .denyAll()) .httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .sessionManagement( @@ -77,7 +77,7 @@ public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); + return new BCryptPasswordEncoder(12); } /** diff --git a/server/src/main/java/br/com/tasknoteapp/server/repository/TaskRepository.java b/server/src/main/java/br/com/tasknoteapp/server/repository/TaskRepository.java index 6d8978b..beca8bb 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/repository/TaskRepository.java +++ b/server/src/main/java/br/com/tasknoteapp/server/repository/TaskRepository.java @@ -2,6 +2,7 @@ package br.com.tasknoteapp.server.repository; import br.com.tasknoteapp.server.entity.TaskEntity; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -11,6 +12,8 @@ public interface TaskRepository extends JpaRepository { List findAllByUser_id(Long userId); + Optional findByIdAndUser_id(Long id, Long userId); + @Query( """ select distinct t 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 e6e0ead..49ae13e 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 @@ -78,12 +78,8 @@ public class LoginRequest { + "email='" + email + '\'' - + ", password='" - + password - + '\'' - + ", passwordAgain='" - + passwordAgain - + '\'' + + ", password='[REDACTED]'" + + ", passwordAgain='[REDACTED]'" + ", lang='" + lang + '\'' 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 2dfd157..344b464 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 @@ -1,13 +1,15 @@ package br.com.tasknoteapp.server.request; import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; import java.util.List; /** This record represents a note patch payload. */ public record NotePatchRequest( - String title, - String description, - @Pattern( + @Size(max = 100) String title, + @Size(max = 50000) String description, + @Size(max = 200) + @Pattern( regexp = "^(https?://.*|#.*)?$", message = "URL must start with https:// or #") String url, 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 5e40f72..9cf287d 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 @@ -2,13 +2,15 @@ package br.com.tasknoteapp.server.request; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; import java.util.List; /** This record represents a note request to be created. */ public record NoteRequest( - @NotNull String title, - @NotNull String description, - @Pattern( + @NotNull @Size(max = 100) String title, + @NotNull @Size(max = 50000) String description, + @Size(max = 200) + @Pattern( regexp = "^(https?://.*|#.*)?$", message = "URL must start with https:// or #") String url, 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 ae8bef6..acdefbd 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 @@ -1,13 +1,15 @@ package br.com.tasknoteapp.server.request; import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; import java.util.List; /** This record represents a task patch payload. */ public record TaskPatchRequest( - String description, + @Size(max = 2000) String description, Boolean done, List< + @Size(max = 200) @Pattern( regexp = "^(https?://.*|#.*)?$", message = "URL must start with https:// or #") 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 7b35d21..dccd149 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 @@ -3,12 +3,14 @@ package br.com.tasknoteapp.server.request; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; import java.util.List; /** This record represents a task request to be created. */ public record TaskRequest( - @NotNull @NotEmpty String description, + @NotNull @NotEmpty @Size(max = 2000) String description, List< + @Size(max = 200) @Pattern( regexp = "^(https?://.*|#.*)?$", message = "URL must start with https:// or #") diff --git a/server/src/main/java/br/com/tasknoteapp/server/request/UserPatchRequest.java b/server/src/main/java/br/com/tasknoteapp/server/request/UserPatchRequest.java index f60ceba..ad9831a 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/request/UserPatchRequest.java +++ b/server/src/main/java/br/com/tasknoteapp/server/request/UserPatchRequest.java @@ -2,4 +2,10 @@ package br.com.tasknoteapp.server.request; /** This record represents a user patch payload. */ public record UserPatchRequest( - String name, String email, String password, String passwordAgain, String lang) {} + String name, + String email, + String password, + String passwordAgain, + String lang, + String currentPassword) {} + 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 5bcb031..94082d8 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 @@ -292,7 +292,7 @@ public class AuthService { String token = jwtService.generateToken(currentUser); - logger.info("User refreshed! Token {}", token); + logger.info("User refreshed! Token {}...", token.substring(0, 6)); return token; } @@ -329,11 +329,26 @@ public class AuthService { boolean shouldUpdate = false; boolean emailChanged = false; + boolean changingEmail = + !Objects.isNull(patchRequest.email()) && !patchRequest.email().isBlank(); + boolean changingPassword = + !Objects.isNull(patchRequest.password()) && !patchRequest.password().isBlank(); + + if (changingEmail || changingPassword) { + if (Objects.isNull(patchRequest.currentPassword()) + || patchRequest.currentPassword().isBlank()) { + throw new BadPasswordException("Current password is required to change email or password"); + } + if (!passwordEncoder.matches(patchRequest.currentPassword(), currentUser.getPassword())) { + throw new InvalidCredentialsException(); + } + } + if (!Objects.isNull(patchRequest.name()) && !patchRequest.name().isBlank()) { currentUser.setName(patchRequest.name().trim()); shouldUpdate = true; } - if (!Objects.isNull(patchRequest.email()) && !patchRequest.email().isBlank()) { + if (changingEmail) { currentUser.setEmail(patchRequest.email().trim()); shouldUpdate = true; emailChanged = true; @@ -344,8 +359,7 @@ public class AuthService { } boolean updatePassword = - !Objects.isNull(patchRequest.password()) - && !patchRequest.password().isBlank() + changingPassword && !Objects.isNull(patchRequest.passwordAgain()) && !patchRequest.passwordAgain().isBlank(); @@ -563,11 +577,11 @@ public class AuthService { // if it's more than 3 times in the last 10 minutes, raise timer of 3 hours. if (userPwdList.size() >= 3) { - UserPwdLimitEntity mostRecent = userPwdList.getFirst(); - logger.warn("Oldest: {}", mostRecent.getWhenHappened()); - Duration duration = Duration.between(mostRecent.getWhenHappened(), LocalDateTime.now()); + UserPwdLimitEntity oldest = userPwdList.getLast(); + logger.warn("Oldest failed attempt: {}", oldest.getWhenHappened()); + Duration duration = Duration.between(oldest.getWhenHappened(), LocalDateTime.now()); if (duration.toMinutes() <= 3L) { - logger.warn("Wait more {}", 3L - duration.toMinutes()); + logger.warn("Account locked, minutes remaining: {}", 3L - duration.toMinutes()); throw new MaxLoginLimitAttemptException(); } } 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 30fa11f..9827c8f 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 @@ -95,7 +95,7 @@ public class TaskService { UserEntity user = getCurrentUser(); logger.info("Get task ID {} to user ID {}", taskId, user.getId()); - Optional task = taskRepository.findById(taskId); + Optional task = taskRepository.findByIdAndUser_id(taskId, user.getId()); if (task.isEmpty()) { throw new TaskNotFoundException(); } @@ -152,7 +152,7 @@ public class TaskService { logger.info("Patching task ID {} to user ID {}", taskId, user.getId()); - Optional task = taskRepository.findById(taskId); + Optional task = taskRepository.findByIdAndUser_id(taskId, user.getId()); if (task.isEmpty()) { throw new TaskNotFoundException(); } @@ -198,7 +198,7 @@ public class TaskService { logger.info("Deleting task ID {} to user ID {}", taskId, user.getId()); - Optional task = taskRepository.findById(taskId); + Optional task = taskRepository.findByIdAndUser_id(taskId, user.getId()); if (task.isEmpty()) { throw new TaskNotFoundException(); } 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 04516fb..5ee9c60 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 @@ -3,8 +3,8 @@ package br.com.tasknoteapp.server.service.impl; import br.com.tasknoteapp.server.entity.UserEntity; import br.com.tasknoteapp.server.service.JwtService; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.security.Keys; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; @@ -29,7 +29,7 @@ class JwtServiceImpl implements JwtService { private static final long MINUTE = SECOND * 60; private static final long HOUR = MINUTE * 60; private static final long DAY = HOUR * 24; - private static final long EXPIRATION_TIME = DAY * 7; + private static final long EXPIRATION_TIME = MINUTE * 30; private final SecretKey key; public JwtServiceImpl(@Value("${br.com.tasknote.server.jwt-secret}") String secretKey) { @@ -122,7 +122,7 @@ class JwtServiceImpl implements JwtService { try { return Optional.of( Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload()); - } catch (MalformedJwtException me) { + } catch (JwtException e) { return Optional.empty(); } } 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 f74336e..98fef30 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 @@ -1,72 +1,17 @@ package br.com.tasknoteapp.server.util; -import br.com.tasknoteapp.server.exception.BadAlgorithmException; -import java.nio.charset.StandardCharsets; import java.util.UUID; /** This class provides method to handle UUIDs. */ public class UuidUtil { - private final UUID namespaceUrl = UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8"); /** - * Generated a unique UUID to a given email. + * Generates a cryptographically random UUID for use as an email confirmation token. * - * @param email The email to create the UUID. - * @return The generated UUID. + * @param email The user email (unused; kept for API compatibility). + * @return A random UUID. */ public UUID generateEmailUuid(String email) { - return generateUuidFromName(namespaceUrl, email.toLowerCase().trim()); - } - - private UUID generateUuidFromName(UUID namespace, String name) { - // SHA-1 digest of namespace UUID + name - byte[] namespaceBytes = toBytes(namespace); - byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); - - byte[] combined = new byte[namespaceBytes.length + nameBytes.length]; - System.arraycopy(namespaceBytes, 0, combined, 0, namespaceBytes.length); - System.arraycopy(nameBytes, 0, combined, namespaceBytes.length, nameBytes.length); - - byte[] sha1 = sha1(combined); - - // Manipulate bits to make it UUID v5 (version 5, SHA-1) - sha1[6] &= 0x0f; - sha1[6] |= 0x50; - sha1[8] &= 0x3f; - sha1[8] |= (byte) 0x80; - - return bytesToUuid(sha1); - } - - private byte[] toBytes(UUID uuid) { - long msb = uuid.getMostSignificantBits(); - long lsb = uuid.getLeastSignificantBits(); - byte[] bytes = new byte[16]; - - for (int i = 0; i < 8; i++) { - bytes[i] = (byte) ((msb >>> (8 * (7 - i))) & 0xFF); - bytes[8 + i] = (byte) ((lsb >>> (8 * (7 - i))) & 0xFF); - } - return bytes; - } - - private byte[] sha1(byte[] input) { - try { - return java.security.MessageDigest.getInstance("SHA-1").digest(input); - } catch (Exception e) { - throw new BadAlgorithmException("SHA-1 algorithm not available"); - } - } - - private UUID bytesToUuid(byte[] hash) { - long msb = 0; - long lsb = 0; - for (int i = 0; i < 8; i++) { - msb = (msb << 8) | (hash[i] & 0xff); - } - for (int i = 8; i < 16; i++) { - lsb = (lsb << 8) | (hash[i] & 0xff); - } - return new UUID(msb, lsb); + return UUID.randomUUID(); } } diff --git a/server/src/main/resources/application-native.yml b/server/src/main/resources/application-native.yml index 1f28ab1..969f1a9 100644 --- a/server/src/main/resources/application-native.yml +++ b/server/src/main/resources/application-native.yml @@ -2,14 +2,14 @@ br: com: tasknote: server: - jwt-secret: ${SECURITY_KEY:empty} + jwt-secret: ${SECURITY_KEY} target-env: ${TARGET_ENV:development} cors: allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost} logging: level: root: ${ROOT_LOG_LEVEL:INFO} - br.com.tasknoteapp: TRACE + br.com.tasknoteapp: INFO mailgun: api-key: ${MAILGUN_APIKEY:abc123456} @@ -19,7 +19,7 @@ mailgun: server: port: 8585 error: - include-message: always + include-message: never servlet: context-path: ${SERVER_SERVLET_CONTEXT_PATH:/server} spring: @@ -27,7 +27,7 @@ spring: name: tasknote-api datasource: driver-class-name: org.postgresql.Driver - password: ${POSTGRES_PASSWORD:default} + password: ${POSTGRES_PASSWORD} url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5435}/${POSTGRES_DB:tasknote} username: ${POSTGRES_USER:tasknoteuser} flyway: diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index c75033c..4360ec1 100644 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -2,14 +2,14 @@ br: com: tasknote: server: - jwt-secret: ${SECURITY_KEY:empty} + jwt-secret: ${SECURITY_KEY} target-env: ${TARGET_ENV:development} cors: allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost} logging: level: root: ${ROOT_LOG_LEVEL:INFO} - br.com.tasknoteapp: TRACE + br.com.tasknoteapp: INFO mailgun: api-key: ${MAILGUN_APIKEY:abc123456} @@ -19,7 +19,7 @@ mailgun: server: port: 8585 error: - include-message: always + include-message: never servlet: context-path: ${SERVER_SERVLET_CONTEXT_PATH:/server} spring: @@ -27,7 +27,7 @@ spring: name: tasknote-api datasource: driver-class-name: org.postgresql.Driver - password: ${POSTGRES_PASSWORD:default} + password: ${POSTGRES_PASSWORD} url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5435}/${POSTGRES_DB:tasknote} username: ${POSTGRES_USER:tasknoteuser} flyway: diff --git a/server/src/main/resources/db/migration/V202606291000__restore_notes_description_check.sql b/server/src/main/resources/db/migration/V202606291000__restore_notes_description_check.sql new file mode 100644 index 0000000..06d4374 --- /dev/null +++ b/server/src/main/resources/db/migration/V202606291000__restore_notes_description_check.sql @@ -0,0 +1,2 @@ +ALTER TABLE tasknote.notes + ADD CONSTRAINT chk_notes_description_max_length CHECK (length(description) <= 50000) NOT VALID; diff --git a/server/src/test/java/br/com/tasknoteapp/server/controller/UserControllerTest.java b/server/src/test/java/br/com/tasknoteapp/server/controller/UserControllerTest.java index 42603f3..5dcd4b1 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/controller/UserControllerTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/controller/UserControllerTest.java @@ -103,7 +103,8 @@ class UserControllerTest { void patchUserInfo_happyPath_shouldSucceed() throws Exception { UserResponse response = new UserResponse(1L, "John", "email@example.com", false, null, null, null, null); - UserPatchRequest request = new UserPatchRequest("John Doe", response.email(), null, null, null); + UserPatchRequest request = + new UserPatchRequest("John Doe", response.email(), null, null, null, null); when(authService.patchUserInfo(request)).thenReturn(response); String jsonString = 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 bf346db..f0ea115 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 @@ -264,9 +264,11 @@ class AuthServiceTest { when(userRepository.findByEmail(request.email())).thenReturn(Optional.of(existing)); UserPwdLimitEntity limit1 = new UserPwdLimitEntity(); - limit1.setWhenHappened(LocalDateTime.now().minusMinutes(1)); + limit1.setWhenHappened(LocalDateTime.now().minusSeconds(30)); UserPwdLimitEntity limit2 = new UserPwdLimitEntity(); + limit2.setWhenHappened(LocalDateTime.now().minusMinutes(1)); UserPwdLimitEntity limit3 = new UserPwdLimitEntity(); + limit3.setWhenHappened(LocalDateTime.now().minusMinutes(2)); when(userPwdLimitRepository.findTop3ByUser_idOrderByWhenHappenedDesc(existing.getId())) .thenReturn(List.of(limit1, limit2, limit3)); @@ -417,12 +419,15 @@ class AuthServiceTest { existing.setName(null); existing.setEmail(email); existing.setAdmin(false); + existing.setPassword("hashedCurrentPassword"); when(userRepository.findByEmail(email)).thenReturn(Optional.of(existing)); - when(userRepository.save(any())).thenReturn(existing); + String currentPassword = "currentPw123@"; + when(passwordEncoder.matches(currentPassword, "hashedCurrentPassword")).thenReturn(true); + UserPatchRequest patchRequest = - new UserPatchRequest("Kong", "newemail@domain.com", null, null, null); + new UserPatchRequest("Kong", "newemail@domain.com", null, null, null, currentPassword); UserResponse response = authService.patchUserInfo(patchRequest); Assertions.assertNotNull(response); @@ -441,13 +446,17 @@ class AuthServiceTest { existing.setName(null); existing.setEmail(email); existing.setAdmin(false); + existing.setPassword("hashedCurrentPassword"); when(userRepository.findByEmail(email)).thenReturn(Optional.of(existing)); - when(userRepository.save(any())).thenReturn(existing); + String currentPassword = "currentPw123@"; + when(passwordEncoder.matches(currentPassword, "hashedCurrentPassword")).thenReturn(true); + String newPassword = "TestHackedPw@difficult!#:)"; UserPatchRequest patchRequest = - new UserPatchRequest("Kong", "newemail@domain.com", newPassword, newPassword, "en"); + new UserPatchRequest( + "Kong", "newemail@domain.com", newPassword, newPassword, "en", currentPassword); when(authUtil.validatePassword(patchRequest.password())).thenReturn(Optional.empty()); diff --git a/server/src/test/java/br/com/tasknoteapp/server/service/TaskServiceTest.java b/server/src/test/java/br/com/tasknoteapp/server/service/TaskServiceTest.java index 38093a4..8820ec4 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/service/TaskServiceTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/service/TaskServiceTest.java @@ -85,7 +85,7 @@ class TaskServiceTest { taskEntity.setHighPriority(true); taskEntity.setTags(Set.of(new TagEntity("test", userEntity))); taskEntity.setUser(userEntity); - when(taskRepository.findById(taskId)).thenReturn(Optional.of(taskEntity)); + when(taskRepository.findByIdAndUser_id(taskId, USER_ID)).thenReturn(Optional.of(taskEntity)); TaskResponse taskResponse = taskService.getTaskById(taskId); @@ -108,7 +108,7 @@ class TaskServiceTest { Long taskId = 9976L; - when(taskRepository.findById(taskId)).thenReturn(Optional.empty()); + when(taskRepository.findByIdAndUser_id(taskId, USER_ID)).thenReturn(Optional.empty()); assertThrows(TaskNotFoundException.class, () -> taskService.getTaskById(taskId)); } @@ -312,7 +312,7 @@ class TaskServiceTest { taskEntity.setHighPriority(true); taskEntity.setTags(Set.of(new TagEntity("test", userEntity))); taskEntity.setUser(userEntity); - when(taskRepository.findById(taskId)).thenReturn(Optional.of(taskEntity)); + when(taskRepository.findByIdAndUser_id(taskId, USER_ID)).thenReturn(Optional.of(taskEntity)); when(taskUrlRepository.findAllById_taskId(taskId)).thenReturn(List.of()); @@ -335,7 +335,7 @@ class TaskServiceTest { Long taskId = 2526L; - when(taskRepository.findById(taskId)).thenReturn(Optional.empty()); + when(taskRepository.findByIdAndUser_id(taskId, USER_ID)).thenReturn(Optional.empty()); assertThrows(TaskNotFoundException.class, () -> taskService.deleteTask(taskId)); } @@ -359,7 +359,7 @@ class TaskServiceTest { taskEntity.setDone(false); taskEntity.setTags(Set.of(new TagEntity("test", userEntity))); taskEntity.setUser(userEntity); - when(taskRepository.findById(taskId)).thenReturn(Optional.of(taskEntity)); + when(taskRepository.findByIdAndUser_id(taskId, USER_ID)).thenReturn(Optional.of(taskEntity)); when(taskUrlRepository.findAllById_taskId(taskId)).thenReturn(List.of()); @@ -410,7 +410,7 @@ class TaskServiceTest { taskEntity.setDone(false); taskEntity.setTags(Set.of(new TagEntity("test", userEntity))); taskEntity.setUser(userEntity); - when(taskRepository.findById(taskId)).thenReturn(Optional.of(taskEntity)); + when(taskRepository.findByIdAndUser_id(taskId, USER_ID)).thenReturn(Optional.of(taskEntity)); TaskUrlEntity urlEntity = new TaskUrlEntity(); urlEntity.setId(new TaskUrlEntityPk(taskId, "www.url.com")); @@ -460,7 +460,7 @@ class TaskServiceTest { Long taskId = 2525L; - when(taskRepository.findById(taskId)).thenReturn(Optional.empty()); + when(taskRepository.findByIdAndUser_id(taskId, USER_ID)).thenReturn(Optional.empty()); List tags = List.of("test"); TaskPatchRequest patch = @@ -488,7 +488,7 @@ class TaskServiceTest { taskEntity.setDone(false); taskEntity.setTags(Set.of(new TagEntity("test", userEntity))); taskEntity.setUser(userEntity); - when(taskRepository.findById(taskId)).thenReturn(Optional.of(taskEntity)); + when(taskRepository.findByIdAndUser_id(taskId, USER_ID)).thenReturn(Optional.of(taskEntity)); when(taskUrlRepository.findAllById_taskId(taskId)).thenReturn(List.of()); diff --git a/server/src/test/java/br/com/tasknoteapp/server/service/impl/JwtServiceImplTest.java b/server/src/test/java/br/com/tasknoteapp/server/service/impl/JwtServiceImplTest.java index 2c839dc..b536b7c 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/service/impl/JwtServiceImplTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/service/impl/JwtServiceImplTest.java @@ -3,14 +3,12 @@ package br.com.tasknoteapp.server.service.impl; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import br.com.tasknoteapp.server.entity.UserEntity; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; @@ -100,7 +98,7 @@ class JwtServiceImplTest { LocalDateTime expiration = jwtService.extractExpiration(token); assertNotNull(expiration); - LocalDateTime expectedExpiration = LocalDateTime.now().plusDays(7).withNano(0); + LocalDateTime expectedExpiration = LocalDateTime.now().plusMinutes(30).withNano(0); assertFalse(ChronoUnit.SECONDS.between(expiration.withNano(0), expectedExpiration) > 5); } @@ -131,7 +129,7 @@ class JwtServiceImplTest { .signWith(getKey()) .compact(); - assertThrows(ExpiredJwtException.class, () -> jwtService.isTokenExpired(expiredToken)); + assertTrue(jwtService.isTokenExpired(expiredToken)); } @Test diff --git a/server/src/test/java/br/com/tasknoteapp/server/util/UuidUtilTest.java b/server/src/test/java/br/com/tasknoteapp/server/util/UuidUtilTest.java index 1abfd7a..41e7580 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/util/UuidUtilTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/util/UuidUtilTest.java @@ -13,6 +13,6 @@ class UuidUtilTest { UUID uuid = uuidUtil.generateEmailUuid(email); Assertions.assertNotNull(uuid); - Assertions.assertEquals(uuid, uuidUtil.generateEmailUuid(email)); + Assertions.assertNotNull(uuidUtil.generateEmailUuid(email)); } } diff --git a/terraform-gcp/cloudrun.tf b/terraform-gcp/cloudrun.tf index 3834669..e4523ad 100644 --- a/terraform-gcp/cloudrun.tf +++ b/terraform-gcp/cloudrun.tf @@ -26,12 +26,22 @@ resource "google_cloud_run_v2_service" "backend" { value = google_sql_database_instance.instance.private_ip_address } env { - name = "POSTGRES_USER" - value = var.db_user + name = "POSTGRES_USER" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.db_user.secret_id + version = google_secret_manager_secret_version.db_user_version.version + } + } } env { - name = "POSTGRES_PASSWORD" - value = var.db_password + name = "POSTGRES_PASSWORD" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.db_password.secret_id + version = google_secret_manager_secret_version.db_password_version.version + } + } } env { name = "POSTGRES_PORT" diff --git a/terraform-gcp/secrets.tf b/terraform-gcp/secrets.tf index 7ac9a65..171075f 100644 --- a/terraform-gcp/secrets.tf +++ b/terraform-gcp/secrets.tf @@ -3,6 +3,52 @@ resource "google_service_account" "cloudrun_sa" { display_name = "TaskNote Cloud Run Service Account" } +resource "google_secret_manager_secret" "db_password" { + secret_id = "db-password" + replication { + user_managed { + replicas { + location = var.region + } + } + } + depends_on = [google_project_service.secretmanager] +} + +resource "google_secret_manager_secret_version" "db_password_version" { + secret = google_secret_manager_secret.db_password.id + secret_data = var.db_password +} + +resource "google_secret_manager_secret" "db_user" { + secret_id = "db-user" + replication { + user_managed { + replicas { + location = var.region + } + } + } + depends_on = [google_project_service.secretmanager] +} + +resource "google_secret_manager_secret_version" "db_user_version" { + secret = google_secret_manager_secret.db_user.id + secret_data = var.db_user +} + +resource "google_secret_manager_secret_iam_member" "db_password_access" { + secret_id = google_secret_manager_secret.db_password.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.cloudrun_sa.email}" +} + +resource "google_secret_manager_secret_iam_member" "db_user_access" { + secret_id = google_secret_manager_secret.db_user.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.cloudrun_sa.email}" +} + resource "google_secret_manager_secret" "security_key" { secret_id = "security-key" replication { diff --git a/tools/confirm-user.sh b/tools/confirm-user.sh index 5d0dc21..adaee43 100755 --- a/tools/confirm-user.sh +++ b/tools/confirm-user.sh @@ -2,7 +2,7 @@ set -euo pipefail docker run --rm -i --network=host \ - -e PGPASSWORD=default \ + -e PGPASSWORD="${PGPASSWORD:?PGPASSWORD env var is required}" \ postgres:15.8-bookworm \ psql -h localhost -U tasknoteuser -d tasknote \ -c "UPDATE tasknote.users SET email_confirmed_at = created_at WHERE id > 0;"