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:
@@ -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",
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
|
||||
Reference in New Issue
Block a user