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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/main/kotlin/com/moa/common/auth/AdminAuth.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.moa.common.auth

@Target(AnnotationTarget.FUNCTION)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Limiting @AdminAuth to FUNCTION prevents protecting an entire controller/class with a single annotation. If you want class-level protection, include AnnotationTarget.CLASS in @Target and update the interceptor to also check the declaring class (e.g., handler.beanType) for the annotation.

Suggested change
@Target(AnnotationTarget.FUNCTION)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 나중에 할게 필요할때...

@Retention(AnnotationRetention.RUNTIME)
annotation class AdminAuth
28 changes: 28 additions & 0 deletions src/main/kotlin/com/moa/common/auth/AdminAuthInterceptor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.moa.common.auth

import com.moa.common.exception.UnauthorizedException
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.stereotype.Component
import org.springframework.web.method.HandlerMethod
import org.springframework.web.servlet.HandlerInterceptor

@Component
class AdminAuthInterceptor(
private val adminProperties: AdminProperties,
) : HandlerInterceptor {

override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
if (handler is HandlerMethod && handler.hasMethodAnnotation(AdminAuth::class.java)) {
val key = request.getHeader(ADMIN_KEY_HEADER)
if (key != adminProperties.apiKey) {
throw UnauthorizedException()
}
Comment on lines +18 to +20
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct string comparison can leak timing information about the admin key (especially if this endpoint is exposed publicly). Use a constant-time comparison (e.g., compare byte arrays with a constant-time primitive) to reduce timing-attack surface.

Copilot uses AI. Check for mistakes.
}
return true
}
Comment on lines +15 to +23
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As written, admin-protected endpoints can fail CORS preflight requests because OPTIONS requests typically won't include X-Admin-Key, causing UnauthorizedException and blocking browsers. Consider bypassing auth for OPTIONS (or handling CORS at a higher level) before checking @AdminAuth.

Copilot uses AI. Check for mistakes.

companion object {
private const val ADMIN_KEY_HEADER = "X-Admin-Key"
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/com/moa/common/auth/AdminProperties.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.moa.common.auth

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "admin")
data class AdminProperties(
val apiKey: String,
)
Comment on lines +5 to +8
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ConfigurationProperties classes are not automatically registered as beans unless the project has @ConfigurationPropertiesScan enabled (or uses @EnableConfigurationProperties(AdminProperties::class)). Since AdminAuthInterceptor injects AdminProperties, missing registration will fail app startup. Fix by registering this properties class via @ConfigurationPropertiesScan in the application/config package, adding @EnableConfigurationProperties(AdminProperties::class) in a @Configuration class, or annotating this class with a Spring stereotype (least preferred vs scan/enable).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Application.kt에서 Scan 하는디?..

11 changes: 10 additions & 1 deletion src/main/kotlin/com/moa/common/config/SwaggerConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,17 @@ class SwaggerConfig {
.bearerFormat(jwt)
.`in`(SecurityScheme.In.HEADER)

val adminKeyScheme = SecurityScheme()
.name("X-Admin-Key")
.type(SecurityScheme.Type.APIKEY)
.`in`(SecurityScheme.In.HEADER)

return OpenAPI()
.components(Components().addSecuritySchemes(securitySchemeName, securityScheme))
.components(
Components()
.addSecuritySchemes(securitySchemeName, securityScheme)
.addSecuritySchemes("AdminKey", adminKeyScheme)
)
.addSecurityItem(securityRequirement)
.servers(
listOf(
Expand Down
7 changes: 7 additions & 0 deletions src/main/kotlin/com/moa/common/config/WebConfig.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
package com.moa.common.config

import com.moa.common.auth.AdminAuthInterceptor
import com.moa.common.auth.AuthMemberResolver
import com.moa.common.auth.OnboardingAuthMemberResolver
import org.springframework.context.annotation.Configuration
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Configuration
class WebConfig(
private val onboardingAuthMemberResolver: OnboardingAuthMemberResolver,
private val authMemberResolver: AuthMemberResolver,
private val adminAuthInterceptor: AdminAuthInterceptor,
) : WebMvcConfigurer {

override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(adminAuthInterceptor)
}

override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(onboardingAuthMemberResolver)
resolvers.add(authMemberResolver)
Expand Down
11 changes: 11 additions & 0 deletions src/main/kotlin/com/moa/controller/VersionController.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package com.moa.controller

import com.moa.common.auth.AdminAuth
import com.moa.common.response.ApiResponse
import com.moa.entity.OsType
import com.moa.service.AppVersionService
import com.moa.service.dto.AppVersionUpdateRequest
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PutMapping
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.RestController
Expand All @@ -19,4 +24,10 @@ class VersionController(
@GetMapping
fun getVersion(@RequestParam osType: OsType) =
ApiResponse.success(appVersionService.getVersion(osType))

@AdminAuth
@SecurityRequirement(name = "AdminKey")
@PutMapping
fun updateVersion(@RequestBody request: AppVersionUpdateRequest) =
ApiResponse.success(appVersionService.updateVersion(request))
}
13 changes: 13 additions & 0 deletions src/main/kotlin/com/moa/service/AppVersionService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.moa.common.exception.NotFoundException
import com.moa.entity.OsType
import com.moa.repository.AppVersionRepository
import com.moa.service.dto.AppVersionResponse
import com.moa.service.dto.AppVersionUpdateRequest
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

Expand All @@ -21,4 +22,16 @@ class AppVersionService(
minimumVersion = version.minimumVersion,
)
}

@Transactional
fun updateVersion(request: AppVersionUpdateRequest): AppVersionResponse {
val version = appVersionRepository.findByOsType(request.osType)
?: throw NotFoundException()
version.latestVersion = request.latestVersion
version.minimumVersion = request.minimumVersion
return AppVersionResponse(
latestVersion = version.latestVersion,
minimumVersion = version.minimumVersion,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.moa.service.dto

import com.moa.entity.OsType

data class AppVersionUpdateRequest(
val osType: OsType,
val latestVersion: String,
val minimumVersion: String,
)
1 change: 1 addition & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ spring:
- moa-secret/oauth.yml
- moa-secret/jwt.yml
- moa-secret/db.yml
- moa-secret/admin.yml

profiles:
default: local
2 changes: 1 addition & 1 deletion src/main/resources/moa-secret
Loading