feat: offer GitHub login over HTTP when an OAuth app is configured
This commit is contained in:
1 parent
88f8e2f957
commit
63279125a5
19 files changed
+1752
-26
No files matched your search
+165
-7
@@ -8,13 +8,15 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
patternAdminCreateRoomRequestSlug = regexp.MustCompile(`^[a-z0-9-]{1,32}$`)
|
||||
patternAdminUpdateRoomRequestSlug = regexp.MustCompile(`^[a-z0-9-]{0,32}$`)
|
||||
patternLoginRequestDevice = regexp.MustCompile(`^[A-Za-z0-9_-]{43}$`)
|
||||
patternLoginRequestUsername = regexp.MustCompile(`^[a-z0-9_-]{3,32}$`)
|
||||
patternRedeemPairingCodeRequestCode = regexp.MustCompile(`^[2-9A-HJ-NP-Z]{8}$`)
|
||||
patternRegisterRequestUsername = regexp.MustCompile(`^[a-z0-9_-]{3,32}$`)
|
||||
patternTrackIDWhenBilibili = regexp.MustCompile(`^BV[1-9A-HJ-NP-Za-km-z]{10}(?:/([2-9]|[1-9][0-9]{1,3}))?$`)
|
||||
patternAdminCreateRoomRequestSlug = regexp.MustCompile(`^[a-z0-9-]{1,32}$`)
|
||||
patternAdminSetGithubCredentialRequestClientID = regexp.MustCompile(`^[A-Za-z0-9._-]{1,100}$`)
|
||||
patternAdminSetGithubCredentialRequestClientSecret = regexp.MustCompile(`^[A-Za-z0-9._-]{1,200}$`)
|
||||
patternAdminUpdateRoomRequestSlug = regexp.MustCompile(`^[a-z0-9-]{0,32}$`)
|
||||
patternLoginRequestDevice = regexp.MustCompile(`^[A-Za-z0-9_-]{43}$`)
|
||||
patternLoginRequestUsername = regexp.MustCompile(`^[a-z0-9_-]{3,32}$`)
|
||||
patternRedeemPairingCodeRequestCode = regexp.MustCompile(`^[2-9A-HJ-NP-Z]{8}$`)
|
||||
patternRegisterRequestUsername = regexp.MustCompile(`^[a-z0-9_-]{3,32}$`)
|
||||
patternTrackIDWhenBilibili = regexp.MustCompile(`^BV[1-9A-HJ-NP-Za-km-z]{10}(?:/([2-9]|[1-9][0-9]{1,3}))?$`)
|
||||
)
|
||||
|
||||
func decodeConnectionRequest(typ string, raw json.RawMessage, path string) (any, bool, error) {
|
||||
@@ -176,6 +178,10 @@ func DecodeAdminSetYoutubeCredentialRequest(body []byte) (AdminSetYoutubeCredent
|
||||
return decodeAdminSetYoutubeCredentialRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeAdminSetGithubCredentialRequest(body []byte) (AdminSetGithubCredentialRequest, error) {
|
||||
return decodeAdminSetGithubCredentialRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeAdminUpdateSiteRequest(body []byte) (AdminUpdateSiteRequest, error) {
|
||||
return decodeAdminUpdateSiteRequest(body, "")
|
||||
}
|
||||
@@ -212,6 +218,18 @@ func DecodeRedeemPairingCodeRequest(body []byte) (RedeemPairingCodeRequest, erro
|
||||
return decodeRedeemPairingCodeRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeStartGithubLoginRequest(body []byte) (StartGithubLoginRequest, error) {
|
||||
return decodeStartGithubLoginRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeFinishGithubLoginRequest(body []byte) (FinishGithubLoginRequest, error) {
|
||||
return decodeFinishGithubLoginRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeUnbindGithubRequest(body []byte) (UnbindGithubRequest, error) {
|
||||
return decodeUnbindGithubRequest(body, "")
|
||||
}
|
||||
|
||||
func decodeAddMyPlaylistItemRequest(raw json.RawMessage, path string) (AddMyPlaylistItemRequest, error) {
|
||||
var v AddMyPlaylistItemRequest
|
||||
m, err := decodeObject(raw, path, "track")
|
||||
@@ -346,6 +364,30 @@ func decodeAdminResetRoomApiKeyRequest(raw json.RawMessage, path string) (AdminR
|
||||
return v, err
|
||||
}
|
||||
|
||||
func decodeAdminSetGithubCredentialRequest(raw json.RawMessage, path string) (AdminSetGithubCredentialRequest, error) {
|
||||
var v AdminSetGithubCredentialRequest
|
||||
m, err := decodeObject(raw, path, "clientId", "clientSecret")
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = requireFields(m, path, "clientId", "clientSecret"); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if v.ClientID, err = decodeString(m["clientId"], field(path, "clientId")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkPattern(v.ClientID, field(path, "clientId"), patternAdminSetGithubCredentialRequestClientID); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if v.ClientSecret, err = decodeString(m["clientSecret"], field(path, "clientSecret")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkPattern(v.ClientSecret, field(path, "clientSecret"), patternAdminSetGithubCredentialRequestClientSecret); err != nil {
|
||||
return v, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeAdminSetYoutubeCredentialRequest(raw json.RawMessage, path string) (AdminSetYoutubeCredentialRequest, error) {
|
||||
var v AdminSetYoutubeCredentialRequest
|
||||
m, err := decodeObject(raw, path, "cookies")
|
||||
@@ -625,6 +667,57 @@ func decodeEnableListenRecordingRequest(raw json.RawMessage, path string) (Enabl
|
||||
return v, err
|
||||
}
|
||||
|
||||
func decodeFinishGithubLoginRequest(raw json.RawMessage, path string) (FinishGithubLoginRequest, error) {
|
||||
var v FinishGithubLoginRequest
|
||||
m, err := decodeObject(raw, path, "code", "nonce")
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = requireFields(m, path, "code", "nonce"); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if v.Code, err = decodeString(m["code"], field(path, "code")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMaxLength(v.Code, field(path, "code"), 128); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if v.Nonce, err = decodeString(m["nonce"], field(path, "nonce")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMinLength(v.Nonce, field(path, "nonce"), 16); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMaxLength(v.Nonce, field(path, "nonce"), 128); err != nil {
|
||||
return v, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeGitHubClient(raw json.RawMessage, path string) (GitHubClient, error) {
|
||||
s, err := decodeString(raw, path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch v := GitHubClient(s); v {
|
||||
case GitHubClientWeb, GitHubClientApp:
|
||||
return v, nil
|
||||
}
|
||||
return "", invalid(path, "取值应为 web、app 之一")
|
||||
}
|
||||
|
||||
func decodeGitHubIntent(raw json.RawMessage, path string) (GitHubIntent, error) {
|
||||
s, err := decodeString(raw, path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch v := GitHubIntent(s); v {
|
||||
case GitHubIntentLogin, GitHubIntentBind, GitHubIntentVerify:
|
||||
return v, nil
|
||||
}
|
||||
return "", invalid(path, "取值应为 login、bind、verify 之一")
|
||||
}
|
||||
|
||||
func decodeHelloRequest(raw json.RawMessage, path string) (HelloRequest, error) {
|
||||
var v HelloRequest
|
||||
m, err := decodeObject(raw, path, "protocolVersion", "roomId", "ticket")
|
||||
@@ -1229,6 +1322,50 @@ func decodeSource(raw json.RawMessage, path string) (Source, error) {
|
||||
return "", invalid(path, "取值应为 netease、qq、bilibili、link 之一")
|
||||
}
|
||||
|
||||
func decodeStartGithubLoginRequest(raw json.RawMessage, path string) (StartGithubLoginRequest, error) {
|
||||
var v StartGithubLoginRequest
|
||||
m, err := decodeObject(raw, path, "client", "currentPassword", "intent", "nonce")
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = requireFields(m, path, "intent", "nonce"); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if r, ok := m["client"]; ok {
|
||||
x, err := decodeGitHubClient(r, field(path, "client"))
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
v.Client = &x
|
||||
}
|
||||
if r, ok := m["currentPassword"]; ok {
|
||||
x, err := decodeString(r, field(path, "currentPassword"))
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMinLength(x, field(path, "currentPassword"), 8); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMaxLength(x, field(path, "currentPassword"), 128); err != nil {
|
||||
return v, err
|
||||
}
|
||||
v.CurrentPassword = &x
|
||||
}
|
||||
if v.Intent, err = decodeGitHubIntent(m["intent"], field(path, "intent")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if v.Nonce, err = decodeString(m["nonce"], field(path, "nonce")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMinLength(v.Nonce, field(path, "nonce"), 16); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMaxLength(v.Nonce, field(path, "nonce"), 128); err != nil {
|
||||
return v, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeTrack(raw json.RawMessage, path string) (Track, error) {
|
||||
var v Track
|
||||
m, err := decodeObject(raw, path, "id", "source")
|
||||
@@ -1260,6 +1397,27 @@ func decodeTrack(raw json.RawMessage, path string) (Track, error) {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeUnbindGithubRequest(raw json.RawMessage, path string) (UnbindGithubRequest, error) {
|
||||
var v UnbindGithubRequest
|
||||
m, err := decodeObject(raw, path, "currentPassword")
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = requireFields(m, path, "currentPassword"); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if v.CurrentPassword, err = decodeString(m["currentPassword"], field(path, "currentPassword")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMinLength(v.CurrentPassword, field(path, "currentPassword"), 8); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMaxLength(v.CurrentPassword, field(path, "currentPassword"), 128); err != nil {
|
||||
return v, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeUpdateMeRequest(raw json.RawMessage, path string) (UpdateMeRequest, error) {
|
||||
var v UpdateMeRequest
|
||||
m, err := decodeObject(raw, path, "email", "name")
|
||||
|
||||
+126
-4
@@ -381,6 +381,16 @@
|
||||
"type": "string",
|
||||
"description": "配对码:8 位,由数字 2–9 和除 I、O 以外的大写字母组成",
|
||||
"pattern": "^[2-9A-HJ-NP-Z]{8}$"
|
||||
},
|
||||
"GitHubIntent": {
|
||||
"type": "string",
|
||||
"description": "login:游客用 GitHub 登录,GitHub 账号没有绑定过时就是注册;bind:已注册的账号绑定 GitHub;verify:只绑定了 GitHub 的账号补设用户名和密码之前,用绑定的 GitHub 确认身份",
|
||||
"enum": ["login", "bind", "verify"]
|
||||
},
|
||||
"GitHubClient": {
|
||||
"type": "string",
|
||||
"description": "发起 GitHub 授权的客户端,决定授权结束之后跳到哪里。web:网页的 /auth/github;app:App 用系统浏览器授权,授权结束之后跳到 moe.dihe.alisten:/auth/github,回到 App",
|
||||
"enum": ["web", "app"]
|
||||
}
|
||||
},
|
||||
"x-ws": {
|
||||
@@ -801,7 +811,7 @@
|
||||
},
|
||||
{
|
||||
"name": "register",
|
||||
"description": "游客注册账号:设置用户名和密码,成员和成员 token 都不变;站点关闭注册时返回 FORBIDDEN。只绑定了 GitHub 的账号也用它补设用户名和密码,不受注册开关限制",
|
||||
"description": "游客注册账号:设置用户名和密码,成员和成员 token 都不变;站点关闭注册时返回 FORBIDDEN。只绑定了 GitHub 的账号也用它补设用户名和密码,不受注册开关限制,但发出请求的成员 token 要在 10 分钟内用绑定的 GitHub 确认过身份(startGithubLogin 的 verify),没有确认时返回 FORBIDDEN:拿到成员 token 的人不能抢先设成自己的密码。补设成功之后这次确认作废,账号在其他设备上的成员 token 随即失效,与修改密码相同;失败时确认仍然有效",
|
||||
"method": "POST",
|
||||
"path": "/api/account",
|
||||
"auth": "member",
|
||||
@@ -1205,7 +1215,7 @@
|
||||
},
|
||||
{
|
||||
"name": "getSite",
|
||||
"description": "品牌名和是否开放注册;没有设置过品牌名时是 Alisten",
|
||||
"description": "品牌名、是否开放注册和是否提供 GitHub 登录;没有设置过品牌名时是 Alisten",
|
||||
"method": "GET",
|
||||
"path": "/api/site",
|
||||
"auth": "none",
|
||||
@@ -1214,9 +1224,10 @@
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"brandName": {"type": "string"},
|
||||
"registrationOpen": {"type": "boolean"}
|
||||
"registrationOpen": {"type": "boolean"},
|
||||
"githubLogin": {"type": "boolean", "description": "后台配置了 GitHub OAuth App;为 false 时 GitHub 相关的接口返回 NOT_FOUND"}
|
||||
},
|
||||
"required": ["brandName", "registrationOpen"]
|
||||
"required": ["brandName", "registrationOpen", "githubLogin"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1489,6 +1500,48 @@
|
||||
"auth": "admin",
|
||||
"status": 204
|
||||
},
|
||||
{
|
||||
"name": "adminGetGithubCredential",
|
||||
"description": "GitHub 登录使用的 OAuth App:Client ID、Client Secret 是否已设置和要填进 OAuth App 的回调地址,不返回 Client Secret",
|
||||
"method": "GET",
|
||||
"path": "/api/admin/credentials/github",
|
||||
"auth": "admin",
|
||||
"response": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"clientId": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "没有配置时为 null"},
|
||||
"clientSecretSet": {"type": "boolean", "description": "是否保存了 Client Secret;它与 Client ID 一起保存、一起清除"},
|
||||
"callbackUrl": {"type": "string", "description": "OAuth App 的 Authorization callback URL 要填这个地址"}
|
||||
},
|
||||
"required": ["clientId", "clientSecretSet", "callbackUrl"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "adminSetGithubCredential",
|
||||
"description": "保存 GitHub 登录使用的 OAuth App,开启 GitHub 登录;Client ID 和 Client Secret 一起提交,覆盖已有的配置,立即对所有实例生效。授权进行中换了 OAuth App 时,GitHub 不认原来签发的授权码,这次登录失败",
|
||||
"method": "PUT",
|
||||
"path": "/api/admin/credentials/github",
|
||||
"auth": "admin",
|
||||
"status": 204,
|
||||
"request": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"clientId": {"type": "string", "description": "1 到 100 个字母、数字、点、下划线或连字符", "pattern": "^[A-Za-z0-9._-]{1,100}$"},
|
||||
"clientSecret": {"type": "string", "description": "1 到 200 个字母、数字、点、下划线或连字符", "pattern": "^[A-Za-z0-9._-]{1,200}$"}
|
||||
},
|
||||
"required": ["clientId", "clientSecret"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "adminDeleteGithubCredential",
|
||||
"description": "清除 GitHub 登录使用的 OAuth App,关闭 GitHub 登录:GitHub 相关的接口返回 NOT_FOUND,已经绑定的 GitHub 保持绑定,已经登录的设备不受影响;只绑定了 GitHub 的账号没有密码,重新开启之前无法在其他设备上登录",
|
||||
"method": "DELETE",
|
||||
"path": "/api/admin/credentials/github",
|
||||
"auth": "admin",
|
||||
"status": 204
|
||||
},
|
||||
{
|
||||
"name": "adminListMedia",
|
||||
"description": "已存储的链接曲目和 B 站的歌",
|
||||
@@ -1741,6 +1794,75 @@
|
||||
},
|
||||
"required": ["memberToken", "member", "account"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "startGithubLogin",
|
||||
"description": "发起 GitHub 授权,返回 GitHub 的授权地址。nonce 由客户端随机生成并保存,兑换时原样带上。GitHub 授权之后跳转到 /auth/github#code=<兑换码>,失败时跳转到 /auth/github#error=<denied|expired|failed>;client 为 app 时跳转到 moe.dihe.alisten:/auth/github,片段相同。只按发起授权时的 client 决定跳转目标,state 已经过期或用过时跳转到网页的 /auth/github;回调时后台已经关闭了 GitHub 登录,跳转到 failed。后台没有配置 GitHub OAuth App 时返回 NOT_FOUND。游客发起 bind 或 verify 时返回 FORBIDDEN,已注册的账号发起 login、已经绑定了 GitHub 的账号发起 bind、设置了密码的账号发起 verify 时返回 CONFLICT。bind 要带上当前密码,没带或不对时返回 INVALID_CREDENTIALS:只凭成员 token 不能更换登录凭证,否则拿到 token 的人换上自己的 GitHub 之后,本人修改密码、后台重置密码都赶不走他",
|
||||
"method": "POST",
|
||||
"path": "/api/auth/github",
|
||||
"auth": "member",
|
||||
"status": 201,
|
||||
"request": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"intent": {"$ref": "#/$defs/GitHubIntent"},
|
||||
"client": {"$ref": "#/$defs/GitHubClient", "description": "App 发起授权时填 app,让授权结束之后跳回 App;省略时为 web"},
|
||||
"nonce": {"type": "string", "minLength": 16, "maxLength": 128},
|
||||
"currentPassword": {"$ref": "#/$defs/AccountPassword", "description": "当前密码,bind 时必须带上,其他意图忽略"}
|
||||
},
|
||||
"required": ["intent", "nonce"]
|
||||
},
|
||||
"response": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"url": {"type": "string"}
|
||||
},
|
||||
"required": ["url"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "finishGithubLogin",
|
||||
"description": "用兑换码和发起授权时的 nonce 完成 GitHub 登录、注册或绑定,必须由发起授权的成员调用。GitHub 账号已经绑定了账号时登录它:返回账号的新成员 token,发出请求的游客 token 随即失效;没有绑定过时把它绑到发出请求的成员上(游客即注册),成员和成员 token 都不变。verify 不修改账号,GitHub 账号要是这个账号绑定的,否则返回 INVALID_CREDENTIALS;确认记在发出请求的成员 token 上,10 分钟内用它补设用户名和密码。兑换码过期、用过或 nonce 不对时返回 INVALID_CREDENTIALS,GitHub 账号已经绑定了其他账号时返回 CONFLICT,注册关闭时首次登录返回 FORBIDDEN,后台没有配置 GitHub OAuth App 时返回 NOT_FOUND",
|
||||
"method": "POST",
|
||||
"path": "/api/auth/github/exchange",
|
||||
"auth": "member",
|
||||
"request": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"code": {"type": "string", "maxLength": 128},
|
||||
"nonce": {"type": "string", "minLength": 16, "maxLength": 128}
|
||||
},
|
||||
"required": ["code", "nonce"]
|
||||
},
|
||||
"response": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"memberToken": {"type": "string", "description": "登录了已有的账号时才有"},
|
||||
"member": {"$ref": "#/$defs/Member"},
|
||||
"account": {"$ref": "#/$defs/Account"}
|
||||
},
|
||||
"required": ["member", "account"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "unbindGithub",
|
||||
"description": "解绑 GitHub;账号还没有设置密码时返回 CONFLICT,解绑之后它就再也无法登录。要带上当前密码,不对时返回 INVALID_CREDENTIALS:只凭成员 token 不能更换登录凭证,理由同 startGithubLogin 的 bind。后台没有配置 GitHub OAuth App 时返回 NOT_FOUND",
|
||||
"method": "POST",
|
||||
"path": "/api/account/github/removals",
|
||||
"auth": "member",
|
||||
"status": 204,
|
||||
"request": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"currentPassword": {"$ref": "#/$defs/AccountPassword"}
|
||||
},
|
||||
"required": ["currentPassword"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+383
-4
@@ -193,6 +193,36 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdminGetGithubCredentialResponse": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"callbackUrl": {
|
||||
"description": "OAuth App 的 Authorization callback URL 要填这个地址",
|
||||
"type": "string"
|
||||
},
|
||||
"clientId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "没有配置时为 null"
|
||||
},
|
||||
"clientSecretSet": {
|
||||
"description": "是否保存了 Client Secret;它与 Client ID 一起保存、一起清除",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"clientId",
|
||||
"clientSecretSet",
|
||||
"callbackUrl"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdminGetNeteaseCredentialResponse": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -331,6 +361,26 @@
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"AdminSetGithubCredentialRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"clientId": {
|
||||
"description": "1 到 100 个字母、数字、点、下划线或连字符",
|
||||
"pattern": "^[A-Za-z0-9._-]{1,100}$",
|
||||
"type": "string"
|
||||
},
|
||||
"clientSecret": {
|
||||
"description": "1 到 200 个字母、数字、点、下划线或连字符",
|
||||
"pattern": "^[A-Za-z0-9._-]{1,200}$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"clientId",
|
||||
"clientSecret"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdminSetYoutubeCredentialRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -732,6 +782,45 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FinishGithubLoginRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"code": {
|
||||
"maxLength": 128,
|
||||
"type": "string"
|
||||
},
|
||||
"nonce": {
|
||||
"maxLength": 128,
|
||||
"minLength": 16,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code",
|
||||
"nonce"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FinishGithubLoginResponse": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"account": {
|
||||
"$ref": "#/components/schemas/Account"
|
||||
},
|
||||
"member": {
|
||||
"$ref": "#/components/schemas/Member"
|
||||
},
|
||||
"memberToken": {
|
||||
"description": "登录了已有的账号时才有",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"member",
|
||||
"account"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"GetMeResponse": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -758,16 +847,38 @@
|
||||
"brandName": {
|
||||
"type": "string"
|
||||
},
|
||||
"githubLogin": {
|
||||
"description": "后台配置了 GitHub OAuth App;为 false 时 GitHub 相关的接口返回 NOT_FOUND",
|
||||
"type": "boolean"
|
||||
},
|
||||
"registrationOpen": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"brandName",
|
||||
"registrationOpen"
|
||||
"registrationOpen",
|
||||
"githubLogin"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"GitHubClient": {
|
||||
"description": "发起 GitHub 授权的客户端,决定授权结束之后跳到哪里。web:网页的 /auth/github;app:App 用系统浏览器授权,授权结束之后跳到 moe.dihe.alisten:/auth/github,回到 App",
|
||||
"enum": [
|
||||
"web",
|
||||
"app"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"GitHubIntent": {
|
||||
"description": "login:游客用 GitHub 登录,GitHub 账号没有绑定过时就是注册;bind:已注册的账号绑定 GitHub;verify:只绑定了 GitHub 的账号补设用户名和密码之前,用绑定的 GitHub 确认身份",
|
||||
"enum": [
|
||||
"login",
|
||||
"bind",
|
||||
"verify"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ImportMyPlatformPlaylistRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -1894,6 +2005,46 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"StartGithubLoginRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"client": {
|
||||
"$ref": "#/components/schemas/GitHubClient",
|
||||
"description": "App 发起授权时填 app,让授权结束之后跳回 App;省略时为 web"
|
||||
},
|
||||
"currentPassword": {
|
||||
"description": "当前密码,bind 时必须带上,其他意图忽略",
|
||||
"maxLength": 128,
|
||||
"minLength": 8,
|
||||
"type": "string"
|
||||
},
|
||||
"intent": {
|
||||
"$ref": "#/components/schemas/GitHubIntent"
|
||||
},
|
||||
"nonce": {
|
||||
"maxLength": 128,
|
||||
"minLength": 16,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"intent",
|
||||
"nonce"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"StartGithubLoginResponse": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"url"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"StoredMedia": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -2052,6 +2203,21 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UnbindGithubRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"currentPassword": {
|
||||
"description": "账号密码",
|
||||
"maxLength": 128,
|
||||
"minLength": 8,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"currentPassword"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UpdateMeRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -2099,7 +2265,7 @@
|
||||
},
|
||||
"info": {
|
||||
"title": "Alisten",
|
||||
"version": "0023496d94e1b43f"
|
||||
"version": "6bee23791ffa52ad"
|
||||
},
|
||||
"openapi": "3.1.0",
|
||||
"paths": {
|
||||
@@ -2143,7 +2309,43 @@
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "游客注册账号:设置用户名和密码,成员和成员 token 都不变;站点关闭注册时返回 FORBIDDEN。只绑定了 GitHub 的账号也用它补设用户名和密码,不受注册开关限制"
|
||||
"summary": "游客注册账号:设置用户名和密码,成员和成员 token 都不变;站点关闭注册时返回 FORBIDDEN。只绑定了 GitHub 的账号也用它补设用户名和密码,不受注册开关限制,但发出请求的成员 token 要在 10 分钟内用绑定的 GitHub 确认过身份(startGithubLogin 的 verify),没有确认时返回 FORBIDDEN:拿到成员 token 的人不能抢先设成自己的密码。补设成功之后这次确认作废,账号在其他设备上的成员 token 随即失效,与修改密码相同;失败时确认仍然有效"
|
||||
}
|
||||
},
|
||||
"/api/account/github/removals": {
|
||||
"post": {
|
||||
"operationId": "unbindGithub",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnbindGithubRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "解绑 GitHub;账号还没有设置密码时返回 CONFLICT,解绑之后它就再也无法登录。要带上当前密码,不对时返回 INVALID_CREDENTIALS:只凭成员 token 不能更换登录凭证,理由同 startGithubLogin 的 bind。后台没有配置 GitHub OAuth App 时返回 NOT_FOUND"
|
||||
}
|
||||
},
|
||||
"/api/account/pairing-codes": {
|
||||
@@ -2440,6 +2642,97 @@
|
||||
"summary": "查询 B 站扫码登录的结果"
|
||||
}
|
||||
},
|
||||
"/api/admin/credentials/github": {
|
||||
"delete": {
|
||||
"operationId": "adminDeleteGithubCredential",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"adminToken": []
|
||||
}
|
||||
],
|
||||
"summary": "清除 GitHub 登录使用的 OAuth App,关闭 GitHub 登录:GitHub 相关的接口返回 NOT_FOUND,已经绑定的 GitHub 保持绑定,已经登录的设备不受影响;只绑定了 GitHub 的账号没有密码,重新开启之前无法在其他设备上登录"
|
||||
},
|
||||
"get": {
|
||||
"operationId": "adminGetGithubCredential",
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AdminGetGithubCredentialResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"adminToken": []
|
||||
}
|
||||
],
|
||||
"summary": "GitHub 登录使用的 OAuth App:Client ID、Client Secret 是否已设置和要填进 OAuth App 的回调地址,不返回 Client Secret"
|
||||
},
|
||||
"put": {
|
||||
"operationId": "adminSetGithubCredential",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AdminSetGithubCredentialRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"adminToken": []
|
||||
}
|
||||
],
|
||||
"summary": "保存 GitHub 登录使用的 OAuth App,开启 GitHub 登录;Client ID 和 Client Secret 一起提交,覆盖已有的配置,立即对所有实例生效。授权进行中换了 OAuth App 时,GitHub 不认原来签发的授权码,这次登录失败"
|
||||
}
|
||||
},
|
||||
"/api/admin/credentials/netease": {
|
||||
"get": {
|
||||
"operationId": "adminGetNeteaseCredential",
|
||||
@@ -3205,6 +3498,92 @@
|
||||
"summary": "修改品牌名,返回存下的名字:去掉首尾空白后保存,应有 1 到 20 个字符,其中至少一个看得见,不能包含控制字符;设成 Alisten 就恢复了默认"
|
||||
}
|
||||
},
|
||||
"/api/auth/github": {
|
||||
"post": {
|
||||
"operationId": "startGithubLogin",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/StartGithubLoginRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/StartGithubLoginResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "发起 GitHub 授权,返回 GitHub 的授权地址。nonce 由客户端随机生成并保存,兑换时原样带上。GitHub 授权之后跳转到 /auth/github#code=<兑换码>,失败时跳转到 /auth/github#error=<denied|expired|failed>;client 为 app 时跳转到 moe.dihe.alisten:/auth/github,片段相同。只按发起授权时的 client 决定跳转目标,state 已经过期或用过时跳转到网页的 /auth/github;回调时后台已经关闭了 GitHub 登录,跳转到 failed。后台没有配置 GitHub OAuth App 时返回 NOT_FOUND。游客发起 bind 或 verify 时返回 FORBIDDEN,已注册的账号发起 login、已经绑定了 GitHub 的账号发起 bind、设置了密码的账号发起 verify 时返回 CONFLICT。bind 要带上当前密码,没带或不对时返回 INVALID_CREDENTIALS:只凭成员 token 不能更换登录凭证,否则拿到 token 的人换上自己的 GitHub 之后,本人修改密码、后台重置密码都赶不走他"
|
||||
}
|
||||
},
|
||||
"/api/auth/github/exchange": {
|
||||
"post": {
|
||||
"operationId": "finishGithubLogin",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/FinishGithubLoginRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/FinishGithubLoginResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "用兑换码和发起授权时的 nonce 完成 GitHub 登录、注册或绑定,必须由发起授权的成员调用。GitHub 账号已经绑定了账号时登录它:返回账号的新成员 token,发出请求的游客 token 随即失效;没有绑定过时把它绑到发出请求的成员上(游客即注册),成员和成员 token 都不变。verify 不修改账号,GitHub 账号要是这个账号绑定的,否则返回 INVALID_CREDENTIALS;确认记在发出请求的成员 token 上,10 分钟内用它补设用户名和密码。兑换码过期、用过或 nonce 不对时返回 INVALID_CREDENTIALS,GitHub 账号已经绑定了其他账号时返回 CONFLICT,注册关闭时首次登录返回 FORBIDDEN,后台没有配置 GitHub OAuth App 时返回 NOT_FOUND"
|
||||
}
|
||||
},
|
||||
"/api/me": {
|
||||
"get": {
|
||||
"operationId": "getMe",
|
||||
@@ -5042,7 +5421,7 @@
|
||||
}
|
||||
},
|
||||
"security": [],
|
||||
"summary": "品牌名和是否开放注册;没有设置过品牌名时是 Alisten"
|
||||
"summary": "品牌名、是否开放注册和是否提供 GitHub 登录;没有设置过品牌名时是 Alisten"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+60
-1
@@ -4,7 +4,7 @@ package protocol
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const Version = "0023496d94e1b43f"
|
||||
const Version = "6bee23791ffa52ad"
|
||||
|
||||
type APIKey struct {
|
||||
Key string `json:"key"`
|
||||
@@ -69,6 +69,12 @@ type AdminGetBilibiliCredentialResponse struct {
|
||||
Set bool `json:"set"`
|
||||
}
|
||||
|
||||
type AdminGetGithubCredentialResponse struct {
|
||||
CallbackURL string `json:"callbackUrl"`
|
||||
ClientID *string `json:"clientId"`
|
||||
ClientSecretSet bool `json:"clientSecretSet"`
|
||||
}
|
||||
|
||||
type AdminGetNeteaseCredentialResponse struct {
|
||||
Account *SourceAccount `json:"account"`
|
||||
Set bool `json:"set"`
|
||||
@@ -123,6 +129,11 @@ type AdminResetAccountPasswordResponse struct {
|
||||
|
||||
type AdminResetRoomApiKeyRequest struct{}
|
||||
|
||||
type AdminSetGithubCredentialRequest struct {
|
||||
ClientID string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
}
|
||||
|
||||
type AdminSetYoutubeCredentialRequest struct {
|
||||
Cookies string `json:"cookies"`
|
||||
}
|
||||
@@ -267,6 +278,17 @@ type ErrorInfo struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type FinishGithubLoginRequest struct {
|
||||
Code string `json:"code"`
|
||||
Nonce string `json:"nonce"`
|
||||
}
|
||||
|
||||
type FinishGithubLoginResponse struct {
|
||||
Account Account `json:"account"`
|
||||
Member Member `json:"member"`
|
||||
MemberToken *string `json:"memberToken,omitempty"`
|
||||
}
|
||||
|
||||
type GetMeResponse struct {
|
||||
Account *Account `json:"account,omitempty"`
|
||||
Member Member `json:"member"`
|
||||
@@ -275,9 +297,25 @@ type GetMeResponse struct {
|
||||
|
||||
type GetSiteResponse struct {
|
||||
BrandName string `json:"brandName"`
|
||||
GithubLogin bool `json:"githubLogin"`
|
||||
RegistrationOpen bool `json:"registrationOpen"`
|
||||
}
|
||||
|
||||
type GitHubClient string
|
||||
|
||||
const (
|
||||
GitHubClientWeb GitHubClient = "web"
|
||||
GitHubClientApp GitHubClient = "app"
|
||||
)
|
||||
|
||||
type GitHubIntent string
|
||||
|
||||
const (
|
||||
GitHubIntentLogin GitHubIntent = "login"
|
||||
GitHubIntentBind GitHubIntent = "bind"
|
||||
GitHubIntentVerify GitHubIntent = "verify"
|
||||
)
|
||||
|
||||
type HelloRequest struct {
|
||||
ProtocolVersion string `json:"protocolVersion"`
|
||||
RoomID string `json:"roomId"`
|
||||
@@ -804,6 +842,17 @@ type SourceAccount struct {
|
||||
Vip *bool `json:"vip"`
|
||||
}
|
||||
|
||||
type StartGithubLoginRequest struct {
|
||||
Client *GitHubClient `json:"client,omitempty"`
|
||||
CurrentPassword *string `json:"currentPassword,omitempty"`
|
||||
Intent GitHubIntent `json:"intent"`
|
||||
Nonce string `json:"nonce"`
|
||||
}
|
||||
|
||||
type StartGithubLoginResponse struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type StoredMedia struct {
|
||||
Key string `json:"key"`
|
||||
LastPlayedAtMs *int64 `json:"lastPlayedAtMs"`
|
||||
@@ -833,6 +882,10 @@ type TrackSummary struct {
|
||||
Track Track `json:"track"`
|
||||
}
|
||||
|
||||
type UnbindGithubRequest struct {
|
||||
CurrentPassword string `json:"currentPassword"`
|
||||
}
|
||||
|
||||
type UpdateMeRequest struct {
|
||||
Email *string `json:"email,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
@@ -929,6 +982,9 @@ var (
|
||||
RouteAdminGetYoutubeCredential = Route{Pattern: "GET /api/admin/credentials/youtube", Auth: AuthAdmin, Status: 200}
|
||||
RouteAdminSetYoutubeCredential = Route{Pattern: "PUT /api/admin/credentials/youtube", Auth: AuthAdmin, Status: 204}
|
||||
RouteAdminDeleteYoutubeCredential = Route{Pattern: "DELETE /api/admin/credentials/youtube", Auth: AuthAdmin, Status: 204}
|
||||
RouteAdminGetGithubCredential = Route{Pattern: "GET /api/admin/credentials/github", Auth: AuthAdmin, Status: 200}
|
||||
RouteAdminSetGithubCredential = Route{Pattern: "PUT /api/admin/credentials/github", Auth: AuthAdmin, Status: 204}
|
||||
RouteAdminDeleteGithubCredential = Route{Pattern: "DELETE /api/admin/credentials/github", Auth: AuthAdmin, Status: 204}
|
||||
RouteAdminListMedia = Route{Pattern: "GET /api/admin/media", Auth: AuthAdmin, Status: 200}
|
||||
RouteAdminDeleteMedia = Route{Pattern: "DELETE /api/admin/media/{key}", Auth: AuthAdmin, Status: 204}
|
||||
RouteAdminUpdateSite = Route{Pattern: "PATCH /api/admin/site", Auth: AuthAdmin, Status: 200}
|
||||
@@ -947,6 +1003,9 @@ var (
|
||||
RouteCopySharedPlaylist = Route{Pattern: "POST /api/shared-playlists/{shareId}/copies", Auth: AuthMember, Status: 201}
|
||||
RouteCreatePairingCode = Route{Pattern: "POST /api/account/pairing-codes", Auth: AuthMember, Status: 201}
|
||||
RouteRedeemPairingCode = Route{Pattern: "POST /api/sessions/pairing", Auth: AuthMember, Status: 201}
|
||||
RouteStartGithubLogin = Route{Pattern: "POST /api/auth/github", Auth: AuthMember, Status: 201}
|
||||
RouteFinishGithubLogin = Route{Pattern: "POST /api/auth/github/exchange", Auth: AuthMember, Status: 200}
|
||||
RouteUnbindGithub = Route{Pattern: "POST /api/account/github/removals", Auth: AuthMember, Status: 204}
|
||||
)
|
||||
|
||||
var RoomRoutes = []RoomRoute{
|
||||
|
||||
Reference in new issue
Block a user