From f81f07bd05b1e2f733f296c1ad44c21faa386a34 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 23 Jul 2023 03:26:17 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=9C=ED=81=90=EB=A6=AC=ED=8B=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=9C=A0=EC=A0=80=20API=20-=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8,=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85,=20?= =?UTF-8?q?=EA=B3=84=EC=A0=95=EC=A0=95=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + build.gradle.kts | 16 ++ .../vividnext/sodalive/SodaLiveApplication.kt | 2 + .../vividnext/sodalive/aws/s3/S3Uploader.kt | 34 +++ .../vividnext/sodalive/common/ApiResponse.kt | 22 ++ .../vividnext/sodalive/common/BaseEntity.kt | 30 +++ .../sodalive/common/SodaException.kt | 3 + .../sodalive/common/SodaExceptionHandler.kt | 60 +++++ .../sodalive/configs/AmazonS3Config.kt | 29 +++ .../sodalive/configs/QueryDslConfig.kt | 18 ++ .../vividnext/sodalive/configs/RedisConfig.kt | 30 +++ .../sodalive/configs/SecurityConfig.kt | 74 ++++++ .../vividnext/sodalive/configs/WebConfig.kt | 19 ++ .../sodalive/jwt/JwtAccessDeniedHandler.kt | 18 ++ .../jwt/JwtAuthenticationEntryPoint.kt | 18 ++ .../kr/co/vividnext/sodalive/jwt/JwtFilter.kt | 45 ++++ .../vividnext/sodalive/jwt/TokenProvider.kt | 133 +++++++++++ .../kr/co/vividnext/sodalive/member/Member.kt | 104 +++++++++ .../sodalive/member/MemberAdapter.kt | 10 + .../sodalive/member/MemberController.kt | 37 +++ .../sodalive/member/MemberRepository.kt | 16 ++ .../sodalive/member/MemberService.kt | 215 ++++++++++++++++++ .../sodalive/member/SignUpValidator.kt | 40 ++++ .../co/vividnext/sodalive/member/auth/Auth.kt | 30 +++ .../member/info/GetMemberInfoResponse.kt | 12 + .../sodalive/member/login/LoginRequest.kt | 8 + .../sodalive/member/login/LoginResponse.kt | 9 + .../member/notification/MemberNotification.kt | 29 +++ .../sodalive/member/signUp/SignUpRequest.kt | 13 ++ .../member/stipulation/Stipulation.kt | 14 ++ .../member/stipulation/StipulationAgree.kt | 30 +++ .../stipulation/StipulationRepository.kt | 10 + .../sodalive/member/token/MemberToken.kt | 11 + .../member/token/MemberTokenRepository.kt | 7 + .../kr/co/vividnext/sodalive/utils/Utils.kt | 34 +++ src/main/resources/application.yml | 66 ++++++ 36 files changed, 1247 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/aws/s3/S3Uploader.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/common/ApiResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/common/BaseEntity.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/common/SodaException.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/common/SodaExceptionHandler.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/configs/AmazonS3Config.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/configs/QueryDslConfig.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAccessDeniedHandler.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAuthenticationEntryPoint.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtFilter.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/MemberAdapter.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/SignUpValidator.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/auth/Auth.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/info/GetMemberInfoResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotification.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/Stipulation.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationAgree.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberTokenRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/utils/Utils.kt diff --git a/.gitignore b/.gitignore index 5218d57..67ee418 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ HELP.md .gradle +.envrc build/ !**/src/main/**/build/ !**/src/test/**/build/ diff --git a/build.gradle.kts b/build.gradle.kts index b8d1b4b..905258f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,11 +33,21 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") + // jwt + implementation("io.jsonwebtoken:jjwt-api:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5") + // querydsl (추가 설정) implementation("com.querydsl:querydsl-jpa:$querydslVersion") kapt("com.querydsl:querydsl-apt:$querydslVersion:jpa") kapt("org.springframework.boot:spring-boot-configuration-processor") + // aws + implementation("com.amazonaws:aws-java-sdk-ses:1.12.380") + implementation("com.amazonaws:aws-java-sdk-s3:1.12.380") + implementation("com.amazonaws:aws-java-sdk-cloudfront:1.12.380") + developmentOnly("org.springframework.boot:spring-boot-devtools") runtimeOnly("com.h2database:h2") runtimeOnly("com.mysql:mysql-connector-j") @@ -45,6 +55,12 @@ dependencies { testImplementation("org.springframework.security:spring-security-test") } +allOpen { + annotation("javax.persistence.Entity") + annotation("javax.persistence.MappedSuperclass") + annotation("javax.persistence.Embeddable") +} + tasks.withType { kotlinOptions { freeCompilerArgs += "-Xjsr305=strict" diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt b/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt index 807674a..bb4472f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt @@ -2,8 +2,10 @@ package kr.co.vividnext.sodalive import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.scheduling.annotation.EnableAsync @SpringBootApplication +@EnableAsync class SodaLiveApplication fun main(args: Array) { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/aws/s3/S3Uploader.kt b/src/main/kotlin/kr/co/vividnext/sodalive/aws/s3/S3Uploader.kt new file mode 100644 index 0000000..bd3486d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/aws/s3/S3Uploader.kt @@ -0,0 +1,34 @@ +package kr.co.vividnext.sodalive.aws.s3 + +import com.amazonaws.services.s3.AmazonS3Client +import com.amazonaws.services.s3.model.ObjectMetadata +import com.amazonaws.services.s3.model.PutObjectRequest +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import java.io.InputStream + +@Component +class S3Uploader(private val amazonS3Client: AmazonS3Client) { + private val logger = LoggerFactory.getLogger(this::class.java) + + fun upload( + inputStream: InputStream, + bucket: String, + filePath: String, + metadata: ObjectMetadata? = null + ): String { + putS3(inputStream, bucket, filePath, metadata) + return filePath + } + + private fun putS3( + inputStream: InputStream, + bucket: String, + filePath: String, + metadata: ObjectMetadata? + ): String { + amazonS3Client.putObject(PutObjectRequest(bucket, filePath, inputStream, metadata)) + logger.info("파일이 업로드 되었습니다.") + return amazonS3Client.getUrl(bucket, filePath).toString() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/ApiResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/ApiResponse.kt new file mode 100644 index 0000000..daf3890 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/ApiResponse.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.common + +data class ApiResponse( + val success: Boolean, + val message: String? = null, + val data: T? = null, + val errorProperty: String? = null +) { + companion object { + fun ok(data: T? = null, message: String? = null) = ApiResponse( + success = true, + message = message, + data = data + ) + + fun error(message: String? = null, errorProperty: String? = null) = ApiResponse( + success = false, + message = message, + errorProperty = errorProperty + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/BaseEntity.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/BaseEntity.kt new file mode 100644 index 0000000..bf2b11d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/BaseEntity.kt @@ -0,0 +1,30 @@ +package kr.co.vividnext.sodalive.common + +import java.time.LocalDateTime +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Id +import javax.persistence.MappedSuperclass +import javax.persistence.PrePersist +import javax.persistence.PreUpdate + +@MappedSuperclass +abstract class BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null + + var createdAt: LocalDateTime? = null + var updatedAt: LocalDateTime? = null + + @PrePersist + fun prePersist() { + createdAt = LocalDateTime.now() + updatedAt = LocalDateTime.now() + } + + @PreUpdate + fun preUpdate() { + updatedAt = LocalDateTime.now() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaException.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaException.kt new file mode 100644 index 0000000..cf3cb9e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaException.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.common + +class SodaException(message: String, val errorProperty: String? = null) : RuntimeException(message) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaExceptionHandler.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaExceptionHandler.kt new file mode 100644 index 0000000..4b9aa7f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaExceptionHandler.kt @@ -0,0 +1,60 @@ +package kr.co.vividnext.sodalive.common + +import org.slf4j.LoggerFactory +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.authentication.BadCredentialsException +import org.springframework.security.authentication.InternalAuthenticationServiceException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.multipart.MaxUploadSizeExceededException + +@RestControllerAdvice +class SodaExceptionHandler { + private val logger = LoggerFactory.getLogger(this::class.java) + + @ExceptionHandler(SodaException::class) + fun handleSudaException(e: SodaException) = run { + logger.error("API error", e) + ApiResponse.error( + message = e.message, + errorProperty = e.errorProperty + ) + } + + @ExceptionHandler(MaxUploadSizeExceededException::class) + fun handleMaxUploadSizeExceededException(e: MaxUploadSizeExceededException) = run { + logger.error("API error", e) + ApiResponse.error(message = "파일용량은 최대 1024MB까지 저장할 수 있습니다.") + } + + @ExceptionHandler(AccessDeniedException::class) + fun handleAccessDeniedException(e: AccessDeniedException) = run { + logger.error("API error", e) + ApiResponse.error(message = "권한이 없습니다.") + } + + @ExceptionHandler(InternalAuthenticationServiceException::class) + fun handleInternalAuthenticationServiceException(e: InternalAuthenticationServiceException) = run { + logger.error("API error", e) + ApiResponse.error("로그인 정보를 확인해주세요.") + } + + @ExceptionHandler(BadCredentialsException::class) + fun handleBadCredentialsException(e: BadCredentialsException) = run { + logger.error("API error", e) + ApiResponse.error("로그인 정보를 확인해주세요.") + } + + @ExceptionHandler(DataIntegrityViolationException::class) + fun handleDataIntegrityViolationException(e: DataIntegrityViolationException) = run { + logger.error("API error", e) + ApiResponse.error("이미 등록되어 있습니다.") + } + + @ExceptionHandler(Exception::class) + fun handleException(e: Exception) = run { + logger.error("API error", e) + ApiResponse.error("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/AmazonS3Config.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/AmazonS3Config.kt new file mode 100644 index 0000000..2e8f95d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/AmazonS3Config.kt @@ -0,0 +1,29 @@ +package kr.co.vividnext.sodalive.configs + +import com.amazonaws.auth.AWSStaticCredentialsProvider +import com.amazonaws.auth.BasicAWSCredentials +import com.amazonaws.services.s3.AmazonS3Client +import com.amazonaws.services.s3.AmazonS3ClientBuilder +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class AmazonS3Config( + @Value("\${cloud.aws.credentials.access-key}") + private val accessKey: String, + @Value("\${cloud.aws.credentials.secret-key}") + private val secretKey: String, + @Value("\${cloud.aws.region.static}") + private val region: String +) { + + @Bean + fun amazonS3Client(): AmazonS3Client { + val awsCredentials = BasicAWSCredentials(accessKey, secretKey) + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(AWSStaticCredentialsProvider(awsCredentials)) + .build() as AmazonS3Client + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/QueryDslConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/QueryDslConfig.kt new file mode 100644 index 0000000..dde8c84 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/QueryDslConfig.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.configs + +import com.querydsl.jpa.impl.JPAQueryFactory +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import javax.persistence.EntityManager +import javax.persistence.PersistenceContext + +@Configuration +class QueryDslConfig( + @PersistenceContext + private val entityManager: EntityManager +) { + @Bean + fun jpaQueryFactory(): JPAQueryFactory { + return JPAQueryFactory(entityManager) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt new file mode 100644 index 0000000..22480ee --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt @@ -0,0 +1,30 @@ +package kr.co.vividnext.sodalive.configs + +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.lettuce.LettuceConnectionFactory +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories + +@Configuration +@EnableRedisRepositories +class RedisConfig( + @Value("\${spring.redis.host}") + private val host: String, + @Value("\${spring.redis.port}") + private val port: Int +) { + @Bean + fun redisConnectionFactory(): RedisConnectionFactory { + return LettuceConnectionFactory(host, port) + } + + @Bean + fun redisTemplate(): RedisTemplate<*, *> { + val redisTemplate: RedisTemplate<*, *> = RedisTemplate() + redisTemplate.setConnectionFactory(redisConnectionFactory()) + return redisTemplate + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt new file mode 100644 index 0000000..aae4472 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt @@ -0,0 +1,74 @@ +package kr.co.vividnext.sodalive.configs + +import kr.co.vividnext.sodalive.jwt.JwtAccessDeniedHandler +import kr.co.vividnext.sodalive.jwt.JwtAuthenticationEntryPoint +import kr.co.vividnext.sodalive.jwt.JwtFilter +import kr.co.vividnext.sodalive.jwt.TokenProvider +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.builders.WebSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +class SecurityConfig( + private val tokenProvider: TokenProvider, + private val accessDeniedHandler: JwtAccessDeniedHandler, + private val authenticationEntryPoint: JwtAuthenticationEntryPoint +) { + @Bean + fun passwordEncoder(): PasswordEncoder { + return BCryptPasswordEncoder() + } + + @Bean + fun webSecurityCustomizer(): WebSecurityCustomizer { + return WebSecurityCustomizer { web: WebSecurity -> + web + .ignoring() + .antMatchers("/h2-console/**", "/favicon.ico", "/error") + } + } + + @Bean + fun filterChain(http: HttpSecurity): SecurityFilterChain { + val jwtFilter = JwtFilter(tokenProvider) + + return http + .cors() + .and() + .csrf().disable() + .exceptionHandling() + .authenticationEntryPoint(authenticationEntryPoint) + .accessDeniedHandler(accessDeniedHandler) + .and() + .headers() + .frameOptions() + .sameOrigin() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/member/check/email").permitAll() + .antMatchers("/member/check/nickname").permitAll() + .antMatchers("/member/signup").permitAll() + .antMatchers("/member/login").permitAll() + .antMatchers("/member/forgot-password").permitAll() + .antMatchers("/stplat/terms_of_service").permitAll() + .antMatchers("/stplat/privacy_policy").permitAll() + .anyRequest().authenticated() + .and() + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java) + .build() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt new file mode 100644 index 0000000..ffbb936 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.configs + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.CorsRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class WebConfig : WebMvcConfigurer { + override fun addCorsMappings(registry: CorsRegistry) { + registry.addMapping("/**") + .allowedOrigins( + "http://localhost:8888", + "https://test-admin.sodalive.net", + "https://admin.sodalive.net" + ) + .allowedMethods("*") + .allowCredentials(true) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAccessDeniedHandler.kt b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAccessDeniedHandler.kt new file mode 100644 index 0000000..d0e9a8b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAccessDeniedHandler.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.jwt + +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.web.access.AccessDeniedHandler +import org.springframework.stereotype.Component +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +@Component +class JwtAccessDeniedHandler : AccessDeniedHandler { + override fun handle( + request: HttpServletRequest, + response: HttpServletResponse, + accessDeniedException: AccessDeniedException + ) { + response.sendError(HttpServletResponse.SC_FORBIDDEN) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAuthenticationEntryPoint.kt b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAuthenticationEntryPoint.kt new file mode 100644 index 0000000..72b5ec6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAuthenticationEntryPoint.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.jwt + +import org.springframework.security.core.AuthenticationException +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.stereotype.Component +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +@Component +class JwtAuthenticationEntryPoint : AuthenticationEntryPoint { + override fun commence( + request: HttpServletRequest, + response: HttpServletResponse, + authException: AuthenticationException + ) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtFilter.kt b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtFilter.kt new file mode 100644 index 0000000..cef7213 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtFilter.kt @@ -0,0 +1,45 @@ +package kr.co.vividnext.sodalive.jwt + +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.util.StringUtils +import org.springframework.web.filter.OncePerRequestFilter +import javax.servlet.FilterChain +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +class JwtFilter(private val tokenProvider: TokenProvider) : OncePerRequestFilter() { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val jwt = resolveToken(request) + val requestURI = request.requestURI + + if (jwt != null && StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { + val authentication = tokenProvider.getAuthentication(jwt) + SecurityContextHolder.getContext().authentication = authentication + logger.debug("Security Context에 '${authentication.name}' 인증정보를 저장했습니다, uri: $requestURI") + } else { + logger.debug("유효한 JWT 토큰이 없습니다., uri: $requestURI") + if (response.status != 200) { + response.status = 401 + } + } + + filterChain.doFilter(request, response) + } + + private fun resolveToken(request: HttpServletRequest): String? { + val bearerToken = request.getHeader(AUTHORIZATION_HEADER) + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7) + } + + return null + } + + companion object { + const val AUTHORIZATION_HEADER = "Authorization" + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt new file mode 100644 index 0000000..0c12ad9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt @@ -0,0 +1,133 @@ +package kr.co.vividnext.sodalive.jwt + +import io.jsonwebtoken.ExpiredJwtException +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.MalformedJwtException +import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.UnsupportedJwtException +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.SignatureException +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.MemberAdapter +import kr.co.vividnext.sodalive.member.MemberRepository +import kr.co.vividnext.sodalive.member.token.MemberToken +import kr.co.vividnext.sodalive.member.token.MemberTokenRepository +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.InitializingBean +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.Authentication +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.stereotype.Component +import java.security.Key +import java.util.Date +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.write + +// 토큰의 생성, 유효성 검증 담당 클래스 +@Component +class TokenProvider( + @Value("\${jwt.secret}") + private val secret: String, + @Value("\${jwt.token-validity-in-seconds}") + private val tokenValidityInSeconds: Long, + private val repository: MemberRepository, + private val tokenRepository: MemberTokenRepository +) : InitializingBean { + + private val logger = LoggerFactory.getLogger(TokenProvider::class.java) + private val tokenValidityInMilliseconds: Long = tokenValidityInSeconds * 1000 + private val tokenLocks: MutableMap = mutableMapOf() + + private lateinit var key: Key + + override fun afterPropertiesSet() { + val keyBytes = Decoders.BASE64.decode(secret) + this.key = Keys.hmacShaKeyFor(keyBytes) + } + + fun createToken(authentication: Authentication, memberId: Long): String { + val authorities = authentication.authorities + .joinToString(separator = ",", transform = GrantedAuthority::getAuthority) + + val now = Date().time + val validity = Date(now + tokenValidityInMilliseconds) + + val token = Jwts.builder() + .setSubject(memberId.toString()) + .claim(AUTHORITIES_KEY, authorities) + .signWith(key, SignatureAlgorithm.HS512) + .setExpiration(validity) + .compact() + + val lock = getOrCreateLock(memberId = memberId) + lock.write { + val memberToken = tokenRepository.findByIdOrNull(memberId) + ?: MemberToken(id = memberId, listOf()) + + val memberTokenSet = memberToken.tokenList.toMutableSet() + memberTokenSet.add(token) + memberToken.tokenList = memberTokenSet.toList() + tokenRepository.save(memberToken) + } + + return token + } + + fun getAuthentication(token: String): Authentication { + val claims = Jwts + .parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .body + + val authorities = claims[AUTHORITIES_KEY].toString().split(",").map { SimpleGrantedAuthority(it) } + val memberToken = tokenRepository.findByIdOrNull(id = claims.subject.toLong()) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + if (!memberToken.tokenList.contains(token)) throw SodaException("로그인 정보를 확인해주세요.") + + val member = repository.findByIdOrNull(id = claims.subject.toLong()) + ?: throw SodaException("로그인 정보를 확인해주세요.") + val principal = MemberAdapter(member) + + return UsernamePasswordAuthenticationToken(principal, token, authorities) + } + + fun validateToken(token: String): Boolean { + try { + Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + + return true + } catch (e: SecurityException) { + logger.info("잘못된 JWT 서명입니다.") + } catch (e: MalformedJwtException) { + logger.info("잘못된 JWT 서명입니다.") + } catch (e: ExpiredJwtException) { + logger.info("만료된 JWT 서명입니다.") + } catch (e: UnsupportedJwtException) { + logger.info("지원되지 않는 JWT 서명입니다.") + } catch (e: IllegalArgumentException) { + logger.info("JWT 토큰이 잘못되었습니다.") + } catch (e: SignatureException) { + logger.info("잘못된 JWT 서명입니다.") + } + + return false + } + + private fun getOrCreateLock(memberId: Long): ReentrantReadWriteLock { + return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() } + } + + companion object { + private const val AUTHORITIES_KEY = "auth" + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt new file mode 100644 index 0000000..e836a94 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt @@ -0,0 +1,104 @@ +package kr.co.vividnext.sodalive.member + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.auth.Auth +import kr.co.vividnext.sodalive.member.notification.MemberNotification +import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree +import javax.persistence.CascadeType +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.FetchType +import javax.persistence.OneToMany +import javax.persistence.OneToOne + +@Entity +data class Member( + val email: String, + var password: String, + var nickname: String, + var profileImage: String? = null, + + @Enumerated(value = EnumType.STRING) + var gender: Gender = Gender.NONE, + + @Enumerated(value = EnumType.STRING) + var role: MemberRole = MemberRole.USER, + + var isActive: Boolean = true, + + var container: String = "web" +) : BaseEntity() { + @OneToMany(mappedBy = "member", cascade = [CascadeType.ALL]) + val stipulationAgrees: MutableList = mutableListOf() + + @OneToOne(mappedBy = "member", fetch = FetchType.LAZY) + var notification: MemberNotification? = null + + @OneToOne(mappedBy = "member", fetch = FetchType.LAZY) + var auth: Auth? = null + + // 소개 + @Column(columnDefinition = "TEXT") + var introduce = "" + + // SNS + var instagramUrl = "" + var youtubeUrl = "" + var websiteUrl = "" + var blogUrl = "" + + var pushToken: String? = null + + // 화폐 + private var pgChargeCan: Int = 0 + private var pgRewardCan: Int = 0 + private var googleChargeCan: Int = 0 + private var googleRewardCan: Int = 0 + private var appleChargeCan: Int = 0 + private var appleRewardCan: Int = 0 + + fun getChargeCan(container: String): Int { + return when (container) { + "ios" -> appleChargeCan + pgChargeCan + "aos" -> googleChargeCan + pgChargeCan + else -> pgChargeCan + } + } + + fun getRewardCan(container: String): Int { + return when (container) { + "ios" -> appleRewardCan + pgRewardCan + "aos" -> googleRewardCan + pgRewardCan + else -> pgRewardCan + } + } + + fun charge(chargeCan: Int, rewardCan: Int, container: String) { + when (container) { + "ios" -> { + appleChargeCan = chargeCan + appleRewardCan = rewardCan + } + + "aos" -> { + googleChargeCan = chargeCan + googleRewardCan = rewardCan + } + + else -> { + pgChargeCan = chargeCan + pgRewardCan = rewardCan + } + } + } +} + +enum class Gender { + MALE, FEMALE, NONE +} + +enum class MemberRole { + ADMIN, BOT, USER, CREATOR, AGENT +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberAdapter.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberAdapter.kt new file mode 100644 index 0000000..ce42b37 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberAdapter.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.member + +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.User + +class MemberAdapter(val member: Member) : User( + member.email, + member.password, + listOf(SimpleGrantedAuthority("ROLE_${member.role.name}")) +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt new file mode 100644 index 0000000..6bbec6a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -0,0 +1,37 @@ +package kr.co.vividnext.sodalive.member + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.login.LoginRequest +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +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.RequestParam +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile + +@RestController +@RequestMapping("/member") +class MemberController(private val service: MemberService) { + @PostMapping("/signup") + fun signUp( + @RequestPart("profileImage", required = false) profileImage: MultipartFile? = null, + @RequestPart("request") requestString: String + ) = service.signUp(profileImage, requestString) + + @PostMapping("/login") + fun login(@RequestBody loginRequest: LoginRequest) = service.login(loginRequest) + + @GetMapping("/info") + fun getMemberInfo( + @RequestParam container: String?, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getMemberInfo(member, container ?: "web")) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt new file mode 100644 index 0000000..d5d41ce --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt @@ -0,0 +1,16 @@ +package kr.co.vividnext.sodalive.member + +import com.querydsl.jpa.impl.JPAQueryFactory +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface MemberRepository : JpaRepository, MemberQueryRepository { + fun findByEmail(email: String): Member? + fun findByNickname(nickname: String): Member? +} + +interface MemberQueryRepository + +@Repository +class MemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : MemberQueryRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt new file mode 100644 index 0000000..263db54 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -0,0 +1,215 @@ +package kr.co.vividnext.sodalive.member + +import com.amazonaws.services.s3.model.ObjectMetadata +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.jwt.TokenProvider +import kr.co.vividnext.sodalive.member.info.GetMemberInfoResponse +import kr.co.vividnext.sodalive.member.login.LoginRequest +import kr.co.vividnext.sodalive.member.login.LoginResponse +import kr.co.vividnext.sodalive.member.signUp.SignUpRequest +import kr.co.vividnext.sodalive.member.stipulation.Stipulation +import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree +import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository +import kr.co.vividnext.sodalive.member.stipulation.StipulationIds +import kr.co.vividnext.sodalive.member.stipulation.StipulationRepository +import kr.co.vividnext.sodalive.utils.generateFileName +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile + +@Service +@Transactional(readOnly = true) +class MemberService( + private val repository: MemberRepository, + private val stipulationRepository: StipulationRepository, + private val stipulationAgreeRepository: StipulationAgreeRepository, + + private val s3Uploader: S3Uploader, + private val validator: SignUpValidator, + private val tokenProvider: TokenProvider, + private val passwordEncoder: PasswordEncoder, + private val authenticationManagerBuilder: AuthenticationManagerBuilder, + + private val objectMapper: ObjectMapper, + + @Value("\${cloud.aws.s3.bucket}") + private val s3Bucket: String +) : UserDetailsService { + @Transactional + fun signUp( + profileImage: MultipartFile?, + requestString: String + ): ApiResponse { + val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID) + ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + + val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID) + ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + + val request = objectMapper.readValue(requestString, SignUpRequest::class.java) + if (!request.isAgreePrivacyPolicy || !request.isAgreeTermsOfService) { + throw SodaException("약관에 동의하셔야 회원가입이 가능합니다.") + } + + validatePassword(request.password) + duplicateCheckEmail(request.email) + duplicateCheckNickname(request.nickname) + + val member = createMember(request) + member.profileImage = uploadProfileImage(profileImage = profileImage, memberId = member.id!!) + agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy) + + return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = login(request.email, request.password)) + } + + fun login(request: LoginRequest): ApiResponse { + return ApiResponse.ok( + message = "로그인 되었습니다.", + data = login(request.email, request.password, request.isAdmin, request.isCreator) + ) + } + + fun getMemberInfo(member: Member, container: String): GetMemberInfoResponse { + return GetMemberInfoResponse( + can = member.getChargeCan(container) + member.getRewardCan(container), + isAuth = member.auth != null, + role = member.role, + messageNotice = member.notification?.message, + followingChannelLiveNotice = member.notification?.live, + followingChannelUploadContentNotice = member.notification?.uploadContent + ) + } + + private fun login( + email: String, + password: String, + isAdmin: Boolean = false, + isCreator: Boolean = false + ): LoginResponse { + val member = repository.findByEmail(email = email) ?: throw SodaException("로그인 정보를 확인해주세요.") + if (!member.isActive) { + throw SodaException("탈퇴한 계정입니다.\n고객센터로 문의해 주시기 바랍니다.") + } + + if (isCreator && member.role != MemberRole.CREATOR) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + if (isAdmin && member.role != MemberRole.ADMIN) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + val authenticationToken = UsernamePasswordAuthenticationToken(email, password) + val authentication = authenticationManagerBuilder.`object`.authenticate(authenticationToken) + SecurityContextHolder.getContext().authentication = authentication + + val jwt = tokenProvider.createToken( + authentication = authentication, + memberId = member.id!! + ) + + return LoginResponse( + userId = member.id!!, + token = jwt, + nickname = member.nickname, + email = member.email, + profileImage = member.profileImage ?: "" + ) + } + + private fun uploadProfileImage(profileImage: MultipartFile?, memberId: Long): String { + return if (profileImage != null) { + val metadata = ObjectMetadata() + metadata.contentLength = profileImage.size + + s3Uploader.upload( + inputStream = profileImage.inputStream, + bucket = s3Bucket, + filePath = "profile/$memberId/${generateFileName(prefix = "$memberId-profile")}", + metadata = metadata + ) + } else { + "profile/default-profile.png" + } + } + + private fun agreeTermsOfServiceAndPrivacyPolicy( + member: Member, + stipulationTermsOfService: Stipulation, + stipulationPrivacyPolicy: Stipulation + ) { + val termsOfServiceAgree = StipulationAgree(true) + termsOfServiceAgree.member = member + termsOfServiceAgree.stipulation = stipulationTermsOfService + stipulationAgreeRepository.save(termsOfServiceAgree) + + val privacyPolicyAgree = StipulationAgree(true) + privacyPolicyAgree.member = member + privacyPolicyAgree.stipulation = stipulationPrivacyPolicy + stipulationAgreeRepository.save(privacyPolicyAgree) + } + + private fun createMember(request: SignUpRequest): Member { + val member = Member( + email = request.email, + password = passwordEncoder.encode(request.password), + nickname = request.nickname, + gender = request.gender, + container = request.container + ) + + return repository.save(member) + } + + private fun validatePassword(password: String) { + val passwordValidationMessage = validator.passwordValidation(password) + if (passwordValidationMessage.trim().isNotEmpty()) { + throw SodaException(passwordValidationMessage) + } + } + + fun duplicateCheckEmail(email: String): ApiResponse { + validateEmail(email) + repository.findByEmail(email)?.let { throw SodaException("이미 사용중인 이메일 입니다.", "email") } + return ApiResponse.ok(message = "사용 가능한 이메일 입니다.") + } + + private fun validateEmail(email: String) { + val emailValidationMessage = validator.emailValidation(email) + if (emailValidationMessage.trim().isNotEmpty()) { + throw SodaException(emailValidationMessage, "email") + } + } + + fun duplicateCheckNickname(nickname: String): ApiResponse { + validateNickname(nickname) + repository.findByNickname(nickname)?.let { throw SodaException("이미 사용중인 닉네임 입니다.", "nickname") } + return ApiResponse.ok(message = "사용 가능한 닉네임 입니다.") + } + + private fun validateNickname(nickname: String) { + val nicknameValidationMessage = validator.nicknameValidation(nickname) + if (nicknameValidationMessage.trim().isNotEmpty()) { + throw SodaException(nicknameValidationMessage, "nickname") + } + } + + override fun loadUserByUsername(username: String): UserDetails { + val member = repository.findByEmail(email = username) + ?: throw UsernameNotFoundException(username) + + return MemberAdapter(member) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/SignUpValidator.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/SignUpValidator.kt new file mode 100644 index 0000000..3d1c0ff --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/SignUpValidator.kt @@ -0,0 +1,40 @@ +package kr.co.vividnext.sodalive.member + +import org.springframework.stereotype.Component + +@Component +class SignUpValidator { + fun emailValidation(email: String): String { + val isNotValidEmail = "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$" + .toRegex(RegexOption.IGNORE_CASE) + .matches(email) + .not() + + if (isNotValidEmail) { + return "올바른 이메일을 입력해 주세요" + } + + return "" + } + + fun nicknameValidation(nickname: String): String { + if (nickname.length < 2) { + return "닉네임은 2자 이상 입력해 주세요." + } + + return "" + } + + fun passwordValidation(password: String): String { + val isNotValidPassword = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d$@!%*#?&]{8,}$" + .toRegex() + .matches(password) + .not() + + if (isNotValidPassword) { + return "영문, 숫자 포함 8자 이상의 비밀번호를 입력해 주세요." + } + + return "" + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/Auth.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/Auth.kt new file mode 100644 index 0000000..570729b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/Auth.kt @@ -0,0 +1,30 @@ +package kr.co.vividnext.sodalive.member.auth + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.CascadeType +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.OneToOne + +@Entity +data class Auth( + @Column(nullable = false) + val name: String, + @Column(nullable = false) + val birth: String, + @Column(columnDefinition = "TEXT", nullable = false) + val uniqueCi: String, + @Column(columnDefinition = "TEXT", nullable = false) + val di: String +) : BaseEntity() { + @OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL]) + @JoinColumn(name = "member_id", nullable = true) + var member: Member? = null + set(value) { + value?.auth = this + field = value + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/info/GetMemberInfoResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/info/GetMemberInfoResponse.kt new file mode 100644 index 0000000..2db88e6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/info/GetMemberInfoResponse.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.member.info + +import kr.co.vividnext.sodalive.member.MemberRole + +data class GetMemberInfoResponse( + val can: Int, + val isAuth: Boolean, + val role: MemberRole, + val messageNotice: Boolean?, + val followingChannelLiveNotice: Boolean?, + val followingChannelUploadContentNotice: Boolean? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginRequest.kt new file mode 100644 index 0000000..f49585f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.member.login + +data class LoginRequest( + val email: String, + val password: String, + val isAdmin: Boolean = false, + val isCreator: Boolean = false +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginResponse.kt new file mode 100644 index 0000000..667f06d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.member.login + +data class LoginResponse( + val userId: Long, + val token: String, + val nickname: String, + val email: String, + val profileImage: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotification.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotification.kt new file mode 100644 index 0000000..4c3007a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotification.kt @@ -0,0 +1,29 @@ +package kr.co.vividnext.sodalive.member.notification + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.OneToOne + +@Entity +data class MemberNotification( + @Column(nullable = false) + var uploadContent: Boolean? = true, + + @Column(nullable = false) + var live: Boolean? = true, + + @Column(nullable = false) + var message: Boolean? = true +) : BaseEntity() { + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + set(value) { + value?.notification = this + field = value + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt new file mode 100644 index 0000000..1d962ab --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.member.signUp + +import kr.co.vividnext.sodalive.member.Gender + +data class SignUpRequest( + val email: String, + val password: String, + val nickname: String, + val gender: Gender, + val isAgreeTermsOfService: Boolean, + val isAgreePrivacyPolicy: Boolean, + val container: String = "api" +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/Stipulation.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/Stipulation.kt new file mode 100644 index 0000000..e854861 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/Stipulation.kt @@ -0,0 +1,14 @@ +package kr.co.vividnext.sodalive.member.stipulation + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Column +import javax.persistence.Entity + +@Entity +data class Stipulation( + @Column(nullable = false) + val title: String, + @Column(columnDefinition = "TEXT", nullable = false) + var description: String, + var isActive: Boolean = true +) : BaseEntity() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationAgree.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationAgree.kt new file mode 100644 index 0000000..e5504e9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationAgree.kt @@ -0,0 +1,30 @@ +package kr.co.vividnext.sodalive.member.stipulation + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +data class StipulationAgree( + val isAgree: Boolean +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + set(value) { + member?.stipulationAgrees?.add(this) + field = value + } + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "stipulation_id", nullable = false) + var stipulation: Stipulation? = null +} + +object StipulationIds { + const val TERMS_OF_SERVICE_ID = 1L + const val PRIVACY_POLICY_ID = 2L +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationRepository.kt new file mode 100644 index 0000000..54332b9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationRepository.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.member.stipulation + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface StipulationRepository : JpaRepository + +@Repository +interface StipulationAgreeRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt new file mode 100644 index 0000000..514e0e8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.member.token + +import org.springframework.data.annotation.Id +import org.springframework.data.redis.core.RedisHash + +@RedisHash("MemberToken") +data class MemberToken( + @Id + val id: Long, + var tokenList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberTokenRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberTokenRepository.kt new file mode 100644 index 0000000..29808a2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberTokenRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.member.token + +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository + +@Repository +interface MemberTokenRepository : CrudRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/utils/Utils.kt b/src/main/kotlin/kr/co/vividnext/sodalive/utils/Utils.kt new file mode 100644 index 0000000..c4a54df --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/utils/Utils.kt @@ -0,0 +1,34 @@ +package kr.co.vividnext.sodalive.utils + +import java.nio.charset.StandardCharsets +import java.security.SecureRandom +import java.util.Base64 +import java.util.Random +import java.util.UUID + +fun generatePassword(length: Int): String { + val characterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()1234567890" + val random = SecureRandom() + val password = StringBuilder() + + for (i in 0 until length) { + val rIndex = random.nextInt(characterSet.length) + password.append(characterSet[rIndex]) + } + + return password.toString() +} + +fun generateFileName(prefix: String = ""): String { + val timestamp = System.currentTimeMillis() + val random = Random().nextInt(10000) + val uuid = UUID.randomUUID().toString() + + return if (prefix.isBlank()) { + Base64 + .getUrlEncoder() + .encodeToString("$uuid-$random-$timestamp".toByteArray(StandardCharsets.UTF_8)) + } else { + "$prefix-$uuid-$random-$timestamp" + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8b13789..c1b7712 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1 +1,67 @@ +server: + shutdown: graceful + +logging: + level: + com: + amazonaws: + util: + EC2MetadataUtils: error + +cloud: + aws: + credentials: + accessKey: ${APP_AWS_ACCESS_KEY} + secretKey: ${APP_AWS_SECRET_KEY} + s3: + bucket: ${S3_BUCKET} + cloudFront: + host: ${CLOUD_FRONT_HOST} + region: + static: ap-northeast-2 + stack: + auto: false + +jwt: + header: Authorization + token-validity-in-seconds: ${JWT_TOKEN_VALIDITY_TIME} + secret: ${JWT_SECRET} + +spring: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + + jpa: + hibernate: + ddl-auto: validate + database: mysql + + servlet: + multipart: + max-file-size: 1024MB + max-request-size: 1024MB +--- +spring: + config: + activate: + on-profile: local + + devtools: + restart: + enabled: true + livereload: + enabled: true + + jpa: + properties: + hibernate: + show_sql: true + format_sql: true