Security/critical fixes (#4)
Main CI-Frontend / Build & Push (push) Successful in 39s
Main CI-Backend / Build & Push (push) Successful in 45s

## What

- Addressing security issues: critical and not so critical (more fixed will be pushed soon)

## Why

- The app needs to be secure and safe.

## Mood
<img width="200" src="https://media2.giphy.com/media/CSpfd57m9WGHnxMWXm/100.webp?cid=36b14facw100seggylsefdj0ap2oopoux3bn3jn6qu59xggq&ep=v1_gifs_search&rid=100.webp&ct=g"/>

Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
2026-06-29 19:23:41 +00:00
parent a55a80e916
commit 169f07801a
26 changed files with 179 additions and 129 deletions
+13
View File
@@ -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));
+1 -1
View File
@@ -29,7 +29,7 @@ export default defineConfig(({ mode }: ConfigEnv) => {
],
build: {
outDir: 'dist',
sourcemap: true
sourcemap: mode === 'development'
},
server: {
port: 5000
+5 -6
View File
@@ -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
@@ -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);
}
/**
@@ -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<TaskEntity, Long> {
List<TaskEntity> findAllByUser_id(Long userId);
Optional<TaskEntity> findByIdAndUser_id(Long id, Long userId);
@Query(
"""
select distinct t
@@ -78,12 +78,8 @@ public class LoginRequest {
+ "email='"
+ email
+ '\''
+ ", password='"
+ password
+ '\''
+ ", passwordAgain='"
+ passwordAgain
+ '\''
+ ", password='[REDACTED]'"
+ ", passwordAgain='[REDACTED]'"
+ ", lang='"
+ lang
+ '\''
@@ -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,
@@ -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,
@@ -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 #")
@@ -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 #")
@@ -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) {}
@@ -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();
}
}
@@ -95,7 +95,7 @@ public class TaskService {
UserEntity user = getCurrentUser();
logger.info("Get task ID {} to user ID {}", taskId, user.getId());
Optional<TaskEntity> task = taskRepository.findById(taskId);
Optional<TaskEntity> 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<TaskEntity> task = taskRepository.findById(taskId);
Optional<TaskEntity> 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<TaskEntity> task = taskRepository.findById(taskId);
Optional<TaskEntity> task = taskRepository.findByIdAndUser_id(taskId, user.getId());
if (task.isEmpty()) {
throw new TaskNotFoundException();
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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:
+4 -4
View File
@@ -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:
@@ -0,0 +1,2 @@
ALTER TABLE tasknote.notes
ADD CONSTRAINT chk_notes_description_max_length CHECK (length(description) <= 50000) NOT VALID;
@@ -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 =
@@ -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());
@@ -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<String> 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());
@@ -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
@@ -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));
}
}
+14 -4
View File
@@ -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"
+46
View File
@@ -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 {
+1 -1
View File
@@ -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;"