From 931d5dde79933ed72895fa1114c144a8f762e2c7 Mon Sep 17 00:00:00 2001 From: aXenDeveloper Date: Sat, 21 Jun 2025 22:47:59 +0200 Subject: [PATCH 1/3] feat: Add logging system --- .github/docs/prd.md | 3 + apps/docs/content/docs/dev/logging.mdx | 49 +- ...man.sql => 0000_productive_king_cobra.sql} | 15 +- apps/web/migrations/0001_mute_flatman.sql | 2 - .../0002_superb_natasha_romanoff.sql | 13 - apps/web/migrations/meta/0000_snapshot.json | 83 +- apps/web/migrations/meta/0001_snapshot.json | 1338 ---------------- apps/web/migrations/meta/0002_snapshot.json | 1422 ----------------- apps/web/migrations/meta/_journal.json | 18 +- packages/vitnode/src/api/config.ts | 5 +- packages/vitnode/src/api/lib/get-user-ip.ts | 14 - .../vitnode/src/api/lib/logger-middleware.ts | 60 + .../src/api/middlewares/global.middleware.ts | 14 + .../middlewares/rate-limiter.middleware.ts | 5 +- packages/vitnode/src/api/models/device.ts | 6 +- packages/vitnode/src/api/models/email.ts | 18 +- .../vitnode/src/api/models/user/sign-up.ts | 3 +- .../api/modules/users/routes/test.route.ts | 16 +- packages/vitnode/src/database/logs.ts | 18 +- 19 files changed, 260 insertions(+), 2842 deletions(-) rename apps/web/migrations/{0000_overjoyed_sandman.sql => 0000_productive_king_cobra.sql} (94%) delete mode 100644 apps/web/migrations/0001_mute_flatman.sql delete mode 100644 apps/web/migrations/0002_superb_natasha_romanoff.sql delete mode 100644 apps/web/migrations/meta/0001_snapshot.json delete mode 100644 apps/web/migrations/meta/0002_snapshot.json delete mode 100644 packages/vitnode/src/api/lib/get-user-ip.ts create mode 100644 packages/vitnode/src/api/lib/logger-middleware.ts diff --git a/.github/docs/prd.md b/.github/docs/prd.md index 7962b96e1..2fa4f21ae 100644 --- a/.github/docs/prd.md +++ b/.github/docs/prd.md @@ -120,6 +120,9 @@ VitNode is designed for individual developers and small teams who need a structu - Component scaffolding - API endpoint generation - Database schema generation from models +- Logging system with structured logs: + - Log levels (debug, info, warn, error) + - Contextual logging with request/response metadata ### File Management diff --git a/apps/docs/content/docs/dev/logging.mdx b/apps/docs/content/docs/dev/logging.mdx index 63e1bc067..d5410c43c 100644 --- a/apps/docs/content/docs/dev/logging.mdx +++ b/apps/docs/content/docs/dev/logging.mdx @@ -1,4 +1,51 @@ --- title: Logging -description: Centralized logging with VitNode +description: Structured logging system for VitNode applications --- + +VitNode provides a logging system that allows you to log messages at different levels _(info, debug, warn, error)_ with structured logs. This is useful for debugging and monitoring your application. + +All logs are stored in your database. You can view them in the `Debug Panel`. + +## Usage + +```ts +import { buildRoute } from '@vitnode/core/api/lib/route'; +``` + +```ts +export const testRoute = buildRoute( + {}, + { + handler: c => { + c.get('log').warn('This is a test info log'); // [!code ++] + + return c.text('test'); + }, + }, +); +``` + +### Variants + +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + + + +```ts tab="Info" +c.get('log').info('This is a test info log'); +``` + +```ts tab="Debug" +c.get('log').debug('This is a test debug log'); +``` + +```ts tab="Warn" +c.get('log').warn('This is a test warning log'); +``` + +```ts tab="Error" +c.get('log').error('This is a test error log'); +``` + + diff --git a/apps/web/migrations/0000_overjoyed_sandman.sql b/apps/web/migrations/0000_productive_king_cobra.sql similarity index 94% rename from apps/web/migrations/0000_overjoyed_sandman.sql rename to apps/web/migrations/0000_productive_king_cobra.sql index 1a58e8ea3..1bd8b04a6 100644 --- a/apps/web/migrations/0000_overjoyed_sandman.sql +++ b/apps/web/migrations/0000_productive_king_cobra.sql @@ -1,3 +1,4 @@ +CREATE TYPE "public"."coreLogsType" AS ENUM('info', 'warn', 'error', 'debug');--> statement-breakpoint CREATE TABLE "core_admin_permissions" ( "id" serial PRIMARY KEY NOT NULL, "roleId" integer, @@ -46,6 +47,16 @@ CREATE TABLE "core_languages_words" ( ); --> statement-breakpoint ALTER TABLE "core_languages_words" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE TABLE "core_logs" ( + "id" serial PRIMARY KEY NOT NULL, + "pluginId" varchar(255) NOT NULL, + "type" "coreLogsType" DEFAULT 'info' NOT NULL, + "content" text NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL, + "ipAddress" varchar(45) NOT NULL +); +--> statement-breakpoint +ALTER TABLE "core_logs" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint CREATE TABLE "core_moderators_permissions" ( "id" serial PRIMARY KEY NOT NULL, "roleId" integer, @@ -81,9 +92,11 @@ CREATE TABLE "core_sessions" ( ALTER TABLE "core_sessions" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint CREATE TABLE "core_sessions_known_devices" ( "id" serial PRIMARY KEY NOT NULL, + "publicId" varchar(32) NOT NULL, "ipAddress" varchar(40) NOT NULL, "userAgent" text NOT NULL, - "lastSeen" timestamp DEFAULT now() NOT NULL + "lastSeen" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "core_sessions_known_devices_publicId_unique" UNIQUE("publicId") ); --> statement-breakpoint ALTER TABLE "core_sessions_known_devices" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint diff --git a/apps/web/migrations/0001_mute_flatman.sql b/apps/web/migrations/0001_mute_flatman.sql deleted file mode 100644 index d2de99678..000000000 --- a/apps/web/migrations/0001_mute_flatman.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE "core_sessions_known_devices" ADD COLUMN "publicId" varchar(32) NOT NULL;--> statement-breakpoint -ALTER TABLE "core_sessions_known_devices" ADD CONSTRAINT "core_sessions_known_devices_publicId_unique" UNIQUE("publicId"); \ No newline at end of file diff --git a/apps/web/migrations/0002_superb_natasha_romanoff.sql b/apps/web/migrations/0002_superb_natasha_romanoff.sql deleted file mode 100644 index 665631ad9..000000000 --- a/apps/web/migrations/0002_superb_natasha_romanoff.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE TYPE "public"."typeLogs" AS ENUM('info', 'warn', 'error', 'debug');--> statement-breakpoint -CREATE TABLE "core_logs" ( - "id" serial PRIMARY KEY NOT NULL, - "pluginCode" varchar(255) NOT NULL, - "type" "typeLogs" DEFAULT 'info' NOT NULL, - "content" text NOT NULL, - "createdAt" timestamp DEFAULT now() NOT NULL, - "userId" integer, - "ipAddress" varchar(45) DEFAULT '' NOT NULL -); ---> statement-breakpoint -ALTER TABLE "core_logs" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint -ALTER TABLE "core_logs" ADD CONSTRAINT "core_logs_userId_core_users_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."core_users"("id") ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/apps/web/migrations/meta/0000_snapshot.json b/apps/web/migrations/meta/0000_snapshot.json index bcd213c2a..eac58458d 100644 --- a/apps/web/migrations/meta/0000_snapshot.json +++ b/apps/web/migrations/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "d8197fc3-2d92-4d22-9381-9ebb1412c8fc", + "id": "cb9548b9-2232-46c0-9d83-f43632160676", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -439,6 +439,58 @@ "checkConstraints": {}, "isRLSEnabled": true }, + "public.core_logs": { + "name": "core_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pluginId": { + "name": "pluginId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "coreLogsType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, "public.core_moderators_permissions": { "name": "core_moderators_permissions", "schema": "", @@ -725,6 +777,12 @@ "primaryKey": true, "notNull": true }, + "publicId": { + "name": "publicId", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, "ipAddress": { "name": "ipAddress", "type": "varchar(40)", @@ -764,7 +822,15 @@ }, "foreignKeys": {}, "compositePrimaryKeys": {}, - "uniqueConstraints": {}, + "uniqueConstraints": { + "core_sessions_known_devices_publicId_unique": { + "name": "core_sessions_known_devices_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, "policies": {}, "checkConstraints": {}, "isRLSEnabled": true @@ -1310,7 +1376,18 @@ "isRLSEnabled": true } }, - "enums": {}, + "enums": { + "public.coreLogsType": { + "name": "coreLogsType", + "schema": "public", + "values": [ + "info", + "warn", + "error", + "debug" + ] + } + }, "schemas": {}, "sequences": {}, "roles": {}, diff --git a/apps/web/migrations/meta/0001_snapshot.json b/apps/web/migrations/meta/0001_snapshot.json deleted file mode 100644 index 26e4c85b4..000000000 --- a/apps/web/migrations/meta/0001_snapshot.json +++ /dev/null @@ -1,1338 +0,0 @@ -{ - "id": "147890d3-87ad-4e29-a83e-43b220c8af80", - "prevId": "d8197fc3-2d92-4d22-9381-9ebb1412c8fc", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.core_admin_permissions": { - "name": "core_admin_permissions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "roleId": { - "name": "roleId", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "protected": { - "name": "protected", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "core_admin_permissions_role_id_idx": { - "name": "core_admin_permissions_role_id_idx", - "columns": [ - { - "expression": "roleId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "core_admin_permissions_user_id_idx": { - "name": "core_admin_permissions_user_id_idx", - "columns": [ - { - "expression": "userId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "core_admin_permissions_roleId_core_roles_id_fk": { - "name": "core_admin_permissions_roleId_core_roles_id_fk", - "tableFrom": "core_admin_permissions", - "tableTo": "core_roles", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "core_admin_permissions_userId_core_users_id_fk": { - "name": "core_admin_permissions_userId_core_users_id_fk", - "tableFrom": "core_admin_permissions", - "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_admin_sessions": { - "name": "core_admin_sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "token": { - "name": "token", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "lastSeen": { - "name": "lastSeen", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "deviceId": { - "name": "deviceId", - "type": "integer", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "core_admin_sessions_token_idx": { - "name": "core_admin_sessions_token_idx", - "columns": [ - { - "expression": "token", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "core_admin_sessions_user_id_idx": { - "name": "core_admin_sessions_user_id_idx", - "columns": [ - { - "expression": "userId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "core_admin_sessions_userId_core_users_id_fk": { - "name": "core_admin_sessions_userId_core_users_id_fk", - "tableFrom": "core_admin_sessions", - "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk": { - "name": "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk", - "tableFrom": "core_admin_sessions", - "tableTo": "core_sessions_known_devices", - "columnsFrom": [ - "deviceId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "core_admin_sessions_token_unique": { - "name": "core_admin_sessions_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_languages": { - "name": "core_languages", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "code": { - "name": "code", - "type": "varchar(32)", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "timezone": { - "name": "timezone", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "default": "'UTC'" - }, - "protected": { - "name": "protected", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "default": { - "name": "default", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "enabled": { - "name": "enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "time24": { - "name": "time24", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "core_languages_code_idx": { - "name": "core_languages_code_idx", - "columns": [ - { - "expression": "code", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "core_languages_name_idx": { - "name": "core_languages_name_idx", - "columns": [ - { - "expression": "name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "core_languages_code_unique": { - "name": "core_languages_code_unique", - "nullsNotDistinct": false, - "columns": [ - "code" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_languages_words": { - "name": "core_languages_words", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "languageCode": { - "name": "languageCode", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "pluginCode": { - "name": "pluginCode", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "itemId": { - "name": "itemId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "tableName": { - "name": "tableName", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "variable": { - "name": "variable", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "core_languages_words_lang_code_idx": { - "name": "core_languages_words_lang_code_idx", - "columns": [ - { - "expression": "languageCode", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "core_languages_words_languageCode_core_languages_code_fk": { - "name": "core_languages_words_languageCode_core_languages_code_fk", - "tableFrom": "core_languages_words", - "tableTo": "core_languages", - "columnsFrom": [ - "languageCode" - ], - "columnsTo": [ - "code" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_moderators_permissions": { - "name": "core_moderators_permissions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "roleId": { - "name": "roleId", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "protected": { - "name": "protected", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "core_moderators_permissions_role_id_idx": { - "name": "core_moderators_permissions_role_id_idx", - "columns": [ - { - "expression": "roleId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "core_moderators_permissions_user_id_idx": { - "name": "core_moderators_permissions_user_id_idx", - "columns": [ - { - "expression": "userId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "core_moderators_permissions_roleId_core_roles_id_fk": { - "name": "core_moderators_permissions_roleId_core_roles_id_fk", - "tableFrom": "core_moderators_permissions", - "tableTo": "core_roles", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "core_moderators_permissions_userId_core_users_id_fk": { - "name": "core_moderators_permissions_userId_core_users_id_fk", - "tableFrom": "core_moderators_permissions", - "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_roles": { - "name": "core_roles", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "protected": { - "name": "protected", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "default": { - "name": "default", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "root": { - "name": "root", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "guest": { - "name": "guest", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "color": { - "name": "color", - "type": "varchar(19)", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_sessions": { - "name": "core_sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "token": { - "name": "token", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "deviceId": { - "name": "deviceId", - "type": "integer", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "core_sessions_user_id_idx": { - "name": "core_sessions_user_id_idx", - "columns": [ - { - "expression": "userId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "core_sessions_userId_core_users_id_fk": { - "name": "core_sessions_userId_core_users_id_fk", - "tableFrom": "core_sessions", - "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "core_sessions_deviceId_core_sessions_known_devices_id_fk": { - "name": "core_sessions_deviceId_core_sessions_known_devices_id_fk", - "tableFrom": "core_sessions", - "tableTo": "core_sessions_known_devices", - "columnsFrom": [ - "deviceId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "core_sessions_token_unique": { - "name": "core_sessions_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_sessions_known_devices": { - "name": "core_sessions_known_devices", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "publicId": { - "name": "publicId", - "type": "varchar(32)", - "primaryKey": false, - "notNull": true - }, - "ipAddress": { - "name": "ipAddress", - "type": "varchar(40)", - "primaryKey": false, - "notNull": true - }, - "userAgent": { - "name": "userAgent", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "lastSeen": { - "name": "lastSeen", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "core_sessions_known_devices_ip_address_idx": { - "name": "core_sessions_known_devices_ip_address_idx", - "columns": [ - { - "expression": "ipAddress", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "core_sessions_known_devices_publicId_unique": { - "name": "core_sessions_known_devices_publicId_unique", - "nullsNotDistinct": false, - "columns": [ - "publicId" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_users": { - "name": "core_users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "nameCode": { - "name": "nameCode", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "password": { - "name": "password", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "newsletter": { - "name": "newsletter", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "avatarColor": { - "name": "avatarColor", - "type": "varchar(6)", - "primaryKey": false, - "notNull": true - }, - "emailVerified": { - "name": "emailVerified", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "roleId": { - "name": "roleId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "birthday": { - "name": "birthday", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "ipAddress": { - "name": "ipAddress", - "type": "varchar(40)", - "primaryKey": false, - "notNull": true - }, - "language": { - "name": "language", - "type": "varchar(32)", - "primaryKey": false, - "notNull": true, - "default": "'en'" - } - }, - "indexes": { - "core_users_name_code_idx": { - "name": "core_users_name_code_idx", - "columns": [ - { - "expression": "nameCode", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "core_users_name_idx": { - "name": "core_users_name_idx", - "columns": [ - { - "expression": "name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "core_users_email_idx": { - "name": "core_users_email_idx", - "columns": [ - { - "expression": "email", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "core_users_roleId_core_roles_id_fk": { - "name": "core_users_roleId_core_roles_id_fk", - "tableFrom": "core_users", - "tableTo": "core_roles", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "core_users_language_core_languages_code_fk": { - "name": "core_users_language_core_languages_code_fk", - "tableFrom": "core_users", - "tableTo": "core_languages", - "columnsFrom": [ - "language" - ], - "columnsTo": [ - "code" - ], - "onDelete": "set default", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "core_users_nameCode_unique": { - "name": "core_users_nameCode_unique", - "nullsNotDistinct": false, - "columns": [ - "nameCode" - ] - }, - "core_users_name_unique": { - "name": "core_users_name_unique", - "nullsNotDistinct": false, - "columns": [ - "name" - ] - }, - "core_users_email_unique": { - "name": "core_users_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_users_confirm_emails": { - "name": "core_users_confirm_emails", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "expires": { - "name": "expires", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "core_users_confirm_emails_userId_core_users_id_fk": { - "name": "core_users_confirm_emails_userId_core_users_id_fk", - "tableFrom": "core_users_confirm_emails", - "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "core_users_confirm_emails_token_unique": { - "name": "core_users_confirm_emails_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_users_forgot_password": { - "name": "core_users_forgot_password", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "ip_address": { - "name": "ip_address", - "type": "varchar(40)", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "core_users_forgot_password_userId_core_users_id_fk": { - "name": "core_users_forgot_password_userId_core_users_id_fk", - "tableFrom": "core_users_forgot_password", - "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "core_users_forgot_password_userId_unique": { - "name": "core_users_forgot_password_userId_unique", - "nullsNotDistinct": false, - "columns": [ - "userId" - ] - }, - "core_users_forgot_password_token_unique": { - "name": "core_users_forgot_password_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_users_sso": { - "name": "core_users_sso", - "schema": "", - "columns": { - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "providerId": { - "name": "providerId", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "providerAccountId": { - "name": "providerAccountId", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "core_users_sso_user_id_idx": { - "name": "core_users_sso_user_id_idx", - "columns": [ - { - "expression": "userId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "core_users_sso_userId_core_users_id_fk": { - "name": "core_users_sso_userId_core_users_id_fk", - "tableFrom": "core_users_sso", - "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.blog_categories": { - "name": "blog_categories", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "title": { - "name": "title", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "titleSeo": { - "name": "titleSeo", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true, - "default": "''" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "blog_categories_titleSeo_unique": { - "name": "blog_categories_titleSeo_unique", - "nullsNotDistinct": false, - "columns": [ - "titleSeo" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.blog_posts": { - "name": "blog_posts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "title": { - "name": "title", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "titleSeo": { - "name": "titleSeo", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "categoryId": { - "name": "categoryId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "blog_posts_categoryId_blog_categories_id_fk": { - "name": "blog_posts_categoryId_blog_categories_id_fk", - "tableFrom": "blog_posts", - "tableTo": "blog_categories", - "columnsFrom": [ - "categoryId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "blog_posts_titleSeo_unique": { - "name": "blog_posts_titleSeo_unique", - "nullsNotDistinct": false, - "columns": [ - "titleSeo" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/apps/web/migrations/meta/0002_snapshot.json b/apps/web/migrations/meta/0002_snapshot.json deleted file mode 100644 index 678a7cb93..000000000 --- a/apps/web/migrations/meta/0002_snapshot.json +++ /dev/null @@ -1,1422 +0,0 @@ -{ - "id": "dc77dff2-5f35-42d7-84b1-6f6a96b81945", - "prevId": "147890d3-87ad-4e29-a83e-43b220c8af80", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.core_admin_permissions": { - "name": "core_admin_permissions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "roleId": { - "name": "roleId", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "protected": { - "name": "protected", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "core_admin_permissions_role_id_idx": { - "name": "core_admin_permissions_role_id_idx", - "columns": [ - { - "expression": "roleId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "core_admin_permissions_user_id_idx": { - "name": "core_admin_permissions_user_id_idx", - "columns": [ - { - "expression": "userId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "core_admin_permissions_roleId_core_roles_id_fk": { - "name": "core_admin_permissions_roleId_core_roles_id_fk", - "tableFrom": "core_admin_permissions", - "tableTo": "core_roles", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "core_admin_permissions_userId_core_users_id_fk": { - "name": "core_admin_permissions_userId_core_users_id_fk", - "tableFrom": "core_admin_permissions", - "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_admin_sessions": { - "name": "core_admin_sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "token": { - "name": "token", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "lastSeen": { - "name": "lastSeen", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "deviceId": { - "name": "deviceId", - "type": "integer", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "core_admin_sessions_token_idx": { - "name": "core_admin_sessions_token_idx", - "columns": [ - { - "expression": "token", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "core_admin_sessions_user_id_idx": { - "name": "core_admin_sessions_user_id_idx", - "columns": [ - { - "expression": "userId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "core_admin_sessions_userId_core_users_id_fk": { - "name": "core_admin_sessions_userId_core_users_id_fk", - "tableFrom": "core_admin_sessions", - "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk": { - "name": "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk", - "tableFrom": "core_admin_sessions", - "tableTo": "core_sessions_known_devices", - "columnsFrom": [ - "deviceId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "core_admin_sessions_token_unique": { - "name": "core_admin_sessions_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_languages": { - "name": "core_languages", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "code": { - "name": "code", - "type": "varchar(32)", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "timezone": { - "name": "timezone", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "default": "'UTC'" - }, - "protected": { - "name": "protected", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "default": { - "name": "default", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "enabled": { - "name": "enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "time24": { - "name": "time24", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "core_languages_code_idx": { - "name": "core_languages_code_idx", - "columns": [ - { - "expression": "code", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "core_languages_name_idx": { - "name": "core_languages_name_idx", - "columns": [ - { - "expression": "name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "core_languages_code_unique": { - "name": "core_languages_code_unique", - "nullsNotDistinct": false, - "columns": [ - "code" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_languages_words": { - "name": "core_languages_words", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "languageCode": { - "name": "languageCode", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "pluginCode": { - "name": "pluginCode", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "itemId": { - "name": "itemId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "tableName": { - "name": "tableName", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "variable": { - "name": "variable", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "core_languages_words_lang_code_idx": { - "name": "core_languages_words_lang_code_idx", - "columns": [ - { - "expression": "languageCode", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "core_languages_words_languageCode_core_languages_code_fk": { - "name": "core_languages_words_languageCode_core_languages_code_fk", - "tableFrom": "core_languages_words", - "tableTo": "core_languages", - "columnsFrom": [ - "languageCode" - ], - "columnsTo": [ - "code" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_logs": { - "name": "core_logs", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "pluginCode": { - "name": "pluginCode", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "typeLogs", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'info'" - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "ipAddress": { - "name": "ipAddress", - "type": "varchar(45)", - "primaryKey": false, - "notNull": true, - "default": "''" - } - }, - "indexes": {}, - "foreignKeys": { - "core_logs_userId_core_users_id_fk": { - "name": "core_logs_userId_core_users_id_fk", - "tableFrom": "core_logs", - "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_moderators_permissions": { - "name": "core_moderators_permissions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "roleId": { - "name": "roleId", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "protected": { - "name": "protected", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "core_moderators_permissions_role_id_idx": { - "name": "core_moderators_permissions_role_id_idx", - "columns": [ - { - "expression": "roleId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "core_moderators_permissions_user_id_idx": { - "name": "core_moderators_permissions_user_id_idx", - "columns": [ - { - "expression": "userId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "core_moderators_permissions_roleId_core_roles_id_fk": { - "name": "core_moderators_permissions_roleId_core_roles_id_fk", - "tableFrom": "core_moderators_permissions", - "tableTo": "core_roles", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "core_moderators_permissions_userId_core_users_id_fk": { - "name": "core_moderators_permissions_userId_core_users_id_fk", - "tableFrom": "core_moderators_permissions", - "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_roles": { - "name": "core_roles", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "protected": { - "name": "protected", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "default": { - "name": "default", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "root": { - "name": "root", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "guest": { - "name": "guest", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "color": { - "name": "color", - "type": "varchar(19)", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_sessions": { - "name": "core_sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "token": { - "name": "token", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "deviceId": { - "name": "deviceId", - "type": "integer", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "core_sessions_user_id_idx": { - "name": "core_sessions_user_id_idx", - "columns": [ - { - "expression": "userId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "core_sessions_userId_core_users_id_fk": { - "name": "core_sessions_userId_core_users_id_fk", - "tableFrom": "core_sessions", - "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "core_sessions_deviceId_core_sessions_known_devices_id_fk": { - "name": "core_sessions_deviceId_core_sessions_known_devices_id_fk", - "tableFrom": "core_sessions", - "tableTo": "core_sessions_known_devices", - "columnsFrom": [ - "deviceId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "core_sessions_token_unique": { - "name": "core_sessions_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_sessions_known_devices": { - "name": "core_sessions_known_devices", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "publicId": { - "name": "publicId", - "type": "varchar(32)", - "primaryKey": false, - "notNull": true - }, - "ipAddress": { - "name": "ipAddress", - "type": "varchar(40)", - "primaryKey": false, - "notNull": true - }, - "userAgent": { - "name": "userAgent", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "lastSeen": { - "name": "lastSeen", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "core_sessions_known_devices_ip_address_idx": { - "name": "core_sessions_known_devices_ip_address_idx", - "columns": [ - { - "expression": "ipAddress", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "core_sessions_known_devices_publicId_unique": { - "name": "core_sessions_known_devices_publicId_unique", - "nullsNotDistinct": false, - "columns": [ - "publicId" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_users": { - "name": "core_users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "nameCode": { - "name": "nameCode", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "password": { - "name": "password", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "newsletter": { - "name": "newsletter", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "avatarColor": { - "name": "avatarColor", - "type": "varchar(6)", - "primaryKey": false, - "notNull": true - }, - "emailVerified": { - "name": "emailVerified", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "roleId": { - "name": "roleId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "birthday": { - "name": "birthday", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "ipAddress": { - "name": "ipAddress", - "type": "varchar(40)", - "primaryKey": false, - "notNull": true - }, - "language": { - "name": "language", - "type": "varchar(32)", - "primaryKey": false, - "notNull": true, - "default": "'en'" - } - }, - "indexes": { - "core_users_name_code_idx": { - "name": "core_users_name_code_idx", - "columns": [ - { - "expression": "nameCode", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "core_users_name_idx": { - "name": "core_users_name_idx", - "columns": [ - { - "expression": "name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "core_users_email_idx": { - "name": "core_users_email_idx", - "columns": [ - { - "expression": "email", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "core_users_roleId_core_roles_id_fk": { - "name": "core_users_roleId_core_roles_id_fk", - "tableFrom": "core_users", - "tableTo": "core_roles", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "core_users_language_core_languages_code_fk": { - "name": "core_users_language_core_languages_code_fk", - "tableFrom": "core_users", - "tableTo": "core_languages", - "columnsFrom": [ - "language" - ], - "columnsTo": [ - "code" - ], - "onDelete": "set default", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "core_users_nameCode_unique": { - "name": "core_users_nameCode_unique", - "nullsNotDistinct": false, - "columns": [ - "nameCode" - ] - }, - "core_users_name_unique": { - "name": "core_users_name_unique", - "nullsNotDistinct": false, - "columns": [ - "name" - ] - }, - "core_users_email_unique": { - "name": "core_users_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_users_confirm_emails": { - "name": "core_users_confirm_emails", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "expires": { - "name": "expires", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "core_users_confirm_emails_userId_core_users_id_fk": { - "name": "core_users_confirm_emails_userId_core_users_id_fk", - "tableFrom": "core_users_confirm_emails", - "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "core_users_confirm_emails_token_unique": { - "name": "core_users_confirm_emails_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_users_forgot_password": { - "name": "core_users_forgot_password", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "ip_address": { - "name": "ip_address", - "type": "varchar(40)", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "core_users_forgot_password_userId_core_users_id_fk": { - "name": "core_users_forgot_password_userId_core_users_id_fk", - "tableFrom": "core_users_forgot_password", - "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "core_users_forgot_password_userId_unique": { - "name": "core_users_forgot_password_userId_unique", - "nullsNotDistinct": false, - "columns": [ - "userId" - ] - }, - "core_users_forgot_password_token_unique": { - "name": "core_users_forgot_password_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.core_users_sso": { - "name": "core_users_sso", - "schema": "", - "columns": { - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "providerId": { - "name": "providerId", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "providerAccountId": { - "name": "providerAccountId", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "core_users_sso_user_id_idx": { - "name": "core_users_sso_user_id_idx", - "columns": [ - { - "expression": "userId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "core_users_sso_userId_core_users_id_fk": { - "name": "core_users_sso_userId_core_users_id_fk", - "tableFrom": "core_users_sso", - "tableTo": "core_users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.blog_categories": { - "name": "blog_categories", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "title": { - "name": "title", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "titleSeo": { - "name": "titleSeo", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true, - "default": "''" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "blog_categories_titleSeo_unique": { - "name": "blog_categories_titleSeo_unique", - "nullsNotDistinct": false, - "columns": [ - "titleSeo" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.blog_posts": { - "name": "blog_posts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "title": { - "name": "title", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "titleSeo": { - "name": "titleSeo", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "categoryId": { - "name": "categoryId", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "blog_posts_categoryId_blog_categories_id_fk": { - "name": "blog_posts_categoryId_blog_categories_id_fk", - "tableFrom": "blog_posts", - "tableTo": "blog_categories", - "columnsFrom": [ - "categoryId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "blog_posts_titleSeo_unique": { - "name": "blog_posts_titleSeo_unique", - "nullsNotDistinct": false, - "columns": [ - "titleSeo" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - } - }, - "enums": { - "public.typeLogs": { - "name": "typeLogs", - "schema": "public", - "values": [ - "info", - "warn", - "error", - "debug" - ] - } - }, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/apps/web/migrations/meta/_journal.json b/apps/web/migrations/meta/_journal.json index 1d68d9686..c0177d153 100644 --- a/apps/web/migrations/meta/_journal.json +++ b/apps/web/migrations/meta/_journal.json @@ -5,22 +5,8 @@ { "idx": 0, "version": "7", - "when": 1749828219011, - "tag": "0000_overjoyed_sandman", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1750322132661, - "tag": "0001_mute_flatman", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1750493644570, - "tag": "0002_superb_natasha_romanoff", + "when": 1750526258416, + "tag": "0000_productive_king_cobra", "breakpoints": true } ] diff --git a/packages/vitnode/src/api/config.ts b/packages/vitnode/src/api/config.ts index 13511351f..be3e25d23 100644 --- a/packages/vitnode/src/api/config.ts +++ b/packages/vitnode/src/api/config.ts @@ -75,13 +75,12 @@ export function VitNodeAPI({ return next(); }); - app.onError(error => { + app.onError((error, c) => { if (error instanceof HTTPException) { return error.getResponse(); } - // eslint-disable-next-line no-console - console.error(error); + c.get('log').error(`Unhandled error: ${error.message}`); return new Response( process.env.NODE_ENV === 'development' diff --git a/packages/vitnode/src/api/lib/get-user-ip.ts b/packages/vitnode/src/api/lib/get-user-ip.ts deleted file mode 100644 index ad9caafb9..000000000 --- a/packages/vitnode/src/api/lib/get-user-ip.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Context } from 'hono'; - -export const getUserIp = (c: Context): string => { - const ip: string = c.req.header('x-forwarded-for')?.toString() ?? '0.0.0.0'; - - if (ip === '0.0.0.0') { - // eslint-disable-next-line no-console - console.error( - '\x1b[31m[VitNode]\x1b[31m No IP found in request. Please check if you passed `x-forwarded-for` header.\x1b[0m', - ); - } - - return ip; -}; diff --git a/packages/vitnode/src/api/lib/logger-middleware.ts b/packages/vitnode/src/api/lib/logger-middleware.ts new file mode 100644 index 000000000..c64b4aa08 --- /dev/null +++ b/packages/vitnode/src/api/lib/logger-middleware.ts @@ -0,0 +1,60 @@ +/* eslint-disable no-console */ +import type { Context } from 'hono'; + +import { core_logs, type CoreLogsType } from '@/database/logs'; + +export interface LoggerMiddlewareType { + debug: (content: string) => void; + error: (content: string) => void; + info: (content: string) => void; + warn: (content: string) => void; +} + +export const loggerMiddleware = (c: Context): LoggerMiddlewareType => { + const logToDbAndConsole = async (content: string, type: CoreLogsType) => { + const pluginId = c.get('plugin')?.id ?? 'core'; + const ipAddress = c.get('ipAddress'); + + await c.get('db').insert(core_logs).values({ + pluginId, + type, + content, + ipAddress, + }); + + const loggers: Record void> = { + debug: msg => + console.debug( + `\x1b[34m[VitNode]\x1b[0m \x1b[38;5;208mDebug (${pluginId})\x1b[0m: ${msg}`, + ), + error: msg => + console.error( + `\x1b[34m[VitNode]\x1b[0m \x1b[31mError (${pluginId})\x1b[0m: ${msg}`, + ), + warn: msg => + console.warn( + `\x1b[34m[VitNode]\x1b[0m \x1b[33mWarning (${pluginId})\x1b[0m: ${msg}`, + ), + info: msg => + console.info( + `\x1b[34m[VitNode]\x1b[0m \x1b[36mInfo (${pluginId})\x1b[0m: ${msg}`, + ), + }; + (loggers[type] ?? loggers.info)(content); + }; + + return { + debug: (content: string) => { + void logToDbAndConsole(content, 'debug'); + }, + error: (content: string) => { + void logToDbAndConsole(content, 'error'); + }, + info: (content: string) => { + void logToDbAndConsole(content, 'info'); + }, + warn: (content: string) => { + void logToDbAndConsole(content, 'warn'); + }, + }; +}; diff --git a/packages/vitnode/src/api/middlewares/global.middleware.ts b/packages/vitnode/src/api/middlewares/global.middleware.ts index 1edd9c5d8..2da743f67 100644 --- a/packages/vitnode/src/api/middlewares/global.middleware.ts +++ b/packages/vitnode/src/api/middlewares/global.middleware.ts @@ -10,6 +10,11 @@ import { SessionAdminModel } from '@/api/models/session-admin'; import type { SSOApiPlugin } from '../models/sso'; +import { + loggerMiddleware, + type LoggerMiddlewareType, +} from '../lib/logger-middleware'; + export interface EnvVitNode extends Env { Variables: EnvVariablesVitNode; } @@ -47,6 +52,8 @@ interface EnvVariablesVitNode { }; }; db: Pick['dbProvider']; + ipAddress: string; + log: LoggerMiddlewareType; plugin: { id: string; }; @@ -77,6 +84,12 @@ export const globalMiddleware = ({ }: Pick & Pick) => { return async (c: Context, next: Next) => { + c.set( + 'ipAddress', + c.req.header('x-forwarded-for') ?? + c.req.raw.headers.get('x-real-ip') ?? + '127.0.0.1', + ); c.set('db', dbProvider); c.set('core', { @@ -100,6 +113,7 @@ export const globalMiddleware = ({ const user = await new SessionModel(c).getUser(); c.set('user', user); c.set('admin', null); + c.set('log', loggerMiddleware(c)); await next(); }; diff --git a/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts b/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts index 72a1dd7b5..6f787461f 100644 --- a/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts +++ b/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts @@ -31,10 +31,7 @@ export const rateLimiterMiddleware = ( }); return async (c: Context, next: Next) => { - const key = - c.req.header('x-forwarded-for') ?? - c.req.raw.headers.get('x-real-ip') ?? - '127.0.0.1'; + const key = c.get('ipAddress'); try { await rateLimiter.consume(key); diff --git a/packages/vitnode/src/api/models/device.ts b/packages/vitnode/src/api/models/device.ts index 99190a0b5..629239a01 100644 --- a/packages/vitnode/src/api/models/device.ts +++ b/packages/vitnode/src/api/models/device.ts @@ -7,8 +7,6 @@ import { getCookie, setCookie } from 'hono/cookie'; import { core_sessions_known_devices } from '@/database/sessions'; import { CONFIG } from '@/lib/config'; -import { getUserIp } from '../lib/get-user-ip'; - export class DeviceModel { constructor(c: Context) { this.c = c; @@ -23,7 +21,7 @@ export class DeviceModel { .insert(core_sessions_known_devices) .values({ publicId, - ipAddress: getUserIp(this.c), + ipAddress: this.c.get('ipAddress'), userAgent: this.getUserAgent(), }) .returning({ id: core_sessions_known_devices.id }); @@ -78,7 +76,7 @@ export class DeviceModel { .get('db') .update(core_sessions_known_devices) .set({ - ipAddress: getUserIp(this.c), + ipAddress: this.c.get('ipAddress'), userAgent: this.getUserAgent(), }) .where(eq(core_sessions_known_devices.publicId, deviceIdFromCookie)); diff --git a/packages/vitnode/src/api/models/email.ts b/packages/vitnode/src/api/models/email.ts index 218a6200f..a2a0ee266 100644 --- a/packages/vitnode/src/api/models/email.ts +++ b/packages/vitnode/src/api/models/email.ts @@ -28,10 +28,18 @@ export class EmailModel { }); } - // TODO: Add logging when email is failed to send - void provider.sendEmail({ - ...args, - metadata: core.metadata, - }); + void provider + .sendEmail({ + ...args, + metadata: core.metadata, + }) + .catch((err: unknown) => { + const error = + err instanceof Error + ? err + : new Error('Unknown error from email provider'); + + this.c.get('log').error(`Failed to send email: ${error.message}`); + }); } } diff --git a/packages/vitnode/src/api/models/user/sign-up.ts b/packages/vitnode/src/api/models/user/sign-up.ts index d5f3a6615..cd98e6977 100644 --- a/packages/vitnode/src/api/models/user/sign-up.ts +++ b/packages/vitnode/src/api/models/user/sign-up.ts @@ -3,7 +3,6 @@ import type { Context } from 'hono'; import { and, count, eq, or } from 'drizzle-orm'; import { HTTPException } from 'hono/http-exception'; -import { getUserIp } from '@/api/lib/get-user-ip'; import { generateAvatarColor } from '@/api/modules/users/avatar-color'; import { core_roles } from '@/database/roles'; import { core_users } from '@/database/users'; @@ -122,7 +121,7 @@ export const signUp = async ( avatarColor: generateAvatarColor(name), roleId, emailVerified, - ipAddress: getUserIp(c), + ipAddress: c.get('ipAddress'), // TODO: Handle language // language: await this.getLanguage(req), }) diff --git a/packages/vitnode/src/api/modules/users/routes/test.route.ts b/packages/vitnode/src/api/modules/users/routes/test.route.ts index 53dfe0cc9..8e82ffc15 100644 --- a/packages/vitnode/src/api/modules/users/routes/test.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/test.route.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; import { buildRoute } from '@/api/lib/route'; import { CONFIG_PLUGIN } from '@/config'; -import { EmailModel } from '../../../models/email'; +// import { EmailModel } from '../../../models/email'; export const testRoute = buildRoute({ ...CONFIG_PLUGIN, @@ -31,11 +31,15 @@ export const testRoute = buildRoute({ }, }, handler: c => { - new EmailModel(c).send({ - html: '

Test email

', - to: 'ithereplay@gmail.com', - subject: 'Test Email', - }); + // new EmailModel(c).send({ + // html: '

Test email

', + // to: 'ithereplay@gmail.com', + // subject: 'Test Email', + // }); + + // throw new Error('Test error'); + + c.get('log').warn('This is a test warning log'); return c.text('test'); }, diff --git a/packages/vitnode/src/database/logs.ts b/packages/vitnode/src/database/logs.ts index d2f67de1f..25e794a70 100644 --- a/packages/vitnode/src/database/logs.ts +++ b/packages/vitnode/src/database/logs.ts @@ -1,17 +1,19 @@ import { pgEnum, pgTable } from 'drizzle-orm/pg-core'; -import { core_users } from './users'; +export const coreLogsType = pgEnum('coreLogsType', [ + 'info', + 'warn', + 'error', + 'debug', +]); -export const typeLogs = pgEnum('typeLogs', ['info', 'warn', 'error', 'debug']); +export type CoreLogsType = (typeof coreLogsType.enumValues)[number]; export const core_logs = pgTable('core_logs', t => ({ id: t.serial().primaryKey(), - pluginCode: t.varchar({ length: 255 }).notNull(), - type: typeLogs().notNull().default('info'), + pluginId: t.varchar({ length: 255 }).notNull(), + type: coreLogsType().notNull().default('info'), content: t.text().notNull(), createdAt: t.timestamp().notNull().defaultNow(), - userId: t.integer().references(() => core_users.id, { - onDelete: 'set null', - }), - ipAddress: t.varchar({ length: 45 }).notNull().default(''), + ipAddress: t.varchar({ length: 45 }).notNull(), })).enableRLS(); From 4a51ab0487a3a87b5c0d56907b0fc1632a4a21d9 Mon Sep 17 00:00:00 2001 From: aXenDeveloper Date: Sun, 22 Jun 2025 18:48:56 +0200 Subject: [PATCH 2/3] perf(blog): Move actions from admin to admin module --- .../content/docs/dev/config/rate-limiter.mdx | 2 +- apps/docs/content/docs/dev/fetcher.mdx | 22 ++++++++ .../src/api/middlewares/global.middleware.ts | 50 ++++++++++++++++--- .../middlewares/rate-limiter.middleware.ts | 4 +- packages/vitnode/src/lib/fetcher-client.ts | 3 ++ packages/vitnode/src/lib/fetcher.ts | 3 ++ packages/vitnode/src/lib/fetcher/core.ts | 4 +- .../src/api/modules/admin/admin.module.ts | 12 +++++ .../categories/categories.admin.module.ts | 12 +++++ .../categories/routes/create.route.ts | 0 .../categories/routes/delete.route.ts | 0 .../categories/routes/edit.route.ts | 0 .../modules/admin/posts/posts.admin.module.ts | 12 +++++ .../{ => admin}/posts/routes/create.route.ts | 0 .../{ => admin}/posts/routes/delete.route.ts | 0 .../{ => admin}/posts/routes/edit.route.ts | 0 .../modules/categories/categories.module.ts | 10 +--- .../src/api/modules/posts/posts.module.ts | 5 +- plugins/blog/src/config.api.ts | 3 +- .../actions/create-edit/create-edit.tsx | 2 +- .../actions/create-edit/mutation-api.ts | 10 ++-- .../row-actions/delete/mutation-api.ts | 5 +- .../posts/actions/create-edit/mutation-api.ts | 10 ++-- .../posts/row-actions/delete/mutation-api.ts | 5 +- 24 files changed, 138 insertions(+), 36 deletions(-) create mode 100644 plugins/blog/src/api/modules/admin/admin.module.ts create mode 100644 plugins/blog/src/api/modules/admin/categories/categories.admin.module.ts rename plugins/blog/src/api/modules/{ => admin}/categories/routes/create.route.ts (100%) rename plugins/blog/src/api/modules/{ => admin}/categories/routes/delete.route.ts (100%) rename plugins/blog/src/api/modules/{ => admin}/categories/routes/edit.route.ts (100%) create mode 100644 plugins/blog/src/api/modules/admin/posts/posts.admin.module.ts rename plugins/blog/src/api/modules/{ => admin}/posts/routes/create.route.ts (100%) rename plugins/blog/src/api/modules/{ => admin}/posts/routes/delete.route.ts (100%) rename plugins/blog/src/api/modules/{ => admin}/posts/routes/edit.route.ts (100%) diff --git a/apps/docs/content/docs/dev/config/rate-limiter.mdx b/apps/docs/content/docs/dev/config/rate-limiter.mdx index 696da72c7..830bef896 100644 --- a/apps/docs/content/docs/dev/config/rate-limiter.mdx +++ b/apps/docs/content/docs/dev/config/rate-limiter.mdx @@ -27,7 +27,7 @@ import { TypeTable } from 'fumadocs-ui/components/type-table'; description: 'The number of requests allowed within the specified duration.', type: 'number', - default: '40', + default: '80 (120 in dev)', }, duration: { description: diff --git a/apps/docs/content/docs/dev/fetcher.mdx b/apps/docs/content/docs/dev/fetcher.mdx index 22395ef94..17f733da2 100644 --- a/apps/docs/content/docs/dev/fetcher.mdx +++ b/apps/docs/content/docs/dev/fetcher.mdx @@ -39,6 +39,28 @@ The fetcher returns a standard Response object, just like the native `fetch` API ## Advanced Features +### Path Prefixing + +When working with nested modules you can import the module and use it directly with the `fetcher` function and add the `prefixPath` option to specify a path prefix. This is useful for organizing your API endpoints and keeping them modular. + +```ts +const response = await fetcher(categoriesAdminModule, { + // [!code ++] + prefixPath: '/admin', // Adds prefix to the path + path: '/categories', + method: 'post', + module: 'categories', + args: { + body: { + name: 'Technology', + description: 'Tech-related articles', + }, + }, +}); + +// This will make a request to: /admin/categories +``` + ### Caching Responses You can leverage Next.js caching by passing cache options: diff --git a/packages/vitnode/src/api/middlewares/global.middleware.ts b/packages/vitnode/src/api/middlewares/global.middleware.ts index 2da743f67..889163af4 100644 --- a/packages/vitnode/src/api/middlewares/global.middleware.ts +++ b/packages/vitnode/src/api/middlewares/global.middleware.ts @@ -84,12 +84,50 @@ export const globalMiddleware = ({ }: Pick & Pick) => { return async (c: Context, next: Next) => { - c.set( - 'ipAddress', - c.req.header('x-forwarded-for') ?? - c.req.raw.headers.get('x-real-ip') ?? - '127.0.0.1', - ); + // Collect possible IP header keys in order of trust/preference + const ipHeaderKeys = [ + 'x-forwarded-for', + 'x-real-ip', + 'cf-connecting-ip', + 'x-client-ip', + 'x-forwarded', + 'x-cluster-client-ip', + 'forwarded-for', + 'forwarded', + 'via', + 'remote-addr', + 'client-ip', + 'ip', + 'x-ip', + 'true-client-ip', + 'fastly-client-ip', + 'x-fastly-client-ip', + ]; + + let ipAddress: string | undefined; + + // Try to get IP from Hono's request header method first + for (const key of ipHeaderKeys) { + const value = c.req.header(key); + if (value) { + ipAddress = value; + break; + } + } + + // If not found, try raw headers (for edge runtimes, etc.) + if (!ipAddress) { + for (const key of ipHeaderKeys) { + const value = c.req.raw.headers.get(key); + if (value) { + ipAddress = value; + break; + } + } + } + + // Fallback to localhost if nothing found + c.set('ipAddress', ipAddress ?? '127.0.0.1'); c.set('db', dbProvider); c.set('core', { diff --git a/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts b/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts index 6f787461f..6d45cea50 100644 --- a/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts +++ b/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts @@ -6,6 +6,8 @@ import { RateLimiterMemory, } from 'rate-limiter-flexible'; +import { CONFIG } from '../../lib/config'; + const createRateLimiter = ({ keyPrefix, ...options @@ -16,7 +18,7 @@ const createRateLimiter = ({ return new RateLimiterMemory({ keyPrefix, - points: options?.points ?? 40, // 40 requests + points: CONFIG.node_development ? 120 : (options?.points ?? 80), // 120 req in dev, 80 in prod duration: options?.duration ?? 60, // per 60 seconds ...options, }); diff --git a/packages/vitnode/src/lib/fetcher-client.ts b/packages/vitnode/src/lib/fetcher-client.ts index 8ebdadc75..a22588912 100644 --- a/packages/vitnode/src/lib/fetcher-client.ts +++ b/packages/vitnode/src/lib/fetcher-client.ts @@ -36,8 +36,10 @@ export async function fetcherClient< args, options, withPagination = false, + prefixPath = '', }: FetcherParams & { options?: Omit; + prefixPath?: string; withPagination?: boolean; }, ): Promise< @@ -50,5 +52,6 @@ export async function fetcherClient< args, options, withPagination, + prefixPath, }); } diff --git a/packages/vitnode/src/lib/fetcher.ts b/packages/vitnode/src/lib/fetcher.ts index ee33935df..1a2a097f1 100644 --- a/packages/vitnode/src/lib/fetcher.ts +++ b/packages/vitnode/src/lib/fetcher.ts @@ -41,9 +41,11 @@ export async function fetcher< options, allowSaveCookies = false, withPagination = false, + prefixPath = '', }: FetcherParams & { allowSaveCookies?: boolean; options?: Omit; + prefixPath?: string; withPagination?: boolean; }, ): Promise< @@ -61,6 +63,7 @@ export async function fetcher< args, options, withPagination, + prefixPath, additionalHeaders: { Cookie: cookie.toString(), ['user-agent']: nextInternalHeaders.get('user-agent') ?? 'node', diff --git a/packages/vitnode/src/lib/fetcher/core.ts b/packages/vitnode/src/lib/fetcher/core.ts index 5b69ccd26..505a73387 100644 --- a/packages/vitnode/src/lib/fetcher/core.ts +++ b/packages/vitnode/src/lib/fetcher/core.ts @@ -42,6 +42,7 @@ interface CoreFetcherOptions< module: ModuleName; options?: Omit; path: SelectedPath; + prefixPath?: string; withPagination?: boolean; } @@ -68,6 +69,7 @@ export async function coreFetcher< options, additionalHeaders = {}, withPagination = false, + prefixPath = '', }: CoreFetcherOptions, ): Promise< InferResponseType @@ -90,7 +92,7 @@ export async function coreFetcher< // Construct the base URL const url = new URL( - `/api/${pluginId}/${module}${formattedPath}`, + `/api/${pluginId}${prefixPath}/${module}${formattedPath}`, CONFIG.backend.origin, ); diff --git a/plugins/blog/src/api/modules/admin/admin.module.ts b/plugins/blog/src/api/modules/admin/admin.module.ts new file mode 100644 index 000000000..e0290055d --- /dev/null +++ b/plugins/blog/src/api/modules/admin/admin.module.ts @@ -0,0 +1,12 @@ +import { buildModule } from '@vitnode/core/api/lib/module'; + +import { CONFIG_PLUGIN } from '../../../const'; +import { categoriesAdminModule } from './categories/categories.admin.module'; +import { postsAdminModule } from './posts/posts.admin.module'; + +export const adminModule = buildModule({ + ...CONFIG_PLUGIN, + name: 'admin', + modules: [categoriesAdminModule, postsAdminModule], + routes: [], +}); diff --git a/plugins/blog/src/api/modules/admin/categories/categories.admin.module.ts b/plugins/blog/src/api/modules/admin/categories/categories.admin.module.ts new file mode 100644 index 000000000..327be697b --- /dev/null +++ b/plugins/blog/src/api/modules/admin/categories/categories.admin.module.ts @@ -0,0 +1,12 @@ +import { buildModule } from '@vitnode/core/api/lib/module'; + +import { CONFIG_PLUGIN } from '../../../../const'; +import { createCategoryRoute } from './routes/create.route'; +import { deleteCategoryRoute } from './routes/delete.route'; +import { editCategoryRoute } from './routes/edit.route'; + +export const categoriesAdminModule = buildModule({ + ...CONFIG_PLUGIN, + name: 'categories', + routes: [createCategoryRoute, editCategoryRoute, deleteCategoryRoute], +}); diff --git a/plugins/blog/src/api/modules/categories/routes/create.route.ts b/plugins/blog/src/api/modules/admin/categories/routes/create.route.ts similarity index 100% rename from plugins/blog/src/api/modules/categories/routes/create.route.ts rename to plugins/blog/src/api/modules/admin/categories/routes/create.route.ts diff --git a/plugins/blog/src/api/modules/categories/routes/delete.route.ts b/plugins/blog/src/api/modules/admin/categories/routes/delete.route.ts similarity index 100% rename from plugins/blog/src/api/modules/categories/routes/delete.route.ts rename to plugins/blog/src/api/modules/admin/categories/routes/delete.route.ts diff --git a/plugins/blog/src/api/modules/categories/routes/edit.route.ts b/plugins/blog/src/api/modules/admin/categories/routes/edit.route.ts similarity index 100% rename from plugins/blog/src/api/modules/categories/routes/edit.route.ts rename to plugins/blog/src/api/modules/admin/categories/routes/edit.route.ts diff --git a/plugins/blog/src/api/modules/admin/posts/posts.admin.module.ts b/plugins/blog/src/api/modules/admin/posts/posts.admin.module.ts new file mode 100644 index 000000000..cbc093b4a --- /dev/null +++ b/plugins/blog/src/api/modules/admin/posts/posts.admin.module.ts @@ -0,0 +1,12 @@ +import { buildModule } from '@vitnode/core/api/lib/module'; + +import { CONFIG_PLUGIN } from '../../../../const'; +import { createPostRoute } from './routes/create.route'; +import { deletePostRoute } from './routes/delete.route'; +import { editPostRoute } from './routes/edit.route'; + +export const postsAdminModule = buildModule({ + ...CONFIG_PLUGIN, + name: 'posts', + routes: [editPostRoute, createPostRoute, deletePostRoute], +}); diff --git a/plugins/blog/src/api/modules/posts/routes/create.route.ts b/plugins/blog/src/api/modules/admin/posts/routes/create.route.ts similarity index 100% rename from plugins/blog/src/api/modules/posts/routes/create.route.ts rename to plugins/blog/src/api/modules/admin/posts/routes/create.route.ts diff --git a/plugins/blog/src/api/modules/posts/routes/delete.route.ts b/plugins/blog/src/api/modules/admin/posts/routes/delete.route.ts similarity index 100% rename from plugins/blog/src/api/modules/posts/routes/delete.route.ts rename to plugins/blog/src/api/modules/admin/posts/routes/delete.route.ts diff --git a/plugins/blog/src/api/modules/posts/routes/edit.route.ts b/plugins/blog/src/api/modules/admin/posts/routes/edit.route.ts similarity index 100% rename from plugins/blog/src/api/modules/posts/routes/edit.route.ts rename to plugins/blog/src/api/modules/admin/posts/routes/edit.route.ts diff --git a/plugins/blog/src/api/modules/categories/categories.module.ts b/plugins/blog/src/api/modules/categories/categories.module.ts index 65fc0c3d9..9868128b0 100644 --- a/plugins/blog/src/api/modules/categories/categories.module.ts +++ b/plugins/blog/src/api/modules/categories/categories.module.ts @@ -2,18 +2,10 @@ import { buildModule } from '@vitnode/core/api/lib/module'; import { CONFIG_PLUGIN } from '@/const'; -import { createCategoryRoute } from './routes/create.route'; -import { deleteCategoryRoute } from './routes/delete.route'; -import { editCategoryRoute } from './routes/edit.route'; import { categoriesRoute } from './routes/get.route'; export const categoriesModule = buildModule({ ...CONFIG_PLUGIN, name: 'categories', - routes: [ - categoriesRoute, - createCategoryRoute, - editCategoryRoute, - deleteCategoryRoute, - ], + routes: [categoriesRoute], }); diff --git a/plugins/blog/src/api/modules/posts/posts.module.ts b/plugins/blog/src/api/modules/posts/posts.module.ts index e267437c8..dc74a95ac 100644 --- a/plugins/blog/src/api/modules/posts/posts.module.ts +++ b/plugins/blog/src/api/modules/posts/posts.module.ts @@ -2,13 +2,10 @@ import { buildModule } from '@vitnode/core/api/lib/module'; import { CONFIG_PLUGIN } from '@/const'; -import { createPostRoute } from './routes/create.route'; -import { deletePostRoute } from './routes/delete.route'; -import { editPostRoute } from './routes/edit.route'; import { postsRoute } from './routes/get.route'; export const postsModule = buildModule({ ...CONFIG_PLUGIN, name: 'posts', - routes: [postsRoute, createPostRoute, editPostRoute, deletePostRoute], + routes: [postsRoute], }); diff --git a/plugins/blog/src/config.api.ts b/plugins/blog/src/config.api.ts index bc3ea3d3c..a30b0eb8d 100644 --- a/plugins/blog/src/config.api.ts +++ b/plugins/blog/src/config.api.ts @@ -2,12 +2,13 @@ import { buildApiPlugin } from '@vitnode/core/api/lib/plugin'; import { CONFIG_PLUGIN } from '@/const'; +import { adminModule } from './api/modules/admin/admin.module'; import { categoriesModule } from './api/modules/categories/categories.module'; import { postsModule } from './api/modules/posts/posts.module'; export const blogApiPlugin = () => { return buildApiPlugin({ ...CONFIG_PLUGIN, - modules: [categoriesModule, postsModule], + modules: [adminModule, categoriesModule, postsModule], }); }; diff --git a/plugins/blog/src/views/admin/categories/actions/create-edit/create-edit.tsx b/plugins/blog/src/views/admin/categories/actions/create-edit/create-edit.tsx index 388ec66b5..770ca86b0 100644 --- a/plugins/blog/src/views/admin/categories/actions/create-edit/create-edit.tsx +++ b/plugins/blog/src/views/admin/categories/actions/create-edit/create-edit.tsx @@ -8,7 +8,7 @@ import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; import { z } from 'zod'; -import type { zodCreateCategorySchema } from '@/api/modules/categories/routes/create.route'; +import type { zodCreateCategorySchema } from '@/api/modules/admin/categories/routes/create.route'; import { createMutationApi, editMutationApi } from './mutation-api'; diff --git a/plugins/blog/src/views/admin/categories/actions/create-edit/mutation-api.ts b/plugins/blog/src/views/admin/categories/actions/create-edit/mutation-api.ts index 67bcb138e..e68dfd31c 100644 --- a/plugins/blog/src/views/admin/categories/actions/create-edit/mutation-api.ts +++ b/plugins/blog/src/views/admin/categories/actions/create-edit/mutation-api.ts @@ -5,14 +5,15 @@ import type { z } from 'zod'; import { fetcher } from '@vitnode/core/lib/fetcher'; import { revalidatePath } from 'next/cache'; -import type { zodCreateCategorySchema } from '@/api/modules/categories/routes/create.route'; +import type { zodCreateCategorySchema } from '../../../../../api/modules/admin/categories/routes/create.route'; -import { categoriesModule } from '@/api/modules/categories/categories.module'; +import { categoriesAdminModule } from '../../../../../api/modules/admin/categories/categories.admin.module'; export const createMutationApi = async ( body: z.infer, ) => { - const res = await fetcher(categoriesModule, { + const res = await fetcher(categoriesAdminModule, { + prefixPath: '/admin', method: 'post', module: 'categories', path: '/', @@ -35,7 +36,8 @@ export const editMutationApi = async ({ id, ...body }: z.infer & { id: number }) => { - const res = await fetcher(categoriesModule, { + const res = await fetcher(categoriesAdminModule, { + prefixPath: '/admin', method: 'put', module: 'categories', path: '/{id}', diff --git a/plugins/blog/src/views/admin/categories/row-actions/delete/mutation-api.ts b/plugins/blog/src/views/admin/categories/row-actions/delete/mutation-api.ts index e0409aa9a..09fbf0d3c 100644 --- a/plugins/blog/src/views/admin/categories/row-actions/delete/mutation-api.ts +++ b/plugins/blog/src/views/admin/categories/row-actions/delete/mutation-api.ts @@ -3,10 +3,11 @@ import { fetcher } from '@vitnode/core/lib/fetcher'; import { revalidatePath } from 'next/cache'; -import { categoriesModule } from '@/api/modules/categories/categories.module'; +import { categoriesAdminModule } from '@/api/modules/admin/categories/categories.admin.module'; export const mutationApi = async (id: number) => { - const res = await fetcher(categoriesModule, { + const res = await fetcher(categoriesAdminModule, { + prefixPath: '/admin', method: 'delete', path: '/{id}', module: 'categories', diff --git a/plugins/blog/src/views/admin/posts/actions/create-edit/mutation-api.ts b/plugins/blog/src/views/admin/posts/actions/create-edit/mutation-api.ts index 9ef413f39..270e90ac8 100644 --- a/plugins/blog/src/views/admin/posts/actions/create-edit/mutation-api.ts +++ b/plugins/blog/src/views/admin/posts/actions/create-edit/mutation-api.ts @@ -5,14 +5,15 @@ import type { z } from 'zod'; import { fetcher } from '@vitnode/core/lib/fetcher'; import { revalidatePath } from 'next/cache'; -import type { zodCreatePostSchema } from '@/api/modules/posts/routes/create.route'; +import type { zodCreatePostSchema } from '@/api/modules/admin/posts/routes/create.route'; -import { postsModule } from '@/api/modules/posts/posts.module'; +import { postsAdminModule } from '@/api/modules/admin/posts/posts.admin.module'; export const createMutationApi = async ( body: z.infer, ) => { - const res = await fetcher(postsModule, { + const res = await fetcher(postsAdminModule, { + prefixPath: '/admin', method: 'post', module: 'posts', path: '/', @@ -35,7 +36,8 @@ export const editMutationApi = async ({ id, ...body }: z.infer & { id: number }) => { - const res = await fetcher(postsModule, { + const res = await fetcher(postsAdminModule, { + prefixPath: '/admin', method: 'put', module: 'posts', path: '/{id}', diff --git a/plugins/blog/src/views/admin/posts/row-actions/delete/mutation-api.ts b/plugins/blog/src/views/admin/posts/row-actions/delete/mutation-api.ts index a336e642d..937a3f6d0 100644 --- a/plugins/blog/src/views/admin/posts/row-actions/delete/mutation-api.ts +++ b/plugins/blog/src/views/admin/posts/row-actions/delete/mutation-api.ts @@ -3,10 +3,11 @@ import { fetcher } from '@vitnode/core/lib/fetcher'; import { revalidatePath } from 'next/cache'; -import { postsModule } from '@/api/modules/posts/posts.module'; +import { postsAdminModule } from '@/api/modules/admin/posts/posts.admin.module'; export const mutationApi = async (id: number) => { - const res = await fetcher(postsModule, { + const res = await fetcher(postsAdminModule, { + prefixPath: '/admin', method: 'delete', path: '/{id}', module: 'posts', From aabe9f93af17bbd03148db7b49a9fdc8cd2e402e Mon Sep 17 00:00:00 2001 From: aXenDeveloper Date: Mon, 23 Jun 2025 17:50:24 +0200 Subject: [PATCH 3/3] feat: Add logs system table in debug panel --- .../content/docs/dev/database/pagination.mdx | 9 +- apps/docs/content/docs/dev/logging.mdx | 10 +- apps/docs/src/app/global.css | 10 +- ...e_king_cobra.sql => 0000_stormy_ronan.sql} | 4 +- apps/web/migrations/meta/0000_snapshot.json | 6 +- apps/web/migrations/meta/_journal.json | 4 +- .../(vitnode-blog)/blog/categories/page.tsx | 7 +- .../(vitnode-blog)/blog/posts/page.tsx | 7 +- .../(vitnode-core)/core/debug/page.tsx | 18 ++- .../(vitnode-core)/core/users/page.tsx | 7 +- apps/web/src/app/global.css | 10 +- apps/web/src/locales/@vitnode/core/en.json | 13 +++ packages/vitnode/scripts/shared/file-utils.ts | 15 +++ .../vitnode/src/api/lib/logger-middleware.ts | 10 +- .../src/api/modules/admin/admin.module.ts | 3 +- .../modules/admin/debug/debug.admin.module.ts | 9 ++ .../modules/admin/debug/routes/logs.route.ts | 80 ++++++++++++++ .../api/modules/admin/routes/session.route.ts | 4 - .../api/modules/users/routes/test.route.ts | 2 +- .../vitnode/src/app_admin/core/debug/page.tsx | 18 ++- .../vitnode/src/app_admin/core/users/page.tsx | 7 +- .../vitnode/src/components/date-format.tsx | 6 + packages/vitnode/src/database/logs.ts | 9 +- packages/vitnode/src/globals.css | 1 + packages/vitnode/src/locales/en.json | 13 +++ .../views/core/debug/debug-admin-view.tsx | 103 +++++++++++++++++- .../src/app_admin/blog/categories/page.tsx | 7 +- .../blog/src/app_admin/blog/posts/page.tsx | 7 +- plugins/blog/src/globals.css | 1 + 29 files changed, 332 insertions(+), 68 deletions(-) rename apps/web/migrations/{0000_productive_king_cobra.sql => 0000_stormy_ronan.sql} (98%) create mode 100644 packages/vitnode/src/api/modules/admin/debug/debug.admin.module.ts create mode 100644 packages/vitnode/src/api/modules/admin/debug/routes/logs.route.ts diff --git a/apps/docs/content/docs/dev/database/pagination.mdx b/apps/docs/content/docs/dev/database/pagination.mdx index b2cac5767..fad97a68b 100644 --- a/apps/docs/content/docs/dev/database/pagination.mdx +++ b/apps/docs/content/docs/dev/database/pagination.mdx @@ -127,18 +127,13 @@ On the frontend, the pagination system works seamlessly with the DataTable compo When fetching data from the API, include the pagination parameters in your request: ```tsx +const query = await searchParams; // Assume searchParams is a Promise from the Next.js page context const res = await fetcher(userModule, { path: '/users', method: 'get', module: 'user', args: { - query: { - cursor: searchParams.cursor, - first: searchParams.first, - last: searchParams.last, - order: searchParams.order, - orderBy: searchParams.orderBy, - }, + query, }, withPagination: true, // Important flag for pagination }); diff --git a/apps/docs/content/docs/dev/logging.mdx b/apps/docs/content/docs/dev/logging.mdx index d5410c43c..93885237a 100644 --- a/apps/docs/content/docs/dev/logging.mdx +++ b/apps/docs/content/docs/dev/logging.mdx @@ -3,7 +3,7 @@ title: Logging description: Structured logging system for VitNode applications --- -VitNode provides a logging system that allows you to log messages at different levels _(info, debug, warn, error)_ with structured logs. This is useful for debugging and monitoring your application. +VitNode provides a logging system that allows you to log messages at different levels _(debug, warn, error)_ with structured logs. This is useful for debugging and monitoring your application. All logs are stored in your database. You can view them in the `Debug Panel`. @@ -18,7 +18,7 @@ export const testRoute = buildRoute( {}, { handler: c => { - c.get('log').warn('This is a test info log'); // [!code ++] + c.get('log').warn('This is a test warn log'); // [!code ++] return c.text('test'); }, @@ -30,11 +30,7 @@ export const testRoute = buildRoute( import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; - - -```ts tab="Info" -c.get('log').info('This is a test info log'); -``` + ```ts tab="Debug" c.get('log').debug('This is a test debug log'); diff --git a/apps/docs/src/app/global.css b/apps/docs/src/app/global.css index 267bc9998..96cd4e75e 100644 --- a/apps/docs/src/app/global.css +++ b/apps/docs/src/app/global.css @@ -20,8 +20,8 @@ --muted-foreground: oklch(0.45 0 0); --accent: oklch(0.95 0 0); --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.577 0.245 27.325); + --destructive: oklch(0.6 0.2 24.45); + --warn: oklch(0.57 0.13 82.37); --border: oklch(0.9 0 0); --input: oklch(0.9 0 0); --ring: oklch(0.7 0.16 262.61); @@ -58,8 +58,8 @@ --muted: oklch(0.22 0 0); --muted-foreground: oklch(0.7 0 0); --accent: oklch(0.28 0 0); - --destructive: oklch(0.704 0.191 22.216); - --destructive-foreground: oklch(0.704 0.191 22.216); + --destructive: oklch(0.62 0.2 25.35); + --warn: oklch(0.76 0.18 81.84); --border: oklch(0.28 0 0); --input: oklch(0.28 0 0); --ring: oklch(0.51 0.16 262.61); @@ -102,7 +102,7 @@ --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); + --color-warn: var(--warn); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); diff --git a/apps/web/migrations/0000_productive_king_cobra.sql b/apps/web/migrations/0000_stormy_ronan.sql similarity index 98% rename from apps/web/migrations/0000_productive_king_cobra.sql rename to apps/web/migrations/0000_stormy_ronan.sql index 1bd8b04a6..f2c645079 100644 --- a/apps/web/migrations/0000_productive_king_cobra.sql +++ b/apps/web/migrations/0000_stormy_ronan.sql @@ -1,4 +1,4 @@ -CREATE TYPE "public"."coreLogsType" AS ENUM('info', 'warn', 'error', 'debug');--> statement-breakpoint +CREATE TYPE "public"."coreLogsType" AS ENUM('warn', 'error', 'debug');--> statement-breakpoint CREATE TABLE "core_admin_permissions" ( "id" serial PRIMARY KEY NOT NULL, "roleId" integer, @@ -50,7 +50,7 @@ ALTER TABLE "core_languages_words" ENABLE ROW LEVEL SECURITY;--> statement-break CREATE TABLE "core_logs" ( "id" serial PRIMARY KEY NOT NULL, "pluginId" varchar(255) NOT NULL, - "type" "coreLogsType" DEFAULT 'info' NOT NULL, + "type" "coreLogsType" NOT NULL, "content" text NOT NULL, "createdAt" timestamp DEFAULT now() NOT NULL, "ipAddress" varchar(45) NOT NULL diff --git a/apps/web/migrations/meta/0000_snapshot.json b/apps/web/migrations/meta/0000_snapshot.json index eac58458d..0b46f9572 100644 --- a/apps/web/migrations/meta/0000_snapshot.json +++ b/apps/web/migrations/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "cb9548b9-2232-46c0-9d83-f43632160676", + "id": "960a9871-cd4a-4144-bc0a-af0717f397a7", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -460,8 +460,7 @@ "type": "coreLogsType", "typeSchema": "public", "primaryKey": false, - "notNull": true, - "default": "'info'" + "notNull": true }, "content": { "name": "content", @@ -1381,7 +1380,6 @@ "name": "coreLogsType", "schema": "public", "values": [ - "info", "warn", "error", "debug" diff --git a/apps/web/migrations/meta/_journal.json b/apps/web/migrations/meta/_journal.json index c0177d153..26c27fd9e 100644 --- a/apps/web/migrations/meta/_journal.json +++ b/apps/web/migrations/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1750526258416, - "tag": "0000_productive_king_cobra", + "when": 1750691750823, + "tag": "0000_stormy_ronan", "breakpoints": true } ] diff --git a/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/categories/page.tsx b/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/categories/page.tsx index 38ce51e93..dcc322a09 100644 --- a/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/categories/page.tsx +++ b/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/categories/page.tsx @@ -7,7 +7,12 @@ import { getTranslations } from 'next-intl/server'; import React from 'react'; import { ActionsCategoriesAdmin } from '@vitnode/blog/views/admin/categories/actions/actions'; -import { CategoriesAdminView } from '@vitnode/blog/views/admin/categories/categories-admin-view'; + +const CategoriesAdminView = React.lazy(async () => + import('@vitnode/blog/views/admin/categories/categories-admin-view').then(mod => ({ + default: mod.CategoriesAdminView, + })), +); export const generateMetadata = async (): Promise => { const t = await getTranslations('@vitnode/blog.admin.nav'); diff --git a/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/posts/page.tsx b/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/posts/page.tsx index f2163bd36..7f45c40e9 100644 --- a/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/posts/page.tsx +++ b/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/posts/page.tsx @@ -7,7 +7,12 @@ import { getTranslations } from 'next-intl/server'; import React from 'react'; import { ActionsPostsAdmin } from '@vitnode/blog/views/admin/posts/actions/actions'; -import { PostsAdminView } from '@vitnode/blog/views/admin/posts/posts-admin-view'; + +const PostsAdminView = React.lazy(async () => + import('@vitnode/blog/views/admin/posts/posts-admin-view').then(mod => ({ + default: mod.PostsAdminView, + })), +); export const generateMetadata = async (): Promise => { const t = await getTranslations('@vitnode/blog.admin.nav'); diff --git a/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/debug/page.tsx b/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/debug/page.tsx index 6c3124468..cfda4a29a 100644 --- a/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/debug/page.tsx +++ b/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/debug/page.tsx @@ -1,9 +1,16 @@ import { getTranslations } from 'next-intl/server'; +import React from 'react'; import { I18nProvider } from '@vitnode/core/components/i18n-provider'; +import { DataTableSkeleton } from '@vitnode/core/components/table/data-table'; import { HeaderContent } from '@vitnode/core/components/ui/header-content'; import { ClearCacheAction } from '@vitnode/core/views/admin/views/core/debug/actions/clear-cache/clear-cache'; -import { DebugAdminView } from '@vitnode/core/views/admin/views/core/debug/debug-admin-view'; + +const DebugAdminView = React.lazy(async () => + import('@vitnode/core/views/admin/views/core/debug/debug-admin-view').then(module => ({ + default: module.DebugAdminView, + })), +); export const generateMetadata = async () => { const t = await getTranslations('admin.debug'); @@ -14,7 +21,9 @@ export const generateMetadata = async () => { }; }; -export default async function Page() { +export default async function Page( + props: React.ComponentProps, +) { const t = await getTranslations('admin.debug'); return ( @@ -24,7 +33,10 @@ export default async function Page() { - + + }> + + ); diff --git a/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/page.tsx b/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/page.tsx index 92394f9be..b4a0323b0 100644 --- a/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/page.tsx +++ b/apps/web/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/page.tsx @@ -5,7 +5,12 @@ import React from 'react'; import { DataTableSkeleton } from '@vitnode/core/components/table/data-table'; import { HeaderContent } from '@vitnode/core/components/ui/header-content'; -import { UsersAdminView } from '@vitnode/core/views/admin/views/core/users/users-admin-view'; + +const UsersAdminView = React.lazy(async () => + import('@vitnode/core/views/admin/views/core/users/users-admin-view').then(module => ({ + default: module.UsersAdminView, + })), +); export const generateMetadata = async (): Promise => { const t = await getTranslations('admin.global.nav.users'); diff --git a/apps/web/src/app/global.css b/apps/web/src/app/global.css index 27f4d2ad0..ef1bf8194 100644 --- a/apps/web/src/app/global.css +++ b/apps/web/src/app/global.css @@ -22,8 +22,8 @@ --muted-foreground: oklch(0.45 0 0); --accent: oklch(0.95 0 0); --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.577 0.245 27.325); + --destructive: oklch(0.6 0.2 24.45); + --warn: oklch(0.57 0.13 82.37); --border: oklch(0.9 0 0); --input: oklch(0.9 0 0); --ring: oklch(0.7 0.16 262.61); @@ -56,8 +56,8 @@ --muted: oklch(0.22 0 0); --muted-foreground: oklch(0.7 0 0); --accent: oklch(0.28 0 0); - --destructive: oklch(0.704 0.191 22.216); - --destructive-foreground: oklch(0.704 0.191 22.216); + --destructive: oklch(0.62 0.2 25.35); + --warn: oklch(0.76 0.18 81.84); --border: oklch(0.28 0 0); --input: oklch(0.28 0 0); --ring: oklch(0.51 0.16 262.61); @@ -96,7 +96,7 @@ --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); + --color-warn: var(--warn); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); diff --git a/apps/web/src/locales/@vitnode/core/en.json b/apps/web/src/locales/@vitnode/core/en.json index 0890d7c28..a3b9c9560 100644 --- a/apps/web/src/locales/@vitnode/core/en.json +++ b/apps/web/src/locales/@vitnode/core/en.json @@ -170,6 +170,19 @@ "success": "Cache cleared successfully.", "confirm": "Yes, clear cache" } + }, + "logs": { + "title": "System Logs", + "id": "ID", + "created_at": "Created At", + "plugin": "Plugin", + "content": "Content", + "type": "Type", + "types": { + "warn": "Warning", + "error": "Error", + "debug": "Debug" + } } } } diff --git a/packages/vitnode/scripts/shared/file-utils.ts b/packages/vitnode/scripts/shared/file-utils.ts index 764c24ace..957583aa5 100644 --- a/packages/vitnode/scripts/shared/file-utils.ts +++ b/packages/vitnode/scripts/shared/file-utils.ts @@ -22,6 +22,7 @@ const relativeImportRegex = /import\s+(?:(?:{[^}]*})|(?:[^{}\s,]*))?\s*(?:,\s*(?:{[^}]*}))?\s*from\s+['"]([./]+[^'"]*)['"]/g; const atImportRegex = /import\s+(?:(?:{[^}]*})|(?:[^{}\s,]*))?\s*(?:,\s*(?:{[^}]*}))?\s*from\s+['"](@\/[^'"]*)['"]/g; +const dynamicAtImportRegex = /import\s*\(\s*['"](@\/[^'"]*)['"]\s*\)/g; const jsExtensionRegex = /\.(js|jsx|ts|tsx)$/; const pageFileRegex = /^page\.(tsx|ts|jsx|js)$/i; @@ -105,6 +106,20 @@ export const transformFileImports = ( }, ); + // Handle dynamic imports (React.lazy) with @/ paths + transformedContent = transformedContent.replace( + dynamicAtImportRegex, + (match, importPath: string) => { + // Remove '@/' prefix and any file extensions + const cleanPath = importPath + .replace(/^@\//, '') + .replace(jsExtensionRegex, ''); + // Return the package import format + + return match.replace(importPath, `${pluginName}/${cleanPath}`); + }, + ); + return transformedContent; }; diff --git a/packages/vitnode/src/api/lib/logger-middleware.ts b/packages/vitnode/src/api/lib/logger-middleware.ts index c64b4aa08..929c0e2a9 100644 --- a/packages/vitnode/src/api/lib/logger-middleware.ts +++ b/packages/vitnode/src/api/lib/logger-middleware.ts @@ -6,7 +6,6 @@ import { core_logs, type CoreLogsType } from '@/database/logs'; export interface LoggerMiddlewareType { debug: (content: string) => void; error: (content: string) => void; - info: (content: string) => void; warn: (content: string) => void; } @@ -35,12 +34,8 @@ export const loggerMiddleware = (c: Context): LoggerMiddlewareType => { console.warn( `\x1b[34m[VitNode]\x1b[0m \x1b[33mWarning (${pluginId})\x1b[0m: ${msg}`, ), - info: msg => - console.info( - `\x1b[34m[VitNode]\x1b[0m \x1b[36mInfo (${pluginId})\x1b[0m: ${msg}`, - ), }; - (loggers[type] ?? loggers.info)(content); + (loggers[type] ?? loggers.debug)(content); }; return { @@ -50,9 +45,6 @@ export const loggerMiddleware = (c: Context): LoggerMiddlewareType => { error: (content: string) => { void logToDbAndConsole(content, 'error'); }, - info: (content: string) => { - void logToDbAndConsole(content, 'info'); - }, warn: (content: string) => { void logToDbAndConsole(content, 'warn'); }, diff --git a/packages/vitnode/src/api/modules/admin/admin.module.ts b/packages/vitnode/src/api/modules/admin/admin.module.ts index e3d3ed8eb..81cc2e8df 100644 --- a/packages/vitnode/src/api/modules/admin/admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/admin.module.ts @@ -1,6 +1,7 @@ import { buildModule } from '@/api/lib/module'; import { CONFIG_PLUGIN } from '@/config'; +import { debugAdminModule } from './debug/debug.admin.module'; import { sessionAdminRoute } from './routes/session.route'; import { usersAdminModule } from './users/users.admin.module'; @@ -8,5 +9,5 @@ export const adminModule = buildModule({ ...CONFIG_PLUGIN, name: 'admin', routes: [sessionAdminRoute], - modules: [usersAdminModule], + modules: [usersAdminModule, debugAdminModule], }); diff --git a/packages/vitnode/src/api/modules/admin/debug/debug.admin.module.ts b/packages/vitnode/src/api/modules/admin/debug/debug.admin.module.ts new file mode 100644 index 000000000..316227e1c --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/debug/debug.admin.module.ts @@ -0,0 +1,9 @@ +import { CONFIG_PLUGIN } from '../../../../config'; +import { buildModule } from '../../../lib/module'; +import { logsDebugAdminRoute } from './routes/logs.route'; + +export const debugAdminModule = buildModule({ + ...CONFIG_PLUGIN, + name: 'debug', + routes: [logsDebugAdminRoute], +}); diff --git a/packages/vitnode/src/api/modules/admin/debug/routes/logs.route.ts b/packages/vitnode/src/api/modules/admin/debug/routes/logs.route.ts new file mode 100644 index 000000000..a9d10cdab --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/debug/routes/logs.route.ts @@ -0,0 +1,80 @@ +import { z } from 'zod'; + +import { CONFIG_PLUGIN } from '@/config'; +import { core_logs, coreLogsType } from '@/database/logs'; + +import { buildRoute } from '../../../../lib/route'; +import { + withPagination, + zodPaginationPageInfo, + zodPaginationQuery, +} from '../../../../lib/with-pagination'; + +export const logsDebugAdminRoute = buildRoute({ + ...CONFIG_PLUGIN, + route: { + method: 'get', + description: 'Get Admin Debug Logs', + path: '/logs', + request: { + query: zodPaginationQuery.extend({ + order: z.enum(['asc', 'desc']).optional(), + orderBy: z.enum(['type', 'createdAt', 'pluginId']).optional(), + }), + }, + responses: { + 200: { + content: { + 'application/json': { + schema: z.object({ + edges: z.array( + z.object({ + id: z.number(), + pluginId: z.string(), + type: z.enum(coreLogsType.enumValues), + content: z.string(), + createdAt: z.date(), + ipAddress: z.string(), + }), + ), + pageInfo: zodPaginationPageInfo, + }), + }, + }, + description: 'List of users', + }, + }, + }, + handler: async c => { + const query = c.req.valid('query'); + const data = await withPagination({ + params: { + query, + }, + primaryCursor: core_logs.id, + query: async ({ limit, where, orderBy }) => + await c + .get('db') + .select({ + id: core_logs.id, + pluginId: core_logs.pluginId, + type: core_logs.type, + content: core_logs.content, + createdAt: core_logs.createdAt, + ipAddress: core_logs.ipAddress, + }) + .from(core_logs) + .where(where) + .orderBy(orderBy) + .limit(limit), + table: core_logs, + orderBy: { + column: query.orderBy ? core_logs[query.orderBy] : core_logs.createdAt, + order: query.order ?? 'desc', + }, + c, + }); + + return c.json(data); + }, +}); diff --git a/packages/vitnode/src/api/modules/admin/routes/session.route.ts b/packages/vitnode/src/api/modules/admin/routes/session.route.ts index ca719bb51..4de23c1eb 100644 --- a/packages/vitnode/src/api/modules/admin/routes/session.route.ts +++ b/packages/vitnode/src/api/modules/admin/routes/session.route.ts @@ -9,10 +9,6 @@ export const sessionAdminRoute = buildRoute({ route: { method: 'get', description: 'Verify admin session', - pluginConfig: { - id: 'core', - name: 'Core', - }, path: '/session', responses: { 200: { diff --git a/packages/vitnode/src/api/modules/users/routes/test.route.ts b/packages/vitnode/src/api/modules/users/routes/test.route.ts index 8e82ffc15..817ac56aa 100644 --- a/packages/vitnode/src/api/modules/users/routes/test.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/test.route.ts @@ -39,7 +39,7 @@ export const testRoute = buildRoute({ // throw new Error('Test error'); - c.get('log').warn('This is a test warning log'); + // c.get('log').warn('This is a test warn log'); return c.text('test'); }, diff --git a/packages/vitnode/src/app_admin/core/debug/page.tsx b/packages/vitnode/src/app_admin/core/debug/page.tsx index 99c9a2ce5..1447ad4fa 100644 --- a/packages/vitnode/src/app_admin/core/debug/page.tsx +++ b/packages/vitnode/src/app_admin/core/debug/page.tsx @@ -1,9 +1,16 @@ import { getTranslations } from 'next-intl/server'; +import React from 'react'; import { I18nProvider } from '@/components/i18n-provider'; +import { DataTableSkeleton } from '@/components/table/data-table'; import { HeaderContent } from '@/components/ui/header-content'; import { ClearCacheAction } from '@/views/admin/views/core/debug/actions/clear-cache/clear-cache'; -import { DebugAdminView } from '@/views/admin/views/core/debug/debug-admin-view'; + +const DebugAdminView = React.lazy(async () => + import('@/views/admin/views/core/debug/debug-admin-view').then(module => ({ + default: module.DebugAdminView, + })), +); export const generateMetadata = async () => { const t = await getTranslations('admin.debug'); @@ -14,7 +21,9 @@ export const generateMetadata = async () => { }; }; -export default async function Page() { +export default async function Page( + props: React.ComponentProps, +) { const t = await getTranslations('admin.debug'); return ( @@ -24,7 +33,10 @@ export default async function Page() { - + + }> + + ); diff --git a/packages/vitnode/src/app_admin/core/users/page.tsx b/packages/vitnode/src/app_admin/core/users/page.tsx index 551042280..a78dc48d8 100644 --- a/packages/vitnode/src/app_admin/core/users/page.tsx +++ b/packages/vitnode/src/app_admin/core/users/page.tsx @@ -5,7 +5,12 @@ import React from 'react'; import { DataTableSkeleton } from '@/components/table/data-table'; import { HeaderContent } from '@/components/ui/header-content'; -import { UsersAdminView } from '@/views/admin/views/core/users/users-admin-view'; + +const UsersAdminView = React.lazy(async () => + import('@/views/admin/views/core/users/users-admin-view').then(module => ({ + default: module.UsersAdminView, + })), +); export const generateMetadata = async (): Promise => { const t = await getTranslations('admin.global.nav.users'); diff --git a/packages/vitnode/src/components/date-format.tsx b/packages/vitnode/src/components/date-format.tsx index f85bde299..039c0480f 100644 --- a/packages/vitnode/src/components/date-format.tsx +++ b/packages/vitnode/src/components/date-format.tsx @@ -12,8 +12,10 @@ import { export const DateFormat = ({ date, updateInterval, + showFullDate, }: { date: Date | string; + showFullDate?: boolean; updateInterval?: number; }) => { const dateToFormat = typeof date === 'string' ? new Date(date) : date; @@ -34,6 +36,10 @@ export const DateFormat = ({ minute: 'numeric', }); + if (showFullDate) { + return fullDate; + } + // When date is < 7 days if (now.getTime() - dateToFormat.getTime() < 604800000) { return ( diff --git a/packages/vitnode/src/database/logs.ts b/packages/vitnode/src/database/logs.ts index 25e794a70..9f510e327 100644 --- a/packages/vitnode/src/database/logs.ts +++ b/packages/vitnode/src/database/logs.ts @@ -1,18 +1,13 @@ import { pgEnum, pgTable } from 'drizzle-orm/pg-core'; -export const coreLogsType = pgEnum('coreLogsType', [ - 'info', - 'warn', - 'error', - 'debug', -]); +export const coreLogsType = pgEnum('coreLogsType', ['warn', 'error', 'debug']); export type CoreLogsType = (typeof coreLogsType.enumValues)[number]; export const core_logs = pgTable('core_logs', t => ({ id: t.serial().primaryKey(), pluginId: t.varchar({ length: 255 }).notNull(), - type: coreLogsType().notNull().default('info'), + type: coreLogsType().notNull(), content: t.text().notNull(), createdAt: t.timestamp().notNull().defaultNow(), ipAddress: t.varchar({ length: 45 }).notNull(), diff --git a/packages/vitnode/src/globals.css b/packages/vitnode/src/globals.css index 507c9d21e..e2c711cdd 100644 --- a/packages/vitnode/src/globals.css +++ b/packages/vitnode/src/globals.css @@ -24,6 +24,7 @@ --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); + --color-warn: var(--warn); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); diff --git a/packages/vitnode/src/locales/en.json b/packages/vitnode/src/locales/en.json index 0890d7c28..a3b9c9560 100644 --- a/packages/vitnode/src/locales/en.json +++ b/packages/vitnode/src/locales/en.json @@ -170,6 +170,19 @@ "success": "Cache cleared successfully.", "confirm": "Yes, clear cache" } + }, + "logs": { + "title": "System Logs", + "id": "ID", + "created_at": "Created At", + "plugin": "Plugin", + "content": "Content", + "type": "Type", + "types": { + "warn": "Warning", + "error": "Error", + "debug": "Debug" + } } } } diff --git a/packages/vitnode/src/views/admin/views/core/debug/debug-admin-view.tsx b/packages/vitnode/src/views/admin/views/core/debug/debug-admin-view.tsx index 53c4c0cd3..47f634998 100644 --- a/packages/vitnode/src/views/admin/views/core/debug/debug-admin-view.tsx +++ b/packages/vitnode/src/views/admin/views/core/debug/debug-admin-view.tsx @@ -1,3 +1,102 @@ -export const DebugAdminView = () => { - return
DebugAdminView
; +import { TriangleAlertIcon, XIcon } from 'lucide-react'; +import { getTranslations } from 'next-intl/server'; + +import { debugAdminModule } from '@/api/modules/admin/debug/debug.admin.module'; +import { DateFormat } from '@/components/date-format'; +import { DataTable } from '@/components/table/data-table'; +import { Badge } from '@/components/ui/badge'; +import { fetcher } from '@/lib/fetcher'; + +export const DebugAdminView = async ({ + searchParams, +}: { + searchParams: Promise>; +}) => { + const t = await getTranslations('admin.debug.logs'); + const query = await searchParams; + const res = await fetcher(debugAdminModule, { + prefixPath: '/admin', + path: '/logs', + method: 'get', + module: 'debug', + args: { + query, + }, + withPagination: true, + }); + const data = await res.json(); + + return ( + { + if (row.type === 'warn') { + return ( + + {t(`types.${row.type}`)} + + ); + } + + if (row.type === 'error') { + return ( + + {t(`types.${row.type}`)} + + ); + } + + return {t(`types.${row.type}`)}; + }, + }, + { + id: 'pluginId', + label: t('plugin'), + className: 'w-48', + }, + { + id: 'createdAt', + label: t('created_at'), + cell: ({ row }) => , + }, + { + id: 'content', + label: t('content'), + cell: ({ row }) => { + const CHARACTERS = 50; + const content = row.content; + const isLong = content.length > CHARACTERS; + const displayContent = isLong + ? content.slice(0, CHARACTERS) + '...' + : content; + + return {displayContent}; + }, + }, + { + id: 'actions', + label: '', + cell: ({ row }) => actions, + }, + ]} + edges={data.edges.map(edge => ({ ...edge }))} + order={{ + columns: ['createdAt', 'pluginId', 'type'], + defaultOrder: { + column: 'createdAt', + order: 'desc', + }, + }} + pageInfo={data.pageInfo} + /> + ); }; diff --git a/plugins/blog/src/app_admin/blog/categories/page.tsx b/plugins/blog/src/app_admin/blog/categories/page.tsx index f7d85148c..a0ce4eae8 100644 --- a/plugins/blog/src/app_admin/blog/categories/page.tsx +++ b/plugins/blog/src/app_admin/blog/categories/page.tsx @@ -7,7 +7,12 @@ import { getTranslations } from 'next-intl/server'; import React from 'react'; import { ActionsCategoriesAdmin } from '@/views/admin/categories/actions/actions'; -import { CategoriesAdminView } from '@/views/admin/categories/categories-admin-view'; + +const CategoriesAdminView = React.lazy(async () => + import('@/views/admin/categories/categories-admin-view').then(mod => ({ + default: mod.CategoriesAdminView, + })), +); export const generateMetadata = async (): Promise => { const t = await getTranslations('@vitnode/blog.admin.nav'); diff --git a/plugins/blog/src/app_admin/blog/posts/page.tsx b/plugins/blog/src/app_admin/blog/posts/page.tsx index 1b8872400..d5353ff54 100644 --- a/plugins/blog/src/app_admin/blog/posts/page.tsx +++ b/plugins/blog/src/app_admin/blog/posts/page.tsx @@ -7,7 +7,12 @@ import { getTranslations } from 'next-intl/server'; import React from 'react'; import { ActionsPostsAdmin } from '@/views/admin/posts/actions/actions'; -import { PostsAdminView } from '@/views/admin/posts/posts-admin-view'; + +const PostsAdminView = React.lazy(async () => + import('@/views/admin/posts/posts-admin-view').then(mod => ({ + default: mod.PostsAdminView, + })), +); export const generateMetadata = async (): Promise => { const t = await getTranslations('@vitnode/blog.admin.nav'); diff --git a/plugins/blog/src/globals.css b/plugins/blog/src/globals.css index 507c9d21e..e2c711cdd 100644 --- a/plugins/blog/src/globals.css +++ b/plugins/blog/src/globals.css @@ -24,6 +24,7 @@ --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); + --color-warn: var(--warn); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring);