feat: ローカルユーザーを作成できるように

This commit is contained in:
usbharu 2024-06-06 23:26:20 +09:00
parent 9c271b8cc8
commit cf48ae651b
25 changed files with 674 additions and 240 deletions

View File

@ -28,6 +28,7 @@ import dev.usbharu.hideout.core.domain.service.userdetail.UserDetailDomainServic
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import dev.usbharu.hideout.core.infrastructure.factory.ActorFactoryImpl
import org.springframework.stereotype.Service
import java.net.URI
@Service
class RegisterLocalActorApplicationService(
@ -41,8 +42,8 @@ class RegisterLocalActorApplicationService(
private val userDetailRepository: UserDetailRepository,
private val idGenerateService: IdGenerateService,
) {
suspend fun register(registerLocalActor: RegisterLocalActor) {
transaction.transaction {
suspend fun register(registerLocalActor: RegisterLocalActor): URI {
return transaction.transaction {
if (actorDomainService.usernameAlreadyUse(registerLocalActor.name)) {
// todo 適切な例外を考える
throw Exception("Username already exists")
@ -61,6 +62,7 @@ class RegisterLocalActorApplicationService(
password = userDetailDomainService.hashPassword(registerLocalActor.password),
)
userDetailRepository.save(userDetail)
actor.url
}
}
}

View File

@ -0,0 +1,26 @@
/*
* 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.application.application
import java.net.URI
data class RegisterApplication(
val name: String,
val redirectUris: Set<URI>,
val useRefreshToken: Boolean,
val scopes: Set<String>,
)

View File

@ -0,0 +1,93 @@
/*
* 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.application.application
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.application.Application
import dev.usbharu.hideout.core.domain.model.application.ApplicationId
import dev.usbharu.hideout.core.domain.model.application.ApplicationName
import dev.usbharu.hideout.core.domain.model.application.ApplicationRepository
import dev.usbharu.hideout.core.domain.service.userdetail.PasswordEncoder
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.SecureTokenGenerator
import org.springframework.security.oauth2.core.AuthorizationGrantType
import org.springframework.security.oauth2.core.ClientAuthenticationMethod
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings
import org.springframework.stereotype.Service
import java.time.Duration
@Service
class RegisterApplicationApplicationService(
private val idGenerateService: IdGenerateService,
private val passwordEncoder: PasswordEncoder,
private val secureTokenGenerator: SecureTokenGenerator,
private val registeredClientRepository: RegisteredClientRepository,
private val transaction: Transaction,
private val applicationRepository: ApplicationRepository,
) {
suspend fun register(registerApplication: RegisterApplication): RegisteredApplication {
return transaction.transaction {
val id = idGenerateService.generateId()
val clientSecret = secureTokenGenerator.generate()
val registeredClient = RegisteredClient
.withId(id.toString())
.clientId(id.toString())
.clientSecret(passwordEncoder.encode(clientSecret))
.clientName(registerApplication.name)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.apply {
if (registerApplication.useRefreshToken) {
authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
tokenSettings(
TokenSettings
.builder()
.accessTokenTimeToLive(Duration.ofSeconds(31536000000))
.build()
)
}
}
.redirectUris { set ->
set.addAll(registerApplication.redirectUris.map { it.toString() })
}
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.scopes { it.addAll(registerApplication.scopes) }
.build()
registeredClientRepository.save(registeredClient)
val application = Application(ApplicationId(id), ApplicationName(registerApplication.name))
applicationRepository.save(application)
RegisteredApplication(
id = id,
name = registerApplication.name,
clientSecret = clientSecret,
clientId = id,
redirectUris = registerApplication.redirectUris
)
}
}
}

View File

@ -0,0 +1,27 @@
/*
* 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.application.application
import java.net.URI
data class RegisteredApplication(
val id: Long,
val name: String,
val redirectUris: Set<URI>,
val clientSecret: String,
val clientId: Long,
)

View File

@ -0,0 +1,59 @@
/*
* 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.application.instance
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.instance.*
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.boot.info.BuildProperties
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Service
import java.time.Instant
@Service
class InitLocalInstanceApplicationService(
private val applicationConfig: ApplicationConfig,
private val instanceRepository: InstanceRepository,
private val idGenerateService: IdGenerateService,
private val buildProperties: BuildProperties,
private val transaction: Transaction,
) {
@EventListener(ApplicationReadyEvent::class)
suspend fun init() = transaction.transaction {
val findByUrl = instanceRepository.findByUrl(applicationConfig.url.toURI())
if (findByUrl == null) {
val instance = Instance(
InstanceId(idGenerateService.generateId()),
InstanceName(applicationConfig.url.host),
InstanceDescription(""),
applicationConfig.url.toURI(),
applicationConfig.url.toURI(),
null,
InstanceSoftware("hideout"),
InstanceVersion(buildProperties.version),
false,
false,
InstanceModerationNote(""),
Instant.now(),
)
instanceRepository.save(instance)
}
}
}

View File

@ -18,13 +18,124 @@ package dev.usbharu.hideout.core.config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.Order
import org.springframework.http.HttpMethod.GET
import org.springframework.http.HttpMethod.POST
import org.springframework.jdbc.core.JdbcOperations
import org.springframework.security.access.hierarchicalroles.RoleHierarchy
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl
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.invoke
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
@Configuration
@EnableWebSecurity(debug = false)
class SecurityConfig {
@Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
@Bean
@Order(1)
fun oauth2Provider(http: HttpSecurity): SecurityFilterChain {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
http {
exceptionHandling {
authenticationEntryPoint = LoginUrlAuthenticationEntryPoint("/login")
}
}
return http.build()
}
@Bean
@Order(3)
fun httpSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/error", permitAll)
authorize("/login", permitAll)
authorize(GET, "/.well-known/**", permitAll)
authorize(GET, "/nodeinfo/2.0", permitAll)
authorize(GET, "/auth/sign_up", hasRole("ANONYMOUS"))
authorize(POST, "/auth/sign_up", permitAll)
authorize(anyRequest, authenticated)
}
formLogin {
}
}
return http.build()
}
@Bean
fun registeredClientRepository(jdbcOperations: JdbcOperations): RegisteredClientRepository {
return JdbcRegisteredClientRepository(jdbcOperations)
}
@Bean
fun roleHierarchy(): RoleHierarchy {
val roleHierarchyImpl = RoleHierarchyImpl.fromHierarchy(
"""
SCOPE_read > SCOPE_read:accounts
SCOPE_read > SCOPE_read:accounts
SCOPE_read > SCOPE_read:blocks
SCOPE_read > SCOPE_read:bookmarks
SCOPE_read > SCOPE_read:favourites
SCOPE_read > SCOPE_read:filters
SCOPE_read > SCOPE_read:follows
SCOPE_read > SCOPE_read:lists
SCOPE_read > SCOPE_read:mutes
SCOPE_read > SCOPE_read:notifications
SCOPE_read > SCOPE_read:search
SCOPE_read > SCOPE_read:statuses
SCOPE_write > SCOPE_write:accounts
SCOPE_write > SCOPE_write:blocks
SCOPE_write > SCOPE_write:bookmarks
SCOPE_write > SCOPE_write:conversations
SCOPE_write > SCOPE_write:favourites
SCOPE_write > SCOPE_write:filters
SCOPE_write > SCOPE_write:follows
SCOPE_write > SCOPE_write:lists
SCOPE_write > SCOPE_write:media
SCOPE_write > SCOPE_write:mutes
SCOPE_write > SCOPE_write:notifications
SCOPE_write > SCOPE_write:reports
SCOPE_write > SCOPE_write:statuses
SCOPE_follow > SCOPE_write:blocks
SCOPE_follow > SCOPE_write:follows
SCOPE_follow > SCOPE_write:mutes
SCOPE_follow > SCOPE_read:blocks
SCOPE_follow > SCOPE_read:follows
SCOPE_follow > SCOPE_read:mutes
SCOPE_admin > SCOPE_admin:read
SCOPE_admin > SCOPE_admin:write
SCOPE_admin:read > SCOPE_admin:read:accounts
SCOPE_admin:read > SCOPE_admin:read:reports
SCOPE_admin:read > SCOPE_admin:read:domain_allows
SCOPE_admin:read > SCOPE_admin:read:domain_blocks
SCOPE_admin:read > SCOPE_admin:read:ip_blocks
SCOPE_admin:read > SCOPE_admin:read:email_domain_blocks
SCOPE_admin:read > SCOPE_admin:read:canonical_email_blocks
SCOPE_admin:write > SCOPE_admin:write:accounts
SCOPE_admin:write > SCOPE_admin:write:reports
SCOPE_admin:write > SCOPE_admin:write:domain_allows
SCOPE_admin:write > SCOPE_admin:write:domain_blocks
SCOPE_admin:write > SCOPE_admin:write:ip_blocks
SCOPE_admin:write > SCOPE_admin:write:email_domain_blocks
SCOPE_admin:write > SCOPE_admin:write:canonical_email_blocks
""".trimIndent()
)
return roleHierarchyImpl
}
}

View File

@ -0,0 +1,46 @@
/*
* 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.config
import dev.usbharu.hideout.generate.JsonOrFormModelMethodProcessor
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor
import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor
@Configuration
class MvcConfigurer(private val jsonOrFormModelMethodProcessor: JsonOrFormModelMethodProcessor) : WebMvcConfigurer {
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(jsonOrFormModelMethodProcessor)
}
}
@Configuration
class JsonOrFormModelMethodProcessorConfig {
@Bean
fun jsonOrFormModelMethodProcessor(converter: List<HttpMessageConverter<*>>): JsonOrFormModelMethodProcessor {
return JsonOrFormModelMethodProcessor(
ServletModelAttributeMethodProcessor(true),
RequestResponseBodyMethodProcessor(
converter
)
)
}
}

View File

@ -0,0 +1,22 @@
/*
* 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.domain.model.application
class Application(
val applicationId: ApplicationId,
val name: ApplicationName,
)

View File

@ -0,0 +1,20 @@
/*
* 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.domain.model.application
@JvmInline
value class ApplicationId(val id: Long)

View File

@ -0,0 +1,20 @@
/*
* 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.domain.model.application
@JvmInline
value class ApplicationName(val name: String)

View File

@ -0,0 +1,22 @@
/*
* 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.domain.model.application
interface ApplicationRepository {
suspend fun save(application: Application): Application
suspend fun delete(application: Application)
}

View File

@ -29,7 +29,7 @@ class LocalActorDomainServiceImpl(
private val applicationConfig: ApplicationConfig,
) : LocalActorDomainService {
override suspend fun usernameAlreadyUse(name: String): Boolean =
actorRepository.findByNameAndDomain(name, applicationConfig.url.host) == null
actorRepository.findByNameAndDomain(name, applicationConfig.url.host) != null
override suspend fun generateKeyPair(): Pair<ActorPublicKey, ActorPrivateKey> {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")

View File

@ -135,10 +135,10 @@ object Actors : Table("actors") {
}
}
object ActorsAlsoKnownAs : Table("actor_alsoknwonas") {
object ActorsAlsoKnownAs : Table("actor_alsoknownas") {
val actorId =
long("actor_id").references(Actors.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
val alsoKnownAs = long("alsoKnownAs").references(Actors.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE)
val alsoKnownAs = long("also_known_as").references(Actors.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE)
override val primaryKey: PrimaryKey = PrimaryKey(actorId, alsoKnownAs)
}

View File

@ -0,0 +1,57 @@
/*
* 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.infrastructure.exposedrepository
import dev.usbharu.hideout.core.domain.model.application.Application
import dev.usbharu.hideout.core.domain.model.application.ApplicationRepository
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.upsert
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Repository
@Repository
class ExposedApplicationRepository : ApplicationRepository, AbstractRepository() {
override suspend fun save(application: Application) = query {
Applications.upsert {
it[id] = application.applicationId.id
it[name] = application.name.name
}
application
}
override suspend fun delete(application: Application): Unit = query {
Applications.deleteWhere { id eq application.applicationId.id }
}
override val logger: Logger
get() = Companion.logger
companion object {
private val logger = LoggerFactory.getLogger(ExposedApplicationRepository::class.java)
}
}
object Applications : Table("applications") {
val id = long("id")
val name = varchar("name", 500)
override val primaryKey: PrimaryKey = PrimaryKey(id)
}

View File

@ -45,7 +45,7 @@ class ActorFactoryImpl(
description = ActorDescription(""),
inbox = URI.create("$userUrl/inbox"),
outbox = URI.create("$userUrl/outbox"),
url = applicationConfig.url.toURI(),
url = URI.create(userUrl),
publicKey = keyPair.first,
privateKey = keyPair.second,
createdAt = Instant.now(),

View File

@ -0,0 +1,39 @@
/*
* 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.infrastructure.springframework.oauth2
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
class HideoutUserDetails(
private val authorities: MutableList<out GrantedAuthority>,
private val password: String,
private val username: String,
val userDetailsId: Long,
) : UserDetails {
override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
return authorities
}
override fun getPassword(): String {
return password
}
override fun getUsername(): String {
return username
}
}

View File

@ -0,0 +1,54 @@
/*
* 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.infrastructure.springframework.oauth2
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository
import kotlinx.coroutines.runBlocking
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
class UserDetailsServiceImpl(
private val actorRepository: ActorRepository,
private val userDetailRepository: UserDetailRepository,
private val applicationConfig: ApplicationConfig,
private val transaction: Transaction,
) : UserDetailsService {
override fun loadUserByUsername(username: String?): UserDetails = runBlocking {
if (username == null) {
throw UsernameNotFoundException("Username not found")
}
transaction.transaction {
val actor = actorRepository.findByNameAndDomain(username, applicationConfig.url.host)
?: throw UsernameNotFoundException("$username not found")
val userDetail = userDetailRepository.findByActorId(actor.id.id)
?: throw UsernameNotFoundException("${actor.id.id} not found")
HideoutUserDetails(
authorities = mutableListOf(),
password = userDetail.password.password,
actor.name.name,
userDetailsId = userDetail.id.id
)
}
}
}

View File

@ -16,16 +16,27 @@
package dev.usbharu.hideout.core.interfaces.api.auth
import org.springframework.ui.Model
import dev.usbharu.hideout.core.application.actor.RegisterLocalActor
import dev.usbharu.hideout.core.application.actor.RegisterLocalActorApplicationService
import jakarta.servlet.http.HttpServletRequest
import org.springframework.stereotype.Controller
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
interface AuthController {
@Controller
class AuthController(private val registerLocalActorApplicationService: RegisterLocalActorApplicationService) {
@GetMapping("/auth/sign_up")
fun signUp(model: Model): String
fun signUp(): String {
return "sign_up"
}
@PostMapping("/auth/sign_up")
suspend fun signUp(@Validated @ModelAttribute signUpForm: SignUpForm): String
suspend fun signUp(@Validated @ModelAttribute signUpForm: SignUpForm, request: HttpServletRequest): String {
val registerLocalActor = RegisterLocalActor(signUpForm.username, signUpForm.password)
val uri = registerLocalActorApplicationService.register(registerLocalActor)
request.login(signUpForm.username, signUpForm.password)
return "redirect:$uri"
}
}

View File

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

View File

@ -1,18 +0,0 @@
create table if not exists filters
(
id bigint primary key not null,
user_id bigint not null,
name varchar(255) not null,
context varchar(500) not null,
action varchar(255) not null,
constraint fk_filters_user_id__id foreign key (user_id) references actors (id) on delete cascade on update cascade
);
create table if not exists filter_keywords
(
id bigint primary key not null,
filter_id bigint not null,
keyword varchar(1000) not null,
mode varchar(100) not null,
constraint fk_filter_keywords_filter_id__id foreign key (filter_id) references filters (id) on delete cascade on update cascade
);

View File

@ -55,17 +55,27 @@ create table if not exists actors
suspend boolean not null,
move_to bigint null default null,
emojis varchar(3000) not null default '',
deleted boolean not null default false,
unique ("name", "domain"),
constraint fk_actors_instance__id foreign key ("instance") references instance (id) on delete restrict on update restrict,
constraint fk_actors_actors__move_to foreign key ("move_to") references actors (id) on delete restrict on update restrict
);
create table if not exists actor_alsoknownas
(
actor_id bigint not null,
also_known_as bigint not null,
constraint fk_actor_alsoknownas_actors__actor_id foreign key ("actor_id") references actors (id) on delete cascade on update cascade,
constraint fk_actor_alsoknownas_actors__also_known_as foreign key ("also_known_as") references actors (id) on delete cascade on update cascade
);
create table if not exists user_details
(
id bigserial primary key,
actor_id bigint not null unique,
password varchar(255) not null,
auto_accept_followee_follow_request boolean not null,
last_migration timestamp null default null,
constraint fk_user_details_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict
);
@ -81,14 +91,6 @@ create table if not exists media
mime_type varchar(255) not null,
description varchar(4000) null
);
create table if not exists meta_info
(
id bigint primary key,
version varchar(1000) not null,
kid varchar(1000) not null,
jwt_private_key varchar(100000) not null,
jwt_public_key varchar(100000) not null
);
create table if not exists posts
(
id bigint primary key,
@ -134,100 +136,6 @@ alter table posts_emojis
alter table posts_emojis
add constraint fk_posts_emojis_emoji_id__id foreign key (emoji_id) references emojis (id) on delete cascade on update cascade;
create table if not exists reactions
(
id bigint primary key,
unicode_emoji varchar(255) null default null,
custom_emoji_id bigint null default null,
post_id bigint not null,
actor_id bigint not null,
unique (post_id, actor_id)
);
alter table reactions
add constraint fk_reactions_post_id__id foreign key (post_id) references posts (id) on delete restrict on update restrict;
alter table reactions
add constraint fk_reactions_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict;
alter table reactions
add constraint fk_reactions_custom_emoji_id__id foreign key (custom_emoji_id) references emojis (id) on delete cascade on update cascade;
create table if not exists timelines
(
id bigint primary key,
user_id bigint not null,
timeline_id bigint not null,
post_id bigint not null,
post_actor_id bigint not null,
created_at bigint not null,
reply_id bigint null,
repost_id bigint null,
visibility int not null,
"sensitive" boolean not null,
is_local boolean not null,
is_pure_repost boolean not null,
media_ids varchar(255) not null,
emoji_ids varchar(255) not null
);
create table if not exists application_authorization
(
id varchar(255) primary key,
registered_client_id varchar(255) not null,
principal_name varchar(255) not null,
authorization_grant_type varchar(255) not null,
authorized_scopes varchar(1000) default null null,
"attributes" varchar(4000) default null null,
"state" varchar(500) default null null,
authorization_code_value varchar(4000) default null null,
authorization_code_issued_at timestamp default null null,
authorization_code_expires_at timestamp default null null,
authorization_code_metadata varchar(2000) default null null,
access_token_value varchar(4000) default null null,
access_token_issued_at timestamp default null null,
access_token_expires_at timestamp default null null,
access_token_metadata varchar(2000) default null null,
access_token_type varchar(255) default null null,
access_token_scopes varchar(1000) default null null,
refresh_token_value varchar(4000) default null null,
refresh_token_issued_at timestamp default null null,
refresh_token_expires_at timestamp default null null,
refresh_token_metadata varchar(2000) default null null,
oidc_id_token_value varchar(4000) default null null,
oidc_id_token_issued_at timestamp default null null,
oidc_id_token_expires_at timestamp default null null,
oidc_id_token_metadata varchar(2000) default null null,
oidc_id_token_claims varchar(2000) default null null,
user_code_value varchar(4000) default null null,
user_code_issued_at timestamp default null null,
user_code_expires_at timestamp default null null,
user_code_metadata varchar(2000) default null null,
device_code_value varchar(4000) default null null,
device_code_issued_at timestamp default null null,
device_code_expires_at timestamp default null null,
device_code_metadata varchar(2000) default null null
);
create table if not exists oauth2_authorization_consent
(
registered_client_id varchar(100),
principal_name varchar(200),
authorities varchar(1000) not null,
constraint pk_oauth2_authorization_consent primary key (registered_client_id, principal_name)
);
create table if not exists registered_client
(
id varchar(100) primary key,
client_id varchar(100) not null,
client_id_issued_at timestamp default current_timestamp not null,
client_secret varchar(200) default null null,
client_secret_expires_at timestamp default null null,
client_name varchar(200) not null,
client_authentication_methods varchar(1000) not null,
authorization_grant_types varchar(1000) not null,
redirect_uris varchar(1000) default null null,
post_logout_redirect_uris varchar(1000) default null null,
scopes varchar(1000) not null,
client_settings varchar(2000) not null,
token_settings varchar(2000) not null
);
create table if not exists relationships
(
@ -254,40 +162,26 @@ insert into actors (id, name, domain, screen_name, description, inbox, outbox, u
values (0, '', '', '', '', '', '', '', '', null, current_timestamp, '', null, null, 0, true, null, null, 0, null,
current_timestamp, false, null, '');
create table if not exists deleted_actors
create table if not exists applications
(
id bigint primary key,
"name" varchar(300) not null,
domain varchar(255) not null,
public_key varchar(10000) not null,
deleted_at timestamp not null,
unique ("name", domain)
id bigint primary key,
name varchar(500) not null
);
create table if not exists notifications
create table if not exists oauth2_registered_client
(
id bigint primary key,
type varchar(100) not null,
user_id bigint not null,
source_actor_id bigint null,
post_id bigint null,
text varchar(3000) null,
reaction_id bigint null,
created_at timestamp not null,
constraint fk_notifications_user_id__id foreign key (user_id) references actors (id) on delete cascade on update cascade,
constraint fk_notifications_source_actor__id foreign key (source_actor_id) references actors (id) on delete cascade on update cascade,
constraint fk_notifications_post_id__id foreign key (post_id) references posts (id) on delete cascade on update cascade,
constraint fk_notifications_reaction_id__id foreign key (reaction_id) references reactions (id) on delete cascade on update cascade
id varchar(100) NOT NULL,
client_id varchar(100) NOT NULL,
client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
client_secret varchar(200) DEFAULT NULL,
client_secret_expires_at timestamp DEFAULT NULL,
client_name varchar(200) NOT NULL,
client_authentication_methods varchar(1000) NOT NULL,
authorization_grant_types varchar(1000) NOT NULL,
redirect_uris varchar(1000) DEFAULT NULL,
post_logout_redirect_uris varchar(1000) DEFAULT NULL,
scopes varchar(1000) NOT NULL,
client_settings varchar(2000) NOT NULL,
token_settings varchar(2000) NOT NULL,
PRIMARY KEY (id)
);
create table if not exists mastodon_notifications
(
id bigint primary key,
user_id bigint not null,
type varchar(100) not null,
created_at timestamp not null,
account_id bigint not null,
status_id bigint null,
report_id bigint null,
relationship_serverance_event_id bigint null
)

View File

@ -6,9 +6,10 @@
</Console>
</Appenders>
<Loggers>
<Root level="INFO">
<Root level="DEBUG">
<AppenderRef ref="Console"/>
</Root>
<Logger name="dev.usbharu.owl.broker.service.QueuedTaskAssignerImpl" level="TRACE"/>
<Logger name="org.mongodb.driver.cluster" level="WARN"/>
</Loggers>
</Configuration>

View File

@ -3,23 +3,12 @@
<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="@{/dev/usbharu/hideout/core/service/auth/sign_up}"
th:disabled="${applicationConfig.private}">
<form method='post' th:action="@{/auth/sign_up}">
<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

@ -21,8 +21,6 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.Order
import org.springframework.http.HttpMethod.*
import org.springframework.security.access.hierarchicalroles.RoleHierarchy
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain
@ -30,7 +28,7 @@ import org.springframework.security.web.SecurityFilterChain
@Configuration
class MastodonSecurityConfig {
@Bean
@Order(4)
@Order(2)
@Suppress("LongMethod")
fun mastodonApiSecurityFilterChain(
http: HttpSecurity,
@ -111,63 +109,4 @@ class MastodonSecurityConfig {
return http.build()
}
@Bean
fun roleHierarchy(): RoleHierarchy {
val roleHierarchyImpl = RoleHierarchyImpl()
roleHierarchyImpl.setHierarchy(
"""
SCOPE_read > SCOPE_read:accounts
SCOPE_read > SCOPE_read:accounts
SCOPE_read > SCOPE_read:blocks
SCOPE_read > SCOPE_read:bookmarks
SCOPE_read > SCOPE_read:favourites
SCOPE_read > SCOPE_read:filters
SCOPE_read > SCOPE_read:follows
SCOPE_read > SCOPE_read:lists
SCOPE_read > SCOPE_read:mutes
SCOPE_read > SCOPE_read:notifications
SCOPE_read > SCOPE_read:search
SCOPE_read > SCOPE_read:statuses
SCOPE_write > SCOPE_write:accounts
SCOPE_write > SCOPE_write:blocks
SCOPE_write > SCOPE_write:bookmarks
SCOPE_write > SCOPE_write:conversations
SCOPE_write > SCOPE_write:favourites
SCOPE_write > SCOPE_write:filters
SCOPE_write > SCOPE_write:follows
SCOPE_write > SCOPE_write:lists
SCOPE_write > SCOPE_write:media
SCOPE_write > SCOPE_write:mutes
SCOPE_write > SCOPE_write:notifications
SCOPE_write > SCOPE_write:reports
SCOPE_write > SCOPE_write:statuses
SCOPE_follow > SCOPE_write:blocks
SCOPE_follow > SCOPE_write:follows
SCOPE_follow > SCOPE_write:mutes
SCOPE_follow > SCOPE_read:blocks
SCOPE_follow > SCOPE_read:follows
SCOPE_follow > SCOPE_read:mutes
SCOPE_admin > SCOPE_admin:read
SCOPE_admin > SCOPE_admin:write
SCOPE_admin:read > SCOPE_admin:read:accounts
SCOPE_admin:read > SCOPE_admin:read:reports
SCOPE_admin:read > SCOPE_admin:read:domain_allows
SCOPE_admin:read > SCOPE_admin:read:domain_blocks
SCOPE_admin:read > SCOPE_admin:read:ip_blocks
SCOPE_admin:read > SCOPE_admin:read:email_domain_blocks
SCOPE_admin:read > SCOPE_admin:read:canonical_email_blocks
SCOPE_admin:write > SCOPE_admin:write:accounts
SCOPE_admin:write > SCOPE_admin:write:reports
SCOPE_admin:write > SCOPE_admin:write:domain_allows
SCOPE_admin:write > SCOPE_admin:write:domain_blocks
SCOPE_admin:write > SCOPE_admin:write:ip_blocks
SCOPE_admin:write > SCOPE_admin:write:email_domain_blocks
SCOPE_admin:write > SCOPE_admin:write:canonical_email_blocks
""".trimIndent()
)
return roleHierarchyImpl
}
}

View File

@ -16,15 +16,35 @@
package dev.usbharu.hideout.mastodon.interfaces.api
import dev.usbharu.hideout.core.application.application.RegisterApplication
import dev.usbharu.hideout.core.application.application.RegisterApplicationApplicationService
import dev.usbharu.hideout.mastodon.interfaces.api.generated.AppApi
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Application
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.AppsRequest
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import java.net.URI
@Controller
class SpringAppApi : AppApi {
class SpringAppApi(private val registerApplicationApplicationService: RegisterApplicationApplicationService) : AppApi {
override suspend fun apiV1AppsPost(appsRequest: AppsRequest): ResponseEntity<Application> {
return super.apiV1AppsPost(appsRequest)
val registerApplication = RegisterApplication(
appsRequest.clientName,
setOf(URI.create(appsRequest.redirectUris)),
false,
appsRequest.scopes?.split(" ").orEmpty().toSet()
)
val registeredApplication = registerApplicationApplicationService.register(registerApplication)
return ResponseEntity.ok(
Application(
registeredApplication.name,
"invalid-vapid-key",
null,
registeredApplication.clientId.toString(),
registeredApplication.clientSecret,
appsRequest.redirectUris
)
)
}
}