Skip to content

Commit 2b58823

Browse files
Merge pull request #199 from community-scripts/fix/198
fix/198: logging improvements and API handlers
2 parents 3161f34 + c33e4f0 commit 2b58823

13 files changed

Lines changed: 336 additions & 54 deletions

File tree

server.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import stripAnsi from 'strip-ansi';
88
import { spawn as ptySpawn } from 'node-pty';
99
import { getSSHExecutionService } from './src/server/ssh-execution-service.js';
1010
import { getDatabase } from './src/server/database-prisma.js';
11+
import { registerGlobalErrorHandlers } from './src/server/logging/globalHandlers.js';
1112

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

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

1922
// WebSocket handler for script execution

src/app/api/servers/[id]/route.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import type { NextRequest } from 'next/server';
22
import { NextResponse } from 'next/server';
33
import { getDatabase } from '../../../../server/database-prisma';
44
import type { CreateServerData } from '../../../../types/server';
5+
import { withApiLogging } from '../../../../server/logging/withApiLogging';
56

6-
export async function GET(
7+
export const GET = withApiLogging(async function GET(
78
request: NextRequest,
89
{ params }: { params: Promise<{ id: string }> }
910
) {
@@ -28,16 +29,16 @@ export async function GET(
2829
}
2930

3031
return NextResponse.json(server);
31-
} catch (error) {
32-
console.error('Error fetching server:', error);
32+
} catch {
33+
// Error handled by withApiLogging
3334
return NextResponse.json(
3435
{ error: 'Failed to fetch server' },
3536
{ status: 500 }
3637
);
3738
}
38-
}
39+
}, { redactBody: true });
3940

40-
export async function PUT(
41+
export const PUT = withApiLogging(async function PUT(
4142
request: NextRequest,
4243
{ params }: { params: Promise<{ id: string }> }
4344
) {
@@ -62,8 +63,9 @@ export async function PUT(
6263
);
6364
}
6465

65-
// Validate SSH port
66-
if (ssh_port !== undefined && (ssh_port < 1 || ssh_port > 65535)) {
66+
// Coerce and validate SSH port
67+
const port = ssh_port !== undefined ? parseInt(String(ssh_port), 10) : 22;
68+
if (Number.isNaN(port) || port < 1 || port > 65535) {
6769
return NextResponse.json(
6870
{ error: 'SSH port must be between 1 and 65535' },
6971
{ status: 400 }
@@ -111,7 +113,7 @@ export async function PUT(
111113
auth_type: authType,
112114
ssh_key,
113115
ssh_key_passphrase,
114-
ssh_port: ssh_port ?? 22,
116+
ssh_port: port,
115117
color,
116118
key_generated: key_generated ?? false,
117119
ssh_key_path
@@ -124,7 +126,7 @@ export async function PUT(
124126
}
125127
);
126128
} catch (error) {
127-
console.error('Error updating server:', error);
129+
// Error handled by withApiLogging
128130

129131
// Handle unique constraint violation
130132
if (error instanceof Error && error.message.includes('UNIQUE constraint failed')) {
@@ -139,9 +141,9 @@ export async function PUT(
139141
{ status: 500 }
140142
);
141143
}
142-
}
144+
}, { redactBody: true });
143145

144-
export async function DELETE(
146+
export const DELETE = withApiLogging(async function DELETE(
145147
request: NextRequest,
146148
{ params }: { params: Promise<{ id: string }> }
147149
) {
@@ -177,12 +179,12 @@ export async function DELETE(
177179
changes: 1
178180
}
179181
);
180-
} catch (error) {
181-
console.error('Error deleting server:', error);
182+
} catch {
183+
// Error handled by withApiLogging
182184
return NextResponse.json(
183185
{ error: 'Failed to delete server' },
184186
{ status: 500 }
185187
);
186188
}
187-
}
189+
}, { redactBody: true });
188190

src/app/api/servers/generate-keypair/route.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import type { NextRequest } from 'next/server';
22
import { NextResponse } from 'next/server';
33
import { getSSHService } from '../../../../server/ssh-service';
44
import { getDatabase } from '../../../../server/database-prisma';
5+
import { withApiLogging } from '../../../../server/logging/withApiLogging';
56

6-
export async function POST(_request: NextRequest) {
7+
export const POST = withApiLogging(async function POST(_request: NextRequest) {
78
try {
89
const sshService = getSSHService();
910
const db = getDatabase();
@@ -20,7 +21,7 @@ export async function POST(_request: NextRequest) {
2021
serverId: serverId
2122
});
2223
} catch (error) {
23-
console.error('Error generating SSH key pair:', error);
24+
// Error handled by withApiLogging
2425
return NextResponse.json(
2526
{
2627
success: false,
@@ -29,4 +30,4 @@ export async function POST(_request: NextRequest) {
2930
{ status: 500 }
3031
);
3132
}
32-
}
33+
}, { redactBody: true });

src/app/api/servers/route.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,23 @@ import type { NextRequest } from 'next/server';
22
import { NextResponse } from 'next/server';
33
import { getDatabase } from '../../../server/database-prisma';
44
import type { CreateServerData } from '../../../types/server';
5+
import { withApiLogging } from '../../../server/logging/withApiLogging';
56

6-
export async function GET() {
7+
export const GET = withApiLogging(async function GET() {
78
try {
89
const db = getDatabase();
910
const servers = await db.getAllServers();
1011
return NextResponse.json(servers);
11-
} catch (error) {
12-
console.error('Error fetching servers:', error);
12+
} catch {
13+
// Error handled by withApiLogging
1314
return NextResponse.json(
1415
{ error: 'Failed to fetch servers' },
1516
{ status: 500 }
1617
);
1718
}
18-
}
19+
}, { redactBody: true });
1920

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

33-
// Validate SSH port
34-
if (ssh_port !== undefined && (ssh_port < 1 || ssh_port > 65535)) {
34+
// Coerce and validate SSH port
35+
const port = ssh_port !== undefined ? parseInt(String(ssh_port), 10) : 22;
36+
if (Number.isNaN(port) || port < 1 || port > 65535) {
3537
return NextResponse.json(
3638
{ error: 'SSH port must be between 1 and 65535' },
3739
{ status: 400 }
@@ -69,7 +71,7 @@ export async function POST(request: NextRequest) {
6971
auth_type: authType,
7072
ssh_key,
7173
ssh_key_passphrase,
72-
ssh_port: ssh_port ?? 22,
74+
ssh_port: port,
7375
color,
7476
key_generated: key_generated ?? false,
7577
ssh_key_path
@@ -82,11 +84,10 @@ export async function POST(request: NextRequest) {
8284
},
8385
{ status: 201 }
8486
);
85-
} catch (error) {
86-
console.error('Error creating server:', error);
87-
87+
} catch {
88+
// Error handled by withApiLogging
8889
// Handle unique constraint violation
89-
if (error instanceof Error && error.message.includes('UNIQUE constraint failed')) {
90+
if (Error instanceof Error && Error.message.includes('UNIQUE constraint failed')) {
9091
return NextResponse.json(
9192
{ error: 'A server with this name already exists' },
9293
{ status: 409 }
@@ -98,5 +99,5 @@ export async function POST(request: NextRequest) {
9899
{ status: 500 }
99100
);
100101
}
101-
}
102+
}, { redactBody: true });
102103

src/app/api/settings/auth-credentials/route.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { NextResponse } from 'next/server';
33
import { getAuthConfig, updateAuthCredentials, updateAuthEnabled } from '~/lib/auth';
44
import fs from 'fs';
55
import path from 'path';
6+
import { withApiLogging } from '../../../../server/logging/withApiLogging';
67

7-
export async function GET() {
8+
export const GET = withApiLogging(async function GET() {
89
try {
910
const authConfig = getAuthConfig();
1011

@@ -14,16 +15,16 @@ export async function GET() {
1415
hasCredentials: authConfig.hasCredentials,
1516
setupCompleted: authConfig.setupCompleted,
1617
});
17-
} catch (error) {
18-
console.error('Error reading auth credentials:', error);
18+
} catch {
19+
// Error handled by withApiLogging
1920
return NextResponse.json(
2021
{ error: 'Failed to read auth configuration' },
2122
{ status: 500 }
2223
);
2324
}
24-
}
25+
}, { redactBody: true });
2526

26-
export async function POST(request: NextRequest) {
27+
export const POST = withApiLogging(async function POST(request: NextRequest) {
2728
try {
2829
const { username, password, enabled } = await request.json() as { username: string; password: string; enabled?: boolean };
2930

@@ -54,16 +55,16 @@ export async function POST(request: NextRequest) {
5455
success: true,
5556
message: 'Authentication credentials updated successfully'
5657
});
57-
} catch (error) {
58-
console.error('Error updating auth credentials:', error);
58+
} catch {
59+
// Error handled by withApiLogging
5960
return NextResponse.json(
6061
{ error: 'Failed to update auth credentials' },
6162
{ status: 500 }
6263
);
6364
}
64-
}
65+
}, { redactBody: true });
6566

66-
export async function PATCH(request: NextRequest) {
67+
export const PATCH = withApiLogging(async function PATCH(request: NextRequest) {
6768
try {
6869
const { enabled } = await request.json() as { enabled: boolean };
6970

@@ -107,11 +108,11 @@ export async function PATCH(request: NextRequest) {
107108
success: true,
108109
message: `Authentication ${enabled ? 'enabled' : 'disabled'} successfully`
109110
});
110-
} catch (error) {
111-
console.error('Error updating auth enabled status:', error);
111+
} catch {
112+
// Error handled by withApiLogging
112113
return NextResponse.json(
113114
{ error: 'Failed to update auth status' },
114115
{ status: 500 }
115116
);
116117
}
117-
}
118+
}, { redactBody: true });

src/app/api/settings/github-token/route.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import type { NextRequest } from 'next/server';
22
import { NextResponse } from 'next/server';
33
import fs from 'fs';
44
import path from 'path';
5+
import { withApiLogging } from '../../../../server/logging/withApiLogging';
56

6-
export async function POST(request: NextRequest) {
7+
export const POST = withApiLogging(async function POST(request: NextRequest) {
78
try {
89
const { token } = await request.json();
910

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

4142
return NextResponse.json({ success: true, message: 'GitHub token saved successfully' });
42-
} catch (error) {
43-
console.error('Error saving GitHub token:', error);
43+
} catch {
44+
// Error handled by withApiLogging
4445
return NextResponse.json(
4546
{ error: 'Failed to save GitHub token' },
4647
{ status: 500 }
4748
);
4849
}
49-
}
50+
}, { redactBody: true });
5051

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

6768
return NextResponse.json({ token });
68-
} catch (error) {
69-
console.error('Error reading GitHub token:', error);
69+
} catch {
70+
// Error handled by withApiLogging
7071
return NextResponse.json(
7172
{ error: 'Failed to read GitHub token' },
7273
{ status: 500 }
7374
);
7475
}
75-
}
76+
}, { redactBody: true });

src/app/api/trpc/[trpc]/route.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type NextRequest } from "next/server";
44
import { env } from "~/env.js";
55
import { appRouter } from "~/server/api/root";
66
import { createTRPCContext } from "~/server/api/trpc";
7+
import logger from "../../../../server/logging/logger";
78

89
const handler = (req: NextRequest) =>
910
fetchRequestHandler({
@@ -14,9 +15,7 @@ const handler = (req: NextRequest) =>
1415
onError:
1516
env.NODE_ENV === "development"
1617
? ({ path, error }) => {
17-
console.error(
18-
`[ERROR] tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
19-
);
18+
logger.error("trpc_error", { path: path ?? "<no-path>" }, error);
2019
}
2120
: undefined,
2221
});

src/server/db.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const globalForPrisma = globalThis as unknown as {
44
prisma: PrismaClient | undefined;
55
};
66

7-
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
7+
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
8+
log: ['warn', 'error']
9+
});
810

911
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import logger from './logger';
2+
import { toSafeError } from './prismaSafeError';
3+
4+
let registered = false;
5+
6+
export function registerGlobalErrorHandlers() {
7+
if (registered) return;
8+
registered = true;
9+
10+
process.on('uncaughtException', (err) => {
11+
const safe = toSafeError(err);
12+
logger.error('uncaught_exception', { name: safe.name, code: safe.code }, err);
13+
});
14+
15+
process.on('unhandledRejection', (reason) => {
16+
const safe = toSafeError(reason as any);
17+
logger.error('unhandled_rejection', { name: safe.name, code: safe.code }, reason);
18+
});
19+
}
20+
21+

0 commit comments

Comments
 (0)