fix: sign in and sign out succeed when the disconnect notice for this device's own session fails
CI / go (push) Canceled after 0s
CI / web (push) Canceled after 0s
CI / images (Dockerfile, server, alisten) (push) Canceled after 0s
CI / images (web/Dockerfile, web, ) (push) Canceled after 0s
CI / images (worker.Dockerfile, worker, alisten-worker) (push) Canceled after 0s
CI / helm-release (push) Canceled after 0s
CI / deploy (push) Canceled after 0s
CI / go (push) Canceled after 0s
CI / web (push) Canceled after 0s
CI / images (Dockerfile, server, alisten) (push) Canceled after 0s
CI / images (web/Dockerfile, web, ) (push) Canceled after 0s
CI / images (worker.Dockerfile, worker, alisten-worker) (push) Canceled after 0s
CI / helm-release (push) Canceled after 0s
CI / deploy (push) Canceled after 0s
This commit is contained in:
1 parent
5ee219caba
commit
99987a63bf
4 files changed
+56
-18
No files matched your search
@@ -42,7 +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, gateway),
|
||||
Accounts: identity.NewAccounts(store, store, nil, gateway, slog.New(slog.DiscardHandler)),
|
||||
Lobby: l,
|
||||
Admin: admin.New(adminToken, l, rooms, store, store, porttest.NewNetEaseQRLogin(), porttest.NewQQQRLogin(), porttest.NewBilibiliQRLogin(), porttest.NewYouTubeCookies(), gateway),
|
||||
Gateway: gateway,
|
||||
|
||||
@@ -3,6 +3,7 @@ package identity
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/dihemoe/alisten/internal/app/port"
|
||||
@@ -36,16 +37,18 @@ type Account struct {
|
||||
Username string
|
||||
}
|
||||
|
||||
// 删除会话之后,用这些会话的票据建立的房间连接立即断开;数据已经修改而断开的通知没有发出时返回错误。
|
||||
// 删除会话之后,用这些会话的票据建立的房间连接立即断开;数据已经修改而断开的通知没有发出时返回错误,
|
||||
// 只有撤销发起请求的这台设备自己的会话(Login、Logout)例外,只记日志。
|
||||
type Accounts struct {
|
||||
members port.Members
|
||||
site port.Site
|
||||
quota port.AccountQuota
|
||||
gateway *realtime.Gateway
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewAccounts(members port.Members, site port.Site, quota port.AccountQuota, gateway *realtime.Gateway) *Accounts {
|
||||
return &Accounts{members: members, site: site, quota: quota, gateway: gateway}
|
||||
func NewAccounts(members port.Members, site port.Site, quota port.AccountQuota, gateway *realtime.Gateway, logger *slog.Logger) *Accounts {
|
||||
return &Accounts{members: members, site: site, quota: quota, gateway: gateway, logger: logger}
|
||||
}
|
||||
|
||||
// 游客没有账号,ok 为 false。
|
||||
@@ -108,8 +111,9 @@ func (a *Accounts) Login(ctx context.Context, current member.Member, currentToke
|
||||
case err != nil:
|
||||
return Session{}, Account{}, err
|
||||
}
|
||||
// 页面随后会重新加载,游客的连接即使没断开也不再使用;此时返回错误会让用户拿不到已经签发的新会话。
|
||||
if err := a.gateway.RevokeSessions(ctx, [][]byte{guest}); err != nil {
|
||||
return Session{}, Account{}, err
|
||||
a.logger.WarnContext(ctx, "登录后断开游客会话的通知没有发出", "error", err)
|
||||
}
|
||||
return Session{MemberToken: token, Member: account.Member}, Account{Username: account.Username}, nil
|
||||
}
|
||||
@@ -144,7 +148,11 @@ func (a *Accounts) Logout(ctx context.Context, token string) error {
|
||||
if err := a.members.DeleteSession(ctx, hash); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.gateway.RevokeSessions(ctx, [][]byte{hash})
|
||||
// 页面随后会重新加载,不因通知没有发出而让退出失败。
|
||||
if err := a.gateway.RevokeSessions(ctx, [][]byte{hash}); err != nil {
|
||||
a.logger.WarnContext(ctx, "退出后断开会话的通知没有发出", "error", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Accounts) LogoutOthers(ctx context.Context, memberID, token string) error {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sync"
|
||||
@@ -62,7 +64,7 @@ func newAccounts(t *testing.T) (a *Accounts, s *Service, store *porttest.Store,
|
||||
}
|
||||
}
|
||||
}
|
||||
return NewAccounts(store, store, q, gateway), New(store, store, store, gateway), store, q, revoked
|
||||
return NewAccounts(store, store, q, gateway, slog.New(slog.DiscardHandler)), New(store, store, store, gateway), store, q, revoked
|
||||
}
|
||||
|
||||
// 不计顺序。
|
||||
@@ -377,22 +379,19 @@ func (noRevocation) PublishSessionsRevoked(context.Context, [][]byte) error {
|
||||
return porttest.ErrRedisDown
|
||||
}
|
||||
|
||||
// 会话已经删除而断开连接的通知没有发出时报错,与重置 API Key 的约定一致。
|
||||
func TestAccountOperationsFailWhenRevocationFails(t *testing.T) {
|
||||
// 撤销的是别的设备的会话:断开的通知没有发出时报错,让用户知道它们没有被踢出去。
|
||||
func TestOtherDeviceOperationsFailWhenRevocationFails(t *testing.T) {
|
||||
store := porttest.New()
|
||||
node := porttest.NewRedis().Node("a")
|
||||
gateway := newGateway(store, node, noRevocation{node})
|
||||
s := New(store, store, store, gateway)
|
||||
a := NewAccounts(store, store, &fakeQuota{exhausted: map[string]bool{}}, gateway)
|
||||
a := NewAccounts(store, store, &fakeQuota{exhausted: map[string]bool{}}, gateway, slog.New(slog.DiscardHandler))
|
||||
alice := registerAccount(t, a, s, "alice", "correct horse")
|
||||
login := func() {
|
||||
t.Helper()
|
||||
guest := createMember(t, s)
|
||||
if _, _, err := a.Login(t.Context(), guest.Member, guest.MemberToken, clientIP, "alice", "correct horse"); !errors.Is(err, realtime.ErrUnavailable) {
|
||||
t.Errorf("广播失败时 Login = %v,期望 realtime.ErrUnavailable", err)
|
||||
}
|
||||
if _, ok := tokenOwner(t, store, guest.MemberToken); ok {
|
||||
t.Error("广播失败时游客原来的会话也应已经删除")
|
||||
if _, _, err := a.Login(t.Context(), guest.Member, guest.MemberToken, clientIP, "alice", "correct horse"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// 每一步之前都先登录一次,让这一步有会话可删。
|
||||
@@ -404,7 +403,6 @@ func TestAccountOperationsFailWhenRevocationFails(t *testing.T) {
|
||||
{"ChangePassword", func() error {
|
||||
return a.ChangePassword(t.Context(), alice.Member.ID, alice.MemberToken, "correct horse", "correct horse")
|
||||
}},
|
||||
{"Logout", func() error { return a.Logout(t.Context(), alice.MemberToken) }},
|
||||
{"ResetPassword", func() error {
|
||||
_, err := a.ResetPassword(t.Context(), "alice")
|
||||
return err
|
||||
@@ -417,6 +415,38 @@ func TestAccountOperationsFailWhenRevocationFails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 登录和退出撤销的是发起请求的这台设备的会话,页面随后会重新加载:广播失败时只记日志,照常成功。
|
||||
func TestLoginAndLogoutSucceedWhenRevocationFails(t *testing.T) {
|
||||
store := porttest.New()
|
||||
node := porttest.NewRedis().Node("a")
|
||||
gateway := newGateway(store, node, noRevocation{node})
|
||||
s := New(store, store, store, gateway)
|
||||
var logs bytes.Buffer
|
||||
a := NewAccounts(store, store, &fakeQuota{exhausted: map[string]bool{}}, gateway, slog.New(slog.NewJSONHandler(&logs, nil)))
|
||||
alice := registerAccount(t, a, s, "alice", "correct horse")
|
||||
|
||||
guest := createMember(t, s)
|
||||
session, _, err := a.Login(t.Context(), guest.Member, guest.MemberToken, clientIP, "alice", "correct horse")
|
||||
if err != nil {
|
||||
t.Fatalf("广播失败时 Login = %v,期望成功", err)
|
||||
}
|
||||
if _, ok := tokenOwner(t, store, guest.MemberToken); ok {
|
||||
t.Error("游客原来的会话应已经删除")
|
||||
}
|
||||
if m, ok := tokenOwner(t, store, session.MemberToken); !ok || m.ID != alice.Member.ID {
|
||||
t.Error("新会话应已经签发")
|
||||
}
|
||||
if err := a.Logout(t.Context(), session.MemberToken); err != nil {
|
||||
t.Fatalf("广播失败时 Logout = %v,期望成功", err)
|
||||
}
|
||||
if _, ok := tokenOwner(t, store, session.MemberToken); ok {
|
||||
t.Error("退出的会话应已经删除")
|
||||
}
|
||||
if n := bytes.Count(logs.Bytes(), []byte(`"level":"WARN"`)); n != 2 {
|
||||
t.Errorf("警告日志 %d 条,期望 Login 和 Logout 各 1 条:\n%s", n, logs.String())
|
||||
}
|
||||
}
|
||||
|
||||
// 在登录校验密码之后、签发新会话之前修改账号的密码。
|
||||
type passwordChangedMembers struct {
|
||||
*porttest.Store
|
||||
@@ -441,7 +471,7 @@ func TestLoginRejectedWhenGuestSessionOrPasswordChangesMeanwhile(t *testing.T) {
|
||||
}
|
||||
|
||||
node := porttest.NewRedis().Node("a")
|
||||
changing := NewAccounts(passwordChangedMembers{store}, store, &fakeQuota{exhausted: map[string]bool{}}, newGateway(store, node, node))
|
||||
changing := NewAccounts(passwordChangedMembers{store}, store, &fakeQuota{exhausted: map[string]bool{}}, newGateway(store, node, node), slog.New(slog.DiscardHandler))
|
||||
guest = createMember(t, s)
|
||||
if _, _, err := changing.Login(t.Context(), guest.Member, guest.MemberToken, clientIP, "alice", "correct horse"); !errors.Is(err, ErrInvalidCredentials) {
|
||||
t.Errorf("密码在校验之后被修改,Login = %v,期望 ErrInvalidCredentials", err)
|
||||
|
||||
@@ -78,7 +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), gateway),
|
||||
Accounts: identity.NewAccounts(store, store, redis.NewAccountQuota(rdb), gateway, base.With("component", "accounts")),
|
||||
Lobby: rooms,
|
||||
Admin: admin.New(cfg.adminToken, rooms, store, store, store, src.neteaseLogin, src.qqLogin, src.bilibiliLogin, store, gateway),
|
||||
Gateway: gateway,
|
||||
|
||||
Reference in new issue
Block a user