diff --git a/src/api/api.controller.ts b/src/api/api.controller.ts index 467ff31..4403726 100644 --- a/src/api/api.controller.ts +++ b/src/api/api.controller.ts @@ -10,6 +10,10 @@ export class ApiController { @Post("token/generate") generateToken(@Body() body: { id: string }) { + if (!body.id) { + throw new Error("Project ID is required"); + } + return this.apiService.generateToken(body.id); } diff --git a/src/api/api.module.ts b/src/api/api.module.ts index dd97f9e..f0c5a48 100644 --- a/src/api/api.module.ts +++ b/src/api/api.module.ts @@ -1,4 +1,4 @@ -import { Module } from "@nestjs/common"; +import { forwardRef, Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; import { Token } from "./entities/token.entity"; import { ProjectModule } from "../project/project.module"; @@ -6,11 +6,19 @@ import { ApiService } from "./api.service"; import { ApiController } from "./api.controller"; import { Project } from "../project/entities/project.entity"; import { ApiTokenGuard } from "./guards/api-token.guard"; +import { RedisModule } from "src/redis/redis.module"; +import { QueryGuard } from "./guards/query.guard"; +import { QueryModule } from "src/query/query.module"; @Module({ - imports: [ProjectModule, TypeOrmModule.forFeature([Token, Project])], + imports: [ + forwardRef(() => RedisModule), + forwardRef(() => QueryModule), + ProjectModule, + TypeOrmModule.forFeature([Token, Project]), + ], controllers: [ApiController], - providers: [ApiService, ApiTokenGuard], - exports: [ApiTokenGuard, TypeOrmModule], + providers: [ApiService, ApiTokenGuard, QueryGuard], + exports: [ApiTokenGuard, ApiService, TypeOrmModule], }) export class ApiModule {} diff --git a/src/api/api.service.ts b/src/api/api.service.ts index 1b6ce01..aea914c 100644 --- a/src/api/api.service.ts +++ b/src/api/api.service.ts @@ -1,15 +1,17 @@ -import { Injectable } from "@nestjs/common"; +import { Inject, Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Token } from "./entities/token.entity"; import { Repository } from "typeorm"; import { Project } from "../project/entities/project.entity"; +import { RedisClient } from "src/redis/redis.service"; @Injectable() export class ApiService { constructor( @InjectRepository(Token) private readonly tokenRepository: Repository, - + @Inject(RedisClient) + private readonly redisClient: RedisClient, @InjectRepository(Project) private readonly projectRepository: Repository ) {} @@ -27,6 +29,25 @@ export class ApiService { return this.tokenRepository.save(token); } + async getTokenDetails(tokenString: string) { + const cached = await this.redisClient.get(`token_${tokenString}`); + + if (cached) { + return cached; + } + + const token = await this.tokenRepository.findOne({ + where: { token: tokenString }, + relations: ["project"], + }); + + if (token) { + await this.redisClient.set(`token_${token.token}`, token, 300); + } + + return token; + } + async revokeToken(tokenString: string) { const token = await this.tokenRepository.findOne({ where: { token: tokenString }, @@ -36,6 +57,8 @@ export class ApiService { throw new Error("Token not found"); } + await this.redisClient.del(`token_${tokenString}`); + token.isActive = false; return this.tokenRepository.save(token); } diff --git a/src/api/entities/token.entity.ts b/src/api/entities/token.entity.ts index e8cb483..1b1cc87 100644 --- a/src/api/entities/token.entity.ts +++ b/src/api/entities/token.entity.ts @@ -6,7 +6,7 @@ export class Token { @PrimaryGeneratedColumn("uuid") token: string; - @Column({ type: "tinyint", default: 0 }) + @Column({ type: "tinyint", default: 1 }) isActive: boolean; @ManyToOne(() => Project, (project) => project.apiTokens) diff --git a/src/api/guards/api-token.guard.ts b/src/api/guards/api-token.guard.ts index b18781b..b403328 100644 --- a/src/api/guards/api-token.guard.ts +++ b/src/api/guards/api-token.guard.ts @@ -1,34 +1,28 @@ import { CanActivate, ExecutionContext, + Inject, Injectable, UnauthorizedException, } from "@nestjs/common"; -import { InjectRepository } from "@nestjs/typeorm"; -import { Repository } from "typeorm"; -import { Token } from "../entities/token.entity"; +import { ApiService } from "../api.service"; @Injectable() export class ApiTokenGuard implements CanActivate { constructor( - @InjectRepository(Token) - private readonly tokenRepository: Repository + @Inject(ApiService) + private readonly apiService: ApiService ) {} async canActivate(context: ExecutionContext): Promise { - return true; - const request = context.switchToHttp().getRequest(); - const token = request.params?.token || request.headers?.["x-api-token"]; + const token = request.headers?.["x-api-token"]; if (!token) { throw new UnauthorizedException("API token is required"); } - const tokenEntity = await this.tokenRepository.findOne({ - where: { token }, - relations: ["project"], - }); + const tokenEntity = await this.apiService.getTokenDetails(token); if (!tokenEntity) { throw new UnauthorizedException("Invalid API token"); @@ -38,6 +32,8 @@ export class ApiTokenGuard implements CanActivate { throw new UnauthorizedException("API token is inactive"); } + request.apiToken = tokenEntity; + return true; } } diff --git a/src/api/guards/query.guard.ts b/src/api/guards/query.guard.ts new file mode 100644 index 0000000..e68f163 --- /dev/null +++ b/src/api/guards/query.guard.ts @@ -0,0 +1,49 @@ +import { + CanActivate, + ExecutionContext, + Inject, + Injectable, + UnauthorizedException, +} from "@nestjs/common"; +import { QueryHandlerService } from "src/query/handler/query.handler.service"; + +@Injectable() +export class QueryGuard implements CanActivate { + constructor( + @Inject(QueryHandlerService) + private readonly queryHandlerService: QueryHandlerService + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const apiToken = request.apiToken; + + if (!apiToken || !apiToken.project) { + throw new UnauthorizedException("Project not found for the API token"); + } + + const queryId = request.params?.id; + + if (!queryId) { + throw new UnauthorizedException("Query ID is required"); + } + + const query = await this.queryHandlerService.getQueryById(queryId); + + if (!query) { + throw new UnauthorizedException("Query not found"); + } + + if (!query.isActive) { + throw new UnauthorizedException("Query is inactive"); + } + + if (query.project.id !== apiToken.project.id) { + throw new UnauthorizedException("You do not have access to this query"); + } + + request.query = query; + + return true; + } +} diff --git a/src/databaseManager/database.manager.module.ts b/src/databaseManager/database.manager.module.ts index 406dbed..e55a39d 100644 --- a/src/databaseManager/database.manager.module.ts +++ b/src/databaseManager/database.manager.module.ts @@ -9,12 +9,14 @@ import { DatabaseManagerController } from "./database/database.manager.controlle import { DatabaseManagerService } from "./database/database.manager.service"; import { DatabaseNodeService } from "./databaseNode/database.node.service"; import { ApiModule } from "src/api/api.module"; +import { RedisModule } from "src/redis/redis.module"; @Module({ imports: [ forwardRef(() => ProjectModule), forwardRef(() => MigrationModule), forwardRef(() => ApiModule), + forwardRef(() => RedisModule), TypeOrmModule.forFeature([Database, DatabaseNode, Project]), ], controllers: [DatabaseManagerController], diff --git a/src/databaseManager/database/database.manager.service.ts b/src/databaseManager/database/database.manager.service.ts index 62466ce..b5331aa 100644 --- a/src/databaseManager/database/database.manager.service.ts +++ b/src/databaseManager/database/database.manager.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@nestjs/common"; +import { Inject, Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Database } from "../entities/database.entity"; import { Repository } from "typeorm"; @@ -6,6 +6,7 @@ import { ProjectService } from "src/project/project.service"; import { DatabaseEncryptionService } from "../database.encryption.service"; import { DatabaseNodeService } from "../databaseNode/database.node.service"; import * as mysql from "mysql2/promise"; +import { RedisClient } from "src/redis/redis.service"; @Injectable() export class DatabaseManagerService extends DatabaseEncryptionService { @@ -13,7 +14,9 @@ export class DatabaseManagerService extends DatabaseEncryptionService { @InjectRepository(Database) private databaseRepository: Repository, private readonly projectService: ProjectService, - private readonly databaseNodeService: DatabaseNodeService + private readonly databaseNodeService: DatabaseNodeService, + @Inject(RedisClient) + private readonly redisClient: RedisClient ) { super(); } @@ -64,6 +67,14 @@ export class DatabaseManagerService extends DatabaseEncryptionService { throw new Error("Project not found"); } + const cached = await this.redisClient.get( + `db_conn_opts_${projectId}_${queryUser}` + ); + + if (cached) { + return cached; + } + const database = await this.databaseRepository.findOne({ where: { project: { id: project.id } }, relations: ["node"], @@ -73,7 +84,7 @@ export class DatabaseManagerService extends DatabaseEncryptionService { throw new Error("Database not found"); } - return { + const connectionOptions = { host: database.node.host, port: database.node.port, user: queryUser ? database.q_username : database.c_username, @@ -82,6 +93,14 @@ export class DatabaseManagerService extends DatabaseEncryptionService { idleTimeout: 150e3, connectTimeout: 2e3, }; + + await this.redisClient.set( + `db_conn_opts_${projectId}_${queryUser}`, + connectionOptions, + 300 + ); + + return connectionOptions; } async createDatabase(projectId: string): Promise { diff --git a/src/project/project.module.ts b/src/project/project.module.ts index a31a948..855d3b9 100644 --- a/src/project/project.module.ts +++ b/src/project/project.module.ts @@ -4,9 +4,14 @@ import { Project } from "./entities/project.entity"; import { ProjectService } from "./project.service"; import { ProjectController } from "./project.controller"; import { ApiModule } from "src/api/api.module"; +import { RedisModule } from "src/redis/redis.module"; @Module({ - imports: [forwardRef(() => ApiModule), TypeOrmModule.forFeature([Project])], + imports: [ + forwardRef(() => ApiModule), + forwardRef(() => RedisModule), + TypeOrmModule.forFeature([Project]), + ], controllers: [ProjectController], providers: [ProjectService], exports: [ProjectService], diff --git a/src/project/project.service.ts b/src/project/project.service.ts index 7c8a9f5..a1069a4 100644 --- a/src/project/project.service.ts +++ b/src/project/project.service.ts @@ -1,13 +1,16 @@ -import { Injectable } from "@nestjs/common"; +import { Inject, Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { Project } from "./entities/project.entity"; +import { RedisClient } from "src/redis/redis.service"; @Injectable() export class ProjectService { constructor( @InjectRepository(Project) - private readonly projectRepository: Repository + private readonly projectRepository: Repository, + @Inject(RedisClient) + private readonly redisClient: RedisClient ) {} create(name: string) { @@ -15,17 +18,33 @@ export class ProjectService { return this.projectRepository.save(project); } - findById(id: string) { - return this.projectRepository.findOne({ where: { id: id } }); + async findById(id: string) { + const cached = await this.redisClient.get(`project_${id}`); + + if (cached) { + return cached; + } + + const project = await this.projectRepository.findOne({ where: { id: id } }); + + if (project) { + await this.redisClient.set(`project_${id}`, project, 300); + } + + return project; } - updateDatabase(projectId: string, databaseId: string) { + async updateDatabase(projectId: string, databaseId: string) { + await this.redisClient.del(`project_${projectId}`); + return this.projectRepository.update(projectId, { database: { id: databaseId }, }); } - updateRedisNode(projectId: string, redisNodeId: { id: string }[]) { + async updateRedisNode(projectId: string, redisNodeId: { id: string }[]) { + await this.redisClient.del(`project_${projectId}`); + return this.projectRepository.update(projectId, { redisNodes: redisNodeId, }); diff --git a/src/query/base/base-query.controller.ts b/src/query/base/base-query.controller.ts index 00add3c..dd42cfd 100644 --- a/src/query/base/base-query.controller.ts +++ b/src/query/base/base-query.controller.ts @@ -11,6 +11,7 @@ import { Response } from "express"; import { QueryHandlerService } from "../handler/query.handler.service"; import { ApiTokenGuard } from "src/api/guards/api-token.guard"; import { QueryExecuterService } from "../executer/query.executer.service"; +import { QueryGuard } from "src/api/guards/query.guard"; @UseGuards(ApiTokenGuard) export abstract class BaseQueryController { @@ -31,22 +32,24 @@ export abstract class BaseQueryController { } @Post("update/:id") + @UseGuards(QueryGuard) async updateQuery( @Body() updateData: Partial<{ source: string }>, - @Inject("id") id: string + @Param("id") id: string ) { return this.queryHandlerService.updateQuery(id, updateData); } - @Post("/run/:token") + @Post("/run/:id") + @UseGuards(QueryGuard) async runQuery( - @Param("token") token: string, + @Param("id") id: string, @Body() query: Record, @Headers() headers: Record, @Res() res: Response ) { const queryResult = await this.queryExecuterService.runQueryQueued( - token, + id, query, headers ); diff --git a/src/query/executer/query.executer.service.ts b/src/query/executer/query.executer.service.ts index c96d617..349914e 100644 --- a/src/query/executer/query.executer.service.ts +++ b/src/query/executer/query.executer.service.ts @@ -14,7 +14,7 @@ import { DatabaseManagerService } from "src/databaseManager/database/database.ma import { InjectQueue } from "@nestjs/bullmq"; import { QUEUE_NAMES } from "src/queue/constants"; import { Queue, QueueEvents } from "bullmq"; -import { FunctionService } from "src/project/function/function.service"; +import { FunctionService } from "src/query/function/function.service"; @Injectable() export class QueryExecuterService { diff --git a/src/project/function/function.controller.ts b/src/query/function/function.controller.ts similarity index 100% rename from src/project/function/function.controller.ts rename to src/query/function/function.controller.ts diff --git a/src/project/function/function.service.ts b/src/query/function/function.service.ts similarity index 96% rename from src/project/function/function.service.ts rename to src/query/function/function.service.ts index b30cb99..99f5b49 100644 --- a/src/project/function/function.service.ts +++ b/src/query/function/function.service.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { FunctionEntity } from "src/query/entities/function.entity"; import { In, Repository } from "typeorm"; -import { ProjectService } from "../project.service"; +import { ProjectService } from "../../project/project.service"; @Injectable() export class FunctionService { diff --git a/src/query/handler/query.handler.service.ts b/src/query/handler/query.handler.service.ts index 445dc2a..86c46a3 100644 --- a/src/query/handler/query.handler.service.ts +++ b/src/query/handler/query.handler.service.ts @@ -3,6 +3,7 @@ import { Repository } from "typeorm"; import { Query } from "../entities/query.entity"; import { InjectRepository } from "@nestjs/typeorm"; import { ProjectService } from "src/project/project.service"; +import { RedisClient } from "src/redis/redis.service"; @Injectable() export class QueryHandlerService { @@ -10,7 +11,9 @@ export class QueryHandlerService { @InjectRepository(Query) private readonly queryRepository: Repository, @Inject(ProjectService) - private readonly projectService: ProjectService + private readonly projectService: ProjectService, + @Inject(RedisClient) + private readonly redisClient: RedisClient ) {} async createQuery( @@ -34,6 +37,25 @@ export class QueryHandlerService { return query; } + async getQueryById(id: string) { + const cached = await this.redisClient.get(`query_${id}`); + + if (cached) { + return cached; + } + + const query = await this.queryRepository.findOne({ + where: { id }, + relations: ["project"], + }); + + if (query) { + await this.redisClient.set(`query_${id}`, query, 300); + } + + return query; + } + async updateQuery(id: string, updateData: Partial) { const query = await this.queryRepository.findOne({ where: { id } }); @@ -41,6 +63,8 @@ export class QueryHandlerService { throw new Error("Query not found"); } + await this.redisClient.del(`query_${id}`); + Object.assign(query, updateData); return this.queryRepository.save(query); } diff --git a/src/query/query.module.ts b/src/query/query.module.ts index 28ad62a..4917a4f 100644 --- a/src/query/query.module.ts +++ b/src/query/query.module.ts @@ -10,9 +10,10 @@ import { CommandController } from "./command/command.controller"; import { ApiModule } from "src/api/api.module"; import { QueueModule } from "src/queue/queue.module"; import { FunctionEntity } from "./entities/function.entity"; -import { FunctionService } from "src/project/function/function.service"; -import { FunctionController } from "src/project/function/function.controller"; +import { FunctionService } from "src/query/function/function.service"; +import { FunctionController } from "src/query/function/function.controller"; import { RedisManagerModule } from "src/redisManager/redisManager.module"; +import { RedisModule } from "src/redis/redis.module"; @Module({ imports: [ @@ -20,11 +21,12 @@ import { RedisManagerModule } from "src/redisManager/redisManager.module"; forwardRef(() => DatabaseManagerModule), forwardRef(() => ApiModule), forwardRef(() => QueueModule), + forwardRef(() => RedisModule), forwardRef(() => RedisManagerModule), TypeOrmModule.forFeature([Query, FunctionEntity]), ], controllers: [QueryController, CommandController, FunctionController], providers: [QueryExecuterService, QueryHandlerService, FunctionService], - exports: [QueryExecuterService, TypeOrmModule], + exports: [QueryExecuterService, TypeOrmModule, QueryHandlerService], }) export class QueryModule {} diff --git a/src/redis/redis.service.ts b/src/redis/redis.service.ts index b8e8af1..d6f4268 100644 --- a/src/redis/redis.service.ts +++ b/src/redis/redis.service.ts @@ -12,7 +12,7 @@ export class RedisClient { async set( key: string, - value: string, + value: any, expireInSeconds?: number ): Promise<"OK" | null> { if (!this.redis) { @@ -20,18 +20,23 @@ export class RedisClient { } if (expireInSeconds) { - return await this.redis.set(key, value, "EX", expireInSeconds); + return await this.redis.set( + key, + JSON.stringify(value), + "EX", + expireInSeconds + ); } - return await this.redis.set(key, value); + return await this.redis.set(key, JSON.stringify(value)); } - async get(key: string): Promise { + async get(key: string): Promise { if (!this.redis) { return null; } - return await this.redis.get(key); + return JSON.parse(await this.redis.get(key)); } async del(key: string): Promise { diff --git a/tests/base/case1.ts b/tests/base/case1.ts index c2f0198..d637724 100644 --- a/tests/base/case1.ts +++ b/tests/base/case1.ts @@ -10,33 +10,33 @@ import * as path from "path"; (async () => { try { - const node = await createDatabaseNode("localhost", 3306, "root", "root"); + // const node = await createDatabaseNode("localhost", 3306, "root", "root"); - console.log("Database node created:", node); + // console.log("Database node created:", node); - const project = await createProject("Test Project"); + // const project = await createProject("Test Project"); - console.log("Project created:", project); + // console.log("Project created:", project); - const db = await createDatabase(project.id, node.id); + // const db = await createDatabase(project.id, node.id); - console.log("Database created:", db); + // console.log("Database created:", db); - const migration = await createMigration( - db.id, - "CREATE TABLE `test` (id INT AUTO_INCREMENT PRIMARY KEY, col1 VARCHAR(255))", - "DROP TABLE `test`" - ); + // const migration = await createMigration( + // db.id, + // "CREATE TABLE `test` (id INT AUTO_INCREMENT PRIMARY KEY, col1 VARCHAR(255))", + // "DROP TABLE `test`" + // ); - console.log("Migration created:", migration); + // console.log("Migration created:", migration); - const migrationResult = await databaseMigrationUp(db.id); + // const migrationResult = await databaseMigrationUp(db.id); - console.log("Migrations applied:", migrationResult); + // console.log("Migrations applied:", migrationResult); const payloadPath = path.join(__dirname, "case1-payload.js"); const query = await createQuery( - project, + { token: "04c38f93-f2fb-4d2c-a8e2-791effa35239" }, fs.readFileSync(payloadPath, { encoding: "utf-8" }) ); diff --git a/tests/functions/createQuery.ts b/tests/functions/createQuery.ts index 5c87a2c..a1629ca 100644 --- a/tests/functions/createQuery.ts +++ b/tests/functions/createQuery.ts @@ -3,10 +3,14 @@ import { config } from "../config"; export default async (project: { token: string }, source: string) => { try { - const response = await axios.post(`${config.url}/query/create`, { - source, - projectToken: project.token, - }); + const response = await axios.post( + `${config.url}/query/create`, + { + source, + projectToken: project.token, + }, + { headers: { "x-api-token": "efbeccd6-dde1-47dc-b3aa-4fbd773d5429" } } + ); return response.data; } catch (error) { console.error("Error creating query:", error); diff --git a/tests/functions/runQuery.ts b/tests/functions/runQuery.ts index 4087a95..8798ca8 100644 --- a/tests/functions/runQuery.ts +++ b/tests/functions/runQuery.ts @@ -5,7 +5,8 @@ export default async (token: string, queryData: Record) => { try { const response = await axios.post( `${config.url}/query/run/${token}`, - queryData + queryData, + { headers: { "x-api-token": "efbeccd6-dde1-47dc-b3aa-4fbd773d5429" } } ); return response;