feat: offer GitHub login over HTTP when an OAuth app is configured

This commit is contained in:
DiheChen committed 2026-11-10 12:55:58 +08:00
1 parent 88f8e2f957
commit 63279125a5
19 files changed
+1752 -26

No files matched your search

+165 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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{