From 269289966c319b984d773bc88f1d5cb948c2678c Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 15 Aug 2023 13:30:35 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20User=E3=81=AB=E3=83=89=E3=83=A1?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E7=9F=A5=E8=AD=98=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/dev/usbharu/hideout/Application.kt | 16 ++- .../dev/usbharu/hideout/config/Config.kt | 44 ++++++- .../domain/model/hideout/entity/User.kt | 112 +++++++++++++++++- .../hideout/query/FollowerQueryServiceImpl.kt | 8 +- .../hideout/repository/UserRepositoryImpl.kt | 2 +- .../hideout/service/user/UserServiceImpl.kt | 4 +- src/main/resources/application.conf | 28 +++-- .../hideout/plugins/ActivityPubKtTest.kt | 26 ++-- .../usbharu/hideout/plugins/KtorKeyMapTest.kt | 8 +- .../usbharu/hideout/plugins/SecurityKtTest.kt | 2 +- .../routing/activitypub/UsersAPTest.kt | 2 +- .../routing/api/internal/v1/UsersTest.kt | 2 +- .../service/ap/APNoteServiceImplTest.kt | 15 +-- .../ap/APReceiveFollowServiceImplTest.kt | 4 +- .../service/auth/JwtServiceImplTest.kt | 4 +- .../hideout/service/user/UserServiceTest.kt | 2 +- 16 files changed, 230 insertions(+), 49 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt index 0acd75e5..6a745122 100644 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ b/src/main/kotlin/dev/usbharu/hideout/Application.kt @@ -5,6 +5,7 @@ import com.auth0.jwk.JwkProviderBuilder import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.usbharu.hideout.config.CharacterLimit import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.ConfigData import dev.usbharu.hideout.domain.model.job.DeliverPostJob @@ -45,6 +46,11 @@ val Application.property: Application.(propertyName: String) -> String environment.config.property(it).getString() } +val Application.propertyOrNull: Application.(propertyName: String) -> String? + get() = { + environment.config.propertyOrNull(it)?.getString() + } + // application.conf references the main function. This annotation prevents the IDE from marking it as unused. @Suppress("unused", "LongMethod") fun Application.parent() { @@ -52,7 +58,15 @@ fun Application.parent() { url = property("hideout.url"), objectMapper = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false), + characterLimit = CharacterLimit( + general = CharacterLimit.General.of( + url = propertyOrNull("hideout.character-limit.general.url")?.toIntOrNull(), + domain = propertyOrNull("hideout.character-limit.general.domain")?.toIntOrNull(), + publicKey = propertyOrNull("hideout.character-limit.general.publicKey")?.toIntOrNull(), + privateKey = propertyOrNull("hideout.character-limit.general.privateKey")?.toIntOrNull() + ) + ) ) val module = org.koin.dsl.module { diff --git a/src/main/kotlin/dev/usbharu/hideout/config/Config.kt b/src/main/kotlin/dev/usbharu/hideout/config/Config.kt index 02358c41..62ca7112 100644 --- a/src/main/kotlin/dev/usbharu/hideout/config/Config.kt +++ b/src/main/kotlin/dev/usbharu/hideout/config/Config.kt @@ -10,5 +10,47 @@ object Config { data class ConfigData( val url: String = "", val domain: String = url.substringAfter("://").substringBeforeLast(":"), - val objectMapper: ObjectMapper = jacksonObjectMapper() + val objectMapper: ObjectMapper = jacksonObjectMapper(), + val characterLimit: CharacterLimit = CharacterLimit() ) + +data class CharacterLimit( + val general: General = General.of(), + val post: Post = Post(), + val account: Account = Account(), + val instance: Instance = Instance() +) { + data class General private constructor( + val url: Int, + val domain: Int, + val publicKey: Int, + val privateKey: Int + ) { + companion object { + fun of(url: Int? = null, domain: Int? = null, publicKey: Int? = null, privateKey: Int? = null): General { + return General( + url ?: 1000, + domain ?: 1000, + publicKey ?: 10000, + privateKey ?: 10000 + ) + } + } + } + + data class Post( + val text: Int = 3000, + val overview: Int = 3000 + ) + + data class Account( + val id: Int = 300, + val name: Int = 300, + val description: Int = 10000 + ) + + data class Instance( + val name: Int = 600, + val description: Int = 10000 + ) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/User.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/User.kt index 6754df4f..9a2c4e4a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/User.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/User.kt @@ -1,8 +1,10 @@ package dev.usbharu.hideout.domain.model.hideout.entity +import dev.usbharu.hideout.config.Config +import org.slf4j.LoggerFactory import java.time.Instant -data class User( +data class User private constructor( val id: Long, val name: String, val domain: String, @@ -21,4 +23,112 @@ data class User( " password=****, inbox='$inbox', outbox='$outbox', url='$url', publicKey='$publicKey'," + " privateKey=****, createdAt=$createdAt)" } + + companion object { + private val logger = LoggerFactory.getLogger(User::class.java) + + @Suppress("LongParameterList", "FunctionMinLength") + fun of( + id: Long, + name: String, + domain: String, + screenName: String, + description: String, + password: String? = null, + inbox: String, + outbox: String, + url: String, + publicKey: String, + privateKey: String? = null, + createdAt: Instant + ): User { + val characterLimit = Config.configData.characterLimit + + // idは0未満ではいけない + require(id >= 0) { "id must be greater than or equal to 0." } + + // nameは空文字以外を含める必要がある + require(name.isNotBlank()) { "name must contain non-blank characters." } + + // nameは指定された長さ以下である必要がある + val limitedName = if (name.length >= characterLimit.account.id) { + logger.warn("name must not exceed ${characterLimit.account.id} characters.") + name.substring(0, characterLimit.account.id) + } else { + name + } + + // domainは空文字以外を含める必要がある + require(domain.isNotBlank()) { "domain must contain non-blank characters." } + + // domainは指定された長さ以下である必要がある + require(domain.length <= characterLimit.general.domain) { + "domain must not exceed ${characterLimit.general.domain} characters." + } + + // screenNameは空文字以外を含める必要がある + require(screenName.isNotBlank()) { "screenName must contain non-blank characters." } + + // screenNameは指定された長さ以下である必要がある + val limitedScreenName = if (screenName.length >= characterLimit.account.name) { + logger.warn("screenName must not exceed ${characterLimit.account.name} characters.") + screenName.substring(0, characterLimit.account.name) + } else { + screenName + } + + // descriptionは指定された長さ以下である必要がある + val limitedDescription = if (description.length >= characterLimit.account.description) { + logger.warn("description must not exceed ${characterLimit.account.description} characters.") + description.substring(0, characterLimit.account.description) + } else { + description + } + + // ローカルユーザーはpasswordとprivateKeyをnullにしてはいけない + if (domain == Config.configData.domain) { + requireNotNull(password) { "password and privateKey must not be null for local users." } + requireNotNull(privateKey) { "password and privateKey must not be null for local users." } + } + + // urlは空文字以外を含める必要がある + require(url.isNotBlank()) { "url must contain non-blank characters." } + + // urlは指定された長さ以下である必要がある + require(url.length <= characterLimit.general.url) { + "url must not exceed ${characterLimit.general.url} characters." + } + + // inboxは空文字以外を含める必要がある + require(inbox.isNotBlank()) { "inbox must contain non-blank characters." } + + // inboxは指定された長さ以下である必要がある + require(inbox.length <= characterLimit.general.url) { + "inbox must not exceed ${characterLimit.general.url} characters." + } + + // outboxは空文字以外を含める必要がある + require(outbox.isNotBlank()) { "outbox must contain non-blank characters." } + + // outboxは指定された長さ以下である必要がある + require(outbox.length <= characterLimit.general.url) { + "outbox must not exceed ${characterLimit.general.url} characters." + } + + return User( + id = id, + name = limitedName, + domain = domain, + screenName = limitedScreenName, + description = limitedDescription, + password = password, + inbox = inbox, + outbox = outbox, + url = url, + publicKey = publicKey, + privateKey = privateKey, + createdAt = createdAt + ) + } + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/query/FollowerQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/query/FollowerQueryServiceImpl.kt index c9f0bdc8..3a938620 100644 --- a/src/main/kotlin/dev/usbharu/hideout/query/FollowerQueryServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/query/FollowerQueryServiceImpl.kt @@ -38,7 +38,7 @@ class FollowerQueryServiceImpl : FollowerQueryService { ) .select { Users.id eq id } .map { - User( + User.of( id = it[followers[Users.id]], name = it[followers[Users.name]], domain = it[followers[Users.domain]], @@ -83,7 +83,7 @@ class FollowerQueryServiceImpl : FollowerQueryService { ) .select { Users.name eq name and (Users.domain eq domain) } .map { - User( + User.of( id = it[followers[Users.id]], name = it[followers[Users.name]], domain = it[followers[Users.domain]], @@ -128,7 +128,7 @@ class FollowerQueryServiceImpl : FollowerQueryService { ) .select { followers[Users.id] eq id } .map { - User( + User.of( id = it[followers[Users.id]], name = it[followers[Users.name]], domain = it[followers[Users.domain]], @@ -173,7 +173,7 @@ class FollowerQueryServiceImpl : FollowerQueryService { ) .select { followers[Users.name] eq name and (followers[Users.domain] eq domain) } .map { - User( + User.of( id = it[followers[Users.id]], name = it[followers[Users.name]], domain = it[followers[Users.domain]], diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/UserRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/UserRepositoryImpl.kt index 43ea37ef..86f0725a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/UserRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/UserRepositoryImpl.kt @@ -102,7 +102,7 @@ object Users : Table("users") { } fun ResultRow.toUser(): User { - return User( + return User.of( id = this[Users.id], name = this[Users.name], domain = this[Users.domain], diff --git a/src/main/kotlin/dev/usbharu/hideout/service/user/UserServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/user/UserServiceImpl.kt index 6558d770..3d2a93dd 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/user/UserServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/user/UserServiceImpl.kt @@ -32,7 +32,7 @@ class UserServiceImpl( val nextId = userRepository.nextId() val hashedPassword = userAuthService.hash(user.password) val keyPair = userAuthService.generateKeyPair() - val userEntity = User( + val userEntity = User.of( id = nextId, name = user.name, domain = Config.configData.domain, @@ -51,7 +51,7 @@ class UserServiceImpl( override suspend fun createRemoteUser(user: RemoteUserCreateDto): User { val nextId = userRepository.nextId() - val userEntity = User( + val userEntity = User.of( id = nextId, name = user.name, domain = user.domain, diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index db3cc28b..70c07785 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -19,11 +19,25 @@ hideout { username = "" password = "" } -} - -jwt { - privateKey = "MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAtfJaLrzXILUg1U3N1KV8yJr92GHn5OtYZR7qWk1Mc4cy4JGjklYup7weMjBD9f3bBVoIsiUVX6xNcYIr0Ie0AQIDAQABAkEAg+FBquToDeYcAWBe1EaLVyC45HG60zwfG1S4S3IB+y4INz1FHuZppDjBh09jptQNd+kSMlG1LkAc/3znKTPJ7QIhANpyB0OfTK44lpH4ScJmCxjZV52mIrQcmnS3QzkxWQCDAiEA1Tn7qyoh+0rOO/9vJHP8U/beo51SiQMw0880a1UaiisCIQDNwY46EbhGeiLJR1cidr+JHl86rRwPDsolmeEF5AdzRQIgK3KXL3d0WSoS//K6iOkBX3KMRzaFXNnDl0U/XyeGMuUCIHaXv+n+Brz5BDnRbWS+2vkgIe9bUNlkiArpjWvX+2we" - issuer = "http://0.0.0.0:8080/" - audience = "http://0.0.0.0:8080/hello" - realm = "Access to 'hello'" + character-limit { + general { + url = 1000 + domain = 255 + publicKey = 10000 + privateKey = 10000 + } + post { + text = 3000 + overview = 3000 + } + account { + id = 300 + name = 300 + description = 10000 + } + instance { + name = 600 + description = 10000 + } + } } diff --git a/src/test/kotlin/dev/usbharu/hideout/plugins/ActivityPubKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/plugins/ActivityPubKtTest.kt index e7106465..3fb3f006 100644 --- a/src/test/kotlin/dev/usbharu/hideout/plugins/ActivityPubKtTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/plugins/ActivityPubKtTest.kt @@ -24,19 +24,19 @@ class ActivityPubKtTest { val keyPairGenerator = KeyPairGenerator.getInstance("RSA") keyPairGenerator.initialize(1024) val generateKeyPair = keyPairGenerator.generateKeyPair() - User( - 1, - "test", - "localhost", - "test", - "", - "", - "", - "", - "", - "", - generateKeyPair.private.toPem(), - Instant.now() + User.of( + id = 1, + name = "test", + domain = "localhost", + screenName = "test", + description = "", + password = "", + inbox = "https://example.com/inbox", + outbox = "https://example.com/outbox", + url = "https://example.com", + publicKey = "", + privateKey = generateKeyPair.private.toPem(), + createdAt = Instant.now() ) } } diff --git a/src/test/kotlin/dev/usbharu/hideout/plugins/KtorKeyMapTest.kt b/src/test/kotlin/dev/usbharu/hideout/plugins/KtorKeyMapTest.kt index c7326013..0c7eb45e 100644 --- a/src/test/kotlin/dev/usbharu/hideout/plugins/KtorKeyMapTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/plugins/KtorKeyMapTest.kt @@ -20,16 +20,16 @@ class KtorKeyMapTest { val keyPairGenerator = KeyPairGenerator.getInstance("RSA") keyPairGenerator.initialize(1024) val generateKeyPair = keyPairGenerator.generateKeyPair() - User( + User.of( 1, "test", "localhost", "test", "", "", - "", - "", - "", + "https://example.com/inbox", + "https://example.com/outbox", + "https://example.com", "", generateKeyPair.private.toPem(), createdAt = Instant.now() diff --git a/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt index 0d91ce6f..a47cc283 100644 --- a/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt @@ -53,7 +53,7 @@ class SecurityKtTest { } val metaService = mock() val userQueryService = mock { - onBlocking { findByNameAndDomain(eq("testUser"), eq("example.com")) } doReturn User( + onBlocking { findByNameAndDomain(eq("testUser"), eq("example.com")) } doReturn User.of( id = 1L, name = "testUser", domain = "example.com", diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt index 8fbb324f..81b780c8 100644 --- a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt @@ -172,7 +172,7 @@ class UsersAPTest { config = ApplicationConfig("empty.conf") } val userService = mock { - onBlocking { findByNameAndDomain(eq("test"), anyString()) } doReturn User( + onBlocking { findByNameAndDomain(eq("test"), anyString()) } doReturn User.of( 1L, "test", "example.com", diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt index 82a72db4..9bc0db7d 100644 --- a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt @@ -80,7 +80,7 @@ class UsersTest { val userCreateDto = UserCreate("test", "XXXXXXX") val userService = mock { onBlocking { usernameAlreadyUse(any()) } doReturn false - onBlocking { createLocalUser(any()) } doReturn User( + onBlocking { createLocalUser(any()) } doReturn User.of( id = 12345, name = "test", domain = "example.com", diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt index df22d1e1..fd65cbba 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt @@ -29,7 +29,7 @@ class APNoteServiceImplTest { @Test fun `createPost 新しい投稿`() = runTest { val followers = listOf( - User( + User.of( 2L, "follower", "follower.example.com", @@ -38,11 +38,11 @@ class APNoteServiceImplTest { "https://follower.example.com/inbox", "https://follower.example.com/outbox", "https://follower.example.com", - "", + "https://follower.example.com", publicKey = "", createdAt = Instant.now() ), - User( + User.of( 3L, "follower2", "follower2.example.com", @@ -51,23 +51,24 @@ class APNoteServiceImplTest { "https://follower2.example.com/inbox", "https://follower2.example.com/outbox", "https://follower2.example.com", - "", + "https://follower2.example.com", publicKey = "", createdAt = Instant.now() ) ) val userQueryService = mock { - onBlocking { findById(eq(1L)) } doReturn User( + onBlocking { findById(eq(1L)) } doReturn User.of( 1L, "test", "example.com", "testUser", "test user", + "a", "https://example.com/inbox", "https://example.com/outbox", - "https:.//example.com", - "", + "https://example.com", publicKey = "", + privateKey = "a", createdAt = Instant.now() ) } diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowServiceImplTest.kt index 7b7f2c21..98222b11 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowServiceImplTest.kt @@ -101,7 +101,7 @@ class APReceiveFollowServiceImplTest { } val userQueryService = mock { onBlocking { findByUrl(eq("https://example.com")) } doReturn - User( + User.of( id = 1L, name = "test", domain = "example.com", @@ -114,7 +114,7 @@ class APReceiveFollowServiceImplTest { createdAt = Instant.now() ) onBlocking { findByUrl(eq("https://follower.example.com")) } doReturn - User( + User.of( id = 2L, name = "follower", domain = "follower.example.com", diff --git a/src/test/kotlin/dev/usbharu/hideout/service/auth/JwtServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/auth/JwtServiceImplTest.kt index 44fc2844..3df97e3d 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/auth/JwtServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/auth/JwtServiceImplTest.kt @@ -54,7 +54,7 @@ class JwtServiceImplTest { } val jwtService = JwtServiceImpl(metaService, refreshTokenRepository, mock(), mock()) val token = jwtService.createToken( - User( + User.of( id = 1L, name = "test", domain = "example.com", @@ -108,7 +108,7 @@ class JwtServiceImplTest { ) } val userService = mock { - onBlocking { findById(1L) } doReturn User( + onBlocking { findById(1L) } doReturn User.of( id = 1L, name = "test", domain = "example.com", diff --git a/src/test/kotlin/dev/usbharu/hideout/service/user/UserServiceTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/user/UserServiceTest.kt index e32ca6f1..deb5b5e9 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/user/UserServiceTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/user/UserServiceTest.kt @@ -49,7 +49,7 @@ class UserServiceTest { @Test fun `createRemoteUser リモートユーザーを作成できる`() = runTest { - Config.configData = ConfigData(domain = "example.com", url = "https://example.com") + Config.configData = ConfigData(domain = "remote.example.com", url = "https://remote.example.com") val userRepository = mock { onBlocking { nextId() } doReturn 113345L