From 616d4ad87bbb880defbd2825ba83325f50a82839 Mon Sep 17 00:00:00 2001 From: JohnDoe6345789 Date: Thu, 25 Dec 2025 17:54:34 +0000 Subject: [PATCH] feat: Implement CRUD operations for pages, schemas, tenants, users, workflows, and SMTP configurations - Added functions to set, update, delete, and retrieve page configurations. - Introduced model schema management with functions for adding, updating, deleting, and retrieving schemas. - Implemented tenant management with functions for adding, updating, deleting, and retrieving tenants. - Created user management functions for adding, updating, deleting, and retrieving users. - Developed workflow management functions for adding, updating, deleting, and retrieving workflows. - Added SMTP configuration management with functions to get and set SMTP configurations. - Implemented functions for managing god credentials, including expiry management and first login flags. - Introduced power transfer request management with functions for adding, updating, deleting, and retrieving requests. - Added Lua snippet management functions for retrieving snippets by ID and category, and searching snippets. --- frontends/nextjs/src/lib/db/Database.ts | 934 ++++++++++++++++++ .../lib/db/database-admin/clear-database.ts | 43 + .../lib/db/database-admin/export-database.ts | 27 + .../lib/db/database-admin/import-database.ts | 30 + .../nextjs/src/lib/db/database-admin/index.ts | 4 + .../db/database-admin/seed-default-data.ts | 151 +++ .../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 + .../god-credentials/get-first-login-flags.ts | 15 + .../get-god-credentials-expiry-duration.ts | 12 + .../get-god-credentials-expiry.ts | 12 + .../src/lib/db/god-credentials/index.ts | 8 + .../reset-god-credentials-expiry.ts | 11 + .../god-credentials/set-first-login-flag.ts | 15 + .../set-god-credentials-expiry-duration.ts | 13 + .../set-god-credentials-expiry.ts | 13 + .../should-show-god-credentials.ts | 31 + frontends/nextjs/src/lib/db/index.ts | 17 + .../add-power-transfer-request.ts | 17 + .../delete-power-transfer-request.ts | 9 + .../src/lib/db/power-transfers/index.ts | 5 + .../update-power-transfer-request.ts | 12 + .../src/lib/db/smtp-config/get-smtp-config.ts | 22 + .../nextjs/src/lib/db/smtp-config/index.ts | 2 + .../src/lib/db/smtp-config/set-smtp-config.ts | 24 + .../lib/lua/functions/get-snippet-by-id.ts | 10 + .../lua/functions/get-snippets-by-category.ts | 13 + .../nextjs/src/lib/lua/functions/index.ts | 3 + .../src/lib/lua/functions/search-snippets.ts | 15 + 73 files changed, 2207 insertions(+) create mode 100644 frontends/nextjs/src/lib/db/Database.ts create mode 100644 frontends/nextjs/src/lib/db/database-admin/clear-database.ts create mode 100644 frontends/nextjs/src/lib/db/database-admin/export-database.ts create mode 100644 frontends/nextjs/src/lib/db/database-admin/import-database.ts create mode 100644 frontends/nextjs/src/lib/db/database-admin/index.ts create mode 100644 frontends/nextjs/src/lib/db/database-admin/seed-default-data.ts create mode 100644 frontends/nextjs/src/lib/db/functions/credentials/get-credentials.ts create mode 100644 frontends/nextjs/src/lib/db/functions/credentials/get-password-change-timestamps.ts create mode 100644 frontends/nextjs/src/lib/db/functions/credentials/index.ts create mode 100644 frontends/nextjs/src/lib/db/functions/credentials/set-credential.ts create mode 100644 frontends/nextjs/src/lib/db/functions/credentials/set-password-change-timestamps.ts create mode 100644 frontends/nextjs/src/lib/db/functions/credentials/verify-credentials.ts create mode 100644 frontends/nextjs/src/lib/db/functions/hash-password.ts create mode 100644 frontends/nextjs/src/lib/db/functions/index.ts create mode 100644 frontends/nextjs/src/lib/db/functions/initialize-database.ts create mode 100644 frontends/nextjs/src/lib/db/functions/lua-scripts/add-lua-script.ts create mode 100644 frontends/nextjs/src/lib/db/functions/lua-scripts/delete-lua-script.ts create mode 100644 frontends/nextjs/src/lib/db/functions/lua-scripts/get-lua-scripts.ts create mode 100644 frontends/nextjs/src/lib/db/functions/lua-scripts/index.ts create mode 100644 frontends/nextjs/src/lib/db/functions/lua-scripts/set-lua-scripts.ts create mode 100644 frontends/nextjs/src/lib/db/functions/lua-scripts/update-lua-script.ts create mode 100644 frontends/nextjs/src/lib/db/functions/pages/add-page.ts create mode 100644 frontends/nextjs/src/lib/db/functions/pages/delete-page.ts create mode 100644 frontends/nextjs/src/lib/db/functions/pages/get-pages.ts create mode 100644 frontends/nextjs/src/lib/db/functions/pages/index.ts create mode 100644 frontends/nextjs/src/lib/db/functions/pages/set-pages.ts create mode 100644 frontends/nextjs/src/lib/db/functions/pages/update-page.ts create mode 100644 frontends/nextjs/src/lib/db/functions/schemas/add-schema.ts create mode 100644 frontends/nextjs/src/lib/db/functions/schemas/delete-schema.ts create mode 100644 frontends/nextjs/src/lib/db/functions/schemas/get-schemas.ts create mode 100644 frontends/nextjs/src/lib/db/functions/schemas/index.ts create mode 100644 frontends/nextjs/src/lib/db/functions/schemas/set-schemas.ts create mode 100644 frontends/nextjs/src/lib/db/functions/schemas/update-schema.ts create mode 100644 frontends/nextjs/src/lib/db/functions/tenants/add-tenant.ts create mode 100644 frontends/nextjs/src/lib/db/functions/tenants/delete-tenant.ts create mode 100644 frontends/nextjs/src/lib/db/functions/tenants/get-tenants.ts create mode 100644 frontends/nextjs/src/lib/db/functions/tenants/index.ts create mode 100644 frontends/nextjs/src/lib/db/functions/tenants/set-tenants.ts create mode 100644 frontends/nextjs/src/lib/db/functions/tenants/update-tenant.ts create mode 100644 frontends/nextjs/src/lib/db/functions/users/add-user.ts create mode 100644 frontends/nextjs/src/lib/db/functions/users/delete-user.ts create mode 100644 frontends/nextjs/src/lib/db/functions/users/get-users.ts create mode 100644 frontends/nextjs/src/lib/db/functions/users/index.ts create mode 100644 frontends/nextjs/src/lib/db/functions/users/set-users.ts create mode 100644 frontends/nextjs/src/lib/db/functions/users/update-user.ts create mode 100644 frontends/nextjs/src/lib/db/functions/verify-password.ts create mode 100644 frontends/nextjs/src/lib/db/functions/workflows/add-workflow.ts create mode 100644 frontends/nextjs/src/lib/db/functions/workflows/delete-workflow.ts create mode 100644 frontends/nextjs/src/lib/db/functions/workflows/get-workflows.ts create mode 100644 frontends/nextjs/src/lib/db/functions/workflows/index.ts create mode 100644 frontends/nextjs/src/lib/db/functions/workflows/set-workflows.ts create mode 100644 frontends/nextjs/src/lib/db/functions/workflows/update-workflow.ts create mode 100644 frontends/nextjs/src/lib/db/god-credentials/get-first-login-flags.ts create mode 100644 frontends/nextjs/src/lib/db/god-credentials/get-god-credentials-expiry-duration.ts create mode 100644 frontends/nextjs/src/lib/db/god-credentials/get-god-credentials-expiry.ts create mode 100644 frontends/nextjs/src/lib/db/god-credentials/index.ts create mode 100644 frontends/nextjs/src/lib/db/god-credentials/reset-god-credentials-expiry.ts create mode 100644 frontends/nextjs/src/lib/db/god-credentials/set-first-login-flag.ts create mode 100644 frontends/nextjs/src/lib/db/god-credentials/set-god-credentials-expiry-duration.ts create mode 100644 frontends/nextjs/src/lib/db/god-credentials/set-god-credentials-expiry.ts create mode 100644 frontends/nextjs/src/lib/db/god-credentials/should-show-god-credentials.ts create mode 100644 frontends/nextjs/src/lib/db/power-transfers/add-power-transfer-request.ts create mode 100644 frontends/nextjs/src/lib/db/power-transfers/delete-power-transfer-request.ts create mode 100644 frontends/nextjs/src/lib/db/power-transfers/index.ts create mode 100644 frontends/nextjs/src/lib/db/power-transfers/update-power-transfer-request.ts create mode 100644 frontends/nextjs/src/lib/db/smtp-config/get-smtp-config.ts create mode 100644 frontends/nextjs/src/lib/db/smtp-config/index.ts create mode 100644 frontends/nextjs/src/lib/db/smtp-config/set-smtp-config.ts create mode 100644 frontends/nextjs/src/lib/lua/functions/get-snippet-by-id.ts create mode 100644 frontends/nextjs/src/lib/lua/functions/get-snippets-by-category.ts create mode 100644 frontends/nextjs/src/lib/lua/functions/index.ts create mode 100644 frontends/nextjs/src/lib/lua/functions/search-snippets.ts diff --git a/frontends/nextjs/src/lib/db/Database.ts b/frontends/nextjs/src/lib/db/Database.ts new file mode 100644 index 000000000..0d58478e1 --- /dev/null +++ b/frontends/nextjs/src/lib/db/Database.ts @@ -0,0 +1,934 @@ +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/database-admin/clear-database.ts b/frontends/nextjs/src/lib/db/database-admin/clear-database.ts new file mode 100644 index 000000000..ec39baa67 --- /dev/null +++ b/frontends/nextjs/src/lib/db/database-admin/clear-database.ts @@ -0,0 +1,43 @@ +import { getAdapter } from '../dbal-client' + +const ENTITY_TYPES = [ + 'User', + 'Credential', + 'Workflow', + 'LuaScript', + 'PageConfig', + 'ModelSchema', + 'AppConfiguration', + 'Comment', + 'ComponentNode', + 'ComponentConfig', + 'SystemConfig', + 'CssCategory', + 'DropdownConfig', + 'InstalledPackage', + 'PackageData', + 'Tenant', + 'PowerTransferRequest', + 'SMTPConfig', + 'PasswordResetToken', +] as const + +/** + * Clear all data from the database + */ +export async function clearDatabase(): Promise { + const adapter = getAdapter() + for (const entityType of ENTITY_TYPES) { + try { + const result = await adapter.list(entityType) + for (const item of result.data as any[]) { + const id = item.id || item.packageId || item.name || item.key || item.username + if (id) { + await adapter.delete(entityType, id) + } + } + } catch { + // Skip if entity type doesn't exist + } + } +} diff --git a/frontends/nextjs/src/lib/db/database-admin/export-database.ts b/frontends/nextjs/src/lib/db/database-admin/export-database.ts new file mode 100644 index 000000000..da01e8c6e --- /dev/null +++ b/frontends/nextjs/src/lib/db/database-admin/export-database.ts @@ -0,0 +1,27 @@ +import type { DatabaseSchema } from '../types' +import { getUsers } from '../users' +import { getWorkflows } from '../workflows' +import { getLuaScripts } from '../lua-scripts' +import { getPages } from '../pages' +import { getSchemas } from '../schemas' +import { getAppConfig } from '../app-config' +import { getComments } from '../comments' +import { getComponentHierarchy, getComponentConfigs } from '../components' + +/** + * Export database contents as JSON string + */ +export async function exportDatabase(): Promise { + const data: Partial = { + users: await getUsers(), + workflows: await getWorkflows(), + luaScripts: await getLuaScripts(), + pages: await getPages(), + schemas: await getSchemas(), + appConfig: (await getAppConfig()) || undefined, + comments: await getComments(), + componentHierarchy: await getComponentHierarchy(), + componentConfigs: await getComponentConfigs(), + } + return JSON.stringify(data, null, 2) +} diff --git a/frontends/nextjs/src/lib/db/database-admin/import-database.ts b/frontends/nextjs/src/lib/db/database-admin/import-database.ts new file mode 100644 index 000000000..ff6749189 --- /dev/null +++ b/frontends/nextjs/src/lib/db/database-admin/import-database.ts @@ -0,0 +1,30 @@ +import type { DatabaseSchema } from '../types' +import { setUsers } from '../users' +import { setWorkflows } from '../workflows' +import { setLuaScripts } from '../lua-scripts' +import { setPages } from '../pages' +import { setSchemas } from '../schemas' +import { setAppConfig } from '../app-config' +import { setComments } from '../comments' +import { setComponentHierarchy, setComponentConfigs } from '../components' + +/** + * Import database contents from JSON string + */ +export async function importDatabase(jsonData: string): Promise { + try { + const data = JSON.parse(jsonData) as Partial + + if (data.users) await setUsers(data.users) + if (data.workflows) await setWorkflows(data.workflows) + if (data.luaScripts) await setLuaScripts(data.luaScripts) + if (data.pages) await setPages(data.pages) + if (data.schemas) await setSchemas(data.schemas) + if (data.appConfig) await setAppConfig(data.appConfig) + if (data.comments) await setComments(data.comments) + if (data.componentHierarchy) await setComponentHierarchy(data.componentHierarchy) + if (data.componentConfigs) await setComponentConfigs(data.componentConfigs) + } catch (error) { + throw new Error('Failed to import database: Invalid JSON') + } +} diff --git a/frontends/nextjs/src/lib/db/database-admin/index.ts b/frontends/nextjs/src/lib/db/database-admin/index.ts new file mode 100644 index 000000000..23fedd11b --- /dev/null +++ b/frontends/nextjs/src/lib/db/database-admin/index.ts @@ -0,0 +1,4 @@ +export { clearDatabase } from './clear-database' +export { exportDatabase } from './export-database' +export { importDatabase } from './import-database' +export { seedDefaultData } from './seed-default-data' diff --git a/frontends/nextjs/src/lib/db/database-admin/seed-default-data.ts b/frontends/nextjs/src/lib/db/database-admin/seed-default-data.ts new file mode 100644 index 000000000..7d2ae1bb4 --- /dev/null +++ b/frontends/nextjs/src/lib/db/database-admin/seed-default-data.ts @@ -0,0 +1,151 @@ +import { getUsers, setUsers } from '../users' +import { setCredential } from '../credentials' +import { getAppConfig, setAppConfig } from '../app-config' +import { getCssClasses, setCssClasses } from '../css-classes' +import { getDropdownConfigs, setDropdownConfigs } from '../dropdown-configs' +import { hashPassword } from '../hash-password' +import type { CssCategory, DropdownConfig } from '../types' +import type { User, AppConfiguration } from '../../types/level-types' + +const DEFAULT_CSS_CLASSES: 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'], + }, +] + +const DEFAULT_DROPDOWN_CONFIGS: 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' }, + ], + }, +] + +/** + * Seed database with default data + */ +export async function seedDefaultData(): Promise { + // Create default users if none exist + const users = await getUsers() + if (users.length === 0) { + const defaultUsers: User[] = [ + { + id: 'user_supergod', + username: 'supergod', + email: 'supergod@system.local', + role: 'supergod', + createdAt: Date.now(), + isInstanceOwner: true, + }, + { + id: 'user_god', + username: 'god', + email: 'god@system.local', + role: 'god', + createdAt: Date.now(), + isInstanceOwner: false, + }, + ] + await setUsers(defaultUsers) + + // Set default passwords + for (const user of defaultUsers) { + const hash = await hashPassword(user.username) + await setCredential(user.username, hash) + } + } + + // Create default app config if none exists + const appConfig = await getAppConfig() + if (!appConfig) { + const defaultConfig: AppConfiguration = { + id: 'app_001', + name: 'MetaBuilder App', + schemas: [], + workflows: [], + luaScripts: [], + pages: [], + theme: { + colors: {}, + fonts: {}, + }, + } + await setAppConfig(defaultConfig) + } + + // Create default CSS classes if none exist + const cssClasses = await getCssClasses() + if (cssClasses.length === 0) { + await setCssClasses(DEFAULT_CSS_CLASSES) + } + + // Create default dropdown configs if none exist + const dropdowns = await getDropdownConfigs() + if (dropdowns.length === 0) { + await setDropdownConfigs(DEFAULT_DROPDOWN_CONFIGS) + } +} diff --git a/frontends/nextjs/src/lib/db/functions/credentials/get-credentials.ts b/frontends/nextjs/src/lib/db/functions/credentials/get-credentials.ts new file mode 100644 index 000000000..212f7c809 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/credentials/get-credentials.ts @@ -0,0 +1,14 @@ +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 new file mode 100644 index 000000000..7b178d8af --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/credentials/get-password-change-timestamps.ts @@ -0,0 +1,19 @@ +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 new file mode 100644 index 000000000..17c3967a1 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/credentials/index.ts @@ -0,0 +1,5 @@ +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 new file mode 100644 index 000000000..b3c31d089 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/credentials/set-credential.ts @@ -0,0 +1,19 @@ +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 new file mode 100644 index 000000000..5bcb47d41 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/credentials/set-password-change-timestamps.ts @@ -0,0 +1,14 @@ +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 new file mode 100644 index 000000000..97f81b91b --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/credentials/verify-credentials.ts @@ -0,0 +1,14 @@ +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 new file mode 100644 index 000000000..012b0cd04 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/hash-password.ts @@ -0,0 +1,13 @@ +/** + * 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 new file mode 100644 index 000000000..fb28ee2ca --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/index.ts @@ -0,0 +1,25 @@ +// 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 new file mode 100644 index 000000000..cc8d49a60 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/initialize-database.ts @@ -0,0 +1,14 @@ +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 new file mode 100644 index 000000000..aefc7dbc5 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/lua-scripts/add-lua-script.ts @@ -0,0 +1,19 @@ +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 new file mode 100644 index 000000000..8c7b7b5e8 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/lua-scripts/delete-lua-script.ts @@ -0,0 +1,9 @@ +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 new file mode 100644 index 000000000..28d0b3944 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/lua-scripts/get-lua-scripts.ts @@ -0,0 +1,18 @@ +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 new file mode 100644 index 000000000..89514e013 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/lua-scripts/index.ts @@ -0,0 +1,5 @@ +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 new file mode 100644 index 000000000..762a00320 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/lua-scripts/set-lua-scripts.ts @@ -0,0 +1,22 @@ +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 new file mode 100644 index 000000000..d4ae9d58c --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/lua-scripts/update-lua-script.ts @@ -0,0 +1,21 @@ +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 new file mode 100644 index 000000000..67f8b31cd --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/pages/add-page.ts @@ -0,0 +1,20 @@ +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 new file mode 100644 index 000000000..7b4e67825 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/pages/delete-page.ts @@ -0,0 +1,9 @@ +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 new file mode 100644 index 000000000..17a940fe7 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/pages/get-pages.ts @@ -0,0 +1,19 @@ +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 new file mode 100644 index 000000000..87ebb83eb --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/pages/index.ts @@ -0,0 +1,5 @@ +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 new file mode 100644 index 000000000..1ab2f0cb5 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/pages/set-pages.ts @@ -0,0 +1,23 @@ +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 new file mode 100644 index 000000000..3f9375f20 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/pages/update-page.ts @@ -0,0 +1,22 @@ +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 new file mode 100644 index 000000000..2962fde6e --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/schemas/add-schema.ts @@ -0,0 +1,22 @@ +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 new file mode 100644 index 000000000..2c7a7a250 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/schemas/delete-schema.ts @@ -0,0 +1,9 @@ +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 new file mode 100644 index 000000000..cc7118e20 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/schemas/get-schemas.ts @@ -0,0 +1,21 @@ +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 new file mode 100644 index 000000000..a5c44cf69 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/schemas/index.ts @@ -0,0 +1,5 @@ +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 new file mode 100644 index 000000000..df919dfdf --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/schemas/set-schemas.ts @@ -0,0 +1,25 @@ +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 new file mode 100644 index 000000000..dce8e5ef4 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/schemas/update-schema.ts @@ -0,0 +1,24 @@ +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 new file mode 100644 index 000000000..b3af456fa --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/tenants/add-tenant.ts @@ -0,0 +1,18 @@ +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 new file mode 100644 index 000000000..2a0f76858 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/tenants/delete-tenant.ts @@ -0,0 +1,9 @@ +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 new file mode 100644 index 000000000..c4c2cc893 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/tenants/get-tenants.ts @@ -0,0 +1,17 @@ +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 new file mode 100644 index 000000000..78ba1fb2d --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/tenants/index.ts @@ -0,0 +1,5 @@ +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 new file mode 100644 index 000000000..a5ba83a0e --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/tenants/set-tenants.ts @@ -0,0 +1,21 @@ +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 new file mode 100644 index 000000000..7ad226d71 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/tenants/update-tenant.ts @@ -0,0 +1,19 @@ +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 new file mode 100644 index 000000000..1bfb0f0d7 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/users/add-user.ts @@ -0,0 +1,24 @@ +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 new file mode 100644 index 000000000..c4e6195d1 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/users/delete-user.ts @@ -0,0 +1,9 @@ +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 new file mode 100644 index 000000000..ff5a9ad1b --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/users/get-users.ts @@ -0,0 +1,21 @@ +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 new file mode 100644 index 000000000..b6e680b5d --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/users/index.ts @@ -0,0 +1,5 @@ +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 new file mode 100644 index 000000000..af711f8ce --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/users/set-users.ts @@ -0,0 +1,27 @@ +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 new file mode 100644 index 000000000..7cb11ca51 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/users/update-user.ts @@ -0,0 +1,23 @@ +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 new file mode 100644 index 000000000..309cc55c8 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/verify-password.ts @@ -0,0 +1,12 @@ +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 new file mode 100644 index 000000000..470118611 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/workflows/add-workflow.ts @@ -0,0 +1,19 @@ +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 new file mode 100644 index 000000000..0b6a80d30 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/workflows/delete-workflow.ts @@ -0,0 +1,9 @@ +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 new file mode 100644 index 000000000..102515eed --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/workflows/get-workflows.ts @@ -0,0 +1,18 @@ +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 new file mode 100644 index 000000000..35db1eaaa --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/workflows/index.ts @@ -0,0 +1,5 @@ +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 new file mode 100644 index 000000000..25c35a3c2 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/workflows/set-workflows.ts @@ -0,0 +1,22 @@ +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 new file mode 100644 index 000000000..3458ec265 --- /dev/null +++ b/frontends/nextjs/src/lib/db/functions/workflows/update-workflow.ts @@ -0,0 +1,21 @@ +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-first-login-flags.ts b/frontends/nextjs/src/lib/db/god-credentials/get-first-login-flags.ts new file mode 100644 index 000000000..7dbd2e659 --- /dev/null +++ b/frontends/nextjs/src/lib/db/god-credentials/get-first-login-flags.ts @@ -0,0 +1,15 @@ +import { getAdapter } from '../dbal-client' + +/** + * Get first login flags for all users + */ +export async function getFirstLoginFlags(): Promise> { + const adapter = getAdapter() + const result = await adapter.list('User') + const users = result.data as any[] + const flags: Record = {} + for (const user of users) { + flags[user.username] = user.firstLogin ?? true + } + return flags +} 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 new file mode 100644 index 000000000..c47fcf9c2 --- /dev/null +++ b/frontends/nextjs/src/lib/db/god-credentials/get-god-credentials-expiry-duration.ts @@ -0,0 +1,12 @@ +import { getAdapter } from '../dbal-client' + +/** + * Get god credentials expiry duration in ms + */ +export async function getGodCredentialsExpiryDuration(): Promise { + const adapter = getAdapter() + const config = await adapter.findFirst('SystemConfig', { + where: { key: 'god_credentials_expiry_duration' }, + }) + 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 new file mode 100644 index 000000000..66e67b6b8 --- /dev/null +++ b/frontends/nextjs/src/lib/db/god-credentials/get-god-credentials-expiry.ts @@ -0,0 +1,12 @@ +import { getAdapter } from '../dbal-client' + +/** + * Get god credentials expiry timestamp + */ +export async function getGodCredentialsExpiry(): Promise { + const adapter = getAdapter() + const config = await adapter.findFirst('SystemConfig', { + where: { key: 'god_credentials_expiry' }, + }) + return config ? Number(config.value) : 0 +} diff --git a/frontends/nextjs/src/lib/db/god-credentials/index.ts b/frontends/nextjs/src/lib/db/god-credentials/index.ts new file mode 100644 index 000000000..e7cea4057 --- /dev/null +++ b/frontends/nextjs/src/lib/db/god-credentials/index.ts @@ -0,0 +1,8 @@ +export { getGodCredentialsExpiry } from './get-god-credentials-expiry' +export { setGodCredentialsExpiry } from './set-god-credentials-expiry' +export { getFirstLoginFlags } from './get-first-login-flags' +export { setFirstLoginFlag } from './set-first-login-flag' +export { getGodCredentialsExpiryDuration } from './get-god-credentials-expiry-duration' +export { setGodCredentialsExpiryDuration } from './set-god-credentials-expiry-duration' +export { shouldShowGodCredentials } from './should-show-god-credentials' +export { resetGodCredentialsExpiry } from './reset-god-credentials-expiry' diff --git a/frontends/nextjs/src/lib/db/god-credentials/reset-god-credentials-expiry.ts b/frontends/nextjs/src/lib/db/god-credentials/reset-god-credentials-expiry.ts new file mode 100644 index 000000000..0e0235b20 --- /dev/null +++ b/frontends/nextjs/src/lib/db/god-credentials/reset-god-credentials-expiry.ts @@ -0,0 +1,11 @@ +import { setGodCredentialsExpiry } from './set-god-credentials-expiry' +import { getGodCredentialsExpiryDuration } from './get-god-credentials-expiry-duration' + +/** + * Reset god credentials expiry to extend it by the configured duration + */ +export async function resetGodCredentialsExpiry(): Promise { + const duration = await getGodCredentialsExpiryDuration() + const expiryTime = Date.now() + duration + await setGodCredentialsExpiry(expiryTime) +} 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 new file mode 100644 index 000000000..824a2d5d5 --- /dev/null +++ b/frontends/nextjs/src/lib/db/god-credentials/set-first-login-flag.ts @@ -0,0 +1,15 @@ +import { getAdapter } from '../dbal-client' + +/** + * Set first login flag for a user + */ +export async function setFirstLoginFlag(username: string, isFirstLogin: boolean): Promise { + const adapter = getAdapter() + // Find the user first + const user = await adapter.findFirst('User', { + where: { username }, + }) + if (user) { + await adapter.update('User', user.id, { firstLogin: isFirstLogin }) + } +} diff --git a/frontends/nextjs/src/lib/db/god-credentials/set-god-credentials-expiry-duration.ts b/frontends/nextjs/src/lib/db/god-credentials/set-god-credentials-expiry-duration.ts new file mode 100644 index 000000000..eee4ea45c --- /dev/null +++ b/frontends/nextjs/src/lib/db/god-credentials/set-god-credentials-expiry-duration.ts @@ -0,0 +1,13 @@ +import { getAdapter } from '../dbal-client' + +/** + * Set god credentials expiry duration in ms + */ +export async function setGodCredentialsExpiryDuration(durationMs: number): Promise { + const adapter = getAdapter() + await adapter.upsert('SystemConfig', { + where: { key: 'god_credentials_expiry_duration' }, + update: { value: durationMs.toString() }, + create: { key: 'god_credentials_expiry_duration', value: durationMs.toString() }, + }) +} diff --git a/frontends/nextjs/src/lib/db/god-credentials/set-god-credentials-expiry.ts b/frontends/nextjs/src/lib/db/god-credentials/set-god-credentials-expiry.ts new file mode 100644 index 000000000..bb784fb86 --- /dev/null +++ b/frontends/nextjs/src/lib/db/god-credentials/set-god-credentials-expiry.ts @@ -0,0 +1,13 @@ +import { getAdapter } from '../dbal-client' + +/** + * Set god credentials expiry timestamp + */ +export async function setGodCredentialsExpiry(timestamp: number): Promise { + const adapter = getAdapter() + await adapter.upsert('SystemConfig', { + where: { key: 'god_credentials_expiry' }, + update: { value: timestamp.toString() }, + create: { key: 'god_credentials_expiry', value: timestamp.toString() }, + }) +} 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 new file mode 100644 index 000000000..fd127cb53 --- /dev/null +++ b/frontends/nextjs/src/lib/db/god-credentials/should-show-god-credentials.ts @@ -0,0 +1,31 @@ +import { getGodCredentialsExpiry } from './get-god-credentials-expiry' +import { setGodCredentialsExpiry } from './set-god-credentials-expiry' +import { getGodCredentialsExpiryDuration } from './get-god-credentials-expiry-duration' +import { getAdapter } from '../dbal-client' + +/** + * Check if god credentials should be shown + */ +export async function shouldShowGodCredentials(): Promise { + const adapter = getAdapter() + const expiry = await getGodCredentialsExpiry() + + // Get god user's password change timestamp + const godUser = await adapter.findFirst('User', { + where: { username: 'god' }, + }) + const godPasswordChangeTime = godUser?.passwordChangeTimestamp ? Number(godUser.passwordChangeTimestamp) : 0 + + if (expiry === 0) { + const duration = await getGodCredentialsExpiryDuration() + const expiryTime = Date.now() + duration + await setGodCredentialsExpiry(expiryTime) + return true + } + + if (godPasswordChangeTime > expiry) { + return false + } + + return Date.now() < expiry +} diff --git a/frontends/nextjs/src/lib/db/index.ts b/frontends/nextjs/src/lib/db/index.ts index fe3354828..6caccacbc 100644 --- a/frontends/nextjs/src/lib/db/index.ts +++ b/frontends/nextjs/src/lib/db/index.ts @@ -1,5 +1,6 @@ // Types export type { CssCategory, DropdownConfig, DatabaseSchema, ComponentNode, ComponentConfig } from './types' +export { DB_KEYS } from './types' // DBAL Client export { getAdapter, closeAdapter } from './dbal-client' @@ -20,6 +21,14 @@ export * from './schemas' export * from './comments' export * from './app-config' export * from './components' +export * from './css-classes' +export * from './dropdown-configs' +export * from './tenants' +export * from './packages' +export * from './power-transfers' +export * from './smtp-config' +export * from './god-credentials' +export * from './database-admin' // Import all for namespace class import { initializeDatabase } from './initialize-database' @@ -34,6 +43,14 @@ import * as schemas from './schemas' import * as comments from './comments' import * as appConfig from './app-config' import * as components from './components' +import * as cssClasses from './css-classes' +import * as dropdownConfigs from './dropdown-configs' +import * as tenants from './tenants' +import * as packages from './packages' +import * as powerTransfers from './power-transfers' +import * as smtpConfig from './smtp-config' +import * as godCredentials from './god-credentials' +import * as databaseAdmin from './database-admin' /** * Database namespace class - groups all DB operations as static methods diff --git a/frontends/nextjs/src/lib/db/power-transfers/add-power-transfer-request.ts b/frontends/nextjs/src/lib/db/power-transfers/add-power-transfer-request.ts new file mode 100644 index 000000000..e63bb0acc --- /dev/null +++ b/frontends/nextjs/src/lib/db/power-transfers/add-power-transfer-request.ts @@ -0,0 +1,17 @@ +import { getAdapter } from '../dbal-client' +import type { PowerTransferRequest } from '../../types/level-types' + +/** + * Add a new power transfer request + */ +export async function addPowerTransferRequest(request: PowerTransferRequest): Promise { + const adapter = getAdapter() + await adapter.create('PowerTransferRequest', { + id: request.id, + fromUserId: request.fromUserId, + toUserId: request.toUserId, + status: request.status, + createdAt: BigInt(request.createdAt), + expiresAt: BigInt(request.expiresAt), + }) +} diff --git a/frontends/nextjs/src/lib/db/power-transfers/delete-power-transfer-request.ts b/frontends/nextjs/src/lib/db/power-transfers/delete-power-transfer-request.ts new file mode 100644 index 000000000..47579f4ae --- /dev/null +++ b/frontends/nextjs/src/lib/db/power-transfers/delete-power-transfer-request.ts @@ -0,0 +1,9 @@ +import { getAdapter } from '../dbal-client' + +/** + * Delete a power transfer request + */ +export async function deletePowerTransferRequest(requestId: string): Promise { + const adapter = getAdapter() + await adapter.delete('PowerTransferRequest', requestId) +} diff --git a/frontends/nextjs/src/lib/db/power-transfers/index.ts b/frontends/nextjs/src/lib/db/power-transfers/index.ts new file mode 100644 index 000000000..b7433fbb8 --- /dev/null +++ b/frontends/nextjs/src/lib/db/power-transfers/index.ts @@ -0,0 +1,5 @@ +export { getPowerTransferRequests } from './get-power-transfer-requests' +export { setPowerTransferRequests } from './set-power-transfer-requests' +export { addPowerTransferRequest } from './add-power-transfer-request' +export { updatePowerTransferRequest } from './update-power-transfer-request' +export { deletePowerTransferRequest } from './delete-power-transfer-request' diff --git a/frontends/nextjs/src/lib/db/power-transfers/update-power-transfer-request.ts b/frontends/nextjs/src/lib/db/power-transfers/update-power-transfer-request.ts new file mode 100644 index 000000000..8bcb6076a --- /dev/null +++ b/frontends/nextjs/src/lib/db/power-transfers/update-power-transfer-request.ts @@ -0,0 +1,12 @@ +import { getAdapter } from '../dbal-client' +import type { PowerTransferRequest } from '../../types/level-types' + +/** + * Update an existing power transfer request + */ +export async function updatePowerTransferRequest(requestId: string, updates: Partial): Promise { + const adapter = getAdapter() + const data: Record = {} + if (updates.status !== undefined) data.status = updates.status + await adapter.update('PowerTransferRequest', requestId, data) +} diff --git a/frontends/nextjs/src/lib/db/smtp-config/get-smtp-config.ts b/frontends/nextjs/src/lib/db/smtp-config/get-smtp-config.ts new file mode 100644 index 000000000..73f8f5156 --- /dev/null +++ b/frontends/nextjs/src/lib/db/smtp-config/get-smtp-config.ts @@ -0,0 +1,22 @@ +import { getAdapter } from '../dbal-client' +import type { SMTPConfig } from '../../password-utils' + +/** + * Get SMTP configuration + */ +export async function getSMTPConfig(): Promise { + const adapter = getAdapter() + const result = await adapter.list('SMTPConfig') + const config = (result.data as any[])[0] + 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, + } +} diff --git a/frontends/nextjs/src/lib/db/smtp-config/index.ts b/frontends/nextjs/src/lib/db/smtp-config/index.ts new file mode 100644 index 000000000..88db57d33 --- /dev/null +++ b/frontends/nextjs/src/lib/db/smtp-config/index.ts @@ -0,0 +1,2 @@ +export { getSMTPConfig } from './get-smtp-config' +export { setSMTPConfig } from './set-smtp-config' diff --git a/frontends/nextjs/src/lib/db/smtp-config/set-smtp-config.ts b/frontends/nextjs/src/lib/db/smtp-config/set-smtp-config.ts new file mode 100644 index 000000000..326091159 --- /dev/null +++ b/frontends/nextjs/src/lib/db/smtp-config/set-smtp-config.ts @@ -0,0 +1,24 @@ +import { getAdapter } from '../dbal-client' +import type { SMTPConfig } from '../../password-utils' + +/** + * Set SMTP configuration (replaces existing) + */ +export async function setSMTPConfig(config: SMTPConfig): Promise { + const adapter = getAdapter() + // Delete all existing + const existing = await adapter.list('SMTPConfig') + for (const item of existing.data as any[]) { + await adapter.delete('SMTPConfig', item.id) + } + // Create new + await adapter.create('SMTPConfig', { + host: config.host, + port: config.port, + secure: config.secure, + username: config.username, + password: config.password, + fromEmail: config.fromEmail, + fromName: config.fromName, + }) +} diff --git a/frontends/nextjs/src/lib/lua/functions/get-snippet-by-id.ts b/frontends/nextjs/src/lib/lua/functions/get-snippet-by-id.ts new file mode 100644 index 000000000..32c43b402 --- /dev/null +++ b/frontends/nextjs/src/lib/lua/functions/get-snippet-by-id.ts @@ -0,0 +1,10 @@ +import { LUA_SNIPPETS, type LuaSnippet } from '../snippets/lua-snippets-data' + +/** + * Get a snippet by its ID + * @param id - Snippet ID + * @returns The snippet or undefined if not found + */ +export const getSnippetById = (id: string): LuaSnippet | undefined => { + return LUA_SNIPPETS.find(snippet => snippet.id === id) +} diff --git a/frontends/nextjs/src/lib/lua/functions/get-snippets-by-category.ts b/frontends/nextjs/src/lib/lua/functions/get-snippets-by-category.ts new file mode 100644 index 000000000..3cbb60ff5 --- /dev/null +++ b/frontends/nextjs/src/lib/lua/functions/get-snippets-by-category.ts @@ -0,0 +1,13 @@ +import { LUA_SNIPPETS, type LuaSnippet } from '../snippets/lua-snippets-data' + +/** + * Get snippets filtered by category + * @param category - Category name or 'All' for all snippets + * @returns Array of matching snippets + */ +export const getSnippetsByCategory = (category: string): LuaSnippet[] => { + if (category === 'All') { + return LUA_SNIPPETS + } + return LUA_SNIPPETS.filter(snippet => snippet.category === category) +} diff --git a/frontends/nextjs/src/lib/lua/functions/index.ts b/frontends/nextjs/src/lib/lua/functions/index.ts new file mode 100644 index 000000000..836b014a6 --- /dev/null +++ b/frontends/nextjs/src/lib/lua/functions/index.ts @@ -0,0 +1,3 @@ +export { getSnippetsByCategory } from './get-snippets-by-category' +export { searchSnippets } from './search-snippets' +export { getSnippetById } from './get-snippet-by-id' diff --git a/frontends/nextjs/src/lib/lua/functions/search-snippets.ts b/frontends/nextjs/src/lib/lua/functions/search-snippets.ts new file mode 100644 index 000000000..1b2003973 --- /dev/null +++ b/frontends/nextjs/src/lib/lua/functions/search-snippets.ts @@ -0,0 +1,15 @@ +import { LUA_SNIPPETS, type LuaSnippet } from '../snippets/lua-snippets-data' + +/** + * Search snippets by query string + * @param query - Search query + * @returns Array of matching snippets + */ +export const 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)) + ) +}