Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import stripAnsi from 'strip-ansi';
import { spawn as ptySpawn } from 'node-pty';
import { getSSHExecutionService } from './src/server/ssh-execution-service.js';
import { getDatabase } from './src/server/database-prisma.js';
import { registerGlobalErrorHandlers } from './src/server/logging/globalHandlers.js';

const dev = process.env.NODE_ENV !== 'production';
const hostname = '0.0.0.0';
const port = parseInt(process.env.PORT || '3000', 10);

const app = next({ dev, hostname, port });
// Register global handlers once at bootstrap
registerGlobalErrorHandlers();
const handle = app.getRequestHandler();

// WebSocket handler for script execution
Expand Down
30 changes: 16 additions & 14 deletions src/app/api/servers/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { getDatabase } from '../../../../server/database-prisma';
import type { CreateServerData } from '../../../../types/server';
import { withApiLogging } from '../../../../server/logging/withApiLogging';

export async function GET(
export const GET = withApiLogging(async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
Expand All @@ -28,16 +29,16 @@ export async function GET(
}

return NextResponse.json(server);
} catch (error) {
console.error('Error fetching server:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to fetch server' },
{ status: 500 }
);
}
}
}, { redactBody: true });

export async function PUT(
export const PUT = withApiLogging(async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
Expand All @@ -62,8 +63,9 @@ export async function PUT(
);
}

// Validate SSH port
if (ssh_port !== undefined && (ssh_port < 1 || ssh_port > 65535)) {
// Coerce and validate SSH port
const port = ssh_port !== undefined ? parseInt(String(ssh_port), 10) : 22;
if (Number.isNaN(port) || port < 1 || port > 65535) {
return NextResponse.json(
{ error: 'SSH port must be between 1 and 65535' },
{ status: 400 }
Expand Down Expand Up @@ -111,7 +113,7 @@ export async function PUT(
auth_type: authType,
ssh_key,
ssh_key_passphrase,
ssh_port: ssh_port ?? 22,
ssh_port: port,
color,
key_generated: key_generated ?? false,
ssh_key_path
Expand All @@ -124,7 +126,7 @@ export async function PUT(
}
);
} catch (error) {
console.error('Error updating server:', error);
// Error handled by withApiLogging

// Handle unique constraint violation
if (error instanceof Error && error.message.includes('UNIQUE constraint failed')) {
Expand All @@ -139,9 +141,9 @@ export async function PUT(
{ status: 500 }
);
}
}
}, { redactBody: true });

export async function DELETE(
export const DELETE = withApiLogging(async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
Expand Down Expand Up @@ -177,12 +179,12 @@ export async function DELETE(
changes: 1
}
);
} catch (error) {
console.error('Error deleting server:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to delete server' },
{ status: 500 }
);
}
}
}, { redactBody: true });

7 changes: 4 additions & 3 deletions src/app/api/servers/generate-keypair/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { getSSHService } from '../../../../server/ssh-service';
import { getDatabase } from '../../../../server/database-prisma';
import { withApiLogging } from '../../../../server/logging/withApiLogging';

export async function POST(_request: NextRequest) {
export const POST = withApiLogging(async function POST(_request: NextRequest) {
try {
const sshService = getSSHService();
const db = getDatabase();
Expand All @@ -20,7 +21,7 @@ export async function POST(_request: NextRequest) {
serverId: serverId
});
} catch (error) {
console.error('Error generating SSH key pair:', error);
// Error handled by withApiLogging
return NextResponse.json(
{
success: false,
Expand All @@ -29,4 +30,4 @@ export async function POST(_request: NextRequest) {
{ status: 500 }
);
}
}
}, { redactBody: true });
27 changes: 14 additions & 13 deletions src/app/api/servers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@ import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { getDatabase } from '../../../server/database-prisma';
import type { CreateServerData } from '../../../types/server';
import { withApiLogging } from '../../../server/logging/withApiLogging';

export async function GET() {
export const GET = withApiLogging(async function GET() {
try {
const db = getDatabase();
const servers = await db.getAllServers();
return NextResponse.json(servers);
} catch (error) {
console.error('Error fetching servers:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to fetch servers' },
{ status: 500 }
);
}
}
}, { redactBody: true });

export async function POST(request: NextRequest) {
export const POST = withApiLogging(async function POST(request: NextRequest) {
try {
const body = await request.json();
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color, key_generated, ssh_key_path }: CreateServerData = body;
Expand All @@ -30,8 +31,9 @@ export async function POST(request: NextRequest) {
);
}

// Validate SSH port
if (ssh_port !== undefined && (ssh_port < 1 || ssh_port > 65535)) {
// Coerce and validate SSH port
const port = ssh_port !== undefined ? parseInt(String(ssh_port), 10) : 22;
if (Number.isNaN(port) || port < 1 || port > 65535) {
return NextResponse.json(
{ error: 'SSH port must be between 1 and 65535' },
{ status: 400 }
Expand Down Expand Up @@ -69,7 +71,7 @@ export async function POST(request: NextRequest) {
auth_type: authType,
ssh_key,
ssh_key_passphrase,
ssh_port: ssh_port ?? 22,
ssh_port: port,
color,
key_generated: key_generated ?? false,
ssh_key_path
Expand All @@ -82,11 +84,10 @@ export async function POST(request: NextRequest) {
},
{ status: 201 }
);
} catch (error) {
console.error('Error creating server:', error);

} catch {
// Error handled by withApiLogging
// Handle unique constraint violation
if (error instanceof Error && error.message.includes('UNIQUE constraint failed')) {
if (Error instanceof Error && Error.message.includes('UNIQUE constraint failed')) {
return NextResponse.json(
{ error: 'A server with this name already exists' },
{ status: 409 }
Expand All @@ -98,5 +99,5 @@ export async function POST(request: NextRequest) {
{ status: 500 }
);
}
}
}, { redactBody: true });

25 changes: 13 additions & 12 deletions src/app/api/settings/auth-credentials/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { NextResponse } from 'next/server';
import { getAuthConfig, updateAuthCredentials, updateAuthEnabled } from '~/lib/auth';
import fs from 'fs';
import path from 'path';
import { withApiLogging } from '../../../../server/logging/withApiLogging';

export async function GET() {
export const GET = withApiLogging(async function GET() {
try {
const authConfig = getAuthConfig();

Expand All @@ -14,16 +15,16 @@ export async function GET() {
hasCredentials: authConfig.hasCredentials,
setupCompleted: authConfig.setupCompleted,
});
} catch (error) {
console.error('Error reading auth credentials:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to read auth configuration' },
{ status: 500 }
);
}
}
}, { redactBody: true });

export async function POST(request: NextRequest) {
export const POST = withApiLogging(async function POST(request: NextRequest) {
try {
const { username, password, enabled } = await request.json() as { username: string; password: string; enabled?: boolean };

Expand Down Expand Up @@ -54,16 +55,16 @@ export async function POST(request: NextRequest) {
success: true,
message: 'Authentication credentials updated successfully'
});
} catch (error) {
console.error('Error updating auth credentials:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to update auth credentials' },
{ status: 500 }
);
}
}
}, { redactBody: true });

export async function PATCH(request: NextRequest) {
export const PATCH = withApiLogging(async function PATCH(request: NextRequest) {
try {
const { enabled } = await request.json() as { enabled: boolean };

Expand Down Expand Up @@ -107,11 +108,11 @@ export async function PATCH(request: NextRequest) {
success: true,
message: `Authentication ${enabled ? 'enabled' : 'disabled'} successfully`
});
} catch (error) {
console.error('Error updating auth enabled status:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to update auth status' },
{ status: 500 }
);
}
}
}, { redactBody: true });
17 changes: 9 additions & 8 deletions src/app/api/settings/github-token/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
import { withApiLogging } from '../../../../server/logging/withApiLogging';

export async function POST(request: NextRequest) {
export const POST = withApiLogging(async function POST(request: NextRequest) {
try {
const { token } = await request.json();

Expand Down Expand Up @@ -39,16 +40,16 @@ export async function POST(request: NextRequest) {
fs.writeFileSync(envPath, envContent);

return NextResponse.json({ success: true, message: 'GitHub token saved successfully' });
} catch (error) {
console.error('Error saving GitHub token:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to save GitHub token' },
{ status: 500 }
);
}
}
}, { redactBody: true });

export async function GET() {
export const GET = withApiLogging(async function GET() {
try {
// Path to the .env file
const envPath = path.join(process.cwd(), '.env');
Expand All @@ -65,11 +66,11 @@ export async function GET() {
const token = githubTokenMatch ? githubTokenMatch[1] : null;

return NextResponse.json({ token });
} catch (error) {
console.error('Error reading GitHub token:', error);
} catch {
// Error handled by withApiLogging
return NextResponse.json(
{ error: 'Failed to read GitHub token' },
{ status: 500 }
);
}
}
}, { redactBody: true });
5 changes: 2 additions & 3 deletions src/app/api/trpc/[trpc]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type NextRequest } from "next/server";
import { env } from "~/env.js";
import { appRouter } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/trpc";
import logger from "../../../../server/logging/logger";

const handler = (req: NextRequest) =>
fetchRequestHandler({
Expand All @@ -14,9 +15,7 @@ const handler = (req: NextRequest) =>
onError:
env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`[ERROR] tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
);
logger.error("trpc_error", { path: path ?? "<no-path>" }, error);
}
: undefined,
});
Expand Down
4 changes: 3 additions & 1 deletion src/server/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};

export const prisma = globalForPrisma.prisma ?? new PrismaClient();
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: ['warn', 'error']
});

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
21 changes: 21 additions & 0 deletions src/server/logging/globalHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import logger from './logger';
import { toSafeError } from './prismaSafeError';

let registered = false;

export function registerGlobalErrorHandlers() {
if (registered) return;
registered = true;

process.on('uncaughtException', (err) => {
const safe = toSafeError(err);
logger.error('uncaught_exception', { name: safe.name, code: safe.code }, err);
});

process.on('unhandledRejection', (reason) => {
const safe = toSafeError(reason as any);
logger.error('unhandled_rejection', { name: safe.name, code: safe.code }, reason);
});
}


Loading