Merge pull request #22 from usbharu/feature/web-ui

Feature/web UI
This commit is contained in:
usbharu 2023-07-30 16:45:48 +09:00 committed by GitHub
commit 4bec7a49d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 922 additions and 261 deletions

2
.gitignore vendored
View File

@ -37,3 +37,5 @@ out/
*.db
/src/main/resources/static/
/node_modules/
/src/main/web/generated/
/stats.html

View File

@ -80,12 +80,14 @@ dependencies {
implementation("org.xerial:sqlite-jdbc:3.40.1.0")
implementation("io.ktor:ktor-server-websockets-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("io.insert-koin:koin-core:$koin_version")
implementation("io.insert-koin:koin-ktor:$koin_version")
implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
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")

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

@ -95,17 +95,15 @@ fun Application.parent() {
runBlocking {
inject<IServerInitialiseService>().value.init()
}
configureCompression()
configureHTTP()
configureStaticRouting()
configureMonitoring()
configureSerialization()
register(inject<IUserService>().value)
configureSecurity(
inject<IUserAuthService>().value,
inject<IMetaService>().value,
inject<IUserRepository>().value,
inject<IJwtService>().value,
inject<JwkProvider>().value,
inject<IMetaService>().value
)
configureRouting(
httpSignatureVerifyService = inject<HttpSignatureVerifyService>().value,
@ -114,6 +112,10 @@ fun Application.parent() {
activityPubUserService = inject<ActivityPubUserService>().value,
postService = inject<IPostApiService>().value,
userApiService = inject<IUserApiService>().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
protected constructor() : super()
@Suppress("LongParameterList")
constructor(
type: List<String> = emptyList(),
name: String,

View File

@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.JsonNode
import dev.usbharu.hideout.service.activitypub.ExtendedActivityVocabulary
class ObjectDeserializer : JsonDeserializer<Object>() {
@Suppress("LongMethod")
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Object {
requireNotNull(p)
val treeNode: JsonNode = requireNotNull(p.codec?.readTree(p))

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

@ -18,7 +18,7 @@ data class User(
) {
override fun toString(): String {
return "User(id=$id, name='$name', domain='$domain', screenName='$screenName', description='$description'," +
" password=****, inbox='$inbox', outbox='$outbox', url='$url', publicKey='$publicKey'," +
" privateKey=****, createdAt=$createdAt)"
" password=****, inbox='$inbox', outbox='$outbox', url='$url', publicKey='$publicKey'," +
" privateKey=****, createdAt=$createdAt)"
}
}

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
import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.routing.activitypub.inbox
import dev.usbharu.hideout.routing.activitypub.outbox
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.users
import dev.usbharu.hideout.routing.wellknown.webfinger
@ -11,6 +13,9 @@ import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
import dev.usbharu.hideout.service.api.IPostApiService
import dev.usbharu.hideout.service.api.IUserApiService
import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService
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 io.ktor.server.application.*
import io.ktor.server.plugins.autohead.*
@ -23,7 +28,11 @@ fun Application.configureRouting(
userService: IUserService,
activityPubUserService: ActivityPubUserService,
postService: IPostApiService,
userApiService: IUserApiService
userApiService: IUserApiService,
userAuthService: IUserAuthService,
userRepository: IUserRepository,
jwtService: IJwtService,
metaService: IMetaService
) {
install(AutoHeadResponse)
routing {
@ -34,6 +43,7 @@ fun Application.configureRouting(
route("/api/internal/v1") {
posts(postService)
users(userService, userApiService)
auth(userAuthService, userRepository, jwtService)
}
}
}

View File

@ -2,19 +2,12 @@ package dev.usbharu.hideout.plugins
import com.auth0.jwk.JwkProvider
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.user.IUserAuthService
import dev.usbharu.hideout.util.JsonWebKeyUtil
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.*
@ -22,11 +15,8 @@ const val TOKEN_AUTH = "jwt-auth"
@Suppress("MagicNumber")
fun Application.configureSecurity(
userAuthService: IUserAuthService,
metaService: IMetaService,
userRepository: IUserRepository,
jwtService: IJwtService,
jwkProvider: JwkProvider
jwkProvider: JwkProvider,
metaService: IMetaService
) {
val issuer = Config.configData.url
install(Authentication) {
@ -48,24 +38,6 @@ fun Application.configureSecurity(
}
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") {
//language=JSON
val jwt = metaService.getJwtMeta()
@ -74,12 +46,5 @@ fun Application.configureSecurity(
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?,
userId: Long?
): List<Post> {
TODO("Not yet implemented")
return query {
Posts.select { Posts.visibility eq Visibility.PUBLIC.ordinal }.map { it.toPost() }
}
}
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

@ -42,11 +42,11 @@ fun Route.users(userService: IUserService, userApiService: IUserApiService) {
authenticate(TOKEN_AUTH, optional = true) {
get {
val userParameter = (
call.parameters["name"]
?: throw ParameterNotExistException(
"Parameter(name='userName@domain') does not exist."
)
call.parameters["name"]
?: throw ParameterNotExistException(
"Parameter(name='userName@domain') does not exist."
)
)
if (userParameter.toLongOrNull() != null) {
return@get call.respond(userApiService.findById(userParameter.toLong()))
} else {
@ -92,11 +92,11 @@ fun Route.users(userService: IUserService, userApiService: IUserApiService) {
route("/following") {
get {
val userParameter = (
call.parameters["name"]
?: throw ParameterNotExistException(
"Parameter(name='userName@domain') does not exist."
)
call.parameters["name"]
?: throw ParameterNotExistException(
"Parameter(name='userName@domain') does not exist."
)
)
if (userParameter.toLongOrNull() != null) {
return@get call.respond(userApiService.findFollowings(userParameter.toLong()))
}

View File

@ -1,12 +1,12 @@
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
@Suppress("LongParameterList")
interface IPostApiService {
suspend fun createPost(postForm: dev.usbharu.hideout.domain.model.hideout.form.Post, userId: Long): Post
suspend fun getById(id: Long, 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?): PostResponse
suspend fun getAll(
since: Instant? = null,
until: Instant? = null,
@ -14,7 +14,7 @@ interface IPostApiService {
maxId: Long? = null,
limit: Int? = null,
userId: Long? = null
): List<Post>
): List<PostResponse>
suspend fun getByUser(
nameOrId: String,
@ -24,5 +24,5 @@ interface IPostApiService {
maxId: Long? = null,
limit: Int? = 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.domain.model.hideout.dto.PostCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.exception.PostNotFoundException
import dev.usbharu.hideout.repository.IPostRepository
import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse
import dev.usbharu.hideout.repository.*
import dev.usbharu.hideout.service.post.IPostService
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 java.time.Instant
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
class PostApiServiceImpl(
private val postService: IPostService,
private val postRepository: IPostRepository
private val postRepository: IPostRepository,
private val userRepository: IUserRepository
) : IPostApiService {
override suspend fun createPost(postForm: FormPost, userId: Long): Post {
return postService.createLocal(
override suspend fun createPost(postForm: FormPost, userId: Long): PostResponse {
val createdPost = postService.createLocal(
PostCreateDto(
text = postForm.text,
overview = postForm.overview,
@ -27,11 +33,20 @@ class PostApiServiceImpl(
userId = userId
)
)
val creator = userRepository.findById(userId)
return PostResponse.from(createdPost, creator!!)
}
override suspend fun getById(id: Long, userId: Long?): Post {
return postRepository.findOneById(id, userId)
?: throw PostNotFoundException("$id was not found or is not authorized.")
@Suppress("InjectDispatcher")
suspend fun <T> query(block: suspend () -> T): T =
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(
@ -41,7 +56,12 @@ class PostApiServiceImpl(
maxId: Long?,
limit: Int?,
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(
nameOrId: String,
@ -51,23 +71,22 @@ class PostApiServiceImpl(
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post> {
): List<PostResponse> {
val idOrNull = nameOrId.toLongOrNull()
return if (idOrNull == null) {
val acct = AcctUtil.parse(nameOrId)
postRepository.findByUserNameAndDomain(
username = acct.username,
s = acct.domain
?: Config.configData.domain,
since = since,
until = until,
minId = minId,
maxId = maxId,
limit = limit,
userId = userId
)
query {
Posts.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id }).select {
Users.name.eq(acct.username)
.and(Users.domain eq (acct.domain ?: Config.configData.domain))
}.map { PostResponse.from(it.toPost(), it.toUser()) }
}
} 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> {
return query {
jobs.select(

View File

@ -20,7 +20,7 @@ paths:
schema:
type: array
items:
$ref: "#/components/schemas/Post"
$ref: "#/components/schemas/PostResponse"
401:
$ref: "#/components/responses/Unauthorized"
403:
@ -37,7 +37,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/Post"
$ref: "#/components/schemas/PostRequest"
responses:
200:
description: 成功
@ -65,7 +65,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/Post"
$ref: "#/components/schemas/PostResponse"
401:
$ref: "#/components/responses/Unauthorized"
403:
@ -90,7 +90,7 @@ paths:
schema:
type: array
items:
$ref: "#/components/schemas/Post"
$ref: "#/components/schemas/PostResponse"
401:
$ref: "#/components/responses/Unauthorized"
403:
@ -114,7 +114,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/Post"
$ref: "#/components/schemas/PostResponse"
401:
$ref: "#/components/responses/Unauthorized"
403:
@ -137,7 +137,7 @@ paths:
schema:
type: array
items:
$ref: "#/components/schemas/User"
$ref: "#/components/schemas/UserResponse"
post:
summary: ユーザーを作成する
@ -181,7 +181,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/User"
$ref: "#/components/schemas/UserResponse"
404:
$ref: "#/components/responses/NotFound"
@ -198,7 +198,7 @@ paths:
schema:
type: array
items:
$ref: "#/components/schemas/User"
$ref: "#/components/schemas/UserResponse"
post:
summary: ユーザーをフォローする
security:
@ -228,7 +228,47 @@ paths:
schema:
type: array
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:
responses:
@ -261,8 +301,23 @@ components:
type: string
schemas:
User:
Visibility:
type: string
enum:
- public
- unlisted
- followers
- direct
UserResponse:
type: object
required:
- id
- name
- domain
- screenName
- description
- url
- createdAt
properties:
id:
type: number
@ -277,23 +332,30 @@ components:
type: string
description:
type: string
nullable: true
url:
type: string
readOnly: true
createdAt:
type: number
readOnly: true
Post:
PostResponse:
type: object
required:
- id
- user
- text
- createdAt
- visibility
- url
- sensitive
properties:
id:
type: integer
format: int64
readOnly: true
userId:
type: integer
format: int64
readOnly: true
user:
$ref: "#/components/schemas/UserResponse"
overview:
type: string
text:
@ -303,12 +365,7 @@ components:
format: int64
readOnly: true
visibility:
type: string
enum:
- public
- unlisted
- followers
- direct
$ref: "#/components/schemas/Visibility"
url:
type: string
format: uri
@ -323,13 +380,49 @@ components:
readOnly: true
sensitive:
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:
BearerAuth:
type: http
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 = () => {
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) => {
console.log(form)
}
createEffect(() => {
setApi(
new DefaultApi(new Configuration({
basePath: window.location.origin + "/api/internal/v1",
accessToken : cookie.token as string
})))
})
const [username, setUsername] = createSignal("")
const [password, setPassword] = createSignal("")
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))
})
const theme = createTheme({
palette: {
mode: prefersDarkMode() ? 'dark' : 'light',
}
}>
<input name="username" type="text" placeholder="Username" required
onChange={(e) => setUsername(e.currentTarget.value)}/>
<input name="password" type="password" placeholder="Password" required
onChange={(e) => setPassword(e.currentTarget.value)}/>
<button type="submit">Submit</button>
</form>
})
return (
<ApiProvider api={api()}>
<ThemeProvider theme={theme}>
<CssBaseline/>
<Router>
<Routes>
<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.exception.InvalidRefreshTokenException
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.core.IMetaService
import dev.usbharu.hideout.service.user.IUserAuthService
@ -24,6 +25,7 @@ import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.config.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.anyString
@ -70,7 +72,10 @@ class SecurityKtTest {
val jwkProvider = mock<JwkProvider>()
application {
configureSerialization()
configureSecurity(userAuthService, metaService, userRepository, jwtService, jwkProvider)
configureSecurity(jwkProvider, metaService)
routing {
auth(userAuthService, userRepository, jwtService)
}
}
client.post("/login") {
@ -97,7 +102,10 @@ class SecurityKtTest {
val jwkProvider = mock<JwkProvider>()
application {
configureSerialization()
configureSecurity(userAuthService, metaService, userRepository, jwtService, jwkProvider)
configureSecurity(jwkProvider, metaService)
routing {
auth(userAuthService, userRepository, jwtService)
}
}
client.post("/login") {
contentType(ContentType.Application.Json)
@ -122,7 +130,10 @@ class SecurityKtTest {
val jwkProvider = mock<JwkProvider>()
application {
configureSerialization()
configureSecurity(userAuthService, metaService, userRepository, jwtService, jwkProvider)
configureSecurity(jwkProvider, metaService)
routing {
auth(userAuthService, userRepository, jwtService)
}
}
client.post("/login") {
contentType(ContentType.Application.Json)
@ -140,7 +151,10 @@ class SecurityKtTest {
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
configureSecurity(mock(), mock())
routing {
auth(mock(), mock(), mock())
}
}
client.get("/auth-check").apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
@ -155,7 +169,10 @@ class SecurityKtTest {
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
configureSecurity(mock(), mock())
routing {
auth(mock(), mock(), mock())
}
}
client.get("/auth-check") {
header("Authorization", "Digest dsfjjhogalkjdfmlhaog")
@ -172,7 +189,10 @@ class SecurityKtTest {
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
configureSecurity(mock(), mock())
routing {
auth(mock(), mock(), mock())
}
}
client.get("/auth-check") {
header("Authorization", "")
@ -190,7 +210,10 @@ class SecurityKtTest {
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
configureSecurity(mock(), mock())
routing {
auth(mock(), mock(), mock())
}
}
client.get("/auth-check") {
header("Authorization", "Bearer ")
@ -244,11 +267,12 @@ class SecurityKtTest {
)
)
}
val userRepository = mock<IUserRepository>()
val jwtService = mock<IJwtService>()
application {
configureSerialization()
configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider)
configureSecurity(jwkProvider, metaService)
routing {
auth(mock(), mock(), mock())
}
}
client.get("/auth-check") {
@ -304,11 +328,12 @@ class SecurityKtTest {
)
)
}
val userRepository = mock<IUserRepository>()
val jwtService = mock<IJwtService>()
application {
configureSerialization()
configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider)
configureSecurity(jwkProvider, metaService)
routing {
auth(mock(), mock(), mock())
}
}
client.get("/auth-check") {
header("Authorization", "Bearer $token")
@ -362,11 +387,12 @@ class SecurityKtTest {
)
)
}
val userRepository = mock<IUserRepository>()
val jwtService = mock<IJwtService>()
application {
configureSerialization()
configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider)
configureSecurity(jwkProvider, metaService)
routing {
auth(mock(), mock(), mock())
}
}
client.get("/auth-check") {
header("Authorization", "Bearer $token")
@ -420,11 +446,12 @@ class SecurityKtTest {
)
)
}
val userRepository = mock<IUserRepository>()
val jwtService = mock<IJwtService>()
application {
configureSerialization()
configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider)
configureSecurity(jwkProvider, metaService)
routing {
auth(mock(), mock(), mock())
}
}
client.get("/auth-check") {
header("Authorization", "Bearer $token")
@ -477,11 +504,12 @@ class SecurityKtTest {
)
)
}
val userRepository = mock<IUserRepository>()
val jwtService = mock<IJwtService>()
application {
configureSerialization()
configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider)
configureSecurity(jwkProvider, metaService)
routing {
auth(mock(), mock(), mock())
}
}
client.get("/auth-check") {
header("Authorization", "Bearer $token")
@ -501,7 +529,10 @@ class SecurityKtTest {
}
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), jwtService, mock())
configureSecurity(mock(), mock())
routing {
auth(mock(), mock(), jwtService)
}
}
client.post("/refresh-token") {
header("Content-Type", "application/json")
@ -523,7 +554,10 @@ class SecurityKtTest {
application {
configureStatusPages()
configureSerialization()
configureSecurity(mock(), mock(), mock(), jwtService, mock())
configureSecurity(mock(), mock())
routing {
auth(mock(), mock(), jwtService)
}
}
client.post("/refresh-token") {
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.fasterxml.jackson.module.kotlin.readValue
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.Visibility
import dev.usbharu.hideout.plugins.TOKEN_AUTH
@ -32,18 +34,27 @@ class PostsTest {
environment {
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(
Post(
PostResponse(
id = 12345,
userId = 4321,
user = user,
text = "test1",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1"
),
Post(
PostResponse(
id = 123456,
userId = 4322,
user = user,
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
@ -64,7 +75,7 @@ class PostsTest {
}
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
@ -89,27 +100,35 @@ class PostsTest {
val payload = mock<Payload> {
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(
Post(
PostResponse(
id = 12345,
userId = 4321,
user = user,
text = "test1",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1"
),
Post(
PostResponse(
id = 123456,
userId = 4322,
user = user,
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2"
),
Post(
PostResponse(
id = 1234567,
userId = 4333,
user = user,
text = "Followers only",
visibility = Visibility.FOLLOWERS,
createdAt = Instant.now().toEpochMilli(),
@ -156,9 +175,18 @@ class PostsTest {
environment {
config = ApplicationConfig("empty.conf")
}
val post = Post(
12345,
1234,
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 post = PostResponse(
id = 12345,
user = user,
text = "aaa",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
@ -169,7 +197,7 @@ class PostsTest {
}
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
@ -187,9 +215,17 @@ class PostsTest {
environment {
config = ApplicationConfig("empty.conf")
}
val post = Post(
val post = PostResponse(
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",
visibility = Visibility.FOLLOWERS,
createdAt = Instant.now().toEpochMilli(),
@ -242,14 +278,22 @@ class PostsTest {
onBlocking { createPost(any(), any()) } doAnswer {
val argument = it.getArgument<dev.usbharu.hideout.domain.model.hideout.form.Post>(0)
val userId = it.getArgument<Long>(1)
Post(
123L,
userId,
null,
argument.text,
Instant.now().toEpochMilli(),
Visibility.PUBLIC,
"https://example.com"
PostResponse(
id = 123L,
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()
),
overview = null,
text = argument.text,
createdAt = Instant.now().toEpochMilli(),
visibility = Visibility.PUBLIC,
url = "https://example.com"
)
}
}
@ -290,18 +334,27 @@ class PostsTest {
environment {
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(
Post(
PostResponse(
id = 12345,
userId = 1,
user = user,
text = "test1",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1"
),
Post(
PostResponse(
id = 123456,
userId = 1,
user = user,
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
@ -323,7 +376,7 @@ class PostsTest {
}
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
@ -342,18 +395,27 @@ class PostsTest {
environment {
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(
Post(
PostResponse(
id = 12345,
userId = 1,
user = user,
text = "test1",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1"
),
Post(
PostResponse(
id = 123456,
userId = 1,
user = user,
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
@ -375,7 +437,7 @@ class PostsTest {
}
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
@ -394,18 +456,27 @@ class PostsTest {
environment {
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(
Post(
PostResponse(
id = 12345,
userId = 1,
user = user,
text = "test1",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1"
),
Post(
PostResponse(
id = 123456,
userId = 1,
user = user,
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
@ -427,7 +498,7 @@ class PostsTest {
}
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
@ -446,18 +517,27 @@ class PostsTest {
environment {
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(
Post(
PostResponse(
id = 12345,
userId = 1,
user = user,
text = "test1",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1"
),
Post(
PostResponse(
id = 123456,
userId = 1,
user = user,
text = "test2",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
@ -479,7 +559,7 @@ class PostsTest {
}
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
@ -498,9 +578,17 @@ class PostsTest {
environment {
config = ApplicationConfig("empty.conf")
}
val post = Post(
val post = PostResponse(
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",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
@ -511,7 +599,7 @@ class PostsTest {
}
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
@ -530,9 +618,17 @@ class PostsTest {
environment {
config = ApplicationConfig("empty.conf")
}
val post = Post(
val post = PostResponse(
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",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
@ -543,7 +639,7 @@ class PostsTest {
}
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
@ -562,9 +658,17 @@ class PostsTest {
environment {
config = ApplicationConfig("empty.conf")
}
val post = Post(
val post = PostResponse(
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",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
@ -575,7 +679,7 @@ class PostsTest {
}
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)
@ -594,9 +698,17 @@ class PostsTest {
environment {
config = ApplicationConfig("empty.conf")
}
val post = Post(
val post = PostResponse(
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",
visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(),
@ -607,7 +719,7 @@ class PostsTest {
}
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
configureSecurity(mock(), mock())
routing {
route("/api/internal/v1") {
posts(postService)

View File

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

View File

@ -48,13 +48,21 @@ class ActivityPubReceiveFollowServiceImplTest {
firstValue.invoke(scheduleContext, ReceiveFollowJob)
val actor = scheduleContext.props.props[ReceiveFollowJob.actor.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://example.com", targetActor)
//language=JSON
assertEquals(
"""{"type":"Follow","name":"Follow","actor":"https://follower.example.com","object":"https://example.com","@context":null}""",
follow
Json.parseToJsonElement(
"""{
"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>(
ReceiveFollowJob.actor.name to "https://follower.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
)

View File

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