Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
public record LoginRequest(
@NotBlank(message = "이메일을 입력해주세요.")
String email,

@NotBlank(message = "비밀번호를 입력해주세요.")
String password
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
import jakarta.validation.constraints.Pattern;

public record PasswordResetRequest(
@NotBlank @Email
@NotBlank(message = "이메일을 입력해주세요.")
@Email(message = "유효한 이메일 주소를 입력해주세요.")
String email,

@NotBlank
@NotBlank(message = "리셋 토큰을 입력해주세요.")
String resetToken,

@NotBlank
@NotBlank(message = "새 비밀번호를 입력해주세요.")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,}$",
message = "비밀번호는 영문, 숫자, 특수문자를 포함하여 8자 이상이어야 합니다.")
String newPassword
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import jakarta.validation.constraints.NotNull;

public record SocialLoginRequest(
@NotNull UserProvider provider,
@NotBlank String accessToken
@NotNull(message = "소셜 로그인 제공자를 선택해주세요.")
UserProvider provider,

@NotBlank(message = "액세스 토큰을 입력해주세요.")
String accessToken
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
import jakarta.validation.constraints.NotNull;

public record VerificationRequest(
@NotBlank @Email
@NotBlank(message = "이메일을 입력해주세요.")
@Email(message = "유효한 이메일 주소를 입력해주세요.")
String email,

@NotNull
@NotNull(message = "검증 목적을 선택해주세요.")
VerificationPurpose purpose
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
import jakarta.validation.constraints.NotNull;

public record VerificationVerifyRequest(
@NotBlank @Email
@NotBlank(message = "이메일을 입력해주세요.")
@Email(message = "유효한 이메일 주소를 입력해주세요.")
String email,

@NotBlank
@NotBlank(message = "검증 코드를 입력해주세요.")
String code,

@NotNull
@NotNull(message = "검증 목적을 선택해주세요.")
VerificationPurpose purpose
) {
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.loopon.auth.application.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;

public record AuthResult(
@Schema(description = "액세스 토큰 (Bearer)", example = "eyJhbGciOiJIUzI1NiJ9...")
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 논의 필요: accessToken의 Schema 설명이 "(Bearer)"로 되어 있어 응답 값 자체에 Bearer 접두어가 포함되는 것으로 오해될 수 있습니다. 실제로는 JWT 문자열만 내려주고 클라이언트가 Authorization: Bearer <token> 형태로 붙이는 구조라면, 설명을 "액세스 토큰(JWT). Authorization 헤더에는 Bearer 접두어를 붙여 전송"처럼 더 명확히 해주는 게 좋습니다.

Suggested change
@Schema(description = "액세스 토큰 (Bearer)", example = "eyJhbGciOiJIUzI1NiJ9...")
@Schema(description = "액세스 토큰(JWT). Authorization 헤더 전송 시 'Bearer ' 접두어를 붙여 사용", example = "eyJhbGciOiJIUzI1NiJ9...")

Copilot uses AI. Check for mistakes.
String accessToken,

@Schema(description = "리프레시 토큰", example = "d1f2e3c4b5a6...")
String refreshToken
) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
@Component
@RequiredArgsConstructor
public class AppleAuthClientAdapter {
private final RestClient kakaoRestClient;
private final RestClient appleRestClient;
private final AppleClientSecretGenerator secretGenerator;

@Value("${apple.client-id}")
Expand All @@ -33,7 +33,7 @@ public AppleTokenResponse getTokens(String authorizationCode) {
params.add("grant_type", "authorization_code");

try {
return kakaoRestClient.post()
return appleRestClient.post()
.uri("https://appleid.apple.com/auth/token")
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 논의 필요: appleRestClientbaseUrl("https://appleid.apple.com")로 빈 등록해두었는데, 여기서는 .uri("https://appleid.apple.com/auth/token")로 절대 URL을 다시 지정하고 있어 baseUrl 설정이 사실상 무의미해집니다. 상대 경로(/auth/token)를 사용하도록 맞추거나, 반대로 baseUrl 설정을 제거해 한 곳에서만 URL을 관리하도록 정리하는 편이 유지보수에 유리합니다.

Suggested change
.uri("https://appleid.apple.com/auth/token")
.uri("/auth/token")

Copilot uses AI. Check for mistakes.
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(params)
Expand Down

This file was deleted.

7 changes: 7 additions & 0 deletions src/main/java/com/loopon/global/config/RestClientConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ public RestClient kakaoRestClient() {
.build();
}

@Bean("appleRestClient")
public RestClient appleRestClient() {
return RestClient.builder()
.baseUrl("https://appleid.apple.com")
.build();
}

@Bean("geminiRestClient")
public RestClient geminiRestClient() {
return RestClient.builder()
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/loopon/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class SecurityConfig {

private static final String[] PUBLIC_URLS = {
"/",
"/health",
"/favicon.ico",
Comment on lines 33 to 35
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 반드시 수정: PUBLIC_URLS에 포함된 "/api/terms/{termId}"는 Spring Security의 Ant 패턴 매칭에서 PathVariable 템플릿으로 해석되지 않아 /api/terms/1 같은 실제 요청이 permitAll로 처리되지 않습니다. 단순히 */**로 치환하면 PATCH /api/terms/{termId}(약관 동의 수정)까지 공개될 수 있으니, HttpMethod.GET에 한정한 requestMatcher로 /api/terms/api/terms/*만 permitAll 처리하도록 분리하는 방식이 안전합니다.

Copilot uses AI. Check for mistakes.
"/v3/api-docs/**",
"/swagger-ui/**",
Expand All @@ -50,7 +51,7 @@ public class SecurityConfig {
private static final String[] API_URLS = {
"/api/users/me",
"/api/users/profile",
"/api/users/password",
"/api/users/password"
};

@Bean
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/com/loopon/global/health/HealthCheckController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.loopon.global.health;

import com.loopon.global.domain.dto.CommonResponse;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.Map;

@RestController
@Hidden
@RequiredArgsConstructor
public class HealthCheckController {

@Value("${spring.profiles.active:default}")
private String activeProfile;

@GetMapping("/")
public ResponseEntity<CommonResponse<Map<String, String>>> systemStatus() {
Map<String, String> status = Map.of(
"status", "UP",
"profile", activeProfile,
"serverTime", LocalDateTime.now().toString(),
"message", "LoopOn API Server is running!"
);
Comment on lines +22 to +29
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 논의 필요: / 엔드포인트가 profile, serverTime 등을 포함한 상세 상태 정보를 인증 없이 반환하고 있고(SecurityConfig/JWT 필터에서도 public 처리), 운영 환경/프로파일 정보가 외부에 노출될 수 있습니다. 단순 헬스체크 목적이면 /health처럼 최소한의 고정 응답만 주거나, 상세 정보는 actuator/내부망/관리자 권한으로 제한하는 구성이 더 안전합니다.

Copilot uses AI. Check for mistakes.

return ResponseEntity.ok(CommonResponse.onSuccess(status));
}

@GetMapping("/health")
public String healthCheck() {
return "OK";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final List<String> excludeUrlPatterns = List.of(
"/",
"/health",
"/favicon.ico",
Comment on lines 30 to 32
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 반드시 수정: excludeUrlPatterns"/api/terms/{termId}"AntPathMatcher에서 PathVariable 템플릿으로 처리되지 않아 /api/terms/1에 매칭되지 않습니다. 또한 이 리스트는 HTTP method를 구분하지 않기 때문에, 와일드카드로 수정 시 PATCH /api/terms/{termId} 같은 인증 필요 API까지 필터가 스킵될 수 있습니다. 공개가 필요한 경우라도 shouldNotFilter에서 method까지 함께 검사하거나(예: GET만 스킵), 해당 패턴을 제외하는 형태로 정리하는 게 안전합니다.

Copilot uses AI. Check for mistakes.
"/v3/api-docs/**",
"/swagger-ui/**",
Expand All @@ -40,6 +41,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
"/api/users/upload-profile-image",
"/api/auth/login/**",
"/api/auth/reissue",
"/api/auth/logout",
"/api/auth/verification/**"
);

Expand Down