Commit Graph

132 Commits

Author SHA1 Message Date
Vantz Stockwell
733fe6aca1 feat: admin user management UI
- Add admin-only "Users" nav link in header
- Create /admin/users page with full CRUD:
  create user, edit, delete, reset password, reset TOTP
- Matches existing wraith dark theme
- Client-side admin guard redirects non-admins

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 13:07:20 -04:00
Vantz Stockwell
6d76558bc3 feat: multi-user isolation with admin/user roles
Full per-user data isolation across all tables:
- Migration adds userId FK to hosts, host_groups, credentials, ssh_keys,
  connection_logs. Backfills existing data to admin@wraith.local.
- All services scope queries by userId from JWT (req.user.sub).
  Users can only see/modify their own data. Cross-user access returns 403.
- Two roles: admin (full access + user management) and user (own data only).
- Admin endpoints: list/create/edit/delete users, reset password, reset TOTP.
  Protected by AdminGuard. Admins cannot delete themselves or remove own role.
- JWT payload now includes role. Frontend auth store exposes isAdmin getter.
- Seed script fixed: checks for admin@wraith.local specifically (not any user).
  Uses upsert, seeds with role=admin. Migration cleans up duplicate users.
- Connection logs now attributed to the connecting user via WS auth.
- Deleting a user CASCADEs to all their hosts, credentials, keys, and logs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 12:57:38 -04:00
Vantz Stockwell
b749242583 feat(sftp): SFTP sidebar follows terminal CWD
- Inject shell integration (PROMPT_COMMAND/precmd) on SSH connect that
  emits OSC 7 escape sequences reporting the working directory on every
  prompt. Supports bash and zsh.
- Frontend captures OSC 7 via xterm.js parser, updates session store CWD.
- SFTP sidebar watches session CWD and navigates when it changes.
- SFTP starts at ~/ (user home) instead of / on initial connect, resolved
  via SFTP realpath('.') on the backend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 12:44:42 -04:00
Vantz Stockwell
95271f065a fix(rdp): proper display scaling via Guacamole display.scale()
Remove CSS width/height !important override that broke Guacamole's
internal rendering pipeline. Replace with display.scale() auto-fitting
using ResizeObserver for responsive container sizing. Scale mouse
coordinates back to remote display space to keep input accurate.
Clean up diagnostic instruction logging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 12:20:21 -04:00
Vantz Stockwell
f9070c81f3 diag(rdp): expand instruction logging to find desktop frames
Log first 50 instructions, then every 200th, plus any draw operation
targeting layer 0 (main display). Need to determine if RDPGFX desktop
frames are arriving or if only cursor operations are being received.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 12:11:12 -04:00
Vantz Stockwell
6ddd343234 diag(rdp): add instruction + display dimension logging
Temporary diagnostics to debug blank screen after successful RDP connection.
Logs first 30 instruction opcodes and display dimensions on ready.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 12:04:25 -04:00
Vantz Stockwell
1bf225ae27 fix(rdp): send client capability instructions before CONNECT handshake
ROOT CAUSE: guacd showed "User resolution is 0x0 at 0 DPI" and
immediately killed every RDP connection.

The Guacamole protocol requires five client capability instructions
(size, audio, video, image, timezone) BETWEEN receiving 'args' and
sending 'connect'. Our handshake skipped all five and jumped straight
to CONNECT. guacd never received the display dimensions, defaulted to
0x0, and terminated the connection.

Now sends the complete handshake:
  select → (receive args) → size → audio → video → image → timezone → connect

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 06:32:36 -04:00
Vantz Stockwell
34bea52e0b fix(rdp): TCP stream buffering + error surfacing for guacd pipeline
Three bugs fixed:

1. TCP stream fragmentation — guacd→browser data pipe treated each TCP
   chunk as a complete instruction. TCP is a stream protocol; instructions
   WILL be split across chunks (especially display/image data). Added
   instruction buffer that accumulates data and only forwards complete
   instructions (terminated by ';').

2. Missing client.onerror — when guacd fails the RDP connection (NLA,
   auth, TLS), it sends a Guacamole error instruction. No handler was
   registered, so errors were silently swallowed. User saw blank canvas
   with no feedback. Now surfaces errors via console and gateway callback.

3. Missing client.onstatechange — no connection state tracking. Added
   state transition logging for diagnostics.

Also improved CONNECT handshake logging to surface connection parameters
(host, port, user, domain, security mode) without exposing passwords.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 06:17:32 -04:00
Vantz Stockwell
36251c3caa fix(rdp): correct guacd debug entrypoint override
ENTRYPOINT not CMD — guacamole/guacd image sets its own entrypoint,
so command override was being appended as args to guacd binary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 05:58:13 -04:00
Vantz Stockwell
c062cd502d debug(rdp): enable guacd debug logging + log all guacd responses
guacd was dying silently with no error instruction sent back.
Enable -L debug -f for verbose FreeRDP diagnostics and log
first 5 guacd→browser instructions plus connection parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 05:53:13 -04:00
Vantz Stockwell
80463235b0 fix(rdp): VERSION echo + guacd host networking for overlay reach
- Echo VERSION_X_Y_Z args back to guacd in CONNECT handshake
- Set guacd to network_mode: host so it can reach RDP targets on
  NetBird/Tailscale overlay networks (100.64.x.x)
- App container uses host.docker.internal to reach guacd on host
- Add diagnostic logging for guacd→browser instruction relay

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 05:23:02 -04:00
Vantz Stockwell
9d3a93bea9 fix(rdp): parse guacd args response and send matching positional connect values 2026-03-14 05:05:00 -04:00
Vantz Stockwell
72526487c3 fix(rdp): replace class extends with direct instance method override on Guacamole.Tunnel 2026-03-14 04:51:03 -04:00
Vantz Stockwell
76db0a6936 fix(rdp): override Guacamole.Tunnel instance methods, fix sendMessage encoding 2026-03-14 04:39:44 -04:00
Vantz Stockwell
e3a978b639 fix(sftp): cache SFTP channel per session to prevent channel exhaustion 2026-03-14 04:18:29 -04:00
Vantz Stockwell
3b1c1aeda1 feat(sftp): add download save-to-disk + upload support, remove debug banner 2026-03-14 04:11:45 -04:00
Vantz Stockwell
a9702795a4 fix(sftp): use Nuxt auto-import names SftpFileTree/SftpFileEditor 2026-03-14 03:45:04 -04:00
Vantz Stockwell
aa1bbb28c4 fix(sftp): fix FileTree not visible — flex overflow layout issue 2026-03-14 03:21:25 -04:00
Vantz Stockwell
fd9e30b3bf debug: add visible SFTP diagnostic banner + WS error/close handlers 2026-03-14 03:02:49 -04:00
Vantz Stockwell
f124d4b7d2 fix(rdp): convert to manual ws.Server, fix URL path, fix double session 2026-03-14 02:40:37 -04:00
Vantz Stockwell
3b5e5e0d36 debug: add onmessage logging to useSftp composable 2026-03-14 02:34:58 -04:00
Vantz Stockwell
e39d8fbdda debug: comprehensive SFTP channel + readdir diagnostic logging 2026-03-14 02:22:42 -04:00
Vantz Stockwell
711ef73786 fix: SFTP list not sent + RDP connect button does nothing
SFTP: Added console logging to diagnose, plus a watcher that sends
the pending list when sessionId becomes available (covers the race
where WS opens before sessionId is set).

RDP: connectHost() was returning early for non-SSH protocols.
Removed the guard and use host.protocol instead of hardcoded 'ssh'.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 02:04:33 -04:00
Vantz Stockwell
46e2cb6e2f debug: add SFTP message logging to diagnose empty directory listing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 02:01:33 -04:00
Vantz Stockwell
9e30e5915f fix: SFTP sidebar empty on load — list sent before WebSocket open
The list('/') call fired immediately after connect(), but the
WebSocket was still in CONNECTING state so send() silently dropped
the message. Now buffers the initial list request and sends it
in the onopen callback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:56:59 -04:00
Vantz Stockwell
aa457b54d4 fix: infinite remount loop — use stable key for session components
When replaceSession changed the session ID from pending-XXX to a
real UUID, Vue's :key="session.id" treated it as a new element,
destroyed and recreated TerminalInstance, which called connectToHost
again, got another UUID, replaced again — infinite loop.

Added a stable `key` field to sessions that never changes after
creation, used as the Vue :key instead of the mutable `id`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:51:31 -04:00
Vantz Stockwell
4ccf138744 fix: seed script checks for any existing user, not just admin@wraith.local
Previously the seed only checked for admin@wraith.local by email,
so it would create a duplicate if the admin had changed their email.
Now skips seeding entirely if any user exists.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:48:19 -04:00
Vantz Stockwell
2216ee79aa fix: close WebSocket and auto-remove session on SSH connect failure
When SSH connection fails, close the WebSocket immediately and
auto-remove the pending session after 3 seconds so the user sees
the error message before the panel clears. Prevents stuck sessions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:32:48 -04:00
Vantz Stockwell
6262ab6e7e debug: verify ssh2 key parsing and log derived public key
Uses ssh2 utils.parseKey() to check if the key decrypts and
parses correctly, logs the key type and public key fingerprint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:13:51 -04:00
Vantz Stockwell
36c8527c28 debug: add SSH auth diagnostic logging
Logs key format, length, auth method selection, and ssh2 debug
output for auth/key events to diagnose why key auth is rejected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:08:54 -04:00
Vantz Stockwell
11e1705110 fix: detect orphaned SSH key references and missing auth methods
When a credential's sshKeyId points to a deleted/missing SSH key row,
the connection attempt silently had zero auth methods. Now throws a
clear error explaining the SSH key is missing. Also catches the case
where a credential has neither password nor SSH key configured.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:04:41 -04:00
Vantz Stockwell
f06dcbaa6b chore: expose postgres on port 4211 for external access
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:55:35 -04:00
Vantz Stockwell
639ac329a8 fix: TDZ crash on SSH connect failure — sessionId referenced before assignment
When SSH timed out, the onClose callback referenced `const sessionId`
before connect() resolved, causing a Temporal Dead Zone ReferenceError
that killed the process. Changed to `let` with try/catch so connection
failures send an error message to the client instead of crashing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:50:35 -04:00
Vantz Stockwell
8d95fc5652 fix: add crash handlers to catch process-killing exceptions
Process-level uncaughtException/unhandledRejection handlers plus
try/catch around upgrade and connection handlers. This will log
whatever is crashing the server on browser WebSocket connections
before the process dies, instead of silently restarting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:36:44 -04:00
Vantz Stockwell
8207b78b36 fix: hijack upgrade event before WsAdapter can consume the socket
WsAdapter registered its upgrade handler first and destroyed sockets
for non-matching paths. Now we remove all existing upgrade listeners,
install ours first, and forward non-terminal/sftp upgrades to the
original WsAdapter handlers for RDP.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 23:28:54 -04:00
Vantz Stockwell
55b944bfc5 fix: restore WsAdapter for RdpModule gateway, manual handler coexists
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 23:21:48 -04:00
Vantz Stockwell
734ab5633e fix: bypass NestJS WsAdapter — manual WebSocket upgrade handling
The NestJS WsAdapter silently swallowed WebSocket connections through
NPM despite 101 responses in the access log. Replaced with manual
ws.Server instances using noServer mode and explicit HTTP upgrade
event handling. Gateways are now plain @Injectable services, not
@WebSocketGateway decorators.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 23:12:01 -04:00
Vantz Stockwell
10f3537b01 fix: move WebSocket paths under /api/ prefix to work through NPM proxy
NPM forwards /api/* correctly but silently drops WebSocket upgrades on
/ws/* despite toggle being enabled and custom nginx config. Moving
gateways to /api/ws/terminal and /api/ws/sftp so they ride the same
proxy rules that already work for REST endpoints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 15:13:45 -04:00
Vantz Stockwell
9dc5938fa6 debug: add HTTP upgrade event listener to diagnose WebSocket routing
handleConnection never fires despite browser getting open event.
Adding server-level upgrade listener to see if upgrades reach NestJS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:34:19 -04:00
Vantz Stockwell
77a76262f5 debug: add verbose WebSocket logging to terminal gateway
Need to see if handleConnection is even being called.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:27:43 -04:00
Vantz Stockwell
c4d7ad1833 fix: WebSocket auth always fails — client object has no URL property
With the NestJS ws adapter, the JWT token URL is on the HTTP upgrade
request (second arg to handleConnection), not on the WebSocket client
object. client.url was undefined, new URL(undefined) threw, catch
returned null, and every connection got 4001 Unauthorized.

Fix: Pass the IncomingMessage req to validateClient and prefer req.url.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 11:16:03 -04:00
Vantz Stockwell
d74bb28960 fix: terminal never appears — pending session removed before WS connects
Root cause: TerminalInstance.onMounted() called sessions.removeSession()
on the pending session, dropping sessions.length to 0. SessionContainer's
v-if="hasSessions" went false, unmounting the entire terminal UI before
the WebSocket could establish and add the real session.

Fix: Added replaceSession() to session store. TerminalInstance no longer
removes the pending session — instead passes its ID to connectToHost(),
which swaps it in-place when the backend responds with the real session ID.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 11:08:34 -04:00
Vantz Stockwell
6759327ee3 fix: null reference crash in saveHostInline after edit
Save editingHost.id to local var before nulling the ref.
Prevents TypeError on Array.find during post-save refresh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 11:01:38 -04:00
Vantz Stockwell
340a862826 fix: wire up Edit Host modal with pre-populated fields
- Edit now populates name, hostname, port, protocol, group, credential, tags
- Modal title shows "Edit Host" vs "New Host" appropriately
- Save calls updateHost for existing hosts, createHost for new
- Added group dropdown selector to host modal
- Button shows "Update" when editing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:49:22 -04:00
Vantz Stockwell
1f32ce4620 fix: credential picker in host modal, fix xterm dimensions crash
- Added credential dropdown to New Host modal (loads from vault API)
- Fixed xterm.js "Cannot read dimensions" crash by guarding fitAddon.fit()
  with requestAnimationFrame and container dimension checks
- Added WebGL context loss handler
- credentialId now passed when creating hosts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:31:39 -04:00
Vantz Stockwell
7e347ce378 fix: add tags input to host modal, pass groupId when creating from group
- Tags field added to New Host modal (comma separated)
- Clicking + on a group now passes groupId to the new host
- Shows "Adding to: GroupName" in modal when creating from a group
- Refreshes tree after host creation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:30:29 -04:00
Vantz Stockwell
ae97bd5a4a feat: convert Settings to right sidebar panel, remove light mode
- Settings now opens as a slide-over sidebar from nav bar (no page nav)
- Removed App Theme toggle (dark mode only — no working light mode)
- Terminal theme picker, font size, scrollback all in sidebar
- Removed standalone /settings page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:25:24 -04:00
Vantz Stockwell
74d3c0bd9a feat: add delete button for groups in sidebar
Shows × on hover next to the + button. Confirms before deleting.
Hosts in deleted groups become ungrouped.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:23:02 -04:00
Vantz Stockwell
3b14a7c1d1 feat: Termius-inspired UI — right sidebar, host counts, terminal themes
Left sidebar:
- Groups now show recursive host count badges
- Hosts in tree show up to 3 tags inline

Right sidebar (Host Details panel):
- Click any host card to open details panel on the right
- Shows address, port, protocol, group, credential, tags, color, notes
- Connect, Edit, Delete action buttons at bottom
- Selected card gets ring highlight

Terminal themes (10 prebuilt):
- Wraith (default), Dracula, Nord, Solarized Dark, Monokai, One Dark,
  Gruvbox Dark, Tokyo Night, Catppuccin Mocha, Cyberpunk
- Visual theme picker in Settings with color preview + sample text
- Persisted to /api/settings and localStorage for immediate use
- useTerminal reads theme on terminal creation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:19:57 -04:00
Vantz Stockwell
b88878b171 fix: use Nuxt prefixed component names for connections/ subdirectory
Root cause of hosts not displaying: Nuxt auto-imports components in
subdirectories with the directory name as prefix. HostCard → ConnectionsHostCard,
HostTree → ConnectionsHostTree, QuickConnect → ConnectionsQuickConnect.
Unprefixed names silently resolve to nothing — no errors, just invisible components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:11:37 -04:00