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 <noreply@anthropic.com>
This commit is contained in:
FelixG
2026-02-22 12:31:58 +01:00
parent 3802924c6a
commit 11875777b8
8 changed files with 43 additions and 61 deletions

View File

@@ -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",

View File

@@ -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' });
}

View File

@@ -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 {

View File

@@ -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');
}

View File

@@ -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<string[]> {
function readInstruction(tcpSocket: Socket, buf: { value: string }): Promise<string[]> {
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<string, string> = {
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());
}
);
}

View File

@@ -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());
}
);
}

View File

@@ -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"

View File

@@ -1,5 +1,3 @@
version: '3.9'
services:
postgres:
image: postgres:16-alpine