Skip to content
Open
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
6 changes: 6 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 포맷만 바꾼 커밋 목록. git blame에서 제외해 실제 코드 작성자를 추적할 수 있게 한다.
# GitHub은 이 파일을 자동으로 인식한다.
# 로컬 적용: git config blame.ignoreRevsFile .git-blame-ignore-revs

# chore: 전체 코드 spotless 포맷 적용
2befe6b396f6e1904b3d30590c595d1fd926cf57
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@
public record CursorSliceResponse<T>(List<T> content, boolean hasNext, Long nextCursor) {

public static <T> CursorSliceResponse<T> from(CursorSliceResult<T> result) {
return new CursorSliceResponse<>(
result.content(),
result.hasNext(),
result.nextCursor()
);
return new CursorSliceResponse<>(result.content(), result.hasNext(), result.nextCursor());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,4 @@ public class StreamServerApplication {
public static void main(String[] args) {
SpringApplication.run(StreamServerApplication.class, args);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,4 @@ void verify() {
void writeDocs() {
new Documenter(modules).writeDocumentation();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,5 @@
class StreamServerApplicationTests {

@Test
void contextLoads() {
}

void contextLoads() {}
}
11 changes: 11 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import com.diffplug.gradle.spotless.SpotlessExtension

plugins {
alias(libs.plugins.springBoot) apply false
alias(libs.plugins.spotless) apply false
}

allprojects {
Expand All @@ -13,6 +16,7 @@ allprojects {

subprojects {
apply(plugin = "java")
apply(plugin = "com.diffplug.spotless")

// Java 21 (LTS) baseline — docs/conventions/architecture.md §1
extensions.configure<JavaPluginExtension> {
Expand All @@ -33,4 +37,11 @@ subprojects {
tasks.withType<Test> {
useJUnitPlatform()
}

extensions.configure<SpotlessExtension> {
java {
// AOSP 프로파일 — 들여쓰기 4칸, 한 줄 100자
googleJavaFormat().aosp()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ public BusinessException(ErrorCode errorCode) {
}

public BusinessException(ErrorCode errorCode, Object... formatArgs) {
super(formatArgs.length == 0 ? errorCode.message() : errorCode.message().formatted(formatArgs));
super(
formatArgs.length == 0
? errorCode.message()
: errorCode.message().formatted(formatArgs));
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
@Accessors(fluent = true)
@AllArgsConstructor
public enum CommonErrorCode implements ErrorCode {

INVALID_INPUT(ErrorStatus.BAD_REQUEST, "유효하지 않은 입력값입니다."),
UNAUTHORIZED(ErrorStatus.UNAUTHORIZED, "인증이 필요합니다."),
FORBIDDEN(ErrorStatus.FORBIDDEN, "접근 권한이 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package kr.ac.kookmin.stream.common;

/**
* 학생회 부서. ADMIN에게만 부여되며, member 도메인의 학부(Department)와는 다른 개념이다.
*/
/** 학생회 부서. ADMIN에게만 부여되며, member 도메인의 학부(Department)와는 다른 개념이다. */
public enum CouncilDepartment {
PRESIDENCY, // 회장단
EXECUTIVE, // 집행부
GENERAL_AFFAIRS, // 총무부
PLANNING, // 기획부
PR, // 홍보부
MEDIA, // 미디어부
WELFARE, // 복지부
COMMUNICATION // 소통부
PRESIDENCY, // 회장단
EXECUTIVE, // 집행부
GENERAL_AFFAIRS, // 총무부
PLANNING, // 기획부
PR, // 홍보부
MEDIA, // 미디어부
WELFARE, // 복지부
COMMUNICATION // 소통부
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

public interface ErrorCode {
String name();

int status();

String message();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

public interface PrincipalProvider {
Long userId();

Set<Role> roles();

Set<CouncilDepartment> councilDepartments();
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
package kr.ac.kookmin.stream.member;

public record Member(
Long id,
String studentNo,
String name
) {}
public record Member(Long id, String studentNo, String name) {}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
@Accessors(fluent = true)
@AllArgsConstructor
public enum MemberErrorCode implements ErrorCode {

MEMBER_NOT_FOUND(ErrorStatus.NOT_FOUND, "회원을 찾을 수 없습니다.");

private final int status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;

public Member getById(Long id) {
return memberRepository.findById(id)
.orElseThrow(() -> new BusinessException(MemberErrorCode.MEMBER_NOT_FOUND));
return memberRepository
.findById(id)
.orElseThrow(() -> new BusinessException(MemberErrorCode.MEMBER_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,15 @@
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;

/**
* 인증 없이 여는 엔드포인트. SecurityConfig의 permitAll 대상이며 용도별로 묶어 관리한다.
*/
/** 인증 없이 여는 엔드포인트. SecurityConfig의 permitAll 대상이며 용도별로 묶어 관리한다. */
@Getter
@Accessors(fluent = true)
public enum PublicEndpoints {
HEALTH_CHECK(List.of("/actuator/health")),
SWAGGER(List.of("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**"));

HEALTH_CHECK(List.of(
"/actuator/health"
)),
SWAGGER(List.of(
"/swagger-ui/**",
"/swagger-ui.html",
"/v3/api-docs/**"
));

private static final List<PathPattern> ALL_PATH_PATTERNS = Arrays.stream(values())
.flatMap(endpoints -> endpoints.pathPatterns.stream())
.toList();
private static final List<PathPattern> ALL_PATH_PATTERNS =
Arrays.stream(values()).flatMap(endpoints -> endpoints.pathPatterns.stream()).toList();

private final List<String> patterns;
private final List<PathPattern> pathPatterns;
Expand All @@ -39,13 +29,13 @@ public enum PublicEndpoints {

public static String[] allPatterns() {
return Arrays.stream(values())
.flatMap(endpoints -> endpoints.patterns.stream())
.toArray(String[]::new);
.flatMap(endpoints -> endpoints.patterns.stream())
.toArray(String[]::new);
}

public static boolean isPublic(String path) {
PathContainer pathContainer = PathContainer.parsePath(path);
return ALL_PATH_PATTERNS.stream()
.anyMatch(pathPattern -> pathPattern.matches(pathContainer));
.anyMatch(pathPattern -> pathPattern.matches(pathContainer));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,28 @@ public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(request -> request
.requestMatchers(PublicEndpoints.allPatterns()).permitAll()
.requestMatchers("/v1/admin/**").hasAuthority(Role.ADMIN.name())
.requestMatchers("/v1/app/**").hasAuthority(Role.STUDENT.name())
.anyRequest().authenticated())
.exceptionHandling(exception -> exception
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler))
// ExceptionTranslationFilter 뒤에 두어야 필터가 던진 인증 예외가 EntryPoint로 넘어간다
.addFilterBefore(JwtAuthFilter.of(jwtProvider), AuthorizationFilter.class)
.build();
return http.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(
request ->
request.requestMatchers(PublicEndpoints.allPatterns())
.permitAll()
.requestMatchers("/v1/admin/**")
.hasAuthority(Role.ADMIN.name())
.requestMatchers("/v1/app/**")
.hasAuthority(Role.STUDENT.name())
.anyRequest()
.authenticated())
.exceptionHandling(
exception ->
exception
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler))
// ExceptionTranslationFilter 뒤에 두어야 필터가 던진 인증 예외가 EntryPoint로 넘어간다
.addFilterBefore(JwtAuthFilter.of(jwtProvider), AuthorizationFilter.class)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;

/**
* 인증은 됐으나 권한이 없는 요청(403)을 공통 에러 응답으로 내보낸다.
*/
/** 인증은 됐으나 권한이 없는 요청(403)을 공통 에러 응답으로 내보낸다. */
@Component
@RequiredArgsConstructor
public class RestAccessDeniedHandler implements AccessDeniedHandler {
Expand All @@ -23,11 +21,10 @@ public class RestAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException
) {
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) {
handlerExceptionResolver.resolveException(
request, response, null, new BusinessException(CommonErrorCode.FORBIDDEN));
request, response, null, new BusinessException(CommonErrorCode.FORBIDDEN));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;

/**
* 인증되지 않은 요청(401). 로그인 페이지로 리다이렉트하는 기본 동작 대신 공통 에러 응답으로 내보낸다.
*/
/** 인증되지 않은 요청(401). 로그인 페이지로 리다이렉트하는 기본 동작 대신 공통 에러 응답으로 내보낸다. */
@Component
@RequiredArgsConstructor
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
Expand All @@ -23,11 +21,10 @@ public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authenticationException
) {
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authenticationException) {
handlerExceptionResolver.resolveException(
request, response, null, new BusinessException(CommonErrorCode.UNAUTHORIZED));
request, response, null, new BusinessException(CommonErrorCode.UNAUTHORIZED));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import org.springframework.security.core.AuthenticationException;

/**
* 토큰이 만료됐거나 서명·형식이 올바르지 않을 때. ExceptionTranslationFilter가 잡아
* SecurityConfig에 설정된 AuthenticationEntryPoint로 넘긴다.
* 토큰이 만료됐거나 서명·형식이 올바르지 않을 때. ExceptionTranslationFilter가 잡아 SecurityConfig에 설정된
* AuthenticationEntryPoint로 넘긴다.
*/
public class InvalidTokenException extends AuthenticationException {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,12 @@ public static JwtAuthFilter of(JwtProvider jwtProvider) {

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null) {
SecurityContextHolder.getContext().setAuthentication(UserAuthentication.from(jwtProvider.parse(token)));
SecurityContextHolder.getContext()
.setAuthentication(UserAuthentication.from(jwtProvider.parse(token)));
}
filterChain.doFilter(request, response);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,4 @@
import kr.ac.kookmin.stream.common.CouncilDepartment;
import kr.ac.kookmin.stream.common.Role;

public record JwtPayload(
Long userId,
Set<Role> roles,
Set<CouncilDepartment> councilDepartments
) {}
public record JwtPayload(Long userId, Set<Role> roles, Set<CouncilDepartment> councilDepartments) {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,4 @@
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "jwt")
public record JwtProperties(
String secretKey,
String issuer,
long accessTokenExpiry
) {}
public record JwtProperties(String secretKey, String issuer, long accessTokenExpiry) {}
Loading