refactor(platform): target Lombok boilerplate
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
+3
-4
@@ -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")) {
|
||||
|
||||
+2
-9
@@ -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.
|
||||
*
|
||||
|
||||
+3
-4
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-10
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
+10
-27
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
+3
-4
@@ -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.
|
||||
*
|
||||
|
||||
+3
-5
@@ -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);
|
||||
|
||||
+3
-4
@@ -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();
|
||||
|
||||
+4
-7
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
+14
-43
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-6
@@ -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.
|
||||
*
|
||||
|
||||
+3
-12
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user