feat: expose device pairing over HTTP
This commit is contained in:
1 parent
e95476185b
commit
1157843f50
23 files changed
+587
-75
No files matched your search
@@ -133,6 +133,14 @@ export interface CreateMyPlaylistRequest {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type CreatePairingCodeRequest = Record<string, never>
|
||||
|
||||
export interface CreatePairingCodeResponse {
|
||||
code: string
|
||||
expiresAtMs: number
|
||||
qrImage: string
|
||||
}
|
||||
|
||||
export interface CreateRoomRequest {
|
||||
description: string
|
||||
name: string
|
||||
@@ -227,6 +235,16 @@ export interface MoveMyPlaylistItemsRequest {
|
||||
tracks: Track[]
|
||||
}
|
||||
|
||||
export interface RedeemPairingCodeRequest {
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface RedeemPairingCodeResponse {
|
||||
account: Account
|
||||
member: Member
|
||||
memberToken: string
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
password: string
|
||||
username: string
|
||||
@@ -348,5 +366,7 @@ export function httpClient(send: HttpSend) {
|
||||
unshareMyPlaylist: (id: string) => send<void>({ method: 'DELETE', path: `/api/me/playlists/${encodeURIComponent(id)}/share`, auth: 'member' }),
|
||||
getSharedPlaylist: (shareId: string) => send<SharedPlaylist>({ method: 'GET', path: `/api/shared-playlists/${encodeURIComponent(shareId)}`, auth: 'none' }),
|
||||
copySharedPlaylist: (shareId: string, body: CopySharedPlaylistRequest) => send<MyPlaylist>({ method: 'POST', path: `/api/shared-playlists/${encodeURIComponent(shareId)}/copies`, auth: 'member', body }),
|
||||
createPairingCode: () => send<CreatePairingCodeResponse>({ method: 'POST', path: '/api/account/pairing-codes', auth: 'member', body: {} }),
|
||||
redeemPairingCode: (body: RedeemPairingCodeRequest) => send<RedeemPairingCodeResponse>({ method: 'POST', path: '/api/sessions/pairing', auth: 'member', body }),
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protocolgen from protocol/idl.json; DO NOT EDIT.
|
||||
|
||||
export const PROTOCOL_VERSION = 'cbfa923a9cbe9f0a'
|
||||
export const PROTOCOL_VERSION = '0023496d94e1b43f'
|
||||
|
||||
export interface APIKey {
|
||||
key: string
|
||||
@@ -8,7 +8,8 @@ export interface APIKey {
|
||||
|
||||
export interface Account {
|
||||
email?: string
|
||||
username: string
|
||||
github: boolean
|
||||
username?: string
|
||||
}
|
||||
|
||||
export type ActivityKind = 'added' | 'removed' | 'liked' | 'voted' | 'skipped' | 'failed'
|
||||
|
||||
@@ -464,7 +464,7 @@ describe('playlistsStore', () => {
|
||||
const { playlistsStore } = await import('./playlists-store')
|
||||
const { profileStore } = await import('@/features/settings')
|
||||
playlistsStore.start()
|
||||
await profileStore.registered({ username: 'alice' })
|
||||
await profileStore.registered({ username: 'alice', github: false })
|
||||
await flush()
|
||||
expect(playlistsStore.state.playlists).toEqual([favorites])
|
||||
window.dispatchEvent(new Event('focus'))
|
||||
@@ -477,8 +477,8 @@ describe('playlistsStore', () => {
|
||||
const { profileStore } = await import('@/features/settings')
|
||||
holdReads = true
|
||||
playlistsStore.start()
|
||||
await profileStore.registered({ username: 'alice' })
|
||||
await profileStore.registered({ username: 'bob' })
|
||||
await profileStore.registered({ username: 'alice', github: false })
|
||||
await profileStore.registered({ username: 'bob', github: false })
|
||||
await flush()
|
||||
release.shift()!()
|
||||
await flush()
|
||||
@@ -493,12 +493,12 @@ describe('playlistsStore', () => {
|
||||
const { profileStore } = await import('@/features/settings')
|
||||
const { notificationStore } = await import('@/shared/ui')
|
||||
playlistsStore.start()
|
||||
await profileStore.registered({ username: 'alice' })
|
||||
await profileStore.registered({ username: 'alice', github: false })
|
||||
await flush()
|
||||
failAdd = true
|
||||
const done = playlistsStore.setMembership(favorites, rain, true)
|
||||
await flush()
|
||||
await profileStore.registered({ username: 'bob' })
|
||||
await profileStore.registered({ username: 'bob', github: false })
|
||||
release.shift()!()
|
||||
await done
|
||||
expect(notificationStore.state.notifications).toEqual([])
|
||||
@@ -508,12 +508,12 @@ describe('playlistsStore', () => {
|
||||
const { playlistsStore } = await import('./playlists-store')
|
||||
const { profileStore } = await import('@/features/settings')
|
||||
playlistsStore.start()
|
||||
await profileStore.registered({ username: 'alice' })
|
||||
await profileStore.registered({ username: 'alice', github: false })
|
||||
await flush()
|
||||
holdReads = true
|
||||
const reading = playlistsStore.containing(sunny)
|
||||
await flush()
|
||||
await profileStore.registered({ username: 'bob' })
|
||||
await profileStore.registered({ username: 'bob', github: false })
|
||||
release.shift()!()
|
||||
await reading
|
||||
expect(playlistsStore.state.memberships.size).toBe(0)
|
||||
|
||||
@@ -167,15 +167,15 @@ describe('账号状态', () => {
|
||||
})
|
||||
|
||||
it('取回资料时一并取回账号', async () => {
|
||||
me = { member, account: { username: 'alice' } }
|
||||
me = { member, account: { username: 'alice', github: false } }
|
||||
const { profileStore } = await load()
|
||||
await profileStore.load()
|
||||
expect(profileStore.state).toMatchObject({ member, account: { username: 'alice' } })
|
||||
expect(profileStore.state).toMatchObject({ member, account: { username: 'alice', github: false } })
|
||||
})
|
||||
|
||||
it('游客没有账号', async () => {
|
||||
const { profileStore } = await load()
|
||||
await profileStore.registered({ username: 'alice' })
|
||||
await profileStore.registered({ username: 'alice', github: false })
|
||||
await profileStore.load()
|
||||
expect(profileStore.state).toMatchObject({ member, account: null })
|
||||
})
|
||||
@@ -183,18 +183,18 @@ describe('账号状态', () => {
|
||||
it('注册之后设置账号', async () => {
|
||||
const { profileStore } = await load()
|
||||
await profileStore.load()
|
||||
await profileStore.registered({ username: 'alice' })
|
||||
expect(profileStore.state).toMatchObject({ member, account: { username: 'alice' } })
|
||||
await profileStore.registered({ username: 'alice', github: false })
|
||||
expect(profileStore.state).toMatchObject({ member, account: { username: 'alice', github: false } })
|
||||
})
|
||||
|
||||
it('身份被拒绝或更换之后没有账号', async () => {
|
||||
const { profileStore, identityEvents } = await load()
|
||||
profileStore.start()
|
||||
await profileStore.registered({ username: 'alice' })
|
||||
await profileStore.registered({ username: 'alice', github: false })
|
||||
identityEvents.emit({ kind: 'rejected' })
|
||||
expect(profileStore.state).toMatchObject({ member: null, account: null })
|
||||
|
||||
await profileStore.registered({ username: 'alice' })
|
||||
await profileStore.registered({ username: 'alice', github: false })
|
||||
identityEvents.emit({ kind: 'renewed', member: renewed })
|
||||
expect(profileStore.state).toMatchObject({ member: renewed, account: null })
|
||||
})
|
||||
@@ -207,14 +207,14 @@ describe('邮箱', () => {
|
||||
|
||||
it('已注册时取自账号,不读本机', async () => {
|
||||
local.setItem(emailKey, 'local@example.com')
|
||||
me = { member, account: { username: 'alice', email: 'Alice@Example.COM' } }
|
||||
me = { member, account: { username: 'alice', email: 'Alice@Example.COM', github: false } }
|
||||
const { profileStore } = await load()
|
||||
profileStore.start()
|
||||
await profileStore.load()
|
||||
expect(profileStore.state.email).toBe('Alice@Example.COM')
|
||||
|
||||
local.removeItem(emailKey)
|
||||
me = { member, account: { username: 'alice' } }
|
||||
me = { member, account: { username: 'alice', github: false } }
|
||||
await profileStore.load()
|
||||
expect(profileStore.state.email).toBe('')
|
||||
})
|
||||
@@ -228,18 +228,18 @@ describe('邮箱', () => {
|
||||
})
|
||||
|
||||
it('已注册时保存到账号,不写本机', async () => {
|
||||
me = { member, account: { username: 'alice' } }
|
||||
me = { member, account: { username: 'alice', github: false } }
|
||||
const { profileStore } = await load()
|
||||
await profileStore.load()
|
||||
|
||||
expect(await profileStore.save({ email: ' Alice@Example.COM ' })).toBe(true)
|
||||
expect(updates).toEqual([{ email: ' Alice@Example.COM ' }])
|
||||
expect(profileStore.state).toMatchObject({ email: 'Alice@Example.COM', account: { username: 'alice', email: 'Alice@Example.COM' } })
|
||||
expect(profileStore.state).toMatchObject({ email: 'Alice@Example.COM', account: { username: 'alice', email: 'Alice@Example.COM', github: false } })
|
||||
expect(local.getItem(emailKey)).toBeNull()
|
||||
|
||||
expect(await profileStore.save({ email: '' })).toBe(true)
|
||||
expect(profileStore.state.email).toBe('')
|
||||
expect(profileStore.state.account).toEqual({ username: 'alice' })
|
||||
expect(profileStore.state.account).toEqual({ username: 'alice', github: false })
|
||||
})
|
||||
|
||||
it('游客保存到本机', async () => {
|
||||
@@ -255,17 +255,17 @@ describe('邮箱', () => {
|
||||
const { profileStore } = await load()
|
||||
profileStore.start()
|
||||
await profileStore.load()
|
||||
await profileStore.registered({ username: 'alice' })
|
||||
await profileStore.registered({ username: 'alice', github: false })
|
||||
|
||||
expect(updates).toEqual([{ email: 'guest@example.com' }])
|
||||
expect(local.getItem(emailKey)).toBeNull()
|
||||
expect(profileStore.state).toMatchObject({ email: 'guest@example.com', account: { username: 'alice', email: 'guest@example.com' } })
|
||||
expect(profileStore.state).toMatchObject({ email: 'guest@example.com', account: { username: 'alice', email: 'guest@example.com', github: false } })
|
||||
})
|
||||
|
||||
it('注册时本机没有邮箱就不发请求', async () => {
|
||||
const { profileStore } = await load()
|
||||
await profileStore.load()
|
||||
await profileStore.registered({ username: 'alice' })
|
||||
await profileStore.registered({ username: 'alice', github: false })
|
||||
expect(updates).toEqual([])
|
||||
})
|
||||
|
||||
@@ -274,21 +274,21 @@ describe('邮箱', () => {
|
||||
patchFails = true
|
||||
const { profileStore } = await load()
|
||||
profileStore.start()
|
||||
await profileStore.registered({ username: 'alice' })
|
||||
await profileStore.registered({ username: 'alice', github: false })
|
||||
expect(local.getItem(emailKey)).toBe('guest@example.com')
|
||||
|
||||
patchFails = false
|
||||
me = { member, account: { username: 'alice' } }
|
||||
me = { member, account: { username: 'alice', github: false } }
|
||||
await profileStore.load()
|
||||
expect(updates).toEqual([{ email: 'guest@example.com' }])
|
||||
expect(local.getItem(emailKey)).toBeNull()
|
||||
expect(profileStore.state).toMatchObject({ email: 'guest@example.com', account: { username: 'alice', email: 'guest@example.com' } })
|
||||
expect(profileStore.state).toMatchObject({ email: 'guest@example.com', account: { username: 'alice', email: 'guest@example.com', github: false } })
|
||||
})
|
||||
|
||||
it('服务端判定本机的邮箱不合格时删除副本,之后加载不再转存', async () => {
|
||||
local.setItem(emailKey, 'legacy')
|
||||
patchError = { status: 400, code: 'INVALID_ARGUMENT' }
|
||||
me = { member, account: { username: 'alice' } }
|
||||
me = { member, account: { username: 'alice', github: false } }
|
||||
const { profileStore } = await load()
|
||||
await profileStore.load()
|
||||
expect(local.getItem(emailKey)).toBeNull()
|
||||
@@ -301,7 +301,7 @@ describe('邮箱', () => {
|
||||
it('重试仍失败时保留本机的副本,下次加载再试', async () => {
|
||||
local.setItem(emailKey, 'guest@example.com')
|
||||
patchFails = true
|
||||
me = { member, account: { username: 'alice' } }
|
||||
me = { member, account: { username: 'alice', github: false } }
|
||||
const { profileStore } = await load()
|
||||
await profileStore.load()
|
||||
expect(local.getItem(emailKey)).toBe('guest@example.com')
|
||||
@@ -313,26 +313,26 @@ describe('邮箱', () => {
|
||||
})
|
||||
|
||||
it('已注册且本机没有副本,或账号已有邮箱时,加载不发更新请求', async () => {
|
||||
me = { member, account: { username: 'alice' } }
|
||||
me = { member, account: { username: 'alice', github: false } }
|
||||
const { profileStore } = await load()
|
||||
await profileStore.load()
|
||||
|
||||
local.setItem(emailKey, 'guest@example.com')
|
||||
me = { member, account: { username: 'alice', email: 'alice@example.com' } }
|
||||
me = { member, account: { username: 'alice', email: 'alice@example.com', github: false } }
|
||||
await profileStore.load()
|
||||
expect(updates).toEqual([])
|
||||
})
|
||||
|
||||
it('用户清空账号的邮箱时一并删除本机残留的副本,之后加载不再转存', async () => {
|
||||
local.setItem(emailKey, 'guest@example.com')
|
||||
me = { member, account: { username: 'alice', email: 'alice@example.com' } }
|
||||
me = { member, account: { username: 'alice', email: 'alice@example.com', github: false } }
|
||||
const { profileStore } = await load()
|
||||
await profileStore.load()
|
||||
expect(await profileStore.save({ email: '' })).toBe(true)
|
||||
expect(local.getItem(emailKey)).toBeNull()
|
||||
|
||||
updates.length = 0
|
||||
me = { member, account: { username: 'alice' } }
|
||||
me = { member, account: { username: 'alice', github: false } }
|
||||
await profileStore.load()
|
||||
expect(updates).toEqual([])
|
||||
})
|
||||
|
||||
@@ -90,7 +90,11 @@ class ProfileStore extends Store<ProfileState> {
|
||||
// 用户对账号邮箱的修改是最终决定,本机残留的副本不能再覆盖它。
|
||||
if (changes.email !== undefined)
|
||||
localStorage.removeItem(emailKey)
|
||||
this.setState({ member, email, account: { username: account.username, ...(email && { email }) }, failed: false })
|
||||
const next: Account = { ...account }
|
||||
delete next.email
|
||||
if (email)
|
||||
next.email = email
|
||||
this.setState({ member, email, account: next, failed: false })
|
||||
}
|
||||
else {
|
||||
const email = changes.email ?? this.state.email
|
||||
|
||||
Reference in new issue
Block a user