-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(backend): adding s3 supports, and also expose uploading project images #154
Changes from all commits
e66de71
3d150e0
5b1dcb7
2cb5608
3ec5259
ef3bcf2
0d2357b
dadc166
63ea6a5
da2b890
8deecfc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,4 +16,6 @@ models/ | |
|
||
*/**/database.sqlite | ||
./backend/src/database.sqlite | ||
.codefox | ||
.codefox | ||
|
||
.env |
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# Server Configuration | ||
PORT=8080 | ||
|
||
# DEV PROD OR TEST | ||
NODE_ENV="DEV" | ||
# JWT Configuration | ||
JWT_SECRET="your_jwt_secret_here" | ||
JWT_REFRESH="your_jwt_refresh_secret_here" | ||
SALT_ROUNDS=10 | ||
|
||
# OpenAI Configuration | ||
OPENAI_BASE_URI="http://localhost:3001" | ||
|
||
# S3/Cloudflare R2 Configuration (Optional) | ||
# If not provided, local file storage will be used | ||
S3_ACCESS_KEY_ID="your_s3_access_key_id" # Must be 32 characters for Cloudflare R2 | ||
S3_SECRET_ACCESS_KEY="your_s3_secret_access_key" | ||
S3_REGION="auto" # Use 'auto' for Cloudflare R2 | ||
S3_BUCKET_NAME="your_bucket_name" | ||
S3_ENDPOINT="https://<account_id>.r2.cloudflarestorage.com" # Cloudflare R2 endpoint | ||
S3_ACCOUNT_ID="your_cloudflare_account_id" # Your Cloudflare account ID | ||
S3_PUBLIC_URL="https://pub-xxx.r2.dev" # Your R2 public bucket URL | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,4 +55,5 @@ log-*/ | |
|
||
|
||
# Backend | ||
/backend/package-lock.json | ||
/backend/package-lock.json | ||
.env |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { ConfigModule as NestConfigModule } from '@nestjs/config'; | ||
import { AppConfigService } from './config.service'; | ||
import { EnvironmentVariables } from './env.validation'; | ||
import { plainToInstance } from 'class-transformer'; | ||
import { validateSync } from 'class-validator'; | ||
|
||
const validate = (config: Record<string, unknown>) => { | ||
const validatedConfig = plainToInstance(EnvironmentVariables, config, { | ||
enableImplicitConversion: true, | ||
}); | ||
|
||
const errors = validateSync(validatedConfig, { | ||
skipMissingProperties: false, | ||
}); | ||
|
||
if (errors.length > 0) { | ||
throw new Error(errors.toString()); | ||
} | ||
|
||
return validatedConfig; | ||
}; | ||
|
||
@Module({ | ||
imports: [ | ||
NestConfigModule.forRoot({ | ||
validate, | ||
isGlobal: true, | ||
}), | ||
], | ||
providers: [AppConfigService], | ||
exports: [AppConfigService], | ||
}) | ||
export class AppConfigModule {} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,71 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { Injectable } from '@nestjs/common'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { ConfigService as NestConfigService } from '@nestjs/config'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { EnvironmentVariables } from './env.validation'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@Injectable() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export class AppConfigService { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
constructor(private configService: NestConfigService<EnvironmentVariables>) {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Get server port from environment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
get port(): number { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return this.configService.get('PORT'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Get JWT secret key for token generation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
get jwtSecret(): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return this.configService.get('JWT_SECRET'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Get JWT refresh token secret | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
get jwtRefresh(): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return this.configService.get('JWT_REFRESH'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Get password hashing salt rounds | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
get saltRounds(): number { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return this.configService.get('SALT_ROUNDS'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Get OpenAI API base URI | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
get openaiBaseUri(): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return this.configService.get('OPENAI_BASE_URI'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+9
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Config getters can throw if environment variables are missing. The config getters directly access environment variables without checking if they exist. This could lead to runtime errors if required environment variables are not set. Consider adding default values or explicit error handling: get jwtSecret(): string {
- return this.configService.get('JWT_SECRET');
+ const secret = this.configService.get('JWT_SECRET');
+ if (!secret) {
+ throw new Error('JWT_SECRET environment variable is not set');
+ }
+ return secret;
} Or use the built-in ConfigService default value capability: get jwtSecret(): string {
- return this.configService.get('JWT_SECRET');
+ return this.configService.get('JWT_SECRET', 'default-secret-for-development-only');
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Get S3/Cloudflare R2 configuration object | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
get s3Config() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
accessKeyId: this.configService.get('S3_ACCESS_KEY_ID'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
secretAccessKey: this.configService.get('S3_SECRET_ACCESS_KEY'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
region: this.configService.get('S3_REGION'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
bucketName: this.configService.get('S3_BUCKET_NAME'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
endpoint: this.configService.get('S3_ENDPOINT'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
accountId: this.configService.get('S3_ACCOUNT_ID'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
publicUrl: this.configService.get('S3_PUBLIC_URL'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Check if S3 storage is properly configured | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
get hasS3Configured(): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const config = this.s3Config; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return !!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
config.accessKeyId && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
config.secretAccessKey && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
config.region && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(config.endpoint || config.accountId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+62
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion S3 configuration validation is incomplete. The Add bucket name validation to the S3 configuration check: get hasS3Configured(): boolean {
const config = this.s3Config;
return !!(
config.accessKeyId &&
config.secretAccessKey &&
config.region &&
+ config.bucketName &&
(config.endpoint || config.accountId)
);
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { IsOptional, IsString, IsNumber, IsIn } from 'class-validator'; | ||
|
||
export class EnvironmentVariables { | ||
@IsNumber() | ||
PORT: number; | ||
|
||
@IsString() | ||
@IsIn(['DEV', 'PROD', 'TEST']) | ||
NODE_ENV: string; | ||
|
||
@IsString() | ||
JWT_SECRET: string; | ||
|
||
@IsString() | ||
JWT_REFRESH: string; | ||
|
||
@IsNumber() | ||
SALT_ROUNDS: number; | ||
|
||
@IsString() | ||
OPENAI_BASE_URI: string; | ||
|
||
// S3/Cloudflare R2 Configuration - all optional | ||
@IsOptional() | ||
@IsString() | ||
S3_ACCESS_KEY_ID?: string; | ||
|
||
@IsOptional() | ||
@IsString() | ||
S3_SECRET_ACCESS_KEY?: string; | ||
|
||
@IsOptional() | ||
@IsString() | ||
S3_REGION?: string; | ||
|
||
@IsOptional() | ||
@IsString() | ||
S3_BUCKET_NAME?: string; | ||
|
||
@IsOptional() | ||
@IsString() | ||
S3_ENDPOINT?: string; | ||
|
||
@IsOptional() | ||
@IsString() | ||
S3_ACCOUNT_ID?: string; | ||
|
||
@IsOptional() | ||
@IsString() | ||
S3_PUBLIC_URL?: string; | ||
Comment on lines
+23
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainS3 configuration is optional but should have local fallback implementation. Making S3 configuration optional is good for flexibility, but ensure there's a fallback for storing files locally when S3 is not configured. Ensure the upload service has a fallback mechanism when S3 credentials aren't provided. You should implement a strategy pattern that can switch between S3 storage and local file storage based on configuration. Let's check if this is already implemented: 🏁 Script executed: #!/bin/bash
# Check if there's a fallback mechanism for local file storage
echo "Checking for local file storage fallback implementation..."
# Look for conditional storage selection based on config
rg -A 10 -B 10 "S3_" --glob "*upload*.ts" --no-ignore
# Check for filesystem operations indicating local storage
rg -i "fs\.|writeFile|createReadStream|createWriteStream" --glob "*upload*.ts" --no-ignore Length of output: 863 Action Required: Implement Conditional Storage Strategy The environment validation in
|
||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -15,6 +15,7 @@ import { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
CreateProjectInput, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
FetchPublicProjectsInputs, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
IsValidProjectInput, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
UpdateProjectPhotoInput, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} from './dto/project.input'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { Logger, UseGuards } from '@nestjs/common'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { ProjectGuard } from '../guard/project.guard'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -87,22 +88,33 @@ export class ProjectsResolver { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return this.projectService.subscribeToProject(userId, projectId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@UseGuards(ProjectGuard) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@Mutation(() => Project) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async updateProjectPhotoUrl( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async updateProjectPhoto( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@GetUserIdFromToken() userId: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@Args('projectId', { type: () => ID }) projectId: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@Args('photoUrl') photoUrl: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@Args('input') input: UpdateProjectPhotoInput, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
): Promise<Project> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.logger.log( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
`User ${userId} updating photo URL for project ${projectId}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { projectId, file } = input; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.logger.log(`User ${userId} uploading photo for project ${projectId}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Extract the file data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { createReadStream, mimetype } = await file; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Buffer the file content | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const chunks = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
for await (const chunk of createReadStream()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
chunks.push(chunk); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const buffer = Buffer.concat(chunks); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+100
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider adding file size validation and error handling. While the file buffering implementation is correct, there are some potential improvements for robustness:
Consider adding the following improvements: // Extract the file data
const { createReadStream, mimetype } = await file;
// Buffer the file content
const chunks = [];
+ let totalSize = 0;
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB limit
try {
for await (const chunk of createReadStream()) {
+ totalSize += chunk.length;
+ if (totalSize > MAX_FILE_SIZE) {
+ throw new Error(`File size exceeds the maximum allowed size of ${MAX_FILE_SIZE / (1024 * 1024)}MB`);
+ }
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);
+ } catch (error) {
+ this.logger.error(`Error processing file upload: ${error.message}`);
+ throw new Error(`Failed to process file upload: ${error.message}`);
+ } 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Call the service with the extracted buffer and mimetype | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return this.projectService.updateProjectPhotoUrl( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
userId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
projectId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
photoUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
buffer, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
mimetype, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+110
to
116
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent ❓ Verification inconclusiveConsider validating file type for security. The service now accepts a buffer and mimetype, but there's no validation to ensure the file is an acceptable image type. Consider adding file type validation before passing to the service: // Call the service with the extracted buffer and mimetype
+ const ALLOWED_MIMETYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
+ if (!ALLOWED_MIMETYPES.includes(mimetype)) {
+ throw new Error(`Unsupported file type: ${mimetype}. Allowed types: ${ALLOWED_MIMETYPES.join(', ')}`);
+ }
return this.projectService.updateProjectPhotoUrl(
userId,
projectId,
buffer,
mimetype,
); 🏁 Script executed: #!/bin/bash
# Check if there's mimetype validation in the service method
rg -A 10 "updateProjectPhotoUrl.*buffer.*mimetype" --glob "*.ts" backend/src/ Length of output: 79 Security Update Required: Validate Image File Type I verified that there’s no evidence of file type validation in the // Call the service with the extracted buffer and mimetype
+ const ALLOWED_MIMETYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
+ if (!ALLOWED_MIMETYPES.includes(mimetype)) {
+ throw new Error(`Unsupported file type: ${mimetype}. Allowed types: ${ALLOWED_MIMETYPES.join(', ')}`);
+ }
return this.projectService.updateProjectPhotoUrl(
userId,
projectId,
buffer,
mimetype,
); This update will mitigate potential security risks by ensuring that only expected image file types are processed. 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@Mutation(() => Project) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async updateProjectPublicStatus( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@GetUserIdFromToken() userId: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
GraphQL upload package added correctly but consider security implications.
The graphql-upload-minimal package is correctly added for handling file uploads in GraphQL, but ensure you implement proper file size limits and validation.
Make sure to:
Here's a shell script to check if file size limits are implemented:
🏁 Script executed:
Length of output: 415
Action: Enhance Security for GraphQL File Uploads
The dependency in backend/package.json is added correctly and the file size limit is actively configured in backend/src/main.ts via:
However, please ensure the following additional security measures are implemented: