feat: Userにドメイン知識を追加

This commit is contained in:
usbharu 2023-08-15 13:30:35 +09:00
parent 5a56c61ee4
commit 269289966c
16 changed files with 230 additions and 49 deletions

View File

@ -5,6 +5,7 @@ import com.auth0.jwk.JwkProviderBuilder
import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.usbharu.hideout.config.CharacterLimit
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.config.ConfigData import dev.usbharu.hideout.config.ConfigData
import dev.usbharu.hideout.domain.model.job.DeliverPostJob import dev.usbharu.hideout.domain.model.job.DeliverPostJob
@ -45,6 +46,11 @@ val Application.property: Application.(propertyName: String) -> String
environment.config.property(it).getString() 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. // application.conf references the main function. This annotation prevents the IDE from marking it as unused.
@Suppress("unused", "LongMethod") @Suppress("unused", "LongMethod")
fun Application.parent() { fun Application.parent() {
@ -52,7 +58,15 @@ fun Application.parent() {
url = property("hideout.url"), url = property("hideout.url"),
objectMapper = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) objectMapper = jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY) .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 { val module = org.koin.dsl.module {

View File

@ -10,5 +10,47 @@ object Config {
data class ConfigData( data class ConfigData(
val url: String = "", val url: String = "",
val domain: String = url.substringAfter("://").substringBeforeLast(":"), 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
)
}

View File

@ -1,8 +1,10 @@
package dev.usbharu.hideout.domain.model.hideout.entity package dev.usbharu.hideout.domain.model.hideout.entity
import dev.usbharu.hideout.config.Config
import org.slf4j.LoggerFactory
import java.time.Instant import java.time.Instant
data class User( data class User private constructor(
val id: Long, val id: Long,
val name: String, val name: String,
val domain: String, val domain: String,
@ -21,4 +23,112 @@ data class User(
" password=****, inbox='$inbox', outbox='$outbox', url='$url', publicKey='$publicKey'," + " password=****, inbox='$inbox', outbox='$outbox', url='$url', publicKey='$publicKey'," +
" privateKey=****, createdAt=$createdAt)" " 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
)
}
}
} }

View File

@ -38,7 +38,7 @@ class FollowerQueryServiceImpl : FollowerQueryService {
) )
.select { Users.id eq id } .select { Users.id eq id }
.map { .map {
User( User.of(
id = it[followers[Users.id]], id = it[followers[Users.id]],
name = it[followers[Users.name]], name = it[followers[Users.name]],
domain = it[followers[Users.domain]], domain = it[followers[Users.domain]],
@ -83,7 +83,7 @@ class FollowerQueryServiceImpl : FollowerQueryService {
) )
.select { Users.name eq name and (Users.domain eq domain) } .select { Users.name eq name and (Users.domain eq domain) }
.map { .map {
User( User.of(
id = it[followers[Users.id]], id = it[followers[Users.id]],
name = it[followers[Users.name]], name = it[followers[Users.name]],
domain = it[followers[Users.domain]], domain = it[followers[Users.domain]],
@ -128,7 +128,7 @@ class FollowerQueryServiceImpl : FollowerQueryService {
) )
.select { followers[Users.id] eq id } .select { followers[Users.id] eq id }
.map { .map {
User( User.of(
id = it[followers[Users.id]], id = it[followers[Users.id]],
name = it[followers[Users.name]], name = it[followers[Users.name]],
domain = it[followers[Users.domain]], domain = it[followers[Users.domain]],
@ -173,7 +173,7 @@ class FollowerQueryServiceImpl : FollowerQueryService {
) )
.select { followers[Users.name] eq name and (followers[Users.domain] eq domain) } .select { followers[Users.name] eq name and (followers[Users.domain] eq domain) }
.map { .map {
User( User.of(
id = it[followers[Users.id]], id = it[followers[Users.id]],
name = it[followers[Users.name]], name = it[followers[Users.name]],
domain = it[followers[Users.domain]], domain = it[followers[Users.domain]],

View File

@ -102,7 +102,7 @@ object Users : Table("users") {
} }
fun ResultRow.toUser(): User { fun ResultRow.toUser(): User {
return User( return User.of(
id = this[Users.id], id = this[Users.id],
name = this[Users.name], name = this[Users.name],
domain = this[Users.domain], domain = this[Users.domain],

View File

@ -32,7 +32,7 @@ class UserServiceImpl(
val nextId = userRepository.nextId() val nextId = userRepository.nextId()
val hashedPassword = userAuthService.hash(user.password) val hashedPassword = userAuthService.hash(user.password)
val keyPair = userAuthService.generateKeyPair() val keyPair = userAuthService.generateKeyPair()
val userEntity = User( val userEntity = User.of(
id = nextId, id = nextId,
name = user.name, name = user.name,
domain = Config.configData.domain, domain = Config.configData.domain,
@ -51,7 +51,7 @@ class UserServiceImpl(
override suspend fun createRemoteUser(user: RemoteUserCreateDto): User { override suspend fun createRemoteUser(user: RemoteUserCreateDto): User {
val nextId = userRepository.nextId() val nextId = userRepository.nextId()
val userEntity = User( val userEntity = User.of(
id = nextId, id = nextId,
name = user.name, name = user.name,
domain = user.domain, domain = user.domain,

View File

@ -19,11 +19,25 @@ hideout {
username = "" username = ""
password = "" password = ""
} }
} character-limit {
general {
jwt { url = 1000
privateKey = "MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAtfJaLrzXILUg1U3N1KV8yJr92GHn5OtYZR7qWk1Mc4cy4JGjklYup7weMjBD9f3bBVoIsiUVX6xNcYIr0Ie0AQIDAQABAkEAg+FBquToDeYcAWBe1EaLVyC45HG60zwfG1S4S3IB+y4INz1FHuZppDjBh09jptQNd+kSMlG1LkAc/3znKTPJ7QIhANpyB0OfTK44lpH4ScJmCxjZV52mIrQcmnS3QzkxWQCDAiEA1Tn7qyoh+0rOO/9vJHP8U/beo51SiQMw0880a1UaiisCIQDNwY46EbhGeiLJR1cidr+JHl86rRwPDsolmeEF5AdzRQIgK3KXL3d0WSoS//K6iOkBX3KMRzaFXNnDl0U/XyeGMuUCIHaXv+n+Brz5BDnRbWS+2vkgIe9bUNlkiArpjWvX+2we" domain = 255
issuer = "http://0.0.0.0:8080/" publicKey = 10000
audience = "http://0.0.0.0:8080/hello" privateKey = 10000
realm = "Access to 'hello'" }
post {
text = 3000
overview = 3000
}
account {
id = 300
name = 300
description = 10000
}
instance {
name = 600
description = 10000
}
}
} }

View File

@ -24,19 +24,19 @@ class ActivityPubKtTest {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA") val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(1024) keyPairGenerator.initialize(1024)
val generateKeyPair = keyPairGenerator.generateKeyPair() val generateKeyPair = keyPairGenerator.generateKeyPair()
User( User.of(
1, id = 1,
"test", name = "test",
"localhost", domain = "localhost",
"test", screenName = "test",
"", description = "",
"", password = "",
"", inbox = "https://example.com/inbox",
"", outbox = "https://example.com/outbox",
"", url = "https://example.com",
"", publicKey = "",
generateKeyPair.private.toPem(), privateKey = generateKeyPair.private.toPem(),
Instant.now() createdAt = Instant.now()
) )
} }
} }

View File

@ -20,16 +20,16 @@ class KtorKeyMapTest {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA") val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(1024) keyPairGenerator.initialize(1024)
val generateKeyPair = keyPairGenerator.generateKeyPair() val generateKeyPair = keyPairGenerator.generateKeyPair()
User( User.of(
1, 1,
"test", "test",
"localhost", "localhost",
"test", "test",
"", "",
"", "",
"", "https://example.com/inbox",
"", "https://example.com/outbox",
"", "https://example.com",
"", "",
generateKeyPair.private.toPem(), generateKeyPair.private.toPem(),
createdAt = Instant.now() createdAt = Instant.now()

View File

@ -53,7 +53,7 @@ class SecurityKtTest {
} }
val metaService = mock<MetaService>() val metaService = mock<MetaService>()
val userQueryService = mock<UserQueryService> { val userQueryService = mock<UserQueryService> {
onBlocking { findByNameAndDomain(eq("testUser"), eq("example.com")) } doReturn User( onBlocking { findByNameAndDomain(eq("testUser"), eq("example.com")) } doReturn User.of(
id = 1L, id = 1L,
name = "testUser", name = "testUser",
domain = "example.com", domain = "example.com",

View File

@ -172,7 +172,7 @@ class UsersAPTest {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val userService = mock<UserQueryService> { val userService = mock<UserQueryService> {
onBlocking { findByNameAndDomain(eq("test"), anyString()) } doReturn User( onBlocking { findByNameAndDomain(eq("test"), anyString()) } doReturn User.of(
1L, 1L,
"test", "test",
"example.com", "example.com",

View File

@ -80,7 +80,7 @@ class UsersTest {
val userCreateDto = UserCreate("test", "XXXXXXX") val userCreateDto = UserCreate("test", "XXXXXXX")
val userService = mock<UserService> { val userService = mock<UserService> {
onBlocking { usernameAlreadyUse(any()) } doReturn false onBlocking { usernameAlreadyUse(any()) } doReturn false
onBlocking { createLocalUser(any()) } doReturn User( onBlocking { createLocalUser(any()) } doReturn User.of(
id = 12345, id = 12345,
name = "test", name = "test",
domain = "example.com", domain = "example.com",

View File

@ -29,7 +29,7 @@ class APNoteServiceImplTest {
@Test @Test
fun `createPost 新しい投稿`() = runTest { fun `createPost 新しい投稿`() = runTest {
val followers = listOf<User>( val followers = listOf<User>(
User( User.of(
2L, 2L,
"follower", "follower",
"follower.example.com", "follower.example.com",
@ -38,11 +38,11 @@ class APNoteServiceImplTest {
"https://follower.example.com/inbox", "https://follower.example.com/inbox",
"https://follower.example.com/outbox", "https://follower.example.com/outbox",
"https://follower.example.com", "https://follower.example.com",
"", "https://follower.example.com",
publicKey = "", publicKey = "",
createdAt = Instant.now() createdAt = Instant.now()
), ),
User( User.of(
3L, 3L,
"follower2", "follower2",
"follower2.example.com", "follower2.example.com",
@ -51,23 +51,24 @@ class APNoteServiceImplTest {
"https://follower2.example.com/inbox", "https://follower2.example.com/inbox",
"https://follower2.example.com/outbox", "https://follower2.example.com/outbox",
"https://follower2.example.com", "https://follower2.example.com",
"", "https://follower2.example.com",
publicKey = "", publicKey = "",
createdAt = Instant.now() createdAt = Instant.now()
) )
) )
val userQueryService = mock<UserQueryService> { val userQueryService = mock<UserQueryService> {
onBlocking { findById(eq(1L)) } doReturn User( onBlocking { findById(eq(1L)) } doReturn User.of(
1L, 1L,
"test", "test",
"example.com", "example.com",
"testUser", "testUser",
"test user", "test user",
"a",
"https://example.com/inbox", "https://example.com/inbox",
"https://example.com/outbox", "https://example.com/outbox",
"https:.//example.com", "https://example.com",
"",
publicKey = "", publicKey = "",
privateKey = "a",
createdAt = Instant.now() createdAt = Instant.now()
) )
} }

View File

@ -101,7 +101,7 @@ class APReceiveFollowServiceImplTest {
} }
val userQueryService = mock<UserQueryService> { val userQueryService = mock<UserQueryService> {
onBlocking { findByUrl(eq("https://example.com")) } doReturn onBlocking { findByUrl(eq("https://example.com")) } doReturn
User( User.of(
id = 1L, id = 1L,
name = "test", name = "test",
domain = "example.com", domain = "example.com",
@ -114,7 +114,7 @@ class APReceiveFollowServiceImplTest {
createdAt = Instant.now() createdAt = Instant.now()
) )
onBlocking { findByUrl(eq("https://follower.example.com")) } doReturn onBlocking { findByUrl(eq("https://follower.example.com")) } doReturn
User( User.of(
id = 2L, id = 2L,
name = "follower", name = "follower",
domain = "follower.example.com", domain = "follower.example.com",

View File

@ -54,7 +54,7 @@ class JwtServiceImplTest {
} }
val jwtService = JwtServiceImpl(metaService, refreshTokenRepository, mock(), mock()) val jwtService = JwtServiceImpl(metaService, refreshTokenRepository, mock(), mock())
val token = jwtService.createToken( val token = jwtService.createToken(
User( User.of(
id = 1L, id = 1L,
name = "test", name = "test",
domain = "example.com", domain = "example.com",
@ -108,7 +108,7 @@ class JwtServiceImplTest {
) )
} }
val userService = mock<UserQueryService> { val userService = mock<UserQueryService> {
onBlocking { findById(1L) } doReturn User( onBlocking { findById(1L) } doReturn User.of(
id = 1L, id = 1L,
name = "test", name = "test",
domain = "example.com", domain = "example.com",

View File

@ -49,7 +49,7 @@ class UserServiceTest {
@Test @Test
fun `createRemoteUser リモートユーザーを作成できる`() = runTest { 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<UserRepository> { val userRepository = mock<UserRepository> {
onBlocking { nextId() } doReturn 113345L onBlocking { nextId() } doReturn 113345L