feat: ログインできるように

This commit is contained in:
usbharu 2024-06-07 17:15:47 +09:00
parent e14a4aa89a
commit 4d68b150dd
15 changed files with 286 additions and 10 deletions

View File

@ -0,0 +1,19 @@
/*
* 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.actor
data class GetUserDetail(val id: Long)

View File

@ -0,0 +1,49 @@
/*
* 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.actor
import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
import dev.usbharu.hideout.core.application.shared.CommandExecutor
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class GetUserDetailApplicationService(
private val actorRepository: ActorRepository,
private val userDetailRepository: UserDetailRepository,
private val customEmojiRepository: CustomEmojiRepository,
transaction: Transaction,
) :
AbstractApplicationService<GetUserDetail, UserDetail>(transaction, Companion.logger) {
companion object {
val logger = LoggerFactory.getLogger(GetUserDetailApplicationService::class.java)
}
override suspend fun internalExecute(command: GetUserDetail, executor: CommandExecutor): UserDetail {
val userDetail = userDetailRepository.findById(command.id)
?: throw IllegalArgumentException("actor does not exist")
val actor = actorRepository.findById(userDetail.actorId)!!
val emojis = customEmojiRepository.findByIds(actor.emojis.map { it.emojiId })
return UserDetail.of(actor, userDetail, emojis)
}
}

View File

@ -0,0 +1,70 @@
/*
* 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.actor
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail
import java.time.Instant
data class UserDetail(
val id: Long,
val userDetailId: Long,
val name: String,
val domain: String,
val screenName: String,
val url: String,
val iconUrl: String,
val description: String,
val locked: Boolean,
val emojis: List<CustomEmoji>,
val createdAt: Instant,
val lastPostAt: Instant?,
val postsCount: Int,
val followingCount: Int?,
val followersCount: Int?,
val moveTo: Long?,
val suspend: Boolean,
) {
companion object {
fun of(
actor: Actor,
userDetail: UserDetail,
customEmojis: List<CustomEmoji>,
): dev.usbharu.hideout.core.application.actor.UserDetail {
return UserDetail(
actor.id.id,
userDetail.id.id,
actor.name.name,
actor.domain.domain,
actor.screenName.screenName,
actor.url.toString(),
actor.url.toString(),
actor.description.description,
actor.locked,
customEmojis,
actor.createdAt,
actor.lastPostAt,
actor.postsCount.postsCount,
actor.followingCount?.relationshipCount,
actor.followersCount?.relationshipCount,
actor.moveTo?.id,
actor.suspend
)
}
}
}

View File

@ -21,4 +21,5 @@ interface CustomEmojiRepository {
suspend fun findById(id: Long): CustomEmoji? suspend fun findById(id: Long): CustomEmoji?
suspend fun delete(customEmoji: CustomEmoji) suspend fun delete(customEmoji: CustomEmoji)
suspend fun findByNamesAndDomain(names: List<String>, domain: String): List<CustomEmoji> suspend fun findByNamesAndDomain(names: List<String>, domain: String): List<CustomEmoji>
suspend fun findByIds(ids: List<Long>): List<CustomEmoji>
} }

View File

@ -20,4 +20,5 @@ interface UserDetailRepository {
suspend fun save(userDetail: UserDetail): UserDetail suspend fun save(userDetail: UserDetail): UserDetail
suspend fun delete(userDetail: UserDetail) suspend fun delete(userDetail: UserDetail)
suspend fun findByActorId(actorId: Long): UserDetail? suspend fun findByActorId(actorId: Long): UserDetail?
suspend fun findById(id: Long): UserDetail?
} }

View File

@ -70,8 +70,8 @@ class CustomEmojiRepositoryImpl : CustomEmojiRepository,
CustomEmojis.deleteWhere { id eq customEmoji.id.emojiId } CustomEmojis.deleteWhere { id eq customEmoji.id.emojiId }
} }
override suspend fun findByNamesAndDomain(names: List<String>, domain: String): List<CustomEmoji> { override suspend fun findByNamesAndDomain(names: List<String>, domain: String): List<CustomEmoji> = query {
return CustomEmojis return@query CustomEmojis
.selectAll() .selectAll()
.where { .where {
CustomEmojis.name inList names and (CustomEmojis.domain eq domain) CustomEmojis.name inList names and (CustomEmojis.domain eq domain)
@ -79,6 +79,15 @@ class CustomEmojiRepositoryImpl : CustomEmojiRepository,
.map { it.toCustomEmoji() } .map { it.toCustomEmoji() }
} }
override suspend fun findByIds(ids: List<Long>): List<CustomEmoji> = query {
return@query CustomEmojis
.selectAll()
.where {
CustomEmojis.id inList ids
}
.map { it.toCustomEmoji() }
}
companion object { companion object {
private val logger = LoggerFactory.getLogger(CustomEmojiRepositoryImpl::class.java) private val logger = LoggerFactory.getLogger(CustomEmojiRepositoryImpl::class.java)
} }

View File

@ -74,6 +74,21 @@ class UserDetailRepositoryImpl : UserDetailRepository, AbstractRepository() {
} }
} }
override suspend fun findById(id: Long): UserDetail? = query {
UserDetails
.selectAll().where { UserDetails.id eq id }
.singleOrNull()
?.let {
UserDetail.create(
UserDetailId(it[UserDetails.id]),
ActorId(it[UserDetails.actorId]),
UserDetailHashedPassword(it[UserDetails.password]),
it[UserDetails.autoAcceptFolloweeFollowRequest],
it[UserDetails.lastMigration]
)
}
}
companion object { companion object {
private val logger = LoggerFactory.getLogger(UserDetailRepositoryImpl::class.java) private val logger = LoggerFactory.getLogger(UserDetailRepositoryImpl::class.java)
} }

View File

@ -0,0 +1,21 @@
/*
* 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.CommandExecutor
class Oauth2CommandExecutor(override val executor: String, val userDetailId: Long) : CommandExecutor

View File

@ -0,0 +1,34 @@
/*
* 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.context.SecurityContextHolder
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.stereotype.Component
@Component
class Oauth2CommandExecutorFactory {
fun getCommandExecutor(): Oauth2CommandExecutor {
val principal = SecurityContextHolder.getContext().authentication.principal as Jwt
return Oauth2CommandExecutor(
principal.subject,
principal.getClaim<String>("uid").toLong()
)
}
}

View File

@ -43,6 +43,8 @@ class JsonOrFormModelMethodProcessor(
webRequest: NativeWebRequest, webRequest: NativeWebRequest,
binderFactory: WebDataBinderFactory?, binderFactory: WebDataBinderFactory?,
): Any? { ): Any? {
val contentType = webRequest.getHeader("Content-Type").orEmpty() val contentType = webRequest.getHeader("Content-Type").orEmpty()
logger.trace("ContentType is {}", contentType) logger.trace("ContentType is {}", contentType)
if (contentType.contains(isJsonRegex)) { if (contentType.contains(isJsonRegex)) {

View File

@ -16,14 +16,19 @@
package dev.usbharu.hideout.mastodon.interfaces.api package dev.usbharu.hideout.mastodon.interfaces.api
import dev.usbharu.hideout.core.application.actor.GetUserDetail
import dev.usbharu.hideout.core.application.actor.GetUserDetailApplicationService
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.Oauth2CommandExecutorFactory
import dev.usbharu.hideout.mastodon.interfaces.api.generated.AccountApi import dev.usbharu.hideout.mastodon.interfaces.api.generated.AccountApi
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.* import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.*
import kotlinx.coroutines.flow.Flow
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
@Controller @Controller
class SpringAccountApi : AccountApi { class SpringAccountApi(
private val oauth2CommandExecutorFactory: Oauth2CommandExecutorFactory,
private val getUserDetailApplicationService: GetUserDetailApplicationService,
) : AccountApi {
override suspend fun apiV1AccountsIdBlockPost(id: String): ResponseEntity<Relationship> { override suspend fun apiV1AccountsIdBlockPost(id: String): ResponseEntity<Relationship> {
return super.apiV1AccountsIdBlockPost(id) return super.apiV1AccountsIdBlockPost(id)
} }
@ -68,7 +73,55 @@ class SpringAccountApi : AccountApi {
} }
override suspend fun apiV1AccountsVerifyCredentialsGet(): ResponseEntity<CredentialAccount> { override suspend fun apiV1AccountsVerifyCredentialsGet(): ResponseEntity<CredentialAccount> {
return super.apiV1AccountsVerifyCredentialsGet() val commandExecutor = oauth2CommandExecutorFactory.getCommandExecutor()
val localActor =
getUserDetailApplicationService.execute(GetUserDetail(commandExecutor.userDetailId), commandExecutor)
return ResponseEntity.ok(
CredentialAccount(
id = localActor.id.toString(),
username = localActor.name,
acct = localActor.name + "@" + localActor.domain,
url = localActor.url,
displayName = localActor.screenName,
note = localActor.description,
avatar = localActor.iconUrl,
avatarStatic = localActor.iconUrl,
header = localActor.iconUrl,
headerStatic = localActor.iconUrl,
locked = localActor.locked,
fields = emptyList(),
emojis = localActor.emojis.map {
CustomEmoji(
shortcode = it.name,
url = it.url.toString(),
staticUrl = it.url.toString(),
true,
category = it.category.orEmpty()
)
},
bot = false,
group = false,
discoverable = true,
createdAt = localActor.createdAt.toString(),
lastStatusAt = localActor.lastPostAt?.toString(),
statusesCount = localActor.postsCount,
followersCount = localActor.followersCount,
followingCount = localActor.followingCount,
moved = localActor.moveTo != null,
noindex = true,
suspendex = localActor.suspend,
limited = false,
role = null,
source = AccountSource(
localActor.description,
emptyList(),
AccountSource.Privacy.PUBLIC,
false,
0
)
)
)
} }
override suspend fun apiV1FollowRequestsAccountIdAuthorizePost(accountId: String): ResponseEntity<Relationship> { override suspend fun apiV1FollowRequestsAccountIdAuthorizePost(accountId: String): ResponseEntity<Relationship> {

View File

@ -242,6 +242,9 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/AppsRequest" $ref: "#/components/schemas/AppsRequest"
application/x-www-form-urlencoded:
schema:
$ref: "#/components/schemas/AppsRequest"
responses: responses:
200: 200:
@ -1660,8 +1663,6 @@ components:
- created_at - created_at
- last_status_at - last_status_at
- statuses_count - statuses_count
- followers_count
- followers_count
- source - source
AccountSource: AccountSource:

View File

@ -5,7 +5,8 @@
{{/vars}} {{/vars}}
*/{{#discriminator}} */{{#discriminator}}
{{>typeInfoAnnotation}}{{/discriminator}} {{>typeInfoAnnotation}}{{/discriminator}}
{{#discriminator}}interface {{classname}}{{/discriminator}}{{^discriminator}}{{#hasVars}}data {{/hasVars}}class {{classname}}(
{{#discriminator}}interface {{classname}}{{/discriminator}}{{^discriminator}}{{#hasVars}}data {{/hasVars}}class {{classname}} @ConstructorProperties( {{#vars}}"{{baseName}}",{{/vars}} ) constructor(
{{#requiredVars}} {{#requiredVars}}
{{>dataClassReqVar}}{{^-last}}, {{>dataClassReqVar}}{{^-last}},
{{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}}, {{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}},

View File

@ -2,4 +2,4 @@
@Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} @Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}
@ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}{{#deprecated}} @ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}{{#deprecated}}
@Deprecated(message = ""){{/deprecated}} @Deprecated(message = ""){{/deprecated}}
@get:JsonProperty("{{{baseName}}}"){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInCamelCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}} @get:JsonProperty("{{{baseName}}}"){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{#lambda.camelcase}} {{{name}}} {{/lambda.camelcase}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInCamelCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}

View File

@ -20,7 +20,7 @@ import java.util.Objects
{{#swagger1AnnotationLibrary}} {{#swagger1AnnotationLibrary}}
import io.swagger.annotations.ApiModelProperty import io.swagger.annotations.ApiModelProperty
{{/swagger1AnnotationLibrary}} {{/swagger1AnnotationLibrary}}
import java.beans.ConstructorProperties
{{#models}} {{#models}}
{{#model}} {{#model}}
{{#isEnum}}{{>enumClass}}{{/isEnum}}{{^isEnum}}{{>dataClass}}{{/isEnum}} {{#isEnum}}{{>enumClass}}{{/isEnum}}{{^isEnum}}{{>dataClass}}{{/isEnum}}