feat: expose account registration, login and admin recovery over HTTP
This commit is contained in:
1 parent
62d99221fe
commit
246baf5a0e
17 files changed
+1643
-53
No files matched your search
@@ -0,0 +1,79 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/dihemoe/alisten/internal/app/identity"
|
||||
"github.com/dihemoe/alisten/protocol"
|
||||
)
|
||||
|
||||
func (s *server) register(ctx context.Context, c call) (any, error) {
|
||||
req, err := protocol.DecodeRegisterRequest(c.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account, err := s.Accounts.Register(ctx, c.member.ID, c.clientIP, req.Username, req.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return protocol.RegisterResponse{Account: accountDTO(account)}, nil
|
||||
}
|
||||
|
||||
func (s *server) login(ctx context.Context, c call) (any, error) {
|
||||
req, err := protocol.DecodeLoginRequest(c.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
session, account, device, err := s.Accounts.Login(ctx, c.member, bearer(c.r), c.clientIP, req.Username, req.Password, deref(req.Device))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return protocol.LoginResponse{MemberToken: session.MemberToken, Member: memberDTO(session.Member), Account: accountDTO(account), Device: device}, nil
|
||||
}
|
||||
|
||||
func (s *server) logout(ctx context.Context, c call) (any, error) {
|
||||
return nil, s.Accounts.Logout(ctx, bearer(c.r))
|
||||
}
|
||||
|
||||
func (s *server) changePassword(ctx context.Context, c call) (any, error) {
|
||||
req, err := protocol.DecodeChangePasswordRequest(c.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, s.Accounts.ChangePassword(ctx, c.member.ID, bearer(c.r), req.CurrentPassword, req.NewPassword)
|
||||
}
|
||||
|
||||
func (s *server) adminGetAccount(ctx context.Context, c call) (any, error) {
|
||||
username := c.r.PathValue("username")
|
||||
if !usernamePattern.MatchString(username) {
|
||||
return nil, identity.ErrAccountNotFound
|
||||
}
|
||||
account, err := s.Accounts.Find(ctx, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return protocol.AdminAccount{Member: memberDTO(account.Member), Username: account.Username}, nil
|
||||
}
|
||||
|
||||
func (s *server) adminResetAccountPassword(ctx context.Context, c call) (any, error) {
|
||||
if _, err := protocol.DecodeAdminResetAccountPasswordRequest(c.body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
username := c.r.PathValue("username")
|
||||
if !usernamePattern.MatchString(username) {
|
||||
return nil, identity.ErrAccountNotFound
|
||||
}
|
||||
password, err := s.Accounts.ResetPassword(ctx, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return protocol.AdminResetAccountPasswordResponse{Password: password}, nil
|
||||
}
|
||||
|
||||
func accountDTO(a identity.Account) protocol.Account {
|
||||
dto := protocol.Account{Username: a.Username}
|
||||
if a.Email != "" {
|
||||
dto.Email = &a.Email
|
||||
}
|
||||
return dto
|
||||
}
|
||||
@@ -37,6 +37,7 @@ const (
|
||||
|
||||
type Services struct {
|
||||
Identity *identity.Service
|
||||
Accounts *identity.Accounts
|
||||
Lobby *lobby.Service
|
||||
Admin *admin.Service
|
||||
Gateway *realtime.Gateway
|
||||
@@ -98,6 +99,10 @@ func New(s Services, metrics prometheus.Registerer, tracer trace.TracerProvider,
|
||||
srv.handle(mux, protocol.RouteCreateMember, srv.createMember)
|
||||
srv.handle(mux, protocol.RouteGetMe, srv.getMe)
|
||||
srv.handle(mux, protocol.RouteUpdateMe, srv.updateMe)
|
||||
srv.handle(mux, protocol.RouteRegister, srv.register)
|
||||
srv.handle(mux, protocol.RouteChangePassword, srv.changePassword)
|
||||
srv.handle(mux, protocol.RouteLogin, srv.login)
|
||||
srv.handle(mux, protocol.RouteLogout, srv.logout)
|
||||
srv.handle(mux, protocol.RouteListRooms, srv.listRooms)
|
||||
srv.handle(mux, protocol.RouteGetRoom, srv.getRoom)
|
||||
srv.handle(mux, protocol.RouteCreateRoom, srv.createRoom)
|
||||
@@ -127,6 +132,10 @@ func New(s Services, metrics prometheus.Registerer, tracer trace.TracerProvider,
|
||||
srv.handle(mux, protocol.RouteAdminListMedia, srv.adminListMedia)
|
||||
srv.handle(mux, protocol.RouteAdminDeleteMedia, srv.adminDeleteMedia)
|
||||
srv.handle(mux, protocol.RouteAdminUpdateSite, srv.adminUpdateSite)
|
||||
srv.handle(mux, protocol.RouteAdminOpenRegistration, srv.adminOpenRegistration)
|
||||
srv.handle(mux, protocol.RouteAdminCloseRegistration, srv.adminCloseRegistration)
|
||||
srv.handle(mux, protocol.RouteAdminGetAccount, srv.adminGetAccount)
|
||||
srv.handle(mux, protocol.RouteAdminResetAccountPassword, srv.adminResetAccountPassword)
|
||||
mux.HandleFunc("GET /ws", srv.websocket)
|
||||
mux.HandleFunc("GET /api/room/events", srv.roomEvents)
|
||||
mux.HandleFunc("GET /media/{source}/{id}", srv.redirectMedia)
|
||||
@@ -279,6 +288,15 @@ var knownErrors = []struct {
|
||||
{identity.ErrInvalidTicket, protocol.ErrorCodeUnauthorized},
|
||||
{identity.ErrBotSession, protocol.ErrorCodeUnauthorized},
|
||||
{identity.ErrInvalidEmail, protocol.ErrorCodeInvalidArgument},
|
||||
{identity.ErrInvalidCredentials, protocol.ErrorCodeInvalidCredentials},
|
||||
{identity.ErrWrongCurrentPassword, protocol.ErrorCodeInvalidCredentials},
|
||||
{identity.ErrAlreadyLoggedIn, protocol.ErrorCodeConflict},
|
||||
{identity.ErrRegistrationClosed, protocol.ErrorCodeForbidden},
|
||||
{identity.ErrNotRegistered, protocol.ErrorCodeForbidden},
|
||||
{identity.ErrTooManyAttempts, protocol.ErrorCodeRateLimited},
|
||||
{identity.ErrAccountNotFound, protocol.ErrorCodeNotFound},
|
||||
{port.ErrUsernameTaken, protocol.ErrorCodeConflict},
|
||||
{port.ErrAlreadyRegistered, protocol.ErrorCodeConflict},
|
||||
{realtime.ErrRequesterNotAllowed, protocol.ErrorCodeInvalidArgument},
|
||||
{site.ErrInvalidBrandName, protocol.ErrorCodeInvalidArgument},
|
||||
{realtime.ErrUnavailable, protocol.ErrorCodeUnavailable},
|
||||
|
||||
@@ -42,6 +42,7 @@ func services(store *porttest.Store, rooms port.RoomDirectory, adminToken string
|
||||
l := lobby.New(rooms, node)
|
||||
return Services{
|
||||
Identity: identity.New(store, rooms, store, gateway),
|
||||
Accounts: identity.NewAccounts(store, store, nil, nil),
|
||||
Lobby: l,
|
||||
Admin: admin.New(adminToken, l, rooms, store, store, porttest.NewNetEaseQRLogin(), porttest.NewQQQRLogin(), porttest.NewBilibiliQRLogin(), porttest.NewYouTubeCookies(), gateway),
|
||||
Gateway: gateway,
|
||||
|
||||
@@ -13,11 +13,12 @@ import (
|
||||
"github.com/dihemoe/alisten/protocol"
|
||||
)
|
||||
|
||||
// 路径中的房间 ID 和别名不经过协议解码。格式不正确的房间 ID 或别名一定不存在,其中含有 U+0000 等字符的还会让
|
||||
// 路径中的房间 ID、别名和用户名不经过协议解码。格式不正确的房间 ID、别名或用户名一定不存在,其中含有 U+0000 等字符的还会让
|
||||
// PostgreSQL 报错,所以直接按不存在处理。
|
||||
var (
|
||||
uuidPattern = regexp.MustCompile(`^(?i)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
|
||||
slugPattern = regexp.MustCompile(`^[a-z0-9-]{1,32}$`)
|
||||
uuidPattern = regexp.MustCompile(`^(?i)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
|
||||
slugPattern = regexp.MustCompile(`^[a-z0-9-]{1,32}$`)
|
||||
usernamePattern = regexp.MustCompile(`^[a-z0-9_-]{3,32}$`)
|
||||
)
|
||||
|
||||
func (s *server) createMember(ctx context.Context, c call) (any, error) {
|
||||
@@ -31,8 +32,17 @@ func (s *server) createMember(ctx context.Context, c call) (any, error) {
|
||||
return protocol.CreateMemberResponse{MemberToken: session.MemberToken, Member: memberDTO(session.Member)}, nil
|
||||
}
|
||||
|
||||
func (s *server) getMe(_ context.Context, c call) (any, error) {
|
||||
return protocol.GetMeResponse{Member: memberDTO(c.member)}, nil
|
||||
func (s *server) getMe(ctx context.Context, c call) (any, error) {
|
||||
resp := protocol.GetMeResponse{Member: memberDTO(c.member)}
|
||||
account, registered, err := s.Accounts.Account(ctx, c.member.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if registered {
|
||||
dto := accountDTO(account)
|
||||
resp.Account = &dto
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *server) updateMe(ctx context.Context, c call) (any, error) {
|
||||
|
||||
@@ -11,7 +11,7 @@ func (s *server) getSite(ctx context.Context, _ call) (any, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return protocol.GetSiteResponse{BrandName: settings.BrandName}, nil
|
||||
return protocol.GetSiteResponse{BrandName: settings.BrandName, RegistrationOpen: settings.RegistrationOpen}, nil
|
||||
}
|
||||
|
||||
func (s *server) adminUpdateSite(ctx context.Context, c call) (any, error) {
|
||||
@@ -25,3 +25,14 @@ func (s *server) adminUpdateSite(ctx context.Context, c call) (any, error) {
|
||||
}
|
||||
return protocol.AdminUpdateSiteResponse{BrandName: name}, nil
|
||||
}
|
||||
|
||||
func (s *server) adminOpenRegistration(ctx context.Context, c call) (any, error) {
|
||||
if _, err := protocol.DecodeAdminOpenRegistrationRequest(c.body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, s.Site.SetRegistrationOpen(ctx, true)
|
||||
}
|
||||
|
||||
func (s *server) adminCloseRegistration(ctx context.Context, _ call) (any, error) {
|
||||
return nil, s.Site.SetRegistrationOpen(ctx, false)
|
||||
}
|
||||
@@ -15,14 +15,17 @@ func TestSite(t *testing.T) {
|
||||
brandName := func(name string) response {
|
||||
return response{status: http.StatusOK, body: `{"brandName":"` + name + `"}`}
|
||||
}
|
||||
site := func(name string) response {
|
||||
return response{status: http.StatusOK, body: `{"brandName":"` + name + `","registrationOpen":true}`}
|
||||
}
|
||||
|
||||
if got := send(t, server, http.MethodGet, "/api/site", ""); got != brandName("Alisten") {
|
||||
if got := send(t, server, http.MethodGet, "/api/site", ""); got != site("Alisten") {
|
||||
t.Errorf("没有设置过时 GET /api/site = %#v,期望默认的 Alisten", got)
|
||||
}
|
||||
if got := send(t, server, http.MethodPatch, "/api/admin/site", `{"brandName":" 周末听歌会 "}`, adminHeader...); got != brandName("周末听歌会") {
|
||||
t.Errorf("PATCH /api/admin/site = %#v,期望返回去掉首尾空白之后存下的名字", got)
|
||||
}
|
||||
if got := send(t, server, http.MethodGet, "/api/site", ""); got != brandName("周末听歌会") {
|
||||
if got := send(t, server, http.MethodGet, "/api/site", ""); got != site("周末听歌会") {
|
||||
t.Errorf("修改之后 GET /api/site = %#v,期望去掉首尾空白的新名字", got)
|
||||
}
|
||||
|
||||
@@ -36,7 +39,7 @@ func TestSite(t *testing.T) {
|
||||
if got := send(t, server, http.MethodPatch, "/api/admin/site", `{"brandName":"别的名字"}`); got != unauthorized {
|
||||
t.Errorf("不带管理 token 的 PATCH = %#v,期望 %#v", got, unauthorized)
|
||||
}
|
||||
if got := send(t, server, http.MethodGet, "/api/site", ""); got != brandName("周末听歌会") {
|
||||
if got := send(t, server, http.MethodGet, "/api/site", ""); got != site("周末听歌会") {
|
||||
t.Errorf("被拒绝的修改之后 GET /api/site = %#v,期望不变", got)
|
||||
}
|
||||
if logs.Len() != 0 {
|
||||
|
||||
@@ -78,6 +78,7 @@ func newPublic(cfg serveConfig, pool *pgxpool.Pool, rdb *goredis.Client, instanc
|
||||
rooms := lobby.New(store, leases)
|
||||
services := api.Services{
|
||||
Identity: identity.New(store, store, store, gateway),
|
||||
Accounts: identity.NewAccounts(store, store, redis.NewAccountQuota(rdb), redis.NewDevices(rdb)),
|
||||
Lobby: rooms,
|
||||
Admin: admin.New(cfg.adminToken, rooms, store, store, store, src.neteaseLogin, src.qqLogin, src.bilibiliLogin, store, gateway),
|
||||
Gateway: gateway,
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
登录按客户端 IP 限流:同一个 IP 1 分钟内最多尝试 10 次,换一个用户名也算在内;请求经受信任的代理转发时,按 X-Forwarded-For 中的客户端地址计数,另一个客户端不受影响。
|
||||
|
||||
-- request --
|
||||
POST /api/members
|
||||
|
||||
{}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$guest", "member": {"id": "$guestId", "name": "$guestName", "avatarHash": ""}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $guest
|
||||
X-Forwarded-For: 198.51.100.7
|
||||
|
||||
{"username": "user01", "password": "wrong horse"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $guest
|
||||
X-Forwarded-For: 198.51.100.7
|
||||
|
||||
{"username": "user02", "password": "wrong horse"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $guest
|
||||
X-Forwarded-For: 198.51.100.7
|
||||
|
||||
{"username": "user03", "password": "wrong horse"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $guest
|
||||
X-Forwarded-For: 198.51.100.7
|
||||
|
||||
{"username": "user04", "password": "wrong horse"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $guest
|
||||
X-Forwarded-For: 198.51.100.7
|
||||
|
||||
{"username": "user05", "password": "wrong horse"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $guest
|
||||
X-Forwarded-For: 198.51.100.7
|
||||
|
||||
{"username": "user06", "password": "wrong horse"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $guest
|
||||
X-Forwarded-For: 198.51.100.7
|
||||
|
||||
{"username": "user07", "password": "wrong horse"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $guest
|
||||
X-Forwarded-For: 198.51.100.7
|
||||
|
||||
{"username": "user08", "password": "wrong horse"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $guest
|
||||
X-Forwarded-For: 198.51.100.7
|
||||
|
||||
{"username": "user09", "password": "wrong horse"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $guest
|
||||
X-Forwarded-For: 198.51.100.7
|
||||
|
||||
{"username": "user10", "password": "wrong horse"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $guest
|
||||
X-Forwarded-For: 198.51.100.7
|
||||
|
||||
{"username": "user11", "password": "wrong horse"}
|
||||
-- response --
|
||||
429
|
||||
{"error": {"code": "RATE_LIMITED", "message": "尝试次数太多,请稍后再试。"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $guest
|
||||
X-Forwarded-For: 198.51.100.8
|
||||
|
||||
{"username": "user12", "password": "wrong horse"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
账号:游客注册之后成员和成员 token 都不变;其他游客用账号登录,得到新的成员 token 和这台设备的登录设备凭证,自己原来的 token 随即失效;修改密码、退出登录让对应的成员 token 失效;后台可以查找账号、重置密码。
|
||||
|
||||
-- request --
|
||||
POST /api/members
|
||||
|
||||
{}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$alice", "member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}}
|
||||
|
||||
-- request --
|
||||
POST /api/account
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"username": "alice", "password": "correct horse"}
|
||||
-- response --
|
||||
201
|
||||
{"account": {"username": "alice"}}
|
||||
|
||||
-- request --
|
||||
GET /api/me
|
||||
Authorization: Bearer $alice
|
||||
-- response --
|
||||
200
|
||||
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "account": {"username": "alice"}}
|
||||
|
||||
-- request --
|
||||
PATCH /api/me
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"email": " Alice@Example.COM "}
|
||||
-- response --
|
||||
200
|
||||
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": "c160f8cc69a4f0bf2b0362752353d060"}}
|
||||
|
||||
-- request --
|
||||
GET /api/me
|
||||
Authorization: Bearer $alice
|
||||
-- response --
|
||||
200
|
||||
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": "c160f8cc69a4f0bf2b0362752353d060"}, "account": {"username": "alice", "email": "Alice@Example.COM"}}
|
||||
|
||||
-- request --
|
||||
POST /api/account
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"username": "alice2", "password": "correct horse"}
|
||||
-- response --
|
||||
409
|
||||
{"error": {"code": "CONFLICT", "message": "已经注册过了"}}
|
||||
|
||||
-- request --
|
||||
POST /api/members
|
||||
|
||||
{}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$bob", "member": {"id": "$bobId", "name": "$bobName", "avatarHash": ""}}
|
||||
|
||||
-- request --
|
||||
GET /api/me
|
||||
Authorization: Bearer $bob
|
||||
-- response --
|
||||
200
|
||||
{"member": {"id": "$bobId", "name": "$bobName", "avatarHash": ""}}
|
||||
|
||||
-- request --
|
||||
PATCH /api/me
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{"email": "Alice@Example.COM"}
|
||||
-- response --
|
||||
200
|
||||
{"member": {"id": "$bobId", "name": "$bobName", "avatarHash": "c160f8cc69a4f0bf2b0362752353d060"}}
|
||||
|
||||
-- request --
|
||||
GET /api/me
|
||||
Authorization: Bearer $bob
|
||||
-- response --
|
||||
200
|
||||
{"member": {"id": "$bobId", "name": "$bobName", "avatarHash": "c160f8cc69a4f0bf2b0362752353d060"}}
|
||||
|
||||
-- request --
|
||||
PATCH /api/me
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{"email": ""}
|
||||
-- response --
|
||||
200
|
||||
{"member": {"id": "$bobId", "name": "$bobName", "avatarHash": ""}}
|
||||
|
||||
-- request --
|
||||
POST /api/account
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{"username": "alice", "password": "another horse"}
|
||||
-- response --
|
||||
409
|
||||
{"error": {"code": "CONFLICT", "message": "用户名已被占用"}}
|
||||
|
||||
-- request --
|
||||
POST /api/account
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{"username": "Bob", "password": "another horse"}
|
||||
-- response --
|
||||
400
|
||||
{"error": {"code": "INVALID_ARGUMENT", "message": "username:格式不正确"}}
|
||||
|
||||
-- request --
|
||||
POST /api/account
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{"username": "bob", "password": "short"}
|
||||
-- response --
|
||||
400
|
||||
{"error": {"code": "INVALID_ARGUMENT", "message": "password:长度不能少于 8 个字符"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{"username": "alice", "password": "wrong horse"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{"username": "alice", "password": "correct horse"}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$alice2", "member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": "c160f8cc69a4f0bf2b0362752353d060"}, "account": {"username": "alice", "email": "Alice@Example.COM"}, "device": "$bobDevice"}
|
||||
|
||||
-- request --
|
||||
GET /api/me
|
||||
Authorization: Bearer $alice2
|
||||
-- response --
|
||||
200
|
||||
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": "c160f8cc69a4f0bf2b0362752353d060"}, "account": {"username": "alice", "email": "Alice@Example.COM"}}
|
||||
|
||||
-- request --
|
||||
PATCH /api/me
|
||||
Authorization: Bearer $alice2
|
||||
|
||||
{"email": ""}
|
||||
-- response --
|
||||
200
|
||||
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}}
|
||||
|
||||
-- request --
|
||||
GET /api/me
|
||||
Authorization: Bearer $alice
|
||||
-- response --
|
||||
200
|
||||
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "account": {"username": "alice"}}
|
||||
|
||||
-- request --
|
||||
GET /api/me
|
||||
Authorization: Bearer $bob
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "UNAUTHORIZED", "message": "成员 token 无效"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $alice2
|
||||
|
||||
{"username": "alice", "password": "correct horse"}
|
||||
-- response --
|
||||
409
|
||||
{"error": {"code": "CONFLICT", "message": "已经登录了账号,先退出再登录其他账号"}}
|
||||
|
||||
-- request --
|
||||
PUT /api/account/password
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"currentPassword": "wrong horse", "newPassword": "battery staple"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "当前密码错误"}}
|
||||
|
||||
-- request --
|
||||
PUT /api/account/password
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"currentPassword": "correct horse", "newPassword": "battery staple"}
|
||||
-- response --
|
||||
204
|
||||
|
||||
-- request --
|
||||
GET /api/me
|
||||
Authorization: Bearer $alice2
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "UNAUTHORIZED", "message": "成员 token 无效"}}
|
||||
|
||||
-- request --
|
||||
DELETE /api/sessions/current
|
||||
Authorization: Bearer $alice
|
||||
-- response --
|
||||
204
|
||||
|
||||
-- request --
|
||||
GET /api/me
|
||||
Authorization: Bearer $alice
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "UNAUTHORIZED", "message": "成员 token 无效"}}
|
||||
|
||||
-- request --
|
||||
GET /api/admin/accounts/alice
|
||||
Authorization: Bearer admin-secret
|
||||
-- response --
|
||||
200
|
||||
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "username": "alice"}
|
||||
|
||||
-- request --
|
||||
GET /api/admin/accounts/nobody
|
||||
Authorization: Bearer admin-secret
|
||||
-- response --
|
||||
404
|
||||
{"error": {"code": "NOT_FOUND", "message": "没有这个用户名的账号"}}
|
||||
|
||||
-- request --
|
||||
GET /api/admin/accounts/Not%20Valid
|
||||
Authorization: Bearer admin-secret
|
||||
-- response --
|
||||
404
|
||||
{"error": {"code": "NOT_FOUND", "message": "没有这个用户名的账号"}}
|
||||
|
||||
-- request --
|
||||
POST /api/admin/accounts/alice/password
|
||||
Authorization: Bearer admin-secret
|
||||
|
||||
{}
|
||||
-- response --
|
||||
200
|
||||
{"password": "$resetPassword"}
|
||||
|
||||
-- request --
|
||||
POST /api/members
|
||||
|
||||
{}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$dave", "member": {"id": "$daveId", "name": "$daveName", "avatarHash": ""}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $dave
|
||||
|
||||
{"username": "alice", "password": "$resetPassword"}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$alice4", "member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "account": {"username": "alice"}, "device": "$daveDevice"}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
登录设备凭证:账号密码登录成功时返回这台设备的凭证,退出登录之后设备留着它。按用户名计数的额度 1 分钟内最多 5 次,被别人猜密码用完之后,
|
||||
不带凭证的登录被限流,带着这个账号的凭证仍然能登录,拿到同一个凭证。改密码之后,此前签发的凭证不再被认出,同样被限流。
|
||||
|
||||
-- request --
|
||||
POST /api/members
|
||||
|
||||
{}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$alice", "member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}}
|
||||
|
||||
-- request --
|
||||
POST /api/account
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"username": "alice", "password": "correct horse"}
|
||||
-- response --
|
||||
201
|
||||
{"account": {"username": "alice"}}
|
||||
|
||||
-- request --
|
||||
POST /api/members
|
||||
|
||||
{}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$laptop", "member": {"id": "$laptopId", "name": "$laptopName", "avatarHash": ""}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $laptop
|
||||
|
||||
{"username": "alice", "password": "correct horse"}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$alice2", "member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "account": {"username": "alice"}, "device": "$device"}
|
||||
|
||||
-- request --
|
||||
DELETE /api/sessions/current
|
||||
Authorization: Bearer $alice2
|
||||
-- response --
|
||||
204
|
||||
|
||||
-- request --
|
||||
POST /api/members
|
||||
|
||||
{}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$attacker", "member": {"id": "$attackerId", "name": "$attackerName", "avatarHash": ""}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $attacker
|
||||
|
||||
{"username": "alice", "password": "wrong horse 1"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $attacker
|
||||
|
||||
{"username": "alice", "password": "wrong horse 2"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $attacker
|
||||
|
||||
{"username": "alice", "password": "wrong horse 3"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $attacker
|
||||
|
||||
{"username": "alice", "password": "wrong horse 4"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "INVALID_CREDENTIALS", "message": "用户名或密码错误"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $attacker
|
||||
|
||||
{"username": "alice", "password": "wrong horse 5"}
|
||||
-- response --
|
||||
429
|
||||
{"error": {"code": "RATE_LIMITED", "message": "尝试次数太多,请稍后再试。"}}
|
||||
|
||||
-- request --
|
||||
POST /api/members
|
||||
|
||||
{}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$laptop2", "member": {"id": "$laptop2Id", "name": "$laptop2Name", "avatarHash": ""}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $laptop2
|
||||
|
||||
{"username": "alice", "password": "correct horse"}
|
||||
-- response --
|
||||
429
|
||||
{"error": {"code": "RATE_LIMITED", "message": "尝试次数太多,请稍后再试。"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $laptop2
|
||||
|
||||
{"username": "alice", "password": "correct horse", "device": "not a device"}
|
||||
-- response --
|
||||
400
|
||||
{"error": {"code": "INVALID_ARGUMENT", "message": "device:格式不正确"}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $laptop2
|
||||
|
||||
{"username": "alice", "password": "correct horse", "device": "$device"}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$alice3", "member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "account": {"username": "alice"}, "device": "$device"}
|
||||
|
||||
-- request --
|
||||
PUT /api/account/password
|
||||
Authorization: Bearer $alice3
|
||||
|
||||
{"currentPassword": "correct horse", "newPassword": "battery staple"}
|
||||
-- response --
|
||||
204
|
||||
|
||||
-- request --
|
||||
POST /api/members
|
||||
|
||||
{}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$laptop3", "member": {"id": "$laptop3Id", "name": "$laptop3Name", "avatarHash": ""}}
|
||||
|
||||
-- request --
|
||||
POST /api/sessions
|
||||
Authorization: Bearer $laptop3
|
||||
|
||||
{"username": "alice", "password": "battery staple", "device": "$device"}
|
||||
-- response --
|
||||
429
|
||||
{"error": {"code": "RATE_LIMITED", "message": "尝试次数太多,请稍后再试。"}}
|
||||
+50
-4
@@ -1,10 +1,10 @@
|
||||
站点:品牌名默认是 Alisten;后台修改时返回存下的名字,首尾的空白已经去掉,之后读到的也是它;空的、超过 20 个字符的和看不见的名字不能保存;修改品牌名要管理 token。
|
||||
站点:品牌名默认是 Alisten,默认开放注册;后台修改品牌名时返回存下的名字,首尾的空白已经去掉,之后读到的也是它;空的、超过 20 个字符的和看不见的名字不能保存;后台可以关闭和重新开放注册,关闭之后游客不能注册;修改站点设置要管理 token。
|
||||
|
||||
-- request --
|
||||
GET /api/site
|
||||
-- response --
|
||||
200
|
||||
{"brandName": "Alisten"}
|
||||
{"brandName": "Alisten", "registrationOpen": true}
|
||||
|
||||
-- request --
|
||||
PATCH /api/admin/site
|
||||
@@ -19,7 +19,7 @@ Authorization: Bearer admin-secret
|
||||
GET /api/site
|
||||
-- response --
|
||||
200
|
||||
{"brandName": "周末听歌会"}
|
||||
{"brandName": "周末听歌会", "registrationOpen": true}
|
||||
|
||||
-- request --
|
||||
PATCH /api/admin/site
|
||||
@@ -56,8 +56,54 @@ PATCH /api/admin/site
|
||||
401
|
||||
{"error": {"code": "UNAUTHORIZED", "message": "管理 token 无效"}}
|
||||
|
||||
-- request --
|
||||
DELETE /api/admin/registration
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "UNAUTHORIZED", "message": "管理 token 无效"}}
|
||||
|
||||
-- request --
|
||||
DELETE /api/admin/registration
|
||||
Authorization: Bearer admin-secret
|
||||
-- response --
|
||||
204
|
||||
|
||||
-- request --
|
||||
GET /api/site
|
||||
-- response --
|
||||
200
|
||||
{"brandName": "周末听歌会"}
|
||||
{"brandName": "周末听歌会", "registrationOpen": false}
|
||||
|
||||
-- request --
|
||||
POST /api/members
|
||||
|
||||
{}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$guest", "member": {"id": "$guestId", "name": "$guestName", "avatarHash": ""}}
|
||||
|
||||
-- request --
|
||||
POST /api/account
|
||||
Authorization: Bearer $guest
|
||||
|
||||
{"username": "guest", "password": "correct horse"}
|
||||
-- response --
|
||||
403
|
||||
{"error": {"code": "FORBIDDEN", "message": "注册已关闭"}}
|
||||
|
||||
-- request --
|
||||
PUT /api/admin/registration
|
||||
Authorization: Bearer admin-secret
|
||||
|
||||
{}
|
||||
-- response --
|
||||
204
|
||||
|
||||
-- request --
|
||||
POST /api/account
|
||||
Authorization: Bearer $guest
|
||||
|
||||
{"username": "guest", "password": "correct horse"}
|
||||
-- response --
|
||||
201
|
||||
{"account": {"username": "guest"}}
|
||||
@@ -10,6 +10,9 @@ 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}$`)
|
||||
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}))?$`)
|
||||
)
|
||||
|
||||
@@ -72,6 +75,18 @@ func DecodeUpdateMeRequest(body []byte) (UpdateMeRequest, error) {
|
||||
return decodeUpdateMeRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeRegisterRequest(body []byte) (RegisterRequest, error) {
|
||||
return decodeRegisterRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeChangePasswordRequest(body []byte) (ChangePasswordRequest, error) {
|
||||
return decodeChangePasswordRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeLoginRequest(body []byte) (LoginRequest, error) {
|
||||
return decodeLoginRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeCreateRoomRequest(body []byte) (CreateRoomRequest, error) {
|
||||
return decodeCreateRoomRequest(body, "")
|
||||
}
|
||||
@@ -124,6 +139,14 @@ func DecodeAdminUpdateSiteRequest(body []byte) (AdminUpdateSiteRequest, error) {
|
||||
return decodeAdminUpdateSiteRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeAdminOpenRegistrationRequest(body []byte) (AdminOpenRegistrationRequest, error) {
|
||||
return decodeAdminOpenRegistrationRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeAdminResetAccountPasswordRequest(body []byte) (AdminResetAccountPasswordRequest, error) {
|
||||
return decodeAdminResetAccountPasswordRequest(body, "")
|
||||
}
|
||||
|
||||
func decodeAdminCheckBilibiliLoginRequest(raw json.RawMessage, path string) (AdminCheckBilibiliLoginRequest, error) {
|
||||
var v AdminCheckBilibiliLoginRequest
|
||||
m, err := decodeObject(raw, path, "key")
|
||||
@@ -225,6 +248,18 @@ func decodeAdminCreateRoomRequest(raw json.RawMessage, path string) (AdminCreate
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeAdminOpenRegistrationRequest(raw json.RawMessage, path string) (AdminOpenRegistrationRequest, error) {
|
||||
var v AdminOpenRegistrationRequest
|
||||
_, err := decodeObject(raw, path)
|
||||
return v, err
|
||||
}
|
||||
|
||||
func decodeAdminResetAccountPasswordRequest(raw json.RawMessage, path string) (AdminResetAccountPasswordRequest, error) {
|
||||
var v AdminResetAccountPasswordRequest
|
||||
_, err := decodeObject(raw, path)
|
||||
return v, err
|
||||
}
|
||||
|
||||
func decodeAdminResetRoomApiKeyRequest(raw json.RawMessage, path string) (AdminResetRoomApiKeyRequest, error) {
|
||||
var v AdminResetRoomApiKeyRequest
|
||||
_, err := decodeObject(raw, path)
|
||||
@@ -340,6 +375,36 @@ func decodeAdminUpdateSiteRequest(raw json.RawMessage, path string) (AdminUpdate
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeChangePasswordRequest(raw json.RawMessage, path string) (ChangePasswordRequest, error) {
|
||||
var v ChangePasswordRequest
|
||||
m, err := decodeObject(raw, path, "currentPassword", "newPassword")
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = requireFields(m, path, "currentPassword", "newPassword"); 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
|
||||
}
|
||||
if v.NewPassword, err = decodeString(m["newPassword"], field(path, "newPassword")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMinLength(v.NewPassword, field(path, "newPassword"), 8); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMaxLength(v.NewPassword, field(path, "newPassword"), 128); err != nil {
|
||||
return v, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeChatSendRequest(raw json.RawMessage, path string) (ChatSendRequest, error) {
|
||||
var v ChatSendRequest
|
||||
m, err := decodeObject(raw, path, "content")
|
||||
@@ -432,6 +497,43 @@ func decodeHelloRequest(raw json.RawMessage, path string) (HelloRequest, error)
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeLoginRequest(raw json.RawMessage, path string) (LoginRequest, error) {
|
||||
var v LoginRequest
|
||||
m, err := decodeObject(raw, path, "device", "password", "username")
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = requireFields(m, path, "password", "username"); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if r, ok := m["device"]; ok {
|
||||
x, err := decodeString(r, field(path, "device"))
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkPattern(x, field(path, "device"), patternLoginRequestDevice); err != nil {
|
||||
return v, err
|
||||
}
|
||||
v.Device = &x
|
||||
}
|
||||
if v.Password, err = decodeString(m["password"], field(path, "password")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMinLength(v.Password, field(path, "password"), 8); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMaxLength(v.Password, field(path, "password"), 128); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if v.Username, err = decodeString(m["username"], field(path, "username")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkPattern(v.Username, field(path, "username"), patternLoginRequestUsername); err != nil {
|
||||
return v, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodePingRequest(raw json.RawMessage, path string) (PingRequest, error) {
|
||||
var v PingRequest
|
||||
m, err := decodeObject(raw, path, "clientTimeMs")
|
||||
@@ -583,6 +685,33 @@ func decodeRecommendListRequest(raw json.RawMessage, path string) (RecommendList
|
||||
return v, err
|
||||
}
|
||||
|
||||
func decodeRegisterRequest(raw json.RawMessage, path string) (RegisterRequest, error) {
|
||||
var v RegisterRequest
|
||||
m, err := decodeObject(raw, path, "password", "username")
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = requireFields(m, path, "password", "username"); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if v.Password, err = decodeString(m["password"], field(path, "password")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMinLength(v.Password, field(path, "password"), 8); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMaxLength(v.Password, field(path, "password"), 128); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if v.Username, err = decodeString(m["username"], field(path, "username")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkPattern(v.Username, field(path, "username"), patternRegisterRequestUsername); err != nil {
|
||||
return v, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeRoomSyncRequest(raw json.RawMessage, path string) (RoomSyncRequest, error) {
|
||||
var v RoomSyncRequest
|
||||
_, err := decodeObject(raw, path)
|
||||
|
||||
+172
-6
@@ -12,6 +12,7 @@
|
||||
"enum": [
|
||||
"INVALID_ARGUMENT",
|
||||
"UNAUTHORIZED",
|
||||
"INVALID_CREDENTIALS",
|
||||
"FORBIDDEN",
|
||||
"NOT_FOUND",
|
||||
"CONFLICT",
|
||||
@@ -26,6 +27,7 @@
|
||||
"x-httpStatus": {
|
||||
"INVALID_ARGUMENT": 400,
|
||||
"UNAUTHORIZED": 401,
|
||||
"INVALID_CREDENTIALS": 401,
|
||||
"FORBIDDEN": 403,
|
||||
"NOT_FOUND": 404,
|
||||
"CONFLICT": 409,
|
||||
@@ -139,6 +141,40 @@
|
||||
},
|
||||
"required": ["id", "name", "avatarHash"]
|
||||
},
|
||||
"Username": {
|
||||
"type": "string",
|
||||
"description": "用户名:3 到 32 个小写字母、数字、下划线或连字符",
|
||||
"pattern": "^[a-z0-9_-]{3,32}$"
|
||||
},
|
||||
"AccountPassword": {
|
||||
"type": "string",
|
||||
"description": "账号密码",
|
||||
"minLength": 8,
|
||||
"maxLength": 128
|
||||
},
|
||||
"LoginDevice": {
|
||||
"type": "string",
|
||||
"description": "登录设备凭证:用账号密码登录成功时发给这台设备,43 个 base64url 字符",
|
||||
"pattern": "^[A-Za-z0-9_-]{43}$"
|
||||
},
|
||||
"Account": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"username": {"type": "string"},
|
||||
"email": {"type": "string", "description": "账号设置了邮箱时才有"}
|
||||
},
|
||||
"required": ["username"]
|
||||
},
|
||||
"AdminAccount": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"member": {"$ref": "#/$defs/Member"},
|
||||
"username": {"type": "string"}
|
||||
},
|
||||
"required": ["member", "username"]
|
||||
},
|
||||
"Members": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -658,7 +694,7 @@
|
||||
},
|
||||
{
|
||||
"name": "getMe",
|
||||
"description": "当前成员的资料",
|
||||
"description": "当前成员的资料;已注册的成员带上账号",
|
||||
"method": "GET",
|
||||
"path": "/api/me",
|
||||
"auth": "member",
|
||||
@@ -666,14 +702,15 @@
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"member": {"$ref": "#/$defs/Member"}
|
||||
"member": {"$ref": "#/$defs/Member"},
|
||||
"account": {"$ref": "#/$defs/Account"}
|
||||
},
|
||||
"required": ["member"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "updateMe",
|
||||
"description": "修改昵称和头像;email 只用来计算头像哈希,空字符串表示清除头像",
|
||||
"description": "修改昵称和头像;email 用来计算头像哈希,已注册的账号同时保存它,去掉首尾空白后应形如 name@example.com,空字符串表示清除",
|
||||
"method": "PATCH",
|
||||
"path": "/api/me",
|
||||
"auth": "member",
|
||||
@@ -694,6 +731,85 @@
|
||||
"required": ["member"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "register",
|
||||
"description": "游客注册账号:设置用户名和密码,成员和成员 token 都不变;站点关闭注册时返回 FORBIDDEN",
|
||||
"method": "POST",
|
||||
"path": "/api/account",
|
||||
"auth": "member",
|
||||
"status": 201,
|
||||
"request": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"username": {"$ref": "#/$defs/Username"},
|
||||
"password": {"$ref": "#/$defs/AccountPassword"}
|
||||
},
|
||||
"required": ["username", "password"]
|
||||
},
|
||||
"response": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"account": {"$ref": "#/$defs/Account"}
|
||||
},
|
||||
"required": ["account"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "changePassword",
|
||||
"description": "修改密码:账号在其他设备上的成员 token 随即失效,发出请求的这个不变;当前密码错误时返回 INVALID_CREDENTIALS",
|
||||
"method": "PUT",
|
||||
"path": "/api/account/password",
|
||||
"auth": "member",
|
||||
"status": 204,
|
||||
"request": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"currentPassword": {"$ref": "#/$defs/AccountPassword"},
|
||||
"newPassword": {"$ref": "#/$defs/AccountPassword"}
|
||||
},
|
||||
"required": ["currentPassword", "newPassword"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "login",
|
||||
"description": "游客登录账号:返回账号的新成员 token,发出请求的游客 token 随即失效;用户名或密码错误时返回 INVALID_CREDENTIALS,已经登录了账号时返回 CONFLICT",
|
||||
"method": "POST",
|
||||
"path": "/api/sessions",
|
||||
"auth": "member",
|
||||
"status": 201,
|
||||
"request": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"username": {"$ref": "#/$defs/Username"},
|
||||
"password": {"$ref": "#/$defs/AccountPassword"},
|
||||
"device": {"$ref": "#/$defs/LoginDevice", "description": "本机此前登录这个账号时拿到的凭证:有人反复猜这个账号的密码时,带上它本机仍然能登录"}
|
||||
},
|
||||
"required": ["username", "password"]
|
||||
},
|
||||
"response": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"memberToken": {"type": "string"},
|
||||
"member": {"$ref": "#/$defs/Member"},
|
||||
"account": {"$ref": "#/$defs/Account"},
|
||||
"device": {"$ref": "#/$defs/LoginDevice", "description": "本机以后登录这个账号时要带上的凭证,退出登录之后也要保留:带来的凭证有效时原样返回,否则是新签发的"}
|
||||
},
|
||||
"required": ["memberToken", "member", "account", "device"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "logout",
|
||||
"description": "退出登录:发出请求的成员 token 随即失效",
|
||||
"method": "DELETE",
|
||||
"path": "/api/sessions/current",
|
||||
"auth": "member",
|
||||
"status": 204
|
||||
},
|
||||
{
|
||||
"name": "listRooms",
|
||||
"description": "房间列表",
|
||||
@@ -769,7 +885,7 @@
|
||||
},
|
||||
{
|
||||
"name": "getSite",
|
||||
"description": "品牌名;没有设置过时是 Alisten",
|
||||
"description": "品牌名和是否开放注册;没有设置过品牌名时是 Alisten",
|
||||
"method": "GET",
|
||||
"path": "/api/site",
|
||||
"auth": "none",
|
||||
@@ -777,9 +893,10 @@
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"brandName": {"type": "string"}
|
||||
"brandName": {"type": "string"},
|
||||
"registrationOpen": {"type": "boolean"}
|
||||
},
|
||||
"required": ["brandName"]
|
||||
"required": ["brandName", "registrationOpen"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1097,6 +1214,55 @@
|
||||
},
|
||||
"required": ["brandName"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "adminOpenRegistration",
|
||||
"description": "开放注册",
|
||||
"method": "PUT",
|
||||
"path": "/api/admin/registration",
|
||||
"auth": "admin",
|
||||
"status": 204,
|
||||
"request": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "adminCloseRegistration",
|
||||
"description": "关闭注册:不能再注册新账号,已有账号照常登录,游客照常使用",
|
||||
"method": "DELETE",
|
||||
"path": "/api/admin/registration",
|
||||
"auth": "admin",
|
||||
"status": 204
|
||||
},
|
||||
{
|
||||
"name": "adminGetAccount",
|
||||
"description": "按用户名查找账号",
|
||||
"method": "GET",
|
||||
"path": "/api/admin/accounts/{username}",
|
||||
"auth": "admin",
|
||||
"response": {"$ref": "#/$defs/AdminAccount"}
|
||||
},
|
||||
{
|
||||
"name": "adminResetAccountPassword",
|
||||
"description": "重置账号的密码:生成随机的新密码,只在这次响应中返回;账号在所有设备上的成员 token 随即失效",
|
||||
"method": "POST",
|
||||
"path": "/api/admin/accounts/{username}/password",
|
||||
"auth": "admin",
|
||||
"request": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
},
|
||||
"response": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"password": {"type": "string"}
|
||||
},
|
||||
"required": ["password"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+477
-5
@@ -14,6 +14,22 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Account": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"email": {
|
||||
"description": "账号设置了邮箱时才有",
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"username"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ActivityKind": {
|
||||
"enum": [
|
||||
"added",
|
||||
@@ -25,6 +41,22 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AdminAccount": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"member": {
|
||||
"$ref": "#/components/schemas/Member"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"member",
|
||||
"username"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdminCheckBilibiliLoginRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -246,6 +278,26 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdminOpenRegistrationRequest": {
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"AdminResetAccountPasswordRequest": {
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"AdminResetAccountPasswordResponse": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"password"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdminResetRoomApiKeyRequest": {
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
@@ -331,6 +383,28 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ChangePasswordRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"currentPassword": {
|
||||
"description": "账号密码",
|
||||
"maxLength": 128,
|
||||
"minLength": 8,
|
||||
"type": "string"
|
||||
},
|
||||
"newPassword": {
|
||||
"description": "账号密码",
|
||||
"maxLength": 128,
|
||||
"minLength": 8,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"currentPassword",
|
||||
"newPassword"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ChatSendRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -479,6 +553,7 @@
|
||||
"enum": [
|
||||
"INVALID_ARGUMENT",
|
||||
"UNAUTHORIZED",
|
||||
"INVALID_CREDENTIALS",
|
||||
"FORBIDDEN",
|
||||
"NOT_FOUND",
|
||||
"CONFLICT",
|
||||
@@ -496,6 +571,7 @@
|
||||
"FORBIDDEN": 403,
|
||||
"INTERNAL": 500,
|
||||
"INVALID_ARGUMENT": 400,
|
||||
"INVALID_CREDENTIALS": 401,
|
||||
"NOTHING_PLAYING": 409,
|
||||
"NOT_FOUND": 404,
|
||||
"PROVIDER_ERROR": 502,
|
||||
@@ -537,6 +613,9 @@
|
||||
"GetMeResponse": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"account": {
|
||||
"$ref": "#/components/schemas/Account"
|
||||
},
|
||||
"member": {
|
||||
"$ref": "#/components/schemas/Member"
|
||||
}
|
||||
@@ -551,10 +630,14 @@
|
||||
"properties": {
|
||||
"brandName": {
|
||||
"type": "string"
|
||||
},
|
||||
"registrationOpen": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"brandName"
|
||||
"brandName",
|
||||
"registrationOpen"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -573,6 +656,58 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"LoginRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"device": {
|
||||
"description": "本机此前登录这个账号时拿到的凭证:有人反复猜这个账号的密码时,带上它本机仍然能登录",
|
||||
"pattern": "^[A-Za-z0-9_-]{43}$",
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"description": "账号密码",
|
||||
"maxLength": 128,
|
||||
"minLength": 8,
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"description": "用户名:3 到 32 个小写字母、数字、下划线或连字符",
|
||||
"pattern": "^[a-z0-9_-]{3,32}$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"username",
|
||||
"password"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"LoginResponse": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"account": {
|
||||
"$ref": "#/components/schemas/Account"
|
||||
},
|
||||
"device": {
|
||||
"description": "本机以后登录这个账号时要带上的凭证,退出登录之后也要保留:带来的凭证有效时原样返回,否则是新签发的",
|
||||
"pattern": "^[A-Za-z0-9_-]{43}$",
|
||||
"type": "string"
|
||||
},
|
||||
"member": {
|
||||
"$ref": "#/components/schemas/Member"
|
||||
},
|
||||
"memberToken": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"memberToken",
|
||||
"member",
|
||||
"account",
|
||||
"device"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Member": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -892,6 +1027,39 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RegisterRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"password": {
|
||||
"description": "账号密码",
|
||||
"maxLength": 128,
|
||||
"minLength": 8,
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"description": "用户名:3 到 32 个小写字母、数字、下划线或连字符",
|
||||
"pattern": "^[a-z0-9_-]{3,32}$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"username",
|
||||
"password"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RegisterResponse": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"account": {
|
||||
"$ref": "#/components/schemas/Account"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"account"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RoomInfo": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -1343,10 +1511,185 @@
|
||||
},
|
||||
"info": {
|
||||
"title": "Alisten",
|
||||
"version": "473ee31f5ef912b4"
|
||||
"version": "bb812527ea1133df"
|
||||
},
|
||||
"openapi": "3.1.0",
|
||||
"paths": {
|
||||
"/api/account": {
|
||||
"post": {
|
||||
"operationId": "register",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RegisterRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RegisterResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "游客注册账号:设置用户名和密码,成员和成员 token 都不变;站点关闭注册时返回 FORBIDDEN"
|
||||
}
|
||||
},
|
||||
"/api/account/password": {
|
||||
"put": {
|
||||
"operationId": "changePassword",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ChangePasswordRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "修改密码:账号在其他设备上的成员 token 随即失效,发出请求的这个不变;当前密码错误时返回 INVALID_CREDENTIALS"
|
||||
}
|
||||
},
|
||||
"/api/admin/accounts/{username}": {
|
||||
"get": {
|
||||
"operationId": "adminGetAccount",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "username",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AdminAccount"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"adminToken": []
|
||||
}
|
||||
],
|
||||
"summary": "按用户名查找账号"
|
||||
}
|
||||
},
|
||||
"/api/admin/accounts/{username}/password": {
|
||||
"post": {
|
||||
"operationId": "adminResetAccountPassword",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "username",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AdminResetAccountPasswordRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AdminResetAccountPasswordResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"adminToken": []
|
||||
}
|
||||
],
|
||||
"summary": "重置账号的密码:生成随机的新密码,只在这次响应中返回;账号在所有设备上的成员 token 随即失效"
|
||||
}
|
||||
},
|
||||
"/api/admin/credentials/bilibili": {
|
||||
"get": {
|
||||
"operationId": "adminGetBilibiliCredential",
|
||||
@@ -1864,6 +2207,66 @@
|
||||
"summary": "删除一首已存储的歌的音频和封面,并清除它的准备结果"
|
||||
}
|
||||
},
|
||||
"/api/admin/registration": {
|
||||
"delete": {
|
||||
"operationId": "adminCloseRegistration",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"adminToken": []
|
||||
}
|
||||
],
|
||||
"summary": "关闭注册:不能再注册新账号,已有账号照常登录,游客照常使用"
|
||||
},
|
||||
"put": {
|
||||
"operationId": "adminOpenRegistration",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AdminOpenRegistrationRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"adminToken": []
|
||||
}
|
||||
],
|
||||
"summary": "开放注册"
|
||||
}
|
||||
},
|
||||
"/api/admin/rooms": {
|
||||
"get": {
|
||||
"operationId": "adminListRooms",
|
||||
@@ -2201,7 +2604,7 @@
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "当前成员的资料"
|
||||
"summary": "当前成员的资料;已注册的成员带上账号"
|
||||
},
|
||||
"patch": {
|
||||
"operationId": "updateMe",
|
||||
@@ -2242,7 +2645,7 @@
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "修改昵称和头像;email 只用来计算头像哈希,空字符串表示清除头像"
|
||||
"summary": "修改昵称和头像;email 用来计算头像哈希,已注册的账号同时保存它,去掉首尾空白后应形如 name@example.com,空字符串表示清除"
|
||||
}
|
||||
},
|
||||
"/api/members": {
|
||||
@@ -2973,6 +3376,75 @@
|
||||
"summary": "按 UUID 或别名查找房间"
|
||||
}
|
||||
},
|
||||
"/api/sessions": {
|
||||
"post": {
|
||||
"operationId": "login",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LoginRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LoginResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "游客登录账号:返回账号的新成员 token,发出请求的游客 token 随即失效;用户名或密码错误时返回 INVALID_CREDENTIALS,已经登录了账号时返回 CONFLICT"
|
||||
}
|
||||
},
|
||||
"/api/sessions/current": {
|
||||
"delete": {
|
||||
"operationId": "logout",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "退出登录:发出请求的成员 token 随即失效"
|
||||
}
|
||||
},
|
||||
"/api/site": {
|
||||
"get": {
|
||||
"operationId": "getSite",
|
||||
@@ -2999,7 +3471,7 @@
|
||||
}
|
||||
},
|
||||
"security": [],
|
||||
"summary": "品牌名;没有设置过时是 Alisten"
|
||||
"summary": "品牌名和是否开放注册;没有设置过品牌名时是 Alisten"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+83
-26
@@ -4,12 +4,17 @@ package protocol
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const Version = "473ee31f5ef912b4"
|
||||
const Version = "bb812527ea1133df"
|
||||
|
||||
type APIKey struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
Email *string `json:"email,omitempty"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type ActivityKind string
|
||||
|
||||
const (
|
||||
@@ -21,6 +26,11 @@ const (
|
||||
ActivityKindFailed ActivityKind = "failed"
|
||||
)
|
||||
|
||||
type AdminAccount struct {
|
||||
Member Member `json:"member"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type AdminCheckBilibiliLoginRequest struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
@@ -94,6 +104,14 @@ func (v AdminListRoomsResponse) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(plain(v))
|
||||
}
|
||||
|
||||
type AdminOpenRegistrationRequest struct{}
|
||||
|
||||
type AdminResetAccountPasswordRequest struct{}
|
||||
|
||||
type AdminResetAccountPasswordResponse struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type AdminResetRoomApiKeyRequest struct{}
|
||||
|
||||
type AdminSetYoutubeCredentialRequest struct {
|
||||
@@ -123,6 +141,11 @@ type AdminUpdateSiteResponse struct {
|
||||
BrandName string `json:"brandName"`
|
||||
}
|
||||
|
||||
type ChangePasswordRequest struct {
|
||||
CurrentPassword string `json:"currentPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
|
||||
type ChatMessageEvent struct {
|
||||
Content string `json:"content"`
|
||||
Member Member `json:"member"`
|
||||
@@ -180,18 +203,19 @@ const (
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
ErrorCodeInvalidArgument ErrorCode = "INVALID_ARGUMENT"
|
||||
ErrorCodeUnauthorized ErrorCode = "UNAUTHORIZED"
|
||||
ErrorCodeForbidden ErrorCode = "FORBIDDEN"
|
||||
ErrorCodeNotFound ErrorCode = "NOT_FOUND"
|
||||
ErrorCodeConflict ErrorCode = "CONFLICT"
|
||||
ErrorCodeQueueFull ErrorCode = "QUEUE_FULL"
|
||||
ErrorCodeNothingPlaying ErrorCode = "NOTHING_PLAYING"
|
||||
ErrorCodeRateLimited ErrorCode = "RATE_LIMITED"
|
||||
ErrorCodeProviderError ErrorCode = "PROVIDER_ERROR"
|
||||
ErrorCodeUnavailable ErrorCode = "UNAVAILABLE"
|
||||
ErrorCodeClientOutdated ErrorCode = "CLIENT_OUTDATED"
|
||||
ErrorCodeInternal ErrorCode = "INTERNAL"
|
||||
ErrorCodeInvalidArgument ErrorCode = "INVALID_ARGUMENT"
|
||||
ErrorCodeUnauthorized ErrorCode = "UNAUTHORIZED"
|
||||
ErrorCodeInvalidCredentials ErrorCode = "INVALID_CREDENTIALS"
|
||||
ErrorCodeForbidden ErrorCode = "FORBIDDEN"
|
||||
ErrorCodeNotFound ErrorCode = "NOT_FOUND"
|
||||
ErrorCodeConflict ErrorCode = "CONFLICT"
|
||||
ErrorCodeQueueFull ErrorCode = "QUEUE_FULL"
|
||||
ErrorCodeNothingPlaying ErrorCode = "NOTHING_PLAYING"
|
||||
ErrorCodeRateLimited ErrorCode = "RATE_LIMITED"
|
||||
ErrorCodeProviderError ErrorCode = "PROVIDER_ERROR"
|
||||
ErrorCodeUnavailable ErrorCode = "UNAVAILABLE"
|
||||
ErrorCodeClientOutdated ErrorCode = "CLIENT_OUTDATED"
|
||||
ErrorCodeInternal ErrorCode = "INTERNAL"
|
||||
)
|
||||
|
||||
type ErrorInfo struct {
|
||||
@@ -200,11 +224,13 @@ type ErrorInfo struct {
|
||||
}
|
||||
|
||||
type GetMeResponse struct {
|
||||
Member Member `json:"member"`
|
||||
Account *Account `json:"account,omitempty"`
|
||||
Member Member `json:"member"`
|
||||
}
|
||||
|
||||
type GetSiteResponse struct {
|
||||
BrandName string `json:"brandName"`
|
||||
BrandName string `json:"brandName"`
|
||||
RegistrationOpen bool `json:"registrationOpen"`
|
||||
}
|
||||
|
||||
type HelloRequest struct {
|
||||
@@ -231,6 +257,19 @@ func (v ListRoomsResponse) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(plain(v))
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Device *string `json:"device,omitempty"`
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
Account Account `json:"account"`
|
||||
Device string `json:"device"`
|
||||
Member Member `json:"member"`
|
||||
MemberToken string `json:"memberToken"`
|
||||
}
|
||||
|
||||
type Member struct {
|
||||
AvatarHash string `json:"avatarHash"`
|
||||
ID string `json:"id"`
|
||||
@@ -397,6 +436,15 @@ func (v RecommendListResponse) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(plain(v))
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type RegisterResponse struct {
|
||||
Account Account `json:"account"`
|
||||
}
|
||||
|
||||
type RoomActivityEvent struct {
|
||||
Entry *Entry `json:"entry,omitempty"`
|
||||
Kind ActivityKind `json:"kind"`
|
||||
@@ -609,6 +657,10 @@ var (
|
||||
RouteCreateMember = Route{Pattern: "POST /api/members", Auth: AuthNone, Status: 201}
|
||||
RouteGetMe = Route{Pattern: "GET /api/me", Auth: AuthMember, Status: 200}
|
||||
RouteUpdateMe = Route{Pattern: "PATCH /api/me", Auth: AuthMember, Status: 200}
|
||||
RouteRegister = Route{Pattern: "POST /api/account", Auth: AuthMember, Status: 201}
|
||||
RouteChangePassword = Route{Pattern: "PUT /api/account/password", Auth: AuthMember, Status: 204}
|
||||
RouteLogin = Route{Pattern: "POST /api/sessions", Auth: AuthMember, Status: 201}
|
||||
RouteLogout = Route{Pattern: "DELETE /api/sessions/current", Auth: AuthMember, Status: 204}
|
||||
RouteListRooms = Route{Pattern: "GET /api/rooms", Auth: AuthNone, Status: 200}
|
||||
RouteGetRoom = Route{Pattern: "GET /api/rooms/{ref}", Auth: AuthNone, Status: 200}
|
||||
RouteCreateRoom = Route{Pattern: "POST /api/rooms", Auth: AuthNone, Status: 201, Location: true}
|
||||
@@ -635,6 +687,10 @@ var (
|
||||
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}
|
||||
RouteAdminOpenRegistration = Route{Pattern: "PUT /api/admin/registration", Auth: AuthAdmin, Status: 204}
|
||||
RouteAdminCloseRegistration = Route{Pattern: "DELETE /api/admin/registration", Auth: AuthAdmin, Status: 204}
|
||||
RouteAdminGetAccount = Route{Pattern: "GET /api/admin/accounts/{username}", Auth: AuthAdmin, Status: 200}
|
||||
RouteAdminResetAccountPassword = Route{Pattern: "POST /api/admin/accounts/{username}/password", Auth: AuthAdmin, Status: 200}
|
||||
)
|
||||
|
||||
var RoomRoutes = []RoomRoute{
|
||||
@@ -652,17 +708,18 @@ var RoomRoutes = []RoomRoute{
|
||||
}
|
||||
|
||||
var errorCodeHTTPStatus = map[ErrorCode]int{
|
||||
ErrorCodeInvalidArgument: 400,
|
||||
ErrorCodeUnauthorized: 401,
|
||||
ErrorCodeForbidden: 403,
|
||||
ErrorCodeNotFound: 404,
|
||||
ErrorCodeConflict: 409,
|
||||
ErrorCodeQueueFull: 409,
|
||||
ErrorCodeNothingPlaying: 409,
|
||||
ErrorCodeRateLimited: 429,
|
||||
ErrorCodeProviderError: 502,
|
||||
ErrorCodeUnavailable: 503,
|
||||
ErrorCodeInternal: 500,
|
||||
ErrorCodeInvalidArgument: 400,
|
||||
ErrorCodeUnauthorized: 401,
|
||||
ErrorCodeInvalidCredentials: 401,
|
||||
ErrorCodeForbidden: 403,
|
||||
ErrorCodeNotFound: 404,
|
||||
ErrorCodeConflict: 409,
|
||||
ErrorCodeQueueFull: 409,
|
||||
ErrorCodeNothingPlaying: 409,
|
||||
ErrorCodeRateLimited: 429,
|
||||
ErrorCodeProviderError: 502,
|
||||
ErrorCodeUnavailable: 503,
|
||||
ErrorCodeInternal: 500,
|
||||
}
|
||||
|
||||
// IDL 没有为某个取值指定 HTTP 状态码时返回 0。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protocolgen from protocol/idl.json; DO NOT EDIT.
|
||||
|
||||
import type { APIKey, ErrorInfo, Member, QQLoginType, QRLogin, QRLoginResult, RoomListing, SourceAccount, StoredMedia } from '../realtime/protocol.gen'
|
||||
import type { APIKey, Account, AdminAccount, ErrorInfo, Member, QQLoginType, QRLogin, QRLoginResult, RoomListing, SourceAccount, StoredMedia } from '../realtime/protocol.gen'
|
||||
|
||||
export interface AdminCheckBilibiliLoginRequest {
|
||||
key: string
|
||||
@@ -59,6 +59,14 @@ export interface AdminListRoomsResponse {
|
||||
items: RoomListing[]
|
||||
}
|
||||
|
||||
export type AdminOpenRegistrationRequest = Record<string, never>
|
||||
|
||||
export type AdminResetAccountPasswordRequest = Record<string, never>
|
||||
|
||||
export interface AdminResetAccountPasswordResponse {
|
||||
password: string
|
||||
}
|
||||
|
||||
export type AdminResetRoomApiKeyRequest = Record<string, never>
|
||||
|
||||
export interface AdminSetYoutubeCredentialRequest {
|
||||
@@ -88,6 +96,11 @@ export interface AdminUpdateSiteResponse {
|
||||
brandName: string
|
||||
}
|
||||
|
||||
export interface ChangePasswordRequest {
|
||||
currentPassword: string
|
||||
newPassword: string
|
||||
}
|
||||
|
||||
export type CreateMemberRequest = Record<string, never>
|
||||
|
||||
export interface CreateMemberResponse {
|
||||
@@ -114,17 +127,41 @@ export interface CreateTicketResponse {
|
||||
}
|
||||
|
||||
export interface GetMeResponse {
|
||||
account?: Account
|
||||
member: Member
|
||||
}
|
||||
|
||||
export interface GetSiteResponse {
|
||||
brandName: string
|
||||
registrationOpen: boolean
|
||||
}
|
||||
|
||||
export interface ListRoomsResponse {
|
||||
items: RoomListing[]
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
device?: string
|
||||
password: string
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
account: Account
|
||||
device: string
|
||||
member: Member
|
||||
memberToken: string
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
password: string
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
account: Account
|
||||
}
|
||||
|
||||
export interface UpdateMeRequest {
|
||||
email?: string
|
||||
name?: string
|
||||
@@ -154,6 +191,10 @@ export function httpClient(send: HttpSend) {
|
||||
createMember: () => send<CreateMemberResponse>({ method: 'POST', path: '/api/members', auth: 'none', body: {} }),
|
||||
getMe: () => send<GetMeResponse>({ method: 'GET', path: '/api/me', auth: 'member' }),
|
||||
updateMe: (body: UpdateMeRequest) => send<UpdateMeResponse>({ method: 'PATCH', path: '/api/me', auth: 'member', body }),
|
||||
register: (body: RegisterRequest) => send<RegisterResponse>({ method: 'POST', path: '/api/account', auth: 'member', body }),
|
||||
changePassword: (body: ChangePasswordRequest) => send<void>({ method: 'PUT', path: '/api/account/password', auth: 'member', body }),
|
||||
login: (body: LoginRequest) => send<LoginResponse>({ method: 'POST', path: '/api/sessions', auth: 'member', body }),
|
||||
logout: () => send<void>({ method: 'DELETE', path: '/api/sessions/current', auth: 'member' }),
|
||||
listRooms: () => send<ListRoomsResponse>({ method: 'GET', path: '/api/rooms', auth: 'none' }),
|
||||
getRoom: (ref: string) => send<RoomListing>({ method: 'GET', path: `/api/rooms/${encodeURIComponent(ref)}`, auth: 'none' }),
|
||||
createRoom: (body: CreateRoomRequest) => send<CreateRoomResponse>({ method: 'POST', path: '/api/rooms', auth: 'none', body }),
|
||||
@@ -180,5 +221,9 @@ export function httpClient(send: HttpSend) {
|
||||
adminListMedia: () => send<AdminListMediaResponse>({ method: 'GET', path: '/api/admin/media', auth: 'admin' }),
|
||||
adminDeleteMedia: (key: string) => send<void>({ method: 'DELETE', path: `/api/admin/media/${encodeURIComponent(key)}`, auth: 'admin' }),
|
||||
adminUpdateSite: (body: AdminUpdateSiteRequest) => send<AdminUpdateSiteResponse>({ method: 'PATCH', path: '/api/admin/site', auth: 'admin', body }),
|
||||
adminOpenRegistration: () => send<void>({ method: 'PUT', path: '/api/admin/registration', auth: 'admin', body: {} }),
|
||||
adminCloseRegistration: () => send<void>({ method: 'DELETE', path: '/api/admin/registration', auth: 'admin' }),
|
||||
adminGetAccount: (username: string) => send<AdminAccount>({ method: 'GET', path: `/api/admin/accounts/${encodeURIComponent(username)}`, auth: 'admin' }),
|
||||
adminResetAccountPassword: (username: string) => send<AdminResetAccountPasswordResponse>({ method: 'POST', path: `/api/admin/accounts/${encodeURIComponent(username)}/password`, auth: 'admin', body: {} }),
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,23 @@
|
||||
// Code generated by protocolgen from protocol/idl.json; DO NOT EDIT.
|
||||
|
||||
export const PROTOCOL_VERSION = '473ee31f5ef912b4'
|
||||
export const PROTOCOL_VERSION = 'bb812527ea1133df'
|
||||
|
||||
export interface APIKey {
|
||||
key: string
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
email?: string
|
||||
username: string
|
||||
}
|
||||
|
||||
export type ActivityKind = 'added' | 'removed' | 'liked' | 'voted' | 'skipped' | 'failed'
|
||||
|
||||
export interface AdminAccount {
|
||||
member: Member
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface ChatMessageEvent {
|
||||
content: string
|
||||
member: Member
|
||||
@@ -32,7 +42,7 @@ export interface Entry {
|
||||
|
||||
export type EntryStatus = 'preparing' | 'ready'
|
||||
|
||||
export type ErrorCode = 'INVALID_ARGUMENT' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'NOT_FOUND' | 'CONFLICT' | 'QUEUE_FULL' | 'NOTHING_PLAYING' | 'RATE_LIMITED' | 'PROVIDER_ERROR' | 'UNAVAILABLE' | 'CLIENT_OUTDATED' | 'INTERNAL'
|
||||
export type ErrorCode = 'INVALID_ARGUMENT' | 'UNAUTHORIZED' | 'INVALID_CREDENTIALS' | 'FORBIDDEN' | 'NOT_FOUND' | 'CONFLICT' | 'QUEUE_FULL' | 'NOTHING_PLAYING' | 'RATE_LIMITED' | 'PROVIDER_ERROR' | 'UNAVAILABLE' | 'CLIENT_OUTDATED' | 'INTERNAL'
|
||||
|
||||
export interface ErrorInfo {
|
||||
code: ErrorCode
|
||||
|
||||
Reference in new issue
Block a user