From 8cc8431d7522819a9d7f94f9d2fa2365478da6db Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Sun, 2 Feb 2025 19:49:26 -0600 Subject: [PATCH 1/3] feat: integrate JwtCacheModule and enhance JWT authentication with caching support --- backend/src/app.module.ts | 2 + backend/src/auth/auth.module.ts | 6 +- backend/src/auth/auth.service.ts | 2 +- backend/src/chat/chat.module.ts | 7 +- backend/src/decorator/auth.decorator.ts | 2 +- backend/src/guard/jwt-auth.guard.ts | 67 +++++++++++++--- .../src/{guard => interceptor}/roles.guard.ts | 0 backend/src/jwt-cache/jwt-cache.module.ts | 8 ++ .../{auth => jwt-cache}/jwt-cache.service.ts | 76 ++++++++++--------- 9 files changed, 122 insertions(+), 48 deletions(-) rename backend/src/{guard => interceptor}/roles.guard.ts (100%) create mode 100644 backend/src/jwt-cache/jwt-cache.module.ts rename backend/src/{auth => jwt-cache}/jwt-cache.service.ts (65%) diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index cf25f005..7a24bb36 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -14,6 +14,7 @@ import { User } from './user/user.model'; import { AppResolver } from './app.resolver'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { LoggingInterceptor } from 'src/interceptor/LoggingInterceptor'; +import { PromptToolModule } from './prompt-tool/prompt-tool.module'; @Module({ imports: [ @@ -41,6 +42,7 @@ import { LoggingInterceptor } from 'src/interceptor/LoggingInterceptor'; ProjectModule, TokenModule, ChatModule, + PromptToolModule, TypeOrmModule.forFeature([User]), ], providers: [ diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index 91fa0b61..13c46ac4 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -7,7 +7,8 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { AuthService } from './auth.service'; import { User } from 'src/user/user.model'; import { AuthResolver } from './auth.resolver'; -import { JwtCacheService } from 'src/auth/jwt-cache.service'; +import { JwtCacheService } from 'src/jwt-cache/jwt-cache.service'; +import { JwtCacheModule } from 'src/jwt-cache/jwt-cache.module'; @Module({ imports: [ @@ -21,8 +22,9 @@ import { JwtCacheService } from 'src/auth/jwt-cache.service'; }), inject: [ConfigService], }), + JwtCacheModule, ], - providers: [AuthService, AuthResolver, JwtCacheService], + providers: [AuthService, AuthResolver], exports: [AuthService, JwtModule], }) export class AuthModule {} diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index c7d0d453..2b3ee94d 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -14,7 +14,7 @@ import { RegisterUserInput } from 'src/user/dto/register-user.input'; import { User } from 'src/user/user.model'; import { In, Repository } from 'typeorm'; import { CheckTokenInput } from './dto/check-token.input'; -import { JwtCacheService } from 'src/auth/jwt-cache.service'; +import { JwtCacheService } from 'src/jwt-cache/jwt-cache.service'; import { Menu } from './menu/menu.model'; import { Role } from './role/role.model'; diff --git a/backend/src/chat/chat.module.ts b/backend/src/chat/chat.module.ts index 79d61018..00df9643 100644 --- a/backend/src/chat/chat.module.ts +++ b/backend/src/chat/chat.module.ts @@ -9,9 +9,14 @@ import { ChatGuard } from '../guard/chat.guard'; import { AuthModule } from '../auth/auth.module'; import { UserService } from 'src/user/user.service'; import { PubSub } from 'graphql-subscriptions'; +import { JwtCacheModule } from 'src/jwt-cache/jwt-cache.module'; @Module({ - imports: [TypeOrmModule.forFeature([Chat, User, Message]), AuthModule], + imports: [ + TypeOrmModule.forFeature([Chat, User, Message]), + AuthModule, + JwtCacheModule, + ], providers: [ ChatResolver, ChatProxyService, diff --git a/backend/src/decorator/auth.decorator.ts b/backend/src/decorator/auth.decorator.ts index 944f360d..724ef12b 100644 --- a/backend/src/decorator/auth.decorator.ts +++ b/backend/src/decorator/auth.decorator.ts @@ -13,7 +13,7 @@ import { applyDecorators, UseGuards } from '@nestjs/common'; import { Roles } from './roles.decorator'; import { Menu } from './menu.decorator'; -import { RolesGuard } from 'src/guard/roles.guard'; +import { RolesGuard } from 'src/interceptor/roles.guard'; import { MenuGuard } from 'src/guard/menu.guard'; export function Auth() { diff --git a/backend/src/guard/jwt-auth.guard.ts b/backend/src/guard/jwt-auth.guard.ts index 077dd923..5739d0b9 100644 --- a/backend/src/guard/jwt-auth.guard.ts +++ b/backend/src/guard/jwt-auth.guard.ts @@ -1,33 +1,82 @@ -// guards/jwt-auth.guard.ts import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, + Logger, } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; import { JwtService } from '@nestjs/jwt'; +import { JwtCacheService } from 'src/jwt-cache/jwt-cache.service'; @Injectable() export class JWTAuthGuard implements CanActivate { - constructor(private readonly jwtService: JwtService) {} + private readonly logger = new Logger(JWTAuthGuard.name); + + constructor( + private readonly jwtService: JwtService, + private readonly jwtCacheService: JwtCacheService, + ) {} async canActivate(context: ExecutionContext): Promise { + this.logger.debug('Starting JWT authentication process'); + const gqlContext = GqlExecutionContext.create(context); const { req } = gqlContext.getContext(); + try { + const token = this.extractTokenFromHeader(req); + + const payload = await this.verifyToken(token); + + const isTokenValid = await this.jwtCacheService.isTokenStored(token); + if (!isTokenValid) { + throw new UnauthorizedException('Token has been invalidated'); + } + + req.user = payload; + + return true; + } catch (error) { + if (error instanceof UnauthorizedException) { + throw error; + } + this.logger.error('Authentication failed:', error); + throw new UnauthorizedException('Invalid authentication token'); + } + } + + private extractTokenFromHeader(req: any): string { const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - throw new UnauthorizedException('Authorization token is missing'); + + if (!authHeader) { + throw new UnauthorizedException('Authorization header is missing'); + } + + const [type, token] = authHeader.split(' '); + + if (type !== 'Bearer') { + throw new UnauthorizedException('Invalid authorization header format'); } - const token = authHeader.split(' ')[1]; + if (!token) { + throw new UnauthorizedException('Token is missing'); + } + + return token; + } + + private async verifyToken(token: string): Promise { try { - const payload = this.jwtService.verify(token); - req.user = payload; - return true; + return await this.jwtService.verifyAsync(token); } catch (error) { - throw new UnauthorizedException('Invalid token'); + if (error.name === 'TokenExpiredError') { + throw new UnauthorizedException('Token has expired'); + } + if (error.name === 'JsonWebTokenError') { + throw new UnauthorizedException('Invalid token'); + } + throw error; } } } diff --git a/backend/src/guard/roles.guard.ts b/backend/src/interceptor/roles.guard.ts similarity index 100% rename from backend/src/guard/roles.guard.ts rename to backend/src/interceptor/roles.guard.ts diff --git a/backend/src/jwt-cache/jwt-cache.module.ts b/backend/src/jwt-cache/jwt-cache.module.ts new file mode 100644 index 00000000..77f4accb --- /dev/null +++ b/backend/src/jwt-cache/jwt-cache.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { JwtCacheService } from './jwt-cache.service'; + +@Module({ + exports: [JwtCacheService], + providers: [JwtCacheService], +}) +export class JwtCacheModule {} diff --git a/backend/src/auth/jwt-cache.service.ts b/backend/src/jwt-cache/jwt-cache.service.ts similarity index 65% rename from backend/src/auth/jwt-cache.service.ts rename to backend/src/jwt-cache/jwt-cache.service.ts index 7ad1f30e..db0fb75e 100644 --- a/backend/src/auth/jwt-cache.service.ts +++ b/backend/src/jwt-cache/jwt-cache.service.ts @@ -10,36 +10,37 @@ import { Database } from 'sqlite3'; export class JwtCacheService implements OnModuleInit, OnModuleDestroy { private db: Database; private readonly logger = new Logger(JwtCacheService.name); + private cleanupInterval: NodeJS.Timeout; constructor() { this.db = new Database(':memory:'); - this.logger.log('JwtCacheService instantiated'); + this.logger.log('JwtCacheService instantiated with in-memory database'); } async onModuleInit() { this.logger.log('Initializing JwtCacheService'); await this.createTable(); - await this.clearTable(); + this.startCleanupTask(); this.logger.log('JwtCacheService initialized successfully'); } async onModuleDestroy() { this.logger.log('Destroying JwtCacheService'); - await this.clearTable(); + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + } await this.closeDatabase(); this.logger.log('JwtCacheService destroyed successfully'); } private createTable(): Promise { - this.logger.debug('Creating jwt_cache table'); return new Promise((resolve, reject) => { this.db.run( - ` - CREATE TABLE IF NOT EXISTS jwt_cache ( + `CREATE TABLE IF NOT EXISTS jwt_cache ( token TEXT PRIMARY KEY, - created_at INTEGER NOT NULL - ) - `, + created_at INTEGER NOT NULL, + expires_at INTEGER NOT NULL + )`, (err) => { if (err) { this.logger.error('Failed to create jwt_cache table', err.stack); @@ -53,23 +54,34 @@ export class JwtCacheService implements OnModuleInit, OnModuleDestroy { }); } - private clearTable(): Promise { - this.logger.debug('Clearing jwt_cache table'); + private startCleanupTask() { + const CLEANUP_INTERVAL = 5 * 60 * 1000; + this.cleanupInterval = setInterval(() => { + this.cleanupExpiredTokens().catch((err) => + this.logger.error('Failed to cleanup expired tokens', err), + ); + }, CLEANUP_INTERVAL); + } + + private cleanupExpiredTokens(): Promise { return new Promise((resolve, reject) => { - this.db.run('DELETE FROM jwt_cache', (err) => { - if (err) { - this.logger.error('Failed to clear jwt_cache table', err.stack); - reject(err); - } else { - this.logger.debug('jwt_cache table cleared successfully'); - resolve(); - } - }); + const now = Date.now(); + this.db.run( + 'DELETE FROM jwt_cache WHERE expires_at < ?', + [now], + (err) => { + if (err) { + this.logger.error('Failed to cleanup expired tokens', err.stack); + reject(err); + } else { + resolve(); + } + }, + ); }); } private closeDatabase(): Promise { - this.logger.debug('Closing database connection'); return new Promise((resolve, reject) => { this.db.close((err) => { if (err) { @@ -84,17 +96,18 @@ export class JwtCacheService implements OnModuleInit, OnModuleDestroy { } async storeToken(token: string): Promise { - this.logger.debug(`Storing token: ${token.substring(0, 10)}...`); + const now = Date.now(); + const expiresAt = now + 24 * 60 * 60 * 1000; + return new Promise((resolve, reject) => { this.db.run( - 'INSERT OR REPLACE INTO jwt_cache (token, created_at) VALUES (?, ?)', - [token, Date.now()], + 'INSERT OR REPLACE INTO jwt_cache (token, created_at, expires_at) VALUES (?, ?, ?)', + [token, now, expiresAt], (err) => { if (err) { this.logger.error('Failed to store token', err.stack); reject(err); } else { - this.logger.debug('Token stored successfully'); resolve(); } }, @@ -103,21 +116,17 @@ export class JwtCacheService implements OnModuleInit, OnModuleDestroy { } async isTokenStored(token: string): Promise { - this.logger.debug( - `Checking if token is stored: ${token.substring(0, 10)}...`, - ); return new Promise((resolve, reject) => { + const now = Date.now(); this.db.get( - 'SELECT token FROM jwt_cache WHERE token = ?', - [token], + 'SELECT token FROM jwt_cache WHERE token = ? AND expires_at > ?', + [token, now], (err, row) => { if (err) { this.logger.error('Failed to check token', err.stack); reject(err); } else { - const isStored = !!row; - this.logger.debug(`Token ${isStored ? 'is' : 'is not'} stored`); - resolve(isStored); + resolve(!!row); } }, ); @@ -131,7 +140,6 @@ export class JwtCacheService implements OnModuleInit, OnModuleDestroy { this.logger.error('Failed to remove token', err.stack); reject(err); } else { - this.logger.debug('Token removed successfully'); resolve(); } }); From 48ea5946e2b8cefa29fffb5e7d4e26bb506a159b Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Sun, 2 Feb 2025 19:49:30 -0600 Subject: [PATCH 2/3] feat: add PromptTool module, service, and resolver for project description enhancement --- backend/src/prompt-tool/prompt-tool.module.ts | 13 ++++++ .../src/prompt-tool/prompt-tool.resolver.ts | 14 ++++++ .../src/prompt-tool/prompt-tool.service.ts | 44 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 backend/src/prompt-tool/prompt-tool.module.ts create mode 100644 backend/src/prompt-tool/prompt-tool.resolver.ts create mode 100644 backend/src/prompt-tool/prompt-tool.service.ts diff --git a/backend/src/prompt-tool/prompt-tool.module.ts b/backend/src/prompt-tool/prompt-tool.module.ts new file mode 100644 index 00000000..5f8d2799 --- /dev/null +++ b/backend/src/prompt-tool/prompt-tool.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { ProjectModule } from '../project/project.module'; +import { PromptToolService } from './prompt-tool.service'; +import { PromptToolResolver } from './prompt-tool.resolver'; +import { JwtModule } from '@nestjs/jwt'; +import { AuthModule } from '../auth/auth.module'; +import { JwtCacheModule } from 'src/jwt-cache/jwt-cache.module'; + +@Module({ + imports: [ProjectModule, JwtCacheModule, AuthModule], + providers: [PromptToolResolver, PromptToolService], +}) +export class PromptToolModule {} diff --git a/backend/src/prompt-tool/prompt-tool.resolver.ts b/backend/src/prompt-tool/prompt-tool.resolver.ts new file mode 100644 index 00000000..278bf92c --- /dev/null +++ b/backend/src/prompt-tool/prompt-tool.resolver.ts @@ -0,0 +1,14 @@ +import { Resolver, Mutation, Args } from '@nestjs/graphql'; +import { JWTAuth } from '../decorator/jwt-auth.decorator'; +import { PromptToolService } from './prompt-tool.service'; + +@Resolver() +export class PromptToolResolver { + constructor(private readonly promptService: PromptToolService) {} + + @Mutation(() => String) + @JWTAuth() + async regenerateDescription(@Args('input') input: string): Promise { + return this.promptService.regenerateDescription(input); + } +} diff --git a/backend/src/prompt-tool/prompt-tool.service.ts b/backend/src/prompt-tool/prompt-tool.service.ts new file mode 100644 index 00000000..304cdd94 --- /dev/null +++ b/backend/src/prompt-tool/prompt-tool.service.ts @@ -0,0 +1,44 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Message, MessageRole } from 'src/chat/message.model'; +import { OpenAIModelProvider } from 'src/common/model-provider/openai-model-provider'; + +@Injectable() +export class PromptToolService { + private readonly logger = new Logger(PromptToolService.name); + private readonly model = OpenAIModelProvider.getInstance(); + + async regenerateDescription(description: string): Promise { + try { + const response = await this.model.chatSync({ + messages: [ + { + role: MessageRole.System, + content: `You help users expand their project descriptions by rewriting them from a first-person perspective. +Format requirements: +1. Always start with "I want to..." +2. Write as if the user is speaking +3. Add 1-2 relevant details while maintaining the original idea +4. Keep it conversational and natural +5. Maximum 2-3 sentences +6. Never add features that weren't implied in the original description +Example: +Input: "create a todo app" +Output: "I want to create a todo app where I can keep track of my daily tasks. I think it would be helpful if I could organize them by priority and due dates."`, + }, + { + role: MessageRole.User, + content: description, + }, + ], + }); + + this.logger.debug('Enhanced description generated'); + return response; + } catch (error) { + this.logger.error( + `Error generating enhanced description: ${error.message}`, + ); + throw new Error('Failed to enhance project description'); + } + } +} From 8d61782fe9aad1a1f20a235185988cff9f663e51 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 01:51:09 +0000 Subject: [PATCH 3/3] [autofix.ci] apply automated fixes --- backend/src/auth/auth.module.ts | 1 - backend/src/guard/project.guard.ts | 1 - backend/src/project/dto/project.input.ts | 2 -- backend/src/project/project.resolver.ts | 6 +----- backend/src/prompt-tool/prompt-tool.module.ts | 1 - backend/src/prompt-tool/prompt-tool.service.ts | 2 +- 6 files changed, 2 insertions(+), 11 deletions(-) diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index 13c46ac4..1f636342 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -7,7 +7,6 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { AuthService } from './auth.service'; import { User } from 'src/user/user.model'; import { AuthResolver } from './auth.resolver'; -import { JwtCacheService } from 'src/jwt-cache/jwt-cache.service'; import { JwtCacheModule } from 'src/jwt-cache/jwt-cache.module'; @Module({ diff --git a/backend/src/guard/project.guard.ts b/backend/src/guard/project.guard.ts index 6964fcca..a6dac748 100644 --- a/backend/src/guard/project.guard.ts +++ b/backend/src/guard/project.guard.ts @@ -9,7 +9,6 @@ import { GqlExecutionContext } from '@nestjs/graphql'; import { JwtService } from '@nestjs/jwt'; import { ProjectService } from '../project/project.service'; -import { Reflector } from '@nestjs/core'; @Injectable() export class ProjectGuard implements CanActivate { diff --git a/backend/src/project/dto/project.input.ts b/backend/src/project/dto/project.input.ts index 7bc63731..9f39fa3d 100644 --- a/backend/src/project/dto/project.input.ts +++ b/backend/src/project/dto/project.input.ts @@ -1,7 +1,5 @@ // DTOs for Project APIs import { InputType, Field, ID } from '@nestjs/graphql'; -import { ProjectPackages } from '../project-packages.model'; -import { Optional } from '@nestjs/common'; /** * @deprecated We don't need project upsert diff --git a/backend/src/project/project.resolver.ts b/backend/src/project/project.resolver.ts index 8ed1f973..d9f4b7f6 100644 --- a/backend/src/project/project.resolver.ts +++ b/backend/src/project/project.resolver.ts @@ -2,11 +2,7 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { ProjectService } from './project.service'; import { Project } from './project.model'; -import { - CreateProjectInput, - IsValidProjectInput, - UpsertProjectInput, -} from './dto/project.input'; +import { CreateProjectInput, IsValidProjectInput } from './dto/project.input'; import { UseGuards } from '@nestjs/common'; import { ProjectGuard } from '../guard/project.guard'; import { GetUserIdFromToken } from '../decorator/get-auth-token.decorator'; diff --git a/backend/src/prompt-tool/prompt-tool.module.ts b/backend/src/prompt-tool/prompt-tool.module.ts index 5f8d2799..e0ee1130 100644 --- a/backend/src/prompt-tool/prompt-tool.module.ts +++ b/backend/src/prompt-tool/prompt-tool.module.ts @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common'; import { ProjectModule } from '../project/project.module'; import { PromptToolService } from './prompt-tool.service'; import { PromptToolResolver } from './prompt-tool.resolver'; -import { JwtModule } from '@nestjs/jwt'; import { AuthModule } from '../auth/auth.module'; import { JwtCacheModule } from 'src/jwt-cache/jwt-cache.module'; diff --git a/backend/src/prompt-tool/prompt-tool.service.ts b/backend/src/prompt-tool/prompt-tool.service.ts index 304cdd94..66c87501 100644 --- a/backend/src/prompt-tool/prompt-tool.service.ts +++ b/backend/src/prompt-tool/prompt-tool.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; -import { Message, MessageRole } from 'src/chat/message.model'; +import { MessageRole } from 'src/chat/message.model'; import { OpenAIModelProvider } from 'src/common/model-provider/openai-model-provider'; @Injectable()