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
14 changes: 13 additions & 1 deletion .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ on:
jobs:
build:
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
S3_BUCKET: ${{ secrets.S3_BUCKET }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
# GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
# GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
# GOOGLE_REDIRECT_URI: ${{ secrets.GOOGLE_REDIRECT_URI }}
# KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }}
# KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }}
# KAKAO_REDIRECT_URI: ${{ secrets.KAKAO_REDIRECT_URI }}

steps:
- name: ✔️ 리포지토리 가져오기
Expand All @@ -26,4 +38,4 @@ jobs:
run: ./gradlew build -x installLocalGitHook -x spotlessInternalRegisterDependencies -x spotlessJava -x spotlessJavaApply -x spotlessApply -x spotlessJavaCheck -x spotlessCheck -x test

- name: ✔️ Gradle test
run: ./gradlew --info test -Dspring.profiles.active=test
run: ./gradlew --info test -Dspring.profiles.active=test
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ out/
# General
.DS_Store
.AppleDouble
.LSOverride
.LSOverride

### aws 및 환경변수 정보 ###
env/
9 changes: 8 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ dependencies {
testRuntimeOnly 'com.h2database:h2'

// 스웨거
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0'

// 스프링 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'
Expand All @@ -58,6 +58,13 @@ dependencies {

// validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// s3
implementation(platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.1.1"))
implementation("io.awspring.cloud:spring-cloud-aws-starter-s3")

// oauth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}

tasks.named('test') {
Expand Down
19 changes: 19 additions & 0 deletions docker/deploy/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,23 @@ fi
echo "dangling 이미지 삭제"
docker image prune -f

echo "멈춘 container 삭제"
docker container prune -f

for i in {1..10}; do
if [ "$i" -eq 10 ]; then
echo "Health check failed"
docker compose down
exit 1
fi

if curl "http://localhost:8080/health"; then
echo "컨테이너가 정상적으로 실행되었습니다..."
break
fi

echo "spring boot application health check 중..."
sleep 15
done

echo "모든 작업이 완료되었습니다."
5 changes: 0 additions & 5 deletions docker/deploy/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,6 @@ services:
restart: always
depends_on:
- database
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:8080/health" ]
interval: 20s
timeout: 10s
retries: 5

networks:
umc_code_play:
Expand Down
5 changes: 0 additions & 5 deletions env/local-db.env

This file was deleted.

13 changes: 0 additions & 13 deletions env/local-spring.env

This file was deleted.

5 changes: 0 additions & 5 deletions env/prod-db.env

This file was deleted.

14 changes: 0 additions & 14 deletions env/prod-spring.env

This file was deleted.

2 changes: 2 additions & 0 deletions src/main/java/umc/codeplay/CodeplayApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class CodeplayApplication {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,21 @@ public enum ErrorStatus implements BaseErrorCode {

NOT_AUTHORIZED(HttpStatus.BAD_REQUEST, "AUTH400", "인증되지 않은 요청입니다."),
ID_OR_PASSWORD_WRONG(HttpStatus.BAD_REQUEST, "AUTH401", "아이디 혹은 비밀번호가 잘못되었습니다."),
INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "AUTH402", "유효하지 않은 리프레시 토큰입니다.");
INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "AUTH402", "유효하지 않은 리프레시 토큰입니다."),
OAUTH_TOKEN_REQUEST_FAILED(HttpStatus.BAD_REQUEST, "AUTH403", "외부인증 토큰 요청에 실패했습니다."),
OAUTH_USERINFO_REQUEST_FAILED(HttpStatus.BAD_REQUEST, "AUTH404", "외부인증 유저 정보 요청에 실패했습니다."),
AUTHORIZATION_METHOD_ERROR(HttpStatus.BAD_REQUEST, "AUTH405", "인증 방식이 잘못되었습니다."),
INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH406", "유효하지 않은 OAuth 제공자입니다."),

AWS_SERVICE_UNAVAILABLE(HttpStatus.BAD_REQUEST, "AWS400", "AWS S3에 파일을 업로드할 수 없습니다."),
AWS_METHOD_NOT_ALLOWED(
HttpStatus.METHOD_NOT_ALLOWED,
"AWS405",
"AWS S3 presigned url에서 해당 method는 허용되지 않습니다."),

MUSIC_NOT_FOUND(HttpStatus.BAD_REQUEST, "MUSIC400", "음원을 찾을 수 없습니다."),

LIKE_NOT_FOUND(HttpStatus.BAD_REQUEST, "LIKE400", "해당 좋아요를 찾을 수 없습니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/umc/codeplay/config/AWSConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package umc.codeplay.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

@Configuration
public class AWSConfig {

@Value("${spring.cloud.aws.credentials.access-key}")
private String accessKey;

@Value("${spring.cloud.aws.credentials.secret-key}")
private String secretKey;

@Value("${spring.cloud.aws.region.static}")
private String region;

@Bean
public S3Presigner s3Presigner() {
return S3Presigner.builder()
.region(Region.of(region))
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)))
.build();
}
}
6 changes: 3 additions & 3 deletions src/main/java/umc/codeplay/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
auth
// 로그인, 회원가입 등 토큰 없이 접근해야 하는 API 허용
.requestMatchers(
"/oauth/**",
"/health",
"/auth/refresh",
"/auth/signup",
"/auth/login",
"/health/s3",
"/auth/**",
"/v2/api-docs",
"/v3/api-docs",
"/v3/api-docs/**",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package umc.codeplay.config.properties;

import lombok.Data;

@Data
public class BaseOAuthProperties {

private String clientId;
private String clientSecret;
private String redirectUri;
private String scope;
private String authorizationUri;
private String tokenUri;
private String userInfoUri;
private String additionalParameters;

public String getUrl() {
return authorizationUri
+ "?client_id="
+ clientId
+ "&redirect_uri="
+ redirectUri
+ "&response_type=code"
+ "&scope="
+ scope
+ additionalParameters;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package umc.codeplay.config.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "google.oauth2")
public class GoogleOAuthProperties extends BaseOAuthProperties {
// BaseOAuthProperties 의 필드를 그대로 상속받아 사용.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package umc.codeplay.config.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "kakao.oauth2")
public class KakaoOAuthProperties extends BaseOAuthProperties {
// BaseOAuthProperties 의 필드를 그대로 상속받아 사용.
}
22 changes: 11 additions & 11 deletions src/main/java/umc/codeplay/controller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.util.Collection;
import java.util.stream.Collectors;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
Expand All @@ -13,12 +15,12 @@

import lombok.RequiredArgsConstructor;

import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import umc.codeplay.apiPayLoad.ApiResponse;
import umc.codeplay.apiPayLoad.code.status.ErrorStatus;
import umc.codeplay.apiPayLoad.exception.handler.GeneralHandler;
import umc.codeplay.converter.MemberConverter;
import umc.codeplay.domain.Member;
import umc.codeplay.domain.enums.SocialStatus;
import umc.codeplay.dto.MemberRequestDTO;
import umc.codeplay.dto.MemberResponseDTO;
import umc.codeplay.jwt.JwtUtil;
Expand All @@ -36,7 +38,11 @@ public class AuthController {

@PostMapping("/login")
public ApiResponse<MemberResponseDTO.LoginResultDTO> login(
@RequestBody MemberRequestDTO.LoginDto request) {
@Validated @RequestBody MemberRequestDTO.LoginDto request) {
if (memberService.getSocialStatus(request.getEmail()) != SocialStatus.NONE) {
throw new GeneralHandler(ErrorStatus.AUTHORIZATION_METHOD_ERROR);
}

// 아이디/비밀번호를 사용해 AuthenticationToken 생성
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword());
Expand All @@ -62,7 +68,7 @@ public ApiResponse<MemberResponseDTO.LoginResultDTO> login(

@PostMapping("/signup")
public ApiResponse<MemberResponseDTO.JoinResultDTO> join(
@RequestBody MemberRequestDTO.JoinDto request) {
@Validated @RequestBody MemberRequestDTO.JoinDto request) {
Member member = memberService.joinMember(request);
MemberResponseDTO.JoinResultDTO newJoinResult = MemberConverter.toJoinResultDTO(member);

Expand All @@ -71,8 +77,8 @@ public ApiResponse<MemberResponseDTO.JoinResultDTO> join(

@PostMapping("/refresh")
public ApiResponse<MemberResponseDTO.LoginResultDTO> refresh(
@RequestHeader("Refresh-Token") String refreshToken,
@RequestParam("email") String email) {
@RequestHeader("Refresh-Token") @NotNull(message = "리프레시 토큰은 필수 헤더입니다.") String refreshToken,
@Validated @RequestParam("email") @NotBlank(message = "이메일은 필수 입력값입니다.") String email) {
// 리프레시 토큰 유효성 검사
if (jwtUtil.validateToken(refreshToken)
&& (jwtUtil.getTypeFromToken(refreshToken).equals("refresh"))) {
Expand All @@ -98,10 +104,4 @@ public ApiResponse<MemberResponseDTO.LoginResultDTO> refresh(
throw new GeneralHandler(ErrorStatus.INVALID_REFRESH_TOKEN);
}
}

@SecurityRequirement(name = "JWT TOKEN")
@GetMapping("/test")
public ApiResponse<String> test() {
return ApiResponse.onSuccess("test");
}
}
49 changes: 49 additions & 0 deletions src/main/java/umc/codeplay/controller/FileController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package umc.codeplay.controller;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import lombok.RequiredArgsConstructor;

import io.swagger.v3.oas.annotations.Operation;
import software.amazon.awssdk.http.SdkHttpMethod;
import umc.codeplay.apiPayLoad.ApiResponse;
import umc.codeplay.dto.FileResponseDTO;
import umc.codeplay.service.FileService;

import static umc.codeplay.service.FileService.buildFilename;

@RestController
@RequestMapping("/files")
@RequiredArgsConstructor
public class FileController {

private final FileService fileService;

@Operation(
summary = "Download용 Presigned URL 생성",
description = "다운로드를 위한 Presigned URL 생성 - 유효시간 존재")
@GetMapping("/download")
public ApiResponse<FileResponseDTO.DownloadFile> getUrl(
@RequestParam(value = "fileName") String fileName) {
String downloadUrl = fileService.generatePreSignedUrl(fileName, SdkHttpMethod.GET);
FileResponseDTO.DownloadFile result = new FileResponseDTO.DownloadFile(downloadUrl);

return ApiResponse.onSuccess(result);
}

@Operation(
summary = "Upload용 Presigned URL 생성",
description = "업로드를 위한 Presigned URL 생성 - 유효시간 존재")
@PostMapping("/upload")
public ApiResponse<FileResponseDTO.UploadFile> generateUrl(
@RequestParam(value = "fileName") String fileName) {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
String newFileName = buildFilename(fileName);
Long musicId = fileService.uploadMusic(newFileName, username);

String uploadUrl = fileService.generatePreSignedUrl(newFileName, SdkHttpMethod.PUT);
FileResponseDTO.UploadFile result = new FileResponseDTO.UploadFile(uploadUrl, musicId);
return ApiResponse.onSuccess(result);
}
}
Loading
Loading