Merge pull request #18 from beaniejoy/feature/17

Spring Security를 사용한 인증 프로세스 적용
This commit is contained in:
Hanbin Lee
2022-11-01 03:25:40 +09:00
committed by GitHub
95 changed files with 748 additions and 102 deletions

View File

@@ -1,79 +0,0 @@
buildscript {
ext {
springBootVersion = '2.7.0'
dependencyManagementVersion = '1.0.11.RELEASE'
kotlinVersion = '1.6.21'
flywayVersion = '7.15.0'
}
repositories {
mavenCentral()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
classpath "io.spring.gradle:dependency-management-plugin:${dependencyManagementVersion}"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
classpath "org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}"
classpath "org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}"
}
}
apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: "kotlin-jpa"
apply plugin: "kotlin-noarg"
apply plugin: "kotlin-allopen"
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'io.beaniejoy.dongecafe'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
allOpen {
annotation("javax.persistence.Entity")
annotation("javax.persistence.MappedSuperclass")
annotation("javax.persistence.Embeddable")
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}")
implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation('io.github.microutils:kotlin-logging:2.1.21')
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
// log4j2
// implementation 'org.springframework.boot:spring-boot-starter-log4j2'
// testImplementation 'org.springframework.boot:spring-boot-starter-log4j2'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'mysql:mysql-connector-java' // MySQL
runtimeOnly 'com.h2database:h2' // H2
implementation "org.flywaydb:flyway-core:${flywayVersion}" // flyway
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
//configurations {
// all {
// // log4j2 적용을 위해 기존 spring boot에서 제공하는 logging exclude
// exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
// }
//}
test {
useJUnitPlatform()
}

89
build.gradle.kts Normal file
View File

@@ -0,0 +1,89 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
buildscript {
repositories {
mavenCentral()
}
}
plugins {
id("org.springframework.boot") version "2.7.0"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.6.21"
kotlin("plugin.spring") version "1.6.21" apply false // TODO: apply false what?
kotlin("plugin.jpa") version "1.6.21" apply false
}
java.sourceCompatibility = JavaVersion.VERSION_17
allprojects {
group = "io.beaniejoy.dongecafe"
version = "0.0.1-SNAPSHOT"
repositories {
mavenCentral()
}
}
subprojects {
apply(plugin = "java")
apply(plugin = "io.spring.dependency-management")
apply(plugin = "org.springframework.boot")
apply(plugin = "org.jetbrains.kotlin.plugin.spring")
apply(plugin = "kotlin")
apply(plugin = "kotlin-spring")
apply(plugin = "kotlin-jpa")
repositories {
mavenCentral()
}
// configurations {
// all {
// // log4j2 적용을 위해 기존 spring boot에서 제공하는 logging exclude
// exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
// }
// }
dependencies {
// Spring Boot Project
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
developmentOnly("org.springframework.boot:spring-boot-devtools")
//kotlin
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
// DB
runtimeOnly("mysql:mysql-connector-java") // MySQL
runtimeOnly("com.h2database:h2") // H2
implementation("org.flywaydb:flyway-core:7.15.0") // flyway
// Logging
// log4j2
// implementation("org.springframework.boot:spring-boot-starter-log4j2")
// testImplementation("org.springframework.boot:spring-boot-starter-log4j2")
implementation("io.github.microutils:kotlin-logging:2.1.21")
// Test
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
}

View File

@@ -0,0 +1,3 @@
dependencies {
implementation(project(":dongne-common"))
}

View File

@@ -4,8 +4,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class DongneCafeApiApplication
class DongneAccountApiApplication
fun main(args: Array<String>) {
runApplication<DongneCafeApiApplication>(*args)
runApplication<DongneAccountApiApplication>(*args)
}

View File

@@ -0,0 +1,73 @@
package io.beaniejoy.dongnecafe.common.config
import io.beaniejoy.dongnecafe.common.security.ApiAuthenticationFilter
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.factory.PasswordEncoderFactories
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.AuthenticationFailureHandler
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Autowired
lateinit var authenticationConfiguration: AuthenticationConfiguration
@Autowired
lateinit var apiAuthenticationSuccessHandler: AuthenticationSuccessHandler
@Autowired
lateinit var apiAuthenticationFailureHandler: AuthenticationFailureHandler
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
return http
.csrf().disable()
.formLogin().disable()
.authorizeRequests()
.antMatchers("/auth/members/sign-up").permitAll()
.antMatchers("/test").hasRole("USER") // 임시 인가 테스트용
.anyRequest().authenticated()
.and()
.addFilterBefore(apiAuthenticationFilter(), UsernamePasswordAuthenticationFilter::class.java)
.build()
}
@Bean
fun webSecurityCustomizer(): WebSecurityCustomizer {
return WebSecurityCustomizer { web ->
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations())
web.ignoring().antMatchers("/error")
}
}
@Bean
fun passwordEncoder(): PasswordEncoder {
return PasswordEncoderFactories.createDelegatingPasswordEncoder()
}
@Bean
fun apiAuthenticationFilter(): ApiAuthenticationFilter {
return ApiAuthenticationFilter(
AntPathRequestMatcher("/auth/authenticate", HttpMethod.POST.name)
).apply {
this.setAuthenticationManager(authenticationConfiguration.authenticationManager)
this.setAuthenticationSuccessHandler(apiAuthenticationSuccessHandler)
this.setAuthenticationFailureHandler(apiAuthenticationFailureHandler)
}
}
}

View File

@@ -0,0 +1,12 @@
package io.beaniejoy.dongnecafe.common.entity
import org.springframework.data.domain.AuditorAware
import org.springframework.stereotype.Component
import java.util.*
@Component
class BaseEntityAuditorAware: AuditorAware<String> {
override fun getCurrentAuditor(): Optional<String> {
return Optional.of("system")
}
}

View File

@@ -0,0 +1,53 @@
package io.beaniejoy.dongnecafe.common.security
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.beaniejoy.dongnecafe.domain.member.model.request.SignInRequest
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
import org.springframework.util.StringUtils
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
class ApiAuthenticationFilter(requestMatcher: AntPathRequestMatcher) :
AbstractAuthenticationProcessingFilter(requestMatcher) {
private val objectMapper = jacksonObjectMapper()
override fun attemptAuthentication(
request: HttpServletRequest,
response: HttpServletResponse,
): Authentication {
if (isValidRequest(request).not()) {
throw IllegalStateException("request is not supported. check request method and content-type")
}
val signInRequest = objectMapper.readValue(request.reader, SignInRequest::class.java)
request.setAttribute("email", signInRequest.email)
val token = signInRequest.let {
if (StringUtils.hasText(it.email).not() || StringUtils.hasText(it.password).not()) {
throw IllegalArgumentException("Email & Password are not empty!!")
}
UsernamePasswordAuthenticationToken(it.email, it.password)
}
return authenticationManager.authenticate(token)
}
private fun isValidRequest(request: HttpServletRequest): Boolean {
if (request.method != HttpMethod.POST.name) {
return false
}
if (request.contentType != MediaType.APPLICATION_JSON_VALUE) {
return false
}
return true
}
}

View File

@@ -0,0 +1,43 @@
package io.beaniejoy.dongnecafe.common.security
import mu.KLogging
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Component
/**
* 실제 인증 절차 수행
* @property userDetailsService email로 계정 찾기
*/
@Component
class ApiAuthenticationProvider(
private val userDetailsService: UserDetailsService,
private val passwordEncoder: PasswordEncoder
) : AuthenticationProvider {
companion object: KLogging()
override fun authenticate(authentication: Authentication): Authentication {
logger.info { "start authentication" }
val email = authentication.name
val password = authentication.credentials as String?
val user = userDetailsService.loadUserByUsername(email)
if (!passwordEncoder.matches(password, user.password)) {
throw BadCredentialsException("Input password does not match stored password")
}
logger.info { "User password ${user.password}" }
// password null로 반환
return UsernamePasswordAuthenticationToken(user, null, user.authorities)
}
override fun supports(authentication: Class<*>): Boolean {
return UsernamePasswordAuthenticationToken::class.java.isAssignableFrom(authentication)
}
}

View File

@@ -0,0 +1,42 @@
package io.beaniejoy.dongnecafe.common.security.handler
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.beaniejoy.dongnecafe.common.security.model.AuthenticationResult
import io.beaniejoy.dongnecafe.domain.member.model.request.SignInRequest
import mu.KLogging
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.authentication.AuthenticationFailureHandler
import org.springframework.stereotype.Component
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Component
class ApiAuthenticationFailureHandler : AuthenticationFailureHandler {
private val objectMapper = jacksonObjectMapper()
companion object : KLogging()
override fun onAuthenticationFailure(
request: HttpServletRequest,
response: HttpServletResponse,
exception: AuthenticationException,
) {
val email = request.getAttribute("email") as String
logger.error { "[AUTH FAILED] $email" }
response.apply {
this.status = HttpStatus.UNAUTHORIZED.value()
this.contentType = MediaType.APPLICATION_JSON_VALUE
}
objectMapper.writeValue(
response.writer,
AuthenticationResult(
email = email,
msg = "authentication failed"
)
)
}
}

View File

@@ -0,0 +1,43 @@
package io.beaniejoy.dongnecafe.common.security.handler
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.beaniejoy.dongnecafe.common.security.model.AuthenticationResult
import mu.KLogging
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.core.Authentication
import org.springframework.security.core.userdetails.User
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
import org.springframework.stereotype.Component
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Component
class ApiAuthenticationSuccessHandler :AuthenticationSuccessHandler {
private val objectMapper = jacksonObjectMapper()
companion object: KLogging()
override fun onAuthenticationSuccess(
request: HttpServletRequest,
response: HttpServletResponse,
authentication: Authentication,
) {
val user = authentication.principal as User
logger.info { "[AUTH SUCCESS] email: ${user.username}, authorities: ${user.authorities}" }
response.apply {
this.status = HttpStatus.OK.value()
this.contentType = MediaType.APPLICATION_JSON_VALUE
}
objectMapper.writeValue(
response.writer,
AuthenticationResult(
email = user.username,
authorities = user.authorities,
msg = "authentication success"
)
)
}
}

View File

@@ -0,0 +1,11 @@
package io.beaniejoy.dongnecafe.common.security.model
import com.fasterxml.jackson.annotation.JsonInclude
import org.springframework.security.core.GrantedAuthority
data class AuthenticationResult(
val email: String,
@JsonInclude(JsonInclude.Include.NON_EMPTY)
val authorities: Collection<GrantedAuthority> = listOf(),
val msg: String
)

View File

@@ -0,0 +1,23 @@
package io.beaniejoy.dongnecafe.controller
import io.beaniejoy.dongnecafe.domain.member.model.request.SignInRequest
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
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.RestController
@RestController
@RequestMapping("/auth")
class AuthController(
private val authenticationManagerBuilder: AuthenticationManagerBuilder
) {
// @PostMapping("/authenticate")
// fun signIn(@RequestBody signInRequest: SignInRequest) {
// val authenticationToken =
// UsernamePasswordAuthenticationToken(signInRequest.email, signInRequest.password)
//
// val authenticate = authenticationManagerBuilder.`object`.authenticate(authenticationToken)
// }
}

View File

@@ -0,0 +1,19 @@
package io.beaniejoy.dongnecafe.controller
import io.beaniejoy.dongnecafe.domain.member.model.request.MemberRegisterRequest
import io.beaniejoy.dongnecafe.service.MemberService
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.RestController
@RestController
@RequestMapping("/auth/members")
class MemberController(
private val memberService: MemberService
) {
@PostMapping("/sign-up")
fun signUp(@RequestBody resource: MemberRegisterRequest): Long {
return memberService.registerMember(resource)
}
}

View File

@@ -0,0 +1,13 @@
package io.beaniejoy.dongnecafe.controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class TestController {
@GetMapping("/test")
fun test(): String {
return "authorize OK!"
}
}

View File

@@ -0,0 +1,3 @@
package io.beaniejoy.dongnecafe.error
class MemberExistedException(email: String): RuntimeException("Member[$email] is already existed")

View File

@@ -0,0 +1,3 @@
package io.beaniejoy.dongnecafe.error
class MemberDeactivatedException(email: String): RuntimeException("Member[$email] is deactivated")

View File

@@ -0,0 +1,33 @@
package io.beaniejoy.dongnecafe.service
import io.beaniejoy.dongnecafe.domain.member.entity.Member
import io.beaniejoy.dongnecafe.domain.member.model.request.MemberRegisterRequest
import io.beaniejoy.dongnecafe.domain.member.repository.MemberRepository
import io.beaniejoy.dongnecafe.error.MemberExistedException
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
@Transactional
class MemberService(
private val memberRepository: MemberRepository,
private val passwordEncoder: PasswordEncoder
) {
fun registerMember(resource: MemberRegisterRequest): Long {
memberRepository.findByEmail(resource.email!!)?.also {
throw MemberExistedException(resource.email!!)
}
val registeredMember = memberRepository.save(
Member.createMember(
email = resource.email!!,
password = passwordEncoder.encode(resource.password!!),
address = resource.address!!,
phoneNumber = resource.phoneNumber!!
)
)
return registeredMember.id
}
}

View File

@@ -0,0 +1,38 @@
package io.beaniejoy.dongnecafe.service
import io.beaniejoy.dongnecafe.domain.member.entity.Member
import io.beaniejoy.dongnecafe.domain.member.repository.MemberRepository
import io.beaniejoy.dongnecafe.error.MemberDeactivatedException
import mu.KLogging
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Component
@Component("userDetailsService")
class UserDetailsServiceImpl(
private val memberRepository: MemberRepository
) : UserDetailsService {
companion object: KLogging()
override fun loadUserByUsername(email: String): UserDetails {
return memberRepository.findByEmail(email)?.let {
logger.info { "[LOAD MEMBER] email: ${it.email}, role: ${it.roleType}, activated: ${it.activated}" }
createSecurityUser(it)
} ?: throw UsernameNotFoundException(email)
}
private fun createSecurityUser(member: Member): User {
if (member.activated.not()) {
throw MemberDeactivatedException(member.email)
}
return User(
/* username = */ member.email,
/* password = */ member.password,
/* authorities = */ listOf(SimpleGrantedAuthority(member.roleType.name))
)
}
}

View File

@@ -0,0 +1,27 @@
spring:
profiles:
active: local
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: none # use [service-api] flyway migration
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
format_sql: true
show-sql: false
open-in-view:
flyway:
enabled: false
devtools:
livereload:
enabled: false # no use devtools' LiveReload Server
server:
port: 9090
logging:
level:
org.hibernate.SQL: debug # logger 통해 로깅
# org.hibernate.type: trace

View File

@@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
internal class DongneCafeSirenOrderApplicationTests {
internal class DongneAccountApiApplicationTests {
@Test
fun contextLoads() {

View File

@@ -0,0 +1,19 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar
val jar: Jar by tasks
val bootJar: BootJar by tasks
bootJar.enabled = false
jar.enabled = true
allOpen {
annotation("javax.persistence.Entity")
annotation("javax.persistence.Embeddable")
annotation("javax.persistence.MappedSuperclass")
}
noArg {
annotation("javax.persistence.Entity")
annotation("javax.persistence.Embeddable")
annotation("javax.persistence.MappedSuperclass")
}

View File

@@ -1,4 +1,4 @@
package io.beaniejoy.dongnecafe.common.entity
package io.beaniejoy.dongnecafe.common
import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.CreatedDate

View File

@@ -1,6 +1,6 @@
package io.beaniejoy.dongnecafe.domain.cafe.entity
import io.beaniejoy.dongnecafe.common.entity.BaseTimeEntity
import io.beaniejoy.dongnecafe.common.BaseTimeEntity
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuRegisterRequest
import javax.persistence.*

View File

@@ -1,6 +1,6 @@
package io.beaniejoy.dongnecafe.domain.cafe.entity
import io.beaniejoy.dongnecafe.common.entity.BaseTimeEntity
import io.beaniejoy.dongnecafe.common.BaseTimeEntity
import javax.persistence.*
@Entity

View File

@@ -1,6 +1,6 @@
package io.beaniejoy.dongnecafe.domain.cafe.entity
import io.beaniejoy.dongnecafe.common.entity.BaseTimeEntity
import io.beaniejoy.dongnecafe.common.BaseTimeEntity
import io.beaniejoy.dongnecafe.domain.cafe.model.request.MenuOptionRegisterRequest
import java.math.BigDecimal
import javax.persistence.*

View File

@@ -1,6 +1,6 @@
package io.beaniejoy.dongnecafe.domain.cafe.entity
import io.beaniejoy.dongnecafe.common.entity.BaseTimeEntity
import io.beaniejoy.dongnecafe.common.BaseTimeEntity
import io.beaniejoy.dongnecafe.domain.cafe.model.request.OptionDetailRegisterRequest
import javax.persistence.*

View File

@@ -1,6 +1,6 @@
package io.beaniejoy.dongnecafe.domain.cafe.entity
import io.beaniejoy.dongnecafe.common.entity.BaseTimeEntity
import io.beaniejoy.dongnecafe.common.BaseTimeEntity
import java.math.BigDecimal
import javax.persistence.*

View File

@@ -0,0 +1,8 @@
package io.beaniejoy.dongnecafe.domain.member.constant
enum class RoleType(val value: String) {
ROLE_ADMIN("어드민 관리자"),
ROLE_USER("일반 사용자"),
ROLE_OWNER("카페 관리자");
;
}

View File

@@ -0,0 +1,60 @@
package io.beaniejoy.dongnecafe.domain.member.entity
import io.beaniejoy.dongnecafe.common.BaseTimeEntity
import io.beaniejoy.dongnecafe.domain.member.constant.RoleType
import javax.persistence.*
@Entity
@Table(name = "member")
class Member(
email: String,
password: String,
address: String,
phoneNumber: String
): BaseTimeEntity() {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id", nullable = false)
val id: Long = 0L
@Column(name = "email", nullable = false)
var email: String = email
protected set
@Column(name = "password", nullable = false)
var password: String = password
protected set
@Column(name = "address", nullable = false)
var address: String = address
protected set
@Column(name = "phone_number", nullable = false)
var phoneNumber: String = phoneNumber
protected set
@Enumerated(EnumType.STRING)
@Column(name = "role_type", nullable = false)
var roleType: RoleType = RoleType.ROLE_USER
protected set
@Column(name = "activated", nullable = false)
var activated: Boolean = true
protected set
companion object {
fun createMember(
email: String,
password: String,
address: String,
phoneNumber: String
): Member {
return Member(
email = email,
password = password,
address = address,
phoneNumber = phoneNumber
)
}
}
}

View File

@@ -0,0 +1,8 @@
package io.beaniejoy.dongnecafe.domain.member.model.request
class MemberRegisterRequest(
val email: String? = null,
val password: String? = null,
val address: String? = null,
val phoneNumber: String? = null
)

View File

@@ -0,0 +1,6 @@
package io.beaniejoy.dongnecafe.domain.member.model.request
data class SignInRequest(
val email: String,
val password: String
)

View File

@@ -0,0 +1,8 @@
package io.beaniejoy.dongnecafe.domain.member.repository
import io.beaniejoy.dongnecafe.domain.member.entity.Member
import org.springframework.data.jpa.repository.JpaRepository
interface MemberRepository : JpaRepository<Member, Long> {
fun findByEmail(email: String): Member?
}

View File

@@ -0,0 +1,3 @@
dependencies {
implementation(project(":dongne-common"))
}

View File

@@ -0,0 +1,11 @@
package io.beaniejoy.dongnecafe
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class DongneServiceApiApplication
fun main(args: Array<String>) {
runApplication<DongneServiceApiApplication>(*args)
}

View File

@@ -0,0 +1,8 @@
package io.beaniejoy.dongnecafe.common.config
import org.springframework.context.annotation.Configuration
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
@Configuration
@EnableJpaAuditing
class AuditingConfig

View File

@@ -0,0 +1,34 @@
package io.beaniejoy.dongnecafe.common.config
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer
import org.springframework.security.web.SecurityFilterChain
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
return http
.csrf().disable()
.formLogin().disable()
.authorizeRequests()
.anyRequest().authenticated() // 임시 허용
.and()
.build()
}
@Bean
fun webSecurityCustomizer(): WebSecurityCustomizer {
return WebSecurityCustomizer { web ->
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations())
web.ignoring().antMatchers("/error")
}
}
}

View File

@@ -12,7 +12,7 @@ import org.springframework.data.web.PageableDefault
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/cafes")
@RequestMapping("/api/cafes")
class CafeController(
private val cafeService: CafeService
) {

View File

@@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/cafes/{cafeId}/menus")
@RequestMapping("/api/cafes/{cafeId}/menus")
class CafeMenuController(
private val cafeMenuService: CafeMenuService
) {

View File

@@ -0,0 +1,5 @@
spring:
datasource:
url: jdbc:mysql://localhost:3306/dongne?autoreconnect=true&characterEncoding=utf8&serverTimezone=Asia/Seoul
username: root
password: beaniejoy # TODO 추후 보안에 대해 생각해보기

View File

@@ -13,8 +13,11 @@ spring:
show-sql: false
flyway:
baseline-on-migrate: true
baseline-version: 0
baseline-version: "000"
locations: classpath:db/migration,classpath:db/seed
devtools:
livereload:
enabled: false # no use devtools' LiveReload Server
logging:
level:

View File

@@ -0,0 +1,14 @@
CREATE TABLE `member` (
`member_id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '회원 ID',
`email` varchar(20) NOT NULL COMMENT '계정 이메일',
`password` varchar(255) NOT NULL COMMENT '비밀번호',
`address` varchar(100) NOT NULL COMMENT '회원 주소',
`phone_number` varchar(11) NOT NULL COMMENT '회원 전화번호',
`role_type` varchar(20) COMMENT '회원 권한',
`activated` tinyint(1) NOT NULL COMMENT '계정 활성화 여부',
`created_at` datetime NOT NULL COMMENT '회원 등록날짜',
`created_by` varchar(20) NOT NULL COMMENT '회원 등록자',
`updated_at` datetime NULL COMMENT '회원 변경날짜',
`updated_by` varchar(20) NULL COMMENT '회원 변경자',
PRIMARY KEY (`member_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,12 @@
package io.beaniejoy.dongnecafe
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
internal class DongneServiceApiApplicationTests {
@Test
fun contextLoads() {
}
}

View File

@@ -1,6 +1,8 @@
package io.beaniejoy.dongnecafe.domain.cafe.service
import io.beaniejoy.dongnecafe.domain.cafe.model.request.*
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuUpdateRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.request.MenuOptionUpdateRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.request.OptionDetailUpdateRequest
import io.beaniejoy.dongnecafe.domain.cafe.utils.CafeTestUtils
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired

View File

@@ -98,7 +98,7 @@ internal class CafeServiceTest {
// TODO 'findByIdOrNull'은 kotlin test 라이브러리 필요한 듯
val mockCafe = mock(Cafe::class.java)
doReturn(Optional.of(cafe)).`when`(mockCafeRepository.findById(eq(cafeId)))
`when`(mockCafeRepository.findById(cafeId)).thenReturn(Optional.of(mockCafe))
doNothing().`when`(mockCafe).updateInfo(
name = anyString(),
@@ -109,7 +109,7 @@ internal class CafeServiceTest {
// when
mockCafeService.updateInfo(
id = eq(cafeId),
id = cafeId,
name = "updated_name",
address = "updated_address",
phoneNumber = "updated_phoneNumber",

View File

@@ -1,15 +1,8 @@
package io.beaniejoy.dongnecafe.domain.cafe.utils
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeRegisterRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuRegisterRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.request.MenuOptionRegisterRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.request.OptionDetailRegisterRequest
import io.beaniejoy.dongnecafe.domain.cafe.entity.Cafe
import io.beaniejoy.dongnecafe.domain.cafe.entity.CafeMenu
import io.beaniejoy.dongnecafe.domain.cafe.entity.MenuOption
import io.beaniejoy.dongnecafe.domain.cafe.entity.OptionDetail
import org.junit.jupiter.api.Assertions.*
import java.math.BigDecimal
import javax.persistence.GeneratedValue
class CafeTestUtils {

View File

@@ -1 +0,0 @@
rootProject.name = 'dongne-cafe-api'

4
settings.gradle.kts Normal file
View File

@@ -0,0 +1,4 @@
rootProject.name = "dongne-cafe-api"
include("dongne-common")
include("dongne-service-api")
include("dongne-account-api")