Commit Graph
21 Commits
Author SHA1 Message Date
Claude 8c590c0b6d fix(server): harden private-channel visibility propagation
Three review findings on the #93 feature:

- RefreshChannelVisibility targeted clients by their connect-time role
  snapshot; a user whose role changed mid-session was evaluated against the
  stale role. Resolve the current role from the DB per client (fail closed).
- Visibility updates are targeted, unsequenced messages, so a client that
  disconnected before an override change and later resumed via replay never
  converged (stale sidebar until a fresh connect). Track a visibility-change
  sequence watermark and force resumes from at/before it onto the
  full-ready path.
- The admin SPA interpolated channel/user names into single-quoted JS
  strings inside onclick attributes with HTML-escaping only; a name
  containing a quote broke out of the string literal (XSS in the admin
  panel, reachable by any user allowed to create channels). Add a jsq()
  helper (JS-escape then HTML-escape) and use it for every onclick name
  interpolation.

Follow-up to #93.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
2026-07-19 11:20:15 +00:00
Claude 9e6ff47194 feat(server,admin): private channels via per-role permission overrides
The read side of channel visibility was already complete — channel_overrides
rows deny ReadMessages and every list/focus/send/voice path enforces them —
but nothing could write those rows. Add the missing write path and UI:

- db: UpsertChannelOverride / DeleteChannelOverride / ListChannelRoleOverrides
  (roles LEFT JOIN overrides so the UI gets everything in one call)
- admin API: GET/PUT/DELETE /admin/api/channels/{id}/permissions[/{roleId}]
  with unknown permission bits masked via the new permissions.AllPerms,
  audit logging, and immediate permission-cache invalidation
- ws: Hub.RefreshChannelVisibility sends targeted channel_create /
  channel_delete to connected clients after an override change, unsubscribes
  hidden clients from the channel topic, and clears their focus. Sent outside
  the sequenced replay path on purpose: a replayed channel_delete would be
  filtered by the post-change allowed-channel set, inverting its audience.
- admin panel: per-channel Access modal (lock icon) with per-role
  "Can access" checkboxes; unchecking writes deny = ReadMessages|ConnectVoice

Known limits (follow-ups): users offline during a revoke keep a stale
sidebar entry until their next fresh connect (server still denies access),
and users already in a voice channel are not kicked when it goes private.

Closes #93

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
2026-07-19 10:50:11 +00:00
J3vbandClaude Fable 5 20e9e2f6a5 fix(ws): don't clean voice state when a replacement connection exists
readPump teardown always ran handleVoiceLeave, trusting the joined_at
guard in LeaveVoiceChannelIfMatch to protect replacement sessions. On
reconnect the voice session TRANSFERS to the replacement client with
the same joined_at, so the guard cannot tell the two apart: whenever
teardown snapshotted voiceChID before the transfer zeroed it, the old
connection deleted the replacement's voice_state row (flaked on the
Windows CI runner as TestServeWS_Reconnect_PreservesVoiceState).

Gate voice cleanup on !replaced — the same condition the
presence-offline broadcast four lines down already uses. A genuinely
final disconnect behaves exactly as before, and a stale row from a
crashed replacement is still swept by the fresh-connect cleanup.

Also deflake TestHub_ConcurrentRegisterUnregister: poll for quiescence
with a deadline instead of a fixed 50ms sleep that loses to the -race
scheduler on slow runners.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:50:38 +02:00
Claude 1c5fbbc246 add service layer foundation (Phase A, Step 1)
Introduce Server/service/ package with MessageService, ChannelService,
and PermissionService that encapsulate business logic previously
scattered across REST and WS handlers. The PermissionService adds
per-user in-memory caching with TTL-based expiry to eliminate
per-message DB round-trips at scale.

Services are wired into the WS hub via deps structs (strangler-fig
pattern) — existing handlers continue to work unchanged, with service
references available for incremental migration.

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
2026-04-05 20:42:32 +00:00
J3vb 3d18ce4f99 fix: Go E2EE security hardening — key holder tracking, base64 loose validation, rate limits, test schema
- I-1: Add key holder election in Hub (lowest userID per channel); reject
  non-key-holder voice_e2ee_offer with NOT_KEY_HOLDER error
- I-2: Accept raw (unpadded) base64 in E2EE announce/offer handlers via
  decodeBase64Loose fallback
- I-6: Copy E2EE public key value while h.mu.RLock is held in getClientE2EEPubKey
- I-7: Lower loginRateLimitPerMinute from 60 to 5
- C-1: TOCTOU fix — target channel check held under same lock as client lookup
- C-2: Include is_key_holder bool in voice_token payload so client knows
  whether to initiate key distribution
- M-5/M-6: Add ErrCodeBadPayload/ErrCodeNotKeyHolder error constants
- Fix pre-existing api build errors: block_handler.go getUserFromContext,
  router.go RequirePermission arg count
- Add user_blocks table to all test DB schemas (ws, api DM)
- Add voice_e2ee_test.go and constants_test.go covering all fixes
2026-04-04 23:16:31 +02:00
J3vb c3a8aa477c fix: resolve 20 code review bugs across Rust, TypeScript, and Go
Critical/High Rust (Tauri client):
- BUG-140: replace .run() with .build() + RunEvent::Exit handler; native error dialog on startup failure
- BUG-141: eliminate PTT thread TOCTOU race with Mutex critical section; add AtomicBool shutdown and catch_unwind
- BUG-144: fix TOFU cert store corruption — read-before-write rollback restores previous fingerprint on save failure (all 3 write sites)
- BUG-145: add VK code range guard (1..=254) in is_key_down; fix cast to (state as i16) < 0
- BUG-147: replace bare spawns with JoinSet; abort_all + drain on exit; unconditional closed event
- BUG-150: add CRLF guard in handle_connection before header rewriting
- BUG-151: wrap header read loop in tokio::time::timeout(10s)
- BUG-158: extract CERTS_STORE/SETTINGS_STORE to constants.rs (eliminate 3 duplicates)
- HIGH-2: PTT thread self-cleanup uses unwrap_or_else defensive pattern
- HIGH-4: ws_send distinguishes Full vs Closed errors; warn log on backpressure

Critical/High TypeScript (Tauri client):
- BUG-142: join-generation counter prevents stale connectAndSetup completions
- BUG-143: replace 8 mutable LiveKit session fields with discriminated union SessionState
- BUG-146: 60s token refresh deadline; cleared on reply or voice leave
- BUG-148: ResizeObserver hoisted to outer scope; disconnect() in destroy() before ac.abort()
- BUG-152: dismissSignal.aborted guard already present (no change needed)
- BUG-153: measureRendered split into two-pass read-then-write; eliminates per-message reflow
- BUG-154: WS dedup cache batch-evicts to 80% on overflow (amortised O(1))
- BUG-157: pendingUpdates replaced with coalesced function-composition slot (O(1) queue depth)

Go server:
- BUG-149: safe two-value type assertion in getOutboundIP with localhost fallback
- BUG-155: broadcast buffer 256→1024; broadcastDrops atomic counter exposed in /api/v1/metrics
- BUG-156: LiveKitHealthCheck and implementations accept ctx context.Context; all call sites pass r.Context() (12 files)
- BUG-159: MaxMessageBytes constant in config/constants.go; replaces 1<<20 literals in serve.go and updater.go
- HIGH-1: cert store rollback reads old value before write; restores previous cert on save failure

All validation passes: go build, go vet, cargo check, npm typecheck
2026-04-03 23:18:06 +02:00
J3vb 5edebb43b3 fix: add 30s session sweep to kick revoked WS connections (BUG-109)
Idle WebSocket connections only revalidated sessions every 10 sent
messages, allowing revoked tokens to stay connected indefinitely.
Add sweepRevokedSessions() on a 30s ticker that checks all connected
clients against the DB and kicks any with deleted/expired sessions
or banned users.
2026-04-02 12:12:56 +02:00
jevb 384e94d9f9 fix: close 3 security audit findings (BUG-108, BUG-122, BUG-126)
BUG-122: Remove channelID==0 bypass in deliverBroadcast that leaked
all channel-scoped broadcasts to unfocused clients. Clients must now
send channel_focus to receive channel events.

BUG-126: Reject edits and reactions on soft-deleted messages in
handleChatEdit and handleReaction.

BUG-108: Revoke all other sessions when a user changes their password
or enables/disables TOTP 2FA. Adds DeleteOtherSessions DB function.

7 new test cases covering all three fixes.
2026-04-02 10:37:34 +02:00
jevb f3c9f98b91 fix: resolve 4 server bugs (Phase 1: BUG-085, BUG-087, BUG-090, BUG-091)
- BUG-085: ring buffer EventsSince off-by-one — change < to <= so
  afterSeq == oldestSeq returns nil (triggers full ready payload)
- BUG-087: GracefulStop not idempotent — wrap body in sync.Once to
  prevent double lkProcess.Stop() on concurrent calls
- BUG-090: FTS query truncation at byte boundary — use []rune
  truncation to preserve valid UTF-8 for CJK/emoji input
- BUG-091: updater downloadFile double-closes file on Windows —
  add closed sentinel to guard defer against explicit Close()
2026-04-01 17:52:06 +02:00
jevb 2a62f31c39 test: boost server coverage — auth 60→95%, db 69→81%, config 75→85%
Add comprehensive tests across all Go packages:
- auth: username validation, concurrent rate limiting, TOTP stores, timing
- config: env overrides, default credential detection, voice defaults
- db: search, message queries, special char handling
- api: handler edge cases, error paths, DM/invite/TOTP coverage
- ws: voice handler paths, integration scenarios
- updater: version comparison, timeout handling

6 of 8 packages now at 80%+ coverage.
2026-04-01 11:38:11 +02:00
jevb deda095db1 test: add WS coverage tests + refactor handlers, update docs
Add hub, livekit, export, and coverage boost tests for Server/ws.
Refactor handlers_chat.go and serve.go for testability.
Sync docs: fix backup endpoint path, add DELETE /auth/account,
add audit logging + account deletion to security.md.
2026-03-31 18:07:23 +02:00
jevb 76cb9b9630 fix: security hardening, DM auth, LiveKit stability, and voice call timer
Security fixes (from multi-reviewer code review):
- Add DM participant auth checks to channel_focus, typing, and REST
  message endpoints — prevents unauthorized access to DM channels
- Fix TOCTOU race in GetOrCreateDMChannel using IMMEDIATE transaction
- Validate YAML credentials before LiveKit config interpolation
- Add CSS variable injection prevention in custom theme loader
- Validate localStorage JSON before unsafe type casts

LiveKit stability:
- Track remote mic audio elements for cleanup on abnormal disconnect
- Remove duplicate token refresh timer scheduling
- Add .catch() to all floating applyMicMuteState promises
- Clear reconnectAc after async post-connect work completes
- Fix double cmd.Wait() race in LiveKit process Stop()
- Reorder voice_join guards: validate channel before livekit==nil check
- Add startup warning for external LiveKit webhook CIDR mismatch

DM system fixes:
- Emit dm_channel_close WebSocket event from REST close handler
- Re-open DM for caller when channel already exists
- Fix unread count incrementing for own messages and active DMs
- Reset channelBeforeDm after Back navigation (stale state bug)

New feature:
- Voice call duration timer in VoiceWidget (MM:SS / HH:MM:SS elapsed)
- Accent color restored on app startup (was only applied in settings)

Test infrastructure:
- Add DM tables to all test schemas (hubTestSchema)
- Inject test LiveKit client in voice handler tests (fixes 28 failures)
2026-03-27 16:14:54 +01:00
jevb 3236918012 refactor: server hardening + client decomposition + protocol resilience
Server:
- Split monolithic voice_handlers.go into voice_join/leave/controls/broadcast
- Add metrics endpoint (admin-IP-restricted /api/v1/metrics)
- Add orphaned attachment cleanup in maintenance loop
- Add sentinel errors (db/errors.go, ws/errors.go)
- Add ring buffer for event replay on reconnect
- Add heartbeat monitoring with stale connection sweep
- Improve hub with panic recovery, graceful shutdown, seq tracking
- Typed message structs replace raw map[string]interface{}

Client:
- Decompose MainPage into ChatArea + SidebarArea controllers
- Add disposable.ts lifecycle management pattern
- Add member list right-click context menu (kick/ban/role)
- Tighten CSP (media-src, font-src, object-src, base-uri)
- Improve store with shallowEqual, 500-msg cap, batch updates
- Add search API endpoint wiring
- Fix LiveKit session cleanup and reconnection

Docs:
- Add CODEMAPS for architecture, backend, frontend, data, deps
- Add protocol-schema.json (machine-readable, 36 message types)
- Add platform research report
- Update PROTOCOL.md with seq/replay fields
2026-03-21 10:08:44 +01:00
jevb 72d5412ba7 feat: rewrite server voice handlers for LiveKit (Phase 1)
Replace custom Pion WebRTC SFU with LiveKit token-based flow.

Server changes:
- client.go: remove PeerConnection, voiceDone, negoMu; add
  setVoiceChID/clearVoiceChID
- voice_handlers.go: 961 -> ~295 lines; voice_join now generates
  LiveKit token, voice_leave calls RemoveParticipant; delete
  SDP/ICE/RTP/soundboard handlers
- hub.go: replace SFU + VoiceRooms with LiveKitClient +
  LiveKitProcess; remove speaker broadcast goroutine
- messages.go: add buildVoiceToken, remove buildVoiceOffer/
  Answer/ICE; simplify buildVoiceConfig
- handlers.go: remove voice_offer/answer/ice/soundboard dispatch
- router.go: replace NewSFU with NewLiveKitClient, add optional
  LiveKit process auto-start

Deleted files (14):
- sfu.go, voice_room.go, speaker_detector.go, rtp_audio_level.go,
  speaker_broadcast.go, api/voice_handler.go
- All corresponding test files

All server tests pass (go test ./...).
2026-03-20 05:30:27 +01:00
jevb b78c9319fa fix: resolve CI failures — coverage exclusion and stale test removal
- Exclude voiceSession.ts from coverage (browser API dependency, same
  pattern as audio.ts/vad.ts/webrtc.ts)
- Remove stale TestHub_Register_CleansUpOldVoiceState test that tested
  old duplicate-login behavior removed in b53c729
2026-03-18 04:30:38 +01:00
jevb 6034bb1929 feat: add GetClient helper and ICE candidate callback 2026-03-18 03:47:30 +01:00
jevb ce4326766a fix: resolve all golangci-lint issues blocking CI server build
Fix 70+ errcheck violations by adding explicit error discards (`_ =`)
for unchecked return values across test helpers and deferred Close()
calls. Remove unused `senderID` field from broadcastMsg and unused
`defaultCleanupMaxWindow` const. Apply De Morgan's law, remove empty
branch, and simplify redundant type declaration per staticcheck.
2026-03-17 08:09:52 +01:00
jevb 9c1d99683c fix: address PR review findings (issues #9-#14)
- Fix capacity over-allocation and use strings.Builder in getReactionsBatch (#9)
- Replace `any` types and cache Tauri invoke in window-state.ts (#10)
- Remove custom `contains` helper, fix NilHub tests to pass nil (#11)
- Add nil guards before hub method calls in admin handlers (#12)
- Run golangci-lint v2: modernize interface{}/any, range-over-int loops,
  remove dead code, fix errcheck, add .golangci.yml config (#13)
- Add 23 client unit test suites (694 tests), exclude Tauri-coupled
  files from coverage, achieve 80%+ threshold (#14)

Closes #9, closes #10, closes #11, closes #12, closes #13, closes #14
2026-03-17 04:11:04 +01:00
jevb c1c25ed26c feat: implement full client UI from mockup — 10 phases, 331 tests
Client UI:
- Design system: Colors, Typography, Controls resource dictionaries
- Message actions: reply compose bar, hover edit/delete/reply buttons
- Rich content: code blocks, attachments, system messages, content parser
- Server strip: 72px sidebar with server icons, home button, add server
- Status picker: popup for changing online/idle/dnd/invisible status
- ConnectPage: server health check dots with auto-refresh
- User popup: profile card with banner, avatar, roles, member since
- Emoji picker: 6 categories, search, grid of Unicode emojis
- Settings overlay: full-screen with sidebar navigation
- Friends/DM view: sidebar + friends list with tabs (online/all/pending)
- Toast notifications: auto-dismiss after 3s with fade animation

Models & services:
- Attachment model added to Message, ApiMessage, ChatMessagePayload
- EditMessageAsync, DeleteMessageAsync, SendStatusChangeAsync APIs
- MessageContentParser (code blocks, inline code, bold, italic)
- EmojiData, ToastService, HealthStatusToBrushConverter

Server (from prior session):
- Voice room management, SFU, speaker detection
- ACME/TLS support, config improvements
- Protocol and schema updates

Tests: 331 passing (61 converter + 24 voice service + 34 voice VM +
41 parser + 9 edit/delete + existing)
2026-03-15 11:42:25 +01:00
jevb 25449eb204 feat: redesign login UI, add save-password, fix permissions, add audit logging, member_join broadcast
- Redesign ConnectPage with modern dark theme, profile cards with delete buttons, login/register toggle
- Add DPAPI-encrypted password saving with "Remember my password" checkbox
- Fix permission bit constants to match SCHEMA.md (Member role 0x663)
- Add migration 004 to fix existing Member role permissions
- Add comprehensive audit logging across all server packages (auth, admin, ws, setup)
- Add member_join WebSocket broadcast so new users appear in members list in real-time
- Add host URL normalization (strip scheme prefix) for reverse proxy compatibility
- Add REST API client, ChatService orchestrator, WebSocket service with reconnection
- Add model types (WsEnvelope payloads, API responses), converters, tests
2026-03-15 00:31:39 +01:00
jevb 36640e3051 feat: implement Phase 4 real-time chat (WebSocket hub + message REST)
- db: channel_queries (ListChannels, GetChannel, CRUD, permissions),
  message_queries (CreateMessage, GetMessage, GetMessages paginated,
  EditMessage, DeleteMessage soft, AddReaction, RemoveReaction,
  GetReactions, SearchMessages FTS5, UpdateReadState)
- db: fix in-memory DB isolation — SetMaxOpenConns(1) for :memory: path
- ws/hub: replace stub with full Hub (register/unregister, broadcast to
  channel/all, send to user, thread-safe, buffered broadcast channel)
- ws/client: Client with send channel, NewTestClient helpers for tests
- ws/handlers: dispatch chat_send/edit/delete, reaction_add/remove,
  typing_start, presence_update — all with rate limiting and permission checks
- ws/messages: JSON builder helpers for all server→client message types
- ws/serve: ServeWS HTTP handler, WS auth handshake (10s timeout),
  ready payload, writePump/readPump goroutines, graceful disconnect
- api: channel_handler — GET /channels, GET /channels/{id}/messages,
  GET /search; fixed double-mount of /api/v1 route group
- api/router: mount channel routes, start hub, register /api/v1/ws

Test coverage: api 77.6%, auth 90.9%, db 84.2%, ws 26.7% (serve.go
requires live WS connection; hub/handlers/messages fully covered)
2026-03-14 21:17:09 +01:00