feat: expose account registration, login and admin recovery over HTTP
This commit is contained in:
1 parent
cc46cc5684
commit
a5749aeb7b
16 files changed
+1455
-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, err := s.Accounts.Login(ctx, c.member, bearer(c.r), c.clientIP, req.Username, req.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return protocol.LoginResponse{MemberToken: session.MemberToken, Member: memberDTO(session.Member), Account: accountDTO(account)}, 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),
|
||||
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)),
|
||||
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"}}
|
||||
|
||||
-- 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"}}
|
||||
+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"}}
|
||||
Reference in new issue
Block a user