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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -76,8 +77,6 @@ playwright-report/
playwright/.cache/
debug-setup-page.png

.playground/

packages/*/dist

# Generated Prisma Client
Expand Down
2 changes: 1 addition & 1 deletion apps/app/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const config: NextConfig = {
};

if (process.env.VERCEL !== '1') {
config.output = 'standalone'; // Required for Docker builds
config.output = 'standalone';
}

export default config;
143 changes: 64 additions & 79 deletions apps/app/src/actions/files/upload-file.ts
Original file line number Diff line number Diff line change
@@ -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];
Expand All @@ -34,94 +34,79 @@ const uploadAttachmentSchema = z.object({
pathToRevalidate: z.string().optional(),
});

export const uploadFile = async (input: z.infer<typeof uploadAttachmentSchema>) => {
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;
}
});
38 changes: 17 additions & 21 deletions apps/app/src/actions/safe-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -68,16 +68,13 @@ 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));
const { fileData: _, ...inputForLog } = clientInput as any;
logger.info('Input ->', JSON.stringify(inputForLog, 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));
}

return result;
// Also log validation errors if they exist
if (result.validationErrors) {
logger.warn('Validation Errors ->', JSON.stringify(result.validationErrors, null, 2));
}

return result;
Expand Down Expand Up @@ -154,13 +151,15 @@ export const authActionClient = actionClientWithMeta
throw new Error('Member not found');
}

const { fileData: _, ...inputForAuditLog } = clientInput as any;

const data = {
userId: session.user.id,
email: session.user.email,
name: session.user.name,
organizationId: session.session.activeOrganizationId,
action: metadata.name,
input: clientInput,
input: inputForAuditLog,
ipAddress: headersList.get('x-forwarded-for') || null,
userAgent: headersList.get('user-agent') || null,
};
Expand Down Expand Up @@ -212,7 +211,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
Expand Down Expand Up @@ -273,16 +272,13 @@ 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));

// Also log validation errors if they exist
if (result.validationErrors) {
logger('Validation Errors ->', JSON.stringify(result.validationErrors, null, 2));
}
const { fileData: _, ...inputForLog } = clientInput as any;
logger.info('Input ->', JSON.stringify(inputForLog, null, 2));
logger.info('Result ->', JSON.stringify(result.data, null, 2));

return result;
// Also log validation errors if they exist
if (result.validationErrors) {
logger.warn('Validation Errors ->', JSON.stringify(result.validationErrors, null, 2));
}

return result;
Expand Down
Loading
Loading