Skip to content

Commit

Permalink
Merge branch 'main' into feat-community-preview
Browse files Browse the repository at this point in the history
  • Loading branch information
NarwhalChen committed Mar 5, 2025
2 parents d19be43 + 90ebd1f commit 9e7740e
Show file tree
Hide file tree
Showing 27 changed files with 1,926 additions and 231 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ models/

*/**/database.sqlite
./backend/src/database.sqlite
.codefox
.codefox

.env
5 changes: 0 additions & 5 deletions backend/.env

This file was deleted.

5 changes: 0 additions & 5 deletions backend/.env.development

This file was deleted.

23 changes: 23 additions & 0 deletions backend/.env.example
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

3 changes: 2 additions & 1 deletion backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ log-*/


# Backend
/backend/package-lock.json
/backend/package-lock.json
.env
3 changes: 3 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
},
"dependencies": {
"@apollo/server": "^4.11.0",
"@aws-sdk/client-s3": "^3.758.0",
"@huggingface/hub": "latest",
"@huggingface/transformers": "latest",
"@nestjs/apollo": "^12.2.0",
Expand All @@ -45,6 +46,7 @@
"@types/toposort": "^2.0.7",
"axios": "^1.7.7",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dotenv": "^16.4.7",
"eslint-plugin-unused-imports": "^4.1.4",
Expand All @@ -53,6 +55,7 @@
"gpt-3-encoder": "^1.1.4",
"graphql": "^16.9.0",
"graphql-subscriptions": "^2.0.0",
"graphql-upload-minimal": "^1.6.1",
"graphql-ws": "^5.16.0",
"lodash": "^4.17.21",
"markdown-to-txt": "^2.0.1",
Expand Down
34 changes: 34 additions & 0 deletions backend/src/config/config.module.ts
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 {}
71 changes: 71 additions & 0 deletions backend/src/config/config.service.ts
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');
}

/**
* 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)
);
}
}
51 changes: 51 additions & 0 deletions backend/src/config/env.validation.ts
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;
}
3 changes: 3 additions & 0 deletions backend/src/guard/project.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { JwtService } from '@nestjs/jwt';

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

/**
* This guard checks if the user is authorized to access a project.
*/
@Injectable()
export class ProjectGuard implements CanActivate {
private readonly logger = new Logger('ProjectGuard');
Expand Down
7 changes: 7 additions & 0 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AppModule } from './app.module';
import 'reflect-metadata';
import * as dotenv from 'dotenv';
import { Logger } from '@nestjs/common';
import { graphqlUploadExpress } from 'graphql-upload-minimal';

async function bootstrap() {
const logger = new Logger('Bootstrap');
Expand All @@ -18,10 +19,16 @@ async function bootstrap() {
'Authorization',
'Access-Control-Allow-Origin',
'Access-Control-Allow-Credentials',
'Apollo-Require-Preflight',
'x-refresh-token',
],
});

app.use(
'/graphql',
graphqlUploadExpress({ maxFileSize: 50000000, maxFiles: 10 }),
);

console.log('process.env.PORT:', process.env.PORT);
const server = await app.listen(process.env.PORT ?? 8080);
logger.log(`Application is running on port ${process.env.PORT ?? 8080}`);
Expand Down
12 changes: 12 additions & 0 deletions backend/src/project/dto/project.input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { InputType, Field, ID, ObjectType } from '@nestjs/graphql';
import { IsNotEmpty, IsString, IsUUID, IsOptional } from 'class-validator';
import { Project } from '../project.model';
import { FileUpload, GraphQLUpload } from 'graphql-upload-minimal';

/**
* @deprecated We don't need project upsert
Expand Down Expand Up @@ -118,3 +119,14 @@ export class FetchPublicProjectsInputs {
@Field()
size: number;
}

@InputType()
export class UpdateProjectPhotoInput {
@IsString()
@Field(() => ID)
projectId: string;

@IsOptional()
@Field(() => GraphQLUpload)
file: FileUpload;
}
26 changes: 26 additions & 0 deletions backend/src/project/project-limits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ForbiddenException, HttpStatus } from '@nestjs/common';
import { GraphQLError } from 'graphql';

export const PROJECT_DAILY_LIMIT = 3; // Maximum number of projects a user can create per day

export enum ProjectErrorCode {
DAILY_LIMIT_EXCEEDED = 'DAILY_LIMIT_EXCEEDED',
}

export class ProjectRateLimitException extends ForbiddenException {
constructor(limit: number) {
super(
`Daily project creation limit of ${limit} reached. Please try again tomorrow.`,
);
}

getGraphQLError(): GraphQLError {
return new GraphQLError(this.message, {
extensions: {
code: ProjectErrorCode.DAILY_LIMIT_EXCEEDED,
limit: PROJECT_DAILY_LIMIT,
status: HttpStatus.TOO_MANY_REQUESTS,
},
});
}
}
6 changes: 5 additions & 1 deletion backend/src/project/project.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import { ProjectGuard } from '../guard/project.guard';
import { ChatService } from 'src/chat/chat.service';
import { User } from 'src/user/user.model';
import { Chat } from 'src/chat/chat.model';
import { AppConfigModule } from 'src/config/config.module';
import { UploadModule } from 'src/upload/upload.module';

@Module({
imports: [
TypeOrmModule.forFeature([Project, Chat, User, ProjectPackages]),
AuthModule, // Import AuthModule to provide JwtService to the ProjectGuard
AuthModule,
AppConfigModule,
UploadModule,
],
providers: [ChatService, ProjectService, ProjectsResolver, ProjectGuard],
exports: [ProjectService, ProjectGuard],
Expand Down
Loading

0 comments on commit 9e7740e

Please sign in to comment.