From 11875777b8320afaacf1135e357d15d830c34e1c Mon Sep 17 00:00:00 2001 From: FelixG Date: Sun, 22 Feb 2026 12:31:58 +0100 Subject: [PATCH] Fix backend TypeScript build errors - Inline tsconfig.base.json settings into backend/tsconfig.json so the Docker build (which only copies backend/) can resolve them - Replace default imports of Node built-ins (crypto, net) with named imports - Replace default bcrypt import with named imports (compare, hash) - Switch @fastify/websocket from v8 to v7 (SocketStream API) to match Fastify v4 peer dependency; update WebSocket handler signatures accordingly - Remove obsolete `version` key from docker-compose.yml Co-Authored-By: Claude Sonnet 4.6 --- backend/package.json | 2 +- backend/src/routes/auth.ts | 4 +-- backend/src/seed.ts | 4 +-- backend/src/utils/encryption.ts | 11 ++++---- backend/src/websocket/rdp.ts | 49 +++++++++++++-------------------- backend/src/websocket/ssh.ts | 26 ++++++----------- backend/tsconfig.json | 6 +++- docker-compose.yml | 2 -- 8 files changed, 43 insertions(+), 61 deletions(-) diff --git a/backend/package.json b/backend/package.json index 7fe7933..6550e7e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,7 +15,7 @@ "dependencies": { "@fastify/cors": "^9.0.1", "@fastify/jwt": "^8.0.1", - "@fastify/websocket": "^8.3.1", + "@fastify/websocket": "^7.2.0", "@prisma/client": "^5.17.0", "bcryptjs": "^2.4.3", "fastify": "^4.28.1", diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 1dd8222..e9230b4 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -1,5 +1,5 @@ import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import bcrypt from 'bcryptjs'; +import { compare as bcryptCompare } from 'bcryptjs'; interface LoginBody { username: string; @@ -30,7 +30,7 @@ export async function authRoutes(fastify: FastifyInstance) { return reply.status(401).send({ error: 'Invalid credentials' }); } - const valid = await bcrypt.compare(password, user.passwordHash); + const valid = await bcryptCompare(password, user.passwordHash); if (!valid) { return reply.status(401).send({ error: 'Invalid credentials' }); } diff --git a/backend/src/seed.ts b/backend/src/seed.ts index 6596224..eb04659 100644 --- a/backend/src/seed.ts +++ b/backend/src/seed.ts @@ -3,7 +3,7 @@ * Creates the initial admin user if one does not already exist. */ import { PrismaClient } from '@prisma/client'; -import bcrypt from 'bcryptjs'; +import { hash as bcryptHash } from 'bcryptjs'; const prisma = new PrismaClient(); @@ -13,7 +13,7 @@ async function main() { const existing = await prisma.user.findUnique({ where: { username } }); if (!existing) { - const passwordHash = await bcrypt.hash(password, 10); + const passwordHash = await bcryptHash(password, 10); await prisma.user.create({ data: { username, passwordHash } }); console.log(`[seed] Created admin user: ${username}`); } else { diff --git a/backend/src/utils/encryption.ts b/backend/src/utils/encryption.ts index 4547782..f7d0fd9 100644 --- a/backend/src/utils/encryption.ts +++ b/backend/src/utils/encryption.ts @@ -1,17 +1,16 @@ -import crypto from 'crypto'; +import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto'; const ALGORITHM = 'aes-256-cbc'; const IV_LENGTH = 16; function getKey(): Buffer { const raw = process.env.ENCRYPTION_KEY || ''; - // Derive exactly 32 bytes from whatever key string is provided - return crypto.createHash('sha256').update(raw).digest(); + return createHash('sha256').update(raw).digest(); } export function encrypt(text: string): string { - const iv = crypto.randomBytes(IV_LENGTH); - const cipher = crypto.createCipheriv(ALGORITHM, getKey(), iv); + const iv = randomBytes(IV_LENGTH); + const cipher = createCipheriv(ALGORITHM, getKey(), iv); const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]); return `${iv.toString('hex')}:${encrypted.toString('hex')}`; } @@ -20,7 +19,7 @@ export function decrypt(encryptedText: string): string { const [ivHex, encryptedHex] = encryptedText.split(':'); const iv = Buffer.from(ivHex, 'hex'); const encrypted = Buffer.from(encryptedHex, 'hex'); - const decipher = crypto.createDecipheriv(ALGORITHM, getKey(), iv); + const decipher = createDecipheriv(ALGORITHM, getKey(), iv); const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]); return decrypted.toString('utf8'); } diff --git a/backend/src/websocket/rdp.ts b/backend/src/websocket/rdp.ts index 858a9a1..0c63449 100644 --- a/backend/src/websocket/rdp.ts +++ b/backend/src/websocket/rdp.ts @@ -1,6 +1,6 @@ -import net from 'net'; -import { FastifyInstance } from 'fastify'; -import '@fastify/websocket'; // trigger type augmentation for { websocket: true } route option +import { createConnection, Socket } from 'net'; +import { FastifyInstance, FastifyRequest } from 'fastify'; +import { SocketStream } from '@fastify/websocket'; import { WebSocket } from 'ws'; import { decrypt } from '../utils/encryption'; @@ -21,7 +21,6 @@ function buildInstruction(opcode: string, ...args: string[]): string { return [encodeElement(opcode), ...args.map(encodeElement)].join(',') + ';'; } -/** Parse a single complete Guacamole instruction string into [opcode, ...args] */ function parseInstruction(instruction: string): string[] { const raw = instruction.endsWith(';') ? instruction.slice(0, -1) : instruction; const elements: string[] = []; @@ -38,14 +37,10 @@ function parseInstruction(instruction: string): string[] { if (raw[pos] === ',') pos++; } - return elements; // elements[0] = opcode, rest = args + return elements; } -/** Read from a TCP socket until a complete Guacamole instruction (ending with ';') is received. */ -function readInstruction( - tcpSocket: net.Socket, - buf: { value: string } -): Promise { +function readInstruction(tcpSocket: Socket, buf: { value: string }): Promise { return new Promise((resolve, reject) => { const check = () => { const idx = buf.value.indexOf(';'); @@ -86,7 +81,9 @@ export async function rdpWebsocket(fastify: FastifyInstance) { fastify.get( '/ws/rdp/:connectionId', { websocket: true }, - async (socket: WebSocket, request) => { + async (connection: SocketStream, request: FastifyRequest) => { + const socket = connection.socket; + // --- Auth --- const query = request.query as { token?: string }; let userId: string; @@ -115,7 +112,7 @@ export async function rdpWebsocket(fastify: FastifyInstance) { const guacdPort = Number(process.env.GUACD_PORT) || 4822; // --- Connect to guacd --- - const guacd = net.createConnection(guacdPort, guacdHost); + const guacd = createConnection(guacdPort, guacdHost); const tcpBuf = { value: '' }; try { @@ -125,7 +122,7 @@ export async function rdpWebsocket(fastify: FastifyInstance) { }); } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'guacd connect failed'; - fastify.log.error({ err }, 'guacd connect failed'); + fastify.log.warn({ err }, 'guacd connect failed'); socket.close(1011, msg); return; } @@ -136,12 +133,10 @@ export async function rdpWebsocket(fastify: FastifyInstance) { // 2. Read args list from guacd const argsInstruction = await readInstruction(guacd, tcpBuf); - // argsInstruction = ['args', 'hostname', 'port', 'username', ...] const argNames = argsInstruction.slice(1); const decryptedPassword = conn.encryptedPassword ? decrypt(conn.encryptedPassword) : ''; - // Build a map of known RDP parameters const rdpParams: Record = { hostname: conn.host, port: String(conn.port), @@ -160,7 +155,7 @@ export async function rdpWebsocket(fastify: FastifyInstance) { 'enable-printing': 'false', }; - // 3. Connect with values for each arg guacd requested + // 3. Connect with values guacd requested const argValues = argNames.map((name) => rdpParams[name] ?? ''); guacd.write(buildInstruction('connect', ...argValues)); @@ -170,31 +165,27 @@ export async function rdpWebsocket(fastify: FastifyInstance) { throw new Error(`guacd handshake failed: expected 'ready', got '${readyInstruction[0]}'`); } - // 5. Flush any remaining buffered bytes to socket - if (tcpBuf.value.length > 0) { - if (socket.readyState === WebSocket.OPEN) { - socket.send(tcpBuf.value); - } + // 5. Flush any buffered bytes that arrived after 'ready' + if (tcpBuf.value.length > 0 && socket.readyState === WebSocket.OPEN) { + socket.send(tcpBuf.value); tcpBuf.value = ''; } } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'Guacamole handshake failed'; - fastify.log.error({ err }, 'Guacamole handshake failed'); + fastify.log.warn({ err }, 'Guacamole handshake failed'); guacd.destroy(); socket.close(1011, msg); return; } - // --- Proxy mode: bidirectional forwarding --- + // --- Proxy mode --- guacd.on('data', (data: Buffer) => { - if (socket.readyState === WebSocket.OPEN) { - socket.send(data.toString('utf8')); - } + if (socket.readyState === WebSocket.OPEN) socket.send(data.toString('utf8')); }); guacd.on('end', () => socket.close()); guacd.on('error', (err) => { - fastify.log.error({ err }, 'guacd socket error'); + fastify.log.warn({ err }, 'guacd socket error'); socket.close(1011, err.message); }); @@ -204,9 +195,7 @@ export async function rdpWebsocket(fastify: FastifyInstance) { } }); - socket.on('close', () => { - guacd.destroy(); - }); + socket.on('close', () => guacd.destroy()); } ); } diff --git a/backend/src/websocket/ssh.ts b/backend/src/websocket/ssh.ts index 5953e15..44c615b 100644 --- a/backend/src/websocket/ssh.ts +++ b/backend/src/websocket/ssh.ts @@ -1,5 +1,5 @@ -import { FastifyInstance } from 'fastify'; -import '@fastify/websocket'; // trigger type augmentation for { websocket: true } route option +import { FastifyInstance, FastifyRequest } from 'fastify'; +import { SocketStream } from '@fastify/websocket'; import { WebSocket } from 'ws'; import { Client as SshClient, ConnectConfig } from 'ssh2'; import { decrypt } from '../utils/encryption'; @@ -19,7 +19,9 @@ export async function sshWebsocket(fastify: FastifyInstance) { fastify.get( '/ws/ssh/:connectionId', { websocket: true }, - async (socket: WebSocket, request) => { + async (connection: SocketStream, request: FastifyRequest) => { + const socket = connection.socket; + // --- Auth via ?token= query param --- const query = request.query as { token?: string }; let userId: string; @@ -72,15 +74,11 @@ export async function sshWebsocket(fastify: FastifyInstance) { // SSH stdout/stderr → WebSocket (binary frames) stream.on('data', (data: Buffer) => { - if (socket.readyState === WebSocket.OPEN) { - socket.send(data); - } + if (socket.readyState === WebSocket.OPEN) socket.send(data); }); stream.stderr.on('data', (data: Buffer) => { - if (socket.readyState === WebSocket.OPEN) { - socket.send(data); - } + if (socket.readyState === WebSocket.OPEN) socket.send(data); }); stream.on('close', () => { @@ -93,17 +91,14 @@ export async function sshWebsocket(fastify: FastifyInstance) { if (!stream.writable) return; if (isBinary) { - // Raw terminal input stream.write(Buffer.isBuffer(message) ? message : Buffer.from(message as string)); } else { - // Control message (JSON text frame) try { const msg: ResizeMessage = JSON.parse(message.toString()); if (msg.type === 'resize') { stream.setWindow(msg.rows || 24, msg.cols || 80, 0, 0); } } catch { - // Non-JSON text: treat as raw input stream.write(message.toString()); } } @@ -117,7 +112,7 @@ export async function sshWebsocket(fastify: FastifyInstance) { }); ssh.on('error', (err) => { - fastify.log.error({ err }, 'SSH error'); + fastify.log.error({ err }, 'SSH connection error'); if (socket.readyState === WebSocket.OPEN) { socket.send(`\r\n\x1b[31mSSH error: ${err.message}\x1b[0m\r\n`); socket.close(1011, err.message); @@ -126,10 +121,7 @@ export async function sshWebsocket(fastify: FastifyInstance) { ssh.connect(sshConfig); - // If WebSocket closes before SSH is ready - socket.on('close', () => { - ssh.end(); - }); + socket.on('close', () => ssh.end()); } ); } diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 8de372f..09bdfeb 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,11 +1,15 @@ { - "extends": "../tsconfig.base.json", "compilerOptions": { "target": "ES2021", "module": "commonjs", "lib": ["ES2021"], "outDir": "./dist", "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, "declaration": true, "sourceMap": true, "moduleResolution": "node" diff --git a/docker-compose.yml b/docker-compose.yml index f72aba5..a27d444 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.9' - services: postgres: image: postgres:16-alpine