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
10 changes: 10 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,13 @@ GITHUB_CLIENT_ID="GITHUB_CLIENT_ID"
GITHUB_CLIENT_SECRET="GITHUB_CLIENT_SECRET"
GITHUB_WEBHOOK_SECRET="GITHUB_WEBHOOK_SECRET"
CALLBACK="CALLBACK"

# Database Configuration
USE_REMOTE_DB=false
DB_TYPE=
DB_HOST=
DB_PORT=0
DB_USERNAME=
DB_NAME=
DB_REGION=us-east-2

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@types/normalize-path": "^3.0.2",
"@types/toposort": "^2.0.7",
"archiver": "^7.0.1",
"aws-sdk": "^2.1692.0",
"axios": "^1.7.7",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
Expand All @@ -69,6 +70,7 @@
"octokit": "^4.1.2",
"openai": "^4.77.0",
"p-queue-es5": "^6.0.2",
"pg": "^8.14.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"simple-git": "^3.27.0",
Expand Down
28 changes: 17 additions & 11 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { EnvironmentVariables } from './config/env.validation';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';
import { join } from 'path';
Expand All @@ -17,14 +18,17 @@ import { LoggingInterceptor } from 'src/interceptor/LoggingInterceptor';
import { PromptToolModule } from './prompt-tool/prompt-tool.module';
import { MailModule } from './mail/mail.module';
import { GitHubModule } from './github/github.module';
import { AppConfigService } from './config/config.service';
import { getDatabaseConfig } from './database.config';

// TODO(Sma1lboy): move to a separate file
function isProduction(): boolean {
return process.env.NODE_ENV === 'production';
}
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
ConfigModule.forRoot({
isGlobal: true,
validate: (config: Record<string, unknown>) => {
return Object.assign(new EnvironmentVariables(), config);
},
}),
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), '../frontend/src/graphql/schema.gql'),
Expand All @@ -36,11 +40,11 @@ function isProduction(): boolean {
},
context: ({ req, res }) => ({ req, res }),
}),
TypeOrmModule.forRoot({
type: 'sqlite',
database: join(process.cwd(), './database.db'),
synchronize: !isProduction(),
entities: [__dirname + '/**/*.model{.ts,.js}'],
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService<EnvironmentVariables>) =>
getDatabaseConfig(new AppConfigService(config)),
inject: [ConfigService],
}),
InitModule,
UserModule,
Expand All @@ -55,10 +59,12 @@ function isProduction(): boolean {
],
providers: [
AppResolver,
AppConfigService,
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
exports: [AppConfigService],
})
export class AppModule {}
13 changes: 7 additions & 6 deletions backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,26 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { Menu } from './menu/menu.model';
import { JwtModule } from '@nestjs/jwt';
import { Role } from './role/role.model';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { User } from 'src/user/user.model';
import { AppConfigService } from 'src/config/config.service';
import { AuthResolver } from './auth.resolver';
import { RefreshToken } from './refresh-token/refresh-token.model';
import { JwtCacheModule } from 'src/jwt-cache/jwt-cache.module';
import { MailModule } from 'src/mail/mail.module';
import { AppConfigModule } from 'src/config/config.module';

@Module({
imports: [
ConfigModule,
AppConfigModule,
TypeOrmModule.forFeature([Role, Menu, User, RefreshToken]),
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
imports: [AppConfigModule],
useFactory: async (config: AppConfigService) => ({
secret: config.jwtSecret,
signOptions: { expiresIn: '24h' },
}),
inject: [ConfigService],
inject: [AppConfigService],
}),
JwtCacheModule,
MailModule,
Expand Down
9 changes: 3 additions & 6 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AppConfigService } from 'src/config/config.service';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { LoginUserInput } from 'src/user/dto/login-user.input';
Expand Down Expand Up @@ -34,7 +34,7 @@
private userRepository: Repository<User>,
private jwtService: JwtService,
private jwtCacheService: JwtCacheService,
private configService: ConfigService,
private configService: AppConfigService,
private mailService: MailService,
@InjectRepository(Menu)
private menuRepository: Repository<Menu>,
Expand All @@ -43,10 +43,7 @@
@InjectRepository(RefreshToken)
private refreshTokenRepository: Repository<RefreshToken>,
) {
// Read the MAIL_ENABLED environment variable, default to 'true'
this.isMailEnabled =
this.configService.get<string>('MAIL_ENABLED', 'false').toLowerCase() ===
'true';
this.isMailEnabled = this.configService.isMailEnabled;
}

async confirmEmail(token: string): Promise<EmailConfirmationResponse> {
Expand Down Expand Up @@ -80,7 +77,7 @@
message: 'Email already confirmed or user not found.',
success: false,
};
} catch (error) {

Check warning on line 80 in backend/src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / autofix

'error' is defined but never used
return {
message: 'Invalid or expired token',
success: false,
Expand Down Expand Up @@ -264,7 +261,7 @@
}

return true;
} catch (error) {

Check warning on line 264 in backend/src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / autofix

'error' is defined but never used
return false;
}
}
Expand Down
48 changes: 48 additions & 0 deletions backend/src/common/decorators/universal-date-column.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { CreateDateColumn, UpdateDateColumn, ColumnType } from 'typeorm';

/**
* Universal date column options
*/
interface UniversalDateOptions {
nullable?: boolean;
update?: boolean;
}

/**
* Get database column type based on environment
* @returns 'timestamp' for remote database, 'datetime' for local
*/
function getDateColumnType(): ColumnType {
// FIXME: optimize this logic to use a config file or environment variable
// const useRemoteDb = process.env.USE_REMOTE_DB === 'true';
// use timestamp for remote database
return 'datetime' as ColumnType;
}

/**
* Universal create date column decorator that handles both remote and local databases
*/
export function UniversalCreateDateColumn(options: UniversalDateOptions = {}) {
return CreateDateColumn({
type: getDateColumnType(),
nullable: options.nullable,
transformer: {
to: (value: Date) => value,
from: (value: any) => (value ? new Date(value) : null),
},
});
}

/**
* Universal update date column decorator that handles both remote and local databases
*/
export function UniversalUpdateDateColumn(options: UniversalDateOptions = {}) {
return UpdateDateColumn({
type: getDateColumnType(),
nullable: options.nullable,
transformer: {
to: (value: Date) => value,
from: (value: any) => (value ? new Date(value) : null),
},
});
}
110 changes: 110 additions & 0 deletions backend/src/config/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,114 @@ export class AppConfigService {
(config.endpoint || config.accountId)
);
}

/**
* Check if production environment
*/
get isProduction(): boolean {
return this.configService.get('NODE_ENV') === 'production';
}

/**
* Check if development environment
*/
get isDevEnv(): boolean {
const env = this.configService.get('NODE_ENV');
return env?.toUpperCase() === 'DEV';
}

/**
* Check if using remote database
*/
get useRemoteDb(): boolean {
return this.configService.get('USE_REMOTE_DB') === 'true';
}

/**
* Get database configuration
*/
get dbConfig() {
return {
host: this.configService.get('DB_HOST'),
port: parseInt(this.configService.get('DB_PORT'), 10),
username: this.configService.get('DB_USERNAME'),
password: this.configService.get('DB_PASSWORD'),
database: this.configService.get('DB_DATABASE') || 'postgres',
region: this.configService.get('DB_REGION'),
};
}

/**
* Get GitHub configuration
*/
get githubConfig() {
return {
appId: this.configService.get('GITHUB_APP_ID'),
privateKeyPath: this.configService.get('GITHUB_PRIVATE_KEY_PATH'),
clientId: this.configService.get('GITHUB_CLIENT_ID'),
clientSecret: this.configService.get('GITHUB_CLIENT_SECRET'),
webhookSecret: this.configService.get('GITHUB_WEBHOOK_SECRET'),
enabled: !!this.configService.get('GITHUB_ENABLED'),
};
}

get githubEnabled(): boolean {
return this.configService.get('GITHUB_ENABLED') === 'true';
}

get githubWebhookSecret(): string {
return this.configService.get('GITHUB_WEBHOOK_SECRET');
}

/**
* Get mail domain for email links
*/
get mailDomain(): string {
return this.configService.get('MAIL_DOMAIN');
}

/**
* Get frontend URL for email links
*/
get frontendUrl(): string {
return this.configService.get('FRONTEND_URL');
}

/**
* Get mail configuration
*/
get mailConfig() {
return {
host: this.configService.get('MAIL_HOST'),
port: parseInt(this.configService.get('MAIL_PORT'), 10),
user: this.configService.get('MAIL_USER'),
password: this.configService.get('MAIL_PASSWORD'),
from: this.configService.get('MAIL_FROM'),
};
}

/**
* Check if mail service is enabled
*/
get isMailEnabled(): boolean {
return (
this.configService.get('MAIL_ENABLED', 'false').toLowerCase() === 'true'
);
}

get githubAppId(): string {
return this.configService.get('GITHUB_APP_ID');
}

get githubPrivateKeyPath(): string {
return this.configService.get('GITHUB_PRIVATE_KEY_PATH');
}

get githubClientId(): string {
return this.configService.get('GITHUB_CLIENT_ID');
}

get githubClientSecret(): string {
return this.configService.get('GITHUB_CLIENT_SECRET');
}
}
68 changes: 68 additions & 0 deletions backend/src/config/env.validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ export class EnvironmentVariables {
@IsString()
DB_DATABASE?: string;

@IsOptional()
@IsString()
DB_REGION?: string;

@IsOptional()
@IsString()
@IsIn(['true', 'false'])
USE_REMOTE_DB?: string;

@IsNumber()
PORT: number = 8000;

Expand Down Expand Up @@ -69,4 +78,63 @@ export class EnvironmentVariables {
@IsOptional()
@IsString()
S3_PUBLIC_URL?: string;

// GitHub Configuration
@IsOptional()
@IsString()
GITHUB_ENABLED: boolean;

@IsOptional()
@IsString()
GITHUB_APP_ID?: string;

@IsOptional()
@IsString()
GITHUB_PRIVATE_KEY_PATH?: string;

@IsOptional()
@IsString()
GITHUB_CLIENT_ID?: string;

@IsOptional()
@IsString()
GITHUB_CLIENT_SECRET?: string;

@IsOptional()
@IsString()
GITHUB_WEBHOOK_SECRET?: string;

// Mail Configuration
@IsOptional()
@IsString()
MAIL_DOMAIN?: string;

@IsOptional()
@IsString()
FRONTEND_URL?: string;

@IsOptional()
@IsString()
MAIL_HOST?: string;

@IsOptional()
@IsString()
MAIL_PORT?: string;

@IsOptional()
@IsString()
MAIL_USER?: string;

@IsOptional()
@IsString()
MAIL_PASSWORD?: string;

@IsOptional()
@IsString()
MAIL_FROM?: string;

@IsOptional()
@IsString()
@IsIn(['true', 'false'])
MAIL_ENABLED?: string;
}
Loading
Loading