From 74b98bc267fd2709f7132c59d2a51fe4292bcb4c Mon Sep 17 00:00:00 2001 From: Claudio Fuentes Date: Fri, 25 Jul 2025 15:38:00 -0400 Subject: [PATCH 1/8] fix: update S3 bucket name environment variable - Changed the S3 bucket name environment variable from AWS_BUCKET_NAME to APP_AWS_BUCKET_NAME for consistency with the new naming conventions. - Ensured that all required AWS environment variables are validated properly. --- apps/app/src/app/s3.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/app/src/app/s3.ts b/apps/app/src/app/s3.ts index 7b942ba5d..0c519803a 100644 --- a/apps/app/src/app/s3.ts +++ b/apps/app/src/app/s3.ts @@ -4,7 +4,7 @@ const APP_AWS_REGION = process.env.APP_AWS_REGION; const APP_AWS_ACCESS_KEY_ID = process.env.APP_AWS_ACCESS_KEY_ID; const APP_AWS_SECRET_ACCESS_KEY = process.env.APP_AWS_SECRET_ACCESS_KEY; -export const BUCKET_NAME = process.env.AWS_BUCKET_NAME; +export const BUCKET_NAME = process.env.APP_AWS_BUCKET_NAME; if (!APP_AWS_ACCESS_KEY_ID || !APP_AWS_SECRET_ACCESS_KEY || !BUCKET_NAME || !APP_AWS_REGION) { // Log the error in production environments From 03508136534baa97427650a74ddf36d1dce9f07a Mon Sep 17 00:00:00 2001 From: Claudio Fuentes Date: Fri, 25 Jul 2025 15:40:20 -0400 Subject: [PATCH 2/8] chore: clean up Next.js configuration in app and portal - Removed unnecessary comment from the output configuration in next.config.ts files for both app and portal. - Ensured consistency in configuration across both applications. --- apps/app/next.config.ts | 2 +- apps/portal/next.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/app/next.config.ts b/apps/app/next.config.ts index 0a6e9d691..184203a4f 100644 --- a/apps/app/next.config.ts +++ b/apps/app/next.config.ts @@ -49,7 +49,7 @@ const config: NextConfig = { }; if (process.env.VERCEL !== '1') { - config.output = 'standalone'; // Required for Docker builds + config.output = 'standalone'; } export default config; diff --git a/apps/portal/next.config.ts b/apps/portal/next.config.ts index 6f6638895..db30680e1 100644 --- a/apps/portal/next.config.ts +++ b/apps/portal/next.config.ts @@ -30,7 +30,7 @@ const config: NextConfig = { }; if (process.env.VERCEL !== '1') { - config.output = 'standalone'; // Required for Docker builds + config.output = 'standalone'; } export default config; From d8dca528e378a8e06947fb2eaa6be01f24d3e191 Mon Sep 17 00:00:00 2001 From: Claudio Fuentes Date: Thu, 31 Jul 2025 10:58:39 -0400 Subject: [PATCH 3/8] chore: update .gitignore and remove unused environment variable - Updated .gitignore to include playground directory and remove redundant entries. - Removed NEXT_PUBLIC_VERCEL_URL from environment variables in env.mjs as it is no longer needed. --- .gitignore | 7 +++---- apps/app/src/env.mjs | 2 -- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index f9ddfb236..e11ebeace 100644 --- a/.gitignore +++ b/.gitignore @@ -56,8 +56,9 @@ tailwind.css # Trigger .trigger -# Playground (for local testing) -/playground +# Playground +playground/ +.playground/ .venv # Ignore lock files to prevent conflicts between different package managers @@ -76,8 +77,6 @@ playwright-report/ playwright/.cache/ debug-setup-page.png -.playground/ - packages/*/dist # Generated Prisma Client diff --git a/apps/app/src/env.mjs b/apps/app/src/env.mjs index 248dba987..a127af704 100644 --- a/apps/app/src/env.mjs +++ b/apps/app/src/env.mjs @@ -46,7 +46,6 @@ export const env = createEnv({ client: { NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(), - NEXT_PUBLIC_VERCEL_URL: z.string().optional(), NEXT_PUBLIC_IS_DUB_ENABLED: z.string().optional(), NEXT_PUBLIC_GTM_ID: z.string().optional(), NEXT_PUBLIC_LINKEDIN_PARTNER_ID: z.string().optional(), @@ -76,7 +75,6 @@ export const env = createEnv({ VERCEL_TEAM_ID: process.env.VERCEL_TEAM_ID, VERCEL_PROJECT_ID: process.env.VERCEL_PROJECT_ID, TRUST_PORTAL_PROJECT_ID: process.env.TRUST_PORTAL_PROJECT_ID, - NEXT_PUBLIC_VERCEL_URL: process.env.NEXT_PUBLIC_VERCEL_URL, NODE_ENV: process.env.NODE_ENV, APP_AWS_ACCESS_KEY_ID: process.env.APP_AWS_ACCESS_KEY_ID, APP_AWS_SECRET_ACCESS_KEY: process.env.APP_AWS_SECRET_ACCESS_KEY, From 50817b52470160473f0d075c703df981070936c3 Mon Sep 17 00:00:00 2001 From: Claudio Fuentes Date: Thu, 31 Jul 2025 11:42:59 -0400 Subject: [PATCH 4/8] refactor: enhance logging and update file upload handling - Replaced simple logger calls with structured logging methods (info, warn, error) for better clarity and debugging. - Refactored the uploadFile action to utilize the new logger and improved error handling. - Updated file upload logic in TaskBody and CommentItem components to use the new action client pattern, enhancing maintainability and readability. - Increased maximum file size limit from 5MB to 10MB for uploads. --- apps/app/src/actions/files/upload-file.ts | 143 +++++------ apps/app/src/actions/safe-action.ts | 16 +- .../tasks/[taskId]/components/TaskBody.tsx | 120 ++++----- .../app/(app)/[orgId]/tasks/[taskId]/page.tsx | 40 ++- .../src/components/comments/CommentItem.tsx | 228 ++++++------------ apps/app/src/utils/logger.ts | 12 +- 6 files changed, 218 insertions(+), 341 deletions(-) diff --git a/apps/app/src/actions/files/upload-file.ts b/apps/app/src/actions/files/upload-file.ts index 6b5e3d377..0aea6a67a 100644 --- a/apps/app/src/actions/files/upload-file.ts +++ b/apps/app/src/actions/files/upload-file.ts @@ -1,13 +1,13 @@ 'use server'; import { BUCKET_NAME, s3Client } from '@/app/s3'; -import { auth } from '@/utils/auth'; +import { logger } from '@/utils/logger'; import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { AttachmentEntityType, AttachmentType, db } from '@db'; import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; import { z } from 'zod'; +import { authActionClient } from '../safe-action'; function mapFileTypeToAttachmentType(fileType: string): AttachmentType { const type = fileType.split('/')[0]; @@ -34,94 +34,79 @@ const uploadAttachmentSchema = z.object({ pathToRevalidate: z.string().optional(), }); -export const uploadFile = async (input: z.infer) => { - const { fileName, fileType, fileData, entityId, entityType, pathToRevalidate } = input; - const session = await auth.api.getSession({ headers: await headers() }); - const organizationId = session?.session.activeOrganizationId; +export const uploadFile = authActionClient + .inputSchema(uploadAttachmentSchema) + .metadata({ + name: 'uploadFile', + track: { + event: 'File Uploaded', + channel: 'server', + }, + }) + .action(async ({ parsedInput, ctx }) => { + const { fileName, fileType, fileData, entityId, entityType, pathToRevalidate } = parsedInput; + const { session } = ctx; + const organizationId = session.activeOrganizationId; + + logger.info(`[uploadFile] Starting upload for ${fileName} in org ${organizationId}`); - if (!organizationId) { - return { - success: false, - error: 'Not authorized - no organization found', - data: null, - }; - } - - try { const fileBuffer = Buffer.from(fileData, 'base64'); - const MAX_FILE_SIZE_MB = 5; + const MAX_FILE_SIZE_MB = 10; const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024; if (fileBuffer.length > MAX_FILE_SIZE_BYTES) { - return { - success: false, - error: `File exceeds the ${MAX_FILE_SIZE_MB}MB limit.`, - data: null, - }; + throw new Error(`File exceeds the ${MAX_FILE_SIZE_MB}MB limit.`); } const timestamp = Date.now(); const sanitizedFileName = fileName.replace(/[^a-zA-Z0-9.-]/g, '_'); const key = `${organizationId}/attachments/${entityType}/${entityId}/${timestamp}-${sanitizedFileName}`; - const putCommand = new PutObjectCommand({ - Bucket: BUCKET_NAME, - Key: key, - Body: fileBuffer, - ContentType: fileType, - }); - - await s3Client.send(putCommand); - - console.log('Creating attachment...'); - console.log({ - name: fileName, - url: key, - type: mapFileTypeToAttachmentType(fileType), - entityId: entityId, - entityType: entityType, - organizationId: organizationId, - }); - - const attachment = await db.attachment.create({ - data: { - name: fileName, - url: key, - type: mapFileTypeToAttachmentType(fileType), - entityId: entityId, - entityType: entityType, - organizationId: organizationId, - }, - }); - - const getCommand = new GetObjectCommand({ - Bucket: BUCKET_NAME, - Key: key, - }); + try { + logger.info(`[uploadFile] Uploading to S3 with key: ${key}`); + const putCommand = new PutObjectCommand({ + Bucket: BUCKET_NAME, + Key: key, + Body: fileBuffer, + ContentType: fileType, + }); + await s3Client.send(putCommand); + logger.info(`[uploadFile] S3 upload successful for key: ${key}`); + + logger.info(`[uploadFile] Creating attachment record in DB for key: ${key}`); + const attachment = await db.attachment.create({ + data: { + name: fileName, + url: key, + type: mapFileTypeToAttachmentType(fileType), + entityId: entityId, + entityType: entityType, + organizationId: organizationId, + }, + }); + logger.info(`[uploadFile] DB record created with id: ${attachment.id}`); + + logger.info(`[uploadFile] Generating signed URL for key: ${key}`); + const getCommand = new GetObjectCommand({ + Bucket: BUCKET_NAME, + Key: key, + }); + const signedUrl = await getSignedUrl(s3Client, getCommand, { + expiresIn: 900, + }); + logger.info(`[uploadFile] Signed URL generated for key: ${key}`); + + if (pathToRevalidate) { + revalidatePath(pathToRevalidate); + } - const signedUrl = await getSignedUrl(s3Client, getCommand, { - expiresIn: 900, - }); - - if (pathToRevalidate) { - revalidatePath(pathToRevalidate); - } - - return { - success: true, - data: { + return { ...attachment, signedUrl, - }, - error: null, - } as const; - } catch (error) { - console.error('Upload file action error:', error); - - return { - success: false, - error: 'Failed to process file upload.', - data: null, - } as const; - } -}; + }; + } catch (error) { + logger.error(`[uploadFile] Error during upload process for key ${key}:`, error); + // Re-throw the error to be handled by the safe action client + throw error; + } + }); diff --git a/apps/app/src/actions/safe-action.ts b/apps/app/src/actions/safe-action.ts index 44e935b67..71230f4ac 100644 --- a/apps/app/src/actions/safe-action.ts +++ b/apps/app/src/actions/safe-action.ts @@ -22,7 +22,7 @@ if (env.UPSTASH_REDIS_REST_URL && env.UPSTASH_REDIS_REST_TOKEN) { export const actionClientWithMeta = createSafeActionClient({ handleServerError(e) { // Log the error for debugging - logger('Server error:', e); + logger.error('Server error:', e); // Throw the error instead of returning it if (e instanceof Error) { @@ -69,12 +69,12 @@ export const authActionClient = actionClientWithMeta }); if (process.env.NODE_ENV === 'development') { - logger('Input ->', JSON.stringify(clientInput, null, 2)); - logger('Result ->', JSON.stringify(result.data, null, 2)); + logger.info('Input ->', JSON.stringify(clientInput, null, 2)); + logger.info('Result ->', JSON.stringify(result.data, null, 2)); // Also log validation errors if they exist if (result.validationErrors) { - logger('Validation Errors ->', JSON.stringify(result.validationErrors, null, 2)); + logger.warn('Validation Errors ->', JSON.stringify(result.validationErrors, null, 2)); } return result; @@ -212,7 +212,7 @@ export const authActionClient = actionClientWithMeta }, }); } catch (error) { - logger('Audit log error:', error); + logger.error('Audit log error:', error); } // Add revalidation logic based on the cursor rules @@ -274,12 +274,12 @@ export const authActionClientWithoutOrg = actionClientWithMeta }); if (process.env.NODE_ENV === 'development') { - logger('Input ->', JSON.stringify(clientInput, null, 2)); - logger('Result ->', JSON.stringify(result.data, null, 2)); + logger.info('Input ->', JSON.stringify(clientInput, null, 2)); + logger.info('Result ->', JSON.stringify(result.data, null, 2)); // Also log validation errors if they exist if (result.validationErrors) { - logger('Validation Errors ->', JSON.stringify(result.validationErrors, null, 2)); + logger.warn('Validation Errors ->', JSON.stringify(result.validationErrors, null, 2)); } return result; diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/TaskBody.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/TaskBody.tsx index 226df8ea6..8ba31c860 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/TaskBody.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/TaskBody.tsx @@ -7,6 +7,7 @@ import { Textarea } from '@comp/ui/textarea'; import type { Attachment } from '@db'; import { AttachmentEntityType } from '@db'; import { Loader2, Paperclip, Plus } from 'lucide-react'; +import { useAction } from 'next-safe-action/hooks'; import { useRouter } from 'next/navigation'; import type React from 'react'; import { useCallback, useRef, useState } from 'react'; @@ -37,76 +38,59 @@ export function TaskBody({ onAttachmentsChange, }: TaskBodyProps) { const fileInputRef = useRef(null); - const [isUploading, setIsUploading] = useState(false); const [busyAttachmentId, setBusyAttachmentId] = useState(null); const router = useRouter(); - - const resetState = () => { - setIsUploading(false); - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - }; + const { execute, isExecuting } = useAction(uploadFile, { + onSuccess: () => { + onAttachmentsChange?.(); + router.refresh(); + toast.success('File uploaded successfully'); + }, + onError: ({ error }) => { + toast.error(error.serverError || 'Failed to upload file.'); + }, + onSettled: () => { + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }, + }); const handleFileSelect = useCallback( async (event: React.ChangeEvent) => { const files = event.target.files; if (!files || files.length === 0) return; - setIsUploading(true); - try { - const uploadedAttachments = []; - for (const file of Array.from(files)) { - const MAX_FILE_SIZE_MB = 5; - const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024; - if (file.size > MAX_FILE_SIZE_BYTES) { - toast.error(`File "${file.name}" exceeds the ${MAX_FILE_SIZE_MB}MB limit.`); - continue; - } - const reader = new FileReader(); - reader.onloadend = async () => { - const base64Data = (reader.result as string)?.split(',')[1]; - if (!base64Data) { - toast.error('Failed to read file data.'); - resetState(); - return; - } - const { success, data, error } = await uploadFile({ - fileName: file.name, - fileType: file.type, - fileData: base64Data, - entityId: taskId, - entityType: AttachmentEntityType.task, - }); + for (const file of Array.from(files)) { + const MAX_FILE_SIZE_MB = 10; + const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024; + if (file.size > MAX_FILE_SIZE_BYTES) { + toast.error(`File "${file.name}" exceeds the ${MAX_FILE_SIZE_MB}MB limit.`); + continue; + } - if (success && data) { - uploadedAttachments.push(data); - } else { - const errorMessage = error || 'Failed to process attachment after upload.'; - toast.error(String(errorMessage)); - } - }; - reader.onerror = () => { - toast.error('Error reading file.'); - resetState(); - }; - reader.readAsDataURL(file); - await new Promise((resolve) => { - const intervalId = setInterval(() => { - if (reader.readyState === FileReader.DONE) { - clearInterval(intervalId); - resolve(undefined); - } - }, 100); + const reader = new FileReader(); + reader.onloadend = async () => { + const base64Data = (reader.result as string)?.split(',')[1]; + if (!base64Data) { + toast.error('Failed to read file data.'); + return; + } + execute({ + fileName: file.name, + fileType: file.type, + fileData: base64Data, + entityId: taskId, + entityType: AttachmentEntityType.task, }); - } - onAttachmentsChange?.(); - router.refresh(); - } finally { - setIsUploading(false); + }; + reader.onerror = () => { + toast.error('Error reading file.'); + }; + reader.readAsDataURL(file); } }, - [taskId, onAttachmentsChange, router], + [execute, taskId, onAttachmentsChange, router], ); const triggerFileInput = () => { @@ -144,8 +128,6 @@ export function TaskBody({ [onAttachmentsChange, router], ); - const isUploadingFile = isUploading; - return (