Merge branch 'develop' into feature/deliver-reaction

# Conflicts:
#	package-lock.json
#	package.json
#	src/main/kotlin/dev/usbharu/hideout/Application.kt
#	src/main/kotlin/dev/usbharu/hideout/domain/model/ap/ObjectDeserializer.kt
#	src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt
This commit is contained in:
usbharu 2023-08-08 15:28:38 +09:00
commit 10ea1bf51b
33 changed files with 921 additions and 252 deletions

View File

@ -80,12 +80,14 @@ dependencies {
implementation("org.xerial:sqlite-jdbc:3.40.1.0") implementation("org.xerial:sqlite-jdbc:3.40.1.0")
implementation("io.ktor:ktor-server-websockets-jvm:$ktor_version") implementation("io.ktor:ktor-server-websockets-jvm:$ktor_version")
implementation("io.ktor:ktor-server-cio-jvm:$ktor_version") implementation("io.ktor:ktor-server-cio-jvm:$ktor_version")
implementation("io.ktor:ktor-server-compression:$ktor_version")
implementation("ch.qos.logback:logback-classic:$logback_version") implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.insert-koin:koin-core:$koin_version") implementation("io.insert-koin:koin-core:$koin_version")
implementation("io.insert-koin:koin-ktor:$koin_version") implementation("io.insert-koin:koin-ktor:$koin_version")
implementation("io.insert-koin:koin-logger-slf4j:$koin_version") implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
implementation("io.insert-koin:koin-annotations:1.2.0") implementation("io.insert-koin:koin-annotations:1.2.0")
implementation("io.ktor:ktor-server-compression-jvm:2.3.0")
ksp("io.insert-koin:koin-ksp-compiler:1.2.0") ksp("io.insert-koin:koin-ksp-compiler:1.2.0")

22
openapitools.json Normal file
View File

@ -0,0 +1,22 @@
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "6.6.0",
"generators": {
"v3.0": {
"generatorName": "typescript-fetch",
"output": "src/main/web/generated",
"glob": "src/main/resources/openapi/api.yaml",
"additionalProperties": {
"modelPropertyNaming": "camelCase",
"supportsES6": true,
"withInterfaces": true,
"typescriptThreePlus": true,
"useSingleRequestParameter": false,
"prependFormOrBodyParameters": true
}
}
}
}
}

View File

@ -2,18 +2,26 @@
"name": "hideout", "name": "hideout",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"solid-js": "^1.7.3" "@solid-primitives/context": "^0.2.1",
"@solid-primitives/storage": "^1.3.11",
"@solidjs/router": "^0.8.2",
"@suid/icons-material": "^0.6.3",
"@suid/material": "^0.12.3",
"solid-js": "^1.7.6"
}, },
"devDependencies": { "devDependencies": {
"@openapitools/openapi-generator-cli": "^2.6.0",
"@suid/vite-plugin": "^0.1.3",
"rollup-plugin-visualizer": "^5.9.2",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"vite": "4.2.3", "vite": "4.2.3",
"vite-plugin-solid": "^2.7.0", "vite-plugin-solid": "^2.7.0"
"@suid/vite-plugin": "^0.1.3"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite",
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"serve": "vite preview" "serve": "vite preview",
"gen-api": "openapi-generator-cli generate"
} }
} }

View File

@ -98,6 +98,7 @@ fun Application.parent() {
runBlocking { runBlocking {
inject<IServerInitialiseService>().value.init() inject<IServerInitialiseService>().value.init()
} }
configureCompression()
configureHTTP() configureHTTP()
configureStaticRouting() configureStaticRouting()
configureMonitoring() configureMonitoring()
@ -117,7 +118,11 @@ fun Application.parent() {
activityPubUserService = inject<ActivityPubUserService>().value, activityPubUserService = inject<ActivityPubUserService>().value,
postService = inject<IPostApiService>().value, postService = inject<IPostApiService>().value,
userApiService = inject<IUserApiService>().value, userApiService = inject<IUserApiService>().value,
reactionService = inject<IReactionService>().value reactionService = inject<IReactionService>().value,
userAuthService = inject<IUserAuthService>().value,
userRepository = inject<IUserRepository>().value,
jwtService = inject<IJwtService>().value,
metaService = inject<IMetaService>().value
) )
} }

View File

@ -10,6 +10,8 @@ open class Note : Object {
var inReplyTo: String? = null var inReplyTo: String? = null
protected constructor() : super() protected constructor() : super()
@Suppress("LongParameterList")
constructor( constructor(
type: List<String> = emptyList(), type: List<String> = emptyList(),
name: String, name: String,

View File

@ -0,0 +1,31 @@
package dev.usbharu.hideout.domain.model.hideout.dto
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
data class PostResponse(
val id: Long,
val user: UserResponse,
val overview: String? = null,
val text: String? = null,
val createdAt: Long,
val visibility: Visibility,
val url: String,
val sensitive: Boolean = false,
) {
companion object {
fun from(post: Post, user: User): PostResponse {
return PostResponse(
id = post.id,
user = UserResponse.from(user),
overview = post.overview,
text = post.text,
createdAt = post.createdAt,
visibility = post.visibility,
url = post.url,
sensitive = post.sensitive
)
}
}
}

View File

@ -0,0 +1,19 @@
package dev.usbharu.hideout.plugins
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.compression.*
fun Application.configureCompression() {
install(Compression) {
gzip {
matchContentType(ContentType.Application.JavaScript)
priority = 1.0
}
deflate {
matchContentType(ContentType.Application.JavaScript)
priority = 10.0
minimumSize(1024) // condition
}
}
}

View File

@ -1,8 +1,10 @@
package dev.usbharu.hideout.plugins package dev.usbharu.hideout.plugins
import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.routing.activitypub.inbox import dev.usbharu.hideout.routing.activitypub.inbox
import dev.usbharu.hideout.routing.activitypub.outbox import dev.usbharu.hideout.routing.activitypub.outbox
import dev.usbharu.hideout.routing.activitypub.usersAP import dev.usbharu.hideout.routing.activitypub.usersAP
import dev.usbharu.hideout.routing.api.internal.v1.auth
import dev.usbharu.hideout.routing.api.internal.v1.posts import dev.usbharu.hideout.routing.api.internal.v1.posts
import dev.usbharu.hideout.routing.api.internal.v1.users import dev.usbharu.hideout.routing.api.internal.v1.users
import dev.usbharu.hideout.routing.wellknown.webfinger import dev.usbharu.hideout.routing.wellknown.webfinger
@ -12,6 +14,9 @@ import dev.usbharu.hideout.service.api.IPostApiService
import dev.usbharu.hideout.service.api.IUserApiService import dev.usbharu.hideout.service.api.IUserApiService
import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService
import dev.usbharu.hideout.service.reaction.IReactionService import dev.usbharu.hideout.service.reaction.IReactionService
import dev.usbharu.hideout.service.auth.IJwtService
import dev.usbharu.hideout.service.core.IMetaService
import dev.usbharu.hideout.service.user.IUserAuthService
import dev.usbharu.hideout.service.user.IUserService import dev.usbharu.hideout.service.user.IUserService
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.autohead.* import io.ktor.server.plugins.autohead.*
@ -25,7 +30,11 @@ fun Application.configureRouting(
activityPubUserService: ActivityPubUserService, activityPubUserService: ActivityPubUserService,
postService: IPostApiService, postService: IPostApiService,
userApiService: IUserApiService, userApiService: IUserApiService,
reactionService: IReactionService reactionService: IReactionService,
userAuthService: IUserAuthService,
userRepository: IUserRepository,
jwtService: IJwtService,
metaService: IMetaService
) { ) {
install(AutoHeadResponse) install(AutoHeadResponse)
routing { routing {
@ -36,6 +45,7 @@ fun Application.configureRouting(
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, reactionService) posts(postService, reactionService)
users(userService, userApiService) users(userService, userApiService)
auth(userAuthService, userRepository, jwtService)
} }
} }
} }

View File

@ -2,19 +2,12 @@ package dev.usbharu.hideout.plugins
import com.auth0.jwk.JwkProvider import com.auth0.jwk.JwkProvider
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken
import dev.usbharu.hideout.domain.model.hideout.form.UserLogin
import dev.usbharu.hideout.exception.UserNotFoundException
import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.auth.IJwtService
import dev.usbharu.hideout.service.core.IMetaService import dev.usbharu.hideout.service.core.IMetaService
import dev.usbharu.hideout.service.user.IUserAuthService
import dev.usbharu.hideout.util.JsonWebKeyUtil import dev.usbharu.hideout.util.JsonWebKeyUtil
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.* import io.ktor.server.auth.jwt.*
import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
@ -22,11 +15,8 @@ const val TOKEN_AUTH = "jwt-auth"
@Suppress("MagicNumber") @Suppress("MagicNumber")
fun Application.configureSecurity( fun Application.configureSecurity(
userAuthService: IUserAuthService, jwkProvider: JwkProvider,
metaService: IMetaService, metaService: IMetaService
userRepository: IUserRepository,
jwtService: IJwtService,
jwkProvider: JwkProvider
) { ) {
val issuer = Config.configData.url val issuer = Config.configData.url
install(Authentication) { install(Authentication) {
@ -48,24 +38,6 @@ fun Application.configureSecurity(
} }
routing { routing {
post("/login") {
val loginUser = call.receive<UserLogin>()
val check = userAuthService.verifyAccount(loginUser.username, loginUser.password)
if (check.not()) {
return@post call.respond(HttpStatusCode.Unauthorized)
}
val user = userRepository.findByNameAndDomain(loginUser.username, Config.configData.domain)
?: throw UserNotFoundException("${loginUser.username} was not found.")
return@post call.respond(jwtService.createToken(user))
}
post("/refresh-token") {
val refreshToken = call.receive<RefreshToken>()
return@post call.respond(jwtService.refreshToken(refreshToken))
}
get("/.well-known/jwks.json") { get("/.well-known/jwks.json") {
//language=JSON //language=JSON
val jwt = metaService.getJwtMeta() val jwt = metaService.getJwtMeta()
@ -74,12 +46,5 @@ fun Application.configureSecurity(
text = JsonWebKeyUtil.publicKeyToJwk(jwt.publicKey, jwt.kid.toString()) text = JsonWebKeyUtil.publicKeyToJwk(jwt.publicKey, jwt.kid.toString())
) )
} }
authenticate(TOKEN_AUTH) {
get("/auth-check") {
val principal = call.principal<JWTPrincipal>() ?: throw IllegalStateException("no principal")
val username = principal.payload.getClaim("uid")
call.respondText("Hello $username")
}
}
} }
} }

View File

@ -88,7 +88,9 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe
limit: Int?, limit: Int?,
userId: Long? userId: Long?
): List<Post> { ): List<Post> {
TODO("Not yet implemented") return query {
Posts.select { Posts.visibility eq Visibility.PUBLIC.ordinal }.map { it.toPost() }
}
} }
override suspend fun findByUserNameAndDomain( override suspend fun findByUserNameAndDomain(

View File

@ -0,0 +1,48 @@
package dev.usbharu.hideout.routing.api.internal.v1
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken
import dev.usbharu.hideout.domain.model.hideout.form.UserLogin
import dev.usbharu.hideout.exception.UserNotFoundException
import dev.usbharu.hideout.plugins.TOKEN_AUTH
import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.auth.IJwtService
import dev.usbharu.hideout.service.user.IUserAuthService
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.auth(
userAuthService: IUserAuthService,
userRepository: IUserRepository,
jwtService: IJwtService
) {
post("/login") {
val loginUser = call.receive<UserLogin>()
val check = userAuthService.verifyAccount(loginUser.username, loginUser.password)
if (check.not()) {
return@post call.respond(HttpStatusCode.Unauthorized)
}
val user = userRepository.findByNameAndDomain(loginUser.username, Config.configData.domain)
?: throw UserNotFoundException("${loginUser.username} was not found.")
return@post call.respond(jwtService.createToken(user))
}
post("/refresh-token") {
val refreshToken = call.receive<RefreshToken>()
return@post call.respond(jwtService.refreshToken(refreshToken))
}
authenticate(TOKEN_AUTH) {
get("/auth-check") {
val principal = call.principal<JWTPrincipal>() ?: throw IllegalStateException("no principal")
val username = principal.payload.getClaim("uid")
call.respondText("Hello $username")
}
}
}

View File

@ -1,12 +1,12 @@
package dev.usbharu.hideout.service.api package dev.usbharu.hideout.service.api
import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse
import java.time.Instant import java.time.Instant
@Suppress("LongParameterList") @Suppress("LongParameterList")
interface IPostApiService { interface IPostApiService {
suspend fun createPost(postForm: dev.usbharu.hideout.domain.model.hideout.form.Post, userId: Long): Post suspend fun createPost(postForm: dev.usbharu.hideout.domain.model.hideout.form.Post, userId: Long): PostResponse
suspend fun getById(id: Long, userId: Long?): Post suspend fun getById(id: Long, userId: Long?): PostResponse
suspend fun getAll( suspend fun getAll(
since: Instant? = null, since: Instant? = null,
until: Instant? = null, until: Instant? = null,
@ -14,7 +14,7 @@ interface IPostApiService {
maxId: Long? = null, maxId: Long? = null,
limit: Int? = null, limit: Int? = null,
userId: Long? = null userId: Long? = null
): List<Post> ): List<PostResponse>
suspend fun getByUser( suspend fun getByUser(
nameOrId: String, nameOrId: String,
@ -24,5 +24,5 @@ interface IPostApiService {
maxId: Long? = null, maxId: Long? = null,
limit: Int? = null, limit: Int? = null,
userId: Long? = null userId: Long? = null
): List<Post> ): List<PostResponse>
} }

View File

@ -2,11 +2,16 @@ package dev.usbharu.hideout.service.api
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse
import dev.usbharu.hideout.exception.PostNotFoundException import dev.usbharu.hideout.repository.*
import dev.usbharu.hideout.repository.IPostRepository
import dev.usbharu.hideout.service.post.IPostService import dev.usbharu.hideout.service.post.IPostService
import dev.usbharu.hideout.util.AcctUtil import dev.usbharu.hideout.util.AcctUtil
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.innerJoin
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import java.time.Instant import java.time.Instant
import dev.usbharu.hideout.domain.model.hideout.form.Post as FormPost import dev.usbharu.hideout.domain.model.hideout.form.Post as FormPost
@ -14,10 +19,11 @@ import dev.usbharu.hideout.domain.model.hideout.form.Post as FormPost
@Single @Single
class PostApiServiceImpl( class PostApiServiceImpl(
private val postService: IPostService, private val postService: IPostService,
private val postRepository: IPostRepository private val postRepository: IPostRepository,
private val userRepository: IUserRepository
) : IPostApiService { ) : IPostApiService {
override suspend fun createPost(postForm: FormPost, userId: Long): Post { override suspend fun createPost(postForm: FormPost, userId: Long): PostResponse {
return postService.createLocal( val createdPost = postService.createLocal(
PostCreateDto( PostCreateDto(
text = postForm.text, text = postForm.text,
overview = postForm.overview, overview = postForm.overview,
@ -27,11 +33,20 @@ class PostApiServiceImpl(
userId = userId userId = userId
) )
) )
val creator = userRepository.findById(userId)
return PostResponse.from(createdPost, creator!!)
} }
override suspend fun getById(id: Long, userId: Long?): Post { @Suppress("InjectDispatcher")
return postRepository.findOneById(id, userId) suspend fun <T> query(block: suspend () -> T): T =
?: throw PostNotFoundException("$id was not found or is not authorized.") newSuspendedTransaction(Dispatchers.IO) { block() }
override suspend fun getById(id: Long, userId: Long?): PostResponse {
val query = query {
Posts.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { Users.id }).select { Posts.id eq id }
.single()
}
return PostResponse.from(query.toPost(), query.toUser())
} }
override suspend fun getAll( override suspend fun getAll(
@ -41,7 +56,12 @@ class PostApiServiceImpl(
maxId: Long?, maxId: Long?,
limit: Int?, limit: Int?,
userId: Long? userId: Long?
): List<Post> = postRepository.findAll(since, until, minId, maxId, limit, userId) ): List<PostResponse> {
return query {
Posts.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id }).selectAll()
.map { PostResponse.from(it.toPost(), it.toUser()) }
}
}
override suspend fun getByUser( override suspend fun getByUser(
nameOrId: String, nameOrId: String,
@ -51,23 +71,22 @@ class PostApiServiceImpl(
maxId: Long?, maxId: Long?,
limit: Int?, limit: Int?,
userId: Long? userId: Long?
): List<Post> { ): List<PostResponse> {
val idOrNull = nameOrId.toLongOrNull() val idOrNull = nameOrId.toLongOrNull()
return if (idOrNull == null) { return if (idOrNull == null) {
val acct = AcctUtil.parse(nameOrId) val acct = AcctUtil.parse(nameOrId)
postRepository.findByUserNameAndDomain( query {
username = acct.username, Posts.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id }).select {
s = acct.domain Users.name.eq(acct.username)
?: Config.configData.domain, .and(Users.domain eq (acct.domain ?: Config.configData.domain))
since = since, }.map { PostResponse.from(it.toPost(), it.toUser()) }
until = until, }
minId = minId,
maxId = maxId,
limit = limit,
userId = userId
)
} else { } else {
postRepository.findByUserId(idOrNull, since, until, minId, maxId, limit, userId) query {
Posts.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id }).select {
Posts.userId eq idOrNull
}.map { PostResponse.from(it.toPost(), it.toUser()) }
}
} }
} }
} }

View File

@ -73,6 +73,7 @@ class ExposedJobRepository(
} }
} }
@Suppress("SuspendFunWithFlowReturnType")
override suspend fun findNext(names: Set<String>, status: Set<JobStatus>, limit: Int): Flow<ScheduledJob> { override suspend fun findNext(names: Set<String>, status: Set<JobStatus>, limit: Int): Flow<ScheduledJob> {
return query { return query {
jobs.select( jobs.select(

View File

@ -20,7 +20,7 @@ paths:
schema: schema:
type: array type: array
items: items:
$ref: "#/components/schemas/Post" $ref: "#/components/schemas/PostResponse"
401: 401:
$ref: "#/components/responses/Unauthorized" $ref: "#/components/responses/Unauthorized"
403: 403:
@ -37,7 +37,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/Post" $ref: "#/components/schemas/PostRequest"
responses: responses:
200: 200:
description: 成功 description: 成功
@ -65,7 +65,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/Post" $ref: "#/components/schemas/PostResponse"
401: 401:
$ref: "#/components/responses/Unauthorized" $ref: "#/components/responses/Unauthorized"
403: 403:
@ -90,7 +90,7 @@ paths:
schema: schema:
type: array type: array
items: items:
$ref: "#/components/schemas/Post" $ref: "#/components/schemas/PostResponse"
401: 401:
$ref: "#/components/responses/Unauthorized" $ref: "#/components/responses/Unauthorized"
403: 403:
@ -114,7 +114,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/Post" $ref: "#/components/schemas/PostResponse"
401: 401:
$ref: "#/components/responses/Unauthorized" $ref: "#/components/responses/Unauthorized"
403: 403:
@ -137,7 +137,7 @@ paths:
schema: schema:
type: array type: array
items: items:
$ref: "#/components/schemas/User" $ref: "#/components/schemas/UserResponse"
post: post:
summary: ユーザーを作成する summary: ユーザーを作成する
@ -181,7 +181,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/User" $ref: "#/components/schemas/UserResponse"
404: 404:
$ref: "#/components/responses/NotFound" $ref: "#/components/responses/NotFound"
@ -198,7 +198,7 @@ paths:
schema: schema:
type: array type: array
items: items:
$ref: "#/components/schemas/User" $ref: "#/components/schemas/UserResponse"
post: post:
summary: ユーザーをフォローする summary: ユーザーをフォローする
security: security:
@ -228,7 +228,47 @@ paths:
schema: schema:
type: array type: array
items: items:
$ref: "#/components/schemas/User" $ref: "#/components/schemas/UserResponse"
/login:
post:
summary: ログインする
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UserLogin"
responses:
200:
description: ログイン成功
content:
application/json:
schema:
$ref: "#/components/schemas/JwtToken"
/refresh-token:
post:
summary: 期限切れトークンの再発行をする
responses:
200:
description: トークンの再発行に成功
content:
application/json:
schema:
$ref: "#/components/schemas/JwtToken"
/auth-check:
get:
summary: 認証チェック
responses:
200:
description: 認証に成功
content:
text/plain:
schema:
type: string
components: components:
responses: responses:
@ -261,8 +301,23 @@ components:
type: string type: string
schemas: schemas:
User: Visibility:
type: string
enum:
- public
- unlisted
- followers
- direct
UserResponse:
type: object type: object
required:
- id
- name
- domain
- screenName
- description
- url
- createdAt
properties: properties:
id: id:
type: number type: number
@ -277,23 +332,30 @@ components:
type: string type: string
description: description:
type: string type: string
nullable: true
url: url:
type: string type: string
readOnly: true readOnly: true
createdAt: createdAt:
type: number type: number
readOnly: true readOnly: true
Post: PostResponse:
type: object type: object
required:
- id
- user
- text
- createdAt
- visibility
- url
- sensitive
properties: properties:
id: id:
type: integer type: integer
format: int64 format: int64
readOnly: true readOnly: true
userId: user:
type: integer $ref: "#/components/schemas/UserResponse"
format: int64
readOnly: true
overview: overview:
type: string type: string
text: text:
@ -303,12 +365,7 @@ components:
format: int64 format: int64
readOnly: true readOnly: true
visibility: visibility:
type: string $ref: "#/components/schemas/Visibility"
enum:
- public
- unlisted
- followers
- direct
url: url:
type: string type: string
format: uri format: uri
@ -323,13 +380,49 @@ components:
readOnly: true readOnly: true
sensitive: sensitive:
type: boolean type: boolean
apId:
type: string
format: url
readOnly: true
PostRequest:
type: object
properties:
overview:
type: string
text:
type: string
visibility:
$ref: "#/components/schemas/Visibility"
repostId:
type: integer
format: int64
replyId:
type: integer
format: int64
sensitive:
type: boolean
JwtToken:
type: object
properties:
token:
type: string
refreshToken:
type: string
RefreshToken:
type: object
properties:
refreshToken:
type: string
UserLogin:
type: object
properties:
username:
type: string
password:
type: string
securitySchemes: securitySchemes:
BearerAuth: BearerAuth:
type: http type: http
scheme: bearer scheme: bearer
bearerFormat: JWT

View File

@ -1,58 +1,44 @@
import {Component, createSignal} from "solid-js"; import {Component, createEffect, createSignal} from "solid-js";
import {Route, Router, Routes} from "@solidjs/router";
import {TopPage} from "./pages/TopPage";
import {createTheme, CssBaseline, ThemeProvider, useMediaQuery} from "@suid/material";
import {createCookieStorage} from "@solid-primitives/storage";
import {ApiProvider} from "./lib/ApiProvider";
import {Configuration, DefaultApi} from "./generated";
import {LoginPage} from "./pages/LoginPage";
export const App: Component = () => { export const App: Component = () => {
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
const [cookie, setCookie] = createCookieStorage()
const [api, setApi] = createSignal(new DefaultApi(new Configuration({
basePath: window.location.origin + "/api/internal/v1",
accessToken: cookie.token as string
})))
const fn = (form: HTMLButtonElement) => { createEffect(() => {
console.log(form) setApi(
} new DefaultApi(new Configuration({
basePath: window.location.origin + "/api/internal/v1",
accessToken : cookie.token as string
})))
})
const [username, setUsername] = createSignal("") const theme = createTheme({
const [password, setPassword] = createSignal("") palette: {
mode: prefersDarkMode() ? 'dark' : 'light',
return (
<form onSubmit={function (e: SubmitEvent) {
e.preventDefault()
fetch("/login", {
method: "POST",
body: JSON.stringify({username: username(), password: password()}),
headers: {
'Content-Type': 'application/json'
}
}).then(res => res.json())
// .then(res => fetch("/auth-check", {
// method: "GET",
// headers: {
// 'Authorization': 'Bearer ' + res.token
// }
// }))
// .then(res => res.json())
.then(res => {
console.log(res.token);
fetch("/refresh-token", {
method: "POST",
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({refreshToken: res.refreshToken}),
}).then(res=> res.json()).then(res => console.log(res.token))
})
} }
})
}> return (
<input name="username" type="text" placeholder="Username" required <ApiProvider api={api()}>
onChange={(e) => setUsername(e.currentTarget.value)}/> <ThemeProvider theme={theme}>
<input name="password" type="password" placeholder="Password" required <CssBaseline/>
onChange={(e) => setPassword(e.currentTarget.value)}/> <Router>
<button type="submit">Submit</button> <Routes>
</form> <Route path="/" component={TopPage}/>
<Route path="/login" component={LoginPage}/>
</Routes>
</Router>
</ThemeProvider>
</ApiProvider>
) )
} }
declare module 'solid-js' {
namespace JSX {
interface Directives {
fn: (form: HTMLFormElement) => void
}
}
}

View File

@ -0,0 +1,8 @@
import {Avatar as SuidAvatar} from "@suid/material";
import {Component, JSXElement} from "solid-js";
export const Avatar: Component<{ src: string }> = (props): JSXElement => {
return (
<SuidAvatar src={props.src}/>
)
}

View File

@ -0,0 +1,14 @@
import {ParentComponent} from "solid-js";
import {Button, ListItem, ListItemAvatar, ListItemButton, ListItemIcon, ListItemText} from "@suid/material";
import {Link} from "@solidjs/router";
export const SidebarButton: ParentComponent<{ text: string,linkTo:string }> = (props) => {
return (
<ListItem>
<ListItemButton component={Link} href={props.linkTo}>
<ListItemIcon>{props.children}</ListItemIcon>
<ListItemText primary={props.text}/>
</ListItemButton>
</ListItem>
)
}

View File

@ -0,0 +1,8 @@
import {createContextProvider} from "@solid-primitives/context";
import {createSignal} from "solid-js";
import {DefaultApi, DefaultApiInterface} from "../generated";
export const [ApiProvider,useApi] = createContextProvider((props:{api:DefaultApiInterface}) => {
const [api,setApi] = createSignal(props.api);
return api
},()=>new DefaultApi());

View File

@ -0,0 +1,16 @@
import {DefaultApiInterface} from "../generated";
export class ApiWrapper {
api: DefaultApiInterface;
constructor(initApi: DefaultApiInterface) {
this.api = initApi;
console.log(this.api);
console.log(this.postsGet());
}
postsGet = async () => this.api.postsGet()
usersUserNameGet = async (userName: string) => this.api.usersUserNameGet(userName);
}

View File

@ -0,0 +1,29 @@
import {Component, Match, Switch} from "solid-js";
import {Home, Lock, Mail, Public} from "@suid/icons-material";
import {IconButton} from "@suid/material";
import {Visibility} from "../generated";
export const ShareScopeIndicator: Component<{ visibility: Visibility }> = (props) => {
return <Switch fallback={<Public/>}>
<Match when={props.visibility == "public"}>
<IconButton>
<Public/>
</IconButton>
</Match>
<Match when={props.visibility == "direct"}>
<IconButton>
<Mail/>
</IconButton>
</Match>
<Match when={props.visibility == "followers"}>
<IconButton>
<Lock/>
</IconButton>
</Match>
<Match when={props.visibility == "unlisted"}>
<IconButton>
<Home/>
</IconButton>
</Match>
</Switch>
}

View File

@ -0,0 +1,45 @@
import {Component, createSignal} from "solid-js";
import {Box, Card, CardActions, CardContent, CardHeader, IconButton, Menu, MenuItem, Typography} from "@suid/material";
import {Avatar} from "../atoms/Avatar";
import {Favorite, MoreVert, Reply, ScreenRotationAlt} from "@suid/icons-material";
import {ShareScopeIndicator} from "../molecules/ShareScopeIndicator";
import {PostResponse} from "../generated";
export const Post: Component<{ post: PostResponse }> = (props) => {
const [anchorEl, setAnchorEl] = createSignal<null | HTMLElement>(null)
const open = () => Boolean(anchorEl());
const handleClose = () => {
setAnchorEl(null);
}
return (
<Card>
<CardHeader avatar={<Avatar src={props.post.user.url + "/icon.jpg"}/>} title={props.post.user.screenName}
subheader={`${props.post.user.name}@${props.post.user.domain}`}
action={<IconButton onclick={(event) => {
setAnchorEl(event.currentTarget)
}}><MoreVert/><Menu disableScrollLock anchorEl={anchorEl()} open={open()} onClose={handleClose}><MenuItem
onclick={handleClose}>aaa</MenuItem></Menu> </IconButton>}/>
<CardContent>
<Typography>
{props.post.text}
</Typography>
</CardContent>
<CardActions disableSpacing>
<IconButton>
<Reply/>
</IconButton>
<IconButton>
<ScreenRotationAlt/>
</IconButton>
<IconButton>
<Favorite/>
</IconButton>
<Box sx={{marginLeft: "auto"}}>
<Typography>{new Date(props.post.createdAt).toDateString()}</Typography>
</Box>
<ShareScopeIndicator visibility={props.post.visibility}/>
</CardActions>
</Card>
)
}

View File

@ -0,0 +1,43 @@
import {Component, createSignal} from "solid-js";
import {Button, IconButton, Paper, Stack, TextField, Typography} from "@suid/material";
import {Avatar} from "../atoms/Avatar";
import {AddPhotoAlternate, Poll, Public} from "@suid/icons-material";
import {useApi} from "../lib/ApiProvider";
export const PostForm: Component<{ label: string }> = (props) => {
const [text, setText] = createSignal("")
const api = useApi()
return (
<Paper sx={{width: "100%"}}>
<Stack>
<Stack direction={"row"} spacing={2} sx={{padding: 2}}>
<Avatar src={""}/>
<TextField label={props.label} multiline rows={4} variant={"standard"} onChange={(event)=>setText(event.target.value)} fullWidth/>
</Stack>
<Stack direction={"row"} justifyContent={"space-between"} sx={{padding: 2}}>
<Stack direction={"row"} justifyContent={"flex-start"} alignItems={"center"}>
<IconButton>
<AddPhotoAlternate/>
</IconButton>
<IconButton>
<Poll/>
</IconButton>
<IconButton>
<Public/>
</IconButton>
</Stack>
<Stack direction={"row"} alignItems={"center"} spacing={2}>
<Typography>
aaa
</Typography>
<Button variant={"contained"} onClick={() => {
api().postsPost({text: text()}).then(()=>setText(""))
}}>
稿
</Button>
</Stack>
</Stack>
</Stack>
</Paper>
)
}

View File

@ -0,0 +1,58 @@
import {Button, Card, CardContent, CardHeader, Modal, Stack, TextField} from "@suid/material";
import {Component, createSignal} from "solid-js";
import {createCookieStorage} from "@solid-primitives/storage";
import {useApi} from "../lib/ApiProvider";
import {useNavigate} from "@solidjs/router";
export const LoginPage: Component = () => {
const [username, setUsername] = createSignal("")
const [password, setPassword] = createSignal("")
const [cookie, setCookie] = createCookieStorage();
const navigator = useNavigate();
const api = useApi();
const onSubmit: () => void = () => {
api().loginPost({password: password(), username: username()}).then(value => {
setCookie("token", value.token);
setCookie("refresh-token", value.refreshToken)
navigator("/")
}).catch(reason => {
console.log(reason);
setPassword("")
})
}
return (
<Modal open>
<Card>
<CardHeader/>
<CardContent>
<Stack spacing={3}>
<TextField
value={username()}
onChange={(event) => setUsername(event.target.value)}
label="Username"
type="text"
autoComplete="username"
variant="standard"
/>
<TextField
value={password()}
onChange={(event) => setPassword(event.target.value)}
label="Password"
type="password"
autoComplete="current-password"
variant="standard"
/>
<Button type={"submit"} onClick={onSubmit}>Login</Button>
</Stack>
</CardContent>
</Card>
</Modal>
)
}

View File

@ -0,0 +1,24 @@
import {Component} from "solid-js";
import {MainPage} from "../templates/MainPage";
import {PostForm} from "../organisms/PostForm";
import {Stack} from "@suid/material";
import {PostResponse} from "../generated";
import {PostList} from "../templates/PostList";
import {useApi} from "../lib/ApiProvider";
import {createStore} from "solid-js/store";
export const TopPage: Component = () => {
const api = useApi()
const [posts, setPosts] = createStore<PostResponse[]>([])
api().postsGet().then((res)=>setPosts(res))
return (
<MainPage>
<Stack spacing={1} alignItems={"stretch"}>
<PostForm label={"投稿する"}/>
<PostList posts={posts}/>
</Stack>
</MainPage>
)
}

View File

@ -0,0 +1,20 @@
import {createSignal, ParentComponent} from "solid-js";
import {Grid} from "@suid/material";
import {Sidebar} from "./Sidebar";
export const MainPage: ParentComponent = (props) => {
return (
<Grid container spacing={2} wrap={"nowrap"}>
<Grid item xs={0} md={3}>
<Sidebar/>
</Grid>
<Grid item xs={12} md={6}>
{props.children}
</Grid>
<Grid item xs={0} md={3}>
</Grid>
</Grid>
)
}

View File

@ -0,0 +1,14 @@
import {Component, For} from "solid-js";
import {CircularProgress} from "@suid/material";
import {Post} from "../organisms/Post";
import {PostResponse} from "../generated";
export const PostList: Component<{ posts: PostResponse[] | undefined }> = (props) => {
return (
<For each={props.posts} fallback={<CircularProgress/>}>
{
(item, index) => <Post post={item}/>
}
</For>
)
}

View File

@ -0,0 +1,13 @@
import {Component} from "solid-js";
import {Button, List, Stack} from "@suid/material";
import {Home} from "@suid/icons-material";
import {SidebarButton} from "../atoms/SidebarButton";
export const Sidebar: Component = (props) => {
return (
<List>
<SidebarButton text={"AP"} linkTo={"/"}></SidebarButton>
<SidebarButton text={"Home"} linkTo={"/"}><Home/></SidebarButton>
</List>
)
}

View File

@ -15,6 +15,7 @@ import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken
import dev.usbharu.hideout.domain.model.hideout.form.UserLogin import dev.usbharu.hideout.domain.model.hideout.form.UserLogin
import dev.usbharu.hideout.exception.InvalidRefreshTokenException import dev.usbharu.hideout.exception.InvalidRefreshTokenException
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.routing.api.internal.v1.auth
import dev.usbharu.hideout.service.auth.IJwtService import dev.usbharu.hideout.service.auth.IJwtService
import dev.usbharu.hideout.service.core.IMetaService import dev.usbharu.hideout.service.core.IMetaService
import dev.usbharu.hideout.service.user.IUserAuthService import dev.usbharu.hideout.service.user.IUserAuthService
@ -24,6 +25,7 @@ import io.ktor.client.request.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.config.* import io.ktor.server.config.*
import io.ktor.server.routing.*
import io.ktor.server.testing.* import io.ktor.server.testing.*
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.anyString
@ -70,7 +72,10 @@ class SecurityKtTest {
val jwkProvider = mock<JwkProvider>() val jwkProvider = mock<JwkProvider>()
application { application {
configureSerialization() configureSerialization()
configureSecurity(userAuthService, metaService, userRepository, jwtService, jwkProvider) configureSecurity(jwkProvider, metaService)
routing {
auth(userAuthService, userRepository, jwtService)
}
} }
client.post("/login") { client.post("/login") {
@ -97,7 +102,10 @@ class SecurityKtTest {
val jwkProvider = mock<JwkProvider>() val jwkProvider = mock<JwkProvider>()
application { application {
configureSerialization() configureSerialization()
configureSecurity(userAuthService, metaService, userRepository, jwtService, jwkProvider) configureSecurity(jwkProvider, metaService)
routing {
auth(userAuthService, userRepository, jwtService)
}
} }
client.post("/login") { client.post("/login") {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
@ -122,7 +130,10 @@ class SecurityKtTest {
val jwkProvider = mock<JwkProvider>() val jwkProvider = mock<JwkProvider>()
application { application {
configureSerialization() configureSerialization()
configureSecurity(userAuthService, metaService, userRepository, jwtService, jwkProvider) configureSecurity(jwkProvider, metaService)
routing {
auth(userAuthService, userRepository, jwtService)
}
} }
client.post("/login") { client.post("/login") {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
@ -140,7 +151,10 @@ class SecurityKtTest {
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper()) Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing {
auth(mock(), mock(), mock())
}
} }
client.get("/auth-check").apply { client.get("/auth-check").apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status) assertEquals(HttpStatusCode.Unauthorized, call.response.status)
@ -155,7 +169,10 @@ class SecurityKtTest {
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper()) Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing {
auth(mock(), mock(), mock())
}
} }
client.get("/auth-check") { client.get("/auth-check") {
header("Authorization", "Digest dsfjjhogalkjdfmlhaog") header("Authorization", "Digest dsfjjhogalkjdfmlhaog")
@ -172,7 +189,10 @@ class SecurityKtTest {
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper()) Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing {
auth(mock(), mock(), mock())
}
} }
client.get("/auth-check") { client.get("/auth-check") {
header("Authorization", "") header("Authorization", "")
@ -190,7 +210,10 @@ class SecurityKtTest {
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing {
auth(mock(), mock(), mock())
}
} }
client.get("/auth-check") { client.get("/auth-check") {
header("Authorization", "Bearer ") header("Authorization", "Bearer ")
@ -244,11 +267,12 @@ class SecurityKtTest {
) )
) )
} }
val userRepository = mock<IUserRepository>()
val jwtService = mock<IJwtService>()
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider) configureSecurity(jwkProvider, metaService)
routing {
auth(mock(), mock(), mock())
}
} }
client.get("/auth-check") { client.get("/auth-check") {
@ -304,11 +328,12 @@ class SecurityKtTest {
) )
) )
} }
val userRepository = mock<IUserRepository>()
val jwtService = mock<IJwtService>()
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider) configureSecurity(jwkProvider, metaService)
routing {
auth(mock(), mock(), mock())
}
} }
client.get("/auth-check") { client.get("/auth-check") {
header("Authorization", "Bearer $token") header("Authorization", "Bearer $token")
@ -362,11 +387,12 @@ class SecurityKtTest {
) )
) )
} }
val userRepository = mock<IUserRepository>()
val jwtService = mock<IJwtService>()
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider) configureSecurity(jwkProvider, metaService)
routing {
auth(mock(), mock(), mock())
}
} }
client.get("/auth-check") { client.get("/auth-check") {
header("Authorization", "Bearer $token") header("Authorization", "Bearer $token")
@ -420,11 +446,12 @@ class SecurityKtTest {
) )
) )
} }
val userRepository = mock<IUserRepository>()
val jwtService = mock<IJwtService>()
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider) configureSecurity(jwkProvider, metaService)
routing {
auth(mock(), mock(), mock())
}
} }
client.get("/auth-check") { client.get("/auth-check") {
header("Authorization", "Bearer $token") header("Authorization", "Bearer $token")
@ -477,11 +504,12 @@ class SecurityKtTest {
) )
) )
} }
val userRepository = mock<IUserRepository>()
val jwtService = mock<IJwtService>()
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider) configureSecurity(jwkProvider, metaService)
routing {
auth(mock(), mock(), mock())
}
} }
client.get("/auth-check") { client.get("/auth-check") {
header("Authorization", "Bearer $token") header("Authorization", "Bearer $token")
@ -501,7 +529,10 @@ class SecurityKtTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), jwtService, mock()) configureSecurity(mock(), mock())
routing {
auth(mock(), mock(), jwtService)
}
} }
client.post("/refresh-token") { client.post("/refresh-token") {
header("Content-Type", "application/json") header("Content-Type", "application/json")
@ -523,7 +554,10 @@ class SecurityKtTest {
application { application {
configureStatusPages() configureStatusPages()
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), jwtService, mock()) configureSecurity(mock(), mock())
routing {
auth(mock(), mock(), jwtService)
}
} }
client.post("/refresh-token") { client.post("/refresh-token") {
header("Content-Type", "application/json") header("Content-Type", "application/json")

View File

@ -4,6 +4,8 @@ import com.auth0.jwt.interfaces.Claim
import com.auth0.jwt.interfaces.Payload import com.auth0.jwt.interfaces.Payload
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse
import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse
import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import dev.usbharu.hideout.plugins.TOKEN_AUTH import dev.usbharu.hideout.plugins.TOKEN_AUTH
@ -32,18 +34,27 @@ class PostsTest {
environment { environment {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val user = UserResponse(
id = 54321,
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
)
val posts = listOf( val posts = listOf(
Post( PostResponse(
id = 12345, id = 12345,
userId = 4321, user = user,
text = "test1", text = "test1",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( PostResponse(
id = 123456, id = 123456,
userId = 4322, user = user,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
@ -64,7 +75,7 @@ class PostsTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService, mock())
@ -89,27 +100,35 @@ class PostsTest {
val payload = mock<Payload> { val payload = mock<Payload> {
on { getClaim(eq("uid")) } doReturn claim on { getClaim(eq("uid")) } doReturn claim
} }
val user = UserResponse(
id = 54321,
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
)
val posts = listOf( val posts = listOf(
Post( PostResponse(
id = 12345, id = 12345,
userId = 4321, user = user,
text = "test1", text = "test1",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( PostResponse(
id = 123456, id = 123456,
userId = 4322, user = user,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
), ),
Post( PostResponse(
id = 1234567, id = 1234567,
userId = 4333, user = user,
text = "Followers only", text = "Followers only",
visibility = Visibility.FOLLOWERS, visibility = Visibility.FOLLOWERS,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
@ -156,9 +175,18 @@ class PostsTest {
environment { environment {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val user = UserResponse(
12345, id = 54321,
1234, name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
)
val post = PostResponse(
id = 12345,
user = user,
text = "aaa", text = "aaa",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
@ -169,7 +197,7 @@ class PostsTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService, mock())
@ -187,9 +215,17 @@ class PostsTest {
environment { environment {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = PostResponse(
12345, 12345,
1234, UserResponse(
id = 54321,
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
),
text = "aaa", text = "aaa",
visibility = Visibility.FOLLOWERS, visibility = Visibility.FOLLOWERS,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
@ -242,14 +278,22 @@ class PostsTest {
onBlocking { createPost(any(), any()) } doAnswer { onBlocking { createPost(any(), any()) } doAnswer {
val argument = it.getArgument<dev.usbharu.hideout.domain.model.hideout.form.Post>(0) val argument = it.getArgument<dev.usbharu.hideout.domain.model.hideout.form.Post>(0)
val userId = it.getArgument<Long>(1) val userId = it.getArgument<Long>(1)
Post( PostResponse(
123L, id = 123L,
userId, user = UserResponse(
null, id = 54321,
argument.text, name = "user1",
Instant.now().toEpochMilli(), domain = "example.com",
Visibility.PUBLIC, screenName = "user 1",
"https://example.com" description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
),
overview = null,
text = argument.text,
createdAt = Instant.now().toEpochMilli(),
visibility = Visibility.PUBLIC,
url = "https://example.com"
) )
} }
} }
@ -290,18 +334,27 @@ class PostsTest {
environment { environment {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val user = UserResponse(
id = 54321,
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
)
val posts = listOf( val posts = listOf(
Post( PostResponse(
id = 12345, id = 12345,
userId = 1, user = user,
text = "test1", text = "test1",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( PostResponse(
id = 123456, id = 123456,
userId = 1, user = user,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
@ -323,7 +376,7 @@ class PostsTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService, mock())
@ -342,18 +395,27 @@ class PostsTest {
environment { environment {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val user = UserResponse(
id = 54321,
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
)
val posts = listOf( val posts = listOf(
Post( PostResponse(
id = 12345, id = 12345,
userId = 1, user = user,
text = "test1", text = "test1",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( PostResponse(
id = 123456, id = 123456,
userId = 1, user = user,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
@ -375,7 +437,7 @@ class PostsTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService, mock())
@ -394,18 +456,27 @@ class PostsTest {
environment { environment {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val user = UserResponse(
id = 54321,
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
)
val posts = listOf( val posts = listOf(
Post( PostResponse(
id = 12345, id = 12345,
userId = 1, user = user,
text = "test1", text = "test1",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( PostResponse(
id = 123456, id = 123456,
userId = 1, user = user,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
@ -427,7 +498,7 @@ class PostsTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService, mock())
@ -446,18 +517,27 @@ class PostsTest {
environment { environment {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val user = UserResponse(
id = 54321,
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
)
val posts = listOf( val posts = listOf(
Post( PostResponse(
id = 12345, id = 12345,
userId = 1, user = user,
text = "test1", text = "test1",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( PostResponse(
id = 123456, id = 123456,
userId = 1, user = user,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
@ -479,7 +559,7 @@ class PostsTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService, mock())
@ -498,9 +578,17 @@ class PostsTest {
environment { environment {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = PostResponse(
id = 123456, id = 123456,
userId = 1, user = UserResponse(
id = 54321,
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
),
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
@ -511,7 +599,7 @@ class PostsTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService, mock())
@ -530,9 +618,17 @@ class PostsTest {
environment { environment {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = PostResponse(
id = 123456, id = 123456,
userId = 1, user = UserResponse(
id = 54321,
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
),
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
@ -543,7 +639,7 @@ class PostsTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService, mock())
@ -562,9 +658,17 @@ class PostsTest {
environment { environment {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = PostResponse(
id = 123456, id = 123456,
userId = 1, user = UserResponse(
id = 54321,
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
),
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
@ -575,7 +679,7 @@ class PostsTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService, mock())
@ -594,9 +698,17 @@ class PostsTest {
environment { environment {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = PostResponse(
id = 123456, id = 123456,
userId = 1, user = UserResponse(
id = 54321,
name = "user1",
domain = "example.com",
screenName = "user 1",
description = "Test user",
url = "https://example.com/users/54321",
createdAt = Instant.now().toEpochMilli()
),
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
@ -607,7 +719,7 @@ class PostsTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService, mock())

View File

@ -58,7 +58,7 @@ class UsersTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(mock(), userService) users(mock(), userService)
@ -96,7 +96,7 @@ class UsersTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(userService, mock()) users(userService, mock())
@ -127,7 +127,7 @@ class UsersTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(userService, mock()) users(userService, mock())
@ -162,7 +162,7 @@ class UsersTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(mock(), userApiService) users(mock(), userApiService)
@ -195,7 +195,7 @@ class UsersTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(mock(), userApiService) users(mock(), userApiService)
@ -228,7 +228,7 @@ class UsersTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(mock(), userApiService) users(mock(), userApiService)
@ -261,7 +261,7 @@ class UsersTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(mock(), userApiService) users(mock(), userApiService)
@ -306,7 +306,7 @@ class UsersTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(mock(), userApiService) users(mock(), userApiService)
@ -351,7 +351,7 @@ class UsersTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(mock(), userApiService) users(mock(), userApiService)
@ -396,7 +396,7 @@ class UsersTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(mock(), userApiService) users(mock(), userApiService)
@ -591,7 +591,7 @@ class UsersTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(mock(), userApiService) users(mock(), userApiService)
@ -636,7 +636,7 @@ class UsersTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(mock(), userApiService) users(mock(), userApiService)
@ -681,7 +681,7 @@ class UsersTest {
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(mock(), userApiService) users(mock(), userApiService)

View File

@ -48,13 +48,21 @@ class ActivityPubReceiveFollowServiceImplTest {
firstValue.invoke(scheduleContext, ReceiveFollowJob) firstValue.invoke(scheduleContext, ReceiveFollowJob)
val actor = scheduleContext.props.props[ReceiveFollowJob.actor.name] val actor = scheduleContext.props.props[ReceiveFollowJob.actor.name]
val targetActor = scheduleContext.props.props[ReceiveFollowJob.targetActor.name] val targetActor = scheduleContext.props.props[ReceiveFollowJob.targetActor.name]
val follow = scheduleContext.props.props[ReceiveFollowJob.follow.name] val follow = scheduleContext.props.props[ReceiveFollowJob.follow.name] as String
assertEquals("https://follower.example.com", actor) assertEquals("https://follower.example.com", actor)
assertEquals("https://example.com", targetActor) assertEquals("https://example.com", targetActor)
//language=JSON //language=JSON
assertEquals( assertEquals(
"""{"type":"Follow","name":"Follow","actor":"https://follower.example.com","object":"https://example.com","@context":null}""", Json.parseToJsonElement(
follow """{
"type": "Follow",
"name": "Follow",
"actor": "https://follower.example.com",
"object": "https://example.com",
"@context": null
}"""
),
Json.parseToJsonElement(follow)
) )
} }
} }
@ -155,7 +163,14 @@ class ActivityPubReceiveFollowServiceImplTest {
data = mapOf<String, Any>( data = mapOf<String, Any>(
ReceiveFollowJob.actor.name to "https://follower.example.com", ReceiveFollowJob.actor.name to "https://follower.example.com",
ReceiveFollowJob.targetActor.name to "https://example.com", ReceiveFollowJob.targetActor.name to "https://example.com",
ReceiveFollowJob.follow.name to """{"type":"Follow","name":"Follow","object":"https://example.com","actor":"https://follower.example.com","@context":null}""" //language=JSON
ReceiveFollowJob.follow.name to """{
"type": "Follow",
"name": "Follow",
"object": "https://example.com",
"actor": "https://follower.example.com",
"@context": null
}"""
), ),
json = Json json = Json
) )

View File

@ -1,21 +1,24 @@
import { defineConfig } from 'vite'; import {defineConfig, splitVendorChunkPlugin} from 'vite';
import solidPlugin from 'vite-plugin-solid'; import solidPlugin from 'vite-plugin-solid';
import suidPlugin from "@suid/vite-plugin"; import suidPlugin from "@suid/vite-plugin";
import visualizer from "rollup-plugin-visualizer";
export default defineConfig({ export default defineConfig({
plugins: [solidPlugin(),suidPlugin()], plugins: [solidPlugin(),suidPlugin(),splitVendorChunkPlugin()],
server: { server: {
port: 3000, port: 3000,
proxy: { proxy: {
'/api': 'http://localhost:8080', '/api': 'http://localhost:8080',
'/login': 'http://localhost:8080',
'/auth-check': 'http://localhost:8080',
'/refresh-token': 'http://localhost:8080',
} }
}, },
root: './src/main/web', root: './src/main/web',
build: { build: {
target: 'esnext', target: 'esnext',
outDir: '../resources/static', outDir: '../resources/static',
rollupOptions:{
plugins: [
visualizer()
]
}
}, },
}); });