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
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
JWT_ACCESS_EXPIRATION: ${{ secrets.JWT_ACCESS_EXPIRATION }}
JWT_REFRESH_EXPIRATION: ${{ secrets.JWT_REFRESH_EXPIRATION }}
PROD_ORIGIN: ${{ secrets.PROD_ORIGIN }}
MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }}
MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }}

- name: Build Spring Boot JAR (skip tests)
run: ./gradlew clean build -x test
Expand Down
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ dependencies {

implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-websocket'

implementation 'org.springframework.boot:spring-boot-starter-mail'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,51 @@
package dev.codehouse.backend.global.config;

import dev.codehouse.backend.global.security.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//임시 설정
http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
http.csrf(AbstractHttpConfigurer::disable);
// http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
http.csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.authorizeHttpRequests(auth -> auth
.requestMatchers("api/auth/**", "api/user/toprank/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import dev.codehouse.backend.global.response.ApiResponse;
import dev.codehouse.backend.global.response.ResponseCode;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
Expand All @@ -18,6 +20,17 @@ public ResponseEntity<ApiResponse<Void>> handleBaseException(BaseException e) {
.body(ApiResponse.error(e.getCode()));
}

//Dto Valid 관련 예외 처리
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Void>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
FieldError fieldError = e.getBindingResult().getFieldError();
String message = (fieldError != null) ? fieldError.getDefaultMessage() : "잘못된 요청입니다.";

return ResponseEntity
.status(ResponseCode.INVALID_REQUEST.getStatus())
.body(ApiResponse.error(ResponseCode.INVALID_REQUEST.getHttpStatusCode(), message));
}

@ExceptionHandler(DuplicateKeyException.class)
public ResponseEntity<ApiResponse<Void>> handleDuplicateKeyException(DuplicateKeyException e) {
return ResponseEntity.status(ResponseCode.DUPLICATE_USERNAME.getStatus())
Expand All @@ -31,32 +44,11 @@ public ResponseEntity<ApiResponse<Void>> handleAuthException(BaseException e) {
.body(ApiResponse.error(e.getCode()));
}





// @ExceptionHandler(IllegalArgumentException.class)
// public ResponseEntity<ApiResponse<Void>> handleIllegalArgumentException(IllegalArgumentException e) {
// ResponseCode responseCode = determineResponseCode(e.getMessage());
// return ResponseEntity.status(responseCode.getStatus())
// .body(ApiResponse.error(responseCode));
// }

@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ApiResponse<Void>> handleRuntimeException(RuntimeException e) {
ResponseCode responseCode = e.getMessage().contains("API") ?
ResponseCode.EXTERNAL_API_ERROR : ResponseCode.DATABASE_ERROR;
return ResponseEntity.status(responseCode.getStatus())
.body(ApiResponse.error(responseCode));
}

// private ResponseCode determineResponseCode(String message) {
// if (message.contains("사용자를 찾을 수 없") || message.contains("존재하지 않는")) {
// return ResponseCode.USER_NOT_FOUND;
// } else if (message.contains("비밀번호") || message.contains("일치하지 않")) {
// return ResponseCode.INVALID_PASSWORD;
// } else {
// return ResponseCode.INVALID_REQUEST;
// }
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public enum ResponseCode {
USER_LOGIN_SUCCESS(HttpStatus.OK, "로그인에 성공했습니다."),
USER_REGISTER_SUCCESS(HttpStatus.CREATED, "회원가입에 성공했습니다"),
USER_CONFIRM_SUCCESS(HttpStatus.OK,"사용자 확인에 성공했습니다"),
REPORT_SUBMIT_SUCCESS(HttpStatus.OK, "신고 제출에 성공했습니다."),
NOTICE_FOUND(HttpStatus.OK, "공지사항 조회 성공"),
NOTICE_UPDATED(HttpStatus.OK, "공지사항 수정 완료"),
USER_POINT_UPDATED(HttpStatus.OK, "사용자 포인트가 수정되었습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dev.codehouse.backend.report.controller;

import dev.codehouse.backend.global.response.ApiResponse;
import dev.codehouse.backend.global.response.ApiResponseFactory;
import dev.codehouse.backend.global.response.ResponseCode;
import dev.codehouse.backend.report.dto.ReportRequest;
import dev.codehouse.backend.report.service.ReportService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/report")
@RequiredArgsConstructor
public class ReportController {

private final ReportService reportService;

@PostMapping("")
public ResponseEntity<ApiResponse<Void>> submitReport(@RequestBody @Valid ReportRequest request) {
reportService.submitReport(request.getReportType(), request.getEmail(), request.getContent());
return ApiResponseFactory.success(ResponseCode.REPORT_SUBMIT_SUCCESS);
}
}
21 changes: 21 additions & 0 deletions src/main/java/dev/codehouse/backend/report/dto/ReportRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dev.codehouse.backend.report.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;

@Getter
public class ReportRequest {
@NotNull(message = "신고 유형은 필수입니다.")
private ReportType reportType; // CHEATING, SPAM, BUG, ETC

@NotBlank(message = "이메일을 입력해주세요.")
@Email(message = "올바른 이메일 형식이 아닙니다.")
private String email;

@NotBlank(message = "내용을 입력해주세요.")
@Size(max = 200, message = "내용은 200자 이하로 작성해주세요.")
private String content;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dev.codehouse.backend.report.dto;

public enum ReportType {
CHEATING,
SPAM,
BUG,
ETC;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dev.codehouse.backend.report.service;

import dev.codehouse.backend.report.dto.ReportType;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ReportService {

private final JavaMailSender mailSender;
private final String ADMIN_EMAIL = "jungwy980@gmail.com";

public void submitReport(ReportType type, String email, String content) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(ADMIN_EMAIL);
message.setSubject("[신고 접수]" + type);
message.setText(
"신고 유형: " + type + "\n" +
"회신 이메일: " + email + "\n" +
"신고 내용:\n" + content
);

mailSender.send(message);
}
}
8 changes: 8 additions & 0 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ spring:
redis:
host: cote-redis
port: 6379
mail:
host: smtp.gmail.com
port: 587
username: ${MAIL_USERNAME}
password: ${MAIL_PASSWORD}
properties:
mail.smtp.auth: true
mail.smtp.starttls.enable: true
jwt:
secret: ${JWT_SECRET}
access-expiration: ${JWT_ACCESS_EXPIRATION}
Expand Down