diff --git a/src/auth/AuthContext.tsx b/src/auth/AuthContext.tsx index b6da71b..ca5d3e2 100644 --- a/src/auth/AuthContext.tsx +++ b/src/auth/AuthContext.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react' import type { PropsWithChildren } from 'react' import { clearAuthSession, getAuthSession, isAccessTokenStale, setAuthSession } from '../lib/auth-storage' import type { AuthSession } from '../lib/auth-storage' -import { loginCenter, logoutCenter, refreshCenter } from '../lib/center-auth-client' +import { loginCenter, logoutCenter, meGuildsCenter, refreshCenter } from '../lib/center-auth-client' import { AuthContext } from './auth-context' import type { AuthContextValue } from './auth-context' @@ -45,6 +45,33 @@ export function AuthProvider({ children }: PropsWithChildren) { setSession(next) return next.accessToken }, + refreshGuilds: async () => { + if (!session) return + let accessToken = session.accessToken + let refreshToken = session.refreshToken + let tokenType = session.tokenType + let expiresIn = session.expiresIn + if (isAccessTokenStale(session.accessToken)) { + const refreshed = await refreshCenter(session.centerApiBase, session.refreshToken) + accessToken = refreshed.accessToken + refreshToken = refreshed.refreshToken + tokenType = refreshed.tokenType + expiresIn = refreshed.expiresIn + } + + const guildData = await meGuildsCenter(session.centerApiBase, accessToken) + const next: AuthSession = { + ...session, + accessToken, + refreshToken, + tokenType, + expiresIn, + guilds: guildData.guilds, + guildAccessTokens: guildData.guildAccessTokens, + } + setAuthSession(next) + setSession(next) + }, }), [session], ) diff --git a/src/auth/auth-context.ts b/src/auth/auth-context.ts index 51c816e..243e7ec 100644 --- a/src/auth/auth-context.ts +++ b/src/auth/auth-context.ts @@ -7,6 +7,7 @@ export type AuthContextValue = { login: (centerApiBase: string, email: string, password: string) => Promise logout: () => Promise ensureFreshToken: () => Promise + refreshGuilds: () => Promise } export const AuthContext = createContext(null) diff --git a/src/index.css b/src/index.css index d87cb54..bc7c327 100644 --- a/src/index.css +++ b/src/index.css @@ -152,6 +152,31 @@ a:hover { gap: 8px; } +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + display: grid; + place-items: center; + z-index: 30; +} + +.modal-card { + width: min(560px, 92vw); + max-height: 80vh; + overflow: auto; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 12px; + padding: 14px; +} + +.modal-list { + display: grid; + gap: 8px; + margin-top: 8px; +} + .list-btn { width: 100%; text-align: left; diff --git a/src/lib/center-auth-client.ts b/src/lib/center-auth-client.ts index 3a7580b..1f3b6fd 100644 --- a/src/lib/center-auth-client.ts +++ b/src/lib/center-auth-client.ts @@ -20,6 +20,11 @@ type RefreshResponse = { expiresIn?: number } +type MeGuildsResponse = { + guilds: Array<{ nodeId: string; name: string; endpoint: string; status: 'active' | 'offline' | 'revoked' }> + guildAccessTokens: Array<{ guildNodeId: string; token: string; tokenType: string; expiresIn?: number }> +} + function centerClient(centerApiBase: string) { const client = axios.create({ baseURL: centerApiBase, @@ -49,3 +54,30 @@ export async function refreshCenter(centerApiBase: string, refreshToken: string) export async function logoutCenter(centerApiBase: string, refreshToken: string): Promise { await centerClient(centerApiBase).post('/auth/logout', { refreshToken }) } + +export async function meGuildsCenter(centerApiBase: string, accessToken: string): Promise { + const res = await centerClient(centerApiBase).get('/auth/me/guilds', { + headers: { Authorization: `Bearer ${accessToken}` }, + }) + return res.data +} + +export async function joinGuildCenter(centerApiBase: string, accessToken: string, guildNodeId: string): Promise { + await centerClient(centerApiBase).post( + '/auth/me/guilds/join', + { guildNodeId }, + { headers: { Authorization: `Bearer ${accessToken}` } }, + ) +} + +export async function guildMembersCenter( + centerApiBase: string, + accessToken: string, + guildNodeId: string, +): Promise> { + const res = await centerClient(centerApiBase).get>( + `/auth/guilds/${encodeURIComponent(guildNodeId)}/members`, + { headers: { Authorization: `Bearer ${accessToken}` } }, + ) + return Array.isArray(res.data) ? res.data : [] +} diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index d8b11cb..9f1d41c 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -2,6 +2,7 @@ import axios from 'axios' import { io, type Socket } from 'socket.io-client' import { useEffect, useMemo, useState } from 'react' import { useAuth } from '../auth/auth-context' +import { guildMembersCenter, joinGuildCenter } from '../lib/center-auth-client' type MessageItem = { messageId: string @@ -11,22 +12,24 @@ type MessageItem = { } type GuildChannel = { id: string; name: string; guildId?: string } -type GuildRecord = { id: string; name: string } -type GuildMember = { id: string; userId: string; guildId: string; status: string } +type MemberItem = { userId: string; email: string; status: string } export default function ChatPage() { - const { session, logout } = useAuth() + const { session, logout, ensureFreshToken, refreshGuilds } = useAuth() const [selectedGuildId, setSelectedGuildId] = useState('') const [selectedChannelId, setSelectedChannelId] = useState('') const [channels, setChannels] = useState([]) const [guildDbId, setGuildDbId] = useState('') - const [members, setMembers] = useState([]) - const [showGuilds, setShowGuilds] = useState(true) - const [showChannels, setShowChannels] = useState(true) - const [showMembers, setShowMembers] = useState(true) + const [members, setMembers] = useState([]) const [messages, setMessages] = useState([]) const [content, setContent] = useState('') const [newChannelName, setNewChannelName] = useState('') + const [joinGuildNodeId, setJoinGuildNodeId] = useState('') + const [selectedMemberIds, setSelectedMemberIds] = useState([]) + const [showCreateChannelModal, setShowCreateChannelModal] = useState(false) + const [showGuilds, setShowGuilds] = useState(true) + const [showChannels, setShowChannels] = useState(true) + const [showMembers, setShowMembers] = useState(true) const [loading, setLoading] = useState(false) const [error, setError] = useState('') @@ -64,9 +67,7 @@ export default function ChatPage() { if (!guild || !guildToken) return setError('') try { - const res = await guildApi().get('/channels', { - params: guildDbId ? { guildId: guildDbId } : undefined, - }) + const res = await guildApi().get('/channels', { params: guildDbId ? { guildId: guildDbId } : undefined }) const list = Array.isArray(res.data) ? (res.data as GuildChannel[]) : [] setChannels(list) if (!guildDbId && list[0]?.guildId) setGuildDbId(list[0].guildId) @@ -77,20 +78,14 @@ export default function ChatPage() { } } - async function loadGuildMetaAndMembers() { - if (!guild || !guildToken) return + async function loadMembers() { + if (!session || !selectedGuildId) return setError('') try { - const guildRes = await guildApi().get('/guilds') - const guildList = Array.isArray(guildRes.data) ? (guildRes.data as GuildRecord[]) : [] - const nextGuildDbId = guildList[0]?.id ?? '' - setGuildDbId(nextGuildDbId) - - const memberRes = await guildApi().get('/members', { - params: nextGuildDbId ? { guildId: nextGuildDbId } : undefined, - }) - const memberList = Array.isArray(memberRes.data) ? (memberRes.data as GuildMember[]) : [] - setMembers(memberList) + const token = await ensureFreshToken() + if (!token) return + const list = await guildMembersCenter(session.centerApiBase, token, selectedGuildId) + setMembers(list) } catch { setError('Failed to load guild members') setMembers([]) @@ -128,6 +123,24 @@ export default function ChatPage() { } } + async function addGuild() { + if (!session || !joinGuildNodeId.trim()) return + setError('') + try { + const token = await ensureFreshToken() + if (!token) return + await joinGuildCenter(session.centerApiBase, token, joinGuildNodeId.trim()) + await refreshGuilds() + setJoinGuildNodeId('') + } catch { + setError('Failed to add guild') + } + } + + function toggleMember(userId: string) { + setSelectedMemberIds((prev) => (prev.includes(userId) ? prev.filter((x) => x !== userId) : [...prev, userId])) + } + async function createChannel() { if (!guild || !guildToken || !newChannelName.trim()) return if (!guildDbId) { @@ -136,9 +149,16 @@ export default function ChatPage() { } setError('') try { - const res = await guildApi().post('/channels', { name: newChannelName.trim(), guildId: guildDbId }) + const payload = { + name: newChannelName.trim(), + guildId: guildDbId, + memberUserIds: selectedMemberIds, + } + const res = await guildApi().post('/channels', payload) const createdId = res.data?.id as string | undefined setNewChannelName('') + setSelectedMemberIds([]) + setShowCreateChannelModal(false) await loadChannels() if (createdId) setSelectedChannelId(createdId) } catch { @@ -147,7 +167,8 @@ export default function ChatPage() { } useEffect(() => { - void loadGuildMetaAndMembers() + void loadMembers() + setSelectedMemberIds([]) }, [selectedGuildId]) useEffect(() => { @@ -186,6 +207,10 @@ export default function ChatPage() {

Guilds

+
+ setJoinGuildNodeId(e.target.value)} placeholder="Guild nodeId" /> + +
    {guilds.map((g) => (
  • @@ -200,13 +225,7 @@ export default function ChatPage() {

    Channels

    - setNewChannelName(e.target.value)} - placeholder="New channel name" - /> - +
      {channels.map((c) => ( @@ -244,9 +263,7 @@ export default function ChatPage() {

      Members

        {members.map((m) => ( -
      • - {m.userId} -
      • +
      • {m.email}
      • ))}
    @@ -259,6 +276,28 @@ export default function ChatPage() {
+ + {showCreateChannelModal ? ( +
setShowCreateChannelModal(false)}> +
e.stopPropagation()}> +

Create channel

+ setNewChannelName(e.target.value)} placeholder="Channel name" /> +

Select members to include

+
+ {members.map((m) => ( + + ))} +
+
+ + +
+
+
+ ) : null} ) }