Skip to content

Commit 1173bef

Browse files
Sma1lboyautofix-ci[bot]NarwhalChen
authored
feat(backend): adding s3 supports, and also expose uploading project images (#154)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - **Enhanced Project Photo Upload:** Users can now update project photos directly through file uploads for a smoother, more reliable experience. - **Expanded Image Flexibility:** The application now supports loading images from any domain, broadening your content sourcing options. - **Improved Upload Performance:** Upgraded file handling ensures consistent and efficient processing for a better overall experience. - **New Configuration Options:** A new example configuration file has been added to guide users on setting up environment variables. - **Bug Fixes** - **Updated Project Photo Mutation:** The mutation for updating project photos has been streamlined, enhancing functionality and usability. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: NarwhalChen <125920907+NarwhalChen@users.noreply.github.com>
1 parent b616285 commit 1173bef

25 files changed

+1826
-225
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ models/
1616

1717
*/**/database.sqlite
1818
./backend/src/database.sqlite
19-
.codefox
19+
.codefox
20+
21+
.env

backend/.env

Lines changed: 0 additions & 5 deletions
This file was deleted.

backend/.env.development

Lines changed: 0 additions & 5 deletions
This file was deleted.

backend/.env.example

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Server Configuration
2+
PORT=8080
3+
4+
# DEV PROD OR TEST
5+
NODE_ENV="DEV"
6+
# JWT Configuration
7+
JWT_SECRET="your_jwt_secret_here"
8+
JWT_REFRESH="your_jwt_refresh_secret_here"
9+
SALT_ROUNDS=10
10+
11+
# OpenAI Configuration
12+
OPENAI_BASE_URI="http://localhost:3001"
13+
14+
# S3/Cloudflare R2 Configuration (Optional)
15+
# If not provided, local file storage will be used
16+
S3_ACCESS_KEY_ID="your_s3_access_key_id" # Must be 32 characters for Cloudflare R2
17+
S3_SECRET_ACCESS_KEY="your_s3_secret_access_key"
18+
S3_REGION="auto" # Use 'auto' for Cloudflare R2
19+
S3_BUCKET_NAME="your_bucket_name"
20+
S3_ENDPOINT="https://<account_id>.r2.cloudflarestorage.com" # Cloudflare R2 endpoint
21+
S3_ACCOUNT_ID="your_cloudflare_account_id" # Your Cloudflare account ID
22+
S3_PUBLIC_URL="https://pub-xxx.r2.dev" # Your R2 public bucket URL
23+

backend/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,5 @@ log-*/
5555

5656

5757
# Backend
58-
/backend/package-lock.json
58+
/backend/package-lock.json
59+
.env

backend/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
},
2929
"dependencies": {
3030
"@apollo/server": "^4.11.0",
31+
"@aws-sdk/client-s3": "^3.758.0",
3132
"@huggingface/hub": "latest",
3233
"@huggingface/transformers": "latest",
3334
"@nestjs/apollo": "^12.2.0",
@@ -45,6 +46,7 @@
4546
"@types/toposort": "^2.0.7",
4647
"axios": "^1.7.7",
4748
"bcrypt": "^5.1.1",
49+
"class-transformer": "^0.5.1",
4850
"class-validator": "^0.14.1",
4951
"dotenv": "^16.4.7",
5052
"eslint-plugin-unused-imports": "^4.1.4",
@@ -53,6 +55,7 @@
5355
"gpt-3-encoder": "^1.1.4",
5456
"graphql": "^16.9.0",
5557
"graphql-subscriptions": "^2.0.0",
58+
"graphql-upload-minimal": "^1.6.1",
5659
"graphql-ws": "^5.16.0",
5760
"lodash": "^4.17.21",
5861
"markdown-to-txt": "^2.0.1",
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Module } from '@nestjs/common';
2+
import { ConfigModule as NestConfigModule } from '@nestjs/config';
3+
import { AppConfigService } from './config.service';
4+
import { EnvironmentVariables } from './env.validation';
5+
import { plainToInstance } from 'class-transformer';
6+
import { validateSync } from 'class-validator';
7+
8+
const validate = (config: Record<string, unknown>) => {
9+
const validatedConfig = plainToInstance(EnvironmentVariables, config, {
10+
enableImplicitConversion: true,
11+
});
12+
13+
const errors = validateSync(validatedConfig, {
14+
skipMissingProperties: false,
15+
});
16+
17+
if (errors.length > 0) {
18+
throw new Error(errors.toString());
19+
}
20+
21+
return validatedConfig;
22+
};
23+
24+
@Module({
25+
imports: [
26+
NestConfigModule.forRoot({
27+
validate,
28+
isGlobal: true,
29+
}),
30+
],
31+
providers: [AppConfigService],
32+
exports: [AppConfigService],
33+
})
34+
export class AppConfigModule {}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { ConfigService as NestConfigService } from '@nestjs/config';
3+
import { EnvironmentVariables } from './env.validation';
4+
5+
@Injectable()
6+
export class AppConfigService {
7+
constructor(private configService: NestConfigService<EnvironmentVariables>) {}
8+
9+
/**
10+
* Get server port from environment
11+
*/
12+
get port(): number {
13+
return this.configService.get('PORT');
14+
}
15+
16+
/**
17+
* Get JWT secret key for token generation
18+
*/
19+
get jwtSecret(): string {
20+
return this.configService.get('JWT_SECRET');
21+
}
22+
23+
/**
24+
* Get JWT refresh token secret
25+
*/
26+
get jwtRefresh(): string {
27+
return this.configService.get('JWT_REFRESH');
28+
}
29+
30+
/**
31+
* Get password hashing salt rounds
32+
*/
33+
get saltRounds(): number {
34+
return this.configService.get('SALT_ROUNDS');
35+
}
36+
37+
/**
38+
* Get OpenAI API base URI
39+
*/
40+
get openaiBaseUri(): string {
41+
return this.configService.get('OPENAI_BASE_URI');
42+
}
43+
44+
/**
45+
* Get S3/Cloudflare R2 configuration object
46+
*/
47+
get s3Config() {
48+
return {
49+
accessKeyId: this.configService.get('S3_ACCESS_KEY_ID'),
50+
secretAccessKey: this.configService.get('S3_SECRET_ACCESS_KEY'),
51+
region: this.configService.get('S3_REGION'),
52+
bucketName: this.configService.get('S3_BUCKET_NAME'),
53+
endpoint: this.configService.get('S3_ENDPOINT'),
54+
accountId: this.configService.get('S3_ACCOUNT_ID'),
55+
publicUrl: this.configService.get('S3_PUBLIC_URL'),
56+
};
57+
}
58+
59+
/**
60+
* Check if S3 storage is properly configured
61+
*/
62+
get hasS3Configured(): boolean {
63+
const config = this.s3Config;
64+
return !!(
65+
config.accessKeyId &&
66+
config.secretAccessKey &&
67+
config.region &&
68+
(config.endpoint || config.accountId)
69+
);
70+
}
71+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { IsOptional, IsString, IsNumber, IsIn } from 'class-validator';
2+
3+
export class EnvironmentVariables {
4+
@IsNumber()
5+
PORT: number;
6+
7+
@IsString()
8+
@IsIn(['DEV', 'PROD', 'TEST'])
9+
NODE_ENV: string;
10+
11+
@IsString()
12+
JWT_SECRET: string;
13+
14+
@IsString()
15+
JWT_REFRESH: string;
16+
17+
@IsNumber()
18+
SALT_ROUNDS: number;
19+
20+
@IsString()
21+
OPENAI_BASE_URI: string;
22+
23+
// S3/Cloudflare R2 Configuration - all optional
24+
@IsOptional()
25+
@IsString()
26+
S3_ACCESS_KEY_ID?: string;
27+
28+
@IsOptional()
29+
@IsString()
30+
S3_SECRET_ACCESS_KEY?: string;
31+
32+
@IsOptional()
33+
@IsString()
34+
S3_REGION?: string;
35+
36+
@IsOptional()
37+
@IsString()
38+
S3_BUCKET_NAME?: string;
39+
40+
@IsOptional()
41+
@IsString()
42+
S3_ENDPOINT?: string;
43+
44+
@IsOptional()
45+
@IsString()
46+
S3_ACCOUNT_ID?: string;
47+
48+
@IsOptional()
49+
@IsString()
50+
S3_PUBLIC_URL?: string;
51+
}

backend/src/guard/project.guard.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { JwtService } from '@nestjs/jwt';
1010

1111
import { ProjectService } from '../project/project.service';
1212

13+
/**
14+
* This guard checks if the user is authorized to access a project.
15+
*/
1316
@Injectable()
1417
export class ProjectGuard implements CanActivate {
1518
private readonly logger = new Logger('ProjectGuard');

0 commit comments

Comments
 (0)