mirror of https://github.com/usbharu/Hideout.git
feat: TwidereでOAuth2ログインができるように
This commit is contained in:
parent
834e40894b
commit
43a914331f
|
@ -15,7 +15,7 @@ plugins {
|
|||
id("org.graalvm.buildtools.native") version "0.9.21"
|
||||
id("io.gitlab.arturbosch.detekt") version "1.23.1"
|
||||
id("com.google.devtools.ksp") version "1.8.21-1.0.11"
|
||||
id("org.springframework.boot") version "3.1.2"
|
||||
id("org.springframework.boot") version "3.1.3"
|
||||
kotlin("plugin.spring") version "1.8.21"
|
||||
id("org.openapi.generator") version "7.0.1"
|
||||
// id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"
|
||||
|
@ -153,6 +153,7 @@ dependencies {
|
|||
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")
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
ksp("io.insert-koin:koin-ksp-compiler:1.2.0")
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
|
@ -174,6 +175,7 @@ dependencies {
|
|||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
implementation("org.springframework.security:spring-security-oauth2-jose")
|
||||
|
||||
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
||||
implementation("io.ktor:ktor-server-host-common-jvm:$ktor_version")
|
||||
|
|
|
@ -5,47 +5,57 @@ import com.nimbusds.jose.jwk.RSAKey
|
|||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet
|
||||
import com.nimbusds.jose.jwk.source.JWKSource
|
||||
import com.nimbusds.jose.proc.SecurityContext
|
||||
import dev.usbharu.hideout.domain.model.UserDetailsImpl
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
|
||||
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
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer
|
||||
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
|
||||
import java.security.interfaces.RSAPublicKey
|
||||
import java.util.*
|
||||
|
||||
@EnableWebSecurity
|
||||
|
||||
@EnableWebSecurity(debug = true)
|
||||
@Configuration
|
||||
class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
fun oauth2SecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
fun oauth2SecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
|
||||
val builder = MvcRequestMatcher.Builder(introspector)
|
||||
|
||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
|
||||
http
|
||||
.exceptionHandling {
|
||||
it.authenticationEntryPoint(LoginUrlAuthenticationEntryPoint("/login"))
|
||||
it.defaultAuthenticationEntryPointFor(
|
||||
LoginUrlAuthenticationEntryPoint("/login"),
|
||||
MediaTypeRequestMatcher(MediaType.TEXT_HTML)
|
||||
)
|
||||
}
|
||||
.oauth2ResourceServer {
|
||||
it.jwt(Customizer.withDefaults())
|
||||
}
|
||||
.csrf {
|
||||
it.disable()
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
|
||||
|
@ -54,7 +64,9 @@ class SecurityConfig {
|
|||
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(
|
||||
|
@ -63,12 +75,18 @@ class SecurityConfig {
|
|||
builder.pattern("/api/v1/instance/**")
|
||||
).permitAll()
|
||||
}
|
||||
http
|
||||
.authorizeHttpRequests {
|
||||
it.requestMatchers(PathRequest.toH2Console()).permitAll()
|
||||
}
|
||||
http
|
||||
.authorizeHttpRequests {
|
||||
it.anyRequest().authenticated()
|
||||
}
|
||||
http
|
||||
.oauth2ResourceServer {
|
||||
it.jwt(Customizer.withDefaults())
|
||||
}
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.csrf {
|
||||
it.ignoringRequestMatchers(builder.pattern("/api/**"))
|
||||
|
@ -127,6 +145,18 @@ class SecurityConfig {
|
|||
.tokenRevocationEndpoint("/oauth/revoke")
|
||||
.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun jwtTokenCustomizer(): OAuth2TokenCustomizer<JwtEncodingContext> {
|
||||
return OAuth2TokenCustomizer { context: JwtEncodingContext ->
|
||||
if (OAuth2TokenType.ACCESS_TOKEN == context.tokenType) {
|
||||
val userDetailsImpl = context.getPrincipal<Authentication>().principal as UserDetailsImpl
|
||||
context.claims.claim("uid", userDetailsImpl.id.toString())
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ConfigurationProperties("hideout.security.jwt")
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
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 org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.security.oauth2.jwt.Jwt
|
||||
import org.springframework.stereotype.Controller
|
||||
|
||||
@Controller
|
||||
class MastodonAccountApiController(private val accountApiService: AccountApiService) : AccountApi {
|
||||
override suspend fun apiV1AccountsVerifyCredentialsGet(): ResponseEntity<CredentialAccount> {
|
||||
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
|
||||
|
||||
return ResponseEntity(
|
||||
accountApiService.verifyCredentials(principal.getClaim<String>("uid").toLong()),
|
||||
HttpStatus.OK
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,18 @@
|
|||
package dev.usbharu.hideout.domain.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import java.io.Serial
|
||||
|
||||
|
@ -18,4 +30,54 @@ class UserDetailsImpl(
|
|||
@Serial
|
||||
private const val serialVersionUID: Long = -899168205656607781L
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
|
||||
@JsonDeserialize(using = UserDetailsDeserializer::class)
|
||||
@JsonAutoDetect(
|
||||
fieldVisibility = JsonAutoDetect.Visibility.ANY,
|
||||
getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
creatorVisibility = JsonAutoDetect.Visibility.NONE
|
||||
)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonSubTypes
|
||||
abstract class UserDetailsMixin
|
||||
|
||||
|
||||
class UserDetailsDeserializer : JsonDeserializer<UserDetailsImpl>() {
|
||||
val SIMPLE_GRANTED_AUTHORITY_SET = object : TypeReference<Set<SimpleGrantedAuthority>>() {}
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): UserDetailsImpl {
|
||||
|
||||
val mapper = p.codec as ObjectMapper
|
||||
val jsonNode: JsonNode = mapper.readTree(p)
|
||||
println(jsonNode)
|
||||
val authorities: Set<GrantedAuthority> = mapper.convertValue(
|
||||
jsonNode["authorities"],
|
||||
SIMPLE_GRANTED_AUTHORITY_SET
|
||||
)
|
||||
|
||||
val password = jsonNode.readText("password")
|
||||
return UserDetailsImpl(
|
||||
jsonNode["id"].longValue(),
|
||||
jsonNode.readText("username"),
|
||||
password,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
authorities.toMutableList(),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
fun JsonNode.readText(field: String, defaultValue: String = ""): String {
|
||||
return when {
|
||||
has(field) -> get(field).asText(defaultValue)
|
||||
else -> defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package dev.usbharu.hideout.service.api.mastodon
|
||||
|
||||
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
|
||||
import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccount
|
||||
import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccountSource
|
||||
import dev.usbharu.hideout.domain.mastodon.model.generated.Role
|
||||
import dev.usbharu.hideout.service.core.Transaction
|
||||
import dev.usbharu.hideout.service.mastodon.AccountService
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
interface AccountApiService {
|
||||
suspend fun verifyCredentials(userid: Long): CredentialAccount
|
||||
}
|
||||
|
||||
|
||||
@Service
|
||||
class AccountApiServiceImpl(private val accountService: AccountService, private val transaction: Transaction) :
|
||||
AccountApiService {
|
||||
override suspend fun verifyCredentials(userid: Long): CredentialAccount = transaction.transaction {
|
||||
val account = accountService.findById(userid)
|
||||
of(account)
|
||||
}
|
||||
|
||||
private fun of(account: Account): CredentialAccount {
|
||||
return CredentialAccount(
|
||||
id = account.id,
|
||||
username = account.username,
|
||||
acct = account.acct,
|
||||
url = account.url,
|
||||
displayName = account.displayName,
|
||||
note = account.note,
|
||||
avatar = account.avatar,
|
||||
avatarStatic = account.avatarStatic,
|
||||
header = account.header,
|
||||
headerStatic = account.headerStatic,
|
||||
locked = account.locked,
|
||||
fields = account.fields,
|
||||
emojis = account.emojis,
|
||||
bot = account.bot,
|
||||
group = account.group,
|
||||
discoverable = account.discoverable,
|
||||
createdAt = account.createdAt,
|
||||
lastStatusAt = account.lastStatusAt,
|
||||
statusesCount = account.statusesCount,
|
||||
followersCount = account.followersCount,
|
||||
noindex = account.noindex,
|
||||
moved = account.moved,
|
||||
suspendex = account.suspendex,
|
||||
limited = account.limited,
|
||||
followingCount = account.followingCount,
|
||||
source = CredentialAccountSource(
|
||||
account.note,
|
||||
account.fields,
|
||||
CredentialAccountSource.Privacy.public,
|
||||
false,
|
||||
0
|
||||
),
|
||||
role = Role(0, "Admin", "", 32)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -3,12 +3,15 @@ package dev.usbharu.hideout.service.auth
|
|||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import dev.usbharu.hideout.domain.model.UserDetailsImpl
|
||||
import dev.usbharu.hideout.domain.model.UserDetailsMixin
|
||||
import dev.usbharu.hideout.service.core.Transaction
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.javatime.timestamp
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.springframework.security.jackson2.CoreJackson2Module
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules
|
||||
import org.springframework.security.oauth2.core.*
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames
|
||||
|
@ -53,7 +56,7 @@ class ExposedOAuth2AuthorizationService(
|
|||
it[registeredClientId] = authorization.registeredClientId
|
||||
it[principalName] = authorization.principalName
|
||||
it[authorizationGrantType] = authorization.authorizationGrantType.value
|
||||
it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() }
|
||||
it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isNotEmpty() }
|
||||
it[attributes] = mapToJson(authorization.attributes)
|
||||
it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE)
|
||||
it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue
|
||||
|
@ -66,7 +69,8 @@ class ExposedOAuth2AuthorizationService(
|
|||
it[accessTokenExpiresAt] = accessToken?.token?.expiresAt
|
||||
it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> mapToJson(it1) }
|
||||
it[accessTokenType] = accessToken?.token?.tokenType?.value
|
||||
it[accessTokenScopes] = accessToken?.run { token.scopes.joinToString(",").takeIf { it.isEmpty() } }
|
||||
it[accessTokenScopes] =
|
||||
accessToken?.run { token.scopes.joinToString(",").takeIf { it.isNotEmpty() } }
|
||||
it[refreshTokenValue] = refreshToken?.token?.tokenValue
|
||||
it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt
|
||||
it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt
|
||||
|
@ -95,7 +99,7 @@ class ExposedOAuth2AuthorizationService(
|
|||
it[registeredClientId] = authorization.registeredClientId
|
||||
it[principalName] = authorization.principalName
|
||||
it[authorizationGrantType] = authorization.authorizationGrantType.value
|
||||
it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() }
|
||||
it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isNotEmpty() }
|
||||
it[attributes] = mapToJson(authorization.attributes)
|
||||
it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE)
|
||||
it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue
|
||||
|
@ -108,7 +112,7 @@ class ExposedOAuth2AuthorizationService(
|
|||
it[accessTokenExpiresAt] = accessToken?.token?.expiresAt
|
||||
it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> mapToJson(it1) }
|
||||
it[accessTokenType] = accessToken?.token?.tokenType?.value
|
||||
it[accessTokenScopes] = accessToken?.token?.scopes?.joinToString(",")?.takeIf { it.isEmpty() }
|
||||
it[accessTokenScopes] = accessToken?.token?.scopes?.joinToString(",")?.takeIf { it.isNotEmpty() }
|
||||
it[refreshTokenValue] = refreshToken?.token?.tokenValue
|
||||
it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt
|
||||
it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt
|
||||
|
@ -331,6 +335,8 @@ class ExposedOAuth2AuthorizationService(
|
|||
this.objectMapper.registerModules(JavaTimeModule())
|
||||
this.objectMapper.registerModules(modules)
|
||||
this.objectMapper.registerModules(OAuth2AuthorizationServerJackson2Module())
|
||||
this.objectMapper.registerModules(CoreJackson2Module())
|
||||
this.objectMapper.addMixIn(UserDetailsImpl::class.java, UserDetailsMixin::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package dev.usbharu.hideout.service.auth
|
||||
|
||||
import dev.usbharu.hideout.config.ApplicationConfig
|
||||
import dev.usbharu.hideout.domain.model.UserDetailsImpl
|
||||
import dev.usbharu.hideout.query.UserQueryService
|
||||
import dev.usbharu.hideout.service.core.Transaction
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||
|
@ -24,10 +24,15 @@ class UserDetailsServiceImpl(
|
|||
}
|
||||
transaction.transaction {
|
||||
val findById = userQueryService.findByNameAndDomain(username, URL(applicationConfig.url).host)
|
||||
User(
|
||||
UserDetailsImpl(
|
||||
findById.id,
|
||||
findById.name,
|
||||
findById.password,
|
||||
emptyList()
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
mutableListOf()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,4 +12,5 @@
|
|||
<logger name="kjob.core.internal.scheduler.JobServiceImpl" level="INFO"/>
|
||||
<logger name="Exposed" level="INFO"/>
|
||||
<logger name="io.ktor.server.plugins.contentnegotiation" level="INFO"/>
|
||||
<logger name="org.springframework.security" level="trace"/>
|
||||
</configuration>
|
||||
|
|
|
@ -167,6 +167,21 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/Application"
|
||||
|
||||
/api/v1/accounts/verify_credentials:
|
||||
get:
|
||||
tags:
|
||||
- account
|
||||
security:
|
||||
- OAuth2:
|
||||
- "read:accounts"
|
||||
responses:
|
||||
200:
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CredentialAccount"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Account:
|
||||
|
@ -251,6 +266,128 @@ components:
|
|||
- followers_count
|
||||
- followers_count
|
||||
|
||||
CredentialAccount:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
acct:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
display_name:
|
||||
type: string
|
||||
note:
|
||||
type: string
|
||||
avatar:
|
||||
type: string
|
||||
avatar_static:
|
||||
type: string
|
||||
header:
|
||||
type: string
|
||||
header_static:
|
||||
type: string
|
||||
locked:
|
||||
type: boolean
|
||||
fields:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Field"
|
||||
emojis:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/CustomEmoji"
|
||||
bot:
|
||||
type: boolean
|
||||
group:
|
||||
type: boolean
|
||||
discoverable:
|
||||
type: boolean
|
||||
nullable: true
|
||||
noindex:
|
||||
type: boolean
|
||||
moved:
|
||||
type: boolean
|
||||
suspendex:
|
||||
type: boolean
|
||||
limited:
|
||||
type: boolean
|
||||
created_at:
|
||||
type: string
|
||||
last_status_at:
|
||||
type: string
|
||||
nullable: true
|
||||
statuses_count:
|
||||
type: integer
|
||||
followers_count:
|
||||
type: integer
|
||||
following_count:
|
||||
type: integer
|
||||
source:
|
||||
$ref: "#/components/schemas/CredentialAccountSource"
|
||||
role:
|
||||
$ref: "#/components/schemas/Role"
|
||||
required:
|
||||
- id
|
||||
- username
|
||||
- acct
|
||||
- url
|
||||
- display_name
|
||||
- note
|
||||
- avatar
|
||||
- avatar_static
|
||||
- header
|
||||
- header_static
|
||||
- locked
|
||||
- fields
|
||||
- emojis
|
||||
- bot
|
||||
- group
|
||||
- discoverable
|
||||
- created_at
|
||||
- last_status_at
|
||||
- statuses_count
|
||||
- followers_count
|
||||
- followers_count
|
||||
- source
|
||||
|
||||
CredentialAccountSource:
|
||||
type: object
|
||||
properties:
|
||||
note:
|
||||
type: string
|
||||
fields:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Field"
|
||||
privacy:
|
||||
type: string
|
||||
enum:
|
||||
- public
|
||||
- unlisted
|
||||
- private
|
||||
- direct
|
||||
sensitive:
|
||||
type: boolean
|
||||
follow_requests_count:
|
||||
type: integer
|
||||
|
||||
Role:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
color:
|
||||
type: string
|
||||
permissions:
|
||||
type: integer
|
||||
highlighted:
|
||||
type: boolean
|
||||
|
||||
Field:
|
||||
type: object
|
||||
properties:
|
||||
|
|
Loading…
Reference in New Issue