diff --git a/.github/workflows/server-ci-spring.yml b/.github/workflows/server-ci-spring.yml index aaf6fc4..638641a 100644 --- a/.github/workflows/server-ci-spring.yml +++ b/.github/workflows/server-ci-spring.yml @@ -1,72 +1,80 @@ -name: Backend Spring CI +# name: Backend Spring CI -on: - pull_request: - branches: [develop] - paths: - - "server/spring/**" +# on: +# pull_request: +# branches: [develop] +# paths: +# - "server/spring/**" -defaults: - run: - working-directory: ./server/spring +# defaults: +# run: +# working-directory: ./server/spring -jobs: - BACKEND-SPRING-CI: - runs-on: ubuntu-20.04 +# jobs: +# BACKEND-SPRING-CI: +# runs-on: ubuntu-20.04 - steps: - - name: Checkout - uses: actions/checkout@v4 +# steps: +# - name: Checkout +# uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: 17 - distribution: "adopt" +# - name: Set up JDK 17 +# uses: actions/setup-java@v4 +# with: +# java-version: 17 +# distribution: "adopt" - - name: Create application.yml - env: - DB_HOST_IP: ${{ secrets.SERVER_ENV_DB_HOST_IP }} - DB_PORT: ${{ secrets.SERVER_ENV_DB_PORT }} - DB_USER_NAME: ${{ secrets.SERVER_ENV_DB_USER_NAME }} - DB_PASSWORD: ${{ secrets.SERVER_ENV_DB_PASSWORD }} - DB_DATABASE_NAME: ${{ secrets.SERVER_ENV_DB_DATABASE_NAME }} - ACCESS_ID: ${{ secrets.SERVER_ENV_ACCESS_ID }} - SECRET_ACCESS_KEY: ${{ secrets.SERVER_ENV_SECRET_ACCESS_KEY }} - JWT_SECRET_KEY: ${{ secrets.SERVER_ENV_JWT_SECRET_KEY }} - GREEN_EYE_SECRET_KEY: ${{secrets.GREEN_EYE_SECRET_KEY}} - GREEN_EYE_REQUEST_URL: ${{secrets.GREEN_EYE_REQUEST_URL}} - CLOUD_FUNCTIONS_EXECUTE_URL: ${{secrets.CLOUD_FUNCTIONS_EXECUTE_URL}} - CLOUD_FUNCTIONS_REQUEST_URL: ${{secrets.CLOUD_FUNCTIONS_REQUEST_URL}} - API_GW_ACCESS_KEY: ${{secrets.API_GW_ACCESS_KEY}} - REFRESH_SECRET_KEY: ${{secrets.REFRESH_SECRET_KEY}} - REDIS_HOST_IP: ${{secrets.REDIS_HOST_IP}} - REDIS_PASSWORD: ${{secrets.REDIS_PASSWORD}} - REDIS_PORT: ${{secrets.REDIS_PORT}} - REDIS_TTL: ${{secrets.REDIS_TTL}} - run: | - touch application.yml - echo "DB_HOST_IP=$DB_HOST_IP" >> application.yml - echo "DB_PORT=$DB_PORT" >> application.yml - echo "DB_USER_NAME=$DB_USER_NAME" >> application.yml - echo "DB_PASSWORD=$DB_PASSWORD" >> application.yml - echo "DB_DATABASE_NAME=$DB_DATABASE_NAME" >> application.yml - echo "ACCESS_ID=$ACCESS_ID" >> application.yml - echo "SECRET_ACCESS_KEY=$SECRET_ACCESS_KEY" >> application.yml - echo "JWT_SECRET_KEY=$JWT_SECRET_KEY" >> application.yml - echo "GREEN_EYE_SECRET_KEY=$GREEN_EYE_SECRET_KEY" >> application.yml - echo "GREEN_EYE_REQUEST_URL=$GREEN_EYE_REQUEST_URL" >> application.yml - echo "CLOUD_FUNCTIONS_EXECUTE_URL=$CLOUD_FUNCTIONS_EXECUTE_URL" >> application.yml - echo "CLOUD_FUNCTIONS_REQUEST_URL=$CLOUD_FUNCTIONS_REQUEST_URL" >> application.yml - echo "API_GW_ACCESS_KEY=$API_GW_ACCESS_KEY" >> application.yml - echo "REFRESH_SECRET_KEY=$REFRESH_SECRET_KEY" >> application.yml - echo "REDIS_HOST_IP=$REDIS_HOST_IP" >> application.yml - echo "REDIS_PASSWORD=$REDIS_PASSWORD" >> application.yml - echo "REDIS_PORT=$REDIS_PORT" >> application.yml - echo "REDIS_TTL=$REDIS_TTL" >> application.yml +# - name: Create application.properties +# env: +# DB_HOST_IP: ${{ secrets.SERVER_ENV_DB_HOST_IP }} +# DB_PORT: ${{ secrets.SERVER_ENV_DB_PORT }} +# DB_USER_NAME: ${{ secrets.SERVER_ENV_DB_USER_NAME }} +# DB_PASSWORD: ${{ secrets.SERVER_ENV_DB_PASSWORD }} +# DB_DATABASE_NAME: ${{ secrets.SERVER_ENV_DB_DATABASE_NAME }} +# ACCESS_ID: ${{ secrets.SERVER_ENV_ACCESS_ID }} +# SECRET_ACCESS_KEY: ${{ secrets.SERVER_ENV_SECRET_ACCESS_KEY }} +# JWT_SECRET_KEY: ${{ secrets.SERVER_ENV_JWT_SECRET_KEY }} +# GREEN_EYE_SECRET_KEY: ${{secrets.GREEN_EYE_SECRET_KEY}} +# GREEN_EYE_REQUEST_URL: ${{secrets.GREEN_EYE_REQUEST_URL}} +# CLOUD_FUNCTIONS_EXECUTE_URL: ${{secrets.CLOUD_FUNCTIONS_EXECUTE_URL}} +# CLOUD_FUNCTIONS_REQUEST_URL: ${{secrets.CLOUD_FUNCTIONS_REQUEST_URL}} +# API_GW_ACCESS_KEY: ${{secrets.API_GW_ACCESS_KEY}} +# REFRESH_SECRET_KEY: ${{secrets.REFRESH_SECRET_KEY}} +# REDIS_HOST_IP: ${{secrets.REDIS_HOST_IP}} +# REDIS_PASSWORD: ${{secrets.REDIS_PASSWORD}} +# REDIS_PORT: ${{secrets.REDIS_PORT}} +# REDIS_TTL: ${{secrets.REDIS_TTL}} +# run: | +# touch application.properties +# echo "DB_HOST_IP=$DB_HOST_IP" >> application.properties +# echo "DB_PORT=$DB_PORT" >> application.properties +# echo "DB_USER_NAME=$DB_USER_NAME" >> application.properties +# echo "DB_PASSWORD=$DB_PASSWORD" >> application.properties +# echo "DB_DATABASE_NAME=$DB_DATABASE_NAME" >> application.properties +# echo "ACCESS_ID=$ACCESS_ID" >> application.properties +# echo "SECRET_ACCESS_KEY=$SECRET_ACCESS_KEY" >> application.properties +# echo "JWT_SECRET_KEY=$JWT_SECRET_KEY" >> application.properties +# echo "GREEN_EYE_SECRET_KEY=$GREEN_EYE_SECRET_KEY" >> application.properties +# echo "GREEN_EYE_REQUEST_URL=$GREEN_EYE_REQUEST_URL" >> application.properties +# echo "CLOUD_FUNCTIONS_EXECUTE_URL=$CLOUD_FUNCTIONS_EXECUTE_URL" >> application.properties +# echo "CLOUD_FUNCTIONS_REQUEST_URL=$CLOUD_FUNCTIONS_REQUEST_URL" >> application.properties +# echo "API_GW_ACCESS_KEY=$API_GW_ACCESS_KEY" >> application.properties +# echo "REFRESH_SECRET_KEY=$REFRESH_SECRET_KEY" >> application.properties +# echo "REDIS_HOST_IP=$REDIS_HOST_IP" >> application.properties +# echo "REDIS_PASSWORD=$REDIS_PASSWORD" >> application.properties +# echo "REDIS_PORT=$REDIS_PORT" >> application.properties +# echo "REDIS_TTL=$REDIS_TTL" >> application.properties - - name: Grant execute permission for gradlew - run: chmod +x gradlew +# - name: Gradle Caching +# uses: actions/cache@v4 +# with: +# path: | +# ~/.gradle/caches +# ~/.gradle/wrapper +# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - - name: Test with Gradle - run: ./gradlew --info test +# - name: Grant execute permission for gradlew +# run: chmod +x gradlew + +# - name: Test with Gradle +# run: ./gradlew --info test diff --git a/.gitignore b/.gitignore index c2c1b52..e1e7417 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ # Mac Os -.DS_Store \ No newline at end of file +.DS_Store + +# VSC setting +.vscode/** \ No newline at end of file diff --git a/server/spring/.gitignore b/server/spring/.gitignore index c2065bc..bd0f554 100644 --- a/server/spring/.gitignore +++ b/server/spring/.gitignore @@ -5,6 +5,10 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ +### env file ### +application.properties +application-prod.properties + ### STS ### .apt_generated .classpath diff --git a/server/spring/build.gradle b/server/spring/build.gradle index efe1e6c..b555efb 100644 --- a/server/spring/build.gradle +++ b/server/spring/build.gradle @@ -23,9 +23,22 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-cache' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'javax.persistence:javax.persistence-api:2.2' + + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("io.jsonwebtoken:jjwt-api:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5") + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/server/spring/src/main/java/catchytape/spring/Application.java b/server/spring/src/main/java/catchytape/spring/Application.java index 6648ac5..a580c33 100644 --- a/server/spring/src/main/java/catchytape/spring/Application.java +++ b/server/spring/src/main/java/catchytape/spring/Application.java @@ -2,8 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.cache.annotation.EnableCaching; -@SpringBootApplication +@SpringBootApplication(exclude = SecurityAutoConfiguration.class) +@EnableCaching public class Application { public static void main(String[] args) { diff --git a/server/spring/src/main/java/catchytape/spring/auth/config/RedisConfig.java b/server/spring/src/main/java/catchytape/spring/auth/config/RedisConfig.java new file mode 100644 index 0000000..70e0296 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/auth/config/RedisConfig.java @@ -0,0 +1,45 @@ +package catchytape.spring.auth.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Value("${spring.data.redis.password}") + private String password; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + + config.setHostName(host); + config.setPort(port); + config.setPassword(password); + + return new LettuceConnectionFactory(config); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + + return redisTemplate; + } + +} diff --git a/server/spring/src/main/java/catchytape/spring/auth/controller/AuthController.java b/server/spring/src/main/java/catchytape/spring/auth/controller/AuthController.java new file mode 100644 index 0000000..3889ff3 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/auth/controller/AuthController.java @@ -0,0 +1,61 @@ +package catchytape.spring.auth.controller; + +import org.springframework.web.bind.annotation.RestController; + +import catchytape.spring.auth.controller.dto.UserSignupRequest; +import catchytape.spring.auth.controller.dto.UserAuthResponse; +import catchytape.spring.auth.controller.dto.UserLoginRequest; +import catchytape.spring.auth.controller.dto.UserRefreshRequest; +import catchytape.spring.auth.service.AuthService; +import catchytape.spring.auth.service.RedisService; +import catchytape.spring.common.exception.CatchyException; +import catchytape.spring.recentPlayed.RecentPlayed; +import catchytape.spring.user.User; +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; + +import org.springframework.web.bind.annotation.PostMapping; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestController +@AllArgsConstructor +@RequestMapping("/users") +public class AuthController { + + private final AuthService authService; + + @PostMapping(value="/signup", consumes="application/json;charset=UTF-8") + public ResponseEntity signup(@RequestBody UserSignupRequest request) throws CatchyException{ + log.info("POST /users/signup - body = nickname: " + request.nickname()); + + return ResponseEntity.ok(authService.signup(request.idToken(), request.nickname())); + } + + @PostMapping(value="/login", consumes="application/json;charset=UTF-8") + public ResponseEntity login(@RequestBody UserLoginRequest request) throws CatchyException { + log.info("POST /users/signup - body = idToken: "); + + return ResponseEntity.ok(authService.login(request.idToken())); + } + + @PostMapping(value="/refresh", consumes="application/json;charset=UTF-8") + public ResponseEntity refresh(@RequestBody UserRefreshRequest request) throws CatchyException { + log.info("POST /users/refresh - body = refreshToken: ", request.refreshToken()); + + return ResponseEntity.ok(this.authService.refreshToken(request.refreshToken())); + } + + @GetMapping("/test") + public ResponseEntity test() throws CatchyException { + return ResponseEntity.ok(this.authService.test()); + } +} diff --git a/server/spring/src/main/java/catchytape/spring/auth/controller/dto/UserAuthResponse.java b/server/spring/src/main/java/catchytape/spring/auth/controller/dto/UserAuthResponse.java new file mode 100644 index 0000000..b29a7db --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/auth/controller/dto/UserAuthResponse.java @@ -0,0 +1,8 @@ +package catchytape.spring.auth.controller.dto; + +import lombok.Builder; + +@Builder +public record UserAuthResponse(String accessToken, String refreshToken) { + +} diff --git a/server/spring/src/main/java/catchytape/spring/auth/controller/dto/UserLoginRequest.java b/server/spring/src/main/java/catchytape/spring/auth/controller/dto/UserLoginRequest.java new file mode 100644 index 0000000..fd11f21 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/auth/controller/dto/UserLoginRequest.java @@ -0,0 +1,3 @@ +package catchytape.spring.auth.controller.dto; + +public record UserLoginRequest(String idToken) {} diff --git a/server/spring/src/main/java/catchytape/spring/auth/controller/dto/UserRefreshRequest.java b/server/spring/src/main/java/catchytape/spring/auth/controller/dto/UserRefreshRequest.java new file mode 100644 index 0000000..46b6316 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/auth/controller/dto/UserRefreshRequest.java @@ -0,0 +1,3 @@ +package catchytape.spring.auth.controller.dto; + +public record UserRefreshRequest(String refreshToken) {} diff --git a/server/spring/src/main/java/catchytape/spring/auth/controller/dto/UserSignupRequest.java b/server/spring/src/main/java/catchytape/spring/auth/controller/dto/UserSignupRequest.java new file mode 100644 index 0000000..a607ee3 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/auth/controller/dto/UserSignupRequest.java @@ -0,0 +1,4 @@ +package catchytape.spring.auth.controller.dto; + +public record UserSignupRequest(String nickname, String idToken) { +} \ No newline at end of file diff --git a/server/spring/src/main/java/catchytape/spring/auth/service/AuthService.java b/server/spring/src/main/java/catchytape/spring/auth/service/AuthService.java new file mode 100644 index 0000000..d9e35ca --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/auth/service/AuthService.java @@ -0,0 +1,126 @@ +package catchytape.spring.auth.service; + +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.reactive.function.client.WebClient; + +import catchytape.spring.auth.controller.dto.UserAuthResponse; +import catchytape.spring.auth.service.dto.GoogleTokenResponse; +import catchytape.spring.common.exception.CatchyException; +import catchytape.spring.user.User; +import catchytape.spring.user.UserRepository; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import java.util.UUID; + +@Slf4j +@Service +@AllArgsConstructor +@Transactional +public class AuthService { + private final RedisService redisService; + private final JwtService jwtService; + private final UserRepository userRepository; + + public String getUserEmail(String googleIdToken) throws CatchyException { + String googleApiUrl = "https://oauth2.googleapis.com/tokeninfo?id_token="+googleIdToken; + + try { + WebClient client = WebClient.create(); + GoogleTokenResponse response = client.get() + .uri(googleApiUrl) + .retrieve() + .bodyToMono(GoogleTokenResponse.class) + .block(); + + if(response.email() == null) { + throw new CatchyException("UNAUTHORIZED", "EXPIRED_TOKEN"); + } + + return response.email(); + + } catch(Exception e) { + if(e instanceof CatchyException) { + throw e; + } + + throw new CatchyException("INTERNAL_SERVER_ERROR", "SERVER_ERROR"); + } + } + + public UserAuthResponse login(String userEmail) throws CatchyException { + try { + User user = this.userRepository.findByUserEmail(userEmail); + + if(user == null) { + throw new CatchyException("UNAUTHORIZED", "NOT_EXIST_USER"); + } + + String userId = user.getUserId(); + String refreshId = UUID.randomUUID().toString(); + this.redisService.setValue(refreshId, userId); + + return this.jwtService.generateJwtToken(userId, refreshId); + } catch(Exception e) { + if(e instanceof CatchyException) { + throw e; + } + + throw new CatchyException("INTERNAL_SERVER_ERROR", "SERVER_ERROR"); + } + } + + public UserAuthResponse signup(String googleIdToken, String nickname) throws CatchyException { + try { + String userEmail = this.getUserEmail(googleIdToken); + + if(this.userRepository.findByUserEmail(userEmail) != null) { + throw new CatchyException("BAD_REQUEST", "ALREADY_EXIST_EMAIL"); + } + + String userId = UUID.randomUUID().toString(); + User newUser = new User(userId, nickname, userEmail); + this.userRepository.save(newUser); + + return this.login(userEmail); + } catch(Exception e) { + if(e instanceof CatchyException) { + throw e; + } + + throw new CatchyException("INTERNAL_SERVER_ERROR", "SERVER_ERROR"); + } + } + + public UserAuthResponse refreshToken(String refreshToken) throws CatchyException { + try { + this.jwtService.isValidToken(refreshToken); + + String refreshId = this.jwtService.decodeToken("refresh_id", refreshToken); + + String userId = this.redisService.getValue(refreshId); + + if(userId == null) { + throw new CatchyException("UNAUTHORIZED", "NOT_EXIST_USER"); + } + + String newRefreshId = UUID.randomUUID().toString(); + this.redisService.deleteValue(refreshId); + this.redisService.setValue(newRefreshId, userId); + + return this.jwtService.generateJwtToken(userId, newRefreshId); + } catch(Exception e) { + if(e instanceof CatchyException) { + throw e; + } + + throw new CatchyException("INTERNAL_SERVER_ERROR", "SERVER_ERROR"); + } + } + + public User test() { + User user = this.userRepository.findByUserEmail("sugamypapa@gmail.com"); + return user; + } +} diff --git a/server/spring/src/main/java/catchytape/spring/auth/service/JwtService.java b/server/spring/src/main/java/catchytape/spring/auth/service/JwtService.java new file mode 100644 index 0000000..83618cf --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/auth/service/JwtService.java @@ -0,0 +1,86 @@ +package catchytape.spring.auth.service; + +import java.security.Key; +import java.time.Duration; +import java.util.Base64; +import java.util.Date; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Service; + +import catchytape.spring.auth.controller.dto.UserAuthResponse; +import catchytape.spring.common.exception.CatchyException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SecurityException; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class JwtService { + @Value("${jwt.secret.key}") + private String secretKey; + private Key key; + private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; + + private final int HOUR_TO_SECONDS = 60*60; + private final int WEEK_TO_SECONDS = 60*60*24*7; + + @PostConstruct /* 의존성 주입이 이루어진 뒤 초기화를 수행 */ + protected void init() { + String encodedKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); + key= Keys.hmacShaKeyFor(encodedKey.getBytes()); + } + + + public UserAuthResponse generateJwtToken(String userId, String refreshId) { + long now = (new Date()).getTime(); + + Date accessTokenExpiresIn = new Date(now + HOUR_TO_SECONDS); + String accessToken = Jwts.builder() + .claim("user_id", userId) + .setExpiration(accessTokenExpiresIn) + .signWith(key, signatureAlgorithm) + .compact(); + + Date refreshTokenExpiresIn = new Date(now + WEEK_TO_SECONDS); + String refreshToken = Jwts.builder() + .claim("refresh_id", refreshId) + .setExpiration(refreshTokenExpiresIn) + .signWith(key, signatureAlgorithm) + .compact(); + + return UserAuthResponse.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + public String decodeToken(String key, String refreshToken) { + Claims claims = Jwts.parserBuilder() + .build() + .parseClaimsJwt(refreshToken) + .getBody(); + + return (String) claims.get(key); + } + + public boolean isValidToken(String token) throws CatchyException { + try { + Jwts.parserBuilder().build().parseClaimsJws(token); + + return true; + } catch(SecurityException | MalformedJwtException e) { + throw new CatchyException("NOT_EXIST_REFRESH_TOKEN", "WRONG_TOKEN"); + } catch (ExpiredJwtException e) { + throw new CatchyException("EXPIRED_TOKEN", "WRONG_TOKEN"); + } + } +} diff --git a/server/spring/src/main/java/catchytape/spring/auth/service/RedisService.java b/server/spring/src/main/java/catchytape/spring/auth/service/RedisService.java new file mode 100644 index 0000000..7d7fd70 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/auth/service/RedisService.java @@ -0,0 +1,41 @@ +package catchytape.spring.auth.service; + +import java.time.Duration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RedisService { + private final RedisTemplate redisTemplate; + + @Value("${spring.cache.redis.time-to-live}") + private int expireSeconds; + + public void setValue(String key, String value) { + ValueOperations values = redisTemplate.opsForValue(); + + values.set(key, value, Duration.ofSeconds(expireSeconds)); + log.info("만료 시간 : ", Duration.ofSeconds(expireSeconds)); + } + + @Transactional(readOnly = true) + public String getValue(String key) { + ValueOperations values = redisTemplate.opsForValue(); + + return values.get(key); + } + + public void deleteValue(String key) { + redisTemplate.delete(key); + } +} diff --git a/server/spring/src/main/java/catchytape/spring/auth/service/dto/GoogleTokenResponse.java b/server/spring/src/main/java/catchytape/spring/auth/service/dto/GoogleTokenResponse.java new file mode 100644 index 0000000..60bc14e --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/auth/service/dto/GoogleTokenResponse.java @@ -0,0 +1,5 @@ +package catchytape.spring.auth.service.dto; + +import lombok.Getter; + +public record GoogleTokenResponse(String email) {} diff --git a/server/spring/src/main/java/catchytape/spring/common/constant/Genres.java b/server/spring/src/main/java/catchytape/spring/common/constant/Genres.java new file mode 100644 index 0000000..588fbea --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/common/constant/Genres.java @@ -0,0 +1,11 @@ +package catchytape.spring.common.constant; + +public enum Genres { + hip_hop, + acoustic, + rnb, + jazz, + rock, + dance, + etc +} diff --git a/server/spring/src/main/java/catchytape/spring/common/domain/ErrorCode.java b/server/spring/src/main/java/catchytape/spring/common/domain/ErrorCode.java new file mode 100644 index 0000000..e7ad5f2 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/common/domain/ErrorCode.java @@ -0,0 +1,40 @@ +package catchytape.spring.common.domain; + +import lombok.Getter; + +@Getter +public enum ErrorCode { + NOT_DUPLICATED_NICKNAME(1000), + DUPLICATED_NICKNAME(1001), + SERVER_ERROR(5000), + SERVICE_ERROR(5001), + MUSIC_ENCODE_ERROR(5002), + ENCODED_MUSIC_UPLOAD_ERROR(5003), + QUERY_ERROR(5004), + ENTITY_ERROR(5005), + REPOSITORY_ERROR(5006), + NOT_EXIST_PLAYLIST_ON_USER(4001), + NOT_EXIST_MUSIC(4002), + ALREADY_ADDED(4003), + INVALID_INPUT_UUID_VALUE(4004), + NOT_EXIST_USER(4005), + ALREADY_EXIST_EMAIL(4006), + NOT_EXIST_GENRE(4007), + INVALID_INPUT_TYPE_VALUE(4008), + NOT_EXIST_MUSIC_ID(4009), + NOT_EXIST_TS_IN_BUCKET(4010), + INVALID_GREEN_EYE_REQUEST(4011), + FAIL_GREEN_EYE_IMAGE_RECOGNITION(4012), + BAD_IMAGE(4013), + NOT_ADDED_MUSIC(4014), + NO_RECENT_PLAYED_MUSIC(4015), + WRONG_TOKEN(4100), + EXPIRED_TOKEN(4101), + NOT_EXIST_REFRESH_TOKEN(4102); + + private final int code; + + ErrorCode(int code) { + this.code = code; + } +} diff --git a/server/spring/src/main/java/catchytape/spring/common/domain/HttpStatusCode.java b/server/spring/src/main/java/catchytape/spring/common/domain/HttpStatusCode.java new file mode 100644 index 0000000..12fa648 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/common/domain/HttpStatusCode.java @@ -0,0 +1,23 @@ +package catchytape.spring.common.domain; + +import lombok.Getter; + +@Getter +public enum HttpStatusCode { + SUCCESS(200, "SUCCESS"), + CONFLICT(409, "DUPLICATED_NICKNAME"), + UNAUTHORIZED(401, "WRONG_TOKEN"), + NOT_FOUND(404, "NOT_FOUND"), + INTERNAL_SERVER_ERROR(500, "SERVER_ERROR"), + BAD_REQUEST(400, "BAD_REQUEST"); + + private final int code; + private final String message; + + HttpStatusCode(int code, String message) { + this.code = code; + this.message = message; + } +} + + diff --git a/server/spring/src/main/java/catchytape/spring/common/exception/CatchyException.java b/server/spring/src/main/java/catchytape/spring/common/exception/CatchyException.java new file mode 100644 index 0000000..29bcd89 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/common/exception/CatchyException.java @@ -0,0 +1,17 @@ +package catchytape.spring.common.exception; + +import catchytape.spring.common.domain.ErrorCode; +import catchytape.spring.common.domain.HttpStatusCode; +import lombok.Getter; + +@Getter +public class CatchyException extends Exception { + private int statusCode; + private int errorCode; + + public CatchyException(String statusMessage, String errorMessage) { + super(errorMessage); + this.statusCode = HttpStatusCode.valueOf(statusMessage).getCode(); + this.errorCode = ErrorCode.valueOf(errorMessage).getCode(); + } +} diff --git a/server/spring/src/main/java/catchytape/spring/common/exception/CatchyExceptionHandler.java b/server/spring/src/main/java/catchytape/spring/common/exception/CatchyExceptionHandler.java new file mode 100644 index 0000000..85f8e88 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/common/exception/CatchyExceptionHandler.java @@ -0,0 +1,18 @@ +package catchytape.spring.common.exception; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import jakarta.servlet.http.HttpServletRequest; + +@RestControllerAdvice +public class CatchyExceptionHandler { + + @ExceptionHandler(CatchyException.class) + public ResponseEntity handleResponse(CatchyException e, HttpServletRequest request) { + + CatchyExceptionResponse errorResponse = new CatchyExceptionResponse(e.getErrorCode(), e.getMessage()); + return ResponseEntity.status(e.getStatusCode()).body(errorResponse); + } +} diff --git a/server/spring/src/main/java/catchytape/spring/common/exception/CatchyExceptionResponse.java b/server/spring/src/main/java/catchytape/spring/common/exception/CatchyExceptionResponse.java new file mode 100644 index 0000000..3369ef9 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/common/exception/CatchyExceptionResponse.java @@ -0,0 +1,13 @@ +package catchytape.spring.common.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class CatchyExceptionResponse { + private int statusCode; + private String message; +} diff --git a/server/spring/src/main/java/catchytape/spring/music/Music.java b/server/spring/src/main/java/catchytape/spring/music/Music.java new file mode 100644 index 0000000..5bb5ef7 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/music/Music.java @@ -0,0 +1,59 @@ +package catchytape.spring.music; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import org.springframework.data.annotation.CreatedDate; + +import catchytape.spring.common.constant.Genres; +import catchytape.spring.musicPlaylist.MusicPlaylist; +import catchytape.spring.recentPlayed.RecentPlayed; +import catchytape.spring.user.User; + +@Table(name="music") +@Entity +public class Music { + @Id + @Column(name = "music_id") + private String music_id; + + @Column() + private String title; + + @Column(nullable = true) + private String lyrics; + + @Column() + private String cover; + + @Column(name = "music_file") + private String music_file; + + @Column() + @Enumerated(EnumType.STRING) + private Genres genre; + + @Column(name = "created_at", updatable = false, nullable = false) + @CreatedDate + private Date created_at; + + @ManyToOne + private User user; + + @OneToMany(mappedBy = "music") + private List music_playlist = new ArrayList<>(); + + @OneToMany(mappedBy = "music") + private List recent_played = new ArrayList<>(); +} diff --git a/server/spring/src/main/java/catchytape/spring/musicPlaylist/MusicPlaylist.java b/server/spring/src/main/java/catchytape/spring/musicPlaylist/MusicPlaylist.java new file mode 100644 index 0000000..60994d9 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/musicPlaylist/MusicPlaylist.java @@ -0,0 +1,48 @@ +package catchytape.spring.musicPlaylist; + +import java.util.Date; +import java.util.List; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import org.springframework.data.annotation.CreatedDate; + +import catchytape.spring.music.Music; +import catchytape.spring.playlist.Playlist; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Entity +@Table(name="music_playlist", indexes = {@Index(name="idx_playlist", columnList = "playlist_id"), @Index(name="idx_created_at", columnList = "created_at")}) +@Getter +public class MusicPlaylist { + @Id + @Column(name = "music_playlist_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long music_playlist_id; + + @ManyToOne(cascade = CascadeType.REMOVE) + private Music music; + + @ManyToOne + @JoinColumn(name="playlist_id") + private Playlist playlist; + + @Column(name = "created_at", updatable = false, nullable = false) + @CreatedDate + private Date created_at; +} diff --git a/server/spring/src/main/java/catchytape/spring/playlist/Playlist.java b/server/spring/src/main/java/catchytape/spring/playlist/Playlist.java new file mode 100644 index 0000000..d63ad98 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/playlist/Playlist.java @@ -0,0 +1,47 @@ +package catchytape.spring.playlist; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import org.springframework.data.annotation.CreatedDate; + +import catchytape.spring.musicPlaylist.MusicPlaylist; +import catchytape.spring.user.User; + +@Table(name="playlist", indexes = @Index(name="idx_playlist_user", columnList = "user_id")) +@Entity +public class Playlist { + @Id + @Column + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long playlist_id; + + @Column + private String playlist_title; + + @ManyToOne + @JoinColumn(name="user_id") + private User user; + + @OneToMany(mappedBy = "playlist") + private List music_playlist = new ArrayList<>(); + + @Column(updatable = false, nullable = false) + @CreatedDate + private Date created_at; + + @Column() + private Date updated_at; +} diff --git a/server/spring/src/main/java/catchytape/spring/recentPlayed/RecentPlayed.java b/server/spring/src/main/java/catchytape/spring/recentPlayed/RecentPlayed.java new file mode 100644 index 0000000..8363a03 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/recentPlayed/RecentPlayed.java @@ -0,0 +1,37 @@ +package catchytape.spring.recentPlayed; + +import java.util.Date; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import catchytape.spring.music.Music; +import catchytape.spring.user.User; + +@Table(name="recent_played", indexes = @Index(name="idx_recent_played_user", columnList = "user_id")) +@Entity(name="recent_played") +public class RecentPlayed { + @Id + @Column + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long recent_played_id; + + @ManyToOne(cascade = CascadeType.REMOVE) + @JoinColumn(name="music_id") + private Music music; + + @ManyToOne + @JoinColumn(name="user_id") + private User user; + + @Column + private Date played_at; +} diff --git a/server/spring/src/main/java/catchytape/spring/user/User.java b/server/spring/src/main/java/catchytape/spring/user/User.java new file mode 100644 index 0000000..598e1ff --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/user/User.java @@ -0,0 +1,79 @@ +package catchytape.spring.user; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.domain.Persistable; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import catchytape.spring.music.Music; +import catchytape.spring.playlist.Playlist; +import catchytape.spring.recentPlayed.RecentPlayed; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name="user") +@Getter +@Entity +public class User implements Persistable { + @Id + @Column(name = "user_id") + private String userId; + + @Column(name = "nickname") + private String nickname; + + @Column(name = "photo") + private String photo; + + @Column(name = "user_email") + private String userEmail; + + @Column(name = "created_at", updatable = false) + @CreatedDate + private LocalDateTime createdAt; + + @Column(name = "is_deleted") + private boolean isDeleted; + + @OneToMany(mappedBy = "user") + private List musics = new ArrayList<>(); + + @OneToMany(mappedBy = "user") + private List playlists = new ArrayList<>(); + + @OneToMany(mappedBy = "user") + private List recent_played = new ArrayList<>(); + + public User(String userId, String nickname, String email) { + this.userId = userId; + this.nickname = nickname; + this.userEmail = email; + this.photo = null; + this.createdAt = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + this.isDeleted = false; + } + + @Override + public String getId() { + return userId; + } + + @Override + public boolean isNew() { + return getCreatedAt() == null; // 객체가 만들어진 시간[BaseEntity에 존재함] + } +} diff --git a/server/spring/src/main/java/catchytape/spring/user/UserRepository.java b/server/spring/src/main/java/catchytape/spring/user/UserRepository.java new file mode 100644 index 0000000..feb29d3 --- /dev/null +++ b/server/spring/src/main/java/catchytape/spring/user/UserRepository.java @@ -0,0 +1,14 @@ +package catchytape.spring.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + + +@Repository +public interface UserRepository extends JpaRepository { + + @Query(value = "select * from user u where u.user_email = :email", nativeQuery = true) + User findByUserEmail(@Param("email") String email); +} diff --git a/server/spring/src/main/resources/application.properties b/server/spring/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/server/spring/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -