Merge pull request #299 from usbharu/feature/register-account

アカウント作成の改善
This commit is contained in:
usbharu 2024-05-16 16:00:51 +09:00 committed by GitHub
commit b225e48cb8
18 changed files with 251 additions and 14 deletions

View File

@ -12,6 +12,7 @@ hideout:
debug:
trace-query-exception: true
trace-query-call: true
private: false
spring:
flyway:

View File

@ -113,6 +113,7 @@ class AccountApiTest {
param("email", "test@example.com")
param("agreement", "true")
param("locale", "")
with(jwt())
with(csrf())
}
.asyncDispatch()
@ -129,6 +130,7 @@ class AccountApiTest {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-2")
param("password", "very-secure-password")
with(jwt())
with(csrf())
}
.asyncDispatch()
@ -145,6 +147,7 @@ class AccountApiTest {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("password", "api-test-user-3")
with(csrf())
with(jwt())
}
.andDo { print() }
.andExpect { status { isUnprocessableEntity() } }
@ -158,6 +161,7 @@ class AccountApiTest {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-4")
with(csrf())
with(jwt())
}
.andExpect { status { isUnprocessableEntity() } }
}

View File

@ -12,6 +12,7 @@ hideout:
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
storage:
type: local
private: false
spring:
flyway:

View File

@ -43,9 +43,9 @@ class NoteQueryServiceImpl(private val postRepository: PostRepository, private v
.selectAll().where { Posts.id eq id }
.let {
(it.toNote() ?: return null) to (
postQueryMapper.map(it)
.singleOrNull() ?: return null
)
postQueryMapper.map(it)
.singleOrNull() ?: return null
)
}
}
@ -57,9 +57,9 @@ class NoteQueryServiceImpl(private val postRepository: PostRepository, private v
.selectAll().where { Posts.apId eq apId }
.let {
(it.toNote() ?: return null) to (
postQueryMapper.map(it)
.singleOrNull() ?: return null
)
postQueryMapper.map(it)
.singleOrNull() ?: return null
)
}
}

View File

@ -0,0 +1,9 @@
package dev.usbharu.hideout.application.config
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties("hideout.security")
data class CaptchaConfig(
val reCaptchaSiteKey: String?,
val reCaptchaSecretKey: String?
)

View File

@ -218,7 +218,7 @@ class SecurityConfig {
authorize(GET, "/users/*", permitAll)
authorize(GET, "/users/*/posts/*", permitAll)
authorize("/auth/sign_up", hasRole("ANONYMOUS"))
authorize("/dev/usbharu/hideout/core/service/auth/sign_up", hasRole("ANONYMOUS"))
authorize(GET, "/files/*", permitAll)
authorize(GET, "/users/*/icon.jpg", permitAll)
authorize(GET, "/users/*/header.jpg", permitAll)

View File

@ -44,7 +44,8 @@ class SpringConfig {
@ConfigurationProperties("hideout")
data class ApplicationConfig(
val url: URL
val url: URL,
val private:Boolean = true
)
@ConfigurationProperties("hideout.storage.s3")

View File

@ -16,12 +16,40 @@
package dev.usbharu.hideout.core.interfaces.api.auth
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.config.CaptchaConfig
import dev.usbharu.hideout.core.service.auth.AuthApiService
import dev.usbharu.hideout.core.service.auth.RegisterAccountDto
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.PostMapping
@Controller
class AuthController {
class AuthController(
private val authApiService: AuthApiService,
private val captchaConfig: CaptchaConfig,
private val applicationConfig: ApplicationConfig
) {
@GetMapping("/auth/sign_up")
@Suppress("FunctionOnlyReturningConstant")
fun signUp(): String = "sign_up"
fun signUp(model: Model): String {
model.addAttribute("siteKey", captchaConfig.reCaptchaSiteKey)
model.addAttribute("applicationConfig", applicationConfig)
return "sign_up"
}
@PostMapping("/auth/sign_up")
suspend fun signUp(@Validated @ModelAttribute signUpForm: SignUpForm, model: Model): String {
val registerAccount = authApiService.registerAccount(
RegisterAccountDto(
signUpForm.username,
signUpForm.password,
signUpForm.recaptchaResponse
)
)
return "redirect:" + registerAccount.url
}
}

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.core.interfaces.api.auth
data class SignUpForm(
val username: String,
val password: String,
val recaptchaResponse: String
)

View File

@ -0,0 +1,23 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.service.auth
import dev.usbharu.hideout.core.domain.model.actor.Actor
interface AuthApiService {
suspend fun registerAccount(registerAccountDto: RegisterAccountDto): Actor
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.service.auth
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.application.config.CaptchaConfig
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.service.user.UserCreateDto
import dev.usbharu.hideout.core.service.user.UserService
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class AuthApiServiceImpl(
private val httpClient: HttpClient,
private val captchaConfig: CaptchaConfig,
private val objectMapper: ObjectMapper,
private val userService: UserService
) :
AuthApiService {
override suspend fun registerAccount(registerAccountDto: RegisterAccountDto): Actor {
if (captchaConfig.reCaptchaSecretKey != null && captchaConfig.reCaptchaSiteKey != null) {
val get =
httpClient.get("https://www.google.com/recaptcha/api/siteverify?secret=" + captchaConfig.reCaptchaSecretKey + "&response=" + registerAccountDto.recaptchaResponse)
val recaptchaResult = objectMapper.readValue<RecaptchaResult>(get.bodyAsText())
logger.debug("reCAPTCHA: {}", recaptchaResult)
require(recaptchaResult.success)
require(!(recaptchaResult.score < 0.5))
}
val createLocalUser = userService.createLocalUser(
UserCreateDto(
registerAccountDto.username,
registerAccountDto.username,
"",
registerAccountDto.password
)
)
return createLocalUser
}
companion object {
private val logger = LoggerFactory.getLogger(AuthApiServiceImpl::class.java)
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.service.auth
data class RecaptchaResult(
val success: Boolean,
val challenge_ts: String,
val hostname: String,
val score: Float,
val action: String
)

View File

@ -0,0 +1,23 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.service.auth
data class RegisterAccountDto(
val username:String,
val password:String,
val recaptchaResponse:String
)

View File

@ -59,6 +59,10 @@ class UserServiceImpl(
}
override suspend fun createLocalUser(user: UserCreateDto): Actor {
if (applicationConfig.private) {
throw IllegalStateException("Instance is a private mode.")
}
val nextId = actorRepository.nextId()
val hashedPassword = userAuthService.hash(user.password)
val keyPair = userAuthService.generateKeyPair()

View File

@ -41,7 +41,7 @@ class MastodonApiSecurityConfig {
authorizeHttpRequests {
authorize(POST, "/api/v1/apps", permitAll)
authorize(GET, "/api/v1/instance/**", permitAll)
authorize(POST, "/api/v1/accounts", permitAll)
authorize(POST, "/api/v1/accounts", authenticated)
authorize(GET, "/api/v1/accounts/verify_credentials", rf.hasScope("read:accounts"))
authorize(GET, "/api/v1/accounts/relationships", rf.hasScope("read:follows"))

View File

@ -10,6 +10,7 @@ hideout:
key-id: a
private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ=="
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
private: true

View File

@ -3,12 +3,23 @@
<head>
<meta charset="UTF-8">
<title>SignUp</title>
<script th:src="https://www.google.com/recaptcha/api.js?render=${siteKey}"></script>
<script th:inline="javascript">
grecaptcha.ready(function () {
grecaptcha.execute( /*[[${siteKey}]]*/ '', {action: 'homepage'}).then(function (token) {
var recaptchaResponse = document.getElementById('recaptchaResponse');
recaptchaResponse.value = token;
});
});
</script>
</head>
<body>
<form method='post' th:action="@{/api/v1/accounts}">
<form method='post' th:action="@{/dev/usbharu/hideout/core/service/auth/sign_up}"
th:disabled="${applicationConfig.private}">
<input name='username' type='text' value=''>
<input name='password' type='password'>
<input type="hidden" name="recaptchaResponse" id="recaptchaResponse">
<input type="submit">
</form>
</body>

View File

@ -28,6 +28,7 @@ import jakarta.validation.Validation
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.*
import utils.TestApplicationConfig.testApplicationConfig
@ -60,7 +61,7 @@ class ActorServiceTest {
actorRepository = actorRepository,
userAuthService = userAuthService,
actorBuilder = actorBuilder,
applicationConfig = testApplicationConfig,
applicationConfig = testApplicationConfig.copy(private = false),
instanceService = mock(),
userDetailRepository = mock(),
deletedActorRepository = mock(),
@ -87,6 +88,39 @@ class ActorServiceTest {
}
}
@Test
fun `createLocalUser applicationconfig privateがtrueのときアカウントを作成できない`() = runTest {
val actorRepository = mock<ActorRepository> {
onBlocking { nextId() } doReturn 110001L
}
val generateKeyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair()
val userAuthService = mock<UserAuthService> {
onBlocking { hash(anyString()) } doReturn "hashedPassword"
onBlocking { generateKeyPair() } doReturn generateKeyPair
}
val userService =
UserServiceImpl(
actorRepository = actorRepository,
userAuthService = userAuthService,
actorBuilder = actorBuilder,
applicationConfig = testApplicationConfig.copy(private = true),
instanceService = mock(),
userDetailRepository = mock(),
deletedActorRepository = mock(),
reactionRepository = mock(),
relationshipRepository = mock(),
postService = mock(),
apSendDeleteService = mock(),
postRepository = mock()
)
assertThrows<IllegalStateException> {
userService.createLocalUser(UserCreateDto("test", "testUser", "XXXXXXXXXXXXX", "test"))
}
}
@Test
fun `createRemoteUser リモートユーザーを作成できる`() = runTest {