Phase 3: account linking and two-way chat relay
Test-first from here on (per request after Phase 2): every module below was written test-then-implementation, confirmed red before green. api: accounts/sessions/chat_messages tables, plus servers.anonymousChatAllowed. Online accounts merge into one global identity per real Mojang uuid (partial unique index on mc_uuid WHERE server_id IS NULL); offline accounts are scoped per-server (partial unique index on (server_id, mc_uuid)) — see schema.ts's accounts comment and link.test.ts's merge-scoping tests. link.ts: storeLinkCode/redeemLinkCode (single-use, Redis-backed with a 10-minute TTL) and session lookup/revocation. Sessions come back in the HTTP response body rather than an httpOnly cookie — a deliberate MVP simplification (see link.ts's doc comment) that sidesteps needing to verify exactly how Elysia's .ws() routes surface cookies; the client sends the token back via X-MCMapper-Session. chat.ts/chat-gateway.ts: mod-originated chat (ws-gateway.ts's new "chat" and "link_request" message types) and browser-originated chat (/ws/chat/:serverId) both persist to chat_messages and publish to a per-server Redis pub/sub channel; browser chat additionally resolves identity (linked session > nickname > rejected if anonymous chat is disabled for that server) and forwards to the mod's own connection via a new serverId->socket registry in ws-gateway.ts (getModSocket). frontend: chat panel + link-code entry on the 2D map page, session token kept in localStorage (matching the no-cookie tradeoff above). Verified end-to-end against live containers, including through the real mod-side Java client: a link code generated by DefaultBackendConnection round-trips through actual HTTP redemption to the correct account, and a browser chat message correctly forwards through to the mod's live ChatListener callback (not just persisted/published).
This commit is contained in:
@@ -20,5 +20,41 @@ html(lang="en")
|
||||
span(x-text="server?.name")
|
||||
span.text-sm.text-neutral-500(x-show="loading") loading servers…
|
||||
span.text-sm.text-red-400(x-show="!loading && !server") No server registered yet — see backend README (bun run seed).
|
||||
div#map.flex-1
|
||||
div.flex-1.flex.overflow-hidden
|
||||
div#map.flex-1
|
||||
aside.w-80.flex.flex-col.border-l.border-neutral-700.bg-neutral-800(x-show="server")
|
||||
div.flex-1.overflow-y-auto.p-2.space-y-1(x-ref="chatLog")
|
||||
template(x-for="msg in chatMessages" x-bind:key="msg.id")
|
||||
p.text-sm.break-words
|
||||
span.font-semibold(x-text="msg.username")
|
||||
span.text-neutral-500.text-xs(x-show="msg.source === 'game'") [game]
|
||||
span.text-neutral-400 :
|
||||
span(x-text="msg.message")
|
||||
|
||||
div.p-2.border-t.border-neutral-700.space-y-2
|
||||
template(x-if="account")
|
||||
div.text-xs.text-neutral-400.flex.items-center.gap-2
|
||||
span
|
||||
| Linked as
|
||||
span.font-semibold.text-neutral-200(x-text="' ' + account.username")
|
||||
button.underline(x-on:click="unlink") unlink
|
||||
|
||||
template(x-if="!account")
|
||||
input.w-full.bg-neutral-900.text-sm.px-2.py-1.rounded.border.border-neutral-700(
|
||||
type="text" placeholder="nickname (optional)" x-model="nickname"
|
||||
x-on:change="saveNickname")
|
||||
|
||||
div.flex.gap-1
|
||||
input.flex-1.bg-neutral-900.text-sm.px-2.py-1.rounded.border.border-neutral-700(
|
||||
type="text" placeholder="message" x-model="chatInput"
|
||||
x-on:keydown.enter="sendChat")
|
||||
button.px-2.py-1.bg-neutral-700.rounded.text-sm(x-on:click="sendChat") Send
|
||||
|
||||
template(x-if="!account")
|
||||
div.flex.gap-1
|
||||
input.flex-1.bg-neutral-900.text-sm.px-2.py-1.rounded.border.border-neutral-700(
|
||||
type="text" placeholder="link code from /mcmapper link" x-model="linkCode")
|
||||
button.px-2.py-1.bg-neutral-700.rounded.text-sm(x-on:click="redeemLink") Link
|
||||
|
||||
p.text-xs.text-amber-400(x-show="linkStatus" x-text="linkStatus")
|
||||
script(src="/js/map.js")
|
||||
|
||||
Reference in New Issue
Block a user