Compare commits

..

No commits in common. "2651545c1706336f05509b8f9ad15f6f76d76369" and "a32cfe348efd39d6b60d8a092acc2fedf28291ec" have entirely different histories.

11 changed files with 33 additions and 229 deletions

View File

@ -55,7 +55,11 @@ tasks.create<GenerateTask>("openApiGenerateMastodonCompatibleApi", GenerateTask:
modelPackage.set("dev.usbharu.hideout.domain.mastodon.model.generated")
configOptions.put("interfaceOnly", "true")
configOptions.put("useSpringBoot3", "true")
configOptions.put("reactive", "true")
additionalProperties.put("useTags", "true")
// importMappings.putAll(mapOf("ReactionResponse" to "ReactionResponse"))
// typeMappings.putAll(mapOf("ReactionResponse" to "ReactionResponse"))
}
repositories {
@ -104,6 +108,8 @@ dependencies {
implementation("org.springframework.data:spring-data-commons")
implementation("org.springframework.boot:spring-boot-starter-jdbc")
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
testImplementation("org.springframework.boot:spring-boot-test-autoconfigure")
testImplementation("org.springframework.boot:spring-boot-starter-test")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")

View File

@ -12,6 +12,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.Order
import org.springframework.http.MediaType
import org.springframework.security.config.Customizer
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
@ -27,6 +28,7 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher
import org.springframework.web.servlet.handler.HandlerMappingIntrospector
import java.security.KeyPairGenerator
import java.security.interfaces.RSAPrivateKey
@ -45,8 +47,9 @@ class SecurityConfig {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
http
.exceptionHandling {
it.authenticationEntryPoint(
LoginUrlAuthenticationEntryPoint("/login")
it.defaultAuthenticationEntryPointFor(
LoginUrlAuthenticationEntryPoint("/login"),
MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
}
.oauth2ResourceServer {
@ -55,33 +58,34 @@ class SecurityConfig {
return http.build()
}
@Bean
@Order(2)
fun defaultSecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
val builder = MvcRequestMatcher.Builder(introspector)
http.authorizeHttpRequests {
it.requestMatchers(builder.pattern("/api/v1/**")).hasAnyAuthority("SCOPE_read", "SCOPE_read:accounts")
}
http
.authorizeHttpRequests {
it.requestMatchers(PathRequest.toH2Console()).permitAll()
it.requestMatchers(
builder.pattern("/inbox"),
builder.pattern("/api/v1/apps"),
builder.pattern("/api/v1/instance/**"),
builder.pattern("/.well-known/**"),
builder.pattern("/error"),
builder.pattern("/nodeinfo/2.0")
builder.pattern("/api/v1/instance/**")
).permitAll()
it.requestMatchers(builder.pattern("/change-password")).authenticated()
it.requestMatchers(builder.pattern("/api/v1/accounts/verify_credentials"))
.hasAnyAuthority("SCOPE_read", "SCOPE_read:accounts")
it.anyRequest().permitAll()
}
http
.authorizeHttpRequests {
it.requestMatchers(PathRequest.toH2Console()).permitAll()
}
http
.authorizeHttpRequests {
it.anyRequest().authenticated()
}
http
.oauth2ResourceServer {
it.jwt(Customizer.withDefaults())
}
.passwordManagement { }
.formLogin(Customizer.withDefaults())
.csrf {
it.ignoringRequestMatchers(builder.pattern("/api/**"))

View File

@ -3,7 +3,6 @@ package dev.usbharu.hideout.controller.mastodon
import dev.usbharu.hideout.controller.mastodon.generated.AccountApi
import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccount
import dev.usbharu.hideout.service.api.mastodon.AccountApiService
import kotlinx.coroutines.runBlocking
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.core.context.SecurityContextHolder
@ -12,10 +11,10 @@ import org.springframework.stereotype.Controller
@Controller
class MastodonAccountApiController(private val accountApiService: AccountApiService) : AccountApi {
override fun apiV1AccountsVerifyCredentialsGet(): ResponseEntity<CredentialAccount> = runBlocking {
override suspend fun apiV1AccountsVerifyCredentialsGet(): ResponseEntity<CredentialAccount> {
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
ResponseEntity(
return ResponseEntity(
accountApiService.verifyCredentials(principal.getClaim<String>("uid").toLong()),
HttpStatus.OK
)

View File

@ -4,7 +4,6 @@ import dev.usbharu.hideout.controller.mastodon.generated.AppApi
import dev.usbharu.hideout.domain.mastodon.model.generated.Application
import dev.usbharu.hideout.domain.mastodon.model.generated.AppsRequest
import dev.usbharu.hideout.service.api.mastodon.AppApiService
import kotlinx.coroutines.runBlocking
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
@ -14,9 +13,9 @@ import org.springframework.web.bind.annotation.RequestParam
@Controller
class MastodonAppsApiController(private val appApiService: AppApiService) : AppApi {
override fun apiV1AppsPost(appsRequest: AppsRequest): ResponseEntity<Application> = runBlocking {
override suspend fun apiV1AppsPost(appsRequest: AppsRequest): ResponseEntity<Application> {
println(appsRequest)
ResponseEntity(
return ResponseEntity(
appApiService.createApp(appsRequest),
HttpStatus.OK
)
@ -28,10 +27,10 @@ class MastodonAppsApiController(private val appApiService: AppApiService) : AppA
produces = ["application/json"],
consumes = ["application/x-www-form-urlencoded"]
)
fun apiV1AppsPost(@RequestParam map: Map<String, String>): ResponseEntity<Application> = runBlocking {
suspend fun apiV1AppsPost(@RequestParam map: Map<String, String>): ResponseEntity<Application> {
val appsRequest =
AppsRequest(map.getValue("client_name"), map.getValue("redirect_uris"), map["scopes"], map["website"])
ResponseEntity(
return ResponseEntity(
appApiService.createApp(appsRequest),
HttpStatus.OK
)

View File

@ -3,14 +3,13 @@ package dev.usbharu.hideout.controller.mastodon
import dev.usbharu.hideout.controller.mastodon.generated.InstanceApi
import dev.usbharu.hideout.domain.mastodon.model.generated.V1Instance
import dev.usbharu.hideout.service.api.mastodon.InstanceApiService
import kotlinx.coroutines.runBlocking
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
@Controller
class MastodonInstanceApiController(private val instanceApiService: InstanceApiService) : InstanceApi {
override fun apiV1InstanceGet(): ResponseEntity<V1Instance> = runBlocking {
ResponseEntity(instanceApiService.v1Instance(), HttpStatus.OK)
override suspend fun apiV1InstanceGet(): ResponseEntity<V1Instance> {
return ResponseEntity(instanceApiService.v1Instance(), HttpStatus.OK)
}
}

View File

@ -5,7 +5,6 @@ import dev.usbharu.hideout.domain.mastodon.model.generated.Status
import dev.usbharu.hideout.domain.mastodon.model.generated.StatusesRequest
import dev.usbharu.hideout.domain.model.UserDetailsImpl
import dev.usbharu.hideout.service.api.mastodon.StatusesApiService
import kotlinx.coroutines.runBlocking
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.core.context.SecurityContextHolder
@ -13,9 +12,9 @@ import org.springframework.stereotype.Controller
@Controller
class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiService) : StatusApi {
override fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity<Status> = runBlocking {
override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity<Status> {
val principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal()
require(principal is UserDetailsImpl)
ResponseEntity(statusesApiService.postStatus(statusesRequest, principal), HttpStatus.OK)
return ResponseEntity(statusesApiService.postStatus(statusesRequest, principal), HttpStatus.OK)
}
}

View File

@ -1,42 +0,0 @@
package dev.usbharu.hideout.controller.wellknown
import dev.usbharu.hideout.config.ApplicationConfig
import org.intellij.lang.annotations.Language
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class HostMetaController(private val applicationConfig: ApplicationConfig) {
val xml = //language=XML
"""<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Link rel="lrdd" type="application/xrd+xml"
template="${applicationConfig.url}/.well-known/webfinger?resource={uri}"/>
</XRD>"""
@Language("JSON")
val json = """{
"links": [
{
"rel": "lrdd",
"type": "application/jrd+json",
"template": "${applicationConfig.url}/.well-known/webfinger?resource={uri}"
}
]
}"""
@GetMapping("/.well-known/host-meta", produces = ["application/xml"])
fun hostmeta(): ResponseEntity<String> {
return ResponseEntity(xml, HttpStatus.OK)
}
@GetMapping("/.well-known/host-meta.json", produces = ["application/json"])
fun hostmetJson(): ResponseEntity<String> {
return ResponseEntity(json, HttpStatus.OK)
}
}

View File

@ -1,64 +0,0 @@
package dev.usbharu.hideout.controller.wellknown
import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.domain.model.wellknown.Nodeinfo
import dev.usbharu.hideout.domain.model.wellknown.Nodeinfo2_0
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class NodeinfoController(private val applicationConfig: ApplicationConfig) {
@GetMapping("/.well-known/nodeinfo")
fun nodeinfo(): ResponseEntity<Nodeinfo> {
return ResponseEntity(
Nodeinfo(
listOf(
Nodeinfo.Links(
"http://nodeinfo.diaspora.software/ns/schema/2.0",
"${applicationConfig.url}/nodeinfo/2.0"
)
)
), HttpStatus.OK
)
}
@GetMapping("/nodeinfo/2.0")
fun nodeinfo2_0(): ResponseEntity<Nodeinfo2_0> {
return ResponseEntity(
Nodeinfo2_0(
version = "2.0",
software = Nodeinfo2_0.Software(
name = "hideout",
version = "0.0.1"
),
protocols = listOf("activitypub"),
services = Nodeinfo2_0.Services(
inbound = emptyList(),
outbound = emptyList()
),
openRegistrations = false,
usage = Nodeinfo2_0.Usage(
users = Nodeinfo2_0.Usage.Users(
total = 1,
activeHalfYear = 1,
activeMonth = 1
),
localPosts = 1,
localComments = 0
),
metadata = Nodeinfo2_0.Metadata(
nodeName = "hideout",
nodeDescription = "hideout test server",
maintainer = Nodeinfo2_0.Metadata.Maintainer("usbharu", "i@usbharu.dev"),
langs = emptyList(),
tosUrl = "",
repositoryUrl = "https://github.com/usbharu/Hideout",
feedbackUrl = "https://github.com/usbharu/Hideout/issues/new/choose",
)
),
HttpStatus.OK
)
}
}

View File

@ -1,37 +0,0 @@
package dev.usbharu.hideout.controller.wellknown
import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.domain.model.wellknown.WebFinger
import dev.usbharu.hideout.service.api.WebFingerApiService
import dev.usbharu.hideout.util.AcctUtil
import kotlinx.coroutines.runBlocking
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import java.net.URL
@Controller
class WebFingerController(
private val webFingerApiService: WebFingerApiService,
private val applicationConfig: ApplicationConfig
) {
@GetMapping("/.well-known/webfinger")
fun webfinger(@RequestParam("resource") resource: String): ResponseEntity<WebFinger> = runBlocking {
val acct = AcctUtil.parse(resource.replace("acct:", ""))
val user =
webFingerApiService.findByNameAndDomain(acct.username, acct.domain ?: URL(applicationConfig.url).host)
val webFinger = WebFinger(
"acct:${user.name}@${user.domain}",
listOf(
WebFinger.Link(
"self",
"application/activity+json",
applicationConfig.url + "/users/" + user.id
)
)
)
ResponseEntity(webFinger, HttpStatus.OK)
}
}

View File

@ -1,11 +0,0 @@
package dev.usbharu.hideout.domain.model.wellknown
data class Nodeinfo(
val links: List<Links>
) {
data class Links(
val rel: String,
val href: String
)
}

View File

@ -1,48 +0,0 @@
package dev.usbharu.hideout.domain.model.wellknown
data class Nodeinfo2_0(
val version: String,
val software: Software,
val protocols: List<String>,
val services: Services,
val openRegistrations: Boolean,
val usage: Usage,
val metadata: Metadata
) {
data class Software(
val name: String,
val version: String
)
data class Services(
val inbound: List<String>,
val outbound: List<String>
)
data class Usage(
val users: Users,
val localPosts: Int,
val localComments: Int
) {
data class Users(
val total: Int,
val activeHalfYear: Int,
val activeMonth: Int
)
}
data class Metadata(
val nodeName: String,
val nodeDescription: String,
val maintainer: Maintainer,
val langs: List<String>,
val tosUrl: String,
val repositoryUrl: String,
val feedbackUrl: String,
) {
data class Maintainer(
val name: String,
val email: String
)
}
}