mirror of https://github.com/usbharu/Hideout.git
Merge pull request #299 from usbharu/feature/register-account
アカウント作成の改善
This commit is contained in:
commit
b225e48cb8
|
@ -12,6 +12,7 @@ hideout:
|
|||
debug:
|
||||
trace-query-exception: true
|
||||
trace-query-call: true
|
||||
private: false
|
||||
|
||||
spring:
|
||||
flyway:
|
||||
|
|
|
@ -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() } }
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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?
|
||||
)
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package dev.usbharu.hideout.core.interfaces.api.auth
|
||||
|
||||
data class SignUpForm(
|
||||
val username: String,
|
||||
val password: String,
|
||||
val recaptchaResponse: String
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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()
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
Loading…
Reference in New Issue