feat(frontend): login with center URL and consume center-issued guild tokens

This commit is contained in:
nav
2026-05-13 08:00:23 +00:00
parent 66c49ff654
commit c906cde209
10 changed files with 170 additions and 125 deletions

View File

@@ -1,7 +1,8 @@
import axios from 'axios'
import { io, type Socket } from 'socket.io-client'
import { useEffect, useMemo, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { getApiClient } from '../lib/api-client'
import { disconnectSocket, getSocketClient } from '../lib/socket-client'
import { useAuth } from '../auth/auth-context'
type MessageItem = {
messageId: string
@@ -17,6 +18,7 @@ type PageInfo = {
}
export default function ChatPage() {
const { session } = useAuth()
const [searchParams, setSearchParams] = useSearchParams()
const guildId = searchParams.get('guildId') ?? ''
const [channelId, setChannelId] = useState(() => searchParams.get('channelId') ?? '')
@@ -34,14 +36,42 @@ export default function ChatPage() {
const [editingMessageId, setEditingMessageId] = useState('')
const [editingContent, setEditingContent] = useState('')
const socket = useMemo(() => getSocketClient('frontend-user'), [])
const guild = useMemo(() => (session?.guilds ?? []).find((g) => g.nodeId === guildId) ?? null, [session, guildId])
const guildToken = useMemo(
() => (session?.guildAccessTokens ?? []).find((x) => x.guildNodeId === guildId)?.token ?? '',
[session, guildId],
)
function guildApi() {
if (!guild || !guildToken) throw new Error('guild or token missing')
return axios.create({
baseURL: `${guild.endpoint}/api`,
timeout: 10000,
headers: {
Authorization: `Bearer ${guildToken}`,
},
})
}
const socket = useMemo<Socket | null>(() => {
if (!guild || !guildToken) return null
return io(`${guild.endpoint}/realtime`, {
transports: ['websocket'],
auth: {
token: guildToken,
},
autoConnect: false,
})
}, [guild, guildToken])
useEffect(() => {
if (!socket) return
socket.on('connect', () => {
setSocketState('online')
if (!channelId) return
socket.emit('join_channel', { channelId })
void getApiClient()
void guildApi()
.get(`/channels/${channelId}/messages`, {
params: {
seq_from: Number(seqFrom || '1'),
@@ -55,14 +85,10 @@ export default function ChatPage() {
})
})
socket.on('disconnect', () => setSocketState('offline'))
socket.on('presence.online', (e: { onlineCount?: number }) => {
setOnlineCount(e.onlineCount ?? 0)
})
socket.on('presence.offline', (e: { onlineCount?: number }) => {
setOnlineCount(e.onlineCount ?? 0)
})
socket.on('presence.online', (e: { onlineCount?: number }) => setOnlineCount(e.onlineCount ?? 0))
socket.on('presence.offline', (e: { onlineCount?: number }) => setOnlineCount(e.onlineCount ?? 0))
socket.on('typing.start', (e: { channelId?: string; userId?: string }) => {
if (!e.channelId || e.channelId !== channelId || !e.userId || e.userId === 'frontend-user') return
if (!e.channelId || e.channelId !== channelId || !e.userId || e.userId === session?.user.id) return
setTypingUsers((prev) => (prev.includes(e.userId as string) ? prev : [...prev, e.userId as string]))
})
socket.on('typing.stop', (e: { channelId?: string; userId?: string }) => {
@@ -78,20 +104,20 @@ export default function ChatPage() {
prev.map((x) => (x.messageId === m.messageId ? { ...x, isDeleted: true, content: '[deleted]' } : x)),
),
)
socket.connect()
socket.connect()
return () => {
socket.removeAllListeners()
disconnectSocket()
socket.disconnect()
}
}, [socket, channelId, seqFrom, seqTo, limit])
}, [socket, channelId, seqFrom, seqTo, limit, guild, guildToken, session?.user.id])
async function pullMessages() {
if (!channelId) return
if (!channelId || !guild || !guildToken) return
setLoading(true)
setError('')
try {
const res = await getApiClient().get(`/channels/${channelId}/messages`, {
const res = await guildApi().get(`/channels/${channelId}/messages`, {
params: {
seq_from: Number(seqFrom || '1'),
seq_to: Number(seqTo || '999999'),
@@ -108,14 +134,14 @@ export default function ChatPage() {
}
async function sendMessage() {
if (!channelId || !content.trim()) return
if (!channelId || !content.trim() || !guild || !guildToken) return
setError('')
try {
await getApiClient().post(`/channels/${channelId}/messages`, {
await guildApi().post(`/channels/${channelId}/messages`, {
content,
authorUserId: 'frontend-user',
authorUserId: session?.user.id ?? 'unknown',
})
socket.emit('typing.stop', { channelId })
socket?.emit('typing.stop', { channelId })
setContent('')
} catch {
setError('发送失败')
@@ -123,10 +149,10 @@ export default function ChatPage() {
}
async function editMessage() {
if (!channelId || !editingMessageId || !editingContent.trim()) return
if (!channelId || !editingMessageId || !editingContent.trim() || !guild || !guildToken) return
setError('')
try {
await getApiClient().patch(`/channels/${channelId}/messages/${editingMessageId}`, {
await guildApi().patch(`/channels/${channelId}/messages/${editingMessageId}`, {
content: editingContent,
})
setEditingContent('')
@@ -137,10 +163,10 @@ export default function ChatPage() {
}
async function deleteMessage(messageId: string) {
if (!channelId || !messageId) return
if (!channelId || !messageId || !guild || !guildToken) return
setError('')
try {
await getApiClient().delete(`/channels/${channelId}/messages/${messageId}`)
await guildApi().delete(`/channels/${channelId}/messages/${messageId}`)
await pullMessages()
} catch {
setError('删除失败')
@@ -161,13 +187,13 @@ export default function ChatPage() {
const wasEmpty = !content.trim()
const nowEmpty = !value.trim()
setContent(value)
if (!channelId) return
if (!channelId || !socket) return
if (wasEmpty && !nowEmpty) socket.emit('typing.start', { channelId })
if (!wasEmpty && nowEmpty) socket.emit('typing.stop', { channelId })
}
useEffect(() => {
if (!channelId || !socket.connected) return
if (!channelId || !socket || !socket.connected) return
socket.emit('join_channel', { channelId })
return () => {
socket.emit('leave_channel', { channelId })
@@ -195,11 +221,7 @@ export default function ChatPage() {
<button onClick={sendMessage}></button>
</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<input
value={editingMessageId}
onChange={(e) => setEditingMessageId(e.target.value)}
placeholder="messageId"
/>
<input value={editingMessageId} onChange={(e) => setEditingMessageId(e.target.value)} placeholder="messageId" />
<input value={editingContent} onChange={(e) => setEditingContent(e.target.value)} placeholder="新内容" />
<button onClick={editMessage}></button>
</div>
@@ -216,8 +238,8 @@ export default function ChatPage() {
{!loading && !messages.length ? <p></p> : null}
{pageInfo ? (
<p>
next_expected_seq: {pageInfo.nextExpectedSeq ?? '-'} | highest_committed_seq:{' '}
{pageInfo.highestCommittedSeq ?? '-'} | has_more: {String(pageInfo.hasMore ?? false)}
next_expected_seq: {pageInfo.nextExpectedSeq ?? '-'} | highest_committed_seq: {pageInfo.highestCommittedSeq ?? '-'} |
has_more: {String(pageInfo.hasMore ?? false)}
</p>
) : null}
</section>