parent
23506e79f1
commit
f81f07bd05
|
@ -1,5 +1,6 @@
|
|||
HELP.md
|
||||
.gradle
|
||||
.envrc
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
|
|
@ -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<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += "-Xjsr305=strict"
|
||||
|
|
|
@ -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<String>) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package kr.co.vividnext.sodalive.common
|
||||
|
||||
data class ApiResponse<T>(
|
||||
val success: Boolean,
|
||||
val message: String? = null,
|
||||
val data: T? = null,
|
||||
val errorProperty: String? = null
|
||||
) {
|
||||
companion object {
|
||||
fun <T> ok(data: T? = null, message: String? = null) = ApiResponse(
|
||||
success = true,
|
||||
message = message,
|
||||
data = data
|
||||
)
|
||||
|
||||
fun error(message: String? = null, errorProperty: String? = null) = ApiResponse<Any>(
|
||||
success = false,
|
||||
message = message,
|
||||
errorProperty = errorProperty
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package kr.co.vividnext.sodalive.common
|
||||
|
||||
class SodaException(message: String, val errorProperty: String? = null) : RuntimeException(message)
|
|
@ -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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<Any, Any>()
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory())
|
||||
return redisTemplate
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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<Long, ReentrantReadWriteLock> = 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"
|
||||
}
|
||||
}
|
|
@ -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<StipulationAgree> = 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
|
||||
}
|
|
@ -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}"))
|
||||
)
|
|
@ -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"))
|
||||
}
|
||||
}
|
|
@ -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<Member, Long>, MemberQueryRepository {
|
||||
fun findByEmail(email: String): Member?
|
||||
fun findByNickname(nickname: String): Member?
|
||||
}
|
||||
|
||||
interface MemberQueryRepository
|
||||
|
||||
@Repository
|
||||
class MemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : MemberQueryRepository
|
|
@ -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<LoginResponse> {
|
||||
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<LoginResponse> {
|
||||
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<Any> {
|
||||
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<Any> {
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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 ""
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
)
|
|
@ -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()
|
|
@ -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
|
||||
}
|
|
@ -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<Stipulation, Long>
|
||||
|
||||
@Repository
|
||||
interface StipulationAgreeRepository : JpaRepository<StipulationAgree, Long>
|
|
@ -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<String>
|
||||
)
|
|
@ -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<MemberToken, Long>
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue