feat: bind room tickets to member sessions and disconnect revoked sessions
This commit is contained in:
1 parent
a80c48824f
commit
f0f38ff511
51 files changed
+1404
-230
No files matched your search
@@ -11,6 +11,14 @@ type Handler = (token: string | null) => Response | Promise<Response>
|
||||
class FakeStorage {
|
||||
readonly #items = new Map<string, string>()
|
||||
|
||||
get length(): number {
|
||||
return this.#items.size
|
||||
}
|
||||
|
||||
key(index: number): string | null {
|
||||
return [...this.#items.keys()][index] ?? null
|
||||
}
|
||||
|
||||
getItem(key: string): string | null {
|
||||
return this.#items.get(key) ?? null
|
||||
}
|
||||
@@ -172,6 +180,18 @@ describe('token 失效时更换身份', () => {
|
||||
expect(events).toEqual([{ kind: 'rejected' }, { kind: 'renewed', member: issued }])
|
||||
})
|
||||
|
||||
it('票据属于被拒绝的身份,一并删除', async () => {
|
||||
local.setItem(memberTokenKey, 'expired')
|
||||
local.setItem('alisten.ticket.r1', 't1')
|
||||
local.setItem('alisten.ticket.r2', 't2')
|
||||
routes.set('PATCH /api/me', unauthorized)
|
||||
const { api, IdentityRenewedError } = await load()
|
||||
await expect(api.updateMe({ name: 'alice' })).rejects.toBeInstanceOf(IdentityRenewedError)
|
||||
|
||||
expect(local.getItem('alisten.ticket.r1')).toBeNull()
|
||||
expect(local.getItem('alisten.ticket.r2')).toBeNull()
|
||||
})
|
||||
|
||||
it('同时被拒绝的请求只更换一次身份', async () => {
|
||||
local.setItem(memberTokenKey, 'expired')
|
||||
routes.set('PATCH /api/me', unauthorized)
|
||||
@@ -202,6 +222,7 @@ describe('token 失效时更换身份', () => {
|
||||
|
||||
it('其他标签页已经换了 token 时不删除新的 token', async () => {
|
||||
local.setItem(memberTokenKey, 'expired')
|
||||
local.setItem('alisten.ticket.r1', 't1')
|
||||
routes.set('PATCH /api/me', () => {
|
||||
local.setItem(memberTokenKey, 'good')
|
||||
return unauthorized()
|
||||
@@ -210,6 +231,7 @@ describe('token 失效时更换身份', () => {
|
||||
await expect(api.updateMe({ name: 'a' })).rejects.toThrow('刚才的操作没有生效')
|
||||
|
||||
expect(local.getItem(memberTokenKey)).toBe('good')
|
||||
expect(local.getItem('alisten.ticket.r1')).toBe('t1')
|
||||
expect(callsTo('POST /api/members')).toHaveLength(0)
|
||||
expect(events).toEqual([{ kind: 'rejected' }, { kind: 'renewed', member }])
|
||||
})
|
||||
@@ -272,6 +294,7 @@ describe('token 失效时更换身份', () => {
|
||||
|
||||
it('修改密码时当前密码错误返回 INVALID_CREDENTIALS,不更换身份', async () => {
|
||||
local.setItem(memberTokenKey, 'good')
|
||||
local.setItem('alisten.ticket.r1', 't1')
|
||||
routes.set('PUT /api/account/password', () => errorResponse(401, 'INVALID_CREDENTIALS', '当前密码错误'))
|
||||
const { api, events, IdentityRenewedError } = await load()
|
||||
const error: unknown = await api.changePassword({ currentPassword: 'wrong horse', newPassword: 'correct horse' }).catch((reason: unknown) => reason)
|
||||
@@ -279,6 +302,7 @@ describe('token 失效时更换身份', () => {
|
||||
expect(error).toMatchObject({ code: 'INVALID_CREDENTIALS', status: 401 })
|
||||
expect(events).toEqual([])
|
||||
expect(local.getItem(memberTokenKey)).toBe('good')
|
||||
expect(local.getItem('alisten.ticket.r1')).toBe('t1')
|
||||
})
|
||||
|
||||
it('账号密码错误不更换身份', async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Auth, CreateTicketRequest, CreateTicketResponse, ErrorResponse, HttpRequest } from './http.gen'
|
||||
import type { ErrorCode, Member } from '@/core/realtime'
|
||||
import { Emitter } from '@/shared/lib/emitter'
|
||||
import { deleteMemberToken, loadAdminToken, loadMemberToken, memberTokenKey, saveMemberToken } from './credentials'
|
||||
import { deleteAllTickets, deleteMemberToken, loadAdminToken, loadMemberToken, memberTokenKey, saveMemberToken } from './credentials'
|
||||
import { httpClient } from './http.gen'
|
||||
|
||||
export class ApiError extends Error {
|
||||
@@ -163,9 +163,11 @@ function renewIdentity(rejected: string): Promise<void> {
|
||||
|
||||
// 更换过程中用 unrenewed 读取成员:新 token 也被拒绝时不再更换,避免循环。
|
||||
async function replaceIdentity(rejected: string): Promise<void> {
|
||||
// 其他标签页可能已经更换了身份,只删除被拒绝的那个 token。
|
||||
if (loadMemberToken() === rejected)
|
||||
// 其他标签页可能已经更换了身份,只删除被拒绝的那个 token。票据属于被拒绝的身份,一并删除。
|
||||
if (loadMemberToken() === rejected) {
|
||||
deleteMemberToken()
|
||||
deleteAllTickets()
|
||||
}
|
||||
identityEvents.emit({ kind: 'rejected' })
|
||||
let member: Member
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protocolgen from protocol/idl.json; DO NOT EDIT.
|
||||
|
||||
export const PROTOCOL_VERSION = 'bb812527ea1133df'
|
||||
export const PROTOCOL_VERSION = '000a0b37ea0f0adc'
|
||||
|
||||
export interface APIKey {
|
||||
key: string
|
||||
|
||||
@@ -47,7 +47,7 @@ export function RoomApiKeyDialog({ room, onClose }: { room: RoomListing, onClose
|
||||
setStatus(undefined)
|
||||
setAttempt(n => n + 1)
|
||||
}
|
||||
// 失败时 Key 可能已经重置,只是没能通知各实例断开旧 Key 的事件流,所以重新获取状态。
|
||||
// 请求失败时服务端仍可能已经重置了 Key(比如响应没有送达),所以重新获取状态。
|
||||
const renew = async (): Promise<string | null> => {
|
||||
const result = await adminStore.call(() => api.adminResetRoomApiKey(room.id))
|
||||
if (!result)
|
||||
|
||||
Reference in new issue
Block a user