From 8014fc3986b9ca7f359147943adcd6f88b75f482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C4=B0pek?= Date: Fri, 10 Apr 2026 21:10:09 +0300 Subject: [PATCH] Refactor error logging across the application to use a centralized logger - Introduced a new logger utility (`logError`) to standardize error logging. - Replaced all instances of `console.error` with `logError` in various API routes and libraries. - Enhanced error logging to sanitize sensitive information, particularly for Prisma and Stripe errors. - Ensured consistent error handling and logging practices throughout the codebase. --- app/api/admin/feedback/[feedbackId]/route.ts | 3 +- app/api/admin/stats/refresh-r2/route.ts | 3 +- app/api/approvals/[requestId]/cancel/route.ts | 3 +- .../approvals/[requestId]/decision/route.ts | 9 +-- app/api/auth/register/route.ts | 3 +- app/api/billing/checkout/route.ts | 3 +- app/api/billing/portal/route.ts | 3 +- app/api/billing/route.ts | 3 +- app/api/comments/[commentId]/route.ts | 9 +-- app/api/feedback/route.ts | 3 +- app/api/feedback/upload/route.ts | 3 +- .../[projectId]/approval-candidates/route.ts | 3 +- .../[projectId]/members/[memberId]/route.ts | 5 +- .../invitations/[invitationId]/route.ts | 3 +- app/api/projects/[projectId]/members/route.ts | 5 +- app/api/projects/[projectId]/route.ts | 7 ++- .../[projectId]/tags/[tagId]/route.ts | 5 +- app/api/projects/[projectId]/tags/route.ts | 5 +- .../[projectId]/videos/[videoId]/route.ts | 7 ++- .../videos/[videoId]/share/route.ts | 9 +-- .../[videoId]/versions/[versionId]/route.ts | 5 +- .../videos/[videoId]/versions/route.ts | 7 ++- .../[projectId]/videos/bunny-init/route.ts | 7 ++- app/api/projects/[projectId]/videos/route.ts | 7 ++- app/api/projects/route.ts | 5 +- app/api/search/route.ts | 3 +- app/api/settings/notifications/route.ts | 9 +-- app/api/stripe/webhook/route.ts | 5 +- app/api/upload/audio/[filename]/route.ts | 3 +- app/api/upload/audio/route.ts | 3 +- app/api/upload/image/[filename]/route.ts | 3 +- app/api/upload/image/route.ts | 3 +- .../versions/[versionId]/approvals/route.ts | 7 ++- .../[versionId]/comments/export/route.ts | 3 +- .../versions/[versionId]/comments/route.ts | 9 +-- .../versions/[versionId]/download/route.ts | 5 +- .../assets/[assetId]/download/route.ts | 3 +- .../[videoId]/assets/[assetId]/route.ts | 3 +- .../[videoId]/assets/bunny-init/route.ts | 7 ++- app/api/videos/[videoId]/assets/route.ts | 5 +- app/api/watch/[videoId]/progress/route.ts | 5 +- app/api/watch/[videoId]/route.ts | 3 +- app/api/watch/[videoId]/upload-token/route.ts | 3 +- .../[workspaceId]/members/[memberId]/route.ts | 5 +- .../invitations/[invitationId]/route.ts | 3 +- .../workspaces/[workspaceId]/members/route.ts | 5 +- app/api/workspaces/[workspaceId]/route.ts | 7 ++- app/api/workspaces/route.ts | 5 +- lib/admin-stats.ts | 11 ++-- lib/invitations.ts | 3 +- lib/logger.ts | 61 +++++++++++++++++++ lib/notifications.ts | 9 +-- lib/r2-cleanup.ts | 3 +- lib/r2-media-proxy.ts | 5 +- lib/rate-limit.ts | 7 ++- lib/video-providers/index.ts | 3 +- scripts/bunny-orphan-cleanup.ts | 5 +- scripts/docker-db-bootstrap.ts | 3 +- scripts/r2-orphan-cleanup.ts | 5 +- scripts/self-host-bootstrap.ts | 3 +- 60 files changed, 235 insertions(+), 115 deletions(-) create mode 100644 lib/logger.ts diff --git a/app/api/admin/feedback/[feedbackId]/route.ts b/app/api/admin/feedback/[feedbackId]/route.ts index 958c323..c6b6450 100644 --- a/app/api/admin/feedback/[feedbackId]/route.ts +++ b/app/api/admin/feedback/[feedbackId]/route.ts @@ -5,6 +5,7 @@ import { db } from '@/lib/db'; import { apiErrors, successResponse } from '@/lib/api-response'; import { rateLimit } from '@/lib/rate-limit'; import { r2Client, R2_BUCKET_NAME } from '@/lib/r2'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ feedbackId: string }> }; @@ -121,7 +122,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { if (message.includes('Record to delete does not exist')) { return apiErrors.notFound('Feedback'); } - console.error('Error deleting feedback:', error); + logError('Error deleting feedback:', error); return apiErrors.internalError('Failed to delete feedback'); } } diff --git a/app/api/admin/stats/refresh-r2/route.ts b/app/api/admin/stats/refresh-r2/route.ts index 0374557..1fcf7e5 100644 --- a/app/api/admin/stats/refresh-r2/route.ts +++ b/app/api/admin/stats/refresh-r2/route.ts @@ -1,6 +1,7 @@ import { auth } from '@/lib/auth'; import { apiErrors, successResponse } from '@/lib/api-response'; import { refreshR2StorageSnapshot } from '@/lib/admin-stats'; +import { logError } from '@/lib/logger'; export async function POST() { try { @@ -16,7 +17,7 @@ export async function POST() { refreshedAt, }); } catch (error) { - console.error('Error refreshing R2 admin stats cache:', error); + logError('Error refreshing R2 admin stats cache:', error); return apiErrors.internalError('Failed to refresh R2 stats'); } } diff --git a/app/api/approvals/[requestId]/cancel/route.ts b/app/api/approvals/[requestId]/cancel/route.ts index e534de7..48762be 100644 --- a/app/api/approvals/[requestId]/cancel/route.ts +++ b/app/api/approvals/[requestId]/cancel/route.ts @@ -4,6 +4,7 @@ import { auth, checkProjectAccess } from '@/lib/auth'; import { db } from '@/lib/db'; import { rateLimit } from '@/lib/rate-limit'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ requestId: string }> }; @@ -87,7 +88,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { if (isSerializableConflict(error)) { return apiErrors.conflict('Request state changed. Please try again.'); } - console.error('Error canceling approval request:', error); + logError('Error canceling approval request:', error); return apiErrors.internalError('Failed to cancel approval request'); } } diff --git a/app/api/approvals/[requestId]/decision/route.ts b/app/api/approvals/[requestId]/decision/route.ts index ac40479..16b0f09 100644 --- a/app/api/approvals/[requestId]/decision/route.ts +++ b/app/api/approvals/[requestId]/decision/route.ts @@ -5,6 +5,7 @@ import { db } from '@/lib/db'; import { notifyUsers } from '@/lib/notifications'; import { rateLimit } from '@/lib/rate-limit'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ requestId: string }> }; @@ -179,7 +180,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { note: note || undefined, url: requestUrl, }).catch((error) => { - console.error('Approval action notification failed:', error); + logError('Approval action notification failed:', error); }); if (updated.status === 'APPROVED') { @@ -191,7 +192,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { approvedByCount: updated.decisions.filter((item) => item.status === 'APPROVED').length, url: requestUrl, }).catch((error) => { - console.error('Approval completed notification failed:', error); + logError('Approval completed notification failed:', error); }); } else if (updated.status === 'REJECTED') { notifyUsers([updated.requestedById], { @@ -203,7 +204,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { note: note || undefined, url: requestUrl, }).catch((error) => { - console.error('Approval rejected notification failed:', error); + logError('Approval rejected notification failed:', error); }); } @@ -219,7 +220,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { if (isSerializableConflict(error)) { return apiErrors.conflict('Request state changed. Please try again.'); } - console.error('Error responding to approval request:', error); + logError('Error responding to approval request:', error); return apiErrors.internalError('Failed to respond to approval request'); } } diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 3303986..b81aa9d 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -5,6 +5,7 @@ import { acceptInvitationTokenForUser, getValidInvitationByToken } from '@/lib/i import { checkRateLimit, getClientIp, rateLimitHeaders, RATE_LIMIT_CONFIGS } from '@/lib/rate-limit'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; import { isInviteCodeRequired } from '@/lib/feature-flags'; +import { logError } from '@/lib/logger'; export async function POST(request: NextRequest) { try { @@ -128,7 +129,7 @@ export async function POST(request: NextRequest) { return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Registration error:', error); + logError('Registration error:', error); return apiErrors.internalError('Failed to create account'); } } diff --git a/app/api/billing/checkout/route.ts b/app/api/billing/checkout/route.ts index cce5392..db2dd8f 100644 --- a/app/api/billing/checkout/route.ts +++ b/app/api/billing/checkout/route.ts @@ -10,6 +10,7 @@ import { rateLimit } from '@/lib/rate-limit'; import { isStripeFeatureEnabled } from '@/lib/feature-flags'; import { getStripe, getStripePriceId, isStripeConfigured } from '@/lib/stripe'; import { isTrustedSameOriginRequest } from '@/lib/request-origin'; +import { logError } from '@/lib/logger'; function getAppOrigin(request: NextRequest) { if (isTrustedSameOriginRequest(request)) { @@ -79,7 +80,7 @@ export async function POST(request: NextRequest) { const response = successResponse({ url: checkoutSession.url }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error creating Stripe checkout session:', error); + logError('Error creating Stripe checkout session:', error); return apiErrors.internalError('Failed to start checkout'); } } diff --git a/app/api/billing/portal/route.ts b/app/api/billing/portal/route.ts index 1f97e4f..7b9f791 100644 --- a/app/api/billing/portal/route.ts +++ b/app/api/billing/portal/route.ts @@ -6,6 +6,7 @@ import { rateLimit } from '@/lib/rate-limit'; import { isStripeFeatureEnabled } from '@/lib/feature-flags'; import { getStripe, isStripeConfigured } from '@/lib/stripe'; import { isTrustedSameOriginRequest } from '@/lib/request-origin'; +import { logError } from '@/lib/logger'; function getAppOrigin(request: NextRequest) { if (isTrustedSameOriginRequest(request)) { @@ -54,7 +55,7 @@ export async function POST(request: NextRequest) { const response = successResponse({ url: portalSession.url }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error creating Stripe portal session:', error); + logError('Error creating Stripe portal session:', error); return apiErrors.internalError('Failed to open billing portal'); } } diff --git a/app/api/billing/route.ts b/app/api/billing/route.ts index 5b7205d..63e03fc 100644 --- a/app/api/billing/route.ts +++ b/app/api/billing/route.ts @@ -3,6 +3,7 @@ import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response import { getBillingOverview } from '@/lib/billing'; import { isStripeFeatureEnabled } from '@/lib/feature-flags'; import { hasStripeRuntimeConfig, isStripeConfigured } from '@/lib/stripe'; +import { logError } from '@/lib/logger'; export async function GET() { try { @@ -39,7 +40,7 @@ export async function GET() { return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error fetching billing overview:', error); + logError('Error fetching billing overview:', error); return apiErrors.internalError('Failed to fetch billing overview'); } } diff --git a/app/api/comments/[commentId]/route.ts b/app/api/comments/[commentId]/route.ts index c0e03f2..0d18f2d 100644 --- a/app/api/comments/[commentId]/route.ts +++ b/app/api/comments/[commentId]/route.ts @@ -10,6 +10,7 @@ import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response import { getGuestIdentityFromRequest } from '@/lib/guest-identity'; import { runWithConcurrency } from '@/lib/async-pool'; import { validateAnnotationStrokes } from '@/lib/validation'; +import { logError } from '@/lib/logger'; const CLEANUP_DELETE_CONCURRENCY = 5; @@ -95,7 +96,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const response = successResponse(commentData); return withCacheControl(response, 'private, no-cache'); } catch (error) { - console.error('Error fetching comment:', error); + logError('Error fetching comment:', error); return apiErrors.internalError('Failed to fetch comment'); } } @@ -230,7 +231,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error updating comment:', error); + logError('Error updating comment:', error); return apiErrors.internalError('Failed to update comment'); } } @@ -334,14 +335,14 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { }) ); } catch (err) { - console.error(`Failed to delete media from R2 (key: ${key}):`, err); + logError(`Failed to delete media from R2 (key: ${key}):`, err); } }); const response = successResponse({ message: 'Comment deleted' }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error deleting comment:', error); + logError('Error deleting comment:', error); return apiErrors.internalError('Failed to delete comment'); } } diff --git a/app/api/feedback/route.ts b/app/api/feedback/route.ts index 97c627b..7343b28 100644 --- a/app/api/feedback/route.ts +++ b/app/api/feedback/route.ts @@ -4,6 +4,7 @@ import { auth } from '@/lib/auth'; import { db } from '@/lib/db'; import { apiErrors, successResponse } from '@/lib/api-response'; import { rateLimit } from '@/lib/rate-limit'; +import { logError } from '@/lib/logger'; interface FeedbackPayload { type?: string; @@ -145,7 +146,7 @@ export async function POST(request: NextRequest) { return successResponse(entry, 201); } catch (error) { - console.error('Error submitting feedback:', error); + logError('Error submitting feedback:', error); return apiErrors.internalError('Failed to submit feedback'); } } diff --git a/app/api/feedback/upload/route.ts b/app/api/feedback/upload/route.ts index 91cbbd0..31db926 100644 --- a/app/api/feedback/upload/route.ts +++ b/app/api/feedback/upload/route.ts @@ -11,6 +11,7 @@ import { } from '@/lib/image-upload-validation'; import { rateLimit } from '@/lib/rate-limit'; import { r2Client, R2_BUCKET_NAME } from '@/lib/r2'; +import { logError } from '@/lib/logger'; const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB const MAX_MULTIPART_BODY_SIZE = MAX_FILE_SIZE + (512 * 1024); // file + multipart overhead @@ -78,7 +79,7 @@ export async function POST(request: NextRequest) { return successResponse({ url: `/api/upload/image/${filename}` }, 201); } catch (error) { - console.error('Error uploading feedback screenshot:', error); + logError('Error uploading feedback screenshot:', error); return apiErrors.internalError('Failed to upload screenshot'); } } diff --git a/app/api/projects/[projectId]/approval-candidates/route.ts b/app/api/projects/[projectId]/approval-candidates/route.ts index 341ed56..e80f76d 100644 --- a/app/api/projects/[projectId]/approval-candidates/route.ts +++ b/app/api/projects/[projectId]/approval-candidates/route.ts @@ -3,6 +3,7 @@ import { auth, checkProjectAccess } from '@/lib/auth'; import { db } from '@/lib/db'; import { getApprovalCandidatesForProject } from '@/lib/approval-workflow'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ projectId: string }> }; @@ -28,7 +29,7 @@ export async function GET(_request: NextRequest, { params }: RouteParams) { const response = successResponse({ candidates }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error fetching approval candidates:', error); + logError('Error fetching approval candidates:', error); return apiErrors.internalError('Failed to fetch approval candidates'); } } diff --git a/app/api/projects/[projectId]/members/[memberId]/route.ts b/app/api/projects/[projectId]/members/[memberId]/route.ts index 292b86d..317390e 100644 --- a/app/api/projects/[projectId]/members/[memberId]/route.ts +++ b/app/api/projects/[projectId]/members/[memberId]/route.ts @@ -4,6 +4,7 @@ import { auth, checkProjectAccess } from '@/lib/auth'; import { ProjectMemberRole } from '@prisma/client'; import { rateLimit } from '@/lib/rate-limit'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ projectId: string; memberId: string }> }; @@ -65,7 +66,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { const response = successResponse(updatedMember); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error updating member role:', error); + logError('Error updating member role:', error); return apiErrors.internalError('Failed to update member role'); } } @@ -116,7 +117,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { const response = successResponse({ message: 'Member removed' }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error removing member:', error); + logError('Error removing member:', error); return apiErrors.internalError('Failed to remove member'); } } diff --git a/app/api/projects/[projectId]/members/invitations/[invitationId]/route.ts b/app/api/projects/[projectId]/members/invitations/[invitationId]/route.ts index f2f4dd6..06ba58b 100644 --- a/app/api/projects/[projectId]/members/invitations/[invitationId]/route.ts +++ b/app/api/projects/[projectId]/members/invitations/[invitationId]/route.ts @@ -4,6 +4,7 @@ import { auth, checkProjectAccess } from '@/lib/auth'; import { db } from '@/lib/db'; import { rateLimit } from '@/lib/rate-limit'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ projectId: string; invitationId: string }> }; @@ -65,7 +66,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { const response = successResponse({ message: 'Invitation canceled' }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error canceling project invitation:', error); + logError('Error canceling project invitation:', error); return apiErrors.internalError('Failed to cancel invitation'); } } diff --git a/app/api/projects/[projectId]/members/route.ts b/app/api/projects/[projectId]/members/route.ts index d936b79..f65693f 100644 --- a/app/api/projects/[projectId]/members/route.ts +++ b/app/api/projects/[projectId]/members/route.ts @@ -5,6 +5,7 @@ import { InvitationRole, ProjectMemberRole } from '@prisma/client'; import { rateLimit } from '@/lib/rate-limit'; import { buildInvitationUrl, createOrRefreshInvitation, sendInvitationEmail } from '@/lib/invitations'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ projectId: string }> }; @@ -78,7 +79,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const response = successResponse({ members, owner, pendingInvitations }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error fetching project members:', error); + logError('Error fetching project members:', error); return apiErrors.internalError('Failed to fetch members'); } } @@ -172,7 +173,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { const response = successResponse({ message: 'Invitation email sent.' }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error inviting project member:', error); + logError('Error inviting project member:', error); return apiErrors.internalError('Failed to invite member'); } } diff --git a/app/api/projects/[projectId]/route.ts b/app/api/projects/[projectId]/route.ts index fe106ec..e3e2464 100644 --- a/app/api/projects/[projectId]/route.ts +++ b/app/api/projects/[projectId]/route.ts @@ -6,6 +6,7 @@ import { collectProjectMediaUrls, deleteMediaFilesBestEffort } from '@/lib/r2-cl import { cleanupBunnyStreamVideosBestEffort } from '@/lib/bunny-stream-cleanup'; import { buildCleanupWarnings, logCleanupWarnings } from '@/lib/cleanup-warnings'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ projectId: string }> }; @@ -79,7 +80,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const response = successResponse(project); return withCacheControl(response, 'private, max-age=30, stale-while-revalidate=60'); } catch (error) { - console.error('Error fetching project:', error); + logError('Error fetching project:', error); return apiErrors.internalError('Failed to fetch project'); } } @@ -128,7 +129,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { const response = successResponse(project); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error updating project:', error); + logError('Error updating project:', error); return apiErrors.internalError('Failed to update project'); } } @@ -212,7 +213,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error deleting project:', error); + logError('Error deleting project:', error); return apiErrors.internalError('Failed to delete project'); } } diff --git a/app/api/projects/[projectId]/tags/[tagId]/route.ts b/app/api/projects/[projectId]/tags/[tagId]/route.ts index cd00a8b..f557ff2 100644 --- a/app/api/projects/[projectId]/tags/[tagId]/route.ts +++ b/app/api/projects/[projectId]/tags/[tagId]/route.ts @@ -3,6 +3,7 @@ import { db } from '@/lib/db'; import { auth, checkProjectAccess } from '@/lib/auth'; import { rateLimit } from '@/lib/rate-limit'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ projectId: string; tagId: string }> }; @@ -68,7 +69,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { const response = successResponse(tag); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error updating tag:', error); + logError('Error updating tag:', error); if ((error as { code?: string }).code === 'P2002') { return apiErrors.conflict('Tag name already exists'); } @@ -115,7 +116,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { const response = successResponse({ message: 'Tag deleted' }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error deleting tag:', error); + logError('Error deleting tag:', error); return apiErrors.internalError('Failed to delete tag'); } } diff --git a/app/api/projects/[projectId]/tags/route.ts b/app/api/projects/[projectId]/tags/route.ts index d262663..c1d2e43 100644 --- a/app/api/projects/[projectId]/tags/route.ts +++ b/app/api/projects/[projectId]/tags/route.ts @@ -5,6 +5,7 @@ import { rateLimit } from '@/lib/rate-limit'; import { validateShareLinkAccess } from '@/lib/share-links'; import { getShareSessionFromRequest } from '@/lib/share-session'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ projectId: string }> }; @@ -64,7 +65,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { : 'private, no-cache'; return withCacheControl(response, cacheControl); } catch (error) { - console.error('Error fetching tags:', error); + logError('Error fetching tags:', error); return apiErrors.internalError('Failed to fetch tags'); } } @@ -125,7 +126,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { const response = successResponse(tag, 201); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error creating tag:', error); + logError('Error creating tag:', error); if ((error as { code?: string }).code === 'P2002') { return apiErrors.conflict('Tag name already exists'); } diff --git a/app/api/projects/[projectId]/videos/[videoId]/route.ts b/app/api/projects/[projectId]/videos/[videoId]/route.ts index e416b22..239f753 100644 --- a/app/api/projects/[projectId]/videos/[videoId]/route.ts +++ b/app/api/projects/[projectId]/videos/[videoId]/route.ts @@ -7,6 +7,7 @@ import { collectVideoMediaUrls, deleteMediaFilesBestEffort } from '@/lib/r2-clea import { cleanupBunnyStreamVideosBestEffort } from '@/lib/bunny-stream-cleanup'; import { buildCleanupWarnings, logCleanupWarnings } from '@/lib/cleanup-warnings'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ projectId: string; videoId: string }> }; @@ -134,7 +135,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { return withCacheControl(response, 'private, no-cache'); } catch (error) { - console.error('Error fetching video:', error); + logError('Error fetching video:', error); return apiErrors.internalError('Failed to fetch video'); } } @@ -189,7 +190,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { const response = successResponse(updatedVideo); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error updating video:', error); + logError('Error updating video:', error); return apiErrors.internalError('Failed to update video'); } } @@ -270,7 +271,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error deleting video:', error); + logError('Error deleting video:', error); return apiErrors.internalError('Failed to delete video'); } } diff --git a/app/api/projects/[projectId]/videos/[videoId]/share/route.ts b/app/api/projects/[projectId]/videos/[videoId]/share/route.ts index 480ab51..dc30be7 100644 --- a/app/api/projects/[projectId]/videos/[videoId]/share/route.ts +++ b/app/api/projects/[projectId]/videos/[videoId]/share/route.ts @@ -7,6 +7,7 @@ import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response import { db } from '@/lib/db'; import { rateLimit } from '@/lib/rate-limit'; import { MAX_SHARE_PASSWORD_LENGTH } from '@/lib/share-links'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ projectId: string; videoId: string }> }; @@ -123,7 +124,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error fetching video share link:', error); + logError('Error fetching video share link:', error); return apiErrors.internalError('Failed to fetch video share link'); } } @@ -238,7 +239,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error creating video share link:', error); + logError('Error creating video share link:', error); return apiErrors.internalError('Failed to create video share link'); } } @@ -314,7 +315,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { const response = successResponse(serializeShareLink(request, videoId, updated)); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error updating video share link:', error); + logError('Error updating video share link:', error); return apiErrors.internalError('Failed to update video share link'); } } @@ -345,7 +346,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { const response = successResponse({ message: 'Video share link revoked' }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error deleting video share link:', error); + logError('Error deleting video share link:', error); return apiErrors.internalError('Failed to delete video share link'); } } diff --git a/app/api/projects/[projectId]/videos/[videoId]/versions/[versionId]/route.ts b/app/api/projects/[projectId]/videos/[videoId]/versions/[versionId]/route.ts index 85fdb7c..478913d 100644 --- a/app/api/projects/[projectId]/videos/[videoId]/versions/[versionId]/route.ts +++ b/app/api/projects/[projectId]/videos/[videoId]/versions/[versionId]/route.ts @@ -5,6 +5,7 @@ import { rateLimit } from '@/lib/rate-limit'; import { cleanupBunnyStreamVideosBestEffort } from '@/lib/bunny-stream-cleanup'; import { buildCleanupWarnings, logCleanupWarnings } from '@/lib/cleanup-warnings'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ projectId: string; videoId: string; versionId: string }> }; @@ -75,7 +76,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { const response = successResponse(updated); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error updating version:', error); + logError('Error updating version:', error); return apiErrors.internalError('Failed to update version'); } } @@ -148,7 +149,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error deleting version:', error); + logError('Error deleting version:', error); return apiErrors.internalError('Failed to delete version'); } } diff --git a/app/api/projects/[projectId]/videos/[videoId]/versions/route.ts b/app/api/projects/[projectId]/videos/[videoId]/versions/route.ts index 1bd5b72..eaa2b93 100644 --- a/app/api/projects/[projectId]/videos/[videoId]/versions/route.ts +++ b/app/api/projects/[projectId]/videos/[videoId]/versions/route.ts @@ -6,6 +6,7 @@ import { rateLimit } from '@/lib/rate-limit'; import { notifyProjectOwner } from '@/lib/notifications'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; import { verifyBunnyUploadToken } from '@/lib/bunny-upload-token'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ projectId: string; videoId: string }> }; @@ -42,7 +43,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const response = successResponse({ versions }); return withCacheControl(response, 'private, max-age=30, stale-while-revalidate=60'); } catch (error) { - console.error('Error fetching versions:', error); + logError('Error fetching versions:', error); return apiErrors.internalError('Failed to fetch versions'); } } @@ -166,13 +167,13 @@ export async function POST(request: NextRequest, { params }: RouteParams) { versionLabel: version.versionLabel || `Version ${version.versionNumber}`, addedBy: session.user.name || 'A team member', url: `${baseUrl}/watch/${video.id}`, - }).catch((err) => console.error('Notification failed:', err)); + }).catch((err) => logError('Notification failed:', err)); } const response = successResponse(version, 201); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error creating version:', error); + logError('Error creating version:', error); return apiErrors.internalError('Failed to create version'); } } diff --git a/app/api/projects/[projectId]/videos/bunny-init/route.ts b/app/api/projects/[projectId]/videos/bunny-init/route.ts index 23f9516..35cab0d 100644 --- a/app/api/projects/[projectId]/videos/bunny-init/route.ts +++ b/app/api/projects/[projectId]/videos/bunny-init/route.ts @@ -7,6 +7,7 @@ import crypto from 'crypto'; import { cleanupBunnyStreamVideos } from '@/lib/bunny-stream-cleanup'; import { createBunnyUploadToken, verifyBunnyUploadToken } from '@/lib/bunny-upload-token'; import { isBunnyUploadsFeatureEnabled } from '@/lib/feature-flags'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ projectId: string }> }; @@ -74,7 +75,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { }); if (!bunnyRes.ok) { - console.error('Failed to create Bunny Stream video', await bunnyRes.text()); + logError('Failed to create Bunny Stream video', await bunnyRes.text()); return apiErrors.internalError('Failed to initialize video upload with provider'); } @@ -107,7 +108,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error initializing Bunny upload:', error); + logError('Error initializing Bunny upload:', error); return apiErrors.internalError('Failed to initialize upload'); } } @@ -153,7 +154,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { const response = successResponse({ message: 'Pending upload cleaned up' }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error cleaning up pending Bunny upload:', error); + logError('Error cleaning up pending Bunny upload:', error); return apiErrors.internalError('Failed to cleanup pending upload'); } } diff --git a/app/api/projects/[projectId]/videos/route.ts b/app/api/projects/[projectId]/videos/route.ts index b582e78..28a716e 100644 --- a/app/api/projects/[projectId]/videos/route.ts +++ b/app/api/projects/[projectId]/videos/route.ts @@ -6,6 +6,7 @@ import { rateLimit } from '@/lib/rate-limit'; import { notifyProjectOwner } from '@/lib/notifications'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; import { verifyBunnyUploadToken } from '@/lib/bunny-upload-token'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ projectId: string }> }; @@ -53,7 +54,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const response = successResponse({ videos }); return withCacheControl(response, 'private, max-age=30, stale-while-revalidate=60'); } catch (error) { - console.error('Error fetching videos:', error); + logError('Error fetching videos:', error); return apiErrors.internalError('Failed to fetch videos'); } } @@ -167,13 +168,13 @@ export async function POST(request: NextRequest, { params }: RouteParams) { videoTitle: title.trim(), addedBy: session.user.name || 'A team member', url: `${baseUrl}/watch/${video.id}`, - }).catch((err) => console.error('Notification failed:', err)); + }).catch((err) => logError('Notification failed:', err)); } const response = successResponse(video, 201); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error creating video:', error); + logError('Error creating video:', error); return apiErrors.internalError('Failed to create video'); } } diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts index 06951f2..33abeed 100644 --- a/app/api/projects/route.ts +++ b/app/api/projects/route.ts @@ -6,6 +6,7 @@ import { rateLimit } from '@/lib/rate-limit'; import { buildBillingAccessWhereInput } from '@/lib/billing'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; import { DEFAULT_COMMENT_TAGS } from '@/lib/comment-tags'; +import { logError } from '@/lib/logger'; // GET /api/projects - List all projects for the authenticated user export async function GET(request: NextRequest) { @@ -94,7 +95,7 @@ export async function GET(request: NextRequest) { return withCacheControl(response, 'private, max-age=30, stale-while-revalidate=60'); } catch (error) { - console.error('Error fetching projects:', error); + logError('Error fetching projects:', error); return apiErrors.internalError('Failed to fetch projects'); } } @@ -194,7 +195,7 @@ export async function POST(request: NextRequest) { const response = successResponse(project, 201); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error creating project:', error); + logError('Error creating project:', error); return apiErrors.internalError('Failed to create project'); } } diff --git a/app/api/search/route.ts b/app/api/search/route.ts index 60eae12..5cce53f 100644 --- a/app/api/search/route.ts +++ b/app/api/search/route.ts @@ -3,6 +3,7 @@ import { db } from '@/lib/db'; import { auth } from '@/lib/auth'; import { apiErrors, successResponse } from '@/lib/api-response'; import { checkRateLimit, rateLimitHeaders, RATE_LIMIT_CONFIGS } from '@/lib/rate-limit'; +import { logError } from '@/lib/logger'; const MAX_Q_LENGTH = 100; const RESULTS_PER_CATEGORY = 5; @@ -114,7 +115,7 @@ export async function GET(request: NextRequest) { response.headers.set('Cache-Control', 'private, no-store'); return response; } catch (err) { - console.error('[search] error:', err); + logError('[search] error:', err); return apiErrors.internalError(); } } diff --git a/app/api/settings/notifications/route.ts b/app/api/settings/notifications/route.ts index ae02fcd..c735224 100644 --- a/app/api/settings/notifications/route.ts +++ b/app/api/settings/notifications/route.ts @@ -5,6 +5,7 @@ import { rateLimit } from '@/lib/rate-limit'; import nodemailer from 'nodemailer'; import { testEmailHtml } from '@/lib/notifications'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; // GET /api/settings/notifications — Fetch current notification preferences export async function GET() { @@ -35,7 +36,7 @@ export async function GET() { return withCacheControl(response, 'private, max-age=30, stale-while-revalidate=60'); } catch (error) { - console.error('Error fetching notification settings:', error); + logError('Error fetching notification settings:', error); return apiErrors.internalError('Failed to fetch settings'); } } @@ -102,7 +103,7 @@ export async function PUT(request: NextRequest) { const response = successResponse(settings); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error updating notification settings:', error); + logError('Error updating notification settings:', error); return apiErrors.internalError('Failed to update settings'); } } @@ -197,7 +198,7 @@ export async function POST(request: NextRequest) { html: testEmailHtml(), }); } catch (emailErr) { - console.error('SMTP test email failed:', emailErr); + logError('SMTP test email failed:', emailErr); return apiErrors.internalError('Failed to send test email — check SMTP settings'); } @@ -207,7 +208,7 @@ export async function POST(request: NextRequest) { return apiErrors.badRequest('Unknown channel'); } catch (error) { - console.error('Error testing notification:', error); + logError('Error testing notification:', error); return apiErrors.internalError('Failed to test notification'); } } diff --git a/app/api/stripe/webhook/route.ts b/app/api/stripe/webhook/route.ts index 7e118fe..39a2611 100644 --- a/app/api/stripe/webhook/route.ts +++ b/app/api/stripe/webhook/route.ts @@ -5,6 +5,7 @@ import { syncStripeSubscriptionToUser, } from '@/lib/billing'; import { getStripe, getStripeWebhookSecret } from '@/lib/stripe'; +import { logError } from '@/lib/logger'; export const runtime = 'nodejs'; @@ -42,7 +43,7 @@ export async function POST(request: NextRequest) { const body = await request.text(); event = stripe.webhooks.constructEvent(body, signature, getStripeWebhookSecret()); } catch (error) { - console.error('Failed to verify Stripe webhook:', error); + logError('Failed to verify Stripe webhook:', error); return new Response('Invalid webhook signature', { status: 400 }); } @@ -82,7 +83,7 @@ export async function POST(request: NextRequest) { headers: { 'Content-Type': 'application/json' }, }); } catch (error) { - console.error('Failed to process Stripe webhook:', error); + logError('Failed to process Stripe webhook:', error); return new Response('Webhook processing failed', { status: 500 }); } } diff --git a/app/api/upload/audio/[filename]/route.ts b/app/api/upload/audio/[filename]/route.ts index b10df65..54edca7 100644 --- a/app/api/upload/audio/[filename]/route.ts +++ b/app/api/upload/audio/[filename]/route.ts @@ -1,5 +1,6 @@ import { apiErrors } from '@/lib/api-response'; import { proxyR2MediaObject } from '@/lib/r2-media-proxy'; +import { logError } from '@/lib/logger'; // Only allow UUID filenames with safe extensions const SAFE_FILENAME = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[a-z0-9]+$/i; @@ -39,7 +40,7 @@ export async function GET( internalErrorMessage: 'Failed to retrieve audio', }); } catch (error: unknown) { - console.error('Error serving audio:', error); + logError('Error serving audio:', error); return apiErrors.internalError('Failed to retrieve audio'); } } diff --git a/app/api/upload/audio/route.ts b/app/api/upload/audio/route.ts index c4b6ef5..cc8631e 100644 --- a/app/api/upload/audio/route.ts +++ b/app/api/upload/audio/route.ts @@ -13,6 +13,7 @@ import { enforceGuestUploadQuota, verifyGuestUploadToken, } from '@/lib/guest-upload-token'; +import { logError } from '@/lib/logger'; const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB const MAX_MULTIPART_BODY_SIZE = MAX_FILE_SIZE + (512 * 1024); // file + multipart overhead @@ -212,7 +213,7 @@ export async function POST(request: NextRequest) { const response = successResponse({ url: voiceUrl }, 201); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error uploading audio:', error); + logError('Error uploading audio:', error); return apiErrors.internalError('Failed to upload audio'); } } diff --git a/app/api/upload/image/[filename]/route.ts b/app/api/upload/image/[filename]/route.ts index 1f3d7b8..7b41bbd 100644 --- a/app/api/upload/image/[filename]/route.ts +++ b/app/api/upload/image/[filename]/route.ts @@ -1,5 +1,6 @@ import { apiErrors } from '@/lib/api-response'; import { proxyR2MediaObject } from '@/lib/r2-media-proxy'; +import { logError } from '@/lib/logger'; // Only allow UUID filenames with safe extensions const SAFE_FILENAME = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[a-z0-9]+$/i; @@ -41,7 +42,7 @@ export async function GET( internalErrorMessage: 'Failed to retrieve image', }); } catch (error: unknown) { - console.error('Error serving image:', error); + logError('Error serving image:', error); return apiErrors.internalError('Failed to retrieve image'); } } diff --git a/app/api/upload/image/route.ts b/app/api/upload/image/route.ts index ee75acb..9803e6b 100644 --- a/app/api/upload/image/route.ts +++ b/app/api/upload/image/route.ts @@ -19,6 +19,7 @@ import { enforceGuestUploadQuota, verifyGuestUploadToken, } from '@/lib/guest-upload-token'; +import { logError } from '@/lib/logger'; const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB const MAX_MULTIPART_BODY_SIZE = MAX_FILE_SIZE + (512 * 1024); // file + multipart overhead @@ -150,7 +151,7 @@ export async function POST(request: NextRequest) { const response = successResponse({ url: imageUrl }, 201); return withCacheControl(response, 'public, max-age=31536000, immutable'); } catch (error) { - console.error('Error uploading image:', error); + logError('Error uploading image:', error); return apiErrors.internalError('Failed to upload image'); } } diff --git a/app/api/versions/[versionId]/approvals/route.ts b/app/api/versions/[versionId]/approvals/route.ts index 43f5223..43d314b 100644 --- a/app/api/versions/[versionId]/approvals/route.ts +++ b/app/api/versions/[versionId]/approvals/route.ts @@ -6,6 +6,7 @@ import { getApprovalCandidatesForProject } from '@/lib/approval-workflow'; import { notifyUsers } from '@/lib/notifications'; import { rateLimit } from '@/lib/rate-limit'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ versionId: string }> }; @@ -54,7 +55,7 @@ export async function GET(_request: NextRequest, { params }: RouteParams) { const response = successResponse({ requests }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error fetching approvals:', error); + logError('Error fetching approvals:', error); return apiErrors.internalError('Failed to fetch approvals'); } } @@ -165,7 +166,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { message: message || undefined, url: requestUrl, }).catch((error) => { - console.error('Approval request notification failed:', error); + logError('Approval request notification failed:', error); }); const response = successResponse({ request: created }, 201); @@ -177,7 +178,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { if (isSerializableConflict(error)) { return apiErrors.conflict('Request state changed. Please try again.'); } - console.error('Error creating approval request:', error); + logError('Error creating approval request:', error); return apiErrors.internalError('Failed to create approval request'); } } diff --git a/app/api/versions/[versionId]/comments/export/route.ts b/app/api/versions/[versionId]/comments/export/route.ts index 511edbc..759079b 100644 --- a/app/api/versions/[versionId]/comments/export/route.ts +++ b/app/api/versions/[versionId]/comments/export/route.ts @@ -9,6 +9,7 @@ import { } from '@/lib/comment-export'; import { apiErrors, withCacheControl } from '@/lib/api-response'; import { rateLimit } from '@/lib/rate-limit'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ versionId: string }> }; const MAX_EXPORT_COMMENTS = 5000; @@ -157,7 +158,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error exporting comments:', error); + logError('Error exporting comments:', error); return apiErrors.internalError('Failed to export comments'); } } diff --git a/app/api/versions/[versionId]/comments/route.ts b/app/api/versions/[versionId]/comments/route.ts index db43e7e..31e049c 100644 --- a/app/api/versions/[versionId]/comments/route.ts +++ b/app/api/versions/[versionId]/comments/route.ts @@ -11,6 +11,7 @@ import { r2Client, R2_BUCKET_NAME } from '@/lib/r2'; import { ensureGuestIdentityFromRequest, getGuestIdentityFromRequest, setGuestIdentityCookie } from '@/lib/guest-identity'; import { extractImageFileNameFromProxyUrl, sanitizeAssetDisplayName } from '@/lib/video-assets'; import { validateAnnotationStrokes } from '@/lib/validation'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ versionId: string }> }; const SAFE_IMAGE_PATH = /^\/api\/upload\/image\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[a-z0-9]+$/i; @@ -180,7 +181,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { response.headers.set('ETag', etag); return withCacheControl(response, 'private, no-cache'); } catch (error) { - console.error('Error fetching comments:', error); + logError('Error fetching comments:', error); return apiErrors.internalError('Failed to fetch comments'); } } @@ -391,7 +392,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { parentAuthor: parentComment?.author?.name || parentComment?.guestName || 'Someone', timestamp: ts, url: `${baseUrl}/watch/${version.video.id}`, - }).catch((err) => console.error('Notification failed:', err)); + }).catch((err) => logError('Notification failed:', err)); } else { notifyProjectOwner(project.ownerId, { type: 'new_comment', @@ -401,7 +402,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { commentText: content?.trim() || (imageUrl ? '(image attachment)' : '(voice note)'), timestamp: ts, url: `${baseUrl}/watch/${version.video.id}`, - }).catch((err) => console.error('Notification failed:', err)); + }).catch((err) => logError('Notification failed:', err)); } } @@ -428,7 +429,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { } return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error creating comment:', error); + logError('Error creating comment:', error); return apiErrors.internalError('Failed to create comment'); } } diff --git a/app/api/versions/[versionId]/download/route.ts b/app/api/versions/[versionId]/download/route.ts index c677893..f605480 100644 --- a/app/api/versions/[versionId]/download/route.ts +++ b/app/api/versions/[versionId]/download/route.ts @@ -7,6 +7,7 @@ import { getShareSessionFromRequest } from '@/lib/share-session'; import { resolveServerBunnyCdnHostname } from '@/lib/bunny-cdn'; import { NextRequest } from 'next/server'; import { DownloadEgressSource } from '@prisma/client'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ versionId: string }> }; type BunnyDownloadSourcePreference = 'auto' | 'original' | 'compressed'; @@ -483,12 +484,12 @@ export async function GET(request: NextRequest, { params }: RouteParams) { }, }); } catch (egressError) { - console.error('Failed to record download egress event:', egressError); + logError('Failed to record download egress event:', egressError); } return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error downloading version:', error); + logError('Error downloading version:', error); return apiErrors.internalError('Failed to download video'); } } diff --git a/app/api/videos/[videoId]/assets/[assetId]/download/route.ts b/app/api/videos/[videoId]/assets/[assetId]/download/route.ts index 0ed0d2c..c82ba66 100644 --- a/app/api/videos/[videoId]/assets/[assetId]/download/route.ts +++ b/app/api/videos/[videoId]/assets/[assetId]/download/route.ts @@ -10,6 +10,7 @@ import { extractAudioFileNameFromProxyUrl, getVideoAssetAccessContext, } from '@/lib/video-assets'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ videoId: string; assetId: string }> }; type BunnySourcePreference = 'auto' | 'original' | 'compressed'; @@ -205,7 +206,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error downloading asset:', error); + logError('Error downloading asset:', error); return apiErrors.internalError('Failed to download asset'); } } diff --git a/app/api/videos/[videoId]/assets/[assetId]/route.ts b/app/api/videos/[videoId]/assets/[assetId]/route.ts index 277927c..a96cb4e 100644 --- a/app/api/videos/[videoId]/assets/[assetId]/route.ts +++ b/app/api/videos/[videoId]/assets/[assetId]/route.ts @@ -10,6 +10,7 @@ import { canDeleteAssetForViewer, getVideoAssetAccessContext, } from '@/lib/video-assets'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ videoId: string; assetId: string }> }; @@ -94,7 +95,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error deleting video asset:', error); + logError('Error deleting video asset:', error); return apiErrors.internalError('Failed to delete asset'); } } diff --git a/app/api/videos/[videoId]/assets/bunny-init/route.ts b/app/api/videos/[videoId]/assets/bunny-init/route.ts index 272ba6c..df65303 100644 --- a/app/api/videos/[videoId]/assets/bunny-init/route.ts +++ b/app/api/videos/[videoId]/assets/bunny-init/route.ts @@ -13,6 +13,7 @@ import { import { isBunnyUploadsFeatureEnabled } from '@/lib/feature-flags'; import { getShareSessionFromRequest } from '@/lib/share-session'; import { getVideoAssetAccessContext, SAFE_BUNNY_VIDEO_ID } from '@/lib/video-assets'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ videoId: string }> }; @@ -58,7 +59,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { }); if (!bunnyRes.ok) { - console.error('Failed to create Bunny Stream video asset', await bunnyRes.text()); + logError('Failed to create Bunny Stream video asset', await bunnyRes.text()); return apiErrors.internalError('Failed to initialize Bunny upload'); } @@ -103,7 +104,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error initializing Bunny asset upload:', error); + logError('Error initializing Bunny asset upload:', error); return apiErrors.internalError('Failed to initialize asset upload'); } } @@ -157,7 +158,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { const response = successResponse({ message: 'Pending upload cleaned up' }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error cleaning up Bunny asset upload:', error); + logError('Error cleaning up Bunny asset upload:', error); return apiErrors.internalError('Failed to cleanup pending upload'); } } diff --git a/app/api/videos/[videoId]/assets/route.ts b/app/api/videos/[videoId]/assets/route.ts index f463e04..61eabd2 100644 --- a/app/api/videos/[videoId]/assets/route.ts +++ b/app/api/videos/[videoId]/assets/route.ts @@ -24,6 +24,7 @@ import { getVideoAssetAccessContext, sanitizeAssetDisplayName, } from '@/lib/video-assets'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ videoId: string }> }; @@ -260,7 +261,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { response.headers.set('ETag', etag); return withCacheControl(response, 'private, no-cache'); } catch (error) { - console.error('Error fetching video assets:', error); + logError('Error fetching video assets:', error); return apiErrors.internalError('Failed to fetch assets'); } } @@ -450,7 +451,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { } return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error creating video asset:', error); + logError('Error creating video asset:', error); return apiErrors.internalError('Failed to create asset'); } } diff --git a/app/api/watch/[videoId]/progress/route.ts b/app/api/watch/[videoId]/progress/route.ts index bcf44bc..92fb79f 100644 --- a/app/api/watch/[videoId]/progress/route.ts +++ b/app/api/watch/[videoId]/progress/route.ts @@ -3,6 +3,7 @@ import { db } from '@/lib/db'; import { auth, computeProjectAccess, projectAccessInclude } from '@/lib/auth'; import { apiErrors, successResponse } from '@/lib/api-response'; import { rateLimit } from '@/lib/rate-limit'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ videoId: string }> }; @@ -62,7 +63,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { updatedAt: progress?.updatedAt || null, }); } catch (error) { - console.error('Error fetching watch progress:', error); + logError('Error fetching watch progress:', error); return apiErrors.internalError('Failed to fetch watch progress'); } } @@ -153,7 +154,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { percentage: watchProgress.percentage, }); } catch (error) { - console.error('Error saving watch progress:', error); + logError('Error saving watch progress:', error); return apiErrors.internalError('Failed to save watch progress'); } } diff --git a/app/api/watch/[videoId]/route.ts b/app/api/watch/[videoId]/route.ts index 28bf927..b925f60 100644 --- a/app/api/watch/[videoId]/route.ts +++ b/app/api/watch/[videoId]/route.ts @@ -6,6 +6,7 @@ import { rateLimit } from '@/lib/rate-limit'; import { validateShareLinkAccess } from '@/lib/share-links'; import { getShareSessionFromRequest } from '@/lib/share-session'; import { getGuestIdentityFromRequest } from '@/lib/guest-identity'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ videoId: string }> }; @@ -207,7 +208,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { return withCacheControl(response, 'private, no-cache'); } catch (error) { - console.error('Error fetching video:', error); + logError('Error fetching video:', error); return apiErrors.internalError('Failed to fetch video'); } } diff --git a/app/api/watch/[videoId]/upload-token/route.ts b/app/api/watch/[videoId]/upload-token/route.ts index 49f4b62..b52c998 100644 --- a/app/api/watch/[videoId]/upload-token/route.ts +++ b/app/api/watch/[videoId]/upload-token/route.ts @@ -12,6 +12,7 @@ import { guestUploadTokenTtlSeconds, type GuestUploadIntent, } from '@/lib/guest-upload-token'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ videoId: string }> }; @@ -96,7 +97,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error issuing guest upload token:', error); + logError('Error issuing guest upload token:', error); return apiErrors.internalError('Failed to issue upload token'); } } diff --git a/app/api/workspaces/[workspaceId]/members/[memberId]/route.ts b/app/api/workspaces/[workspaceId]/members/[memberId]/route.ts index 203e44d..1c98eff 100644 --- a/app/api/workspaces/[workspaceId]/members/[memberId]/route.ts +++ b/app/api/workspaces/[workspaceId]/members/[memberId]/route.ts @@ -4,6 +4,7 @@ import { auth, checkWorkspaceAccess } from '@/lib/auth'; import { WorkspaceMemberRole } from '@prisma/client'; import { rateLimit } from '@/lib/rate-limit'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ workspaceId: string; memberId: string }> }; @@ -69,7 +70,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { const response = successResponse(updatedMember); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error updating member role:', error); + logError('Error updating member role:', error); return apiErrors.internalError('Failed to update member role'); } } @@ -145,7 +146,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { const response = successResponse({ message: 'Member removed' }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error removing member:', error); + logError('Error removing member:', error); return apiErrors.internalError('Failed to remove member'); } } diff --git a/app/api/workspaces/[workspaceId]/members/invitations/[invitationId]/route.ts b/app/api/workspaces/[workspaceId]/members/invitations/[invitationId]/route.ts index 3546d24..23beb0c 100644 --- a/app/api/workspaces/[workspaceId]/members/invitations/[invitationId]/route.ts +++ b/app/api/workspaces/[workspaceId]/members/invitations/[invitationId]/route.ts @@ -4,6 +4,7 @@ import { auth, checkWorkspaceAccess } from '@/lib/auth'; import { db } from '@/lib/db'; import { rateLimit } from '@/lib/rate-limit'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ workspaceId: string; invitationId: string }> }; @@ -68,7 +69,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { const response = successResponse({ message: 'Invitation canceled' }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error canceling workspace invitation:', error); + logError('Error canceling workspace invitation:', error); return apiErrors.internalError('Failed to cancel invitation'); } } diff --git a/app/api/workspaces/[workspaceId]/members/route.ts b/app/api/workspaces/[workspaceId]/members/route.ts index 10ccf61..c3adc0e 100644 --- a/app/api/workspaces/[workspaceId]/members/route.ts +++ b/app/api/workspaces/[workspaceId]/members/route.ts @@ -5,6 +5,7 @@ import { InvitationRole, WorkspaceMemberRole } from '@prisma/client'; import { rateLimit } from '@/lib/rate-limit'; import { buildInvitationUrl, createOrRefreshInvitation, sendInvitationEmail } from '@/lib/invitations'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ workspaceId: string }> }; @@ -121,7 +122,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { ); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error fetching workspace members:', error); + logError('Error fetching workspace members:', error); return apiErrors.internalError('Failed to fetch members'); } } @@ -218,7 +219,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { const response = successResponse({ message: 'Invitation email sent.' }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error inviting workspace member:', error); + logError('Error inviting workspace member:', error); return apiErrors.internalError('Failed to invite member'); } } diff --git a/app/api/workspaces/[workspaceId]/route.ts b/app/api/workspaces/[workspaceId]/route.ts index 7df00fa..04311bb 100644 --- a/app/api/workspaces/[workspaceId]/route.ts +++ b/app/api/workspaces/[workspaceId]/route.ts @@ -6,6 +6,7 @@ import { collectWorkspaceMediaUrls, deleteMediaFilesBestEffort } from '@/lib/r2- import { cleanupBunnyStreamVideosBestEffort } from '@/lib/bunny-stream-cleanup'; import { buildCleanupWarnings, logCleanupWarnings } from '@/lib/cleanup-warnings'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; type RouteParams = { params: Promise<{ workspaceId: string }> }; @@ -73,7 +74,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const response = successResponse(workspace); return withCacheControl(response, 'private, max-age=30, stale-while-revalidate=60'); } catch (error) { - console.error('Error fetching workspace:', error); + logError('Error fetching workspace:', error); return apiErrors.internalError('Failed to fetch workspace'); } } @@ -123,7 +124,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { const response = successResponse(workspace); return withCacheControl(response, 'private, max-age=30, stale-while-revalidate=60'); } catch (error) { - console.error('Error updating workspace:', error); + logError('Error updating workspace:', error); return apiErrors.internalError('Failed to update workspace'); } } @@ -215,7 +216,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { }); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error deleting workspace:', error); + logError('Error deleting workspace:', error); return apiErrors.internalError('Failed to delete workspace'); } } diff --git a/app/api/workspaces/route.ts b/app/api/workspaces/route.ts index f6b5d6a..6783783 100644 --- a/app/api/workspaces/route.ts +++ b/app/api/workspaces/route.ts @@ -4,6 +4,7 @@ import { auth } from '@/lib/auth'; import { rateLimit } from '@/lib/rate-limit'; import { buildBillingAccessWhereInput, getWorkspaceCreationEligibility } from '@/lib/billing'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; +import { logError } from '@/lib/logger'; // GET /api/workspaces - List all workspaces for the authenticated user export async function GET(request: NextRequest) { @@ -72,7 +73,7 @@ export async function GET(request: NextRequest) { ); return withCacheControl(response, 'private, max-age=60, stale-while-revalidate=120'); } catch (error) { - console.error('Error fetching workspaces:', error); + logError('Error fetching workspaces:', error); return apiErrors.internalError('Failed to fetch workspaces'); } } @@ -142,7 +143,7 @@ export async function POST(request: NextRequest) { const response = successResponse(workspace, 201); return withCacheControl(response, 'private, no-store'); } catch (error) { - console.error('Error creating workspace:', error); + logError('Error creating workspace:', error); return apiErrors.internalError('Failed to create workspace'); } } diff --git a/lib/admin-stats.ts b/lib/admin-stats.ts index 5eb5ca7..a3f6b2f 100644 --- a/lib/admin-stats.ts +++ b/lib/admin-stats.ts @@ -3,6 +3,7 @@ import { db } from '@/lib/db'; import { r2Client, R2_BUCKET_NAME } from '@/lib/r2'; import { ListObjectsV2Command, type ListObjectsV2CommandInput } from '@aws-sdk/client-s3'; import { isBunnyUploadsFeatureEnabled } from '@/lib/feature-flags'; +import { logError } from '@/lib/logger'; const BUNNY_API_BASE = 'https://video.bunnycdn.com'; const STORAGE_CACHE_SECONDS = 600; @@ -174,7 +175,7 @@ export async function getCachedTotalStorage(): Promise { const snapshot = await getR2StorageSnapshot(); return snapshot.totalBytes; } catch (err) { - console.error('Failed to fetch total storage stats:', err); + logError('Failed to fetch total storage stats:', err); return -1; } } @@ -184,7 +185,7 @@ export const getCachedBunnyStorageStats = unstable_cache( try { return await fetchBunnyStorageStats(); } catch (err) { - console.error('Failed to fetch Bunny storage stats:', err); + logError('Failed to fetch Bunny storage stats:', err); return { totalBytes: -1, byVideoId: {} } as BunnyStorageStats; } }, @@ -247,7 +248,7 @@ export const getCachedUserBunnyStorage = unstable_cache( perUserStorage[billedUserId] = (perUserStorage[billedUserId] || 0) + size; } } catch (err) { - console.error('Failed to calculate per-user Bunny storage:', err); + logError('Failed to calculate per-user Bunny storage:', err); } return perUserStorage; }, @@ -376,7 +377,7 @@ export async function getCachedUserMediaStorage(): Promise; + + // Prisma client errors: their `.message` can embed raw SQL, WHERE-clause + // values, and schema internals. Only safe to expose the Prisma error code. + if (name.startsWith('PrismaClient')) { + const code = typeof anyErr.code === 'string' ? anyErr.code : 'UNKNOWN'; + return { + type: 'PrismaError', + code, + message: `Database error [${code}]`, + } satisfies SanitizedError; + } + + // Stripe SDK errors carry a `type` string and numeric `statusCode`; their + // `.message` values are designed to be user-safe. + if (typeof anyErr.type === 'string' && typeof anyErr.statusCode === 'number') { + return { + type: anyErr.type, + code: String(anyErr.statusCode), + message: err.message, + } satisfies SanitizedError; + } + + // All other Error instances: include message and type, never the stack. + return { type: name, message: err.message } satisfies SanitizedError; +} + +/** + * Log an error with sanitized details. + * + * Use this everywhere in server-side code instead of `console.error(msg, error)`. + */ +export function logError(context: string, err: unknown): void { + console.error(context, sanitizeError(err)); +} diff --git a/lib/notifications.ts b/lib/notifications.ts index 4999838..1e1d262 100644 --- a/lib/notifications.ts +++ b/lib/notifications.ts @@ -9,6 +9,7 @@ import { emailRow, escapeHtml, } from '@/lib/email-brand'; +import { logError } from '@/lib/logger'; // ============================================ // NOTIFICATION CHANNELS @@ -50,7 +51,7 @@ async function sendTelegram( } return true; } catch (err) { - console.error('Telegram send failed:', err); + logError('Telegram send failed:', err); return false; } } @@ -93,7 +94,7 @@ async function sendEmail(to: string, subject: string, html: string): Promise { try { await db.$executeRaw`SELECT cleanup_rate_limits()`; } catch (error) { - console.error('Rate limit cleanup failed:', error); + logError('Rate limit cleanup failed:', error); } } // Start cleanup interval once per process to avoid duplicate scheduling on module reload. if (!globalForRateLimitCleanup.rateLimitCleanupIntervalStarted && typeof setInterval !== 'undefined') { const interval = setInterval(() => { - cleanupRateLimits().catch(console.error); + cleanupRateLimits().catch((err) => logError('Unexpected error:', err)); }, RATE_LIMIT_CLEANUP_INTERVAL_MS); // Avoid keeping Node.js process alive because of housekeeping timers. diff --git a/lib/video-providers/index.ts b/lib/video-providers/index.ts index 5731005..595c2cf 100644 --- a/lib/video-providers/index.ts +++ b/lib/video-providers/index.ts @@ -4,6 +4,7 @@ import { youtubeProvider } from './youtube'; import { directProvider } from './direct'; import { bunnyProvider } from './bunny'; import type { VideoProvider, VideoSource, VideoMetadata, VideoProviderType } from './types'; +import { logError } from '@/lib/logger'; // Export types export * from './types'; @@ -82,7 +83,7 @@ export async function fetchVideoMetadata(source: VideoSource): Promise { - console.error('[bunny-orphan-cleanup] Fatal error:', error); + logError('[bunny-orphan-cleanup] Fatal error:', error); process.exitCode = 1; }) .finally(async () => { diff --git a/scripts/docker-db-bootstrap.ts b/scripts/docker-db-bootstrap.ts index 0052fe9..c621d45 100644 --- a/scripts/docker-db-bootstrap.ts +++ b/scripts/docker-db-bootstrap.ts @@ -3,6 +3,7 @@ import { Client } from 'pg'; import { readdirSync } from 'node:fs'; import { join } from 'node:path'; import { spawn } from 'node:child_process'; +import { logError } from '@/lib/logger'; type MigrationRow = { migration_name: string; @@ -147,6 +148,6 @@ async function main() { } main().catch((error) => { - console.error('Docker database bootstrap failed:', error); + logError('Docker database bootstrap failed:', error); process.exit(1); }); diff --git a/scripts/r2-orphan-cleanup.ts b/scripts/r2-orphan-cleanup.ts index 6dfa95e..723e4d7 100644 --- a/scripts/r2-orphan-cleanup.ts +++ b/scripts/r2-orphan-cleanup.ts @@ -2,6 +2,7 @@ import { DeleteObjectCommand, ListObjectsV2Command, type ListObjectsV2CommandInp import { db, disconnectDb } from '../lib/db'; import { r2Client, R2_BUCKET_NAME } from '../lib/r2'; import { cleanupExpiredBillingWorkspaces } from './expired-billing-cleanup'; +import { logError } from '@/lib/logger'; const UNATTACHED_UPLOAD_TTL_MS = 15 * 60 * 1000; const CHUNK_SIZE = 500; @@ -166,7 +167,7 @@ async function main() { deleted += 1; } catch (error) { failed += 1; - console.error(`[r2-orphan-cleanup] Failed deleting ${candidate.key}:`, error); + logError(`[r2-orphan-cleanup] Failed deleting ${candidate.key}:`, error); } } @@ -179,7 +180,7 @@ async function main() { main() .catch((error) => { - console.error('[r2-orphan-cleanup] Fatal error:', error); + logError('[r2-orphan-cleanup] Fatal error:', error); process.exitCode = 1; }) .finally(async () => { diff --git a/scripts/self-host-bootstrap.ts b/scripts/self-host-bootstrap.ts index 26f403f..30f228f 100644 --- a/scripts/self-host-bootstrap.ts +++ b/scripts/self-host-bootstrap.ts @@ -1,5 +1,6 @@ import 'dotenv/config'; import { ensureR2BucketExists, R2_BUCKET_NAME } from '@/lib/r2'; +import { logError } from '@/lib/logger'; const shouldCreateBucket = /^(1|true|yes|on)$/i.test(process.env.SELF_HOSTED_AUTO_CREATE_BUCKET ?? ''); @@ -15,6 +16,6 @@ async function main() { } main().catch((error) => { - console.error('Self-host bootstrap failed:', error); + logError('Self-host bootstrap failed:', error); process.exit(1); });