-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] 주요 변경 작업 감사로그 기록 기능 추가 #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| package com.jobdri.jobdri_api.domain.audit.entity; | ||
|
|
||
| import com.jobdri.jobdri_api.domain.user.entity.User; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.FetchType; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.ManyToOne; | ||
| import jakarta.persistence.Table; | ||
| import lombok.AccessLevel; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Entity | ||
| @Table(name = "audit_logs") | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class AuditLog { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "user_id") | ||
| private User user; | ||
|
|
||
| @Column(nullable = false, length = 80) | ||
| private String action; | ||
|
|
||
| @Column(nullable = false, length = 80) | ||
| private String targetType; | ||
|
|
||
| private Long targetId; | ||
|
|
||
| @Column(columnDefinition = "TEXT") | ||
| private String beforeValue; | ||
|
|
||
| @Column(columnDefinition = "TEXT") | ||
| private String afterValue; | ||
|
|
||
| @Column(length = 100) | ||
| private String ipAddress; | ||
|
|
||
| @Column(length = 500) | ||
| private String userAgent; | ||
|
|
||
| @Column(nullable = false) | ||
| private LocalDateTime createdAt; | ||
|
|
||
| @Builder(access = AccessLevel.PRIVATE) | ||
| private AuditLog( | ||
| User user, | ||
| String action, | ||
| String targetType, | ||
| Long targetId, | ||
| String beforeValue, | ||
| String afterValue, | ||
| String ipAddress, | ||
| String userAgent, | ||
| LocalDateTime createdAt | ||
| ) { | ||
| this.user = user; | ||
| this.action = action; | ||
| this.targetType = targetType; | ||
| this.targetId = targetId; | ||
| this.beforeValue = beforeValue; | ||
| this.afterValue = afterValue; | ||
| this.ipAddress = ipAddress; | ||
| this.userAgent = userAgent; | ||
| this.createdAt = createdAt; | ||
| } | ||
|
|
||
| public static AuditLog create( | ||
| User user, | ||
| String action, | ||
| String targetType, | ||
| Long targetId, | ||
| String beforeValue, | ||
| String afterValue, | ||
| String ipAddress, | ||
| String userAgent | ||
| ) { | ||
| return AuditLog.builder() | ||
| .user(user) | ||
| .action(action) | ||
| .targetType(targetType) | ||
| .targetId(targetId) | ||
| .beforeValue(beforeValue) | ||
| .afterValue(afterValue) | ||
| .ipAddress(ipAddress) | ||
| .userAgent(userAgent) | ||
| .createdAt(LocalDateTime.now()) | ||
| .build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.jobdri.jobdri_api.domain.audit.repository; | ||
|
|
||
| import com.jobdri.jobdri_api.domain.audit.entity.AuditLog; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface AuditLogRepository extends JpaRepository<AuditLog, Long> { | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,95 @@ | ||||||||||||||||||||||||||||||
| package com.jobdri.jobdri_api.domain.audit.service; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||||||||||||||||||||||||||||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||||||||||||||||||||||||||||
| import com.jobdri.jobdri_api.domain.audit.entity.AuditLog; | ||||||||||||||||||||||||||||||
| import com.jobdri.jobdri_api.domain.audit.repository.AuditLogRepository; | ||||||||||||||||||||||||||||||
| import com.jobdri.jobdri_api.domain.user.entity.User; | ||||||||||||||||||||||||||||||
| import jakarta.servlet.http.HttpServletRequest; | ||||||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Service; | ||||||||||||||||||||||||||||||
| import org.springframework.transaction.annotation.Propagation; | ||||||||||||||||||||||||||||||
| import org.springframework.transaction.annotation.Transactional; | ||||||||||||||||||||||||||||||
| import org.springframework.web.context.request.RequestContextHolder; | ||||||||||||||||||||||||||||||
| import org.springframework.web.context.request.ServletRequestAttributes; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @Service | ||||||||||||||||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||||||||||||||||
| public class AuditLogService { | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private static final int MAX_USER_AGENT_LENGTH = 500; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private final AuditLogRepository auditLogRepository; | ||||||||||||||||||||||||||||||
| private final ObjectMapper objectMapper; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @Transactional(propagation = Propagation.MANDATORY) | ||||||||||||||||||||||||||||||
| public void record( | ||||||||||||||||||||||||||||||
| User user, | ||||||||||||||||||||||||||||||
| String action, | ||||||||||||||||||||||||||||||
| String targetType, | ||||||||||||||||||||||||||||||
| Long targetId, | ||||||||||||||||||||||||||||||
| Object beforeValue, | ||||||||||||||||||||||||||||||
| Object afterValue | ||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||
| HttpServletRequest request = currentRequest(); | ||||||||||||||||||||||||||||||
| auditLogRepository.save(AuditLog.create( | ||||||||||||||||||||||||||||||
| user, | ||||||||||||||||||||||||||||||
| action, | ||||||||||||||||||||||||||||||
| targetType, | ||||||||||||||||||||||||||||||
| targetId, | ||||||||||||||||||||||||||||||
| toJson(beforeValue), | ||||||||||||||||||||||||||||||
| toJson(afterValue), | ||||||||||||||||||||||||||||||
| resolveIpAddress(request), | ||||||||||||||||||||||||||||||
| truncate(resolveUserAgent(request), MAX_USER_AGENT_LENGTH) | ||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private HttpServletRequest currentRequest() { | ||||||||||||||||||||||||||||||
| if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attributes) { | ||||||||||||||||||||||||||||||
| return attributes.getRequest(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private String resolveIpAddress(HttpServletRequest request) { | ||||||||||||||||||||||||||||||
| if (request == null) { | ||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| String forwardedFor = request.getHeader("X-Forwarded-For"); | ||||||||||||||||||||||||||||||
| if (forwardedFor != null && !forwardedFor.isBlank()) { | ||||||||||||||||||||||||||||||
| return forwardedFor.split(",")[0].trim(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| String realIp = request.getHeader("X-Real-IP"); | ||||||||||||||||||||||||||||||
| if (realIp != null && !realIp.isBlank()) { | ||||||||||||||||||||||||||||||
| return realIp.trim(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return request.getRemoteAddr(); | ||||||||||||||||||||||||||||||
|
Comment on lines
+59
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Harden IP capture against header spoofing. Line 59–66 trust client-supplied forwarding headers directly, so audit IP can be forged. For audit integrity, only use trusted proxy-resolved remote address at this layer. Suggested fix private String resolveIpAddress(HttpServletRequest request) {
if (request == null) {
return null;
}
-
- String forwardedFor = request.getHeader("X-Forwarded-For");
- if (forwardedFor != null && !forwardedFor.isBlank()) {
- return forwardedFor.split(",")[0].trim();
- }
- String realIp = request.getHeader("X-Real-IP");
- if (realIp != null && !realIp.isBlank()) {
- return realIp.trim();
- }
return request.getRemoteAddr();
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private String resolveUserAgent(HttpServletRequest request) { | ||||||||||||||||||||||||||||||
| if (request == null) { | ||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return request.getHeader("User-Agent"); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private String toJson(Object value) { | ||||||||||||||||||||||||||||||
| if (value == null) { | ||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| return objectMapper.writeValueAsString(value); | ||||||||||||||||||||||||||||||
| } catch (JsonProcessingException e) { | ||||||||||||||||||||||||||||||
| return String.valueOf(value); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+82
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep Line 85 currently returns raw Suggested fix try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
- return String.valueOf(value);
+ try {
+ return objectMapper.writeValueAsString(String.valueOf(value));
+ } catch (JsonProcessingException ignored) {
+ return "\"<unserializable>\"";
+ }
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private String truncate(String value, int maxLength) { | ||||||||||||||||||||||||||||||
| if (value == null || value.length() <= maxLength) { | ||||||||||||||||||||||||||||||
| return value; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return value.substring(0, maxLength); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Audit failure currently triggers an incorrect credit refund path.
At Line 84,
auditLogService.record(...)is inside the sametryas the analysis execution, and Line 101 catches allRuntimeExceptions. If audit logging fails after analysis is already saved, Line 102 refunds credit even though the analysis actually succeeded.Suggested fix
🤖 Prompt for AI Agents