From 4c927142975054660344e640bdf8ba2962a77459 Mon Sep 17 00:00:00 2001 From: aXenDeveloper Date: Sun, 29 Dec 2024 23:29:40 +0100 Subject: [PATCH 1/2] feat(backend): Add catch all errors using app filter --- apps/backend/src/app.module.ts | 14 ++--- apps/backend/src/main.ts | 7 ++- packages/backend/src/database/schema/logs.ts | 11 ++++ .../backend/src/helpers/helpers.module.ts | 6 ++ packages/backend/src/main.ts | 11 +++- .../src/utils/all-exceptions.filter.ts | 57 +++++++++++++++++++ .../templates/basic/apps/backend/src/main.ts | 7 ++- 7 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 packages/backend/src/utils/all-exceptions.filter.ts diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index cad33780f..f8ae7b2c1 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -2,7 +2,7 @@ import { createGoogleGenerativeAI } from '@ai-sdk/google'; import { CacheModule } from '@nestjs/cache-manager'; import { Module } from '@nestjs/common'; // import { emailSMTP } from 'vitnode-backend-email-smtp'; -import { emailResend } from 'vitnode-backend-email-resend'; +// import { emailResend } from 'vitnode-backend-email-resend'; import { VitNodeCoreModule } from 'vitnode-backend/app.module'; import { DATABASE_ENVS, schemaDatabase } from './database/config'; @@ -23,13 +23,13 @@ const google = createGoogleGenerativeAI({ ai: google('gemini-2.0-flash-exp'), captcha: { type: 'cloudflare_turnstile', - secret_key: '0x4AAAAAAA32f41qx12_EeCCnJ4mQ6kV6w4', - site_key: '0x4AAAAAAA32f1b5Xe26DZHP', + secret_key: '', + site_key: '', }, - email: emailResend({ - api_key: process.env.EMAIL_RESEND_API_KEY, - from: process.env.EMAIL_RESEND_FROM, - }), + // email: emailResend({ + // api_key: process.env.EMAIL_RESEND_API_KEY, + // from: process.env.EMAIL_RESEND_FROM, + // }), // email: emailSMTP({ // host: process.env.EMAIL_SMTP_HOST, // port: process.env.EMAIL_SMTP_PORT, diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 339e45e88..1e544fb3a 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -1,11 +1,14 @@ import { INestApplication } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; -import { nestjsMainApp } from 'vitnode-backend/main'; +import { nestFactoryOptions, nestjsMainApp } from 'vitnode-backend/main'; import { AppModule } from './app.module'; async function bootstrap() { - const app: INestApplication = await NestFactory.create(AppModule); + const app: INestApplication = await NestFactory.create( + AppModule, + nestFactoryOptions, + ); void nestjsMainApp(app, {}); } diff --git a/packages/backend/src/database/schema/logs.ts b/packages/backend/src/database/schema/logs.ts index 960b82a33..a6a954c5e 100644 --- a/packages/backend/src/database/schema/logs.ts +++ b/packages/backend/src/database/schema/logs.ts @@ -1,5 +1,16 @@ import { pgTable } from 'drizzle-orm/pg-core'; +export const core_logs = pgTable('core_logs', t => ({ + id: t.serial().primaryKey(), + name: t.varchar({ length: 255 }).notNull(), + message: t.text().notNull(), + status: t.smallint().notNull(), + created_at: t.timestamp().notNull().defaultNow(), + headers: t.jsonb(), + method: t.varchar({ length: 10 }), + url: t.varchar({ length: 255 }), +})); + export const core_logs_email = pgTable('core_logs_email', t => ({ id: t.serial().primaryKey(), to: t.varchar({ length: 255 }).notNull(), diff --git a/packages/backend/src/helpers/helpers.module.ts b/packages/backend/src/helpers/helpers.module.ts index eeed4d7d6..d83ed1ff6 100644 --- a/packages/backend/src/helpers/helpers.module.ts +++ b/packages/backend/src/helpers/helpers.module.ts @@ -1,4 +1,5 @@ import { InternalAuthService } from '@/helpers/auth/internal_auth.service'; +import { AllExceptionsFilter } from '@/utils/all-exceptions.filter'; import { DynamicModule, Global, @@ -6,6 +7,7 @@ import { HttpStatus, Module, } from '@nestjs/common'; +import { APP_FILTER } from '@nestjs/core'; import { LanguageModel } from 'ai'; import { AiHelperService } from './ai.service'; @@ -86,6 +88,10 @@ export class GlobalHelpersModule { SSOAuthHelper, ConfigHelperService, AiHelperService, + { + provide: APP_FILTER, + useClass: AllExceptionsFilter, + }, ], exports: [ EmailHelperService, diff --git a/packages/backend/src/main.ts b/packages/backend/src/main.ts index b09162d85..b62070f3f 100644 --- a/packages/backend/src/main.ts +++ b/packages/backend/src/main.ts @@ -1,5 +1,9 @@ /* eslint-disable no-console */ -import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { + INestApplication, + NestApplicationOptions, + ValidationPipe, +} from '@nestjs/common'; import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import cookieParser from 'cookie-parser'; @@ -77,3 +81,8 @@ export const nestjsMainApp = async (app: INestApplication, options?: Args) => { ); }); }; + +export const nestFactoryOptions: NestApplicationOptions = { + rawBody: true, + bodyParser: true, +}; diff --git a/packages/backend/src/utils/all-exceptions.filter.ts b/packages/backend/src/utils/all-exceptions.filter.ts new file mode 100644 index 000000000..c906a08db --- /dev/null +++ b/packages/backend/src/utils/all-exceptions.filter.ts @@ -0,0 +1,57 @@ +import { core_logs } from '@/database/schema/logs'; +import { + ArgumentsHost, + Catch, + HttpException, + Injectable, +} from '@nestjs/common'; +import { BaseExceptionFilter } from '@nestjs/core'; + +import { InternalDatabaseService } from './database/internal_database.service'; + +@Catch() +@Injectable() +export class AllExceptionsFilter extends BaseExceptionFilter { + constructor(private readonly databaseService: InternalDatabaseService) { + super(); + } + + async catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const request = ctx.getRequest(); + + if (!(exception instanceof HttpException)) { + const unCatchError = exception as Error; + + await this.databaseService.db.insert(core_logs).values({ + name: 'Internal Server Error', + message: unCatchError.message, + status: 500, + headers: JSON.parse(JSON.stringify(request.headers)), + method: request.method, + url: request.url, + }); + + super.catch(exception, host); + + return; + } + + if (!exception.message.includes('InternalServerErrorException')) { + super.catch(exception, host); + + return; + } + + await this.databaseService.db.insert(core_logs).values({ + name: 'InternalServerErrorException', + message: exception.message, + status: exception.getStatus(), + headers: JSON.parse(JSON.stringify(request.headers)), + method: request.method, + url: request.url, + }); + + super.catch(exception, host); + } +} diff --git a/packages/create-vitnode-app/templates/basic/apps/backend/src/main.ts b/packages/create-vitnode-app/templates/basic/apps/backend/src/main.ts index 339e45e88..1e544fb3a 100644 --- a/packages/create-vitnode-app/templates/basic/apps/backend/src/main.ts +++ b/packages/create-vitnode-app/templates/basic/apps/backend/src/main.ts @@ -1,11 +1,14 @@ import { INestApplication } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; -import { nestjsMainApp } from 'vitnode-backend/main'; +import { nestFactoryOptions, nestjsMainApp } from 'vitnode-backend/main'; import { AppModule } from './app.module'; async function bootstrap() { - const app: INestApplication = await NestFactory.create(AppModule); + const app: INestApplication = await NestFactory.create( + AppModule, + nestFactoryOptions, + ); void nestjsMainApp(app, {}); } From 5d57765dd34e7ec100d543105f35938d609fa8e7 Mon Sep 17 00:00:00 2001 From: aXenDeveloper Date: Mon, 30 Dec 2024 12:54:34 +0100 Subject: [PATCH 2/2] feat(frontend): Add error logs inside diagnostic tools in AdminCP and connect with API --- apps/backend/src/main.ts | 8 +- apps/frontend/src/plugins/admin/langs/en.json | 12 ++- .../backend/src/core/admin/admin.module.ts | 2 + .../src/core/admin/logs/logs.controller.ts | 24 ++++++ .../src/core/admin/logs/logs.module.ts | 10 +++ .../core/admin/logs/service/show.service.ts | 40 ++++++++++ packages/backend/src/database/schema/logs.ts | 6 +- .../src/components/ui/alert-dialog.tsx | 2 +- packages/frontend/src/components/ui/sheet.tsx | 2 +- .../core/diagnostic/diagnostic-tools-view.tsx | 17 ++++- .../core/diagnostic/logs/actions/actions.tsx | 11 +++ .../core/diagnostic/logs/actions/more.tsx | 75 +++++++++++++++++++ .../views/core/diagnostic/logs/content.tsx | 64 ++++++++++++++++ .../logs/logs-diagnostic-tools-view.tsx | 33 ++++++++ .../views/admin/views/dynamic-admin-view.tsx | 2 +- packages/shared/src/admin/logs.dto.ts | 36 +++++++++ 16 files changed, 330 insertions(+), 14 deletions(-) create mode 100644 packages/backend/src/core/admin/logs/logs.controller.ts create mode 100644 packages/backend/src/core/admin/logs/logs.module.ts create mode 100644 packages/backend/src/core/admin/logs/service/show.service.ts create mode 100644 packages/frontend/src/views/admin/views/core/diagnostic/logs/actions/actions.tsx create mode 100644 packages/frontend/src/views/admin/views/core/diagnostic/logs/actions/more.tsx create mode 100644 packages/frontend/src/views/admin/views/core/diagnostic/logs/content.tsx create mode 100644 packages/frontend/src/views/admin/views/core/diagnostic/logs/logs-diagnostic-tools-view.tsx create mode 100644 packages/shared/src/admin/logs.dto.ts diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 1e544fb3a..56d3d5b44 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -1,14 +1,12 @@ -import { INestApplication } from '@nestjs/common'; +import { INestApplication, NestApplicationOptions } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { nestFactoryOptions, nestjsMainApp } from 'vitnode-backend/main'; import { AppModule } from './app.module'; async function bootstrap() { - const app: INestApplication = await NestFactory.create( - AppModule, - nestFactoryOptions, - ); + const options: NestApplicationOptions = nestFactoryOptions; + const app: INestApplication = await NestFactory.create(AppModule, options); void nestjsMainApp(app, {}); } diff --git a/apps/frontend/src/plugins/admin/langs/en.json b/apps/frontend/src/plugins/admin/langs/en.json index 53251064f..24e1dba3b 100644 --- a/apps/frontend/src/plugins/admin/langs/en.json +++ b/apps/frontend/src/plugins/admin/langs/en.json @@ -83,7 +83,17 @@ }, "enable": "Enable", "disable": "Disable", - "how_to_enable": "How to enable?" + "how_to_enable": "How to enable?", + "error_logs": { + "title": "Error Logs", + "desc": "If you have any issues with your website, you can check the error logs here.", + "name": "Name", + "message": "Message", + "created": "Created", + "more_info": "More Info", + "url": "URL", + "headers": "Headers" + } }, "email": { "hello": "Hello" diff --git a/packages/backend/src/core/admin/admin.module.ts b/packages/backend/src/core/admin/admin.module.ts index 62d555f3a..db660d417 100644 --- a/packages/backend/src/core/admin/admin.module.ts +++ b/packages/backend/src/core/admin/admin.module.ts @@ -4,6 +4,7 @@ import { AdvancedAdminModule } from './advanced/advanced.module'; import { AuthAdminModule } from './auth/auth.module'; import { DashboardAdminModule } from './dashboard/dashboard.module'; import { LanguagesAdminModule } from './languages/languages.module'; +import { LogsAdminModule } from './logs/logs.module'; import { MembersAdminModule } from './members/members.module'; import { PluginsAdminModule } from './plugins/plugins.module'; import { SettingsAdminModule } from './settings/settings.module'; @@ -19,6 +20,7 @@ import { StylesAdminModule } from './styles/styles.module'; StylesAdminModule, AdvancedAdminModule, DashboardAdminModule, + LogsAdminModule, ], }) export class AdminModule {} diff --git a/packages/backend/src/core/admin/logs/logs.controller.ts b/packages/backend/src/core/admin/logs/logs.controller.ts new file mode 100644 index 000000000..eee3c8ab7 --- /dev/null +++ b/packages/backend/src/core/admin/logs/logs.controller.ts @@ -0,0 +1,24 @@ +import { Controllers } from '@/helpers/controller.decorator'; +import { Get, Query } from '@nestjs/common'; +import { ApiOkResponse } from '@nestjs/swagger'; +import { + ShowLogsAdminObj, + ShowLogsAdminQuery, +} from 'vitnode-shared/admin/logs.dto'; + +import { ShowLogsAdminService } from './service/show.service'; + +@Controllers({ + plugin_name: 'Core', + plugin_code: 'logs', + isAdmin: true, +}) +export class LogsAdminController { + constructor(private readonly showLogsAdminService: ShowLogsAdminService) {} + + @ApiOkResponse({ type: ShowLogsAdminObj, description: 'Show logs' }) + @Get() + async show(@Query() query: ShowLogsAdminQuery): Promise { + return await this.showLogsAdminService.show(query); + } +} diff --git a/packages/backend/src/core/admin/logs/logs.module.ts b/packages/backend/src/core/admin/logs/logs.module.ts new file mode 100644 index 000000000..75519359e --- /dev/null +++ b/packages/backend/src/core/admin/logs/logs.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { LogsAdminController } from './logs.controller'; +import { ShowLogsAdminService } from './service/show.service'; + +@Module({ + providers: [ShowLogsAdminService], + controllers: [LogsAdminController], +}) +export class LogsAdminModule {} diff --git a/packages/backend/src/core/admin/logs/service/show.service.ts b/packages/backend/src/core/admin/logs/service/show.service.ts new file mode 100644 index 000000000..5fb0f4a30 --- /dev/null +++ b/packages/backend/src/core/admin/logs/service/show.service.ts @@ -0,0 +1,40 @@ +import { core_logs } from '@/database/schema/logs'; +import { InternalDatabaseService } from '@/utils/database/internal_database.service'; +import { Injectable } from '@nestjs/common'; +import { + ShowLogsAdminObj, + ShowLogsAdminQuery, +} from 'vitnode-shared/admin/logs.dto'; +import { SortDirectionEnum } from 'vitnode-shared/utils/pagination.enum'; + +@Injectable() +export class ShowLogsAdminService { + constructor(private readonly databaseService: InternalDatabaseService) {} + + async show({ + cursor, + first, + last, + }: ShowLogsAdminQuery): Promise { + const pagination = await this.databaseService.paginationCursor({ + cursor, + first, + last, + database: core_logs, + defaultSortBy: { + direction: SortDirectionEnum.desc, + column: 'created_at', + }, + query: async args => + await this.databaseService.db.query.core_logs.findMany(args), + }); + + return { + ...pagination, + edges: pagination.edges.map(edge => ({ + ...edge, + headers: JSON.stringify(edge.headers), + })), + }; + } +} diff --git a/packages/backend/src/database/schema/logs.ts b/packages/backend/src/database/schema/logs.ts index a6a954c5e..e7ed826cb 100644 --- a/packages/backend/src/database/schema/logs.ts +++ b/packages/backend/src/database/schema/logs.ts @@ -6,9 +6,9 @@ export const core_logs = pgTable('core_logs', t => ({ message: t.text().notNull(), status: t.smallint().notNull(), created_at: t.timestamp().notNull().defaultNow(), - headers: t.jsonb(), - method: t.varchar({ length: 10 }), - url: t.varchar({ length: 255 }), + headers: t.jsonb().notNull(), + method: t.varchar({ length: 10 }).notNull(), + url: t.varchar({ length: 255 }).notNull(), })); export const core_logs_email = pgTable('core_logs_email', t => ({ diff --git a/packages/frontend/src/components/ui/alert-dialog.tsx b/packages/frontend/src/components/ui/alert-dialog.tsx index 42d907e47..ebe0b8020 100644 --- a/packages/frontend/src/components/ui/alert-dialog.tsx +++ b/packages/frontend/src/components/ui/alert-dialog.tsx @@ -80,7 +80,7 @@ const AlertDialogHeader = ({ }: React.HTMLAttributes) => (
) => (
=> { }; }; -export const DiagnosticToolsView = async () => { +export const DiagnosticToolsView = async ({ + searchParams, +}: { + searchParams: Promise>; +}) => { const perm = await checkAdminPermissionPage(permission); if (perm) return perm; const t = await getTranslations('admin.core.diagnostic'); @@ -68,7 +75,7 @@ export const DiagnosticToolsView = async () => { -
+
{quickLook.map(item => (
@@ -110,6 +117,12 @@ export const DiagnosticToolsView = async () => { ))}
+ + + + }> + + ); }; diff --git a/packages/frontend/src/views/admin/views/core/diagnostic/logs/actions/actions.tsx b/packages/frontend/src/views/admin/views/core/diagnostic/logs/actions/actions.tsx new file mode 100644 index 000000000..1019c9f40 --- /dev/null +++ b/packages/frontend/src/views/admin/views/core/diagnostic/logs/actions/actions.tsx @@ -0,0 +1,11 @@ +import { LogsAdminObj } from 'vitnode-shared/admin/logs.dto'; + +import { MoreActionsLogsDiagnosticTools } from './more'; + +export const ActionsLogsDiagnosticTools = (props: LogsAdminObj) => { + return ( + <> + + + ); +}; diff --git a/packages/frontend/src/views/admin/views/core/diagnostic/logs/actions/more.tsx b/packages/frontend/src/views/admin/views/core/diagnostic/logs/actions/more.tsx new file mode 100644 index 000000000..3ff089f9a --- /dev/null +++ b/packages/frontend/src/views/admin/views/core/diagnostic/logs/actions/more.tsx @@ -0,0 +1,75 @@ +import { DateFormat } from '@/components/date-format'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from '@/components/ui/sheet'; +import { Textarea } from '@/components/ui/textarea'; +import { TooltipWrapper } from '@/components/ui/tooltip'; +import { SearchIcon } from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import { LogsAdminObj } from 'vitnode-shared/admin/logs.dto'; + +export const MoreActionsLogsDiagnosticTools = ({ + name, + method, + url, + created_at, + headers, + message, +}: LogsAdminObj) => { + const t = useTranslations('admin.core.diagnostic.error_logs'); + + return ( + + + + + + + + + + {name} + + + + + +
+
+ {method} {url} +
+ +
+ +