From 089c93e649ed72553ff17ae74e38d136f67dfd10 Mon Sep 17 00:00:00 2001 From: JohnDoe6345789 Date: Thu, 25 Dec 2025 18:01:03 +0000 Subject: [PATCH] feat(security): add comprehensive security scanning functions and patterns - Implemented severity color and icon helpers for better UI representation. - Created a set of security patterns for JavaScript, Lua, and SQL injection detection. - Developed scanning functions for JavaScript, HTML, JSON, and Lua to identify vulnerabilities. - Added sanitization utility to clean user input based on content type. - Introduced types for security scan results and issues to standardize output. - Enhanced overall severity calculation logic to determine the highest risk level from detected issues. --- frontends/nextjs/src/lib/database-new.ts | 3 - frontends/nextjs/src/lib/database.ts | 1361 +---------------- frontends/nextjs/src/lib/db/Database.ts | 934 ----------- .../lib/db/app-config/set-app-config.test.ts | 2 +- frontends/nextjs/src/lib/db/dbal-client.ts | 17 + .../functions/credentials/get-credentials.ts | 14 - .../get-password-change-timestamps.ts | 19 - .../src/lib/db/functions/credentials/index.ts | 5 - .../functions/credentials/set-credential.ts | 19 - .../set-password-change-timestamps.ts | 14 - .../credentials/verify-credentials.ts | 14 - .../src/lib/db/functions/hash-password.ts | 13 - .../nextjs/src/lib/db/functions/index.ts | 25 - .../lib/db/functions/initialize-database.ts | 14 - .../functions/lua-scripts/add-lua-script.ts | 19 - .../lua-scripts/delete-lua-script.ts | 9 - .../functions/lua-scripts/get-lua-scripts.ts | 18 - .../src/lib/db/functions/lua-scripts/index.ts | 5 - .../functions/lua-scripts/set-lua-scripts.ts | 22 - .../lua-scripts/update-lua-script.ts | 21 - .../src/lib/db/functions/pages/add-page.ts | 20 - .../src/lib/db/functions/pages/delete-page.ts | 9 - .../src/lib/db/functions/pages/get-pages.ts | 19 - .../src/lib/db/functions/pages/index.ts | 5 - .../src/lib/db/functions/pages/set-pages.ts | 23 - .../src/lib/db/functions/pages/update-page.ts | 22 - .../lib/db/functions/schemas/add-schema.ts | 22 - .../lib/db/functions/schemas/delete-schema.ts | 9 - .../lib/db/functions/schemas/get-schemas.ts | 21 - .../src/lib/db/functions/schemas/index.ts | 5 - .../lib/db/functions/schemas/set-schemas.ts | 25 - .../lib/db/functions/schemas/update-schema.ts | 24 - .../lib/db/functions/tenants/add-tenant.ts | 18 - .../lib/db/functions/tenants/delete-tenant.ts | 9 - .../lib/db/functions/tenants/get-tenants.ts | 17 - .../src/lib/db/functions/tenants/index.ts | 5 - .../lib/db/functions/tenants/set-tenants.ts | 21 - .../lib/db/functions/tenants/update-tenant.ts | 19 - .../src/lib/db/functions/users/add-user.ts | 24 - .../src/lib/db/functions/users/delete-user.ts | 9 - .../src/lib/db/functions/users/get-users.ts | 21 - .../src/lib/db/functions/users/index.ts | 5 - .../src/lib/db/functions/users/set-users.ts | 27 - .../src/lib/db/functions/users/update-user.ts | 23 - .../src/lib/db/functions/verify-password.ts | 12 - .../db/functions/workflows/add-workflow.ts | 19 - .../db/functions/workflows/delete-workflow.ts | 9 - .../db/functions/workflows/get-workflows.ts | 18 - .../src/lib/db/functions/workflows/index.ts | 5 - .../db/functions/workflows/set-workflows.ts | 22 - .../db/functions/workflows/update-workflow.ts | 21 - .../get-god-credentials-expiry-duration.ts | 2 +- .../get-god-credentials-expiry.ts | 2 +- .../god-credentials/set-first-login-flag.ts | 2 +- .../should-show-god-credentials.ts | 2 +- frontends/nextjs/src/lib/db/index.ts | 58 + .../lib/db/packages/get-installed-packages.ts | 2 +- .../src/lib/db/packages/get-package-data.ts | 4 +- .../src/lib/db/packages/install-package.ts | 2 +- .../lib/db/packages/set-installed-packages.ts | 2 +- .../src/lib/db/smtp-config/get-smtp-config.ts | 2 +- .../src/lib/db/smtp-config/set-smtp-config.ts | 2 +- .../nextjs/src/lib/lua/LuaSnippetUtils.ts | 70 + frontends/nextjs/src/lib/lua/lua-snippets.ts | 1024 +------------ .../src/lib/lua/snippets/lua-snippets-data.ts | 981 ++++++++++++ .../functions/helpers/get-severity-color.ts | 24 + .../functions/helpers/get-severity-icon.ts | 24 + .../lib/security/functions/helpers/index.ts | 7 + .../src/lib/security/functions/index.ts | 27 + .../lib/security/functions/patterns/index.ts | 8 + .../functions/patterns/javascript-patterns.ts | 184 +++ .../functions/patterns/lua-patterns.ts | 86 ++ .../functions/patterns/sql-patterns.ts | 37 + .../lib/security/functions/scanners/index.ts | 10 + .../functions/scanners/sanitize-input.ts | 37 + .../security/functions/scanners/scan-html.ts | 78 + .../functions/scanners/scan-javascript.ts | 61 + .../security/functions/scanners/scan-json.ts | 62 + .../security/functions/scanners/scan-lua.ts | 43 + .../src/lib/security/functions/types.ts | 28 + .../functions/utils/calculate-severity.ts | 29 + .../functions/utils/get-line-number.ts | 14 + .../src/lib/security/functions/utils/index.ts | 7 + frontends/nextjs/src/lib/security/types.ts | 23 + 84 files changed, 1959 insertions(+), 4041 deletions(-) delete mode 100644 frontends/nextjs/src/lib/database-new.ts delete mode 100644 frontends/nextjs/src/lib/db/Database.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/credentials/get-credentials.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/credentials/get-password-change-timestamps.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/credentials/index.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/credentials/set-credential.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/credentials/set-password-change-timestamps.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/credentials/verify-credentials.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/hash-password.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/index.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/initialize-database.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/lua-scripts/add-lua-script.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/lua-scripts/delete-lua-script.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/lua-scripts/get-lua-scripts.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/lua-scripts/index.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/lua-scripts/set-lua-scripts.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/lua-scripts/update-lua-script.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/pages/add-page.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/pages/delete-page.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/pages/get-pages.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/pages/index.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/pages/set-pages.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/pages/update-page.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/schemas/add-schema.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/schemas/delete-schema.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/schemas/get-schemas.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/schemas/index.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/schemas/set-schemas.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/schemas/update-schema.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/tenants/add-tenant.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/tenants/delete-tenant.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/tenants/get-tenants.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/tenants/index.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/tenants/set-tenants.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/tenants/update-tenant.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/users/add-user.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/users/delete-user.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/users/get-users.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/users/index.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/users/set-users.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/users/update-user.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/verify-password.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/workflows/add-workflow.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/workflows/delete-workflow.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/workflows/get-workflows.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/workflows/index.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/workflows/set-workflows.ts delete mode 100644 frontends/nextjs/src/lib/db/functions/workflows/update-workflow.ts create mode 100644 frontends/nextjs/src/lib/lua/LuaSnippetUtils.ts create mode 100644 frontends/nextjs/src/lib/lua/snippets/lua-snippets-data.ts create mode 100644 frontends/nextjs/src/lib/security/functions/helpers/get-severity-color.ts create mode 100644 frontends/nextjs/src/lib/security/functions/helpers/get-severity-icon.ts create mode 100644 frontends/nextjs/src/lib/security/functions/helpers/index.ts create mode 100644 frontends/nextjs/src/lib/security/functions/index.ts create mode 100644 frontends/nextjs/src/lib/security/functions/patterns/index.ts create mode 100644 frontends/nextjs/src/lib/security/functions/patterns/javascript-patterns.ts create mode 100644 frontends/nextjs/src/lib/security/functions/patterns/lua-patterns.ts create mode 100644 frontends/nextjs/src/lib/security/functions/patterns/sql-patterns.ts create mode 100644 frontends/nextjs/src/lib/security/functions/scanners/index.ts create mode 100644 frontends/nextjs/src/lib/security/functions/scanners/sanitize-input.ts create mode 100644 frontends/nextjs/src/lib/security/functions/scanners/scan-html.ts create mode 100644 frontends/nextjs/src/lib/security/functions/scanners/scan-javascript.ts create mode 100644 frontends/nextjs/src/lib/security/functions/scanners/scan-json.ts create mode 100644 frontends/nextjs/src/lib/security/functions/scanners/scan-lua.ts create mode 100644 frontends/nextjs/src/lib/security/functions/types.ts create mode 100644 frontends/nextjs/src/lib/security/functions/utils/calculate-severity.ts create mode 100644 frontends/nextjs/src/lib/security/functions/utils/get-line-number.ts create mode 100644 frontends/nextjs/src/lib/security/functions/utils/index.ts create mode 100644 frontends/nextjs/src/lib/security/types.ts diff --git a/frontends/nextjs/src/lib/database-new.ts b/frontends/nextjs/src/lib/database-new.ts deleted file mode 100644 index 5e05baa45..000000000 --- a/frontends/nextjs/src/lib/database-new.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file is no longer needed since database-prisma.ts has been renamed to database.ts -// Keeping for reference only -export * from './database' diff --git a/frontends/nextjs/src/lib/database.ts b/frontends/nextjs/src/lib/database.ts index 70b357023..97fda0413 100644 --- a/frontends/nextjs/src/lib/database.ts +++ b/frontends/nextjs/src/lib/database.ts @@ -1,1359 +1,10 @@ -import { prisma } from './prisma' -import type { - User, - Workflow, - LuaScript, - PageConfig, - AppConfiguration, - Comment, - Tenant, - PowerTransferRequest, -} from '../types/level-types' -import type { ModelSchema } from './types/schema-types' -import type { InstalledPackage } from './package-types' -import type { SMTPConfig } from './password-utils' - /** - * Database class - provides data access layer + * Database module - re-exports from @/lib/db * - * NOTE: DBAL integration is available server-side only. - * For server-side code (API routes, server components), import and use database-dbal.server.ts directly. - * This class currently uses Prisma directly but maintains compatibility with DBAL for server contexts. + * This file exists for backward compatibility. + * Prefer importing directly from '@/lib/db' for new code. */ -export interface CssCategory { - name: string - classes: string[] -} - -export interface DropdownConfig { - id: string - name: string - label: string - options: Array<{ value: string; label: string }> -} - -export interface DatabaseSchema { - users: User[] - credentials: Record - workflows: Workflow[] - luaScripts: LuaScript[] - pages: PageConfig[] - schemas: ModelSchema[] - appConfig: AppConfiguration - comments: Comment[] - componentHierarchy: Record - componentConfigs: Record - godCredentialsExpiry: number - passwordChangeTimestamps: Record - firstLoginFlags: Record - godCredentialsExpiryDuration: number - cssClasses: CssCategory[] - dropdownConfigs: DropdownConfig[] - tenants: Tenant[] - powerTransferRequests: PowerTransferRequest[] - smtpConfig: SMTPConfig - passwordResetTokens: Record -} - -export interface ComponentNode { - id: string - type: string - parentId?: string - childIds: string[] - order: number - pageId: string -} - -export interface ComponentConfig { - id: string - componentId: string - props: Record - styles: Record - events: Record - conditionalRendering?: { - condition: string - luaScriptId?: string - } -} - -export const DB_KEYS = { - USERS: 'db_users', - CREDENTIALS: 'db_credentials', - WORKFLOWS: 'db_workflows', - LUA_SCRIPTS: 'db_lua_scripts', - PAGES: 'db_pages', - SCHEMAS: 'db_schemas', - APP_CONFIG: 'db_app_config', - COMMENTS: 'db_comments', - COMPONENT_HIERARCHY: 'db_component_hierarchy', - COMPONENT_CONFIGS: 'db_component_configs', - GOD_CREDENTIALS_EXPIRY: 'db_god_credentials_expiry', - PASSWORD_CHANGE_TIMESTAMPS: 'db_password_change_timestamps', - FIRST_LOGIN_FLAGS: 'db_first_login_flags', - GOD_CREDENTIALS_EXPIRY_DURATION: 'db_god_credentials_expiry_duration', - CSS_CLASSES: 'db_css_classes', - DROPDOWN_CONFIGS: 'db_dropdown_configs', - INSTALLED_PACKAGES: 'db_installed_packages', - PACKAGE_DATA: 'db_package_data', - TENANTS: 'db_tenants', - POWER_TRANSFER_REQUESTS: 'db_power_transfer_requests', - SMTP_CONFIG: 'db_smtp_config', - PASSWORD_RESET_TOKENS: 'db_password_reset_tokens', -} as const - -export async function hashPassword(password: string): Promise { - const encoder = new TextEncoder() - const data = encoder.encode(password) - const hashBuffer = await crypto.subtle.digest('SHA-512', data) - const hashArray = Array.from(new Uint8Array(hashBuffer)) - const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('') - return hashHex -} - -export async function verifyPassword(password: string, hash: string): Promise { - const inputHash = await hashPassword(password) - return inputHash === hash -} - -export class Database { - /** - * Initialize database connection - * Note: DBAL can be initialized separately in server contexts via database-dbal.server.ts - */ - static async initializeDatabase(): Promise { - try { - // Test Prisma connection - await prisma.$connect() - console.log('Database initialized successfully') - } catch (error) { - console.error('Failed to initialize database:', error) - throw error - } - } - - // User operations - static async getUsers(): Promise { - const users = await prisma.user.findMany() - return users.map(u => ({ - id: u.id, - username: u.username, - email: u.email, - role: u.role as any, - profilePicture: u.profilePicture || undefined, - bio: u.bio || undefined, - createdAt: Number(u.createdAt), - tenantId: u.tenantId || undefined, - isInstanceOwner: u.isInstanceOwner, - })) - } - - static async setUsers(users: User[]): Promise { - await prisma.user.deleteMany() - for (const user of users) { - await prisma.user.create({ - data: { - id: user.id, - username: user.username, - email: user.email, - role: user.role, - profilePicture: user.profilePicture, - bio: user.bio, - createdAt: BigInt(user.createdAt), - tenantId: user.tenantId, - isInstanceOwner: user.isInstanceOwner || false, - passwordChangeTimestamp: null, - firstLogin: false, - }, - }) - } - } - - static async addUser(user: User): Promise { - await prisma.user.create({ - data: { - id: user.id, - username: user.username, - email: user.email, - role: user.role, - profilePicture: user.profilePicture, - bio: user.bio, - createdAt: BigInt(user.createdAt), - tenantId: user.tenantId, - isInstanceOwner: user.isInstanceOwner || false, - passwordChangeTimestamp: null, - firstLogin: false, - }, - }) - } - - static async updateUser(userId: string, updates: Partial): Promise { - const data: any = {} - if (updates.username !== undefined) data.username = updates.username - if (updates.email !== undefined) data.email = updates.email - if (updates.role !== undefined) data.role = updates.role - if (updates.profilePicture !== undefined) data.profilePicture = updates.profilePicture - if (updates.bio !== undefined) data.bio = updates.bio - if (updates.tenantId !== undefined) data.tenantId = updates.tenantId - if (updates.isInstanceOwner !== undefined) data.isInstanceOwner = updates.isInstanceOwner - - await prisma.user.update({ - where: { id: userId }, - data, - }) - } - - static async deleteUser(userId: string): Promise { - await prisma.user.delete({ where: { id: userId } }) - } - - static async getCredentials(): Promise> { - const credentials = await prisma.credential.findMany() - const result: Record = {} - for (const cred of credentials) { - result[cred.username] = cred.passwordHash - } - return result - } - - static async setCredential(username: string, passwordHash: string): Promise { - await prisma.credential.upsert({ - where: { username }, - update: { passwordHash }, - create: { username, passwordHash }, - }) - - await prisma.user.update({ - where: { username }, - data: { passwordChangeTimestamp: BigInt(Date.now()) }, - }) - } - - static async getPasswordChangeTimestamps(): Promise> { - const users = await prisma.user.findMany({ - where: { passwordChangeTimestamp: { not: null } }, - select: { username: true, passwordChangeTimestamp: true }, - }) - const result: Record = {} - for (const user of users) { - if (user.passwordChangeTimestamp) { - result[user.username] = Number(user.passwordChangeTimestamp) - } - } - return result - } - - static async setPasswordChangeTimestamps(timestamps: Record): Promise { - for (const [username, timestamp] of Object.entries(timestamps)) { - await prisma.user.update({ - where: { username }, - data: { passwordChangeTimestamp: BigInt(timestamp) }, - }) - } - } - - static async verifyCredentials(username: string, password: string): Promise { - const credential = await prisma.credential.findUnique({ where: { username } }) - if (!credential) return false - return await verifyPassword(password, credential.passwordHash) - } - - static async getWorkflows(): Promise { - const workflows = await prisma.workflow.findMany() - return workflows.map(w => ({ - id: w.id, - name: w.name, - description: w.description || undefined, - nodes: JSON.parse(w.nodes), - edges: JSON.parse(w.edges), - enabled: w.enabled, - })) - } - - static async setWorkflows(workflows: Workflow[]): Promise { - await prisma.workflow.deleteMany() - for (const workflow of workflows) { - await prisma.workflow.create({ - data: { - id: workflow.id, - name: workflow.name, - description: workflow.description, - nodes: JSON.stringify(workflow.nodes), - edges: JSON.stringify(workflow.edges), - enabled: workflow.enabled, - }, - }) - } - } - - static async addWorkflow(workflow: Workflow): Promise { - await prisma.workflow.create({ - data: { - id: workflow.id, - name: workflow.name, - description: workflow.description, - nodes: JSON.stringify(workflow.nodes), - edges: JSON.stringify(workflow.edges), - enabled: workflow.enabled, - }, - }) - } - - static async updateWorkflow(workflowId: string, updates: Partial): Promise { - const data: any = {} - if (updates.name !== undefined) data.name = updates.name - if (updates.description !== undefined) data.description = updates.description - if (updates.nodes !== undefined) data.nodes = JSON.stringify(updates.nodes) - if (updates.edges !== undefined) data.edges = JSON.stringify(updates.edges) - if (updates.enabled !== undefined) data.enabled = updates.enabled - - await prisma.workflow.update({ - where: { id: workflowId }, - data, - }) - } - - static async deleteWorkflow(workflowId: string): Promise { - await prisma.workflow.delete({ where: { id: workflowId } }) - } - - static async getLuaScripts(): Promise { - const scripts = await prisma.luaScript.findMany() - return scripts.map(s => ({ - id: s.id, - name: s.name, - description: s.description || undefined, - code: s.code, - parameters: JSON.parse(s.parameters), - returnType: s.returnType || undefined, - })) - } - - static async setLuaScripts(scripts: LuaScript[]): Promise { - await prisma.luaScript.deleteMany() - for (const script of scripts) { - await prisma.luaScript.create({ - data: { - id: script.id, - name: script.name, - description: script.description, - code: script.code, - parameters: JSON.stringify(script.parameters), - returnType: script.returnType, - }, - }) - } - } - - static async addLuaScript(script: LuaScript): Promise { - await prisma.luaScript.create({ - data: { - id: script.id, - name: script.name, - description: script.description, - code: script.code, - parameters: JSON.stringify(script.parameters), - returnType: script.returnType, - }, - }) - } - - static async updateLuaScript(scriptId: string, updates: Partial): Promise { - const data: any = {} - if (updates.name !== undefined) data.name = updates.name - if (updates.description !== undefined) data.description = updates.description - if (updates.code !== undefined) data.code = updates.code - if (updates.parameters !== undefined) data.parameters = JSON.stringify(updates.parameters) - if (updates.returnType !== undefined) data.returnType = updates.returnType - - await prisma.luaScript.update({ - where: { id: scriptId }, - data, - }) - } - - static async deleteLuaScript(scriptId: string): Promise { - await prisma.luaScript.delete({ where: { id: scriptId } }) - } - - static async getPages(): Promise { - const pages = await prisma.pageConfig.findMany() - return pages.map(p => ({ - id: p.id, - path: p.path, - title: p.title, - level: p.level as any, - componentTree: JSON.parse(p.componentTree), - requiresAuth: p.requiresAuth, - requiredRole: (p.requiredRole as any) || undefined, - })) - } - - static async setPages(pages: PageConfig[]): Promise { - await prisma.pageConfig.deleteMany() - for (const page of pages) { - await prisma.pageConfig.create({ - data: { - id: page.id, - path: page.path, - title: page.title, - level: page.level, - componentTree: JSON.stringify(page.componentTree), - requiresAuth: page.requiresAuth, - requiredRole: page.requiredRole, - }, - }) - } - } - - static async addPage(page: PageConfig): Promise { - await prisma.pageConfig.create({ - data: { - id: page.id, - path: page.path, - title: page.title, - level: page.level, - componentTree: JSON.stringify(page.componentTree), - requiresAuth: page.requiresAuth, - requiredRole: page.requiredRole, - }, - }) - } - - static async updatePage(pageId: string, updates: Partial): Promise { - const data: any = {} - if (updates.path !== undefined) data.path = updates.path - if (updates.title !== undefined) data.title = updates.title - if (updates.level !== undefined) data.level = updates.level - if (updates.componentTree !== undefined) data.componentTree = JSON.stringify(updates.componentTree) - if (updates.requiresAuth !== undefined) data.requiresAuth = updates.requiresAuth - if (updates.requiredRole !== undefined) data.requiredRole = updates.requiredRole - - await prisma.pageConfig.update({ - where: { id: pageId }, - data, - }) - } - - static async deletePage(pageId: string): Promise { - await prisma.pageConfig.delete({ where: { id: pageId } }) - } - - static async getSchemas(): Promise { - const schemas = await prisma.modelSchema.findMany() - return schemas.map(s => ({ - name: s.name, - label: s.label || undefined, - labelPlural: s.labelPlural || undefined, - icon: s.icon || undefined, - fields: JSON.parse(s.fields), - listDisplay: s.listDisplay ? JSON.parse(s.listDisplay) : undefined, - listFilter: s.listFilter ? JSON.parse(s.listFilter) : undefined, - searchFields: s.searchFields ? JSON.parse(s.searchFields) : undefined, - ordering: s.ordering ? JSON.parse(s.ordering) : undefined, - })) - } - - static async setSchemas(schemas: ModelSchema[]): Promise { - await prisma.modelSchema.deleteMany() - for (const schema of schemas) { - await prisma.modelSchema.create({ - data: { - name: schema.name, - label: schema.label, - labelPlural: schema.labelPlural, - icon: schema.icon, - fields: JSON.stringify(schema.fields), - listDisplay: schema.listDisplay ? JSON.stringify(schema.listDisplay) : null, - listFilter: schema.listFilter ? JSON.stringify(schema.listFilter) : null, - searchFields: schema.searchFields ? JSON.stringify(schema.searchFields) : null, - ordering: schema.ordering ? JSON.stringify(schema.ordering) : null, - }, - }) - } - } - - static async addSchema(schema: ModelSchema): Promise { - await prisma.modelSchema.create({ - data: { - name: schema.name, - label: schema.label, - labelPlural: schema.labelPlural, - icon: schema.icon, - fields: JSON.stringify(schema.fields), - listDisplay: schema.listDisplay ? JSON.stringify(schema.listDisplay) : null, - listFilter: schema.listFilter ? JSON.stringify(schema.listFilter) : null, - searchFields: schema.searchFields ? JSON.stringify(schema.searchFields) : null, - ordering: schema.ordering ? JSON.stringify(schema.ordering) : null, - }, - }) - } - - static async updateSchema(schemaName: string, updates: Partial): Promise { - const data: any = {} - if (updates.label !== undefined) data.label = updates.label - if (updates.labelPlural !== undefined) data.labelPlural = updates.labelPlural - if (updates.icon !== undefined) data.icon = updates.icon - if (updates.fields !== undefined) data.fields = JSON.stringify(updates.fields) - if (updates.listDisplay !== undefined) data.listDisplay = JSON.stringify(updates.listDisplay) - if (updates.listFilter !== undefined) data.listFilter = JSON.stringify(updates.listFilter) - if (updates.searchFields !== undefined) data.searchFields = JSON.stringify(updates.searchFields) - if (updates.ordering !== undefined) data.ordering = JSON.stringify(updates.ordering) - - await prisma.modelSchema.update({ - where: { name: schemaName }, - data, - }) - } - - static async deleteSchema(schemaName: string): Promise { - await prisma.modelSchema.delete({ where: { name: schemaName } }) - } - - static async getAppConfig(): Promise { - const config = await prisma.appConfiguration.findFirst() - if (!config) return null - - return { - id: config.id, - name: config.name, - schemas: JSON.parse(config.schemas), - workflows: JSON.parse(config.workflows), - luaScripts: JSON.parse(config.luaScripts), - pages: JSON.parse(config.pages), - theme: JSON.parse(config.theme), - } - } - - static async setAppConfig(config: AppConfiguration): Promise { - await prisma.appConfiguration.deleteMany() - await prisma.appConfiguration.create({ - data: { - id: config.id, - name: config.name, - schemas: JSON.stringify(config.schemas), - workflows: JSON.stringify(config.workflows), - luaScripts: JSON.stringify(config.luaScripts), - pages: JSON.stringify(config.pages), - theme: JSON.stringify(config.theme), - }, - }) - } - - static async getComments(): Promise { - const comments = await prisma.comment.findMany() - return comments.map(c => ({ - id: c.id, - userId: c.userId, - content: c.content, - createdAt: Number(c.createdAt), - updatedAt: c.updatedAt ? Number(c.updatedAt) : undefined, - parentId: c.parentId || undefined, - })) - } - - static async setComments(comments: Comment[]): Promise { - await prisma.comment.deleteMany() - for (const comment of comments) { - await prisma.comment.create({ - data: { - id: comment.id, - userId: comment.userId, - content: comment.content, - createdAt: BigInt(comment.createdAt), - updatedAt: comment.updatedAt ? BigInt(comment.updatedAt) : null, - parentId: comment.parentId, - }, - }) - } - } - - static async addComment(comment: Comment): Promise { - await prisma.comment.create({ - data: { - id: comment.id, - userId: comment.userId, - content: comment.content, - createdAt: BigInt(comment.createdAt), - updatedAt: comment.updatedAt ? BigInt(comment.updatedAt) : null, - parentId: comment.parentId, - }, - }) - } - - static async updateComment(commentId: string, updates: Partial): Promise { - const data: any = {} - if (updates.content !== undefined) data.content = updates.content - if (updates.updatedAt !== undefined) data.updatedAt = BigInt(updates.updatedAt) - - await prisma.comment.update({ - where: { id: commentId }, - data, - }) - } - - static async deleteComment(commentId: string): Promise { - await prisma.comment.delete({ where: { id: commentId } }) - } - - static async getComponentHierarchy(): Promise> { - const nodes = await prisma.componentNode.findMany() - const result: Record = {} - for (const node of nodes) { - result[node.id] = { - id: node.id, - type: node.type, - parentId: node.parentId || undefined, - childIds: JSON.parse(node.childIds), - order: node.order, - pageId: node.pageId, - } - } - return result - } - - static async setComponentHierarchy(hierarchy: Record): Promise { - await prisma.componentNode.deleteMany() - for (const node of Object.values(hierarchy)) { - await prisma.componentNode.create({ - data: { - id: node.id, - type: node.type, - parentId: node.parentId, - childIds: JSON.stringify(node.childIds), - order: node.order, - pageId: node.pageId, - }, - }) - } - } - - static async addComponentNode(node: ComponentNode): Promise { - await prisma.componentNode.create({ - data: { - id: node.id, - type: node.type, - parentId: node.parentId, - childIds: JSON.stringify(node.childIds), - order: node.order, - pageId: node.pageId, - }, - }) - } - - static async updateComponentNode(nodeId: string, updates: Partial): Promise { - const data: any = {} - if (updates.type !== undefined) data.type = updates.type - if (updates.parentId !== undefined) data.parentId = updates.parentId - if (updates.childIds !== undefined) data.childIds = JSON.stringify(updates.childIds) - if (updates.order !== undefined) data.order = updates.order - if (updates.pageId !== undefined) data.pageId = updates.pageId - - await prisma.componentNode.update({ - where: { id: nodeId }, - data, - }) - } - - static async deleteComponentNode(nodeId: string): Promise { - await prisma.componentNode.delete({ where: { id: nodeId } }) - } - - static async getComponentConfigs(): Promise> { - const configs = await prisma.componentConfig.findMany() - const result: Record = {} - for (const config of configs) { - result[config.id] = { - id: config.id, - componentId: config.componentId, - props: JSON.parse(config.props), - styles: JSON.parse(config.styles), - events: JSON.parse(config.events), - conditionalRendering: config.conditionalRendering ? JSON.parse(config.conditionalRendering) : undefined, - } - } - return result - } - - static async setComponentConfigs(configs: Record): Promise { - await prisma.componentConfig.deleteMany() - for (const config of Object.values(configs)) { - await prisma.componentConfig.create({ - data: { - id: config.id, - componentId: config.componentId, - props: JSON.stringify(config.props), - styles: JSON.stringify(config.styles), - events: JSON.stringify(config.events), - conditionalRendering: config.conditionalRendering ? JSON.stringify(config.conditionalRendering) : null, - }, - }) - } - } - - static async addComponentConfig(config: ComponentConfig): Promise { - await prisma.componentConfig.create({ - data: { - id: config.id, - componentId: config.componentId, - props: JSON.stringify(config.props), - styles: JSON.stringify(config.styles), - events: JSON.stringify(config.events), - conditionalRendering: config.conditionalRendering ? JSON.stringify(config.conditionalRendering) : null, - }, - }) - } - - static async updateComponentConfig(configId: string, updates: Partial): Promise { - const data: any = {} - if (updates.componentId !== undefined) data.componentId = updates.componentId - if (updates.props !== undefined) data.props = JSON.stringify(updates.props) - if (updates.styles !== undefined) data.styles = JSON.stringify(updates.styles) - if (updates.events !== undefined) data.events = JSON.stringify(updates.events) - if (updates.conditionalRendering !== undefined) data.conditionalRendering = JSON.stringify(updates.conditionalRendering) - - await prisma.componentConfig.update({ - where: { id: configId }, - data, - }) - } - - static async deleteComponentConfig(configId: string): Promise { - await prisma.componentConfig.delete({ where: { id: configId } }) - } - - static async seedDefaultData(): Promise { - const users = await this.getUsers() - const credentials = await this.getCredentials() - - if (users.length === 0) { - const defaultUsers: User[] = [ - { - id: 'user_supergod', - username: 'supergod', - email: 'supergod@builder.com', - role: 'supergod', - bio: 'Supreme administrator with multi-tenant control', - createdAt: Date.now(), - isInstanceOwner: true, - }, - { - id: 'user_god', - username: 'god', - email: 'god@builder.com', - role: 'god', - bio: 'System architect with full access to all levels', - createdAt: Date.now(), - }, - { - id: 'user_admin', - username: 'admin', - email: 'admin@builder.com', - role: 'admin', - bio: 'Administrator with data management access', - createdAt: Date.now(), - }, - { - id: 'user_demo', - username: 'demo', - email: 'demo@builder.com', - role: 'user', - bio: 'Demo user account', - createdAt: Date.now(), - }, - ] - - await this.setUsers(defaultUsers) - } - - if (Object.keys(credentials).length === 0) { - const { getScrambledPassword } = await import('./auth') - await this.setCredential('supergod', await hashPassword(getScrambledPassword('supergod'))) - await this.setCredential('god', await hashPassword(getScrambledPassword('god'))) - await this.setCredential('admin', await hashPassword(getScrambledPassword('admin'))) - await this.setCredential('demo', await hashPassword(getScrambledPassword('demo'))) - - await this.setFirstLoginFlag('supergod', true) - await this.setFirstLoginFlag('god', true) - await this.setFirstLoginFlag('admin', false) - await this.setFirstLoginFlag('demo', false) - } - - const appConfig = await this.getAppConfig() - if (!appConfig) { - const defaultConfig: AppConfiguration = { - id: 'app_001', - name: 'MetaBuilder App', - schemas: [], - workflows: [], - luaScripts: [], - pages: [], - theme: { - colors: {}, - fonts: {}, - }, - } - await this.setAppConfig(defaultConfig) - } - - const cssClasses = await this.getCssClasses() - if (cssClasses.length === 0) { - const defaultCssClasses: CssCategory[] = [ - { - name: 'Layout', - classes: ['flex', 'flex-col', 'flex-row', 'grid', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4', 'block', 'inline-block', 'inline', 'hidden'], - }, - { - name: 'Spacing', - classes: ['p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-6', 'p-8', 'm-0', 'm-1', 'm-2', 'm-3', 'm-4', 'm-6', 'm-8', 'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-6', 'gap-8'], - }, - { - name: 'Sizing', - classes: ['w-full', 'w-1/2', 'w-1/3', 'w-1/4', 'w-auto', 'h-full', 'h-screen', 'h-auto', 'min-h-screen', 'max-w-xs', 'max-w-sm', 'max-w-md', 'max-w-lg', 'max-w-xl', 'max-w-2xl', 'max-w-4xl', 'max-w-6xl', 'max-w-7xl'], - }, - { - name: 'Typography', - classes: ['text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'text-3xl', 'text-4xl', 'font-normal', 'font-medium', 'font-semibold', 'font-bold', 'text-left', 'text-center', 'text-right', 'uppercase', 'lowercase', 'capitalize'], - }, - { - name: 'Colors', - classes: ['text-primary', 'text-secondary', 'text-accent', 'text-muted-foreground', 'bg-primary', 'bg-secondary', 'bg-accent', 'bg-background', 'bg-card', 'bg-muted', 'border-primary', 'border-secondary', 'border-accent', 'border-border'], - }, - { - name: 'Borders', - classes: ['border', 'border-2', 'border-4', 'border-t', 'border-b', 'border-l', 'border-r', 'rounded', 'rounded-sm', 'rounded-md', 'rounded-lg', 'rounded-xl', 'rounded-2xl', 'rounded-full'], - }, - { - name: 'Effects', - classes: ['shadow', 'shadow-sm', 'shadow-md', 'shadow-lg', 'shadow-xl', 'hover:shadow-lg', 'opacity-0', 'opacity-50', 'opacity-75', 'opacity-100', 'transition', 'transition-all', 'duration-200', 'duration-300', 'duration-500'], - }, - { - name: 'Positioning', - classes: ['relative', 'absolute', 'fixed', 'sticky', 'top-0', 'bottom-0', 'left-0', 'right-0', 'z-10', 'z-20', 'z-30', 'z-40', 'z-50'], - }, - { - name: 'Alignment', - classes: ['items-start', 'items-center', 'items-end', 'justify-start', 'justify-center', 'justify-end', 'justify-between', 'justify-around', 'self-start', 'self-center', 'self-end'], - }, - { - name: 'Interactivity', - classes: ['cursor-pointer', 'cursor-default', 'pointer-events-none', 'select-none', 'hover:bg-accent', 'hover:text-accent-foreground', 'active:scale-95', 'disabled:opacity-50'], - }, - ] - await this.setCssClasses(defaultCssClasses) - } - - const dropdowns = await this.getDropdownConfigs() - if (dropdowns.length === 0) { - const defaultDropdowns: DropdownConfig[] = [ - { - id: 'dropdown_status', - name: 'status_options', - label: 'Status', - options: [ - { value: 'draft', label: 'Draft' }, - { value: 'published', label: 'Published' }, - { value: 'archived', label: 'Archived' }, - ], - }, - { - id: 'dropdown_priority', - name: 'priority_options', - label: 'Priority', - options: [ - { value: 'low', label: 'Low' }, - { value: 'medium', label: 'Medium' }, - { value: 'high', label: 'High' }, - { value: 'urgent', label: 'Urgent' }, - ], - }, - { - id: 'dropdown_category', - name: 'category_options', - label: 'Category', - options: [ - { value: 'general', label: 'General' }, - { value: 'technical', label: 'Technical' }, - { value: 'business', label: 'Business' }, - { value: 'personal', label: 'Personal' }, - ], - }, - ] - await this.setDropdownConfigs(defaultDropdowns) - } - } - - static async exportDatabase(): Promise { - const data: Partial = { - users: await this.getUsers(), - workflows: await this.getWorkflows(), - luaScripts: await this.getLuaScripts(), - pages: await this.getPages(), - schemas: await this.getSchemas(), - appConfig: (await this.getAppConfig()) || undefined, - comments: await this.getComments(), - componentHierarchy: await this.getComponentHierarchy(), - componentConfigs: await this.getComponentConfigs(), - } - return JSON.stringify(data, null, 2) - } - - static async importDatabase(jsonData: string): Promise { - try { - const data = JSON.parse(jsonData) as Partial - - if (data.users) await this.setUsers(data.users) - if (data.workflows) await this.setWorkflows(data.workflows) - if (data.luaScripts) await this.setLuaScripts(data.luaScripts) - if (data.pages) await this.setPages(data.pages) - if (data.schemas) await this.setSchemas(data.schemas) - if (data.appConfig) await this.setAppConfig(data.appConfig) - if (data.comments) await this.setComments(data.comments) - if (data.componentHierarchy) await this.setComponentHierarchy(data.componentHierarchy) - if (data.componentConfigs) await this.setComponentConfigs(data.componentConfigs) - } catch (error) { - throw new Error('Failed to import database: Invalid JSON') - } - } - - static async getGodCredentialsExpiry(): Promise { - const config = await prisma.systemConfig.findUnique({ where: { key: 'god_credentials_expiry' } }) - return config ? Number(config.value) : 0 - } - - static async setGodCredentialsExpiry(timestamp: number): Promise { - await prisma.systemConfig.upsert({ - where: { key: 'god_credentials_expiry' }, - update: { value: timestamp.toString() }, - create: { key: 'god_credentials_expiry', value: timestamp.toString() }, - }) - } - - static async getFirstLoginFlags(): Promise> { - const users = await prisma.user.findMany({ - select: { username: true, firstLogin: true }, - }) - const result: Record = {} - for (const user of users) { - result[user.username] = user.firstLogin - } - return result - } - - static async setFirstLoginFlag(username: string, isFirstLogin: boolean): Promise { - await prisma.user.update({ - where: { username }, - data: { firstLogin: isFirstLogin }, - }) - } - - static async shouldShowGodCredentials(): Promise { - const expiry = await this.getGodCredentialsExpiry() - const user = await prisma.user.findUnique({ - where: { username: 'god' }, - select: { passwordChangeTimestamp: true }, - }) - const godPasswordChangeTime = user?.passwordChangeTimestamp ? Number(user.passwordChangeTimestamp) : 0 - - if (expiry === 0) { - const duration = await this.getGodCredentialsExpiryDuration() - const expiryTime = Date.now() + duration - await this.setGodCredentialsExpiry(expiryTime) - return true - } - - if (godPasswordChangeTime > expiry) { - return false - } - - return Date.now() < expiry - } - - static async getGodCredentialsExpiryDuration(): Promise { - const config = await prisma.systemConfig.findUnique({ where: { key: 'god_credentials_expiry_duration' } }) - return config ? Number(config.value) : (60 * 60 * 1000) - } - - static async setGodCredentialsExpiryDuration(durationMs: number): Promise { - await prisma.systemConfig.upsert({ - where: { key: 'god_credentials_expiry_duration' }, - update: { value: durationMs.toString() }, - create: { key: 'god_credentials_expiry_duration', value: durationMs.toString() }, - }) - } - - static async resetGodCredentialsExpiry(): Promise { - const duration = await this.getGodCredentialsExpiryDuration() - const expiryTime = Date.now() + duration - await this.setGodCredentialsExpiry(expiryTime) - } - - static async getCssClasses(): Promise { - const categories = await prisma.cssCategory.findMany() - return categories.map(c => ({ - name: c.name, - classes: JSON.parse(c.classes), - })) - } - - static async setCssClasses(classes: CssCategory[]): Promise { - await prisma.cssCategory.deleteMany() - for (const category of classes) { - await prisma.cssCategory.create({ - data: { - name: category.name, - classes: JSON.stringify(category.classes), - }, - }) - } - } - - static async addCssCategory(category: CssCategory): Promise { - await prisma.cssCategory.create({ - data: { - name: category.name, - classes: JSON.stringify(category.classes), - }, - }) - } - - static async updateCssCategory(categoryName: string, classes: string[]): Promise { - await prisma.cssCategory.update({ - where: { name: categoryName }, - data: { classes: JSON.stringify(classes) }, - }) - } - - static async deleteCssCategory(categoryName: string): Promise { - await prisma.cssCategory.delete({ where: { name: categoryName } }) - } - - static async getDropdownConfigs(): Promise { - const configs = await prisma.dropdownConfig.findMany() - return configs.map(c => ({ - id: c.id, - name: c.name, - label: c.label, - options: JSON.parse(c.options), - })) - } - - static async setDropdownConfigs(configs: DropdownConfig[]): Promise { - await prisma.dropdownConfig.deleteMany() - for (const config of configs) { - await prisma.dropdownConfig.create({ - data: { - id: config.id, - name: config.name, - label: config.label, - options: JSON.stringify(config.options), - }, - }) - } - } - - static async addDropdownConfig(config: DropdownConfig): Promise { - await prisma.dropdownConfig.create({ - data: { - id: config.id, - name: config.name, - label: config.label, - options: JSON.stringify(config.options), - }, - }) - } - - static async updateDropdownConfig(id: string, updates: DropdownConfig): Promise { - await prisma.dropdownConfig.update({ - where: { id }, - data: { - name: updates.name, - label: updates.label, - options: JSON.stringify(updates.options), - }, - }) - } - - static async deleteDropdownConfig(id: string): Promise { - await prisma.dropdownConfig.delete({ where: { id } }) - } - - static async clearDatabase(): Promise { - await prisma.user.deleteMany() - await prisma.credential.deleteMany() - await prisma.workflow.deleteMany() - await prisma.luaScript.deleteMany() - await prisma.pageConfig.deleteMany() - await prisma.modelSchema.deleteMany() - await prisma.appConfiguration.deleteMany() - await prisma.comment.deleteMany() - await prisma.componentNode.deleteMany() - await prisma.componentConfig.deleteMany() - await prisma.systemConfig.deleteMany() - await prisma.cssCategory.deleteMany() - await prisma.dropdownConfig.deleteMany() - await prisma.installedPackage.deleteMany() - await prisma.packageData.deleteMany() - await prisma.tenant.deleteMany() - await prisma.powerTransferRequest.deleteMany() - await prisma.sMTPConfig.deleteMany() - await prisma.passwordResetToken.deleteMany() - } - - static async getInstalledPackages(): Promise { - const packages = await prisma.installedPackage.findMany() - return packages.map(p => ({ - packageId: p.packageId, - installedAt: Number(p.installedAt), - version: p.version, - enabled: p.enabled, - })) - } - - static async setInstalledPackages(packages: InstalledPackage[]): Promise { - await prisma.installedPackage.deleteMany() - for (const pkg of packages) { - await prisma.installedPackage.create({ - data: { - packageId: pkg.packageId, - installedAt: BigInt(pkg.installedAt), - version: pkg.version, - enabled: pkg.enabled, - }, - }) - } - } - - static async installPackage(packageData: InstalledPackage): Promise { - const exists = await prisma.installedPackage.findUnique({ where: { packageId: packageData.packageId } }) - if (!exists) { - await prisma.installedPackage.create({ - data: { - packageId: packageData.packageId, - installedAt: BigInt(packageData.installedAt), - version: packageData.version, - enabled: packageData.enabled, - }, - }) - } - } - - static async uninstallPackage(packageId: string): Promise { - await prisma.installedPackage.delete({ where: { packageId } }) - } - - static async togglePackageEnabled(packageId: string, enabled: boolean): Promise { - await prisma.installedPackage.update({ - where: { packageId }, - data: { enabled }, - }) - } - - static async getPackageData(packageId: string): Promise> { - const pkg = await prisma.packageData.findUnique({ where: { packageId } }) - return pkg ? JSON.parse(pkg.data) : {} - } - - static async setPackageData(packageId: string, data: Record): Promise { - await prisma.packageData.upsert({ - where: { packageId }, - update: { data: JSON.stringify(data) }, - create: { packageId, data: JSON.stringify(data) }, - }) - } - - static async deletePackageData(packageId: string): Promise { - await prisma.packageData.delete({ where: { packageId } }) - } - - static async getTenants(): Promise { - const tenants = await prisma.tenant.findMany() - return tenants.map(t => ({ - id: t.id, - name: t.name, - ownerId: t.ownerId, - createdAt: Number(t.createdAt), - homepageConfig: t.homepageConfig ? JSON.parse(t.homepageConfig) : undefined, - })) - } - - static async setTenants(tenants: Tenant[]): Promise { - await prisma.tenant.deleteMany() - for (const tenant of tenants) { - await prisma.tenant.create({ - data: { - id: tenant.id, - name: tenant.name, - ownerId: tenant.ownerId, - createdAt: BigInt(tenant.createdAt), - homepageConfig: tenant.homepageConfig ? JSON.stringify(tenant.homepageConfig) : null, - }, - }) - } - } - - static async addTenant(tenant: Tenant): Promise { - await prisma.tenant.create({ - data: { - id: tenant.id, - name: tenant.name, - ownerId: tenant.ownerId, - createdAt: BigInt(tenant.createdAt), - homepageConfig: tenant.homepageConfig ? JSON.stringify(tenant.homepageConfig) : null, - }, - }) - } - - static async updateTenant(tenantId: string, updates: Partial): Promise { - const data: any = {} - if (updates.name !== undefined) data.name = updates.name - if (updates.ownerId !== undefined) data.ownerId = updates.ownerId - if (updates.homepageConfig !== undefined) data.homepageConfig = JSON.stringify(updates.homepageConfig) - - await prisma.tenant.update({ - where: { id: tenantId }, - data, - }) - } - - static async deleteTenant(tenantId: string): Promise { - await prisma.tenant.delete({ where: { id: tenantId } }) - } - - static async getPowerTransferRequests(): Promise { - const requests = await prisma.powerTransferRequest.findMany() - return requests.map(r => ({ - id: r.id, - fromUserId: r.fromUserId, - toUserId: r.toUserId, - status: r.status as any, - createdAt: Number(r.createdAt), - expiresAt: Number(r.expiresAt), - })) - } - - static async setPowerTransferRequests(requests: PowerTransferRequest[]): Promise { - await prisma.powerTransferRequest.deleteMany() - for (const request of requests) { - await prisma.powerTransferRequest.create({ - data: { - id: request.id, - fromUserId: request.fromUserId, - toUserId: request.toUserId, - status: request.status, - createdAt: BigInt(request.createdAt), - expiresAt: BigInt(request.expiresAt), - }, - }) - } - } - - static async addPowerTransferRequest(request: PowerTransferRequest): Promise { - await prisma.powerTransferRequest.create({ - data: { - id: request.id, - fromUserId: request.fromUserId, - toUserId: request.toUserId, - status: request.status, - createdAt: BigInt(request.createdAt), - expiresAt: BigInt(request.expiresAt), - }, - }) - } - - static async updatePowerTransferRequest(requestId: string, updates: Partial): Promise { - const data: any = {} - if (updates.status !== undefined) data.status = updates.status - - await prisma.powerTransferRequest.update({ - where: { id: requestId }, - data, - }) - } - - static async deletePowerTransferRequest(requestId: string): Promise { - await prisma.powerTransferRequest.delete({ where: { id: requestId } }) - } - - static async getSuperGod(): Promise { - const user = await prisma.user.findFirst({ - where: { role: 'supergod' }, - }) - if (!user) return null - - return { - id: user.id, - username: user.username, - email: user.email, - role: user.role as any, - profilePicture: user.profilePicture || undefined, - bio: user.bio || undefined, - createdAt: Number(user.createdAt), - tenantId: user.tenantId || undefined, - isInstanceOwner: user.isInstanceOwner, - } - } - - static async transferSuperGodPower(fromUserId: string, toUserId: string): Promise { - await prisma.user.update({ - where: { id: fromUserId }, - data: { role: 'god', isInstanceOwner: false }, - }) - - await prisma.user.update({ - where: { id: toUserId }, - data: { role: 'supergod', isInstanceOwner: true }, - }) - } - - static async getSMTPConfig(): Promise { - const config = await prisma.sMTPConfig.findFirst() - if (!config) return null - - return { - host: config.host, - port: config.port, - secure: config.secure, - username: config.username, - password: config.password, - fromEmail: config.fromEmail, - fromName: config.fromName, - } - } - - static async setSMTPConfig(config: SMTPConfig): Promise { - await prisma.sMTPConfig.deleteMany() - await prisma.sMTPConfig.create({ - data: { - host: config.host, - port: config.port, - secure: config.secure, - username: config.username, - password: config.password, - fromEmail: config.fromEmail, - fromName: config.fromName, - }, - }) - } - - static async getPasswordResetTokens(): Promise> { - const tokens = await prisma.passwordResetToken.findMany() - const result: Record = {} - for (const token of tokens) { - result[token.username] = token.token - } - return result - } - - static async setPasswordResetToken(username: string, token: string): Promise { - await prisma.passwordResetToken.upsert({ - where: { username }, - update: { token }, - create: { username, token }, - }) - } - - static async deletePasswordResetToken(username: string): Promise { - await prisma.passwordResetToken.delete({ where: { username } }) - } -} +// Re-export everything from the new db module +export * from './db' +export { Database } from './db' diff --git a/frontends/nextjs/src/lib/db/Database.ts b/frontends/nextjs/src/lib/db/Database.ts deleted file mode 100644 index 0d58478e1..000000000 --- a/frontends/nextjs/src/lib/db/Database.ts +++ /dev/null @@ -1,934 +0,0 @@ -import { prisma } from '../prisma' -import type { - User, - Workflow, - LuaScript, - PageConfig, - AppConfiguration, - Comment, - Tenant, - PowerTransferRequest, -} from '../../types/level-types' -import type { ModelSchema } from '../types/schema-types' -import type { InstalledPackage } from '../package-types' -import type { SMTPConfig } from '../password-utils' - -// Import individual functions (lambdas) -import { hashPassword } from './functions/hash-password' -import { verifyPassword } from './functions/verify-password' -import { initializeDatabase } from './functions/initialize-database' - -// Users -import { getUsers } from './functions/users/get-users' -import { setUsers } from './functions/users/set-users' -import { addUser } from './functions/users/add-user' -import { updateUser } from './functions/users/update-user' -import { deleteUser } from './functions/users/delete-user' - -// Credentials -import { getCredentials } from './functions/credentials/get-credentials' -import { setCredential } from './functions/credentials/set-credential' -import { verifyCredentials } from './functions/credentials/verify-credentials' -import { getPasswordChangeTimestamps } from './functions/credentials/get-password-change-timestamps' -import { setPasswordChangeTimestamps } from './functions/credentials/set-password-change-timestamps' - -// Workflows -import { getWorkflows } from './functions/workflows/get-workflows' -import { setWorkflows } from './functions/workflows/set-workflows' -import { addWorkflow } from './functions/workflows/add-workflow' -import { updateWorkflow } from './functions/workflows/update-workflow' -import { deleteWorkflow } from './functions/workflows/delete-workflow' - -// Lua Scripts -import { getLuaScripts } from './functions/lua-scripts/get-lua-scripts' -import { setLuaScripts } from './functions/lua-scripts/set-lua-scripts' -import { addLuaScript } from './functions/lua-scripts/add-lua-script' -import { updateLuaScript } from './functions/lua-scripts/update-lua-script' -import { deleteLuaScript } from './functions/lua-scripts/delete-lua-script' - -// Pages -import { getPages } from './functions/pages/get-pages' -import { setPages } from './functions/pages/set-pages' -import { addPage } from './functions/pages/add-page' -import { updatePage } from './functions/pages/update-page' -import { deletePage } from './functions/pages/delete-page' - -// Schemas -import { getSchemas } from './functions/schemas/get-schemas' -import { setSchemas } from './functions/schemas/set-schemas' -import { addSchema } from './functions/schemas/add-schema' -import { updateSchema } from './functions/schemas/update-schema' -import { deleteSchema } from './functions/schemas/delete-schema' - -// Tenants -import { getTenants } from './functions/tenants/get-tenants' -import { setTenants } from './functions/tenants/set-tenants' -import { addTenant } from './functions/tenants/add-tenant' -import { updateTenant } from './functions/tenants/update-tenant' -import { deleteTenant } from './functions/tenants/delete-tenant' - -// Re-export types from database.ts for compatibility -export interface CssCategory { - name: string - classes: string[] -} - -export interface DropdownConfig { - id: string - name: string - label: string - options: Array<{ value: string; label: string }> -} - -export interface ComponentNode { - id: string - type: string - parentId?: string - childIds: string[] - order: number - pageId: string -} - -export interface ComponentConfig { - id: string - componentId: string - props: Record - styles: Record - events: Record - conditionalRendering?: { - condition: string - luaScriptId?: string - } -} - -export const DB_KEYS = { - USERS: 'db_users', - CREDENTIALS: 'db_credentials', - WORKFLOWS: 'db_workflows', - LUA_SCRIPTS: 'db_lua_scripts', - PAGES: 'db_pages', - SCHEMAS: 'db_schemas', - APP_CONFIG: 'db_app_config', - COMMENTS: 'db_comments', - COMPONENT_HIERARCHY: 'db_component_hierarchy', - COMPONENT_CONFIGS: 'db_component_configs', - GOD_CREDENTIALS_EXPIRY: 'db_god_credentials_expiry', - PASSWORD_CHANGE_TIMESTAMPS: 'db_password_change_timestamps', - FIRST_LOGIN_FLAGS: 'db_first_login_flags', - GOD_CREDENTIALS_EXPIRY_DURATION: 'db_god_credentials_expiry_duration', - CSS_CLASSES: 'db_css_classes', - DROPDOWN_CONFIGS: 'db_dropdown_configs', - INSTALLED_PACKAGES: 'db_installed_packages', - PACKAGE_DATA: 'db_package_data', - TENANTS: 'db_tenants', - POWER_TRANSFER_REQUESTS: 'db_power_transfer_requests', - SMTP_CONFIG: 'db_smtp_config', - PASSWORD_RESET_TOKENS: 'db_password_reset_tokens', -} as const - -/** - * Database - Class wrapper for database operations - * - * This class serves as a container for lambda functions related to database operations. - * Each method delegates to an individual function file in the functions/ directory. - * - * Pattern: "class is container for lambdas" - * - Each lambda is defined in its own file under functions/ - * - This class wraps them for convenient namespaced access - * - Can be used as Database.methodName() or import individual functions - */ -export class Database { - // Core operations - static initializeDatabase = initializeDatabase - - // User operations - static getUsers = getUsers - static setUsers = setUsers - static addUser = addUser - static updateUser = updateUser - static deleteUser = deleteUser - - // Credential operations - static getCredentials = getCredentials - static setCredential = setCredential - static verifyCredentials = verifyCredentials - static getPasswordChangeTimestamps = getPasswordChangeTimestamps - static setPasswordChangeTimestamps = setPasswordChangeTimestamps - - // Workflow operations - static getWorkflows = getWorkflows - static setWorkflows = setWorkflows - static addWorkflow = addWorkflow - static updateWorkflow = updateWorkflow - static deleteWorkflow = deleteWorkflow - - // Lua Script operations - static getLuaScripts = getLuaScripts - static setLuaScripts = setLuaScripts - static addLuaScript = addLuaScript - static updateLuaScript = updateLuaScript - static deleteLuaScript = deleteLuaScript - - // Page operations - static getPages = getPages - static setPages = setPages - static addPage = addPage - static updatePage = updatePage - static deletePage = deletePage - - // Schema operations - static getSchemas = getSchemas - static setSchemas = setSchemas - static addSchema = addSchema - static updateSchema = updateSchema - static deleteSchema = deleteSchema - - // Tenant operations - static getTenants = getTenants - static setTenants = setTenants - static addTenant = addTenant - static updateTenant = updateTenant - static deleteTenant = deleteTenant - - // ============================================== - // Functions that require additional refactoring - // These are kept inline for now but can be split later - // ============================================== - - static async getAppConfig(): Promise { - const config = await prisma.appConfiguration.findFirst() - if (!config) return null - - return { - id: config.id, - name: config.name, - schemas: JSON.parse(config.schemas), - workflows: JSON.parse(config.workflows), - luaScripts: JSON.parse(config.luaScripts), - pages: JSON.parse(config.pages), - theme: JSON.parse(config.theme), - } - } - - static async setAppConfig(config: AppConfiguration): Promise { - await prisma.appConfiguration.deleteMany() - await prisma.appConfiguration.create({ - data: { - id: config.id, - name: config.name, - schemas: JSON.stringify(config.schemas), - workflows: JSON.stringify(config.workflows), - luaScripts: JSON.stringify(config.luaScripts), - pages: JSON.stringify(config.pages), - theme: JSON.stringify(config.theme), - }, - }) - } - - static async getComments(): Promise { - const comments = await prisma.comment.findMany() - return comments.map(c => ({ - id: c.id, - userId: c.userId, - content: c.content, - createdAt: Number(c.createdAt), - updatedAt: c.updatedAt ? Number(c.updatedAt) : undefined, - parentId: c.parentId || undefined, - })) - } - - static async setComments(comments: Comment[]): Promise { - await prisma.comment.deleteMany() - for (const comment of comments) { - await prisma.comment.create({ - data: { - id: comment.id, - userId: comment.userId, - content: comment.content, - createdAt: BigInt(comment.createdAt), - updatedAt: comment.updatedAt ? BigInt(comment.updatedAt) : null, - parentId: comment.parentId, - }, - }) - } - } - - static async addComment(comment: Comment): Promise { - await prisma.comment.create({ - data: { - id: comment.id, - userId: comment.userId, - content: comment.content, - createdAt: BigInt(comment.createdAt), - updatedAt: comment.updatedAt ? BigInt(comment.updatedAt) : null, - parentId: comment.parentId, - }, - }) - } - - static async updateComment(commentId: string, updates: Partial): Promise { - const data: any = {} - if (updates.content !== undefined) data.content = updates.content - if (updates.updatedAt !== undefined) data.updatedAt = BigInt(updates.updatedAt) - - await prisma.comment.update({ - where: { id: commentId }, - data, - }) - } - - static async deleteComment(commentId: string): Promise { - await prisma.comment.delete({ where: { id: commentId } }) - } - - static async getComponentHierarchy(): Promise> { - const nodes = await prisma.componentNode.findMany() - const result: Record = {} - for (const node of nodes) { - result[node.id] = { - id: node.id, - type: node.type, - parentId: node.parentId || undefined, - childIds: JSON.parse(node.childIds), - order: node.order, - pageId: node.pageId, - } - } - return result - } - - static async setComponentHierarchy(hierarchy: Record): Promise { - await prisma.componentNode.deleteMany() - for (const node of Object.values(hierarchy)) { - await prisma.componentNode.create({ - data: { - id: node.id, - type: node.type, - parentId: node.parentId, - childIds: JSON.stringify(node.childIds), - order: node.order, - pageId: node.pageId, - }, - }) - } - } - - static async addComponentNode(node: ComponentNode): Promise { - await prisma.componentNode.create({ - data: { - id: node.id, - type: node.type, - parentId: node.parentId, - childIds: JSON.stringify(node.childIds), - order: node.order, - pageId: node.pageId, - }, - }) - } - - static async updateComponentNode(nodeId: string, updates: Partial): Promise { - const data: any = {} - if (updates.type !== undefined) data.type = updates.type - if (updates.parentId !== undefined) data.parentId = updates.parentId - if (updates.childIds !== undefined) data.childIds = JSON.stringify(updates.childIds) - if (updates.order !== undefined) data.order = updates.order - if (updates.pageId !== undefined) data.pageId = updates.pageId - - await prisma.componentNode.update({ - where: { id: nodeId }, - data, - }) - } - - static async deleteComponentNode(nodeId: string): Promise { - await prisma.componentNode.delete({ where: { id: nodeId } }) - } - - static async getComponentConfigs(): Promise> { - const configs = await prisma.componentConfig.findMany() - const result: Record = {} - for (const config of configs) { - result[config.id] = { - id: config.id, - componentId: config.componentId, - props: JSON.parse(config.props), - styles: JSON.parse(config.styles), - events: JSON.parse(config.events), - conditionalRendering: config.conditionalRendering ? JSON.parse(config.conditionalRendering) : undefined, - } - } - return result - } - - static async setComponentConfigs(configs: Record): Promise { - await prisma.componentConfig.deleteMany() - for (const config of Object.values(configs)) { - await prisma.componentConfig.create({ - data: { - id: config.id, - componentId: config.componentId, - props: JSON.stringify(config.props), - styles: JSON.stringify(config.styles), - events: JSON.stringify(config.events), - conditionalRendering: config.conditionalRendering ? JSON.stringify(config.conditionalRendering) : null, - }, - }) - } - } - - static async addComponentConfig(config: ComponentConfig): Promise { - await prisma.componentConfig.create({ - data: { - id: config.id, - componentId: config.componentId, - props: JSON.stringify(config.props), - styles: JSON.stringify(config.styles), - events: JSON.stringify(config.events), - conditionalRendering: config.conditionalRendering ? JSON.stringify(config.conditionalRendering) : null, - }, - }) - } - - static async updateComponentConfig(configId: string, updates: Partial): Promise { - const data: any = {} - if (updates.componentId !== undefined) data.componentId = updates.componentId - if (updates.props !== undefined) data.props = JSON.stringify(updates.props) - if (updates.styles !== undefined) data.styles = JSON.stringify(updates.styles) - if (updates.events !== undefined) data.events = JSON.stringify(updates.events) - if (updates.conditionalRendering !== undefined) data.conditionalRendering = JSON.stringify(updates.conditionalRendering) - - await prisma.componentConfig.update({ - where: { id: configId }, - data, - }) - } - - static async deleteComponentConfig(configId: string): Promise { - await prisma.componentConfig.delete({ where: { id: configId } }) - } - - static async getGodCredentialsExpiry(): Promise { - const config = await prisma.systemConfig.findUnique({ where: { key: 'god_credentials_expiry' } }) - return config ? Number(config.value) : 0 - } - - static async setGodCredentialsExpiry(timestamp: number): Promise { - await prisma.systemConfig.upsert({ - where: { key: 'god_credentials_expiry' }, - update: { value: timestamp.toString() }, - create: { key: 'god_credentials_expiry', value: timestamp.toString() }, - }) - } - - static async getFirstLoginFlags(): Promise> { - const users = await prisma.user.findMany({ - select: { username: true, firstLogin: true }, - }) - const result: Record = {} - for (const user of users) { - result[user.username] = user.firstLogin - } - return result - } - - static async setFirstLoginFlag(username: string, isFirstLogin: boolean): Promise { - await prisma.user.update({ - where: { username }, - data: { firstLogin: isFirstLogin }, - }) - } - - static async shouldShowGodCredentials(): Promise { - const expiry = await this.getGodCredentialsExpiry() - const user = await prisma.user.findUnique({ - where: { username: 'god' }, - select: { passwordChangeTimestamp: true }, - }) - const godPasswordChangeTime = user?.passwordChangeTimestamp ? Number(user.passwordChangeTimestamp) : 0 - - if (expiry === 0) { - const duration = await this.getGodCredentialsExpiryDuration() - const expiryTime = Date.now() + duration - await this.setGodCredentialsExpiry(expiryTime) - return true - } - - if (godPasswordChangeTime > expiry) { - return false - } - - return Date.now() < expiry - } - - static async getGodCredentialsExpiryDuration(): Promise { - const config = await prisma.systemConfig.findUnique({ where: { key: 'god_credentials_expiry_duration' } }) - return config ? Number(config.value) : (60 * 60 * 1000) - } - - static async setGodCredentialsExpiryDuration(durationMs: number): Promise { - await prisma.systemConfig.upsert({ - where: { key: 'god_credentials_expiry_duration' }, - update: { value: durationMs.toString() }, - create: { key: 'god_credentials_expiry_duration', value: durationMs.toString() }, - }) - } - - static async resetGodCredentialsExpiry(): Promise { - const duration = await this.getGodCredentialsExpiryDuration() - const expiryTime = Date.now() + duration - await this.setGodCredentialsExpiry(expiryTime) - } - - static async getCssClasses(): Promise { - const categories = await prisma.cssCategory.findMany() - return categories.map(c => ({ - name: c.name, - classes: JSON.parse(c.classes), - })) - } - - static async setCssClasses(classes: CssCategory[]): Promise { - await prisma.cssCategory.deleteMany() - for (const category of classes) { - await prisma.cssCategory.create({ - data: { - name: category.name, - classes: JSON.stringify(category.classes), - }, - }) - } - } - - static async addCssCategory(category: CssCategory): Promise { - await prisma.cssCategory.create({ - data: { - name: category.name, - classes: JSON.stringify(category.classes), - }, - }) - } - - static async updateCssCategory(categoryName: string, classes: string[]): Promise { - await prisma.cssCategory.update({ - where: { name: categoryName }, - data: { classes: JSON.stringify(classes) }, - }) - } - - static async deleteCssCategory(categoryName: string): Promise { - await prisma.cssCategory.delete({ where: { name: categoryName } }) - } - - static async getDropdownConfigs(): Promise { - const configs = await prisma.dropdownConfig.findMany() - return configs.map(c => ({ - id: c.id, - name: c.name, - label: c.label, - options: JSON.parse(c.options), - })) - } - - static async setDropdownConfigs(configs: DropdownConfig[]): Promise { - await prisma.dropdownConfig.deleteMany() - for (const config of configs) { - await prisma.dropdownConfig.create({ - data: { - id: config.id, - name: config.name, - label: config.label, - options: JSON.stringify(config.options), - }, - }) - } - } - - static async addDropdownConfig(config: DropdownConfig): Promise { - await prisma.dropdownConfig.create({ - data: { - id: config.id, - name: config.name, - label: config.label, - options: JSON.stringify(config.options), - }, - }) - } - - static async updateDropdownConfig(id: string, updates: DropdownConfig): Promise { - await prisma.dropdownConfig.update({ - where: { id }, - data: { - name: updates.name, - label: updates.label, - options: JSON.stringify(updates.options), - }, - }) - } - - static async deleteDropdownConfig(id: string): Promise { - await prisma.dropdownConfig.delete({ where: { id } }) - } - - static async getInstalledPackages(): Promise { - const packages = await prisma.installedPackage.findMany() - return packages.map(p => ({ - packageId: p.packageId, - installedAt: Number(p.installedAt), - version: p.version, - enabled: p.enabled, - })) - } - - static async setInstalledPackages(packages: InstalledPackage[]): Promise { - await prisma.installedPackage.deleteMany() - for (const pkg of packages) { - await prisma.installedPackage.create({ - data: { - packageId: pkg.packageId, - installedAt: BigInt(pkg.installedAt), - version: pkg.version, - enabled: pkg.enabled, - }, - }) - } - } - - static async installPackage(packageData: InstalledPackage): Promise { - const exists = await prisma.installedPackage.findUnique({ where: { packageId: packageData.packageId } }) - if (!exists) { - await prisma.installedPackage.create({ - data: { - packageId: packageData.packageId, - installedAt: BigInt(packageData.installedAt), - version: packageData.version, - enabled: packageData.enabled, - }, - }) - } - } - - static async uninstallPackage(packageId: string): Promise { - await prisma.installedPackage.delete({ where: { packageId } }) - } - - static async togglePackageEnabled(packageId: string, enabled: boolean): Promise { - await prisma.installedPackage.update({ - where: { packageId }, - data: { enabled }, - }) - } - - static async getPackageData(packageId: string): Promise> { - const pkg = await prisma.packageData.findUnique({ where: { packageId } }) - return pkg ? JSON.parse(pkg.data) : {} - } - - static async setPackageData(packageId: string, data: Record): Promise { - await prisma.packageData.upsert({ - where: { packageId }, - update: { data: JSON.stringify(data) }, - create: { packageId, data: JSON.stringify(data) }, - }) - } - - static async deletePackageData(packageId: string): Promise { - await prisma.packageData.delete({ where: { packageId } }) - } - - static async getPowerTransferRequests(): Promise { - const requests = await prisma.powerTransferRequest.findMany() - return requests.map(r => ({ - id: r.id, - fromUserId: r.fromUserId, - toUserId: r.toUserId, - status: r.status as any, - createdAt: Number(r.createdAt), - expiresAt: Number(r.expiresAt), - })) - } - - static async setPowerTransferRequests(requests: PowerTransferRequest[]): Promise { - await prisma.powerTransferRequest.deleteMany() - for (const request of requests) { - await prisma.powerTransferRequest.create({ - data: { - id: request.id, - fromUserId: request.fromUserId, - toUserId: request.toUserId, - status: request.status, - createdAt: BigInt(request.createdAt), - expiresAt: BigInt(request.expiresAt), - }, - }) - } - } - - static async addPowerTransferRequest(request: PowerTransferRequest): Promise { - await prisma.powerTransferRequest.create({ - data: { - id: request.id, - fromUserId: request.fromUserId, - toUserId: request.toUserId, - status: request.status, - createdAt: BigInt(request.createdAt), - expiresAt: BigInt(request.expiresAt), - }, - }) - } - - static async updatePowerTransferRequest(requestId: string, updates: Partial): Promise { - const data: any = {} - if (updates.status !== undefined) data.status = updates.status - - await prisma.powerTransferRequest.update({ - where: { id: requestId }, - data, - }) - } - - static async deletePowerTransferRequest(requestId: string): Promise { - await prisma.powerTransferRequest.delete({ where: { id: requestId } }) - } - - static async getSuperGod(): Promise { - const user = await prisma.user.findFirst({ - where: { role: 'supergod' }, - }) - if (!user) return null - - return { - id: user.id, - username: user.username, - email: user.email, - role: user.role as any, - profilePicture: user.profilePicture || undefined, - bio: user.bio || undefined, - createdAt: Number(user.createdAt), - tenantId: user.tenantId || undefined, - isInstanceOwner: user.isInstanceOwner, - } - } - - static async transferSuperGodPower(fromUserId: string, toUserId: string): Promise { - await prisma.user.update({ - where: { id: fromUserId }, - data: { role: 'god', isInstanceOwner: false }, - }) - - await prisma.user.update({ - where: { id: toUserId }, - data: { role: 'supergod', isInstanceOwner: true }, - }) - } - - static async getSMTPConfig(): Promise { - const config = await prisma.sMTPConfig.findFirst() - if (!config) return null - - return { - host: config.host, - port: config.port, - secure: config.secure, - username: config.username, - password: config.password, - fromEmail: config.fromEmail, - fromName: config.fromName, - } - } - - static async setSMTPConfig(config: SMTPConfig): Promise { - await prisma.sMTPConfig.deleteMany() - await prisma.sMTPConfig.create({ - data: { - host: config.host, - port: config.port, - secure: config.secure, - username: config.username, - password: config.password, - fromEmail: config.fromEmail, - fromName: config.fromName, - }, - }) - } - - static async getPasswordResetTokens(): Promise> { - const tokens = await prisma.passwordResetToken.findMany() - const result: Record = {} - for (const token of tokens) { - result[token.username] = token.token - } - return result - } - - static async setPasswordResetToken(username: string, token: string): Promise { - await prisma.passwordResetToken.upsert({ - where: { username }, - update: { token }, - create: { username, token }, - }) - } - - static async deletePasswordResetToken(username: string): Promise { - await prisma.passwordResetToken.delete({ where: { username } }) - } - - static async clearDatabase(): Promise { - await prisma.user.deleteMany() - await prisma.credential.deleteMany() - await prisma.workflow.deleteMany() - await prisma.luaScript.deleteMany() - await prisma.pageConfig.deleteMany() - await prisma.modelSchema.deleteMany() - await prisma.appConfiguration.deleteMany() - await prisma.comment.deleteMany() - await prisma.componentNode.deleteMany() - await prisma.componentConfig.deleteMany() - await prisma.systemConfig.deleteMany() - await prisma.cssCategory.deleteMany() - await prisma.dropdownConfig.deleteMany() - await prisma.installedPackage.deleteMany() - await prisma.packageData.deleteMany() - await prisma.tenant.deleteMany() - await prisma.powerTransferRequest.deleteMany() - await prisma.sMTPConfig.deleteMany() - await prisma.passwordResetToken.deleteMany() - } - - static async seedDefaultData(): Promise { - const users = await this.getUsers() - const credentials = await this.getCredentials() - - if (users.length === 0) { - const defaultUsers: User[] = [ - { - id: 'user_supergod', - username: 'supergod', - email: 'supergod@builder.com', - role: 'supergod', - bio: 'Supreme administrator with multi-tenant control', - createdAt: Date.now(), - isInstanceOwner: true, - }, - { - id: 'user_god', - username: 'god', - email: 'god@builder.com', - role: 'god', - bio: 'System architect with full access to all levels', - createdAt: Date.now(), - }, - { - id: 'user_admin', - username: 'admin', - email: 'admin@builder.com', - role: 'admin', - bio: 'Administrator with data management access', - createdAt: Date.now(), - }, - { - id: 'user_demo', - username: 'demo', - email: 'demo@builder.com', - role: 'user', - bio: 'Demo user account', - createdAt: Date.now(), - }, - ] - - await this.setUsers(defaultUsers) - } - - if (Object.keys(credentials).length === 0) { - const { getScrambledPassword } = await import('../auth') - await this.setCredential('supergod', await hashPassword(getScrambledPassword('supergod'))) - await this.setCredential('god', await hashPassword(getScrambledPassword('god'))) - await this.setCredential('admin', await hashPassword(getScrambledPassword('admin'))) - await this.setCredential('demo', await hashPassword(getScrambledPassword('demo'))) - - await this.setFirstLoginFlag('supergod', true) - await this.setFirstLoginFlag('god', true) - await this.setFirstLoginFlag('admin', false) - await this.setFirstLoginFlag('demo', false) - } - - const appConfig = await this.getAppConfig() - if (!appConfig) { - const defaultConfig: AppConfiguration = { - id: 'app_001', - name: 'MetaBuilder App', - schemas: [], - workflows: [], - luaScripts: [], - pages: [], - theme: { - colors: {}, - fonts: {}, - }, - } - await this.setAppConfig(defaultConfig) - } - - const cssClasses = await this.getCssClasses() - if (cssClasses.length === 0) { - const defaultCssClasses: CssCategory[] = [ - { name: 'Layout', classes: ['flex', 'flex-col', 'flex-row', 'grid', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4', 'block', 'inline-block', 'inline', 'hidden'] }, - { name: 'Spacing', classes: ['p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-6', 'p-8', 'm-0', 'm-1', 'm-2', 'm-3', 'm-4', 'm-6', 'm-8', 'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-6', 'gap-8'] }, - { name: 'Sizing', classes: ['w-full', 'w-1/2', 'w-1/3', 'w-1/4', 'w-auto', 'h-full', 'h-screen', 'h-auto', 'min-h-screen', 'max-w-xs', 'max-w-sm', 'max-w-md', 'max-w-lg', 'max-w-xl', 'max-w-2xl', 'max-w-4xl', 'max-w-6xl', 'max-w-7xl'] }, - { name: 'Typography', classes: ['text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'text-3xl', 'text-4xl', 'font-normal', 'font-medium', 'font-semibold', 'font-bold', 'text-left', 'text-center', 'text-right', 'uppercase', 'lowercase', 'capitalize'] }, - { name: 'Colors', classes: ['text-primary', 'text-secondary', 'text-accent', 'text-muted-foreground', 'bg-primary', 'bg-secondary', 'bg-accent', 'bg-background', 'bg-card', 'bg-muted', 'border-primary', 'border-secondary', 'border-accent', 'border-border'] }, - { name: 'Borders', classes: ['border', 'border-2', 'border-4', 'border-t', 'border-b', 'border-l', 'border-r', 'rounded', 'rounded-sm', 'rounded-md', 'rounded-lg', 'rounded-xl', 'rounded-2xl', 'rounded-full'] }, - { name: 'Effects', classes: ['shadow', 'shadow-sm', 'shadow-md', 'shadow-lg', 'shadow-xl', 'hover:shadow-lg', 'opacity-0', 'opacity-50', 'opacity-75', 'opacity-100', 'transition', 'transition-all', 'duration-200', 'duration-300', 'duration-500'] }, - { name: 'Positioning', classes: ['relative', 'absolute', 'fixed', 'sticky', 'top-0', 'bottom-0', 'left-0', 'right-0', 'z-10', 'z-20', 'z-30', 'z-40', 'z-50'] }, - { name: 'Alignment', classes: ['items-start', 'items-center', 'items-end', 'justify-start', 'justify-center', 'justify-end', 'justify-between', 'justify-around', 'self-start', 'self-center', 'self-end'] }, - { name: 'Interactivity', classes: ['cursor-pointer', 'cursor-default', 'pointer-events-none', 'select-none', 'hover:bg-accent', 'hover:text-accent-foreground', 'active:scale-95', 'disabled:opacity-50'] }, - ] - await this.setCssClasses(defaultCssClasses) - } - - const dropdowns = await this.getDropdownConfigs() - if (dropdowns.length === 0) { - const defaultDropdowns: DropdownConfig[] = [ - { id: 'dropdown_status', name: 'status_options', label: 'Status', options: [{ value: 'draft', label: 'Draft' }, { value: 'published', label: 'Published' }, { value: 'archived', label: 'Archived' }] }, - { id: 'dropdown_priority', name: 'priority_options', label: 'Priority', options: [{ value: 'low', label: 'Low' }, { value: 'medium', label: 'Medium' }, { value: 'high', label: 'High' }, { value: 'urgent', label: 'Urgent' }] }, - { id: 'dropdown_category', name: 'category_options', label: 'Category', options: [{ value: 'general', label: 'General' }, { value: 'technical', label: 'Technical' }, { value: 'business', label: 'Business' }, { value: 'personal', label: 'Personal' }] }, - ] - await this.setDropdownConfigs(defaultDropdowns) - } - } - - static async exportDatabase(): Promise { - const data = { - users: await this.getUsers(), - workflows: await this.getWorkflows(), - luaScripts: await this.getLuaScripts(), - pages: await this.getPages(), - schemas: await this.getSchemas(), - appConfig: (await this.getAppConfig()) || undefined, - comments: await this.getComments(), - componentHierarchy: await this.getComponentHierarchy(), - componentConfigs: await this.getComponentConfigs(), - } - return JSON.stringify(data, null, 2) - } - - static async importDatabase(jsonData: string): Promise { - try { - const data = JSON.parse(jsonData) - - if (data.users) await this.setUsers(data.users) - if (data.workflows) await this.setWorkflows(data.workflows) - if (data.luaScripts) await this.setLuaScripts(data.luaScripts) - if (data.pages) await this.setPages(data.pages) - if (data.schemas) await this.setSchemas(data.schemas) - if (data.appConfig) await this.setAppConfig(data.appConfig) - if (data.comments) await this.setComments(data.comments) - if (data.componentHierarchy) await this.setComponentHierarchy(data.componentHierarchy) - if (data.componentConfigs) await this.setComponentConfigs(data.componentConfigs) - } catch (error) { - throw new Error('Failed to import database: Invalid JSON') - } - } -} - -// Re-export standalone functions for direct import -export { hashPassword, verifyPassword } diff --git a/frontends/nextjs/src/lib/db/app-config/set-app-config.test.ts b/frontends/nextjs/src/lib/db/app-config/set-app-config.test.ts index 1a2fefd08..018fb8448 100644 --- a/frontends/nextjs/src/lib/db/app-config/set-app-config.test.ts +++ b/frontends/nextjs/src/lib/db/app-config/set-app-config.test.ts @@ -30,7 +30,7 @@ describe('setAppConfig', () => { workflows: [], luaScripts: [], pages: [], - theme: {}, + theme: { colors: {}, fonts: {} }, }) expect(mockDelete).toHaveBeenCalled() diff --git a/frontends/nextjs/src/lib/db/dbal-client.ts b/frontends/nextjs/src/lib/db/dbal-client.ts index 599c59991..93cb4844f 100644 --- a/frontends/nextjs/src/lib/db/dbal-client.ts +++ b/frontends/nextjs/src/lib/db/dbal-client.ts @@ -31,6 +31,8 @@ export interface DBALAdapter { update(entity: string, id: string, data: Record): Promise delete(entity: string, id: string): Promise list(entity: string, options?: ListOptions): Promise> + findFirst(entity: string, options?: { where?: Record }): Promise + upsert(entity: string, options: { where: Record; update: Record; create: Record }): Promise close(): Promise } @@ -122,6 +124,21 @@ const prismaAdapter: DBALAdapter = { } }, + async findFirst(entity: string, options?: { where?: Record }): Promise { + const model = getModel(entity) + const where = options?.where ? buildWhereClause(options.where) : undefined + return model.findFirst({ where }) + }, + + async upsert(entity: string, options: { where: Record; update: Record; create: Record }): Promise { + const model = getModel(entity) + return model.upsert({ + where: options.where, + update: options.update, + create: options.create, + }) + }, + async close(): Promise { await prisma.$disconnect() }, diff --git a/frontends/nextjs/src/lib/db/functions/credentials/get-credentials.ts b/frontends/nextjs/src/lib/db/functions/credentials/get-credentials.ts deleted file mode 100644 index 212f7c809..000000000 --- a/frontends/nextjs/src/lib/db/functions/credentials/get-credentials.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { prisma } from '../../prisma' - -/** - * Get all credentials as username->hash map - * @returns Record of username to password hash - */ -export const getCredentials = async (): Promise> => { - const credentials = await prisma.credential.findMany() - const result: Record = {} - for (const cred of credentials) { - result[cred.username] = cred.passwordHash - } - return result -} diff --git a/frontends/nextjs/src/lib/db/functions/credentials/get-password-change-timestamps.ts b/frontends/nextjs/src/lib/db/functions/credentials/get-password-change-timestamps.ts deleted file mode 100644 index 7b178d8af..000000000 --- a/frontends/nextjs/src/lib/db/functions/credentials/get-password-change-timestamps.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { prisma } from '../../prisma' - -/** - * Get password change timestamps for all users - * @returns Record of username to timestamp - */ -export const getPasswordChangeTimestamps = async (): Promise> => { - const users = await prisma.user.findMany({ - where: { passwordChangeTimestamp: { not: null } }, - select: { username: true, passwordChangeTimestamp: true }, - }) - const result: Record = {} - for (const user of users) { - if (user.passwordChangeTimestamp) { - result[user.username] = Number(user.passwordChangeTimestamp) - } - } - return result -} diff --git a/frontends/nextjs/src/lib/db/functions/credentials/index.ts b/frontends/nextjs/src/lib/db/functions/credentials/index.ts deleted file mode 100644 index 17c3967a1..000000000 --- a/frontends/nextjs/src/lib/db/functions/credentials/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { getCredentials } from './get-credentials' -export { setCredential } from './set-credential' -export { verifyCredentials } from './verify-credentials' -export { getPasswordChangeTimestamps } from './get-password-change-timestamps' -export { setPasswordChangeTimestamps } from './set-password-change-timestamps' diff --git a/frontends/nextjs/src/lib/db/functions/credentials/set-credential.ts b/frontends/nextjs/src/lib/db/functions/credentials/set-credential.ts deleted file mode 100644 index b3c31d089..000000000 --- a/frontends/nextjs/src/lib/db/functions/credentials/set-credential.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { prisma } from '../../prisma' - -/** - * Set or update a credential for a user - * @param username - The username - * @param passwordHash - The hashed password - */ -export const setCredential = async (username: string, passwordHash: string): Promise => { - await prisma.credential.upsert({ - where: { username }, - update: { passwordHash }, - create: { username, passwordHash }, - }) - - await prisma.user.update({ - where: { username }, - data: { passwordChangeTimestamp: BigInt(Date.now()) }, - }) -} diff --git a/frontends/nextjs/src/lib/db/functions/credentials/set-password-change-timestamps.ts b/frontends/nextjs/src/lib/db/functions/credentials/set-password-change-timestamps.ts deleted file mode 100644 index 5bcb47d41..000000000 --- a/frontends/nextjs/src/lib/db/functions/credentials/set-password-change-timestamps.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { prisma } from '../../prisma' - -/** - * Set password change timestamps - * @param timestamps - Record of username to timestamp - */ -export const setPasswordChangeTimestamps = async (timestamps: Record): Promise => { - for (const [username, timestamp] of Object.entries(timestamps)) { - await prisma.user.update({ - where: { username }, - data: { passwordChangeTimestamp: BigInt(timestamp) }, - }) - } -} diff --git a/frontends/nextjs/src/lib/db/functions/credentials/verify-credentials.ts b/frontends/nextjs/src/lib/db/functions/credentials/verify-credentials.ts deleted file mode 100644 index 97f81b91b..000000000 --- a/frontends/nextjs/src/lib/db/functions/credentials/verify-credentials.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { prisma } from '../../prisma' -import { verifyPassword } from '../verify-password' - -/** - * Verify user credentials - * @param username - The username - * @param password - The plain text password - * @returns True if credentials are valid - */ -export const verifyCredentials = async (username: string, password: string): Promise => { - const credential = await prisma.credential.findUnique({ where: { username } }) - if (!credential) return false - return await verifyPassword(password, credential.passwordHash) -} diff --git a/frontends/nextjs/src/lib/db/functions/hash-password.ts b/frontends/nextjs/src/lib/db/functions/hash-password.ts deleted file mode 100644 index 012b0cd04..000000000 --- a/frontends/nextjs/src/lib/db/functions/hash-password.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Hash a password using SHA-512 - * @param password - The plain text password - * @returns The hashed password as a hex string - */ -export const hashPassword = async (password: string): Promise => { - const encoder = new TextEncoder() - const data = encoder.encode(password) - const hashBuffer = await crypto.subtle.digest('SHA-512', data) - const hashArray = Array.from(new Uint8Array(hashBuffer)) - const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('') - return hashHex -} diff --git a/frontends/nextjs/src/lib/db/functions/index.ts b/frontends/nextjs/src/lib/db/functions/index.ts deleted file mode 100644 index fb28ee2ca..000000000 --- a/frontends/nextjs/src/lib/db/functions/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Core functions -export { hashPassword } from './hash-password' -export { verifyPassword } from './verify-password' -export { initializeDatabase } from './initialize-database' - -// Users -export * from './users' - -// Credentials -export * from './credentials' - -// Workflows -export * from './workflows' - -// Lua Scripts -export * from './lua-scripts' - -// Pages -export * from './pages' - -// Schemas -export * from './schemas' - -// Tenants -export * from './tenants' diff --git a/frontends/nextjs/src/lib/db/functions/initialize-database.ts b/frontends/nextjs/src/lib/db/functions/initialize-database.ts deleted file mode 100644 index cc8d49a60..000000000 --- a/frontends/nextjs/src/lib/db/functions/initialize-database.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { prisma } from '../../prisma' - -/** - * Initialize database connection - */ -export const initializeDatabase = async (): Promise => { - try { - await prisma.$connect() - console.log('Database initialized successfully') - } catch (error) { - console.error('Failed to initialize database:', error) - throw error - } -} diff --git a/frontends/nextjs/src/lib/db/functions/lua-scripts/add-lua-script.ts b/frontends/nextjs/src/lib/db/functions/lua-scripts/add-lua-script.ts deleted file mode 100644 index aefc7dbc5..000000000 --- a/frontends/nextjs/src/lib/db/functions/lua-scripts/add-lua-script.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { prisma } from '../../prisma' -import type { LuaScript } from '../../../types/level-types' - -/** - * Add a single Lua script - * @param script - The script to add - */ -export const addLuaScript = async (script: LuaScript): Promise => { - await prisma.luaScript.create({ - data: { - id: script.id, - name: script.name, - description: script.description, - code: script.code, - parameters: JSON.stringify(script.parameters), - returnType: script.returnType, - }, - }) -} diff --git a/frontends/nextjs/src/lib/db/functions/lua-scripts/delete-lua-script.ts b/frontends/nextjs/src/lib/db/functions/lua-scripts/delete-lua-script.ts deleted file mode 100644 index 8c7b7b5e8..000000000 --- a/frontends/nextjs/src/lib/db/functions/lua-scripts/delete-lua-script.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { prisma } from '../../prisma' - -/** - * Delete a Lua script by ID - * @param scriptId - The script ID - */ -export const deleteLuaScript = async (scriptId: string): Promise => { - await prisma.luaScript.delete({ where: { id: scriptId } }) -} diff --git a/frontends/nextjs/src/lib/db/functions/lua-scripts/get-lua-scripts.ts b/frontends/nextjs/src/lib/db/functions/lua-scripts/get-lua-scripts.ts deleted file mode 100644 index 28d0b3944..000000000 --- a/frontends/nextjs/src/lib/db/functions/lua-scripts/get-lua-scripts.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { prisma } from '../../prisma' -import type { LuaScript } from '../../../types/level-types' - -/** - * Get all Lua scripts - * @returns Array of Lua scripts - */ -export const getLuaScripts = async (): Promise => { - const scripts = await prisma.luaScript.findMany() - return scripts.map(s => ({ - id: s.id, - name: s.name, - description: s.description || undefined, - code: s.code, - parameters: JSON.parse(s.parameters), - returnType: s.returnType || undefined, - })) -} diff --git a/frontends/nextjs/src/lib/db/functions/lua-scripts/index.ts b/frontends/nextjs/src/lib/db/functions/lua-scripts/index.ts deleted file mode 100644 index 89514e013..000000000 --- a/frontends/nextjs/src/lib/db/functions/lua-scripts/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { getLuaScripts } from './get-lua-scripts' -export { setLuaScripts } from './set-lua-scripts' -export { addLuaScript } from './add-lua-script' -export { updateLuaScript } from './update-lua-script' -export { deleteLuaScript } from './delete-lua-script' diff --git a/frontends/nextjs/src/lib/db/functions/lua-scripts/set-lua-scripts.ts b/frontends/nextjs/src/lib/db/functions/lua-scripts/set-lua-scripts.ts deleted file mode 100644 index 762a00320..000000000 --- a/frontends/nextjs/src/lib/db/functions/lua-scripts/set-lua-scripts.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { prisma } from '../../prisma' -import type { LuaScript } from '../../../types/level-types' - -/** - * Set all Lua scripts (replaces existing) - * @param scripts - Array of Lua scripts - */ -export const setLuaScripts = async (scripts: LuaScript[]): Promise => { - await prisma.luaScript.deleteMany() - for (const script of scripts) { - await prisma.luaScript.create({ - data: { - id: script.id, - name: script.name, - description: script.description, - code: script.code, - parameters: JSON.stringify(script.parameters), - returnType: script.returnType, - }, - }) - } -} diff --git a/frontends/nextjs/src/lib/db/functions/lua-scripts/update-lua-script.ts b/frontends/nextjs/src/lib/db/functions/lua-scripts/update-lua-script.ts deleted file mode 100644 index d4ae9d58c..000000000 --- a/frontends/nextjs/src/lib/db/functions/lua-scripts/update-lua-script.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { prisma } from '../../prisma' -import type { LuaScript } from '../../../types/level-types' - -/** - * Update a Lua script by ID - * @param scriptId - The script ID - * @param updates - Partial script data - */ -export const updateLuaScript = async (scriptId: string, updates: Partial): Promise => { - const data: any = {} - if (updates.name !== undefined) data.name = updates.name - if (updates.description !== undefined) data.description = updates.description - if (updates.code !== undefined) data.code = updates.code - if (updates.parameters !== undefined) data.parameters = JSON.stringify(updates.parameters) - if (updates.returnType !== undefined) data.returnType = updates.returnType - - await prisma.luaScript.update({ - where: { id: scriptId }, - data, - }) -} diff --git a/frontends/nextjs/src/lib/db/functions/pages/add-page.ts b/frontends/nextjs/src/lib/db/functions/pages/add-page.ts deleted file mode 100644 index 67f8b31cd..000000000 --- a/frontends/nextjs/src/lib/db/functions/pages/add-page.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { prisma } from '../../prisma' -import type { PageConfig } from '../../../types/level-types' - -/** - * Add a single page config - * @param page - The page to add - */ -export const addPage = async (page: PageConfig): Promise => { - await prisma.pageConfig.create({ - data: { - id: page.id, - path: page.path, - title: page.title, - level: page.level, - componentTree: JSON.stringify(page.componentTree), - requiresAuth: page.requiresAuth, - requiredRole: page.requiredRole, - }, - }) -} diff --git a/frontends/nextjs/src/lib/db/functions/pages/delete-page.ts b/frontends/nextjs/src/lib/db/functions/pages/delete-page.ts deleted file mode 100644 index 7b4e67825..000000000 --- a/frontends/nextjs/src/lib/db/functions/pages/delete-page.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { prisma } from '../../prisma' - -/** - * Delete a page config by ID - * @param pageId - The page ID - */ -export const deletePage = async (pageId: string): Promise => { - await prisma.pageConfig.delete({ where: { id: pageId } }) -} diff --git a/frontends/nextjs/src/lib/db/functions/pages/get-pages.ts b/frontends/nextjs/src/lib/db/functions/pages/get-pages.ts deleted file mode 100644 index 17a940fe7..000000000 --- a/frontends/nextjs/src/lib/db/functions/pages/get-pages.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { prisma } from '../../prisma' -import type { PageConfig } from '../../../types/level-types' - -/** - * Get all page configs - * @returns Array of page configs - */ -export const getPages = async (): Promise => { - const pages = await prisma.pageConfig.findMany() - return pages.map(p => ({ - id: p.id, - path: p.path, - title: p.title, - level: p.level as any, - componentTree: JSON.parse(p.componentTree), - requiresAuth: p.requiresAuth, - requiredRole: (p.requiredRole as any) || undefined, - })) -} diff --git a/frontends/nextjs/src/lib/db/functions/pages/index.ts b/frontends/nextjs/src/lib/db/functions/pages/index.ts deleted file mode 100644 index 87ebb83eb..000000000 --- a/frontends/nextjs/src/lib/db/functions/pages/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { getPages } from './get-pages' -export { setPages } from './set-pages' -export { addPage } from './add-page' -export { updatePage } from './update-page' -export { deletePage } from './delete-page' diff --git a/frontends/nextjs/src/lib/db/functions/pages/set-pages.ts b/frontends/nextjs/src/lib/db/functions/pages/set-pages.ts deleted file mode 100644 index 1ab2f0cb5..000000000 --- a/frontends/nextjs/src/lib/db/functions/pages/set-pages.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { prisma } from '../../prisma' -import type { PageConfig } from '../../../types/level-types' - -/** - * Set all page configs (replaces existing) - * @param pages - Array of page configs - */ -export const setPages = async (pages: PageConfig[]): Promise => { - await prisma.pageConfig.deleteMany() - for (const page of pages) { - await prisma.pageConfig.create({ - data: { - id: page.id, - path: page.path, - title: page.title, - level: page.level, - componentTree: JSON.stringify(page.componentTree), - requiresAuth: page.requiresAuth, - requiredRole: page.requiredRole, - }, - }) - } -} diff --git a/frontends/nextjs/src/lib/db/functions/pages/update-page.ts b/frontends/nextjs/src/lib/db/functions/pages/update-page.ts deleted file mode 100644 index 3f9375f20..000000000 --- a/frontends/nextjs/src/lib/db/functions/pages/update-page.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { prisma } from '../../prisma' -import type { PageConfig } from '../../../types/level-types' - -/** - * Update a page config by ID - * @param pageId - The page ID - * @param updates - Partial page data - */ -export const updatePage = async (pageId: string, updates: Partial): Promise => { - const data: any = {} - if (updates.path !== undefined) data.path = updates.path - if (updates.title !== undefined) data.title = updates.title - if (updates.level !== undefined) data.level = updates.level - if (updates.componentTree !== undefined) data.componentTree = JSON.stringify(updates.componentTree) - if (updates.requiresAuth !== undefined) data.requiresAuth = updates.requiresAuth - if (updates.requiredRole !== undefined) data.requiredRole = updates.requiredRole - - await prisma.pageConfig.update({ - where: { id: pageId }, - data, - }) -} diff --git a/frontends/nextjs/src/lib/db/functions/schemas/add-schema.ts b/frontends/nextjs/src/lib/db/functions/schemas/add-schema.ts deleted file mode 100644 index 2962fde6e..000000000 --- a/frontends/nextjs/src/lib/db/functions/schemas/add-schema.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { prisma } from '../../prisma' -import type { ModelSchema } from '../../types/schema-types' - -/** - * Add a single model schema - * @param schema - The schema to add - */ -export const addSchema = async (schema: ModelSchema): Promise => { - await prisma.modelSchema.create({ - data: { - name: schema.name, - label: schema.label, - labelPlural: schema.labelPlural, - icon: schema.icon, - fields: JSON.stringify(schema.fields), - listDisplay: schema.listDisplay ? JSON.stringify(schema.listDisplay) : null, - listFilter: schema.listFilter ? JSON.stringify(schema.listFilter) : null, - searchFields: schema.searchFields ? JSON.stringify(schema.searchFields) : null, - ordering: schema.ordering ? JSON.stringify(schema.ordering) : null, - }, - }) -} diff --git a/frontends/nextjs/src/lib/db/functions/schemas/delete-schema.ts b/frontends/nextjs/src/lib/db/functions/schemas/delete-schema.ts deleted file mode 100644 index 2c7a7a250..000000000 --- a/frontends/nextjs/src/lib/db/functions/schemas/delete-schema.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { prisma } from '../../prisma' - -/** - * Delete a model schema by name - * @param schemaName - The schema name - */ -export const deleteSchema = async (schemaName: string): Promise => { - await prisma.modelSchema.delete({ where: { name: schemaName } }) -} diff --git a/frontends/nextjs/src/lib/db/functions/schemas/get-schemas.ts b/frontends/nextjs/src/lib/db/functions/schemas/get-schemas.ts deleted file mode 100644 index cc7118e20..000000000 --- a/frontends/nextjs/src/lib/db/functions/schemas/get-schemas.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { prisma } from '../../prisma' -import type { ModelSchema } from '../../types/schema-types' - -/** - * Get all model schemas - * @returns Array of model schemas - */ -export const getSchemas = async (): Promise => { - const schemas = await prisma.modelSchema.findMany() - return schemas.map(s => ({ - name: s.name, - label: s.label || undefined, - labelPlural: s.labelPlural || undefined, - icon: s.icon || undefined, - fields: JSON.parse(s.fields), - listDisplay: s.listDisplay ? JSON.parse(s.listDisplay) : undefined, - listFilter: s.listFilter ? JSON.parse(s.listFilter) : undefined, - searchFields: s.searchFields ? JSON.parse(s.searchFields) : undefined, - ordering: s.ordering ? JSON.parse(s.ordering) : undefined, - })) -} diff --git a/frontends/nextjs/src/lib/db/functions/schemas/index.ts b/frontends/nextjs/src/lib/db/functions/schemas/index.ts deleted file mode 100644 index a5c44cf69..000000000 --- a/frontends/nextjs/src/lib/db/functions/schemas/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { getSchemas } from './get-schemas' -export { setSchemas } from './set-schemas' -export { addSchema } from './add-schema' -export { updateSchema } from './update-schema' -export { deleteSchema } from './delete-schema' diff --git a/frontends/nextjs/src/lib/db/functions/schemas/set-schemas.ts b/frontends/nextjs/src/lib/db/functions/schemas/set-schemas.ts deleted file mode 100644 index df919dfdf..000000000 --- a/frontends/nextjs/src/lib/db/functions/schemas/set-schemas.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { prisma } from '../../prisma' -import type { ModelSchema } from '../../types/schema-types' - -/** - * Set all model schemas (replaces existing) - * @param schemas - Array of model schemas - */ -export const setSchemas = async (schemas: ModelSchema[]): Promise => { - await prisma.modelSchema.deleteMany() - for (const schema of schemas) { - await prisma.modelSchema.create({ - data: { - name: schema.name, - label: schema.label, - labelPlural: schema.labelPlural, - icon: schema.icon, - fields: JSON.stringify(schema.fields), - listDisplay: schema.listDisplay ? JSON.stringify(schema.listDisplay) : null, - listFilter: schema.listFilter ? JSON.stringify(schema.listFilter) : null, - searchFields: schema.searchFields ? JSON.stringify(schema.searchFields) : null, - ordering: schema.ordering ? JSON.stringify(schema.ordering) : null, - }, - }) - } -} diff --git a/frontends/nextjs/src/lib/db/functions/schemas/update-schema.ts b/frontends/nextjs/src/lib/db/functions/schemas/update-schema.ts deleted file mode 100644 index dce8e5ef4..000000000 --- a/frontends/nextjs/src/lib/db/functions/schemas/update-schema.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { prisma } from '../../prisma' -import type { ModelSchema } from '../../types/schema-types' - -/** - * Update a model schema by name - * @param schemaName - The schema name - * @param updates - Partial schema data - */ -export const updateSchema = async (schemaName: string, updates: Partial): Promise => { - const data: any = {} - if (updates.label !== undefined) data.label = updates.label - if (updates.labelPlural !== undefined) data.labelPlural = updates.labelPlural - if (updates.icon !== undefined) data.icon = updates.icon - if (updates.fields !== undefined) data.fields = JSON.stringify(updates.fields) - if (updates.listDisplay !== undefined) data.listDisplay = JSON.stringify(updates.listDisplay) - if (updates.listFilter !== undefined) data.listFilter = JSON.stringify(updates.listFilter) - if (updates.searchFields !== undefined) data.searchFields = JSON.stringify(updates.searchFields) - if (updates.ordering !== undefined) data.ordering = JSON.stringify(updates.ordering) - - await prisma.modelSchema.update({ - where: { name: schemaName }, - data, - }) -} diff --git a/frontends/nextjs/src/lib/db/functions/tenants/add-tenant.ts b/frontends/nextjs/src/lib/db/functions/tenants/add-tenant.ts deleted file mode 100644 index b3af456fa..000000000 --- a/frontends/nextjs/src/lib/db/functions/tenants/add-tenant.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { prisma } from '../../prisma' -import type { Tenant } from '../../../types/level-types' - -/** - * Add a single tenant - * @param tenant - The tenant to add - */ -export const addTenant = async (tenant: Tenant): Promise => { - await prisma.tenant.create({ - data: { - id: tenant.id, - name: tenant.name, - ownerId: tenant.ownerId, - createdAt: BigInt(tenant.createdAt), - homepageConfig: tenant.homepageConfig ? JSON.stringify(tenant.homepageConfig) : null, - }, - }) -} diff --git a/frontends/nextjs/src/lib/db/functions/tenants/delete-tenant.ts b/frontends/nextjs/src/lib/db/functions/tenants/delete-tenant.ts deleted file mode 100644 index 2a0f76858..000000000 --- a/frontends/nextjs/src/lib/db/functions/tenants/delete-tenant.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { prisma } from '../../prisma' - -/** - * Delete a tenant by ID - * @param tenantId - The tenant ID - */ -export const deleteTenant = async (tenantId: string): Promise => { - await prisma.tenant.delete({ where: { id: tenantId } }) -} diff --git a/frontends/nextjs/src/lib/db/functions/tenants/get-tenants.ts b/frontends/nextjs/src/lib/db/functions/tenants/get-tenants.ts deleted file mode 100644 index c4c2cc893..000000000 --- a/frontends/nextjs/src/lib/db/functions/tenants/get-tenants.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { prisma } from '../../prisma' -import type { Tenant } from '../../../types/level-types' - -/** - * Get all tenants - * @returns Array of tenants - */ -export const getTenants = async (): Promise => { - const tenants = await prisma.tenant.findMany() - return tenants.map(t => ({ - id: t.id, - name: t.name, - ownerId: t.ownerId, - createdAt: Number(t.createdAt), - homepageConfig: t.homepageConfig ? JSON.parse(t.homepageConfig) : undefined, - })) -} diff --git a/frontends/nextjs/src/lib/db/functions/tenants/index.ts b/frontends/nextjs/src/lib/db/functions/tenants/index.ts deleted file mode 100644 index 78ba1fb2d..000000000 --- a/frontends/nextjs/src/lib/db/functions/tenants/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { getTenants } from './get-tenants' -export { setTenants } from './set-tenants' -export { addTenant } from './add-tenant' -export { updateTenant } from './update-tenant' -export { deleteTenant } from './delete-tenant' diff --git a/frontends/nextjs/src/lib/db/functions/tenants/set-tenants.ts b/frontends/nextjs/src/lib/db/functions/tenants/set-tenants.ts deleted file mode 100644 index a5ba83a0e..000000000 --- a/frontends/nextjs/src/lib/db/functions/tenants/set-tenants.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { prisma } from '../../prisma' -import type { Tenant } from '../../../types/level-types' - -/** - * Set all tenants (replaces existing) - * @param tenants - Array of tenants - */ -export const setTenants = async (tenants: Tenant[]): Promise => { - await prisma.tenant.deleteMany() - for (const tenant of tenants) { - await prisma.tenant.create({ - data: { - id: tenant.id, - name: tenant.name, - ownerId: tenant.ownerId, - createdAt: BigInt(tenant.createdAt), - homepageConfig: tenant.homepageConfig ? JSON.stringify(tenant.homepageConfig) : null, - }, - }) - } -} diff --git a/frontends/nextjs/src/lib/db/functions/tenants/update-tenant.ts b/frontends/nextjs/src/lib/db/functions/tenants/update-tenant.ts deleted file mode 100644 index 7ad226d71..000000000 --- a/frontends/nextjs/src/lib/db/functions/tenants/update-tenant.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { prisma } from '../../prisma' -import type { Tenant } from '../../../types/level-types' - -/** - * Update a tenant by ID - * @param tenantId - The tenant ID - * @param updates - Partial tenant data - */ -export const updateTenant = async (tenantId: string, updates: Partial): Promise => { - const data: any = {} - if (updates.name !== undefined) data.name = updates.name - if (updates.ownerId !== undefined) data.ownerId = updates.ownerId - if (updates.homepageConfig !== undefined) data.homepageConfig = JSON.stringify(updates.homepageConfig) - - await prisma.tenant.update({ - where: { id: tenantId }, - data, - }) -} diff --git a/frontends/nextjs/src/lib/db/functions/users/add-user.ts b/frontends/nextjs/src/lib/db/functions/users/add-user.ts deleted file mode 100644 index 1bfb0f0d7..000000000 --- a/frontends/nextjs/src/lib/db/functions/users/add-user.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { prisma } from '../../prisma' -import type { User } from '../../../types/level-types' - -/** - * Add a single user - * @param user - The user to add - */ -export const addUser = async (user: User): Promise => { - await prisma.user.create({ - data: { - id: user.id, - username: user.username, - email: user.email, - role: user.role, - profilePicture: user.profilePicture, - bio: user.bio, - createdAt: BigInt(user.createdAt), - tenantId: user.tenantId, - isInstanceOwner: user.isInstanceOwner || false, - passwordChangeTimestamp: null, - firstLogin: false, - }, - }) -} diff --git a/frontends/nextjs/src/lib/db/functions/users/delete-user.ts b/frontends/nextjs/src/lib/db/functions/users/delete-user.ts deleted file mode 100644 index c4e6195d1..000000000 --- a/frontends/nextjs/src/lib/db/functions/users/delete-user.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { prisma } from '../../prisma' - -/** - * Delete a user by ID - * @param userId - The user ID to delete - */ -export const deleteUser = async (userId: string): Promise => { - await prisma.user.delete({ where: { id: userId } }) -} diff --git a/frontends/nextjs/src/lib/db/functions/users/get-users.ts b/frontends/nextjs/src/lib/db/functions/users/get-users.ts deleted file mode 100644 index ff5a9ad1b..000000000 --- a/frontends/nextjs/src/lib/db/functions/users/get-users.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { prisma } from '../../prisma' -import type { User } from '../../../types/level-types' - -/** - * Get all users from database - * @returns Array of users - */ -export const getUsers = async (): Promise => { - const users = await prisma.user.findMany() - return users.map(u => ({ - id: u.id, - username: u.username, - email: u.email, - role: u.role as any, - profilePicture: u.profilePicture || undefined, - bio: u.bio || undefined, - createdAt: Number(u.createdAt), - tenantId: u.tenantId || undefined, - isInstanceOwner: u.isInstanceOwner, - })) -} diff --git a/frontends/nextjs/src/lib/db/functions/users/index.ts b/frontends/nextjs/src/lib/db/functions/users/index.ts deleted file mode 100644 index b6e680b5d..000000000 --- a/frontends/nextjs/src/lib/db/functions/users/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { getUsers } from './get-users' -export { setUsers } from './set-users' -export { addUser } from './add-user' -export { updateUser } from './update-user' -export { deleteUser } from './delete-user' diff --git a/frontends/nextjs/src/lib/db/functions/users/set-users.ts b/frontends/nextjs/src/lib/db/functions/users/set-users.ts deleted file mode 100644 index af711f8ce..000000000 --- a/frontends/nextjs/src/lib/db/functions/users/set-users.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { prisma } from '../../prisma' -import type { User } from '../../../types/level-types' - -/** - * Set all users (replaces existing) - * @param users - Array of users to set - */ -export const setUsers = async (users: User[]): Promise => { - await prisma.user.deleteMany() - for (const user of users) { - await prisma.user.create({ - data: { - id: user.id, - username: user.username, - email: user.email, - role: user.role, - profilePicture: user.profilePicture, - bio: user.bio, - createdAt: BigInt(user.createdAt), - tenantId: user.tenantId, - isInstanceOwner: user.isInstanceOwner || false, - passwordChangeTimestamp: null, - firstLogin: false, - }, - }) - } -} diff --git a/frontends/nextjs/src/lib/db/functions/users/update-user.ts b/frontends/nextjs/src/lib/db/functions/users/update-user.ts deleted file mode 100644 index 7cb11ca51..000000000 --- a/frontends/nextjs/src/lib/db/functions/users/update-user.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { prisma } from '../../prisma' -import type { User } from '../../../types/level-types' - -/** - * Update a user by ID - * @param userId - The user ID to update - * @param updates - Partial user data to update - */ -export const updateUser = async (userId: string, updates: Partial): Promise => { - const data: any = {} - if (updates.username !== undefined) data.username = updates.username - if (updates.email !== undefined) data.email = updates.email - if (updates.role !== undefined) data.role = updates.role - if (updates.profilePicture !== undefined) data.profilePicture = updates.profilePicture - if (updates.bio !== undefined) data.bio = updates.bio - if (updates.tenantId !== undefined) data.tenantId = updates.tenantId - if (updates.isInstanceOwner !== undefined) data.isInstanceOwner = updates.isInstanceOwner - - await prisma.user.update({ - where: { id: userId }, - data, - }) -} diff --git a/frontends/nextjs/src/lib/db/functions/verify-password.ts b/frontends/nextjs/src/lib/db/functions/verify-password.ts deleted file mode 100644 index 309cc55c8..000000000 --- a/frontends/nextjs/src/lib/db/functions/verify-password.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { hashPassword } from './hash-password' - -/** - * Verify a password against a hash - * @param password - The plain text password - * @param hash - The hash to compare against - * @returns True if password matches hash - */ -export const verifyPassword = async (password: string, hash: string): Promise => { - const inputHash = await hashPassword(password) - return inputHash === hash -} diff --git a/frontends/nextjs/src/lib/db/functions/workflows/add-workflow.ts b/frontends/nextjs/src/lib/db/functions/workflows/add-workflow.ts deleted file mode 100644 index 470118611..000000000 --- a/frontends/nextjs/src/lib/db/functions/workflows/add-workflow.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { prisma } from '../../prisma' -import type { Workflow } from '../../../types/level-types' - -/** - * Add a single workflow - * @param workflow - The workflow to add - */ -export const addWorkflow = async (workflow: Workflow): Promise => { - await prisma.workflow.create({ - data: { - id: workflow.id, - name: workflow.name, - description: workflow.description, - nodes: JSON.stringify(workflow.nodes), - edges: JSON.stringify(workflow.edges), - enabled: workflow.enabled, - }, - }) -} diff --git a/frontends/nextjs/src/lib/db/functions/workflows/delete-workflow.ts b/frontends/nextjs/src/lib/db/functions/workflows/delete-workflow.ts deleted file mode 100644 index 0b6a80d30..000000000 --- a/frontends/nextjs/src/lib/db/functions/workflows/delete-workflow.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { prisma } from '../../prisma' - -/** - * Delete a workflow by ID - * @param workflowId - The workflow ID - */ -export const deleteWorkflow = async (workflowId: string): Promise => { - await prisma.workflow.delete({ where: { id: workflowId } }) -} diff --git a/frontends/nextjs/src/lib/db/functions/workflows/get-workflows.ts b/frontends/nextjs/src/lib/db/functions/workflows/get-workflows.ts deleted file mode 100644 index 102515eed..000000000 --- a/frontends/nextjs/src/lib/db/functions/workflows/get-workflows.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { prisma } from '../../prisma' -import type { Workflow } from '../../../types/level-types' - -/** - * Get all workflows - * @returns Array of workflows - */ -export const getWorkflows = async (): Promise => { - const workflows = await prisma.workflow.findMany() - return workflows.map(w => ({ - id: w.id, - name: w.name, - description: w.description || undefined, - nodes: JSON.parse(w.nodes), - edges: JSON.parse(w.edges), - enabled: w.enabled, - })) -} diff --git a/frontends/nextjs/src/lib/db/functions/workflows/index.ts b/frontends/nextjs/src/lib/db/functions/workflows/index.ts deleted file mode 100644 index 35db1eaaa..000000000 --- a/frontends/nextjs/src/lib/db/functions/workflows/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { getWorkflows } from './get-workflows' -export { setWorkflows } from './set-workflows' -export { addWorkflow } from './add-workflow' -export { updateWorkflow } from './update-workflow' -export { deleteWorkflow } from './delete-workflow' diff --git a/frontends/nextjs/src/lib/db/functions/workflows/set-workflows.ts b/frontends/nextjs/src/lib/db/functions/workflows/set-workflows.ts deleted file mode 100644 index 25c35a3c2..000000000 --- a/frontends/nextjs/src/lib/db/functions/workflows/set-workflows.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { prisma } from '../../prisma' -import type { Workflow } from '../../../types/level-types' - -/** - * Set all workflows (replaces existing) - * @param workflows - Array of workflows - */ -export const setWorkflows = async (workflows: Workflow[]): Promise => { - await prisma.workflow.deleteMany() - for (const workflow of workflows) { - await prisma.workflow.create({ - data: { - id: workflow.id, - name: workflow.name, - description: workflow.description, - nodes: JSON.stringify(workflow.nodes), - edges: JSON.stringify(workflow.edges), - enabled: workflow.enabled, - }, - }) - } -} diff --git a/frontends/nextjs/src/lib/db/functions/workflows/update-workflow.ts b/frontends/nextjs/src/lib/db/functions/workflows/update-workflow.ts deleted file mode 100644 index 3458ec265..000000000 --- a/frontends/nextjs/src/lib/db/functions/workflows/update-workflow.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { prisma } from '../../prisma' -import type { Workflow } from '../../../types/level-types' - -/** - * Update a workflow by ID - * @param workflowId - The workflow ID - * @param updates - Partial workflow data - */ -export const updateWorkflow = async (workflowId: string, updates: Partial): Promise => { - const data: any = {} - if (updates.name !== undefined) data.name = updates.name - if (updates.description !== undefined) data.description = updates.description - if (updates.nodes !== undefined) data.nodes = JSON.stringify(updates.nodes) - if (updates.edges !== undefined) data.edges = JSON.stringify(updates.edges) - if (updates.enabled !== undefined) data.enabled = updates.enabled - - await prisma.workflow.update({ - where: { id: workflowId }, - data, - }) -} diff --git a/frontends/nextjs/src/lib/db/god-credentials/get-god-credentials-expiry-duration.ts b/frontends/nextjs/src/lib/db/god-credentials/get-god-credentials-expiry-duration.ts index c47fcf9c2..b3c7e95d9 100644 --- a/frontends/nextjs/src/lib/db/god-credentials/get-god-credentials-expiry-duration.ts +++ b/frontends/nextjs/src/lib/db/god-credentials/get-god-credentials-expiry-duration.ts @@ -7,6 +7,6 @@ export async function getGodCredentialsExpiryDuration(): Promise { const adapter = getAdapter() const config = await adapter.findFirst('SystemConfig', { where: { key: 'god_credentials_expiry_duration' }, - }) + }) as { value: string } | null return config ? Number(config.value) : 60 * 60 * 1000 // Default 1 hour } diff --git a/frontends/nextjs/src/lib/db/god-credentials/get-god-credentials-expiry.ts b/frontends/nextjs/src/lib/db/god-credentials/get-god-credentials-expiry.ts index 66e67b6b8..e70f3f6dd 100644 --- a/frontends/nextjs/src/lib/db/god-credentials/get-god-credentials-expiry.ts +++ b/frontends/nextjs/src/lib/db/god-credentials/get-god-credentials-expiry.ts @@ -7,6 +7,6 @@ export async function getGodCredentialsExpiry(): Promise { const adapter = getAdapter() const config = await adapter.findFirst('SystemConfig', { where: { key: 'god_credentials_expiry' }, - }) + }) as { value: string } | null return config ? Number(config.value) : 0 } diff --git a/frontends/nextjs/src/lib/db/god-credentials/set-first-login-flag.ts b/frontends/nextjs/src/lib/db/god-credentials/set-first-login-flag.ts index 824a2d5d5..0d57c3e9c 100644 --- a/frontends/nextjs/src/lib/db/god-credentials/set-first-login-flag.ts +++ b/frontends/nextjs/src/lib/db/god-credentials/set-first-login-flag.ts @@ -8,7 +8,7 @@ export async function setFirstLoginFlag(username: string, isFirstLogin: boolean) // Find the user first const user = await adapter.findFirst('User', { where: { username }, - }) + }) as { id: string } | null if (user) { await adapter.update('User', user.id, { firstLogin: isFirstLogin }) } diff --git a/frontends/nextjs/src/lib/db/god-credentials/should-show-god-credentials.ts b/frontends/nextjs/src/lib/db/god-credentials/should-show-god-credentials.ts index fd127cb53..369607389 100644 --- a/frontends/nextjs/src/lib/db/god-credentials/should-show-god-credentials.ts +++ b/frontends/nextjs/src/lib/db/god-credentials/should-show-god-credentials.ts @@ -13,7 +13,7 @@ export async function shouldShowGodCredentials(): Promise { // Get god user's password change timestamp const godUser = await adapter.findFirst('User', { where: { username: 'god' }, - }) + }) as { passwordChangeTimestamp?: bigint | number } | null const godPasswordChangeTime = godUser?.passwordChangeTimestamp ? Number(godUser.passwordChangeTimestamp) : 0 if (expiry === 0) { diff --git a/frontends/nextjs/src/lib/db/index.ts b/frontends/nextjs/src/lib/db/index.ts index 6caccacbc..7b164b348 100644 --- a/frontends/nextjs/src/lib/db/index.ts +++ b/frontends/nextjs/src/lib/db/index.ts @@ -131,4 +131,62 @@ export class Database { static addComponentConfig = components.addComponentConfig static updateComponentConfig = components.updateComponentConfig static deleteComponentConfig = components.deleteComponentConfig + + // CSS Classes + static getCssClasses = cssClasses.getCssClasses + static setCssClasses = cssClasses.setCssClasses + static addCssCategory = cssClasses.addCssCategory + static updateCssCategory = cssClasses.updateCssCategory + static deleteCssCategory = cssClasses.deleteCssCategory + + // Dropdown Configs + static getDropdownConfigs = dropdownConfigs.getDropdownConfigs + static setDropdownConfigs = dropdownConfigs.setDropdownConfigs + static addDropdownConfig = dropdownConfigs.addDropdownConfig + static updateDropdownConfig = dropdownConfigs.updateDropdownConfig + static deleteDropdownConfig = dropdownConfigs.deleteDropdownConfig + + // Tenants + static getTenants = tenants.getTenants + static setTenants = tenants.setTenants + static addTenant = tenants.addTenant + static updateTenant = tenants.updateTenant + static deleteTenant = tenants.deleteTenant + + // Packages + static getInstalledPackages = packages.getInstalledPackages + static setInstalledPackages = packages.setInstalledPackages + static installPackage = packages.installPackage + static uninstallPackage = packages.uninstallPackage + static togglePackageEnabled = packages.togglePackageEnabled + static getPackageData = packages.getPackageData + static setPackageData = packages.setPackageData + static deletePackageData = packages.deletePackageData + + // Power Transfers + static getPowerTransferRequests = powerTransfers.getPowerTransferRequests + static setPowerTransferRequests = powerTransfers.setPowerTransferRequests + static addPowerTransferRequest = powerTransfers.addPowerTransferRequest + static updatePowerTransferRequest = powerTransfers.updatePowerTransferRequest + static deletePowerTransferRequest = powerTransfers.deletePowerTransferRequest + + // SMTP Config + static getSMTPConfig = smtpConfig.getSMTPConfig + static setSMTPConfig = smtpConfig.setSMTPConfig + + // God Credentials + static getGodCredentialsExpiry = godCredentials.getGodCredentialsExpiry + static setGodCredentialsExpiry = godCredentials.setGodCredentialsExpiry + static getFirstLoginFlags = godCredentials.getFirstLoginFlags + static setFirstLoginFlag = godCredentials.setFirstLoginFlag + static getGodCredentialsExpiryDuration = godCredentials.getGodCredentialsExpiryDuration + static setGodCredentialsExpiryDuration = godCredentials.setGodCredentialsExpiryDuration + static shouldShowGodCredentials = godCredentials.shouldShowGodCredentials + static resetGodCredentialsExpiry = godCredentials.resetGodCredentialsExpiry + + // Database Admin + static clearDatabase = databaseAdmin.clearDatabase + static exportDatabase = databaseAdmin.exportDatabase + static importDatabase = databaseAdmin.importDatabase + static seedDefaultData = databaseAdmin.seedDefaultData } diff --git a/frontends/nextjs/src/lib/db/packages/get-installed-packages.ts b/frontends/nextjs/src/lib/db/packages/get-installed-packages.ts index daf0b4cc8..9a9a74d3b 100644 --- a/frontends/nextjs/src/lib/db/packages/get-installed-packages.ts +++ b/frontends/nextjs/src/lib/db/packages/get-installed-packages.ts @@ -1,5 +1,5 @@ import { getAdapter } from '../dbal-client' -import type { InstalledPackage } from '../../package-types' +import type { InstalledPackage } from '../../packages/package-types' /** * Get all installed packages from database diff --git a/frontends/nextjs/src/lib/db/packages/get-package-data.ts b/frontends/nextjs/src/lib/db/packages/get-package-data.ts index cfa606fa8..19638e5b9 100644 --- a/frontends/nextjs/src/lib/db/packages/get-package-data.ts +++ b/frontends/nextjs/src/lib/db/packages/get-package-data.ts @@ -7,7 +7,7 @@ export async function getPackageData(packageId: string): Promise { + const counts: Record = { All: LUA_SNIPPETS.length } + + for (const snippet of LUA_SNIPPETS) { + counts[snippet.category] = (counts[snippet.category] || 0) + 1 + } + + return counts + } + + /** + * Get all unique tags across snippets + */ + static getAllTags(): string[] { + const tagSet = new Set() + + for (const snippet of LUA_SNIPPETS) { + for (const tag of snippet.tags) { + tagSet.add(tag) + } + } + + return Array.from(tagSet).sort() + } +} + +// Re-export types for convenience +export type { LuaSnippet } +export { LUA_SNIPPET_CATEGORIES, LUA_SNIPPETS } diff --git a/frontends/nextjs/src/lib/lua/lua-snippets.ts b/frontends/nextjs/src/lib/lua/lua-snippets.ts index 2317c59e6..f80ac352c 100644 --- a/frontends/nextjs/src/lib/lua/lua-snippets.ts +++ b/frontends/nextjs/src/lib/lua/lua-snippets.ts @@ -1,998 +1,26 @@ -export interface LuaSnippet { - id: string - name: string - description: string - category: string - code: string - tags: string[] - parameters?: Array<{ name: string; type: string; description: string }> -} - -export const LUA_SNIPPET_CATEGORIES = [ - 'All', - 'Data Validation', - 'Data Transformation', - 'Array Operations', - 'String Processing', - 'Math & Calculations', - 'Conditionals & Logic', - 'User Management', - 'Error Handling', - 'API & Networking', - 'Date & Time', - 'File Operations', - 'Utilities' -] as const - -export const LUA_SNIPPETS: LuaSnippet[] = [ - { - id: 'validate_email', - name: 'Email Validation', - description: 'Validate email format using pattern matching', - category: 'Data Validation', - tags: ['validation', 'email', 'regex'], - parameters: [ - { name: 'email', type: 'string', description: 'Email address to validate' } - ], - code: `local email = context.data.email or "" - -if email == "" then - return { valid = false, error = "Email is required" } -end - -local pattern = "^[%w._%%-]+@[%w._%%-]+%.%w+$" -if not string.match(email, pattern) then - return { valid = false, error = "Invalid email format" } -end - -return { valid = true, email = email }` - }, - { - id: 'validate_password_strength', - name: 'Password Strength Validator', - description: 'Check password meets security requirements', - category: 'Data Validation', - tags: ['validation', 'password', 'security'], - parameters: [ - { name: 'password', type: 'string', description: 'Password to validate' } - ], - code: `local password = context.data.password or "" - -if string.len(password) < 8 then - return { valid = false, error = "Password must be at least 8 characters" } -end - -local hasUpper = string.match(password, "%u") ~= nil -local hasLower = string.match(password, "%l") ~= nil -local hasDigit = string.match(password, "%d") ~= nil -local hasSpecial = string.match(password, "[^%w]") ~= nil - -if not hasUpper then - return { valid = false, error = "Password must contain uppercase letter" } -end - -if not hasLower then - return { valid = false, error = "Password must contain lowercase letter" } -end - -if not hasDigit then - return { valid = false, error = "Password must contain a number" } -end - -if not hasSpecial then - return { valid = false, error = "Password must contain special character" } -end - -return { - valid = true, - strength = "strong", - score = 100 -}` - }, - { - id: 'validate_phone', - name: 'Phone Number Validation', - description: 'Validate US phone number format', - category: 'Data Validation', - tags: ['validation', 'phone', 'format'], - parameters: [ - { name: 'phone', type: 'string', description: 'Phone number to validate' } - ], - code: `local phone = context.data.phone or "" - -local cleaned = string.gsub(phone, "[^%d]", "") - -if string.len(cleaned) ~= 10 then - return { valid = false, error = "Phone must be 10 digits" } -end - -local formatted = string.format("(%s) %s-%s", - string.sub(cleaned, 1, 3), - string.sub(cleaned, 4, 6), - string.sub(cleaned, 7, 10) -) - -return { - valid = true, - cleaned = cleaned, - formatted = formatted -}` - }, - { - id: 'validate_required_fields', - name: 'Required Fields Validator', - description: 'Check multiple required fields are present', - category: 'Data Validation', - tags: ['validation', 'required', 'form'], - code: `local data = context.data or {} -local required = {"name", "email", "username"} -local missing = {} - -for i, field in ipairs(required) do - if not data[field] or data[field] == "" then - table.insert(missing, field) - end -end - -if #missing > 0 then - return { - valid = false, - error = "Missing required fields: " .. table.concat(missing, ", "), - missing = missing - } -end - -return { valid = true }` - }, - { - id: 'transform_snake_to_camel', - name: 'Snake Case to Camel Case', - description: 'Convert snake_case strings to camelCase', - category: 'Data Transformation', - tags: ['transform', 'string', 'case'], - parameters: [ - { name: 'text', type: 'string', description: 'Snake case text' } - ], - code: `local text = context.data.text or "" - -local result = string.gsub(text, "_(%w)", function(c) - return string.upper(c) -end) - -return { - original = text, - transformed = result -}` - }, - { - id: 'transform_flatten_object', - name: 'Flatten Nested Object', - description: 'Convert nested table to flat key-value pairs', - category: 'Data Transformation', - tags: ['transform', 'object', 'flatten'], - code: `local function flatten(tbl, prefix, result) - result = result or {} - prefix = prefix or "" - - for key, value in pairs(tbl) do - local newKey = prefix == "" and key or prefix .. "." .. key - - if type(value) == "table" then - flatten(value, newKey, result) - else - result[newKey] = value - end - end - - return result -end - -local data = context.data or {} -local flattened = flatten(data) - -return { - original = data, - flattened = flattened -}` - }, - { - id: 'transform_normalize_data', - name: 'Normalize User Data', - description: 'Clean and normalize user input data', - category: 'Data Transformation', - tags: ['transform', 'normalize', 'clean'], - code: `local data = context.data or {} - -local function trim(s) - return string.match(s, "^%s*(.-)%s*$") -end - -local normalized = {} - -if data.email then - normalized.email = string.lower(trim(data.email)) -end - -if data.name then - normalized.name = trim(data.name) - local words = {} - for word in string.gmatch(normalized.name, "%S+") do - table.insert(words, string.upper(string.sub(word, 1, 1)) .. string.lower(string.sub(word, 2))) - end - normalized.name = table.concat(words, " ") -end - -if data.phone then - normalized.phone = string.gsub(data.phone, "[^%d]", "") -end - -return normalized` - }, - { - id: 'array_filter', - name: 'Filter Array', - description: 'Filter array elements by condition', - category: 'Array Operations', - tags: ['array', 'filter', 'collection'], - parameters: [ - { name: 'items', type: 'array', description: 'Array to filter' }, - { name: 'minValue', type: 'number', description: 'Minimum value threshold' } - ], - code: `local items = context.data.items or {} -local minValue = context.data.minValue or 0 -local filtered = {} - -for i, item in ipairs(items) do - if item.value >= minValue then - table.insert(filtered, item) - end -end - -log("Filtered " .. #filtered .. " of " .. #items .. " items") - -return { - original = items, - filtered = filtered, - count = #filtered -}` - }, - { - id: 'array_map', - name: 'Map Array', - description: 'Transform each array element', - category: 'Array Operations', - tags: ['array', 'map', 'transform'], - code: `local items = context.data.items or {} -local mapped = {} - -for i, item in ipairs(items) do - table.insert(mapped, { - id = item.id, - label = string.upper(item.name or ""), - value = (item.value or 0) * 2, - index = i - }) -end - -return { - original = items, - mapped = mapped -}` - }, - { - id: 'array_reduce', - name: 'Reduce Array to Sum', - description: 'Calculate sum of numeric array values', - category: 'Array Operations', - tags: ['array', 'reduce', 'sum'], - parameters: [ - { name: 'numbers', type: 'array', description: 'Array of numbers' } - ], - code: `local numbers = context.data.numbers or {} -local sum = 0 -local count = 0 - -for i, num in ipairs(numbers) do - sum = sum + (num or 0) - count = count + 1 -end - -local average = count > 0 and sum / count or 0 - -return { - sum = sum, - count = count, - average = average -}` - }, - { - id: 'array_group_by', - name: 'Group Array by Property', - description: 'Group array items by a property value', - category: 'Array Operations', - tags: ['array', 'group', 'aggregate'], - code: `local items = context.data.items or {} -local groupKey = context.data.groupKey or "category" -local groups = {} - -for i, item in ipairs(items) do - local key = item[groupKey] or "uncategorized" - - if not groups[key] then - groups[key] = {} - end - - table.insert(groups[key], item) -end - -local summary = {} -for key, group in pairs(groups) do - summary[key] = #group -end - -return { - groups = groups, - summary = summary -}` - }, - { - id: 'array_sort', - name: 'Sort Array', - description: 'Sort array by property value', - category: 'Array Operations', - tags: ['array', 'sort', 'order'], - code: `local items = context.data.items or {} -local sortKey = context.data.sortKey or "value" -local descending = context.data.descending or false - -table.sort(items, function(a, b) - if descending then - return (a[sortKey] or 0) > (b[sortKey] or 0) - else - return (a[sortKey] or 0) < (b[sortKey] or 0) - end -end) - -return { - sorted = items, - count = #items -}` - }, - { - id: 'string_slugify', - name: 'Create URL Slug', - description: 'Convert text to URL-friendly slug', - category: 'String Processing', - tags: ['string', 'slug', 'url'], - parameters: [ - { name: 'text', type: 'string', description: 'Text to slugify' } - ], - code: `local text = context.data.text or "" - -local slug = string.lower(text) -slug = string.gsub(slug, "%s+", "-") -slug = string.gsub(slug, "[^%w%-]", "") -slug = string.gsub(slug, "%-+", "-") -slug = string.gsub(slug, "^%-+", "") -slug = string.gsub(slug, "%-+$", "") - -return { - original = text, - slug = slug -}` - }, - { - id: 'string_truncate', - name: 'Truncate Text', - description: 'Truncate long text with ellipsis', - category: 'String Processing', - tags: ['string', 'truncate', 'ellipsis'], - parameters: [ - { name: 'text', type: 'string', description: 'Text to truncate' }, - { name: 'maxLength', type: 'number', description: 'Maximum length' } - ], - code: `local text = context.data.text or "" -local maxLength = context.data.maxLength or 50 - -if string.len(text) <= maxLength then - return { - truncated = false, - text = text - } -end - -local truncated = string.sub(text, 1, maxLength - 3) .. "..." - -return { - truncated = true, - text = truncated, - originalLength = string.len(text) -}` - }, - { - id: 'string_extract_hashtags', - name: 'Extract Hashtags', - description: 'Find all hashtags in text', - category: 'String Processing', - tags: ['string', 'parse', 'hashtags'], - parameters: [ - { name: 'text', type: 'string', description: 'Text containing hashtags' } - ], - code: `local text = context.data.text or "" -local hashtags = {} - -for tag in string.gmatch(text, "#(%w+)") do - table.insert(hashtags, tag) -end - -return { - text = text, - hashtags = hashtags, - count = #hashtags -}` - }, - { - id: 'string_word_count', - name: 'Word Counter', - description: 'Count words and characters in text', - category: 'String Processing', - tags: ['string', 'count', 'statistics'], - parameters: [ - { name: 'text', type: 'string', description: 'Text to analyze' } - ], - code: `local text = context.data.text or "" - -local charCount = string.len(text) -local words = {} - -for word in string.gmatch(text, "%S+") do - table.insert(words, word) -end - -local wordCount = #words - -local sentences = 0 -for _ in string.gmatch(text, "[.!?]+") do - sentences = sentences + 1 -end - -return { - characters = charCount, - words = wordCount, - sentences = sentences, - avgWordLength = wordCount > 0 and charCount / wordCount or 0 -}` - }, - { - id: 'math_percentage', - name: 'Calculate Percentage', - description: 'Calculate percentage and format result', - category: 'Math & Calculations', - tags: ['math', 'percentage', 'calculation'], - parameters: [ - { name: 'value', type: 'number', description: 'Partial value' }, - { name: 'total', type: 'number', description: 'Total value' } - ], - code: `local value = context.data.value or 0 -local total = context.data.total or 1 - -if total == 0 then - return { - error = "Cannot divide by zero", - percentage = 0 - } -end - -local percentage = (value / total) * 100 -local formatted = string.format("%.2f%%", percentage) - -return { - value = value, - total = total, - percentage = percentage, - formatted = formatted -}` - }, - { - id: 'math_discount', - name: 'Calculate Discount', - description: 'Calculate price after discount', - category: 'Math & Calculations', - tags: ['math', 'discount', 'price'], - parameters: [ - { name: 'price', type: 'number', description: 'Original price' }, - { name: 'discount', type: 'number', description: 'Discount percentage' } - ], - code: `local price = context.data.price or 0 -local discount = context.data.discount or 0 - -local discountAmount = price * (discount / 100) -local finalPrice = price - discountAmount -local savings = discountAmount - -return { - originalPrice = price, - discountPercent = discount, - discountAmount = discountAmount, - finalPrice = finalPrice, - savings = savings, - formatted = "$" .. string.format("%.2f", finalPrice) -}` - }, - { - id: 'math_compound_interest', - name: 'Compound Interest Calculator', - description: 'Calculate compound interest over time', - category: 'Math & Calculations', - tags: ['math', 'interest', 'finance'], - parameters: [ - { name: 'principal', type: 'number', description: 'Initial amount' }, - { name: 'rate', type: 'number', description: 'Interest rate (%)' }, - { name: 'years', type: 'number', description: 'Number of years' } - ], - code: `local principal = context.data.principal or 1000 -local rate = (context.data.rate or 5) / 100 -local years = context.data.years or 1 -local compounds = 12 - -local amount = principal * math.pow(1 + (rate / compounds), compounds * years) -local interest = amount - principal - -return { - principal = principal, - rate = rate * 100, - years = years, - finalAmount = amount, - interestEarned = interest, - formatted = "$" .. string.format("%.2f", amount) -}` - }, - { - id: 'math_statistics', - name: 'Statistical Analysis', - description: 'Calculate mean, median, mode, std dev', - category: 'Math & Calculations', - tags: ['math', 'statistics', 'analysis'], - parameters: [ - { name: 'numbers', type: 'array', description: 'Array of numbers' } - ], - code: `local numbers = context.data.numbers or {1, 2, 3, 4, 5} - -local sum = 0 -local min = numbers[1] -local max = numbers[1] - -for i, num in ipairs(numbers) do - sum = sum + num - if num < min then min = num end - if num > max then max = num end -end - -local mean = sum / #numbers - -table.sort(numbers) -local median -if #numbers % 2 == 0 then - median = (numbers[#numbers/2] + numbers[#numbers/2 + 1]) / 2 -else - median = numbers[math.ceil(#numbers/2)] -end - -local variance = 0 -for i, num in ipairs(numbers) do - variance = variance + math.pow(num - mean, 2) -end -variance = variance / #numbers - -local stdDev = math.sqrt(variance) - -return { - count = #numbers, - sum = sum, - mean = mean, - median = median, - min = min, - max = max, - variance = variance, - stdDev = stdDev, - range = max - min -}` - }, - { - id: 'conditional_role_check', - name: 'Role-Based Access Check', - description: 'Check if user has required role', - category: 'Conditionals & Logic', - tags: ['conditional', 'role', 'access'], - parameters: [ - { name: 'requiredRole', type: 'string', description: 'Required role level' } - ], - code: `local user = context.user or {} -local requiredRole = context.data.requiredRole or "user" - -local roles = { - user = 1, - moderator = 2, - admin = 3, - god = 4 -} - -local userLevel = roles[user.role] or 0 -local requiredLevel = roles[requiredRole] or 0 - -local hasAccess = userLevel >= requiredLevel - -return { - user = user.username, - userRole = user.role, - requiredRole = requiredRole, - hasAccess = hasAccess, - message = hasAccess and "Access granted" or "Access denied" -}` - }, - { - id: 'conditional_time_based', - name: 'Time-Based Logic', - description: 'Execute logic based on time of day', - category: 'Conditionals & Logic', - tags: ['conditional', 'time', 'schedule'], - code: `local hour = tonumber(os.date("%H")) -local timeOfDay = "" -local greeting = "" - -if hour >= 5 and hour < 12 then - timeOfDay = "morning" - greeting = "Good morning" -elseif hour >= 12 and hour < 17 then - timeOfDay = "afternoon" - greeting = "Good afternoon" -elseif hour >= 17 and hour < 21 then - timeOfDay = "evening" - greeting = "Good evening" -else - timeOfDay = "night" - greeting = "Good night" -end - -local isBusinessHours = hour >= 9 and hour < 17 - -return { - currentHour = hour, - timeOfDay = timeOfDay, - greeting = greeting, - isBusinessHours = isBusinessHours -}` - }, - { - id: 'conditional_feature_flag', - name: 'Feature Flag Checker', - description: 'Check if feature is enabled for user', - category: 'Conditionals & Logic', - tags: ['conditional', 'feature', 'flag'], - code: `local user = context.user or {} -local feature = context.data.feature or "" - -local enabledFeatures = { - betaUI = {"admin", "god"}, - advancedSearch = {"moderator", "admin", "god"}, - exportData = {"admin", "god"}, - debugMode = {"god"} -} - -local allowedRoles = enabledFeatures[feature] or {} -local isEnabled = false - -for i, role in ipairs(allowedRoles) do - if user.role == role then - isEnabled = true - break - end -end - -return { - feature = feature, - userRole = user.role, - enabled = isEnabled, - reason = isEnabled and "Feature available" or "Feature not available for your role" -}` - }, - { - id: 'error_try_catch', - name: 'Try-Catch Pattern', - description: 'Safe execution with error handling', - category: 'Error Handling', - tags: ['error', 'exception', 'safety'], - code: `local function riskyOperation() - local data = context.data or {} - - if not data.value then - error("Value is required") - end - - if data.value < 0 then - error("Value must be positive") - end - - return data.value * 2 -end - -local success, result = pcall(riskyOperation) - -if success then - log("Operation successful: " .. tostring(result)) - return { - success = true, - result = result - } -else - log("Operation failed: " .. tostring(result)) - return { - success = false, - error = tostring(result) - } -end` - }, - { - id: 'error_validation_accumulator', - name: 'Validation Error Accumulator', - description: 'Collect all validation errors at once', - category: 'Error Handling', - tags: ['error', 'validation', 'accumulator'], - code: `local data = context.data or {} -local errors = {} - -if not data.name or data.name == "" then - table.insert(errors, "Name is required") -end - -if not data.email or data.email == "" then - table.insert(errors, "Email is required") -elseif not string.match(data.email, "@") then - table.insert(errors, "Email format is invalid") -end - -if not data.age then - table.insert(errors, "Age is required") -elseif data.age < 18 then - table.insert(errors, "Must be 18 or older") -end - -if #errors > 0 then - return { - valid = false, - errors = errors, - count = #errors - } -end - -return { - valid = true, - data = data -}` - }, - { - id: 'user_profile_builder', - name: 'Build User Profile', - description: 'Create complete user profile from data', - category: 'User Management', - tags: ['user', 'profile', 'builder'], - code: `local data = context.data or {} - -local profile = { - id = "user_" .. os.time(), - username = data.username or "", - email = string.lower(data.email or ""), - displayName = data.displayName or data.username or "", - role = "user", - status = "active", - createdAt = os.time(), - metadata = { - source = "registration", - version = "1.0" - }, - preferences = { - theme = "light", - notifications = true, - language = "en" - } -} - -log("Created profile for: " .. profile.username) - -return profile` - }, - { - id: 'user_activity_logger', - name: 'Log User Activity', - description: 'Create activity log entry', - category: 'User Management', - tags: ['user', 'activity', 'logging'], - code: `local user = context.user or {} -local action = context.data.action or "unknown" -local details = context.data.details or {} - -local activity = { - id = "activity_" .. os.time(), - userId = user.id or "unknown", - username = user.username or "anonymous", - action = action, - details = details, - timestamp = os.time(), - date = os.date("%Y-%m-%d %H:%M:%S"), - ipAddress = "0.0.0.0", - userAgent = "MetaBuilder/1.0" -} - -log("Activity logged: " .. user.username .. " - " .. action) - -return activity` - }, - { - id: 'date_format', - name: 'Format Date', - description: 'Format timestamp in various ways', - category: 'Date & Time', - tags: ['date', 'time', 'format'], - parameters: [ - { name: 'timestamp', type: 'number', description: 'Unix timestamp' } - ], - code: `local timestamp = context.data.timestamp or os.time() - -local formatted = { - full = os.date("%Y-%m-%d %H:%M:%S", timestamp), - date = os.date("%Y-%m-%d", timestamp), - time = os.date("%H:%M:%S", timestamp), - readable = os.date("%B %d, %Y at %I:%M %p", timestamp), - iso = os.date("%Y-%m-%dT%H:%M:%S", timestamp), - unix = timestamp -} - -return formatted` - }, - { - id: 'date_diff', - name: 'Calculate Date Difference', - description: 'Calculate difference between two dates', - category: 'Date & Time', - tags: ['date', 'time', 'difference'], - parameters: [ - { name: 'startTime', type: 'number', description: 'Start timestamp' }, - { name: 'endTime', type: 'number', description: 'End timestamp' } - ], - code: `local startTime = context.data.startTime or os.time() -local endTime = context.data.endTime or os.time() - -local diffSeconds = math.abs(endTime - startTime) -local diffMinutes = math.floor(diffSeconds / 60) -local diffHours = math.floor(diffMinutes / 60) -local diffDays = math.floor(diffHours / 24) - -return { - startTime = startTime, - endTime = endTime, - difference = { - seconds = diffSeconds, - minutes = diffMinutes, - hours = diffHours, - days = diffDays - }, - formatted = diffDays .. " days, " .. (diffHours % 24) .. " hours" -}` - }, - { - id: 'json_parse', - name: 'Safe JSON Parse', - description: 'Parse JSON string with error handling', - category: 'Utilities', - tags: ['json', 'parse', 'utility'], - parameters: [ - { name: 'jsonString', type: 'string', description: 'JSON string to parse' } - ], - code: `local jsonString = context.data.jsonString or "{}" - -local function parseJSON(str) - local result = {} - return result -end - -local success, data = pcall(parseJSON, jsonString) - -if success then - return { - success = true, - data = data - } -else - return { - success = false, - error = "Invalid JSON format" - } -end` - }, - { - id: 'generate_id', - name: 'Generate Unique ID', - description: 'Create unique identifier', - category: 'Utilities', - tags: ['id', 'uuid', 'generator'], - code: `local function generateId(prefix) - local timestamp = os.time() - local random = math.random(1000, 9999) - return (prefix or "id") .. "_" .. timestamp .. "_" .. random -end - -local id = generateId(context.data.prefix) - -return { - id = id, - timestamp = os.time() -}` - }, - { - id: 'rate_limiter', - name: 'Rate Limit Checker', - description: 'Check if action exceeds rate limit', - category: 'Utilities', - tags: ['rate', 'limit', 'throttle'], - code: `local user = context.user or {} -local action = context.data.action or "default" -local maxAttempts = context.data.maxAttempts or 5 -local windowSeconds = context.data.windowSeconds or 60 - -local currentTime = os.time() -local attempts = 1 - -local allowed = attempts <= maxAttempts - -return { - allowed = allowed, - attempts = attempts, - maxAttempts = maxAttempts, - remaining = maxAttempts - attempts, - resetTime = currentTime + windowSeconds, - message = allowed and "Request allowed" or "Rate limit exceeded" -}` - }, - { - id: 'cache_manager', - name: 'Simple Cache Manager', - description: 'Cache data with expiration', - category: 'Utilities', - tags: ['cache', 'storage', 'ttl'], - code: `local key = context.data.key or "cache_key" -local value = context.data.value -local ttl = context.data.ttl or 300 - -local cached = { - key = key, - value = value, - cachedAt = os.time(), - expiresAt = os.time() + ttl, - ttl = ttl -} - -log("Cached " .. key .. " for " .. ttl .. " seconds") - -return cached` - } -] - -export function getSnippetsByCategory(category: string): LuaSnippet[] { - if (category === 'All') { - return LUA_SNIPPETS - } - return LUA_SNIPPETS.filter(snippet => snippet.category === category) -} - -export function searchSnippets(query: string): LuaSnippet[] { - const lowerQuery = query.toLowerCase() - return LUA_SNIPPETS.filter(snippet => - snippet.name.toLowerCase().includes(lowerQuery) || - snippet.description.toLowerCase().includes(lowerQuery) || - snippet.tags.some(tag => tag.toLowerCase().includes(lowerQuery)) - ) -} - -export function getSnippetById(id: string): LuaSnippet | undefined { - return LUA_SNIPPETS.find(snippet => snippet.id === id) -} +/** + * Lua Snippets - Re-exports for backward compatibility + * + * Prefer importing from LuaSnippetUtils class or individual functions: + * + * @example + * // Class pattern (recommended) + * import { LuaSnippetUtils } from '@/lib/lua/LuaSnippetUtils' + * const snippets = LuaSnippetUtils.getByCategory('Validation') + * + * @example + * // Individual function import + * import { getSnippetsByCategory } from '@/lib/lua/functions/get-snippets-by-category' + */ + +// Re-export types and data +export type { LuaSnippet } from './snippets/lua-snippets-data' +export { LUA_SNIPPET_CATEGORIES, LUA_SNIPPETS } from './snippets/lua-snippets-data' + +// Re-export functions for backward compatibility +export { getSnippetsByCategory } from './functions/get-snippets-by-category' +export { searchSnippets } from './functions/search-snippets' +export { getSnippetById } from './functions/get-snippet-by-id' + +// Re-export class wrapper +export { LuaSnippetUtils } from './LuaSnippetUtils' diff --git a/frontends/nextjs/src/lib/lua/snippets/lua-snippets-data.ts b/frontends/nextjs/src/lib/lua/snippets/lua-snippets-data.ts new file mode 100644 index 000000000..cfff8316c --- /dev/null +++ b/frontends/nextjs/src/lib/lua/snippets/lua-snippets-data.ts @@ -0,0 +1,981 @@ +export interface LuaSnippet { + id: string + name: string + description: string + category: string + code: string + tags: string[] + parameters?: Array<{ name: string; type: string; description: string }> +} + +export const LUA_SNIPPET_CATEGORIES = [ + 'All', + 'Data Validation', + 'Data Transformation', + 'Array Operations', + 'String Processing', + 'Math & Calculations', + 'Conditionals & Logic', + 'User Management', + 'Error Handling', + 'API & Networking', + 'Date & Time', + 'File Operations', + 'Utilities' +] as const + +export const LUA_SNIPPETS: LuaSnippet[] = [ + { + id: 'validate_email', + name: 'Email Validation', + description: 'Validate email format using pattern matching', + category: 'Data Validation', + tags: ['validation', 'email', 'regex'], + parameters: [ + { name: 'email', type: 'string', description: 'Email address to validate' } + ], + code: `local email = context.data.email or "" + +if email == "" then + return { valid = false, error = "Email is required" } +end + +local pattern = "^[%w._%%-]+@[%w._%%-]+%.%w+$" +if not string.match(email, pattern) then + return { valid = false, error = "Invalid email format" } +end + +return { valid = true, email = email }` + }, + { + id: 'validate_password_strength', + name: 'Password Strength Validator', + description: 'Check password meets security requirements', + category: 'Data Validation', + tags: ['validation', 'password', 'security'], + parameters: [ + { name: 'password', type: 'string', description: 'Password to validate' } + ], + code: `local password = context.data.password or "" + +if string.len(password) < 8 then + return { valid = false, error = "Password must be at least 8 characters" } +end + +local hasUpper = string.match(password, "%u") ~= nil +local hasLower = string.match(password, "%l") ~= nil +local hasDigit = string.match(password, "%d") ~= nil +local hasSpecial = string.match(password, "[^%w]") ~= nil + +if not hasUpper then + return { valid = false, error = "Password must contain uppercase letter" } +end + +if not hasLower then + return { valid = false, error = "Password must contain lowercase letter" } +end + +if not hasDigit then + return { valid = false, error = "Password must contain a number" } +end + +if not hasSpecial then + return { valid = false, error = "Password must contain special character" } +end + +return { + valid = true, + strength = "strong", + score = 100 +}` + }, + { + id: 'validate_phone', + name: 'Phone Number Validation', + description: 'Validate US phone number format', + category: 'Data Validation', + tags: ['validation', 'phone', 'format'], + parameters: [ + { name: 'phone', type: 'string', description: 'Phone number to validate' } + ], + code: `local phone = context.data.phone or "" + +local cleaned = string.gsub(phone, "[^%d]", "") + +if string.len(cleaned) ~= 10 then + return { valid = false, error = "Phone must be 10 digits" } +end + +local formatted = string.format("(%s) %s-%s", + string.sub(cleaned, 1, 3), + string.sub(cleaned, 4, 6), + string.sub(cleaned, 7, 10) +) + +return { + valid = true, + cleaned = cleaned, + formatted = formatted +}` + }, + { + id: 'validate_required_fields', + name: 'Required Fields Validator', + description: 'Check multiple required fields are present', + category: 'Data Validation', + tags: ['validation', 'required', 'form'], + code: `local data = context.data or {} +local required = {"name", "email", "username"} +local missing = {} + +for i, field in ipairs(required) do + if not data[field] or data[field] == "" then + table.insert(missing, field) + end +end + +if #missing > 0 then + return { + valid = false, + error = "Missing required fields: " .. table.concat(missing, ", "), + missing = missing + } +end + +return { valid = true }` + }, + { + id: 'transform_snake_to_camel', + name: 'Snake Case to Camel Case', + description: 'Convert snake_case strings to camelCase', + category: 'Data Transformation', + tags: ['transform', 'string', 'case'], + parameters: [ + { name: 'text', type: 'string', description: 'Snake case text' } + ], + code: `local text = context.data.text or "" + +local result = string.gsub(text, "_(%w)", function(c) + return string.upper(c) +end) + +return { + original = text, + transformed = result +}` + }, + { + id: 'transform_flatten_object', + name: 'Flatten Nested Object', + description: 'Convert nested table to flat key-value pairs', + category: 'Data Transformation', + tags: ['transform', 'object', 'flatten'], + code: `local function flatten(tbl, prefix, result) + result = result or {} + prefix = prefix or "" + + for key, value in pairs(tbl) do + local newKey = prefix == "" and key or prefix .. "." .. key + + if type(value) == "table" then + flatten(value, newKey, result) + else + result[newKey] = value + end + end + + return result +end + +local data = context.data or {} +local flattened = flatten(data) + +return { + original = data, + flattened = flattened +}` + }, + { + id: 'transform_normalize_data', + name: 'Normalize User Data', + description: 'Clean and normalize user input data', + category: 'Data Transformation', + tags: ['transform', 'normalize', 'clean'], + code: `local data = context.data or {} + +local function trim(s) + return string.match(s, "^%s*(.-)%s*$") +end + +local normalized = {} + +if data.email then + normalized.email = string.lower(trim(data.email)) +end + +if data.name then + normalized.name = trim(data.name) + local words = {} + for word in string.gmatch(normalized.name, "%S+") do + table.insert(words, string.upper(string.sub(word, 1, 1)) .. string.lower(string.sub(word, 2))) + end + normalized.name = table.concat(words, " ") +end + +if data.phone then + normalized.phone = string.gsub(data.phone, "[^%d]", "") +end + +return normalized` + }, + { + id: 'array_filter', + name: 'Filter Array', + description: 'Filter array elements by condition', + category: 'Array Operations', + tags: ['array', 'filter', 'collection'], + parameters: [ + { name: 'items', type: 'array', description: 'Array to filter' }, + { name: 'minValue', type: 'number', description: 'Minimum value threshold' } + ], + code: `local items = context.data.items or {} +local minValue = context.data.minValue or 0 +local filtered = {} + +for i, item in ipairs(items) do + if item.value >= minValue then + table.insert(filtered, item) + end +end + +log("Filtered " .. #filtered .. " of " .. #items .. " items") + +return { + original = items, + filtered = filtered, + count = #filtered +}` + }, + { + id: 'array_map', + name: 'Map Array', + description: 'Transform each array element', + category: 'Array Operations', + tags: ['array', 'map', 'transform'], + code: `local items = context.data.items or {} +local mapped = {} + +for i, item in ipairs(items) do + table.insert(mapped, { + id = item.id, + label = string.upper(item.name or ""), + value = (item.value or 0) * 2, + index = i + }) +end + +return { + original = items, + mapped = mapped +}` + }, + { + id: 'array_reduce', + name: 'Reduce Array to Sum', + description: 'Calculate sum of numeric array values', + category: 'Array Operations', + tags: ['array', 'reduce', 'sum'], + parameters: [ + { name: 'numbers', type: 'array', description: 'Array of numbers' } + ], + code: `local numbers = context.data.numbers or {} +local sum = 0 +local count = 0 + +for i, num in ipairs(numbers) do + sum = sum + (num or 0) + count = count + 1 +end + +local average = count > 0 and sum / count or 0 + +return { + sum = sum, + count = count, + average = average +}` + }, + { + id: 'array_group_by', + name: 'Group Array by Property', + description: 'Group array items by a property value', + category: 'Array Operations', + tags: ['array', 'group', 'aggregate'], + code: `local items = context.data.items or {} +local groupKey = context.data.groupKey or "category" +local groups = {} + +for i, item in ipairs(items) do + local key = item[groupKey] or "uncategorized" + + if not groups[key] then + groups[key] = {} + end + + table.insert(groups[key], item) +end + +local summary = {} +for key, group in pairs(groups) do + summary[key] = #group +end + +return { + groups = groups, + summary = summary +}` + }, + { + id: 'array_sort', + name: 'Sort Array', + description: 'Sort array by property value', + category: 'Array Operations', + tags: ['array', 'sort', 'order'], + code: `local items = context.data.items or {} +local sortKey = context.data.sortKey or "value" +local descending = context.data.descending or false + +table.sort(items, function(a, b) + if descending then + return (a[sortKey] or 0) > (b[sortKey] or 0) + else + return (a[sortKey] or 0) < (b[sortKey] or 0) + end +end) + +return { + sorted = items, + count = #items +}` + }, + { + id: 'string_slugify', + name: 'Create URL Slug', + description: 'Convert text to URL-friendly slug', + category: 'String Processing', + tags: ['string', 'slug', 'url'], + parameters: [ + { name: 'text', type: 'string', description: 'Text to slugify' } + ], + code: `local text = context.data.text or "" + +local slug = string.lower(text) +slug = string.gsub(slug, "%s+", "-") +slug = string.gsub(slug, "[^%w%-]", "") +slug = string.gsub(slug, "%-+", "-") +slug = string.gsub(slug, "^%-+", "") +slug = string.gsub(slug, "%-+$", "") + +return { + original = text, + slug = slug +}` + }, + { + id: 'string_truncate', + name: 'Truncate Text', + description: 'Truncate long text with ellipsis', + category: 'String Processing', + tags: ['string', 'truncate', 'ellipsis'], + parameters: [ + { name: 'text', type: 'string', description: 'Text to truncate' }, + { name: 'maxLength', type: 'number', description: 'Maximum length' } + ], + code: `local text = context.data.text or "" +local maxLength = context.data.maxLength or 50 + +if string.len(text) <= maxLength then + return { + truncated = false, + text = text + } +end + +local truncated = string.sub(text, 1, maxLength - 3) .. "..." + +return { + truncated = true, + text = truncated, + originalLength = string.len(text) +}` + }, + { + id: 'string_extract_hashtags', + name: 'Extract Hashtags', + description: 'Find all hashtags in text', + category: 'String Processing', + tags: ['string', 'parse', 'hashtags'], + parameters: [ + { name: 'text', type: 'string', description: 'Text containing hashtags' } + ], + code: `local text = context.data.text or "" +local hashtags = {} + +for tag in string.gmatch(text, "#(%w+)") do + table.insert(hashtags, tag) +end + +return { + text = text, + hashtags = hashtags, + count = #hashtags +}` + }, + { + id: 'string_word_count', + name: 'Word Counter', + description: 'Count words and characters in text', + category: 'String Processing', + tags: ['string', 'count', 'statistics'], + parameters: [ + { name: 'text', type: 'string', description: 'Text to analyze' } + ], + code: `local text = context.data.text or "" + +local charCount = string.len(text) +local words = {} + +for word in string.gmatch(text, "%S+") do + table.insert(words, word) +end + +local wordCount = #words + +local sentences = 0 +for _ in string.gmatch(text, "[.!?]+") do + sentences = sentences + 1 +end + +return { + characters = charCount, + words = wordCount, + sentences = sentences, + avgWordLength = wordCount > 0 and charCount / wordCount or 0 +}` + }, + { + id: 'math_percentage', + name: 'Calculate Percentage', + description: 'Calculate percentage and format result', + category: 'Math & Calculations', + tags: ['math', 'percentage', 'calculation'], + parameters: [ + { name: 'value', type: 'number', description: 'Partial value' }, + { name: 'total', type: 'number', description: 'Total value' } + ], + code: `local value = context.data.value or 0 +local total = context.data.total or 1 + +if total == 0 then + return { + error = "Cannot divide by zero", + percentage = 0 + } +end + +local percentage = (value / total) * 100 +local formatted = string.format("%.2f%%", percentage) + +return { + value = value, + total = total, + percentage = percentage, + formatted = formatted +}` + }, + { + id: 'math_discount', + name: 'Calculate Discount', + description: 'Calculate price after discount', + category: 'Math & Calculations', + tags: ['math', 'discount', 'price'], + parameters: [ + { name: 'price', type: 'number', description: 'Original price' }, + { name: 'discount', type: 'number', description: 'Discount percentage' } + ], + code: `local price = context.data.price or 0 +local discount = context.data.discount or 0 + +local discountAmount = price * (discount / 100) +local finalPrice = price - discountAmount +local savings = discountAmount + +return { + originalPrice = price, + discountPercent = discount, + discountAmount = discountAmount, + finalPrice = finalPrice, + savings = savings, + formatted = "$" .. string.format("%.2f", finalPrice) +}` + }, + { + id: 'math_compound_interest', + name: 'Compound Interest Calculator', + description: 'Calculate compound interest over time', + category: 'Math & Calculations', + tags: ['math', 'interest', 'finance'], + parameters: [ + { name: 'principal', type: 'number', description: 'Initial amount' }, + { name: 'rate', type: 'number', description: 'Interest rate (%)' }, + { name: 'years', type: 'number', description: 'Number of years' } + ], + code: `local principal = context.data.principal or 1000 +local rate = (context.data.rate or 5) / 100 +local years = context.data.years or 1 +local compounds = 12 + +local amount = principal * math.pow(1 + (rate / compounds), compounds * years) +local interest = amount - principal + +return { + principal = principal, + rate = rate * 100, + years = years, + finalAmount = amount, + interestEarned = interest, + formatted = "$" .. string.format("%.2f", amount) +}` + }, + { + id: 'math_statistics', + name: 'Statistical Analysis', + description: 'Calculate mean, median, mode, std dev', + category: 'Math & Calculations', + tags: ['math', 'statistics', 'analysis'], + parameters: [ + { name: 'numbers', type: 'array', description: 'Array of numbers' } + ], + code: `local numbers = context.data.numbers or {1, 2, 3, 4, 5} + +local sum = 0 +local min = numbers[1] +local max = numbers[1] + +for i, num in ipairs(numbers) do + sum = sum + num + if num < min then min = num end + if num > max then max = num end +end + +local mean = sum / #numbers + +table.sort(numbers) +local median +if #numbers % 2 == 0 then + median = (numbers[#numbers/2] + numbers[#numbers/2 + 1]) / 2 +else + median = numbers[math.ceil(#numbers/2)] +end + +local variance = 0 +for i, num in ipairs(numbers) do + variance = variance + math.pow(num - mean, 2) +end +variance = variance / #numbers + +local stdDev = math.sqrt(variance) + +return { + count = #numbers, + sum = sum, + mean = mean, + median = median, + min = min, + max = max, + variance = variance, + stdDev = stdDev, + range = max - min +}` + }, + { + id: 'conditional_role_check', + name: 'Role-Based Access Check', + description: 'Check if user has required role', + category: 'Conditionals & Logic', + tags: ['conditional', 'role', 'access'], + parameters: [ + { name: 'requiredRole', type: 'string', description: 'Required role level' } + ], + code: `local user = context.user or {} +local requiredRole = context.data.requiredRole or "user" + +local roles = { + user = 1, + moderator = 2, + admin = 3, + god = 4 +} + +local userLevel = roles[user.role] or 0 +local requiredLevel = roles[requiredRole] or 0 + +local hasAccess = userLevel >= requiredLevel + +return { + user = user.username, + userRole = user.role, + requiredRole = requiredRole, + hasAccess = hasAccess, + message = hasAccess and "Access granted" or "Access denied" +}` + }, + { + id: 'conditional_time_based', + name: 'Time-Based Logic', + description: 'Execute logic based on time of day', + category: 'Conditionals & Logic', + tags: ['conditional', 'time', 'schedule'], + code: `local hour = tonumber(os.date("%H")) +local timeOfDay = "" +local greeting = "" + +if hour >= 5 and hour < 12 then + timeOfDay = "morning" + greeting = "Good morning" +elseif hour >= 12 and hour < 17 then + timeOfDay = "afternoon" + greeting = "Good afternoon" +elseif hour >= 17 and hour < 21 then + timeOfDay = "evening" + greeting = "Good evening" +else + timeOfDay = "night" + greeting = "Good night" +end + +local isBusinessHours = hour >= 9 and hour < 17 + +return { + currentHour = hour, + timeOfDay = timeOfDay, + greeting = greeting, + isBusinessHours = isBusinessHours +}` + }, + { + id: 'conditional_feature_flag', + name: 'Feature Flag Checker', + description: 'Check if feature is enabled for user', + category: 'Conditionals & Logic', + tags: ['conditional', 'feature', 'flag'], + code: `local user = context.user or {} +local feature = context.data.feature or "" + +local enabledFeatures = { + betaUI = {"admin", "god"}, + advancedSearch = {"moderator", "admin", "god"}, + exportData = {"admin", "god"}, + debugMode = {"god"} +} + +local allowedRoles = enabledFeatures[feature] or {} +local isEnabled = false + +for i, role in ipairs(allowedRoles) do + if user.role == role then + isEnabled = true + break + end +end + +return { + feature = feature, + userRole = user.role, + enabled = isEnabled, + reason = isEnabled and "Feature available" or "Feature not available for your role" +}` + }, + { + id: 'error_try_catch', + name: 'Try-Catch Pattern', + description: 'Safe execution with error handling', + category: 'Error Handling', + tags: ['error', 'exception', 'safety'], + code: `local function riskyOperation() + local data = context.data or {} + + if not data.value then + error("Value is required") + end + + if data.value < 0 then + error("Value must be positive") + end + + return data.value * 2 +end + +local success, result = pcall(riskyOperation) + +if success then + log("Operation successful: " .. tostring(result)) + return { + success = true, + result = result + } +else + log("Operation failed: " .. tostring(result)) + return { + success = false, + error = tostring(result) + } +end` + }, + { + id: 'error_validation_accumulator', + name: 'Validation Error Accumulator', + description: 'Collect all validation errors at once', + category: 'Error Handling', + tags: ['error', 'validation', 'accumulator'], + code: `local data = context.data or {} +local errors = {} + +if not data.name or data.name == "" then + table.insert(errors, "Name is required") +end + +if not data.email or data.email == "" then + table.insert(errors, "Email is required") +elseif not string.match(data.email, "@") then + table.insert(errors, "Email format is invalid") +end + +if not data.age then + table.insert(errors, "Age is required") +elseif data.age < 18 then + table.insert(errors, "Must be 18 or older") +end + +if #errors > 0 then + return { + valid = false, + errors = errors, + count = #errors + } +end + +return { + valid = true, + data = data +}` + }, + { + id: 'user_profile_builder', + name: 'Build User Profile', + description: 'Create complete user profile from data', + category: 'User Management', + tags: ['user', 'profile', 'builder'], + code: `local data = context.data or {} + +local profile = { + id = "user_" .. os.time(), + username = data.username or "", + email = string.lower(data.email or ""), + displayName = data.displayName or data.username or "", + role = "user", + status = "active", + createdAt = os.time(), + metadata = { + source = "registration", + version = "1.0" + }, + preferences = { + theme = "light", + notifications = true, + language = "en" + } +} + +log("Created profile for: " .. profile.username) + +return profile` + }, + { + id: 'user_activity_logger', + name: 'Log User Activity', + description: 'Create activity log entry', + category: 'User Management', + tags: ['user', 'activity', 'logging'], + code: `local user = context.user or {} +local action = context.data.action or "unknown" +local details = context.data.details or {} + +local activity = { + id = "activity_" .. os.time(), + userId = user.id or "unknown", + username = user.username or "anonymous", + action = action, + details = details, + timestamp = os.time(), + date = os.date("%Y-%m-%d %H:%M:%S"), + ipAddress = "0.0.0.0", + userAgent = "MetaBuilder/1.0" +} + +log("Activity logged: " .. user.username .. " - " .. action) + +return activity` + }, + { + id: 'date_format', + name: 'Format Date', + description: 'Format timestamp in various ways', + category: 'Date & Time', + tags: ['date', 'time', 'format'], + parameters: [ + { name: 'timestamp', type: 'number', description: 'Unix timestamp' } + ], + code: `local timestamp = context.data.timestamp or os.time() + +local formatted = { + full = os.date("%Y-%m-%d %H:%M:%S", timestamp), + date = os.date("%Y-%m-%d", timestamp), + time = os.date("%H:%M:%S", timestamp), + readable = os.date("%B %d, %Y at %I:%M %p", timestamp), + iso = os.date("%Y-%m-%dT%H:%M:%S", timestamp), + unix = timestamp +} + +return formatted` + }, + { + id: 'date_diff', + name: 'Calculate Date Difference', + description: 'Calculate difference between two dates', + category: 'Date & Time', + tags: ['date', 'time', 'difference'], + parameters: [ + { name: 'startTime', type: 'number', description: 'Start timestamp' }, + { name: 'endTime', type: 'number', description: 'End timestamp' } + ], + code: `local startTime = context.data.startTime or os.time() +local endTime = context.data.endTime or os.time() + +local diffSeconds = math.abs(endTime - startTime) +local diffMinutes = math.floor(diffSeconds / 60) +local diffHours = math.floor(diffMinutes / 60) +local diffDays = math.floor(diffHours / 24) + +return { + startTime = startTime, + endTime = endTime, + difference = { + seconds = diffSeconds, + minutes = diffMinutes, + hours = diffHours, + days = diffDays + }, + formatted = diffDays .. " days, " .. (diffHours % 24) .. " hours" +}` + }, + { + id: 'json_parse', + name: 'Safe JSON Parse', + description: 'Parse JSON string with error handling', + category: 'Utilities', + tags: ['json', 'parse', 'utility'], + parameters: [ + { name: 'jsonString', type: 'string', description: 'JSON string to parse' } + ], + code: `local jsonString = context.data.jsonString or "{}" + +local function parseJSON(str) + local result = {} + return result +end + +local success, data = pcall(parseJSON, jsonString) + +if success then + return { + success = true, + data = data + } +else + return { + success = false, + error = "Invalid JSON format" + } +end` + }, + { + id: 'generate_id', + name: 'Generate Unique ID', + description: 'Create unique identifier', + category: 'Utilities', + tags: ['id', 'uuid', 'generator'], + code: `local function generateId(prefix) + local timestamp = os.time() + local random = math.random(1000, 9999) + return (prefix or "id") .. "_" .. timestamp .. "_" .. random +end + +local id = generateId(context.data.prefix) + +return { + id = id, + timestamp = os.time() +}` + }, + { + id: 'rate_limiter', + name: 'Rate Limit Checker', + description: 'Check if action exceeds rate limit', + category: 'Utilities', + tags: ['rate', 'limit', 'throttle'], + code: `local user = context.user or {} +local action = context.data.action or "default" +local maxAttempts = context.data.maxAttempts or 5 +local windowSeconds = context.data.windowSeconds or 60 + +local currentTime = os.time() +local attempts = 1 + +local allowed = attempts <= maxAttempts + +return { + allowed = allowed, + attempts = attempts, + maxAttempts = maxAttempts, + remaining = maxAttempts - attempts, + resetTime = currentTime + windowSeconds, + message = allowed and "Request allowed" or "Rate limit exceeded" +}` + }, + { + id: 'cache_manager', + name: 'Simple Cache Manager', + description: 'Cache data with expiration', + category: 'Utilities', + tags: ['cache', 'storage', 'ttl'], + code: `local key = context.data.key or "cache_key" +local value = context.data.value +local ttl = context.data.ttl or 300 + +local cached = { + key = key, + value = value, + cachedAt = os.time(), + expiresAt = os.time() + ttl, + ttl = ttl +} + +log("Cached " .. key .. " for " .. ttl .. " seconds") + +return cached` + } +] + +// Functions moved to ../functions/ directory +// Use LuaSnippetUtils class or import individual functions \ No newline at end of file diff --git a/frontends/nextjs/src/lib/security/functions/helpers/get-severity-color.ts b/frontends/nextjs/src/lib/security/functions/helpers/get-severity-color.ts new file mode 100644 index 000000000..a8eca3f19 --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/helpers/get-severity-color.ts @@ -0,0 +1,24 @@ +/** + * Get Severity Color + * Returns appropriate color classes for a severity level + */ + +/** + * Get color classes for displaying severity + * @param severity - The severity level + * @returns MUI-compatible color string + */ +export const getSeverityColor = (severity: string): string => { + switch (severity) { + case 'critical': + return 'error' + case 'high': + return 'warning' + case 'medium': + return 'info' + case 'low': + return 'secondary' + default: + return 'success' + } +} diff --git a/frontends/nextjs/src/lib/security/functions/helpers/get-severity-icon.ts b/frontends/nextjs/src/lib/security/functions/helpers/get-severity-icon.ts new file mode 100644 index 000000000..01508d445 --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/helpers/get-severity-icon.ts @@ -0,0 +1,24 @@ +/** + * Get Severity Icon + * Returns appropriate emoji icon for a severity level + */ + +/** + * Get emoji icon for displaying severity + * @param severity - The severity level + * @returns Emoji representing the severity + */ +export const getSeverityIcon = (severity: string): string => { + switch (severity) { + case 'critical': + return '🚨' + case 'high': + return '⚠️' + case 'medium': + return '⚡' + case 'low': + return 'ℹ️' + default: + return '✓' + } +} diff --git a/frontends/nextjs/src/lib/security/functions/helpers/index.ts b/frontends/nextjs/src/lib/security/functions/helpers/index.ts new file mode 100644 index 000000000..c9a202df8 --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/helpers/index.ts @@ -0,0 +1,7 @@ +/** + * Helpers Index + * Exports all helper functions + */ + +export { getSeverityColor } from './get-severity-color' +export { getSeverityIcon } from './get-severity-icon' diff --git a/frontends/nextjs/src/lib/security/functions/index.ts b/frontends/nextjs/src/lib/security/functions/index.ts new file mode 100644 index 000000000..0e0c2e380 --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/index.ts @@ -0,0 +1,27 @@ +/** + * Security Functions Index + * Exports all security-related types and functions + */ + +// Types +export type { SecurityScanResult, SecurityIssue, SecurityPattern } from './types' + +// Patterns +export { JAVASCRIPT_PATTERNS } from './patterns/javascript-patterns' +export { LUA_PATTERNS } from './patterns/lua-patterns' +export { SQL_INJECTION_PATTERNS } from './patterns/sql-patterns' + +// Scanners +export { scanJavaScript } from './scanners/scan-javascript' +export { scanLua } from './scanners/scan-lua' +export { scanJSON } from './scanners/scan-json' +export { scanHTML } from './scanners/scan-html' +export { sanitizeInput } from './scanners/sanitize-input' + +// Utils +export { getLineNumber } from './utils/get-line-number' +export { calculateOverallSeverity } from './utils/calculate-severity' + +// Helpers +export { getSeverityColor } from './helpers/get-severity-color' +export { getSeverityIcon } from './helpers/get-severity-icon' diff --git a/frontends/nextjs/src/lib/security/functions/patterns/index.ts b/frontends/nextjs/src/lib/security/functions/patterns/index.ts new file mode 100644 index 000000000..34c2925b2 --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/patterns/index.ts @@ -0,0 +1,8 @@ +/** + * Security Patterns Index + * Exports all security pattern collections + */ + +export { JAVASCRIPT_PATTERNS } from './javascript-patterns' +export { LUA_PATTERNS } from './lua-patterns' +export { SQL_INJECTION_PATTERNS } from './sql-patterns' diff --git a/frontends/nextjs/src/lib/security/functions/patterns/javascript-patterns.ts b/frontends/nextjs/src/lib/security/functions/patterns/javascript-patterns.ts new file mode 100644 index 000000000..aa8214905 --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/patterns/javascript-patterns.ts @@ -0,0 +1,184 @@ +/** + * JavaScript Security Patterns + * Patterns for detecting malicious JavaScript code + */ + +import type { SecurityPattern } from '../types' + +export const JAVASCRIPT_PATTERNS: SecurityPattern[] = [ + { + pattern: /eval\s*\(/gi, + type: 'dangerous', + severity: 'critical', + message: 'Use of eval() detected - can execute arbitrary code', + recommendation: 'Use safe alternatives like JSON.parse() or Function constructor with strict validation' + }, + { + pattern: /Function\s*\(/gi, + type: 'dangerous', + severity: 'high', + message: 'Dynamic Function constructor detected', + recommendation: 'Avoid dynamic code generation or use with extreme caution' + }, + { + pattern: /innerHTML\s*=/gi, + type: 'dangerous', + severity: 'high', + message: 'innerHTML assignment detected - XSS vulnerability risk', + recommendation: 'Use textContent, createElement, or React JSX instead' + }, + { + pattern: /dangerouslySetInnerHTML/gi, + type: 'dangerous', + severity: 'high', + message: 'dangerouslySetInnerHTML detected - XSS vulnerability risk', + recommendation: 'Sanitize HTML content or use safe alternatives' + }, + { + pattern: /document\.write\s*\(/gi, + type: 'dangerous', + severity: 'medium', + message: 'document.write() detected - can cause security issues', + recommendation: 'Use DOM manipulation methods instead' + }, + { + pattern: /\.call\s*\(\s*window/gi, + type: 'suspicious', + severity: 'medium', + message: 'Calling functions with window context', + recommendation: 'Be careful with context manipulation' + }, + { + pattern: /\.apply\s*\(\s*window/gi, + type: 'suspicious', + severity: 'medium', + message: 'Applying functions with window context', + recommendation: 'Be careful with context manipulation' + }, + { + pattern: /__proto__/gi, + type: 'dangerous', + severity: 'critical', + message: 'Prototype pollution attempt detected', + recommendation: 'Never manipulate __proto__ directly' + }, + { + pattern: /constructor\s*\[\s*['"]prototype['"]\s*\]/gi, + type: 'dangerous', + severity: 'critical', + message: 'Prototype manipulation detected', + recommendation: 'Use Object.create() or proper class syntax' + }, + { + pattern: /import\s+.*\s+from\s+['"]https?:/gi, + type: 'dangerous', + severity: 'critical', + message: 'Remote code import detected', + recommendation: 'Only import from trusted, local sources' + }, + { + pattern: /]*>/gi, + type: 'dangerous', + severity: 'critical', + message: 'Script tag injection detected', + recommendation: 'Never inject script tags dynamically' + }, + { + pattern: /on(click|load|error|mouseover|mouseout|focus|blur)\s*=/gi, + type: 'suspicious', + severity: 'medium', + message: 'Inline event handler detected', + recommendation: 'Use addEventListener or React event handlers' + }, + { + pattern: /javascript:\s*/gi, + type: 'dangerous', + severity: 'high', + message: 'javascript: protocol detected', + recommendation: 'Never use javascript: protocol in URLs' + }, + { + pattern: /data:\s*text\/html/gi, + type: 'dangerous', + severity: 'high', + message: 'Data URI with HTML detected', + recommendation: 'Avoid data URIs with executable content' + }, + { + pattern: /setTimeout\s*\(\s*['"`]/gi, + type: 'dangerous', + severity: 'high', + message: 'setTimeout with string argument detected', + recommendation: 'Use setTimeout with function reference instead' + }, + { + pattern: /setInterval\s*\(\s*['"`]/gi, + type: 'dangerous', + severity: 'high', + message: 'setInterval with string argument detected', + recommendation: 'Use setInterval with function reference instead' + }, + { + pattern: /localStorage|sessionStorage/gi, + type: 'warning', + severity: 'low', + message: 'Local/session storage usage detected', + recommendation: 'Use useKV hook for persistent data instead' + }, + { + pattern: /crypto\.subtle|atob|btoa/gi, + type: 'warning', + severity: 'low', + message: 'Cryptographic operation detected', + recommendation: 'Ensure proper key management and secure practices' + }, + { + pattern: /XMLHttpRequest|fetch\s*\(\s*['"`]http/gi, + type: 'warning', + severity: 'medium', + message: 'External HTTP request detected', + recommendation: 'Ensure CORS and security headers are properly configured' + }, + { + pattern: /window\.open/gi, + type: 'suspicious', + severity: 'medium', + message: 'window.open detected', + recommendation: 'Be cautious with popup windows' + }, + { + pattern: /location\.href\s*=/gi, + type: 'suspicious', + severity: 'medium', + message: 'Direct location manipulation detected', + recommendation: 'Use React Router or validate URLs carefully' + }, + { + pattern: /require\s*\(\s*[^'"`]/gi, + type: 'dangerous', + severity: 'high', + message: 'Dynamic require() detected', + recommendation: 'Use static imports only' + }, + { + pattern: /\.exec\s*\(|child_process|spawn|fork|execFile/gi, + type: 'malicious', + severity: 'critical', + message: 'System command execution attempt detected', + recommendation: 'This is not allowed in browser environment' + }, + { + pattern: /fs\.|path\.|os\./gi, + type: 'malicious', + severity: 'critical', + message: 'Node.js system module usage detected', + recommendation: 'File system access not allowed in browser' + }, + { + pattern: /process\.env|process\.exit/gi, + type: 'suspicious', + severity: 'medium', + message: 'Process manipulation detected', + recommendation: 'Not applicable in browser environment' + } +] diff --git a/frontends/nextjs/src/lib/security/functions/patterns/lua-patterns.ts b/frontends/nextjs/src/lib/security/functions/patterns/lua-patterns.ts new file mode 100644 index 000000000..41d71a44e --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/patterns/lua-patterns.ts @@ -0,0 +1,86 @@ +/** + * Lua Security Patterns + * Patterns for detecting malicious Lua code + */ + +import type { SecurityPattern } from '../types' + +export const LUA_PATTERNS: SecurityPattern[] = [ + { + pattern: /os\.(execute|exit|remove|rename|tmpname)/gi, + type: 'malicious', + severity: 'critical', + message: 'Lua OS module system call detected', + recommendation: 'OS module access is disabled for security' + }, + { + pattern: /io\.(popen|tmpfile|open|input|output|lines)/gi, + type: 'malicious', + severity: 'critical', + message: 'Lua file I/O operation detected', + recommendation: 'File system access is disabled for security' + }, + { + pattern: /loadfile|dofile/gi, + type: 'dangerous', + severity: 'critical', + message: 'Lua file loading function detected', + recommendation: 'File loading is disabled for security' + }, + { + pattern: /package\.(loadlib|searchpath|cpath)/gi, + type: 'dangerous', + severity: 'critical', + message: 'Lua dynamic library loading detected', + recommendation: 'Dynamic library loading is disabled' + }, + { + pattern: /debug\.(getinfo|setmetatable|getfenv|setfenv)/gi, + type: 'dangerous', + severity: 'high', + message: 'Lua debug module advanced features detected', + recommendation: 'Limited debug functionality available' + }, + { + pattern: /loadstring\s*\(/gi, + type: 'dangerous', + severity: 'high', + message: 'Lua dynamic code execution detected', + recommendation: 'Use with extreme caution' + }, + { + pattern: /\.\.\s*[[\]]/gi, + type: 'suspicious', + severity: 'medium', + message: 'Potential Lua table manipulation', + recommendation: 'Ensure proper validation' + }, + { + pattern: /_G\s*\[/gi, + type: 'suspicious', + severity: 'high', + message: 'Global environment manipulation detected', + recommendation: 'Avoid modifying global environment' + }, + { + pattern: /getmetatable|setmetatable/gi, + type: 'suspicious', + severity: 'medium', + message: 'Metatable manipulation detected', + recommendation: 'Use carefully to avoid security issues' + }, + { + pattern: /while\s+true\s+do/gi, + type: 'warning', + severity: 'medium', + message: 'Infinite loop detected', + recommendation: 'Ensure proper break conditions exist' + }, + { + pattern: /function\s+(\w+)\s*\([^)]*\)\s*\{[^}]*\1\s*\(/gi, + type: 'warning', + severity: 'low', + message: 'Potential recursive function', + recommendation: 'Ensure recursion has proper termination' + } +] diff --git a/frontends/nextjs/src/lib/security/functions/patterns/sql-patterns.ts b/frontends/nextjs/src/lib/security/functions/patterns/sql-patterns.ts new file mode 100644 index 000000000..a127f5d03 --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/patterns/sql-patterns.ts @@ -0,0 +1,37 @@ +/** + * SQL Injection Patterns + * Patterns for detecting SQL injection attempts + */ + +import type { SecurityPattern } from '../types' + +export const SQL_INJECTION_PATTERNS: SecurityPattern[] = [ + { + pattern: /;\s*(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE)\s+/gi, + type: 'malicious', + severity: 'critical', + message: 'SQL injection attempt detected', + recommendation: 'Use parameterized queries' + }, + { + pattern: /UNION\s+SELECT/gi, + type: 'malicious', + severity: 'critical', + message: 'SQL UNION injection attempt', + recommendation: 'Use parameterized queries' + }, + { + pattern: /'[\s]*OR[\s]*'1'[\s]*=[\s]*'1/gi, + type: 'malicious', + severity: 'critical', + message: 'SQL authentication bypass attempt', + recommendation: 'Never concatenate user input into SQL' + }, + { + pattern: /--[\s]*$/gm, + type: 'suspicious', + severity: 'high', + message: 'SQL comment pattern detected', + recommendation: 'May indicate SQL injection attempt' + } +] diff --git a/frontends/nextjs/src/lib/security/functions/scanners/index.ts b/frontends/nextjs/src/lib/security/functions/scanners/index.ts new file mode 100644 index 000000000..e8682001b --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/scanners/index.ts @@ -0,0 +1,10 @@ +/** + * Scanners Index + * Exports all scanner functions + */ + +export { scanJavaScript } from './scan-javascript' +export { scanLua } from './scan-lua' +export { scanJSON } from './scan-json' +export { scanHTML } from './scan-html' +export { sanitizeInput } from './sanitize-input' diff --git a/frontends/nextjs/src/lib/security/functions/scanners/sanitize-input.ts b/frontends/nextjs/src/lib/security/functions/scanners/sanitize-input.ts new file mode 100644 index 000000000..a969ad41c --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/scanners/sanitize-input.ts @@ -0,0 +1,37 @@ +/** + * Sanitize Input + * Removes dangerous patterns from input based on type + */ + +/** + * Sanitize input by removing dangerous patterns + * @param input - The input string to sanitize + * @param type - The type of content being sanitized + * @returns Sanitized string + */ +export const sanitizeInput = ( + input: string, + type: 'text' | 'html' | 'json' | 'javascript' | 'lua' = 'text' +): string => { + let sanitized = input + + if (type === 'text') { + sanitized = sanitized.replace(/]*>.*?<\/script>/gis, '') + sanitized = sanitized.replace(/on\w+\s*=/gi, '') + sanitized = sanitized.replace(/javascript:/gi, '') + } + + if (type === 'html') { + sanitized = sanitized.replace(/]*>.*?<\/script>/gis, '') + sanitized = sanitized.replace(/on\w+\s*=/gi, '') + sanitized = sanitized.replace(/javascript:/gi, '') + sanitized = sanitized.replace(/data:\s*text\/html/gi, '') + } + + if (type === 'json') { + sanitized = sanitized.replace(/__proto__/gi, '_proto_') + sanitized = sanitized.replace(/constructor\s*\[\s*['"]prototype['"]\s*\]/gi, '') + } + + return sanitized +} diff --git a/frontends/nextjs/src/lib/security/functions/scanners/scan-html.ts b/frontends/nextjs/src/lib/security/functions/scanners/scan-html.ts new file mode 100644 index 000000000..6402caf32 --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/scanners/scan-html.ts @@ -0,0 +1,78 @@ +/** + * Scan HTML + * Scans HTML for security vulnerabilities like XSS + */ + +import type { SecurityScanResult, SecurityIssue } from '../types' +import { calculateOverallSeverity } from '../utils/calculate-severity' + +/** + * Scan HTML string for security vulnerabilities + * @param html - The HTML string to scan + * @returns Security scan result with issues found + */ +export const scanHTML = (html: string): SecurityScanResult => { + const issues: SecurityIssue[] = [] + + // Check for script tags + const scriptTagPattern = /]*>.*?<\/script>/gis + const matches = html.matchAll(scriptTagPattern) + for (const match of matches) { + issues.push({ + type: 'dangerous', + severity: 'critical', + message: 'Script tag detected in HTML', + pattern: match[0].substring(0, 50) + '...', + recommendation: 'Remove script tags or use proper React components' + }) + } + + // Check for inline event handlers + const inlineEventPattern = /on(click|load|error|mouseover|mouseout|focus|blur|submit)\s*=/gi + const inlineMatches = html.matchAll(inlineEventPattern) + for (const match of inlineMatches) { + issues.push({ + type: 'dangerous', + severity: 'high', + message: 'Inline event handler in HTML', + pattern: match[0], + recommendation: 'Use React event handlers instead' + }) + } + + // Check for javascript: protocol + const javascriptProtocol = /href\s*=\s*['"]javascript:/gi + if (javascriptProtocol.test(html)) { + issues.push({ + type: 'dangerous', + severity: 'critical', + message: 'javascript: protocol in href', + pattern: 'javascript:', + recommendation: 'Use proper URLs or event handlers' + }) + } + + // Check for unsandboxed iframes + const iframePattern = /]*>/gi + const iframeMatches = html.matchAll(iframePattern) + for (const match of iframeMatches) { + if (!match[0].includes('sandbox=')) { + issues.push({ + type: 'suspicious', + severity: 'medium', + message: 'Iframe without sandbox attribute', + pattern: match[0], + recommendation: 'Add sandbox attribute to iframes for security' + }) + } + } + + const severity = calculateOverallSeverity(issues) + const safe = severity === 'safe' || severity === 'low' + + return { + safe, + severity, + issues + } +} diff --git a/frontends/nextjs/src/lib/security/functions/scanners/scan-javascript.ts b/frontends/nextjs/src/lib/security/functions/scanners/scan-javascript.ts new file mode 100644 index 000000000..14c2ca9cb --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/scanners/scan-javascript.ts @@ -0,0 +1,61 @@ +/** + * Scan JavaScript + * Scans JavaScript code for security vulnerabilities + */ + +import type { SecurityScanResult, SecurityIssue } from '../types' +import { JAVASCRIPT_PATTERNS } from '../patterns/javascript-patterns' +import { SQL_INJECTION_PATTERNS } from '../patterns/sql-patterns' +import { getLineNumber } from '../utils/get-line-number' +import { calculateOverallSeverity } from '../utils/calculate-severity' + +/** + * Scan JavaScript code for security vulnerabilities + * @param code - The JavaScript code to scan + * @returns Security scan result with issues found + */ +export const scanJavaScript = (code: string): SecurityScanResult => { + const issues: SecurityIssue[] = [] + + // Check JavaScript patterns + for (const pattern of JAVASCRIPT_PATTERNS) { + const matches = code.matchAll(new RegExp(pattern.pattern.source, pattern.pattern.flags)) + for (const match of matches) { + const lineNumber = getLineNumber(code, match.index || 0) + issues.push({ + type: pattern.type, + severity: pattern.severity, + message: pattern.message, + pattern: match[0], + line: lineNumber, + recommendation: pattern.recommendation + }) + } + } + + // Check SQL injection patterns + for (const pattern of SQL_INJECTION_PATTERNS) { + const matches = code.matchAll(new RegExp(pattern.pattern.source, pattern.pattern.flags)) + for (const match of matches) { + const lineNumber = getLineNumber(code, match.index || 0) + issues.push({ + type: pattern.type, + severity: pattern.severity, + message: pattern.message, + pattern: match[0], + line: lineNumber, + recommendation: pattern.recommendation + }) + } + } + + const severity = calculateOverallSeverity(issues) + const safe = severity === 'safe' || severity === 'low' + + return { + safe, + severity, + issues, + sanitizedCode: safe ? code : undefined + } +} diff --git a/frontends/nextjs/src/lib/security/functions/scanners/scan-json.ts b/frontends/nextjs/src/lib/security/functions/scanners/scan-json.ts new file mode 100644 index 000000000..3d95f6f92 --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/scanners/scan-json.ts @@ -0,0 +1,62 @@ +/** + * Scan JSON + * Scans JSON for security vulnerabilities like prototype pollution + */ + +import type { SecurityScanResult, SecurityIssue } from '../types' +import { calculateOverallSeverity } from '../utils/calculate-severity' + +/** + * Scan JSON string for security vulnerabilities + * @param jsonString - The JSON string to scan + * @returns Security scan result with issues found + */ +export const scanJSON = (jsonString: string): SecurityScanResult => { + const issues: SecurityIssue[] = [] + + // Validate JSON format + try { + JSON.parse(jsonString) + } catch (error) { + issues.push({ + type: 'warning', + severity: 'medium', + message: 'Invalid JSON format', + pattern: 'JSON parse error', + recommendation: 'Ensure JSON is properly formatted' + }) + } + + // Check for prototype pollution + const protoPollution = /__proto__|constructor\s*\[\s*['"]prototype['"]\s*\]/gi + if (protoPollution.test(jsonString)) { + issues.push({ + type: 'malicious', + severity: 'critical', + message: 'Prototype pollution attempt in JSON', + pattern: '__proto__', + recommendation: 'Remove prototype manipulation from JSON' + }) + } + + // Check for script tags + if (jsonString.includes('', + recommendation: 'Remove all HTML/script content from JSON' + }) + } + + const severity = calculateOverallSeverity(issues) + const safe = severity === 'safe' || severity === 'low' + + return { + safe, + severity, + issues, + sanitizedCode: safe ? jsonString : undefined + } +} diff --git a/frontends/nextjs/src/lib/security/functions/scanners/scan-lua.ts b/frontends/nextjs/src/lib/security/functions/scanners/scan-lua.ts new file mode 100644 index 000000000..4b92d001e --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/scanners/scan-lua.ts @@ -0,0 +1,43 @@ +/** + * Scan Lua + * Scans Lua code for security vulnerabilities + */ + +import type { SecurityScanResult, SecurityIssue } from '../types' +import { LUA_PATTERNS } from '../patterns/lua-patterns' +import { getLineNumber } from '../utils/get-line-number' +import { calculateOverallSeverity } from '../utils/calculate-severity' + +/** + * Scan Lua code for security vulnerabilities + * @param code - The Lua code to scan + * @returns Security scan result with issues found + */ +export const scanLua = (code: string): SecurityScanResult => { + const issues: SecurityIssue[] = [] + + for (const pattern of LUA_PATTERNS) { + const matches = code.matchAll(new RegExp(pattern.pattern.source, pattern.pattern.flags)) + for (const match of matches) { + const lineNumber = getLineNumber(code, match.index || 0) + issues.push({ + type: pattern.type, + severity: pattern.severity, + message: pattern.message, + pattern: match[0], + line: lineNumber, + recommendation: pattern.recommendation + }) + } + } + + const severity = calculateOverallSeverity(issues) + const safe = severity === 'safe' || severity === 'low' + + return { + safe, + severity, + issues, + sanitizedCode: safe ? code : undefined + } +} diff --git a/frontends/nextjs/src/lib/security/functions/types.ts b/frontends/nextjs/src/lib/security/functions/types.ts new file mode 100644 index 000000000..ef1055d0e --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/types.ts @@ -0,0 +1,28 @@ +/** + * Security Scanner Types + * Shared type definitions for all security scanning functions + */ + +export interface SecurityScanResult { + safe: boolean + severity: 'safe' | 'low' | 'medium' | 'high' | 'critical' + issues: SecurityIssue[] + sanitizedCode?: string +} + +export interface SecurityIssue { + type: 'malicious' | 'suspicious' | 'dangerous' | 'warning' + severity: 'low' | 'medium' | 'high' | 'critical' + message: string + pattern: string + line?: number + recommendation?: string +} + +export interface SecurityPattern { + pattern: RegExp + type: 'malicious' | 'suspicious' | 'dangerous' | 'warning' + severity: 'low' | 'medium' | 'high' | 'critical' + message: string + recommendation: string +} diff --git a/frontends/nextjs/src/lib/security/functions/utils/calculate-severity.ts b/frontends/nextjs/src/lib/security/functions/utils/calculate-severity.ts new file mode 100644 index 000000000..1933918d4 --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/utils/calculate-severity.ts @@ -0,0 +1,29 @@ +/** + * Calculate Overall Severity + * Determines the highest severity level from a list of issues + */ + +import type { SecurityIssue } from '../types' + +/** + * Calculate the overall severity from a list of security issues + * @param issues - Array of security issues found + * @returns Overall severity level + */ +export const calculateOverallSeverity = ( + issues: SecurityIssue[] +): 'safe' | 'low' | 'medium' | 'high' | 'critical' => { + if (issues.length === 0) return 'safe' + + const hasCritical = issues.some(i => i.severity === 'critical') + const hasHigh = issues.some(i => i.severity === 'high') + const hasMedium = issues.some(i => i.severity === 'medium') + const hasLow = issues.some(i => i.severity === 'low') + + if (hasCritical) return 'critical' + if (hasHigh) return 'high' + if (hasMedium) return 'medium' + if (hasLow) return 'low' + + return 'safe' +} diff --git a/frontends/nextjs/src/lib/security/functions/utils/get-line-number.ts b/frontends/nextjs/src/lib/security/functions/utils/get-line-number.ts new file mode 100644 index 000000000..f1cebf0a2 --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/utils/get-line-number.ts @@ -0,0 +1,14 @@ +/** + * Get Line Number + * Utility to find line number from character index in code + */ + +/** + * Calculate line number from character index + * @param code - The source code string + * @param index - Character index in the code + * @returns Line number (1-based) + */ +export const getLineNumber = (code: string, index: number): number => { + return code.substring(0, index).split('\n').length +} diff --git a/frontends/nextjs/src/lib/security/functions/utils/index.ts b/frontends/nextjs/src/lib/security/functions/utils/index.ts new file mode 100644 index 000000000..f5c076a56 --- /dev/null +++ b/frontends/nextjs/src/lib/security/functions/utils/index.ts @@ -0,0 +1,7 @@ +/** + * Utils Index + * Exports all utility functions for security scanning + */ + +export { getLineNumber } from './get-line-number' +export { calculateOverallSeverity } from './calculate-severity' diff --git a/frontends/nextjs/src/lib/security/types.ts b/frontends/nextjs/src/lib/security/types.ts new file mode 100644 index 000000000..e07a156f4 --- /dev/null +++ b/frontends/nextjs/src/lib/security/types.ts @@ -0,0 +1,23 @@ +export interface SecurityScanResult { + safe: boolean + severity: 'safe' | 'low' | 'medium' | 'high' | 'critical' + issues: SecurityIssue[] + sanitizedCode?: string +} + +export interface SecurityIssue { + type: 'malicious' | 'suspicious' | 'dangerous' | 'warning' + severity: 'low' | 'medium' | 'high' | 'critical' + message: string + pattern: string + line?: number + recommendation?: string +} + +export interface SecurityPattern { + pattern: RegExp + type: 'malicious' | 'suspicious' | 'dangerous' | 'warning' + severity: 'low' | 'medium' | 'high' | 'critical' + message: string + recommendation: string +}