Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-jdbc")
implementation("org.springframework.boot:spring-boot-starter-web")
// implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.springframework.ai:spring-ai-openai-spring-boot-starter")
Expand Down
13 changes: 13 additions & 0 deletions src/main/kotlin/com/chillin/auth/AuthController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import com.chillin.auth.appleid.AppleIdService
import com.chillin.auth.request.SignInWithAppleRequest
import com.chillin.auth.response.TokenResponse
import com.chillin.member.MemberService
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

Expand All @@ -22,4 +26,13 @@ class AuthController(
memberService.register(accountId, refreshToken)
return authService.issueToken(accountId)
}

@PostMapping("/oauth2/refresh")
fun refreshToken(@RequestHeader(HttpHeaders.AUTHORIZATION) bearerToken: String): ResponseEntity<TokenResponse> {
val token = bearerToken.substringAfter("Bearer").trim()
val newToken = authService.reissueToken(token)

return if (newToken != null) ResponseEntity.ok(newToken)
else ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()
}
}
46 changes: 42 additions & 4 deletions src/main/kotlin/com/chillin/auth/AuthService.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,52 @@
package com.chillin.auth

import com.chillin.auth.response.TokenResponse
import com.chillin.redis.RedisKeyFactory
import org.slf4j.LoggerFactory
import org.springframework.data.redis.core.StringRedisTemplate
import org.springframework.stereotype.Service

@Service
class AuthService(
private val jwtProvider: JwtProvider
private val jwtProvider: JwtProvider,
private val redisTemplate: StringRedisTemplate
) {
fun issueToken(accountId: String): TokenResponse {
val token = jwtProvider.issueToken(accountId)
return TokenResponse(token, jwtProvider.expirationSeconds)
logger.info("Issuing token...")
val tokens = jwtProvider.issueToken(accountId)

logger.info("Saving refresh token to redis...")
val tokenKeyName = RedisKeyFactory.create(accountId, "refresh-token")
redisTemplate.opsForValue().set(tokenKeyName, tokens.refreshToken)

return tokens
}

fun reissueToken(token: String): TokenResponse? {
logger.info("Validating token...")
val payload = jwtProvider.validate(token)
val accountId = payload.subject

val tokenKeyName = RedisKeyFactory.create(accountId, "refresh-token")
val storedToken =
redisTemplate.opsForValue().get(tokenKeyName) ?: return null // if token is already invalidated

return if (isReuseDetected(token, storedToken)) invalidateToken(tokenKeyName)
else issueToken(accountId)
}

private fun isReuseDetected(token: String, storedToken: String): Boolean {
logger.info("Checking if token is reused...")
return token != storedToken
}

private fun invalidateToken(tokenKeyName: String): TokenResponse? {
logger.info("Invalidating token...")
redisTemplate.delete(tokenKeyName)
return null
}

companion object {
private val logger = LoggerFactory.getLogger(AuthService::class.java)
}
}
}
45 changes: 38 additions & 7 deletions src/main/kotlin/com/chillin/auth/JwtProvider.kt
Original file line number Diff line number Diff line change
@@ -1,30 +1,61 @@
package com.chillin.auth

import com.chillin.auth.response.TokenResponse
import io.jsonwebtoken.Claims
import io.jsonwebtoken.JwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.NestedConfigurationProperty
import java.util.*

@ConfigurationProperties(prefix = "custom.jwt")
class JwtProvider(
private val secretKey: String,
secretKey: String,
private val issuer: String,
val expirationSeconds: Long

@NestedConfigurationProperty
private val expiration: TokenExpiration
) {

fun issueToken(accountId: String): String {
private val sig = Keys.hmacShaKeyFor(secretKey.toByteArray())

fun issueToken(accountId: String): TokenResponse {
val iat = Date()
val exp = Date(iat.toInstant().plusSeconds(expirationSeconds).toEpochMilli())
val sig = Keys.hmacShaKeyFor(secretKey.toByteArray())

return Jwts.builder()
val accessToken = Jwts.builder()
.claims()
.subject(accountId)
.issuer(issuer)
.issuedAt(iat)
.expiration(exp)
.expiration(Date(iat.toInstant().plusSeconds(expiration.accessToken).toEpochMilli()))
.and()
.signWith(sig, Jwts.SIG.HS256)
.compact()

val refreshToken = Jwts.builder()
.claims()
.subject(accountId)
.issuer(issuer)
.issuedAt(iat)
.expiration(Date(iat.toInstant().plusSeconds(expiration.refreshToken).toEpochMilli()))
.and()
.signWith(sig, Jwts.SIG.HS256)
.compact()

return TokenResponse(accessToken, refreshToken)
}

fun validate(token: String): Claims {
try {
return Jwts.parser()
.verifyWith(sig)
.requireIssuer(issuer)
.build()
.parseSignedClaims(token)
.payload
} catch (e: JwtException) {
throw JwtException("Failed to verify token", e)
}
}
}
6 changes: 6 additions & 0 deletions src/main/kotlin/com/chillin/auth/TokenExpiration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.chillin.auth

class TokenExpiration(
val accessToken: Long,
val refreshToken: Long
)
4 changes: 2 additions & 2 deletions src/main/kotlin/com/chillin/auth/response/TokenResponse.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.chillin.auth.response

data class TokenResponse(
val token: String,
val expiresIn: Long,
val accessToken: String,
val refreshToken: String,
val grantType: String = "Bearer"
)
26 changes: 19 additions & 7 deletions src/main/kotlin/com/chillin/drawing/DrawingController.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package com.chillin.drawing

import com.chillin.epson.EpsonConnectService
import com.chillin.epson.request.PrintSettingsRequest
import com.chillin.drawing.request.ImageGenerationRequest
import com.chillin.drawing.request.ImagePrintRequest
import com.chillin.drawing.response.DrawingResponse
import com.chillin.drawing.response.DrawingResponseWrapper
import com.chillin.epson.EpsonConnectService
import com.chillin.epson.request.PrintSettingsRequest
import com.chillin.member.MemberService
import com.chillin.openai.DallEService
import com.chillin.s3.S3Service
import com.chillin.type.DrawingType
import com.chillin.type.MediaSubtype
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
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
Expand All @@ -26,16 +28,22 @@ class DrawingController(
private val dallEService: DallEService,
private val s3Service: S3Service,
private val drawingService: DrawingService,
private val epsonConnectService: EpsonConnectService
private val epsonConnectService: EpsonConnectService,
private val memberService: MemberService
) {
@PostMapping("/gen")
fun generateDrawing(@RequestBody imageGenerationRequest: ImageGenerationRequest): ResponseEntity<DrawingResponse> {
fun generateDrawing(
@RequestBody imageGenerationRequest: ImageGenerationRequest,
@AuthenticationPrincipal accountId: String
): ResponseEntity<DrawingResponse> {
val rawPrompt = imageGenerationRequest.prompt
val pathname = "generated/${UUID.randomUUID()}.${MediaSubtype.JPEG.value}"

val (url, revisedPrompt) = dallEService.generateImage(rawPrompt)
val presignedUrl = s3Service.uploadImage(pathname, url, revisedPrompt)
val savedImage = drawingService.save(pathname, DrawingType.GENERATED, rawPrompt, revisedPrompt)

val member = memberService.findMemberByAccountId(accountId)
val savedImage = drawingService.save(member, pathname, DrawingType.GENERATED, rawPrompt, revisedPrompt)

val responseBody = DrawingResponse(savedImage.drawingId, presignedUrl, rawPrompt)
return ResponseEntity.status(HttpStatus.CREATED).body(responseBody)
Expand All @@ -53,8 +61,12 @@ class DrawingController(
}

@GetMapping
fun getDrawings(@RequestParam type: DrawingType): DrawingResponseWrapper {
val data = drawingService.getAllByType(type).map { drawing ->
fun getDrawings(
@RequestParam type: DrawingType,
@AuthenticationPrincipal accountId: String
): DrawingResponseWrapper {
val member = memberService.findMemberByAccountId(accountId)
val data = drawingService.getMyDrawingsByType(type, member).map { drawing ->
val url = s3Service.getImageUrl(drawing.pathname)
DrawingResponse(drawing.drawingId, url, drawing.rawPrompt)
}
Expand Down
5 changes: 4 additions & 1 deletion src/main/kotlin/com/chillin/drawing/DrawingRepository.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.chillin.drawing;

import com.chillin.drawing.domain.Drawing
import com.chillin.member.Member
import com.chillin.type.DrawingType
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query

interface DrawingRepository : JpaRepository<Drawing, Long> {
fun findAllByTypeOrderByCreatedAtDesc(type: DrawingType): List<Drawing>
@Query("SELECT d FROM Drawing d WHERE d.type = :type AND d.member = :member ORDER BY d.createdAt DESC")
fun findAllByType(type: DrawingType, member: Member): List<Drawing>
}
7 changes: 5 additions & 2 deletions src/main/kotlin/com/chillin/drawing/DrawingService.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.chillin.drawing

import com.chillin.drawing.domain.Drawing
import com.chillin.member.Member
import com.chillin.type.DrawingType
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
Expand All @@ -12,13 +13,14 @@ class DrawingService(
private val logger = LoggerFactory.getLogger(DrawingService::class.java)

fun save(
member: Member,
pathname: String,
drawingType: DrawingType,
rawPrompt: String? = null,
revisedPrompt: String? = null
): Drawing {
logger.info("Saving drawing to db...")
val drawing = Drawing(pathname, drawingType, rawPrompt, revisedPrompt)
val drawing = Drawing(member, pathname, drawingType, rawPrompt, revisedPrompt)

return drawingRepository.save(drawing).apply {
logger.info("Saved drawing to db: drawingId=${drawingId}, pathname=$pathname, drawingType=$drawingType, rawPrompt=$rawPrompt, revisedPrompt=$revisedPrompt")
Expand All @@ -36,5 +38,6 @@ class DrawingService(
}
}

fun getAllByType(type: DrawingType) = drawingRepository.findAllByTypeOrderByCreatedAtDesc(type)
fun getMyDrawingsByType(type: DrawingType, member: Member) =
drawingRepository.findAllByType(type, member)
}
8 changes: 8 additions & 0 deletions src/main/kotlin/com/chillin/drawing/domain/Drawing.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package com.chillin.drawing.domain

import com.chillin.member.Member
import com.chillin.type.DrawingType
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.EntityListeners
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.FetchType
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener
Expand All @@ -19,6 +23,10 @@ import java.time.LocalDateTime
@EntityListeners(AuditingEntityListener::class)
class Drawing(

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
val member: Member,

@Column
val pathname: String,

Expand Down
12 changes: 8 additions & 4 deletions src/main/kotlin/com/chillin/epson/EpsonConnectController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package com.chillin.epson

import com.chillin.adobe.AdobeService
import com.chillin.drawing.DrawingService
import com.chillin.member.MemberService
import com.chillin.s3.S3Service
import com.chillin.type.DrawingType
import com.chillin.type.MediaSubtype
import org.slf4j.LoggerFactory
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
Expand All @@ -16,13 +18,15 @@ import java.util.*
class EpsonConnectController(
private val drawingService: DrawingService,
private val s3Service: S3Service,
private val adobeService: AdobeService
private val adobeService: AdobeService,
private val memberService: MemberService
) {
private val logger = LoggerFactory.getLogger(EpsonConnectController::class.java)

@PostMapping("/scan")
fun receiveScanData(@RequestParam files: Map<String, MultipartFile>) {
@PostMapping("/scan/{accountId}")
fun receiveScanData(@RequestParam files: Map<String, MultipartFile>, @PathVariable accountId: String) {
logger.info("Received {} files={}", files.values.size, files.values.map(MultipartFile::getOriginalFilename))
val member = memberService.findMemberByAccountId(accountId)

files.values.forEach { file ->
val mediaSubtype = MediaSubtype.parse(file.contentType)
Expand All @@ -32,7 +36,7 @@ class EpsonConnectController(
val uploadUrl = s3Service.getImageUrlForPOST(pathname)

adobeService.cutout(downloadUrl, uploadUrl)
drawingService.save(pathname, DrawingType.SCANNED)
drawingService.save(member, pathname, DrawingType.SCANNED)
}
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/com/chillin/exception/ExceptionController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.chillin.exception

import io.jsonwebtoken.JwtException
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice

@RestControllerAdvice
class ExceptionController {

@ExceptionHandler(JwtException::class)
fun handleJwtException(e: JwtException): ResponseEntity<ExceptionResponse> {
val response =
ExceptionResponse(401, HttpStatus.UNAUTHORIZED.reasonPhrase, e.message ?: "Failed to verify token")
return ResponseEntity.status(401).body(response)
}
}
7 changes: 7 additions & 0 deletions src/main/kotlin/com/chillin/exception/ExceptionResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.chillin.exception

class ExceptionResponse(
val code: Int,
val description: String,
val message: String,
)
1 change: 1 addition & 0 deletions src/main/kotlin/com/chillin/member/MemberRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import org.springframework.stereotype.Repository

@Repository
interface MemberRepository : JpaRepository<Member, Long> {
fun findByAccountId(accountId: String): Member?
}
Loading