diff --git a/.kotlin/errors/errors-1727090346428.log b/.kotlin/errors/errors-1727090346428.log new file mode 100644 index 00000000..7cd151b6 --- /dev/null +++ b/.kotlin/errors/errors-1727090346428.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.20 +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/api/build.gradle.kts b/api/build.gradle.kts index f25fbc7c..4503682a 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -9,6 +9,8 @@ dependencies { implementation(project(":applicant:applicant-jpa-adapter")) implementation(project(":applicant:applicant-web-adapter")) implementation(project(":applicant:applicant-s3-adapter")) + implementation(project(":applicant:applicant-redis-adapter")) + implementation(project(":applicant:applicant-smtp-adapter")) implementation(project(":admission:admission-application")) implementation(project(":admission:admission-jpa-adapter")) diff --git a/applicant/applicant-application/src/main/kotlin/com/daegusw/apply/applicant/application/port/in/web/SmtpUseCase.kt b/applicant/applicant-application/src/main/kotlin/com/daegusw/apply/applicant/application/port/in/web/SmtpUseCase.kt new file mode 100644 index 00000000..769f54f5 --- /dev/null +++ b/applicant/applicant-application/src/main/kotlin/com/daegusw/apply/applicant/application/port/in/web/SmtpUseCase.kt @@ -0,0 +1,6 @@ +package com.daegusw.apply.applicant.application.port.`in`.web + +interface SmtpUseCase { + suspend fun verify(email: String, code: String): String + suspend fun send(email: String): String +} \ No newline at end of file diff --git a/applicant/applicant-application/src/main/kotlin/com/daegusw/apply/applicant/application/port/out/redis/RedisPort.kt b/applicant/applicant-application/src/main/kotlin/com/daegusw/apply/applicant/application/port/out/redis/RedisPort.kt new file mode 100644 index 00000000..75f6c344 --- /dev/null +++ b/applicant/applicant-application/src/main/kotlin/com/daegusw/apply/applicant/application/port/out/redis/RedisPort.kt @@ -0,0 +1,8 @@ +package com.daegusw.apply.applicant.application.port.out.redis + +interface RedisPort { + fun save(key: String, value: String, expiration: Long) + fun delete(key: String) + fun get(key: String): String? + fun update(key: String, value: String) +} \ No newline at end of file diff --git a/applicant/applicant-application/src/main/kotlin/com/daegusw/apply/applicant/application/port/out/smtp/SmtpPort.kt b/applicant/applicant-application/src/main/kotlin/com/daegusw/apply/applicant/application/port/out/smtp/SmtpPort.kt new file mode 100644 index 00000000..30a76d7c --- /dev/null +++ b/applicant/applicant-application/src/main/kotlin/com/daegusw/apply/applicant/application/port/out/smtp/SmtpPort.kt @@ -0,0 +1,5 @@ +package com.daegusw.apply.applicant.application.port.out.smtp + +interface SmtpPort { + suspend fun send(e: String) : String +} \ No newline at end of file diff --git a/applicant/applicant-application/src/main/kotlin/com/daegusw/apply/applicant/application/service/SmtpService.kt b/applicant/applicant-application/src/main/kotlin/com/daegusw/apply/applicant/application/service/SmtpService.kt new file mode 100644 index 00000000..918a1712 --- /dev/null +++ b/applicant/applicant-application/src/main/kotlin/com/daegusw/apply/applicant/application/service/SmtpService.kt @@ -0,0 +1,29 @@ +package com.daegusw.apply.applicant.application.service + +import com.daegusw.apply.applicant.application.port.`in`.web.SmtpUseCase +import com.daegusw.apply.applicant.application.port.out.redis.RedisPort +import com.daegusw.apply.applicant.application.port.out.smtp.SmtpPort +import org.springframework.stereotype.Service + +@Service +class SmtpService( + private val stmpPort: SmtpPort, + private val redisPort: RedisPort, +) : SmtpUseCase { + + override suspend fun verify(email: String, code: String): String { + val value = redisPort.get(email) + if((value != null) && value == code){ + redisPort.update(email, "verified") + return "verified" + } + throw RuntimeException("Smtp verification failed") + } + + override suspend fun send(email: String): String { + val code = stmpPort.send(email) + redisPort.save(email,code,3L) //3분 + return "smtp success" + } + +} \ No newline at end of file diff --git a/applicant/applicant-redis-adapter/build.gradle.kts b/applicant/applicant-redis-adapter/build.gradle.kts new file mode 100644 index 00000000..387b9ed8 --- /dev/null +++ b/applicant/applicant-redis-adapter/build.gradle.kts @@ -0,0 +1,4 @@ +dependencies { + implementation("org.springframework.boot:spring-boot-starter-data-redis") + implementation(project(":applicant:applicant-application")) +} \ No newline at end of file diff --git a/applicant/applicant-redis-adapter/src/main/kotlin/com/daegusw/apply/applicant/redis/adapter/RedisAdapter.kt b/applicant/applicant-redis-adapter/src/main/kotlin/com/daegusw/apply/applicant/redis/adapter/RedisAdapter.kt new file mode 100644 index 00000000..a4f2ae67 --- /dev/null +++ b/applicant/applicant-redis-adapter/src/main/kotlin/com/daegusw/apply/applicant/redis/adapter/RedisAdapter.kt @@ -0,0 +1,45 @@ +package com.daegusw.apply.applicant.redis.adapter + +import com.daegusw.apply.applicant.application.port.out.redis.RedisPort +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.stereotype.Component +import java.time.Duration + +@Component +class RedisAdapter( + private val redisTemplate: RedisTemplate, +) : RedisPort { + override fun save(key: String, value: String, expiration: Long) { + try{ + redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(expiration)) + + }catch(e:Exception){ + throw e + } + } + + override fun delete(key: String) { + try{ + redisTemplate.delete(key) + }catch(e:Exception){ + throw e + } + } + + override fun get(key: String): String? { + try { + return redisTemplate.opsForValue().get(key) + }catch(e:Exception){ + throw e + } + } + + override fun update(key: String, value: String) { + try{ + redisTemplate.opsForValue().set(key, value) + }catch(e:Exception){ + throw e + } + } + +} \ No newline at end of file diff --git a/applicant/applicant-redis-adapter/src/main/kotlin/com/daegusw/apply/applicant/redis/adapter/common/RedisConfig.kt b/applicant/applicant-redis-adapter/src/main/kotlin/com/daegusw/apply/applicant/redis/adapter/common/RedisConfig.kt new file mode 100644 index 00000000..a0666d8f --- /dev/null +++ b/applicant/applicant-redis-adapter/src/main/kotlin/com/daegusw/apply/applicant/redis/adapter/common/RedisConfig.kt @@ -0,0 +1,30 @@ +package com.daegusw.apply.applicant.redis.adapter.common + +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 +import org.springframework.data.redis.serializer.StringRedisSerializer + +@Configuration +@EnableRedisRepositories +class RedisConfig( + private val redisProperties: RedisProperties, +) { + @Bean + fun redisConnectionFactory(): RedisConnectionFactory { + return LettuceConnectionFactory(redisProperties.host, redisProperties.port) + } + + @Bean + fun redisTemplate(): RedisTemplate<*, *> { + return RedisTemplate().apply { + this.connectionFactory = redisConnectionFactory() + this.keySerializer = StringRedisSerializer() + this.hashKeySerializer = StringRedisSerializer() + this.valueSerializer = StringRedisSerializer() + } + } +} \ No newline at end of file diff --git a/applicant/applicant-redis-adapter/src/main/kotlin/com/daegusw/apply/applicant/redis/adapter/common/RedisProperties.kt b/applicant/applicant-redis-adapter/src/main/kotlin/com/daegusw/apply/applicant/redis/adapter/common/RedisProperties.kt new file mode 100644 index 00000000..04131dc8 --- /dev/null +++ b/applicant/applicant-redis-adapter/src/main/kotlin/com/daegusw/apply/applicant/redis/adapter/common/RedisProperties.kt @@ -0,0 +1,12 @@ +package com.daegusw.apply.applicant.redis.adapter.common + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.PropertySource +import org.springframework.stereotype.Component + +@Component +@PropertySource("classpath:application.yml") +class RedisProperties ( + @Value("\${spring.redis.host:host}") val host: String, + @Value("\${spring.redis.port:6379}") val port: Int, +) \ No newline at end of file diff --git a/applicant/applicant-redis-adapter/src/main/resources/application.yml b/applicant/applicant-redis-adapter/src/main/resources/application.yml new file mode 100644 index 00000000..899a47e5 --- /dev/null +++ b/applicant/applicant-redis-adapter/src/main/resources/application.yml @@ -0,0 +1,4 @@ +spring: + redis: + host: ${redis.host} + port: 6379 \ No newline at end of file diff --git a/applicant/applicant-smtp-adapter/build.gradle.kts b/applicant/applicant-smtp-adapter/build.gradle.kts new file mode 100644 index 00000000..f0e0cc11 --- /dev/null +++ b/applicant/applicant-smtp-adapter/build.gradle.kts @@ -0,0 +1,7 @@ +dependencies { + implementation("org.springframework.boot:spring-boot-starter-thymeleaf") + implementation("aws.sdk.kotlin:ses:0.16.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation ("org.springframework.boot:spring-boot-starter-mail") + implementation(project(":applicant:applicant-application")) +} \ No newline at end of file diff --git a/applicant/applicant-smtp-adapter/src/main/kotlin/com/daegusw/apply/applicant/smtp/adapter/SmtpAdapter.kt b/applicant/applicant-smtp-adapter/src/main/kotlin/com/daegusw/apply/applicant/smtp/adapter/SmtpAdapter.kt new file mode 100644 index 00000000..3264145d --- /dev/null +++ b/applicant/applicant-smtp-adapter/src/main/kotlin/com/daegusw/apply/applicant/smtp/adapter/SmtpAdapter.kt @@ -0,0 +1,59 @@ +package com.daegusw.apply.applicant.smtp.adapter + +import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider +import aws.sdk.kotlin.services.ses.SesClient +import aws.sdk.kotlin.services.ses.model.* +import com.daegusw.apply.applicant.application.port.out.smtp.SmtpPort +import com.daegusw.apply.applicant.smtp.adapter.common.ApplicantSmtpProperties +import org.springframework.stereotype.Component +import org.thymeleaf.context.Context +import org.thymeleaf.spring5.SpringTemplateEngine + +@Component +class SmtpAdapter( + private val applicantSmtpProperties: ApplicantSmtpProperties, + private val templateEngine: SpringTemplateEngine, + ) : SmtpPort { + override suspend fun send(e: String) : String { + try{ + val code = (1..5).map { (0..9).random() }.joinToString("") + val context = Context() + context.setVariable("code", code) + + val emailRequest = SendEmailRequest { + destination = Destination { + toAddresses = listOf(e) + } + + message = Message { + subject = Content { + data = "인증 번호" + } + + body = Body { + html = Content { + data = templateEngine.process("code", context) + } + } + } + + source = applicantSmtpProperties.sendEmailTo + } + + SesClient { + region = applicantSmtpProperties.region + + credentialsProvider = StaticCredentialsProvider { + accessKeyId = applicantSmtpProperties.accessKey + secretAccessKey = applicantSmtpProperties.secretKey + } + }.use { + it.sendEmail(emailRequest) + } + return code + }catch (e:Exception){ + throw RuntimeException() + } + } + +} \ No newline at end of file diff --git a/applicant/applicant-smtp-adapter/src/main/kotlin/com/daegusw/apply/applicant/smtp/adapter/common/ApplicantSmtpProperties.kt b/applicant/applicant-smtp-adapter/src/main/kotlin/com/daegusw/apply/applicant/smtp/adapter/common/ApplicantSmtpProperties.kt new file mode 100644 index 00000000..0350f7a3 --- /dev/null +++ b/applicant/applicant-smtp-adapter/src/main/kotlin/com/daegusw/apply/applicant/smtp/adapter/common/ApplicantSmtpProperties.kt @@ -0,0 +1,14 @@ +package com.daegusw.apply.applicant.smtp.adapter.common + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.PropertySource +import org.springframework.stereotype.Component + +@Component +@PropertySource("classpath:application.yml") +class ApplicantSmtpProperties ( + @Value("\${cloud.aws.credentials.access-key:access-key}") val accessKey : String, + @Value("\${cloud.aws.credentials.secret-key:secret-key}") val secretKey : String, + @Value("\${cloud.aws.credentials.send-mail-to:send-mail-to}") val sendEmailTo : String, + @Value("\${cloud.aws.region:region}") val region : String, +) \ No newline at end of file diff --git a/applicant/applicant-smtp-adapter/src/main/resources/application.yml b/applicant/applicant-smtp-adapter/src/main/resources/application.yml new file mode 100644 index 00000000..0af2caac --- /dev/null +++ b/applicant/applicant-smtp-adapter/src/main/resources/application.yml @@ -0,0 +1,15 @@ +cloud: + aws: + credentials: + access-key: ${aws.credentials.accessKey} + secret-key: ${aws.credentials.secretKey} + send-mail-to: ${aws.credentials.sendMail} + region: ${aws.region} +spring: + thymeleaf: + prefix: classpath:/templates + suffix: .html + mode: HTML + encoding: UTF-8 + check-template-location: true + cache: false \ No newline at end of file diff --git a/applicant/applicant-smtp-adapter/src/main/resources/templates/stmp.html b/applicant/applicant-smtp-adapter/src/main/resources/templates/stmp.html new file mode 100644 index 00000000..6f201903 --- /dev/null +++ b/applicant/applicant-smtp-adapter/src/main/resources/templates/stmp.html @@ -0,0 +1,27 @@ + + + + + + + +
+

+ IDA 인증번호 안내입니다. +

+

+ 아래 인증번호3분내로 입력해주세요. +

+ +
+

+
+ +
+

+ 아무에게도 이 번호를 공유하지마세요. 회원 정보가 유출되는 문제가 발생 할 수 있습니다.
+

+
+
+ + \ No newline at end of file diff --git a/applicant/applicant-web-adapter/build.gradle.kts b/applicant/applicant-web-adapter/build.gradle.kts index c34fb399..67289bee 100644 --- a/applicant/applicant-web-adapter/build.gradle.kts +++ b/applicant/applicant-web-adapter/build.gradle.kts @@ -9,4 +9,5 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") } \ No newline at end of file diff --git a/applicant/applicant-web-adapter/src/main/kotlin/com/daegusw/apply/applicant/web/adapter/api/SmtpController.kt b/applicant/applicant-web-adapter/src/main/kotlin/com/daegusw/apply/applicant/web/adapter/api/SmtpController.kt new file mode 100644 index 00000000..366f8951 --- /dev/null +++ b/applicant/applicant-web-adapter/src/main/kotlin/com/daegusw/apply/applicant/web/adapter/api/SmtpController.kt @@ -0,0 +1,28 @@ +package com.daegusw.apply.applicant.web.adapter.api + +import com.daegusw.apply.applicant.application.port.`in`.web.SmtpUseCase +import com.daegusw.apply.applicant.web.adapter.api.request.SmtpRequest +import com.daegusw.apply.applicant.web.adapter.api.response.SmtpResponse +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* +import javax.validation.Valid +import javax.validation.constraints.NotEmpty + +@RestController +@RequestMapping("/applicant/stmp") +class SmtpController( + private val smtpUseCase: SmtpUseCase +) { + @ResponseStatus(HttpStatus.OK) + @GetMapping + suspend fun send(@NotEmpty(message = "email is required")@RequestParam email: String) : SmtpResponse { + return SmtpResponse(smtpUseCase.send(email) + "로 전송 완료") + } + + @ResponseStatus(HttpStatus.OK) + @PostMapping + suspend fun verify(@Valid @RequestBody smtpRequest: SmtpRequest) : SmtpResponse { + return SmtpResponse(smtpUseCase.verify(smtpRequest.email,smtpRequest.code)) + } + +} \ No newline at end of file diff --git a/applicant/applicant-web-adapter/src/main/kotlin/com/daegusw/apply/applicant/web/adapter/api/request/SmtpRequest.kt b/applicant/applicant-web-adapter/src/main/kotlin/com/daegusw/apply/applicant/web/adapter/api/request/SmtpRequest.kt new file mode 100644 index 00000000..a4fb1481 --- /dev/null +++ b/applicant/applicant-web-adapter/src/main/kotlin/com/daegusw/apply/applicant/web/adapter/api/request/SmtpRequest.kt @@ -0,0 +1,11 @@ +package com.daegusw.apply.applicant.web.adapter.api.request + +import javax.validation.constraints.Email +import javax.validation.constraints.NotEmpty + +data class SmtpRequest( + @field:Email(message = "email is required") + val email: String, + @field:NotEmpty(message = "code is required") + val code: String, +) diff --git a/applicant/applicant-web-adapter/src/main/kotlin/com/daegusw/apply/applicant/web/adapter/api/response/SmtpResponse.kt b/applicant/applicant-web-adapter/src/main/kotlin/com/daegusw/apply/applicant/web/adapter/api/response/SmtpResponse.kt new file mode 100644 index 00000000..c7066382 --- /dev/null +++ b/applicant/applicant-web-adapter/src/main/kotlin/com/daegusw/apply/applicant/web/adapter/api/response/SmtpResponse.kt @@ -0,0 +1,5 @@ +package com.daegusw.apply.applicant.web.adapter.api.response + +data class SmtpResponse( + val message : String, +) diff --git a/member/member-application/src/main/kotlin/com/daegusw/apply/memnber/application/service/SignupApplicantService.kt b/member/member-application/src/main/kotlin/com/daegusw/apply/memnber/application/service/SignupApplicantService.kt index 78cb84d4..556debd6 100644 --- a/member/member-application/src/main/kotlin/com/daegusw/apply/memnber/application/service/SignupApplicantService.kt +++ b/member/member-application/src/main/kotlin/com/daegusw/apply/memnber/application/service/SignupApplicantService.kt @@ -1,6 +1,7 @@ package com.daegusw.apply.memnber.application.service import com.daegusw.apply.applicant.application.port.out.persistence.CommandApplicantPort +import com.daegusw.apply.applicant.application.port.out.redis.RedisPort import com.daegusw.apply.applicant.domain.applicant.Applicant import com.daegusw.apply.core.hash.Sha512Encoder import com.daegusw.apply.core.idgen.IdGenerator @@ -20,20 +21,25 @@ class SignupApplicantService( private val queryMemberPort: QueryMemberPort, private val commandMemberPort: CommandMemberPort, private val commandApplicantPort: CommandApplicantPort, - private val sha512Encoder: Sha512Encoder + private val sha512Encoder: Sha512Encoder, + private val redisPort: RedisPort ) : SignupApplicantUseCase { override fun signupApplicant(memberCommand: MemberCommand) { if (queryMemberPort.existsByEmail(memberCommand.email)) { throw DuplicateEmailException(memberCommand.email) } - - commandMemberPort.saveMember( - Member( - id = MemberId(IdGenerator.generateUUIDWithLong()), - email = memberCommand.email, - password = Password(sha512Encoder.encode(memberCommand.password)), - role = Role.ROLE_APPLICANT - ) - ).also { commandApplicantPort.save(Applicant(it.id)) } + if (redisPort.get(memberCommand.email) == "verified"){ + commandMemberPort.saveMember( + Member( + id = MemberId(IdGenerator.generateUUIDWithLong()), + email = memberCommand.email, + password = Password(sha512Encoder.encode(memberCommand.password)), + role = Role.ROLE_APPLICANT + ) + ).also { commandApplicantPort.save(Applicant(it.id)) } + } + else{ + throw RuntimeException("Unauthorized Email") + } } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index ab4970cb..28344891 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,7 +22,9 @@ include("applicant:applicant-domain") include("applicant:applicant-application") include("applicant:applicant-jpa-adapter") include("applicant:applicant-s3-adapter") +include("applicant:applicant-smtp-adapter") include("applicant:applicant-web-adapter") +include("applicant:applicant-redis-adapter") include("admission") include("admission:admission-domain")