From 41448903aa924dc5852db8d7bb4d9319cb9f91a7 Mon Sep 17 00:00:00 2001 From: sechmachine <97589681+sechmachine727@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:39:53 +0700 Subject: [PATCH] refactor(platform): target Lombok boilerplate --- .../tests/unit/lombok-platform-boilerplate.md | 90 +++++++++++++++++++ .../config/SecurityProperties.java | 12 +-- .../account/controller/AccountController.java | 7 +- .../controller/BootstrapAccessFilter.java | 11 +-- .../controller/BootstrapController.java | 7 +- .../account/model/dto/ActivationForm.java | 10 +-- .../account/model/dto/BootstrapForm.java | 19 +--- .../account/model/dto/CreateAccountForm.java | 17 ++-- .../feature/account/model/entity/AppUser.java | 42 +++------ .../account/model/entity/InternProfile.java | 22 ++--- .../account/model/entity/SystemState.java | 13 ++- .../account/model/entity/UserActionToken.java | 37 +++----- .../account/service/BootstrapService.java | 11 +-- .../service/DatabaseUserDetailsService.java | 7 +- .../controller/SmtpController.java | 8 +- .../controller/SmtpWarningAdvice.java | 7 +- .../integration/model/dto/SmtpActionForm.java | 11 +-- .../integration/model/dto/SmtpForm.java | 18 +--- .../model/entity/SmtpConfiguration.java | 57 +++--------- .../service/MailDeliveryService.java | 9 +- .../service/SmtpConfigurationService.java | 15 +--- 21 files changed, 190 insertions(+), 240 deletions(-) create mode 100644 docs/tests/unit/lombok-platform-boilerplate.md diff --git a/docs/tests/unit/lombok-platform-boilerplate.md b/docs/tests/unit/lombok-platform-boilerplate.md new file mode 100644 index 0000000..41dda8e --- /dev/null +++ b/docs/tests/unit/lombok-platform-boilerplate.md @@ -0,0 +1,90 @@ +# Test Evidence: Platform Lombok boilerplate retrofit + +- **Test type:** Unit +- **Requirement IDs:** `Engineering policy — targeted Lombok retrofit` +- **Scenario IDs:** `Source-audit RED/GREEN` +- **Test class/method:** `N/A — reproducible source audit; behavior is covered by the affected suites below` +- **Implementation commit:** `pending` + +## Protected behavior + +Platform-owned Spring collaborators, request forms, and JPA entities must not retain eligible handwritten +dependency-assignment constructors, trivial accessors, or empty persistence constructors. The retrofit must preserve +constructor visibility, form normalization, entity encapsulation, defensive copies of credential/token bytes, and all +account, authentication, SMTP, and cross-feature behavior. + +## Test method + +The source audit searches only the 79 members classified as mechanical after reading every root/config, +`feature.account`, and `feature.integration` production type. It deliberately excludes normalized email/display-name +setters, defensive byte-array getters, domain constructors and factories, state transitions, the normalized +`AccountService` public-origin constructor, `SecretCipher` key construction, and the two-constructor JavaMail test +seam. No permanent annotation-presence test was added because annotations are an implementation detail; compilation +and production-shaped tests protect the real contracts. + +## Hand-derived expected result + +Before the retrofit the audit must find 79 eligible handwritten members and exit 1. After targeted Lombok generation, +the same audit must find none and exit 0, while the retained non-mechanical members remain explicit. + +## RED + +**Command** + +```text +matches=$( { rg -n '^ (public )?(AccountController|BootstrapAccessFilter|BootstrapController|BootstrapService|DatabaseUserDetailsService|SmtpController|SmtpWarningAdvice|MailDeliveryService|SmtpConfigurationService)\(' src/main/java/com/lab/labtimesheet/feature/account src/main/java/com/lab/labtimesheet/feature/integration; rg -n '^ public (String getMasterKey|void setMasterKey)\(' src/main/java/com/lab/labtimesheet/config/SecurityProperties.java; rg -n '^ public (String get(Token|Password|ConfirmPassword)|void set(Token|Password|ConfirmPassword))\(' src/main/java/com/lab/labtimesheet/feature/account/model/dto/ActivationForm.java; rg -n '^ public (String get(Email|DisplayName|Password)|void setPassword)\(' src/main/java/com/lab/labtimesheet/feature/account/model/dto/BootstrapForm.java; rg -n '^ public .+ (get(Email|DisplayName|Role|StudentCode|InternshipStart|InternshipEnd)|set(Role|StudentCode|InternshipStart|InternshipEnd))\(' src/main/java/com/lab/labtimesheet/feature/account/model/dto/CreateAccountForm.java; rg -n '^ public (Long getDraftId|void setDraftId)\(' src/main/java/com/lab/labtimesheet/feature/integration/model/dto/SmtpActionForm.java; rg -n '^ public .+ (get(Host|Port|SecurityMode|Username|Password|FromAddress|FromName)|set(Host|Port|SecurityMode|Username|Password|FromAddress|FromName))\(' src/main/java/com/lab/labtimesheet/feature/integration/model/dto/SmtpForm.java; rg -n '^ protected (AppUser|InternProfile|SystemState|UserActionToken|SmtpConfiguration)\(\)' src/main/java/com/lab/labtimesheet/feature/account/model/entity src/main/java/com/lab/labtimesheet/feature/integration/model/entity; rg -n '^ public .+ (get(Id|Email|DisplayName|PasswordHash|GlobalRole|AccountStatus|ActivatedAt|InternshipStatus|InternshipStartDate|InternshipEndDate|UserId|Purpose|ExpiresAt|UsedAt|InvalidatedAt|Status|Host|Port|SecurityMode|Username|SecretKeyVersion|FromAddress|FromName|TestedAt)|isInitialized)\(' src/main/java/com/lab/labtimesheet/feature/account/model/entity src/main/java/com/lab/labtimesheet/feature/integration/model/entity; } ); if [ -n "$matches" ]; then printf '%s\n' "$matches"; printf 'RED: eligible handwritten Lombok boilerplate remains (%s matches)\n' "$(printf '%s\n' "$matches" | wc -l | tr -d ' ')"; exit 1; fi; printf 'GREEN: no eligible handwritten Lombok boilerplate remains\n' +``` + +**Observed result** + +```text +RED: eligible handwritten Lombok boilerplate remains (79 matches) +Process exited with code 1 because the confirmed mechanical members were still handwritten. +``` + +## GREEN + +**Command** + +```text +The exact RED source-audit command above was repeated without alteration. +``` + +**Observed result** + +```text +GREEN: no eligible handwritten Lombok boilerplate remains +Process exited with code 0. +``` + +## Affected suite + +**Command and result** + +```text +export JAVA_HOME=/opt/homebrew/opt/openjdk@25 +export PATH="$JAVA_HOME/bin:$PATH" +./mvnw -DskipTests compile +BUILD SUCCESS — 127 production source files compiled on Java 25. + +export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock +./mvnw -Dtest=LabtimesheetApplicationTests,LayerStructureTest,PlatformFoundationTest,TimeConfigurationTest,SecurityResponseIntegrationTest,AccountWebIntegrationTest,AuthenticationWebIntegrationTest,BootstrapOnboardingWebIntegrationTest,AccountActivationIntegrationTest,BootstrapIntegrationTest,SmtpOnboardingWebIntegrationTest,JavaMailSmtpProbeTest,SmtpIntegrationTest test +BUILD SUCCESS — Tests run: 29, Failures: 0, Errors: 0, Skipped: 0; PostgreSQL 18.4. + +./mvnw test +BUILD SUCCESS — Tests run: 197, Failures: 0, Errors: 0, Skipped: 0; PostgreSQL 18.4. + +./mvnw -DskipTests -Ddoclint=all javadoc:javadoc +BUILD SUCCESS — doclint reported no errors; Maven emitted 66 non-fatal missing-comment warnings across the integrated +tree, including generated default constructors/accessors. + +git diff --check +No output; exit 0. +``` + +## External-test boundaries + +This source audit does not prove Lombok internals or enforce a preferred annotation spelling. The compile and +PostgreSQL-backed affected/full suites prove generated constructor/accessor compatibility with Spring binding, +Security, JPA/Hibernate, Thymeleaf, and existing cross-feature consumers. No dependency, schema, migration, token, +credential, template, container, CI, or runtime configuration was changed. diff --git a/src/main/java/com/lab/labtimesheet/config/SecurityProperties.java b/src/main/java/com/lab/labtimesheet/config/SecurityProperties.java index 7d19cb5..684102a 100644 --- a/src/main/java/com/lab/labtimesheet/config/SecurityProperties.java +++ b/src/main/java/com/lab/labtimesheet/config/SecurityProperties.java @@ -2,21 +2,17 @@ package com.lab.labtimesheet.config; import java.util.Base64; +import lombok.Getter; +import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; /** Security material used to encrypt integration credentials at rest. */ @ConfigurationProperties("lab.security") +@Getter +@Setter public class SecurityProperties { private String masterKey; - public String getMasterKey() { - return masterKey; - } - - public void setMasterKey(String masterKey) { - this.masterKey = masterKey; - } - /** * Decodes and validates the configured AES-256 master key. * diff --git a/src/main/java/com/lab/labtimesheet/feature/account/controller/AccountController.java b/src/main/java/com/lab/labtimesheet/feature/account/controller/AccountController.java index 7f1eb31..ed27903 100644 --- a/src/main/java/com/lab/labtimesheet/feature/account/controller/AccountController.java +++ b/src/main/java/com/lab/labtimesheet/feature/account/controller/AccountController.java @@ -6,6 +6,8 @@ import com.lab.labtimesheet.feature.account.model.dto.ActivationForm; import com.lab.labtimesheet.feature.account.model.dto.CreateAccountForm; import com.lab.labtimesheet.feature.account.service.AccountService; import jakarta.validation.Valid; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import org.hibernate.exception.ConstraintViolationException; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Controller; @@ -20,13 +22,10 @@ import org.springframework.web.bind.annotation.PostMapping; * constraints are mapped to their owning form fields without exposing persistence diagnostics. */ @Controller +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) class AccountController { private final AccountService accounts; - AccountController(AccountService accounts) { - this.accounts = accounts; - } - @GetMapping("/admin/accounts/new") String newAccount(Model model) { if (!model.containsAttribute("accountForm")) { diff --git a/src/main/java/com/lab/labtimesheet/feature/account/controller/BootstrapAccessFilter.java b/src/main/java/com/lab/labtimesheet/feature/account/controller/BootstrapAccessFilter.java index 1360e56..766e4ff 100644 --- a/src/main/java/com/lab/labtimesheet/feature/account/controller/BootstrapAccessFilter.java +++ b/src/main/java/com/lab/labtimesheet/feature/account/controller/BootstrapAccessFilter.java @@ -7,24 +7,17 @@ import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; import org.springframework.web.filter.OncePerRequestFilter; /** * Hides all non-bootstrap application routes until durable first-Admin initialization completes. * Only bootstrap pages, health, public assets, and error rendering remain reachable beforehand. */ +@RequiredArgsConstructor public class BootstrapAccessFilter extends OncePerRequestFilter { private final BootstrapService bootstrap; - /** - * Creates the pre-bootstrap access guard. - * - * @param bootstrap durable installation-state service - */ - public BootstrapAccessFilter(BootstrapService bootstrap) { - this.bootstrap = bootstrap; - } - /** * Returns HTTP 404 for hidden routes before bootstrap so no authentication surface is exposed prematurely. * diff --git a/src/main/java/com/lab/labtimesheet/feature/account/controller/BootstrapController.java b/src/main/java/com/lab/labtimesheet/feature/account/controller/BootstrapController.java index 1fc3a1a..cba2b36 100644 --- a/src/main/java/com/lab/labtimesheet/feature/account/controller/BootstrapController.java +++ b/src/main/java/com/lab/labtimesheet/feature/account/controller/BootstrapController.java @@ -3,6 +3,8 @@ package com.lab.labtimesheet.feature.account.controller; import com.lab.labtimesheet.feature.account.model.dto.BootstrapForm; import com.lab.labtimesheet.feature.account.service.BootstrapService; import jakarta.validation.Valid; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -16,13 +18,10 @@ import org.springframework.web.server.ResponseStatusException; /** Renders and processes the one-time first-Admin installation form. */ @Controller @RequestMapping("/bootstrap") +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) class BootstrapController { private final BootstrapService bootstrap; - BootstrapController(BootstrapService bootstrap) { - this.bootstrap = bootstrap; - } - @GetMapping String form(Model model) { requireOpen(); diff --git a/src/main/java/com/lab/labtimesheet/feature/account/model/dto/ActivationForm.java b/src/main/java/com/lab/labtimesheet/feature/account/model/dto/ActivationForm.java index 9026242..dba4283 100644 --- a/src/main/java/com/lab/labtimesheet/feature/account/model/dto/ActivationForm.java +++ b/src/main/java/com/lab/labtimesheet/feature/account/model/dto/ActivationForm.java @@ -3,10 +3,14 @@ package com.lab.labtimesheet.feature.account.model.dto; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; /** * Validated activation submission. Password fields remain request-local and are never repopulated by the view. */ +@Getter +@Setter public class ActivationForm { @NotBlank(message = "This activation link is invalid or no longer usable") private String token; @@ -34,10 +38,4 @@ public class ActivationForm { confirmPassword = null; } - public String getToken() { return token; } - public void setToken(String token) { this.token = token; } - public String getPassword() { return password; } - public void setPassword(String password) { this.password = password; } - public String getConfirmPassword() { return confirmPassword; } - public void setConfirmPassword(String confirmPassword) { this.confirmPassword = confirmPassword; } } diff --git a/src/main/java/com/lab/labtimesheet/feature/account/model/dto/BootstrapForm.java b/src/main/java/com/lab/labtimesheet/feature/account/model/dto/BootstrapForm.java index ffe1344..5035429 100644 --- a/src/main/java/com/lab/labtimesheet/feature/account/model/dto/BootstrapForm.java +++ b/src/main/java/com/lab/labtimesheet/feature/account/model/dto/BootstrapForm.java @@ -3,11 +3,14 @@ package com.lab.labtimesheet.feature.account.model.dto; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; /** * Validated browser input for creating the first administrator. * The password is deliberately never copied into redirected state or repopulated after validation failure. */ +@Getter public class BootstrapForm { @NotBlank(message = "Email is required") @Email(message = "Enter a valid email address") @@ -20,29 +23,15 @@ public class BootstrapForm { @NotBlank(message = "Password is required") @Size(min = 12, max = 128, message = "Password must contain 12 through 128 characters") + @Setter private String password; - public String getEmail() { - return email; - } - public void setEmail(String email) { this.email = email == null ? null : email.trim(); } - public String getDisplayName() { - return displayName; - } - public void setDisplayName(String displayName) { this.displayName = displayName == null ? null : displayName.trim(); } - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } } diff --git a/src/main/java/com/lab/labtimesheet/feature/account/model/dto/CreateAccountForm.java b/src/main/java/com/lab/labtimesheet/feature/account/model/dto/CreateAccountForm.java index b010d00..e37f044 100644 --- a/src/main/java/com/lab/labtimesheet/feature/account/model/dto/CreateAccountForm.java +++ b/src/main/java/com/lab/labtimesheet/feature/account/model/dto/CreateAccountForm.java @@ -8,9 +8,12 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; import org.springframework.format.annotation.DateTimeFormat; /** Validated, non-secret Admin input for creating an immutable-role account. */ +@Getter public class CreateAccountForm { @NotBlank(message = "Email is required") @Email(message = "Enter a valid email address") @@ -22,15 +25,19 @@ public class CreateAccountForm { private String displayName; @NotNull(message = "Role is required") + @Setter private GlobalRole role; @Size(max = 64, message = "Student code must contain at most 64 characters") + @Setter private String studentCode; @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @Setter private LocalDate internshipStart; @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @Setter private LocalDate internshipEnd; /** @@ -67,16 +74,6 @@ public class CreateAccountForm { return hasText(value) ? value.trim() : null; } - public String getEmail() { return email; } public void setEmail(String email) { this.email = email == null ? null : email.trim(); } - public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName == null ? null : displayName.trim(); } - public GlobalRole getRole() { return role; } - public void setRole(GlobalRole role) { this.role = role; } - public String getStudentCode() { return studentCode; } - public void setStudentCode(String studentCode) { this.studentCode = studentCode; } - public LocalDate getInternshipStart() { return internshipStart; } - public void setInternshipStart(LocalDate internshipStart) { this.internshipStart = internshipStart; } - public LocalDate getInternshipEnd() { return internshipEnd; } - public void setInternshipEnd(LocalDate internshipEnd) { this.internshipEnd = internshipEnd; } } diff --git a/src/main/java/com/lab/labtimesheet/feature/account/model/entity/AppUser.java b/src/main/java/com/lab/labtimesheet/feature/account/model/entity/AppUser.java index 26e0c54..e056fd9 100644 --- a/src/main/java/com/lab/labtimesheet/feature/account/model/entity/AppUser.java +++ b/src/main/java/com/lab/labtimesheet/feature/account/model/entity/AppUser.java @@ -16,6 +16,9 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.Version; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; /** * Persistent global account with immutable role, authentication lifecycle, creator attribution, and optimistic @@ -23,29 +26,37 @@ import jakarta.persistence.Version; */ @Entity @Table(name = "app_users") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class AppUser { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Getter private Long id; @Column(nullable = false, length = 320) + @Getter private String email; @Column(name = "display_name", nullable = false, length = 120) + @Getter private String displayName; @Column(name = "password_hash", length = 255) + @Getter private String passwordHash; @Enumerated(EnumType.STRING) @Column(name = "global_role", nullable = false, length = 16, updatable = false) + @Getter private GlobalRole globalRole; @Enumerated(EnumType.STRING) @Column(name = "account_status", nullable = false, length = 32) + @Getter private AccountStatus accountStatus; @Column(name = "activated_at") + @Getter private Instant activatedAt; @ManyToOne(fetch = FetchType.LAZY) @@ -61,10 +72,6 @@ public class AppUser { @Version private long version; - /** Required by JPA; domain instances are created through named factories. */ - protected AppUser() { - } - private AppUser(String email, String displayName, String passwordHash, GlobalRole globalRole, AccountStatus accountStatus, Instant activatedAt, AppUser createdBy, Instant now) { this.email = email; @@ -124,31 +131,4 @@ public class AppUser { updatedAt = now; } - public Long getId() { - return id; - } - - public String getEmail() { - return email; - } - - public String getDisplayName() { - return displayName; - } - - public String getPasswordHash() { - return passwordHash; - } - - public GlobalRole getGlobalRole() { - return globalRole; - } - - public AccountStatus getAccountStatus() { - return accountStatus; - } - - public Instant getActivatedAt() { - return activatedAt; - } } diff --git a/src/main/java/com/lab/labtimesheet/feature/account/model/entity/InternProfile.java b/src/main/java/com/lab/labtimesheet/feature/account/model/entity/InternProfile.java index 4d4452c..f4690e8 100644 --- a/src/main/java/com/lab/labtimesheet/feature/account/model/entity/InternProfile.java +++ b/src/main/java/com/lab/labtimesheet/feature/account/model/entity/InternProfile.java @@ -11,6 +11,9 @@ import jakarta.persistence.Enumerated; import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.persistence.Version; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; /** * Persistent internship lifecycle and inclusive eligibility dates for an Intern account. @@ -18,6 +21,7 @@ import jakarta.persistence.Version; */ @Entity @Table(name = "intern_profiles") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class InternProfile { @Id @Column(name = "user_id") @@ -33,13 +37,16 @@ public class InternProfile { private String phone; @Column(name = "internship_start_date", nullable = false) + @Getter private LocalDate internshipStartDate; @Column(name = "internship_end_date", nullable = false) + @Getter private LocalDate internshipEndDate; @Enumerated(EnumType.STRING) @Column(name = "internship_status", nullable = false, length = 24) + @Getter private InternshipStatus internshipStatus; @Column(name = "activated_at") @@ -60,10 +67,6 @@ public class InternProfile { @Version private long version; - /** Required by JPA; domain instances are created through {@link #notStarted}. */ - protected InternProfile() { - } - private InternProfile( long userId, String studentCode, LocalDate internshipStartDate, LocalDate internshipEndDate, Instant now) { this.userId = userId; @@ -105,15 +108,4 @@ public class InternProfile { updatedAt = now; } - public InternshipStatus getInternshipStatus() { - return internshipStatus; - } - - public LocalDate getInternshipStartDate() { - return internshipStartDate; - } - - public LocalDate getInternshipEndDate() { - return internshipEndDate; - } } diff --git a/src/main/java/com/lab/labtimesheet/feature/account/model/entity/SystemState.java b/src/main/java/com/lab/labtimesheet/feature/account/model/entity/SystemState.java index d01c219..2635e9b 100644 --- a/src/main/java/com/lab/labtimesheet/feature/account/model/entity/SystemState.java +++ b/src/main/java/com/lab/labtimesheet/feature/account/model/entity/SystemState.java @@ -10,16 +10,21 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.Version; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; /** Durable singleton installation state used to serialize and remember first-Admin bootstrap. */ @Entity @Table(name = "system_state") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class SystemState { @Id @Column(name = "singleton_id") private short singletonId; @Column(nullable = false) + @Getter private boolean initialized; @Column(name = "initialized_at") @@ -38,14 +43,6 @@ public class SystemState { @Version private long version; - /** Required by JPA; Flyway creates the singleton row. */ - protected SystemState() { - } - - public boolean isInitialized() { - return initialized; - } - /** * Marks the installation initialized and retains the first Admin attribution. * diff --git a/src/main/java/com/lab/labtimesheet/feature/account/model/entity/UserActionToken.java b/src/main/java/com/lab/labtimesheet/feature/account/model/entity/UserActionToken.java index 98a6fd2..306f668 100644 --- a/src/main/java/com/lab/labtimesheet/feature/account/model/entity/UserActionToken.java +++ b/src/main/java/com/lab/labtimesheet/feature/account/model/entity/UserActionToken.java @@ -12,6 +12,9 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; /** * Persistent one-time user-action token state. Only a defensive copy of the SHA-256 token hash is stored; raw @@ -19,28 +22,35 @@ import jakarta.persistence.Table; */ @Entity @Table(name = "user_action_tokens") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class UserActionToken { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Getter private Long id; @Column(name = "user_id", nullable = false) + @Getter private Long userId; @Enumerated(EnumType.STRING) @Column(nullable = false, length = 24) + @Getter private TokenPurpose purpose; @Column(name = "token_hash", nullable = false, columnDefinition = "bytea") private byte[] tokenHash; @Column(name = "expires_at", nullable = false) + @Getter private Instant expiresAt; @Column(name = "used_at") + @Getter private Instant usedAt; @Column(name = "invalidated_at") + @Getter private Instant invalidatedAt; @Column(name = "issued_by_user_id") @@ -49,10 +59,6 @@ public class UserActionToken { @Column(name = "created_at", nullable = false) private Instant createdAt; - /** Required by JPA; domain instances are created through named factories. */ - protected UserActionToken() { - } - private UserActionToken(long userId, byte[] tokenHash, Instant expiresAt, long issuedByUserId, Instant now) { this.userId = userId; this.purpose = TokenPurpose.ACTIVATION; @@ -115,18 +121,6 @@ public class UserActionToken { } } - public Long getId() { - return id; - } - - public Long getUserId() { - return userId; - } - - public TokenPurpose getPurpose() { - return purpose; - } - /** * Returns a defensive copy of the persisted token hash. * @@ -136,15 +130,4 @@ public class UserActionToken { return Arrays.copyOf(tokenHash, tokenHash.length); } - public Instant getExpiresAt() { - return expiresAt; - } - - public Instant getUsedAt() { - return usedAt; - } - - public Instant getInvalidatedAt() { - return invalidatedAt; - } } diff --git a/src/main/java/com/lab/labtimesheet/feature/account/service/BootstrapService.java b/src/main/java/com/lab/labtimesheet/feature/account/service/BootstrapService.java index 9ea29aa..8eeb978 100644 --- a/src/main/java/com/lab/labtimesheet/feature/account/service/BootstrapService.java +++ b/src/main/java/com/lab/labtimesheet/feature/account/service/BootstrapService.java @@ -7,6 +7,8 @@ import com.lab.labtimesheet.feature.account.model.entity.AppUser; import com.lab.labtimesheet.feature.account.model.entity.SystemState; import com.lab.labtimesheet.feature.account.repository.AppUserRepository; import com.lab.labtimesheet.feature.account.repository.SystemStateRepository; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,20 +18,13 @@ import org.springframework.transaction.annotation.Transactional; * Successful creation persists the first active Admin and initialization marker atomically. */ @Service +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) public class BootstrapService { private final SystemStateRepository systemStates; private final AppUserRepository users; private final PasswordEncoder passwords; private final Clock clock; - BootstrapService(SystemStateRepository systemStates, AppUserRepository users, PasswordEncoder passwords, - Clock clock) { - this.systemStates = systemStates; - this.users = users; - this.passwords = passwords; - this.clock = clock; - } - /** * Creates the first active Admin exactly once. * diff --git a/src/main/java/com/lab/labtimesheet/feature/account/service/DatabaseUserDetailsService.java b/src/main/java/com/lab/labtimesheet/feature/account/service/DatabaseUserDetailsService.java index d3633af..2013811 100644 --- a/src/main/java/com/lab/labtimesheet/feature/account/service/DatabaseUserDetailsService.java +++ b/src/main/java/com/lab/labtimesheet/feature/account/service/DatabaseUserDetailsService.java @@ -2,6 +2,8 @@ package com.lab.labtimesheet.feature.account.service; import com.lab.labtimesheet.feature.account.model.AccountStatus; import com.lab.labtimesheet.feature.account.repository.AppUserRepository; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -11,13 +13,10 @@ import org.springframework.transaction.annotation.Transactional; /** Adapts persisted account credentials and lifecycle state to Spring Security authentication. */ @Service +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) class DatabaseUserDetailsService implements UserDetailsService { private final AppUserRepository users; - DatabaseUserDetailsService(AppUserRepository users) { - this.users = users; - } - /** * Loads the normalized account and disables authentication unless its lifecycle state is active. * diff --git a/src/main/java/com/lab/labtimesheet/feature/integration/controller/SmtpController.java b/src/main/java/com/lab/labtimesheet/feature/integration/controller/SmtpController.java index b3fb507..c7b3758 100644 --- a/src/main/java/com/lab/labtimesheet/feature/integration/controller/SmtpController.java +++ b/src/main/java/com/lab/labtimesheet/feature/integration/controller/SmtpController.java @@ -9,6 +9,8 @@ import com.lab.labtimesheet.feature.integration.model.dto.SmtpForm; import com.lab.labtimesheet.feature.integration.service.SmtpConfigurationService; import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import org.springframework.mail.MailException; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -25,6 +27,7 @@ import org.springframework.web.bind.annotation.RequestMapping; */ @Controller @RequestMapping("/admin/smtp") +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) class SmtpController { private static final String TEST_FAILURE_MESSAGE = "SMTP test failed. Verify the draft settings and server availability, then try again."; @@ -41,11 +44,6 @@ class SmtpController { private final SmtpConfigurationService smtp; private final AccountService accounts; - SmtpController(SmtpConfigurationService smtp, AccountService accounts) { - this.smtp = smtp; - this.accounts = accounts; - } - @GetMapping String form(Model model) { return renderForm(model, null); diff --git a/src/main/java/com/lab/labtimesheet/feature/integration/controller/SmtpWarningAdvice.java b/src/main/java/com/lab/labtimesheet/feature/integration/controller/SmtpWarningAdvice.java index aaf0d9e..1e8330e 100644 --- a/src/main/java/com/lab/labtimesheet/feature/integration/controller/SmtpWarningAdvice.java +++ b/src/main/java/com/lab/labtimesheet/feature/integration/controller/SmtpWarningAdvice.java @@ -1,6 +1,8 @@ package com.lab.labtimesheet.feature.integration.controller; import com.lab.labtimesheet.feature.integration.service.SmtpConfigurationService; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ModelAttribute; @@ -9,13 +11,10 @@ import org.springframework.web.bind.annotation.ModelAttribute; * configuration is active. */ @ControllerAdvice +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) class SmtpWarningAdvice { private final SmtpConfigurationService smtp; - SmtpWarningAdvice(SmtpConfigurationService smtp) { - this.smtp = smtp; - } - @ModelAttribute("smtpRestricted") boolean smtpRestricted() { return !smtp.hasActiveConfiguration(); diff --git a/src/main/java/com/lab/labtimesheet/feature/integration/model/dto/SmtpActionForm.java b/src/main/java/com/lab/labtimesheet/feature/integration/model/dto/SmtpActionForm.java index d0dad6b..b3e4f1a 100644 --- a/src/main/java/com/lab/labtimesheet/feature/integration/model/dto/SmtpActionForm.java +++ b/src/main/java/com/lab/labtimesheet/feature/integration/model/dto/SmtpActionForm.java @@ -2,18 +2,15 @@ package com.lab.labtimesheet.feature.integration.model.dto; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; +import lombok.Getter; +import lombok.Setter; /** Validated identifier submitted by the SMTP test and activation forms. */ +@Getter +@Setter public class SmtpActionForm { @NotNull(message = "SMTP draft is required") @Positive(message = "SMTP draft is invalid") private Long draftId; - public Long getDraftId() { - return draftId; - } - - public void setDraftId(Long draftId) { - this.draftId = draftId; - } } diff --git a/src/main/java/com/lab/labtimesheet/feature/integration/model/dto/SmtpForm.java b/src/main/java/com/lab/labtimesheet/feature/integration/model/dto/SmtpForm.java index 5810f35..ebd7189 100644 --- a/src/main/java/com/lab/labtimesheet/feature/integration/model/dto/SmtpForm.java +++ b/src/main/java/com/lab/labtimesheet/feature/integration/model/dto/SmtpForm.java @@ -8,11 +8,15 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; /** * Validated Admin input for an SMTP draft. The cleartext password exists only for the current request and is * cleared before the form is rendered again. */ +@Getter +@Setter public class SmtpForm { @NotBlank(message = "Host is required") @Size(max = 255, message = "Host must contain at most 255 characters") @@ -96,18 +100,4 @@ public class SmtpForm { return value == null || value.isEmpty() ? null : value; } - public String getHost() { return host; } - public void setHost(String host) { this.host = host; } - public int getPort() { return port; } - public void setPort(int port) { this.port = port; } - public SecurityMode getSecurityMode() { return securityMode; } - public void setSecurityMode(SecurityMode securityMode) { this.securityMode = securityMode; } - public String getUsername() { return username; } - public void setUsername(String username) { this.username = username; } - public String getPassword() { return password; } - public void setPassword(String password) { this.password = password; } - public String getFromAddress() { return fromAddress; } - public void setFromAddress(String fromAddress) { this.fromAddress = fromAddress; } - public String getFromName() { return fromName; } - public void setFromName(String fromName) { this.fromName = fromName; } } diff --git a/src/main/java/com/lab/labtimesheet/feature/integration/model/entity/SmtpConfiguration.java b/src/main/java/com/lab/labtimesheet/feature/integration/model/entity/SmtpConfiguration.java index 989e81c..cc6cedb 100644 --- a/src/main/java/com/lab/labtimesheet/feature/integration/model/entity/SmtpConfiguration.java +++ b/src/main/java/com/lab/labtimesheet/feature/integration/model/entity/SmtpConfiguration.java @@ -15,6 +15,9 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.persistence.Version; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; /** * Versioned SMTP configuration entity whose credentials remain AES-GCM encrypted at rest. @@ -22,26 +25,33 @@ import jakarta.persistence.Version; */ @Entity @Table(name = "smtp_configurations") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class SmtpConfiguration { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Getter private Long id; @Enumerated(EnumType.STRING) @Column(nullable = false, length = 16) + @Getter private SmtpStatus status; @Column(nullable = false, length = 255) + @Getter private String host; @Column(nullable = false) + @Getter private int port; @Enumerated(EnumType.STRING) @Column(name = "security_mode", nullable = false, length = 16) + @Getter private SecurityMode securityMode; @Column(length = 320) + @Getter private String username; @Column(name = "password_ciphertext") @@ -51,15 +61,19 @@ public class SmtpConfiguration { private byte[] passwordNonce; @Column(name = "secret_key_version") + @Getter private Integer secretKeyVersion; @Column(name = "from_address", nullable = false, length = 320) + @Getter private String fromAddress; @Column(name = "from_name", nullable = false, length = 120) + @Getter private String fromName; @Column(name = "tested_at") + @Getter private Instant testedAt; @Column(name = "tested_by_user_id") @@ -89,10 +103,6 @@ public class SmtpConfiguration { @Version private long version; - /** Required by JPA; revisions are created through {@link #draft}. */ - protected SmtpConfiguration() { - } - /** * Creates an editable SMTP revision with encrypted credential material. * @@ -191,30 +201,6 @@ public class SmtpConfiguration { return value == null || value.isBlank() ? null : value.trim(); } - public Long getId() { - return id; - } - - public SmtpStatus getStatus() { - return status; - } - - public String getHost() { - return host; - } - - public int getPort() { - return port; - } - - public SecurityMode getSecurityMode() { - return securityMode; - } - - public String getUsername() { - return username; - } - /** @return a defensive copy of encrypted password bytes, or {@code null} */ public byte[] getPasswordCiphertext() { return passwordCiphertext == null ? null : passwordCiphertext.clone(); @@ -225,19 +211,4 @@ public class SmtpConfiguration { return passwordNonce == null ? null : passwordNonce.clone(); } - public Integer getSecretKeyVersion() { - return secretKeyVersion; - } - - public String getFromAddress() { - return fromAddress; - } - - public String getFromName() { - return fromName; - } - - public Instant getTestedAt() { - return testedAt; - } } diff --git a/src/main/java/com/lab/labtimesheet/feature/integration/service/MailDeliveryService.java b/src/main/java/com/lab/labtimesheet/feature/integration/service/MailDeliveryService.java index 7075fd0..93e19a6 100644 --- a/src/main/java/com/lab/labtimesheet/feature/integration/service/MailDeliveryService.java +++ b/src/main/java/com/lab/labtimesheet/feature/integration/service/MailDeliveryService.java @@ -4,6 +4,8 @@ import com.lab.labtimesheet.feature.integration.model.dto.SmtpConnection; import com.lab.labtimesheet.feature.integration.model.entity.SmtpConfiguration; import com.lab.labtimesheet.feature.integration.model.SmtpStatus; import com.lab.labtimesheet.feature.integration.repository.SmtpConfigurationRepository; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -12,17 +14,12 @@ import org.springframework.transaction.annotation.Transactional; * Stored credentials are decrypted only while constructing the immediate adapter call. */ @Service +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) public class MailDeliveryService { private final SmtpConfigurationRepository configurations; private final SecretCipher secrets; private final SmtpProbe probe; - MailDeliveryService(SmtpConfigurationRepository configurations, SecretCipher secrets, SmtpProbe probe) { - this.configurations = configurations; - this.secrets = secrets; - this.probe = probe; - } - /** * Reports whether workflows may emit required email. * diff --git a/src/main/java/com/lab/labtimesheet/feature/integration/service/SmtpConfigurationService.java b/src/main/java/com/lab/labtimesheet/feature/integration/service/SmtpConfigurationService.java index 60fabd1..a9f5fc8 100644 --- a/src/main/java/com/lab/labtimesheet/feature/integration/service/SmtpConfigurationService.java +++ b/src/main/java/com/lab/labtimesheet/feature/integration/service/SmtpConfigurationService.java @@ -11,6 +11,8 @@ import com.lab.labtimesheet.feature.integration.model.dto.SmtpDraft; import com.lab.labtimesheet.feature.integration.model.dto.SmtpSetupStatus; import com.lab.labtimesheet.feature.integration.model.entity.SmtpConfiguration; import com.lab.labtimesheet.feature.integration.repository.SmtpConfigurationRepository; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import org.springframework.core.env.Environment; import org.springframework.core.env.Profiles; import org.springframework.stereotype.Service; @@ -21,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional; * A changed draft loses prior test status, and an active revision is retired when its tested successor activates. */ @Service +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) public class SmtpConfigurationService { private final SmtpConfigurationRepository configurations; private final AccountService accounts; @@ -30,18 +33,6 @@ public class SmtpConfigurationService { private final Clock clock; private final MailDeliveryService mailDelivery; - SmtpConfigurationService(SmtpConfigurationRepository configurations, AccountService accounts, - SecretCipher secrets, SmtpProbe probe, Environment environment, Clock clock, - MailDeliveryService mailDelivery) { - this.configurations = configurations; - this.accounts = accounts; - this.secrets = secrets; - this.probe = probe; - this.environment = environment; - this.clock = clock; - this.mailDelivery = mailDelivery; - } - /** * Creates or replaces the editable draft after validating Admin authority and environment transport rules. * Any supplied password is encrypted before persistence and prior test status is cleared.