Skip to content

Commit 1ba8866

Browse files
feat(security-questionnaire): add support for questionnaire file uploads to S3 (#1758)
Co-authored-by: Tofik Hasanov <annexcies@gmail.com>
1 parent 846a43c commit 1ba8866

File tree

7 files changed

+64
-5
lines changed

7 files changed

+64
-5
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ APP_AWS_ACCESS_KEY_ID="" # AWS Access Key ID
1717
APP_AWS_SECRET_ACCESS_KEY="" # AWS Secret Access Key
1818
APP_AWS_REGION="" # AWS Region
1919
APP_AWS_BUCKET_NAME="" # AWS Bucket Name
20+
APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET="" # AWS, Required for Security Questionnaire feature
2021

2122
TRIGGER_SECRET_KEY="" # For background jobs. Self-host or use cloud-version @ https://trigger.dev
2223
# TRIGGER_API_URL="" # Only set if you are self-hosting

SELF_HOSTING.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ Portal (`apps/portal`):
4343

4444
App (`apps/app`):
4545

46+
- **APP_AWS_REGION**, **APP_AWS_ACCESS_KEY_ID**, **APP_AWS_SECRET_ACCESS_KEY**, **APP_AWS_BUCKET_NAME**: AWS S3 credentials for file storage (attachments, general uploads).
47+
- **APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET**: AWS S3 bucket name specifically for questionnaire file uploads. Required for the Security Questionnaire feature. If not set, users will see an error when trying to parse questionnaires.
4648
- **OPENAI_API_KEY**: Enables AI features that call OpenAI models.
4749
- **UPSTASH_REDIS_REST_URL**, **UPSTASH_REDIS_REST_TOKEN**: Optional Redis (Upstash) used for rate limiting/queues/caching.
4850
- **NEXT_PUBLIC_POSTHOG_KEY**, **NEXT_PUBLIC_POSTHOG_HOST**: Client analytics via PostHog; leave unset to disable.
@@ -143,6 +145,12 @@ BETTER_AUTH_URL_PORTAL=http://localhost:3002
143145
NEXT_PUBLIC_BETTER_AUTH_URL_PORTAL=http://localhost:3002
144146
145147
# Optional
148+
# AWS S3 (for file storage)
149+
# APP_AWS_REGION=
150+
# APP_AWS_ACCESS_KEY_ID=
151+
# APP_AWS_SECRET_ACCESS_KEY=
152+
# APP_AWS_BUCKET_NAME=
153+
# APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET=
146154
# OPENAI_API_KEY=
147155
# UPSTASH_REDIS_REST_URL=
148156
# UPSTASH_REDIS_REST_TOKEN=

apps/app/src/app/(app)/[orgId]/security-questionnaire/actions/parse-questionnaire-ai.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { authActionClient } from '@/actions/safe-action';
44
import { parseQuestionnaireTask } from '@/jobs/tasks/vendors/parse-questionnaire';
55
import { tasks } from '@trigger.dev/sdk';
66
import { z } from 'zod';
7+
import { APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET } from '@/app/s3';
78

89
const inputSchema = z.object({
910
inputType: z.enum(['file', 'url', 'attachment', 's3']),
@@ -35,6 +36,11 @@ export const parseQuestionnaireAI = authActionClient
3536
if (!session?.activeOrganizationId) {
3637
throw new Error('No active organization');
3738
}
39+
40+
// Validate questionnaire upload bucket is configured
41+
if (!APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET) {
42+
throw new Error('Questionnaire upload service is not configured. Please set APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET environment variable to use this feature.');
43+
}
3844

3945
const organizationId = session.activeOrganizationId;
4046

apps/app/src/app/(app)/[orgId]/security-questionnaire/actions/upload-questionnaire-file.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use server';
22

3-
import { BUCKET_NAME, s3Client } from '@/app/s3';
3+
import { APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET, s3Client } from '@/app/s3';
44
import { authActionClient } from '@/actions/safe-action';
55
import { PutObjectCommand } from '@aws-sdk/client-s3';
66
import { db } from '@db';
@@ -48,10 +48,14 @@ export const uploadQuestionnaireFile = authActionClient
4848
throw new Error('Unauthorized');
4949
}
5050

51-
if (!s3Client || !BUCKET_NAME) {
51+
if (!s3Client) {
5252
throw new Error('S3 client not configured');
5353
}
5454

55+
if (!APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET) {
56+
throw new Error('Questionnaire upload bucket is not configured. Please set APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET environment variable.');
57+
}
58+
5559
try {
5660
// Convert base64 to buffer
5761
const fileBuffer = Buffer.from(fileData, 'base64');
@@ -70,7 +74,7 @@ export const uploadQuestionnaireFile = authActionClient
7074

7175
// Upload to S3
7276
const putCommand = new PutObjectCommand({
73-
Bucket: BUCKET_NAME,
77+
Bucket: APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET,
7478
Key: s3Key,
7579
Body: fileBuffer,
7680
ContentType: fileType,
@@ -93,6 +97,35 @@ export const uploadQuestionnaireFile = authActionClient
9397
},
9498
};
9599
} catch (error) {
100+
// Provide more helpful error messages for common S3 errors
101+
if (error && typeof error === 'object' && 'Code' in error) {
102+
const awsError = error as { Code: string; message?: string };
103+
104+
if (awsError.Code === 'AccessDenied') {
105+
throw new Error(
106+
`Access denied to S3 bucket "${APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET}". ` +
107+
`Please verify that:\n` +
108+
`1. The bucket "${APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET}" exists\n` +
109+
`2. Your AWS credentials have s3:PutObject permission for this bucket\n` +
110+
`3. The bucket is in the correct region (${process.env.APP_AWS_REGION || 'not set'})\n` +
111+
`4. The bucket name is correct`
112+
);
113+
}
114+
115+
if (awsError.Code === 'NoSuchBucket') {
116+
throw new Error(
117+
`S3 bucket "${APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET}" does not exist. ` +
118+
`Please create the bucket or update APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET environment variable.`
119+
);
120+
}
121+
122+
if (awsError.Code === 'InvalidAccessKeyId' || awsError.Code === 'SignatureDoesNotMatch') {
123+
throw new Error(
124+
`Invalid AWS credentials. Please check APP_AWS_ACCESS_KEY_ID and APP_AWS_SECRET_ACCESS_KEY environment variables.`
125+
);
126+
}
127+
}
128+
96129
throw error instanceof Error
97130
? error
98131
: new Error('Failed to upload questionnaire file');

apps/app/src/app/s3.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const APP_AWS_ACCESS_KEY_ID = process.env.APP_AWS_ACCESS_KEY_ID;
55
const APP_AWS_SECRET_ACCESS_KEY = process.env.APP_AWS_SECRET_ACCESS_KEY;
66

77
export const BUCKET_NAME = process.env.APP_AWS_BUCKET_NAME;
8+
export const APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET = process.env.APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET;
89

910
let s3ClientInstance: S3Client;
1011

apps/app/src/env.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const env = createEnv({
2828
APP_AWS_SECRET_ACCESS_KEY: z.string().optional(),
2929
APP_AWS_REGION: z.string().optional(),
3030
APP_AWS_BUCKET_NAME: z.string().optional(),
31+
APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET: z.string().optional(),
3132
NEXT_PUBLIC_PORTAL_URL: z.string(),
3233
FIRECRAWL_API_KEY: z.string().optional(),
3334
FLEET_URL: z.string().optional(),
@@ -81,6 +82,7 @@ export const env = createEnv({
8182
APP_AWS_SECRET_ACCESS_KEY: process.env.APP_AWS_SECRET_ACCESS_KEY,
8283
APP_AWS_REGION: process.env.APP_AWS_REGION,
8384
APP_AWS_BUCKET_NAME: process.env.APP_AWS_BUCKET_NAME,
85+
APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET: process.env.APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET,
8486
NEXT_PUBLIC_PORTAL_URL: process.env.NEXT_PUBLIC_PORTAL_URL,
8587
FIRECRAWL_API_KEY: process.env.FIRECRAWL_API_KEY,
8688
FLEET_URL: process.env.FLEET_URL,

apps/app/src/jobs/tasks/vendors/parse-questionnaire.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { logger, task } from '@trigger.dev/sdk';
2-
import { BUCKET_NAME, extractS3KeyFromUrl, s3Client } from '@/app/s3';
2+
import { APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET, BUCKET_NAME, extractS3KeyFromUrl, s3Client } from '@/app/s3';
33
import { env } from '@/env.mjs';
44
import { GetObjectCommand } from '@aws-sdk/client-s3';
55
import { openai } from '@ai-sdk/openai';
@@ -309,8 +309,16 @@ async function extractContentFromS3Key(
309309
s3Key: string,
310310
fileType: string,
311311
): Promise<{ content: string; fileType: string }> {
312+
if (!s3Client) {
313+
throw new Error('S3 client is not initialized. Please check AWS S3 environment variables (APP_AWS_REGION, APP_AWS_ACCESS_KEY_ID, APP_AWS_SECRET_ACCESS_KEY).');
314+
}
315+
316+
if (!APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET) {
317+
throw new Error('Questionnaire upload bucket is not configured. Please set APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET environment variable.');
318+
}
319+
312320
const getCommand = new GetObjectCommand({
313-
Bucket: BUCKET_NAME!,
321+
Bucket: APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET,
314322
Key: s3Key,
315323
});
316324

0 commit comments

Comments
 (0)