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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@ out/
### Mac OS ###
.DS_Store

### Claude Code ###
CLAUDE.md

*.yml
application.properties
application.properties
14 changes: 7 additions & 7 deletions scripts/prepare-commit-msg
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
#!/bin/bash
#!/usr/bin/env bash

COMMIT_EDITMSG_FILE_PATH=$1
DEFAULT_COMMIT_MSG=$(cat "$COMMIT_EDITMSG_FILE_PATH")
COMMIT_MSG_FILE_PATH=$1
DEFAULT_COMMIT_MSG=$(<"$COMMIT_MSG_FILE_PATH")

CURRENT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)

ISSUE_NUMBER=$(echo "$CURRENT_BRANCH_NAME" | grep -o '/#.*' | sed 's/\///')
ISSUE_NUMBER=$(grep -oE '#[0-9]+' <<< "$CURRENT_BRANCH_NAME")

if [[ "$DEFAULT_COMMIT_MSG" =~ ^[Mm]erge || "$DEFAULT_COMMIT_MSG" =~ ^[Hh]otfix ]]; then
if [[ "$DEFAULT_COMMIT_MSG" =~ ^([Mm]erge:|[Hh]otfix:) || "$DEFAULT_COMMIT_MSG" =~ \(#[0-9]+\) ]]; then
SUFFIX=""
else
SUFFIX="($ISSUE_NUMBER)"
SUFFIX="${ISSUE_NUMBER:+($ISSUE_NUMBER)}"
fi

echo "$DEFAULT_COMMIT_MSG $SUFFIX" > "$COMMIT_EDITMSG_FILE_PATH"
echo "$DEFAULT_COMMIT_MSG${SUFFIX:+ $SUFFIX}" > "$COMMIT_MSG_FILE_PATH"
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.acon.server.admin.api.controller;

import com.acon.server.admin.api.response.CsrfTokenResponse;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/admin")
public class AdminController {

@GetMapping(path = "/csrf", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<CsrfTokenResponse> getCsrfToken(CsrfToken csrfToken) {
return ResponseEntity.ok(
CsrfTokenResponse.of(
csrfToken.getHeaderName(),
csrfToken.getParameterName(),
csrfToken.getToken()
)
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.acon.server.admin.api.response;

public record CsrfTokenResponse(
String headerName,
String parameterName,
String token
) {

public static CsrfTokenResponse of(
String headerName,
String parameterName,
String token
) {
return new CsrfTokenResponse(headerName, parameterName, token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.acon.server.admin.infra.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "admin")
public class AdminEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false)
private String username;

@Column(nullable = false)
private String password;

@Builder
public AdminEntity(String username, String password) {
this.username = username;
this.password = password;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.acon.server.admin.infra.repository;

import com.acon.server.admin.infra.entity.AdminEntity;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AdminRepository extends JpaRepository<AdminEntity, Long> {

Optional<AdminEntity> findByUsername(String username);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.acon.server.global.admin;

import com.acon.server.global.dto.ErrorResponse;
import com.acon.server.global.exception.ErrorType;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class AdminAccessDeniedHandler implements AccessDeniedHandler {

private final ObjectMapper objectMapper;

@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
ErrorType errorType = ErrorType.ACCESS_DENIED_ERROR;

if (accessDeniedException instanceof MissingCsrfTokenException) {
errorType = ErrorType.MISSING_CSRF_TOKEN_ERROR;
} else if (accessDeniedException instanceof InvalidCsrfTokenException) {
errorType = ErrorType.INVALID_CSRF_TOKEN_ERROR;
}

response.setStatus(errorType.getHttpStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());

objectMapper.writeValue(response.getOutputStream(), ErrorResponse.fail(errorType));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.acon.server.global.admin;

import com.acon.server.global.dto.ErrorResponse;
import com.acon.server.global.exception.ErrorType;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class AdminAuthenticationEntryPoint implements AuthenticationEntryPoint {

private final ObjectMapper objectMapper;

@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
if (authException instanceof InsufficientAuthenticationException) {
ErrorType errorType = ErrorType.UNAUTHORIZED_ERROR;

response.setStatus(errorType.getHttpStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());

objectMapper.writeValue(response.getOutputStream(), ErrorResponse.fail(errorType));

return;
}

response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.acon.server.global.admin;

import com.acon.server.admin.infra.entity.AdminEntity;
import com.acon.server.admin.infra.repository.AdminRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AdminUserDetailsService implements UserDetailsService {

private final AdminRepository adminRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AdminEntity admin = adminRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Admin not found: " + username));

return User.builder()
.username(admin.getUsername())
.password(admin.getPassword())
.roles("ADMIN")
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ public CacheManager cacheManager() {
cacheManager.setCaches(Collections.singletonList(refreshTokenCache));
return cacheManager;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@
import org.springframework.stereotype.Component;

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
// 사용자가 인증은 되었지만 특정 리소스에 접근할 권한이 없을 때 호출

@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException
) {
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) {
setResponse(response);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,15 @@

@Component
@RequiredArgsConstructor
public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
// 사용자가 인증되지 않은 상태에서 보호된 리소스에 접근하려고 할 때 호출

private final ObjectMapper objectMapper;

@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException
) throws IOException {
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
if (authException instanceof JwtAuthenticationException jwtEx) {
ErrorType errorType = jwtEx.getErrorType();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;
private final CustomJwtAuthenticationEntryPoint authenticationEntryPoint;
private final JwtAuthenticationEntryPoint authenticationEntryPoint;

// 각 HTTP 요청에 대해 토큰이 유효한지 확인하고, 유효하다면 해당 사용자를 인증 설정하는 필터링 로직
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,7 @@ public String issueRefreshToken(Long memberId) {
return refreshToken;
}

public String generateToken(
Authentication authentication,
Long tokenExpirationTime
) {
public String generateToken(Authentication authentication, Long tokenExpirationTime) {
final Date now = new Date();

final Claims claims = Jwts.claims()
Expand Down Expand Up @@ -164,5 +161,4 @@ public void deleteRefreshToken(String refreshToken) {
validateRefreshToken(refreshToken);
cache.evict(refreshToken);
}

}
}
Loading