diff --git a/frontends/nextjs/src/lib/auth.ts b/frontends/nextjs/src/lib/auth.ts index fe09f5982..b041b4ad2 100644 --- a/frontends/nextjs/src/lib/auth.ts +++ b/frontends/nextjs/src/lib/auth.ts @@ -1,109 +1,13 @@ /** * Authentication and Authorization Module - * - * Handles user authentication, permission checking, and role-based access control. - * Implements a 5-level hierarchical permission system where each level inherits - * permissions from lower levels. + * @deprecated Import from '@/lib/auth' instead */ -import type { User, UserRole } from './level-types' +export { + canAccessLevel, + getRoleDisplayName, + getScrambledPassword, + DEFAULT_USERS, + DEFAULT_CREDENTIALS, +} from './auth/index' -const SCRAMBLE_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*' - -function generateDeterministicScrambledPassword(seed: string, length: number = 16): string { - // Deterministic output avoids server/client mismatches during hydration. - let hash = 2166136261 - for (let i = 0; i < seed.length; i++) { - hash ^= seed.charCodeAt(i) - hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24) - } - - let password = '' - for (let i = 0; i < length; i++) { - hash = (hash * 1664525 + 1013904223) >>> 0 - password += SCRAMBLE_CHARSET[hash % SCRAMBLE_CHARSET.length] - } - return password -} - -// Pre-generated scrambled passwords for default user accounts -// Each role has a unique scrambled password for authentication -const SCRAMBLED_PASSWORDS = { - supergod: generateDeterministicScrambledPassword('supergod', 16), - god: generateDeterministicScrambledPassword('god', 16), - admin: generateDeterministicScrambledPassword('admin', 16), - demo: generateDeterministicScrambledPassword('demo', 16), -} - -// Default users created during application initialization -// These provide initial access points for different permission levels -export const DEFAULT_USERS: 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(), - }, -] - -// Maps usernames to their scrambled passwords for authentication -export const DEFAULT_CREDENTIALS: Record = SCRAMBLED_PASSWORDS - -/** - * Gets the scrambled password for a given username - * @param username - The username to look up - * @returns The scrambled password, or empty string if not found - */ -export function getScrambledPassword(username: string): string { - return SCRAMBLED_PASSWORDS[username as keyof typeof SCRAMBLED_PASSWORDS] || '' -} - -export function canAccessLevel(userRole: UserRole, level: number): boolean { - const roleHierarchy: Record = { - public: 1, - user: 2, - admin: 3, - god: 4, - supergod: 5, - } - - return roleHierarchy[userRole] >= level -} - -export function getRoleDisplayName(role: UserRole): string { - const names: Record = { - public: 'Public', - user: 'User', - admin: 'Administrator', - god: 'System Architect', - supergod: 'Supreme Administrator', - } - return names[role] -} diff --git a/frontends/nextjs/src/lib/auth/can-access-level.ts b/frontends/nextjs/src/lib/auth/can-access-level.ts new file mode 100644 index 000000000..3a1073889 --- /dev/null +++ b/frontends/nextjs/src/lib/auth/can-access-level.ts @@ -0,0 +1,19 @@ +import type { UserRole } from '../level-types' + +/** + * Role hierarchy mapping roles to their numeric permission levels + */ +const roleHierarchy: Record = { + public: 1, + user: 2, + admin: 3, + god: 4, + supergod: 5, +} + +/** + * Check if a user role has access to a given permission level + */ +export function canAccessLevel(userRole: UserRole, level: number): boolean { + return roleHierarchy[userRole] >= level +} diff --git a/frontends/nextjs/src/lib/auth/default-credentials.ts b/frontends/nextjs/src/lib/auth/default-credentials.ts new file mode 100644 index 000000000..833bc9ca1 --- /dev/null +++ b/frontends/nextjs/src/lib/auth/default-credentials.ts @@ -0,0 +1,6 @@ +import { SCRAMBLED_PASSWORDS } from './scrambled-passwords' + +/** + * Maps usernames to their scrambled passwords for authentication + */ +export const DEFAULT_CREDENTIALS: Record = SCRAMBLED_PASSWORDS diff --git a/frontends/nextjs/src/lib/auth/default-users.ts b/frontends/nextjs/src/lib/auth/default-users.ts new file mode 100644 index 000000000..7b75dc481 --- /dev/null +++ b/frontends/nextjs/src/lib/auth/default-users.ts @@ -0,0 +1,40 @@ +import type { User } from '../level-types' + +/** + * Default users created during application initialization + */ +export const DEFAULT_USERS: 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(), + }, +] diff --git a/frontends/nextjs/src/lib/auth/get-role-display-name.ts b/frontends/nextjs/src/lib/auth/get-role-display-name.ts new file mode 100644 index 000000000..4b89ee94d --- /dev/null +++ b/frontends/nextjs/src/lib/auth/get-role-display-name.ts @@ -0,0 +1,19 @@ +import type { UserRole } from '../level-types' + +/** + * Human-readable display names for user roles + */ +const roleDisplayNames: Record = { + public: 'Public', + user: 'User', + admin: 'Administrator', + god: 'System Architect', + supergod: 'Supreme Administrator', +} + +/** + * Get the display name for a user role + */ +export function getRoleDisplayName(role: UserRole): string { + return roleDisplayNames[role] +} diff --git a/frontends/nextjs/src/lib/auth/get-scrambled-password.ts b/frontends/nextjs/src/lib/auth/get-scrambled-password.ts new file mode 100644 index 000000000..748aed30c --- /dev/null +++ b/frontends/nextjs/src/lib/auth/get-scrambled-password.ts @@ -0,0 +1,8 @@ +import { SCRAMBLED_PASSWORDS } from './scrambled-passwords' + +/** + * Gets the scrambled password for a given username + */ +export function getScrambledPassword(username: string): string { + return SCRAMBLED_PASSWORDS[username as keyof typeof SCRAMBLED_PASSWORDS] || '' +} diff --git a/frontends/nextjs/src/lib/auth/index.ts b/frontends/nextjs/src/lib/auth/index.ts new file mode 100644 index 000000000..3ca67dcc4 --- /dev/null +++ b/frontends/nextjs/src/lib/auth/index.ts @@ -0,0 +1,6 @@ +export { canAccessLevel } from './can-access-level' +export { getRoleDisplayName } from './get-role-display-name' +export { getScrambledPassword } from './get-scrambled-password' +export { DEFAULT_USERS } from './default-users' +export { DEFAULT_CREDENTIALS } from './default-credentials' +export { SCRAMBLED_PASSWORDS } from './scrambled-passwords' diff --git a/frontends/nextjs/src/lib/auth/scrambled-passwords.ts b/frontends/nextjs/src/lib/auth/scrambled-passwords.ts new file mode 100644 index 000000000..8e79636ec --- /dev/null +++ b/frontends/nextjs/src/lib/auth/scrambled-passwords.ts @@ -0,0 +1,30 @@ +const SCRAMBLE_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*' + +/** + * Generate a deterministic scrambled password from a seed + * Uses FNV-1a inspired hash for consistent output across server/client + */ +function generateDeterministicScrambledPassword(seed: string, length: number = 16): string { + let hash = 2166136261 + for (let i = 0; i < seed.length; i++) { + hash ^= seed.charCodeAt(i) + hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24) + } + + let password = '' + for (let i = 0; i < length; i++) { + hash = (hash * 1664525 + 1013904223) >>> 0 + password += SCRAMBLE_CHARSET[hash % SCRAMBLE_CHARSET.length] + } + return password +} + +/** + * Pre-generated scrambled passwords for default user accounts + */ +export const SCRAMBLED_PASSWORDS = { + supergod: generateDeterministicScrambledPassword('supergod', 16), + god: generateDeterministicScrambledPassword('god', 16), + admin: generateDeterministicScrambledPassword('admin', 16), + demo: generateDeterministicScrambledPassword('demo', 16), +} diff --git a/frontends/nextjs/src/lib/db/app-config/get-app-config.ts b/frontends/nextjs/src/lib/db/app-config/get-app-config.ts new file mode 100644 index 000000000..d9cd64407 --- /dev/null +++ b/frontends/nextjs/src/lib/db/app-config/get-app-config.ts @@ -0,0 +1,16 @@ +import { prisma } from '../prisma' +import type { AppConfiguration } from '../../level-types' + +export async function 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), + } +} diff --git a/frontends/nextjs/src/lib/db/app-config/index.ts b/frontends/nextjs/src/lib/db/app-config/index.ts new file mode 100644 index 000000000..cc04523f7 --- /dev/null +++ b/frontends/nextjs/src/lib/db/app-config/index.ts @@ -0,0 +1,2 @@ +export { getAppConfig } from './get-app-config' +export { setAppConfig } from './set-app-config' diff --git a/frontends/nextjs/src/lib/db/app-config/set-app-config.ts b/frontends/nextjs/src/lib/db/app-config/set-app-config.ts new file mode 100644 index 000000000..67d24c826 --- /dev/null +++ b/frontends/nextjs/src/lib/db/app-config/set-app-config.ts @@ -0,0 +1,17 @@ +import { prisma } from '../prisma' +import type { AppConfiguration } from '../../level-types' + +export async function 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), + }, + }) +} diff --git a/frontends/nextjs/src/lib/db/comments/add-comment.ts b/frontends/nextjs/src/lib/db/comments/add-comment.ts new file mode 100644 index 000000000..a69baa918 --- /dev/null +++ b/frontends/nextjs/src/lib/db/comments/add-comment.ts @@ -0,0 +1,15 @@ +import { prisma } from '../prisma' +import type { Comment } from '../../level-types' + +export async function 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, + }, + }) +} diff --git a/frontends/nextjs/src/lib/db/comments/delete-comment.ts b/frontends/nextjs/src/lib/db/comments/delete-comment.ts new file mode 100644 index 000000000..6560ac968 --- /dev/null +++ b/frontends/nextjs/src/lib/db/comments/delete-comment.ts @@ -0,0 +1,5 @@ +import { prisma } from '../prisma' + +export async function deleteComment(commentId: string): Promise { + await prisma.comment.delete({ where: { id: commentId } }) +} diff --git a/frontends/nextjs/src/lib/db/comments/get-comments.ts b/frontends/nextjs/src/lib/db/comments/get-comments.ts new file mode 100644 index 000000000..9547b87dc --- /dev/null +++ b/frontends/nextjs/src/lib/db/comments/get-comments.ts @@ -0,0 +1,14 @@ +import { prisma } from '../prisma' +import type { Comment } from '../../level-types' + +export async function 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, + })) +} diff --git a/frontends/nextjs/src/lib/db/comments/index.ts b/frontends/nextjs/src/lib/db/comments/index.ts new file mode 100644 index 000000000..a15ed90c2 --- /dev/null +++ b/frontends/nextjs/src/lib/db/comments/index.ts @@ -0,0 +1,5 @@ +export { getComments } from './get-comments' +export { setComments } from './set-comments' +export { addComment } from './add-comment' +export { updateComment } from './update-comment' +export { deleteComment } from './delete-comment' diff --git a/frontends/nextjs/src/lib/db/comments/set-comments.ts b/frontends/nextjs/src/lib/db/comments/set-comments.ts new file mode 100644 index 000000000..47e8a641e --- /dev/null +++ b/frontends/nextjs/src/lib/db/comments/set-comments.ts @@ -0,0 +1,18 @@ +import { prisma } from '../prisma' +import type { Comment } from '../../level-types' + +export async function 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, + }, + }) + } +} diff --git a/frontends/nextjs/src/lib/db/comments/update-comment.ts b/frontends/nextjs/src/lib/db/comments/update-comment.ts new file mode 100644 index 000000000..1ecf0d8c4 --- /dev/null +++ b/frontends/nextjs/src/lib/db/comments/update-comment.ts @@ -0,0 +1,9 @@ +import { prisma } from '../prisma' +import type { Comment } from '../../level-types' + +export async function 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 }) +} diff --git a/frontends/nextjs/src/lib/db/components/add-component-config.ts b/frontends/nextjs/src/lib/db/components/add-component-config.ts new file mode 100644 index 000000000..23b1f57e9 --- /dev/null +++ b/frontends/nextjs/src/lib/db/components/add-component-config.ts @@ -0,0 +1,15 @@ +import { prisma } from '../prisma' +import type { ComponentConfig } from '../types' + +export async function 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, + }, + }) +} diff --git a/frontends/nextjs/src/lib/db/components/add-component-node.ts b/frontends/nextjs/src/lib/db/components/add-component-node.ts new file mode 100644 index 000000000..8c85a4b09 --- /dev/null +++ b/frontends/nextjs/src/lib/db/components/add-component-node.ts @@ -0,0 +1,15 @@ +import { prisma } from '../prisma' +import type { ComponentNode } from '../types' + +export async function 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, + }, + }) +} diff --git a/frontends/nextjs/src/lib/db/components/delete-component-config.ts b/frontends/nextjs/src/lib/db/components/delete-component-config.ts new file mode 100644 index 000000000..beb2c06a8 --- /dev/null +++ b/frontends/nextjs/src/lib/db/components/delete-component-config.ts @@ -0,0 +1,5 @@ +import { prisma } from '../prisma' + +export async function deleteComponentConfig(configId: string): Promise { + await prisma.componentConfig.delete({ where: { id: configId } }) +} diff --git a/frontends/nextjs/src/lib/db/components/delete-component-node.ts b/frontends/nextjs/src/lib/db/components/delete-component-node.ts new file mode 100644 index 000000000..0e955c237 --- /dev/null +++ b/frontends/nextjs/src/lib/db/components/delete-component-node.ts @@ -0,0 +1,5 @@ +import { prisma } from '../prisma' + +export async function deleteComponentNode(nodeId: string): Promise { + await prisma.componentNode.delete({ where: { id: nodeId } }) +} diff --git a/frontends/nextjs/src/lib/db/components/get-component-configs.ts b/frontends/nextjs/src/lib/db/components/get-component-configs.ts new file mode 100644 index 000000000..264f021d0 --- /dev/null +++ b/frontends/nextjs/src/lib/db/components/get-component-configs.ts @@ -0,0 +1,18 @@ +import { prisma } from '../prisma' +import type { ComponentConfig } from '../types' + +export async function 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 +} diff --git a/frontends/nextjs/src/lib/db/components/get-component-hierarchy.ts b/frontends/nextjs/src/lib/db/components/get-component-hierarchy.ts new file mode 100644 index 000000000..aa06bdabc --- /dev/null +++ b/frontends/nextjs/src/lib/db/components/get-component-hierarchy.ts @@ -0,0 +1,18 @@ +import { prisma } from '../prisma' +import type { ComponentNode } from '../types' + +export async function 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 +} diff --git a/frontends/nextjs/src/lib/db/components/index.ts b/frontends/nextjs/src/lib/db/components/index.ts new file mode 100644 index 000000000..2521745e6 --- /dev/null +++ b/frontends/nextjs/src/lib/db/components/index.ts @@ -0,0 +1,10 @@ +export { getComponentHierarchy } from './get-component-hierarchy' +export { setComponentHierarchy } from './set-component-hierarchy' +export { addComponentNode } from './add-component-node' +export { updateComponentNode } from './update-component-node' +export { deleteComponentNode } from './delete-component-node' +export { getComponentConfigs } from './get-component-configs' +export { setComponentConfigs } from './set-component-configs' +export { addComponentConfig } from './add-component-config' +export { updateComponentConfig } from './update-component-config' +export { deleteComponentConfig } from './delete-component-config' diff --git a/frontends/nextjs/src/lib/db/components/set-component-configs.ts b/frontends/nextjs/src/lib/db/components/set-component-configs.ts new file mode 100644 index 000000000..fa3d4b20e --- /dev/null +++ b/frontends/nextjs/src/lib/db/components/set-component-configs.ts @@ -0,0 +1,18 @@ +import { prisma } from '../prisma' +import type { ComponentConfig } from '../types' + +export async function 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, + }, + }) + } +} diff --git a/frontends/nextjs/src/lib/db/components/set-component-hierarchy.ts b/frontends/nextjs/src/lib/db/components/set-component-hierarchy.ts new file mode 100644 index 000000000..1005c576f --- /dev/null +++ b/frontends/nextjs/src/lib/db/components/set-component-hierarchy.ts @@ -0,0 +1,18 @@ +import { prisma } from '../prisma' +import type { ComponentNode } from '../types' + +export async function 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, + }, + }) + } +} diff --git a/frontends/nextjs/src/lib/db/components/update-component-config.ts b/frontends/nextjs/src/lib/db/components/update-component-config.ts new file mode 100644 index 000000000..16bb90552 --- /dev/null +++ b/frontends/nextjs/src/lib/db/components/update-component-config.ts @@ -0,0 +1,14 @@ +import { prisma } from '../prisma' +import type { ComponentConfig } from '../types' + +export async function 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 = updates.conditionalRendering ? JSON.stringify(updates.conditionalRendering) : null + } + await prisma.componentConfig.update({ where: { id: configId }, data }) +} diff --git a/frontends/nextjs/src/lib/db/components/update-component-node.ts b/frontends/nextjs/src/lib/db/components/update-component-node.ts new file mode 100644 index 000000000..f76ced7e2 --- /dev/null +++ b/frontends/nextjs/src/lib/db/components/update-component-node.ts @@ -0,0 +1,12 @@ +import { prisma } from '../prisma' +import type { ComponentNode } from '../types' + +export async function 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 }) +} diff --git a/frontends/nextjs/src/lib/db/credentials/delete-password-reset-token.ts b/frontends/nextjs/src/lib/db/credentials/delete-password-reset-token.ts new file mode 100644 index 000000000..a3be5a1cc --- /dev/null +++ b/frontends/nextjs/src/lib/db/credentials/delete-password-reset-token.ts @@ -0,0 +1,15 @@ +import { prisma } from '../prisma' +import { getPasswordResetTokens } from './get-password-reset-tokens' + +/** + * Delete a password reset token + */ +export async function deletePasswordResetToken(username: string): Promise { + const tokens = await getPasswordResetTokens() + delete tokens[username] + await prisma.keyValue.upsert({ + where: { key: 'db_password_reset_tokens' }, + update: { value: JSON.stringify(tokens) }, + create: { key: 'db_password_reset_tokens', value: JSON.stringify(tokens) }, + }) +} diff --git a/frontends/nextjs/src/lib/db/credentials/get-credentials.ts b/frontends/nextjs/src/lib/db/credentials/get-credentials.ts new file mode 100644 index 000000000..98944aa7b --- /dev/null +++ b/frontends/nextjs/src/lib/db/credentials/get-credentials.ts @@ -0,0 +1,13 @@ +import { prisma } from '../prisma' + +/** + * Get all credentials as a username->passwordHash map + */ +export async function getCredentials(): 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/credentials/get-password-change-timestamps.ts b/frontends/nextjs/src/lib/db/credentials/get-password-change-timestamps.ts new file mode 100644 index 000000000..3d70a3934 --- /dev/null +++ b/frontends/nextjs/src/lib/db/credentials/get-password-change-timestamps.ts @@ -0,0 +1,18 @@ +import { prisma } from '../prisma' + +/** + * Get password change timestamps for all users + */ +export async function getPasswordChangeTimestamps(): Promise> { + const users = await prisma.user.findMany({ + where: { passwordChangeTimestamp: { not: null } }, + select: { username: true, passwordChangeTimestamp: true }, + }) + const result: Record = {} + for (const user of users) { + if (user.passwordChangeTimestamp) { + result[user.username] = Number(user.passwordChangeTimestamp) + } + } + return result +} diff --git a/frontends/nextjs/src/lib/db/credentials/get-password-reset-tokens.ts b/frontends/nextjs/src/lib/db/credentials/get-password-reset-tokens.ts new file mode 100644 index 000000000..ae69bcd23 --- /dev/null +++ b/frontends/nextjs/src/lib/db/credentials/get-password-reset-tokens.ts @@ -0,0 +1,9 @@ +import { prisma } from '../prisma' + +/** + * Get password reset tokens + */ +export async function getPasswordResetTokens(): Promise> { + const kv = await prisma.keyValue.findUnique({ where: { key: 'db_password_reset_tokens' } }) + return kv ? JSON.parse(kv.value) : {} +} diff --git a/frontends/nextjs/src/lib/db/credentials/index.ts b/frontends/nextjs/src/lib/db/credentials/index.ts new file mode 100644 index 000000000..9692cc285 --- /dev/null +++ b/frontends/nextjs/src/lib/db/credentials/index.ts @@ -0,0 +1,8 @@ +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' +export { getPasswordResetTokens } from './get-password-reset-tokens' +export { setPasswordResetToken } from './set-password-reset-token' +export { deletePasswordResetToken } from './delete-password-reset-token' diff --git a/frontends/nextjs/src/lib/db/credentials/set-credential.ts b/frontends/nextjs/src/lib/db/credentials/set-credential.ts new file mode 100644 index 000000000..aeffe8965 --- /dev/null +++ b/frontends/nextjs/src/lib/db/credentials/set-credential.ts @@ -0,0 +1,17 @@ +import { prisma } from '../prisma' + +/** + * Set or update a user's credential + */ +export async function setCredential(username: string, passwordHash: string): Promise { + await prisma.credential.upsert({ + where: { username }, + update: { passwordHash }, + create: { username, passwordHash }, + }) + + await prisma.user.update({ + where: { username }, + data: { passwordChangeTimestamp: BigInt(Date.now()) }, + }) +} diff --git a/frontends/nextjs/src/lib/db/credentials/set-password-change-timestamps.ts b/frontends/nextjs/src/lib/db/credentials/set-password-change-timestamps.ts new file mode 100644 index 000000000..0844e247c --- /dev/null +++ b/frontends/nextjs/src/lib/db/credentials/set-password-change-timestamps.ts @@ -0,0 +1,13 @@ +import { prisma } from '../prisma' + +/** + * Set password change timestamps for users + */ +export async function setPasswordChangeTimestamps(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/credentials/set-password-reset-token.ts b/frontends/nextjs/src/lib/db/credentials/set-password-reset-token.ts new file mode 100644 index 000000000..4bc278156 --- /dev/null +++ b/frontends/nextjs/src/lib/db/credentials/set-password-reset-token.ts @@ -0,0 +1,15 @@ +import { prisma } from '../prisma' +import { getPasswordResetTokens } from './get-password-reset-tokens' + +/** + * Set a password reset token for a user + */ +export async function setPasswordResetToken(username: string, token: string): Promise { + const tokens = await getPasswordResetTokens() + tokens[username] = token + await prisma.keyValue.upsert({ + where: { key: 'db_password_reset_tokens' }, + update: { value: JSON.stringify(tokens) }, + create: { key: 'db_password_reset_tokens', value: JSON.stringify(tokens) }, + }) +} diff --git a/frontends/nextjs/src/lib/db/credentials/verify-credentials.ts b/frontends/nextjs/src/lib/db/credentials/verify-credentials.ts new file mode 100644 index 000000000..a561d5f0c --- /dev/null +++ b/frontends/nextjs/src/lib/db/credentials/verify-credentials.ts @@ -0,0 +1,11 @@ +import { prisma } from '../prisma' +import { verifyPassword } from '../hash-password' + +/** + * Verify username/password combination + */ +export async function verifyCredentials(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/hash-password.ts b/frontends/nextjs/src/lib/db/hash-password.ts new file mode 100644 index 000000000..2cde75fd6 --- /dev/null +++ b/frontends/nextjs/src/lib/db/hash-password.ts @@ -0,0 +1,11 @@ +/** + * Hash a password using SHA-512 + */ +export async function hashPassword(password: string): Promise { + const encoder = new TextEncoder() + const data = encoder.encode(password) + const hashBuffer = await crypto.subtle.digest('SHA-512', data) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') + return hashHex +} diff --git a/frontends/nextjs/src/lib/db/index.ts b/frontends/nextjs/src/lib/db/index.ts new file mode 100644 index 000000000..afb2469cd --- /dev/null +++ b/frontends/nextjs/src/lib/db/index.ts @@ -0,0 +1,84 @@ +// Types +export type { CssCategory, DropdownConfig, DatabaseSchema, ComponentNode, ComponentConfig } from './types' + +// Core +export { hashPassword } from './hash-password' +export { verifyPassword } from './verify-password' +export { initializeDatabase } from './initialize-database' + +// Domain re-exports +export * from './users' +export * from './credentials' +export * from './workflows' +export * from './lua-scripts' +export * from './pages' +export * from './schemas' + +// Import all for namespace class +import { initializeDatabase } from './initialize-database' +import { hashPassword } from './hash-password' +import { verifyPassword } from './verify-password' +import * as users from './users' +import * as credentials from './credentials' +import * as workflows from './workflows' +import * as luaScripts from './lua-scripts' +import * as pages from './pages' +import * as schemas from './schemas' + +/** + * Database namespace class - groups all DB operations as static methods + * No instance state - pure function container for backward compatibility + */ +export class Database { + // Core + static initializeDatabase = initializeDatabase + static hashPassword = hashPassword + static verifyPassword = verifyPassword + + // Users + static getUsers = users.getUsers + static setUsers = users.setUsers + static addUser = users.addUser + static updateUser = users.updateUser + static deleteUser = users.deleteUser + static getSuperGod = users.getSuperGod + static transferSuperGodPower = users.transferSuperGodPower + + // Credentials + static getCredentials = credentials.getCredentials + static setCredential = credentials.setCredential + static verifyCredentials = credentials.verifyCredentials + static getPasswordChangeTimestamps = credentials.getPasswordChangeTimestamps + static setPasswordChangeTimestamps = credentials.setPasswordChangeTimestamps + static getPasswordResetTokens = credentials.getPasswordResetTokens + static setPasswordResetToken = credentials.setPasswordResetToken + static deletePasswordResetToken = credentials.deletePasswordResetToken + + // Workflows + static getWorkflows = workflows.getWorkflows + static setWorkflows = workflows.setWorkflows + static addWorkflow = workflows.addWorkflow + static updateWorkflow = workflows.updateWorkflow + static deleteWorkflow = workflows.deleteWorkflow + + // Lua Scripts + static getLuaScripts = luaScripts.getLuaScripts + static setLuaScripts = luaScripts.setLuaScripts + static addLuaScript = luaScripts.addLuaScript + static updateLuaScript = luaScripts.updateLuaScript + static deleteLuaScript = luaScripts.deleteLuaScript + + // Pages + static getPages = pages.getPages + static setPages = pages.setPages + static addPage = pages.addPage + static updatePage = pages.updatePage + static deletePage = pages.deletePage + + // Schemas + static getSchemas = schemas.getSchemas + static setSchemas = schemas.setSchemas + static addSchema = schemas.addSchema + static updateSchema = schemas.updateSchema + static deleteSchema = schemas.deleteSchema +} diff --git a/frontends/nextjs/src/lib/db/initialize-database.ts b/frontends/nextjs/src/lib/db/initialize-database.ts new file mode 100644 index 000000000..491eb2d1e --- /dev/null +++ b/frontends/nextjs/src/lib/db/initialize-database.ts @@ -0,0 +1,14 @@ +import { prisma } from '../prisma' + +/** + * Initialize database connection + */ +export async function initializeDatabase(): 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/lua-scripts/add-lua-script.ts b/frontends/nextjs/src/lib/db/lua-scripts/add-lua-script.ts new file mode 100644 index 000000000..570ca8a08 --- /dev/null +++ b/frontends/nextjs/src/lib/db/lua-scripts/add-lua-script.ts @@ -0,0 +1,18 @@ +import { prisma } from '../prisma' +import type { LuaScript } from '../../level-types' + +/** + * Add a Lua script + */ +export async function addLuaScript(script: LuaScript): Promise { + await prisma.luaScript.create({ + data: { + id: script.id, + name: script.name, + description: script.description, + code: script.code, + parameters: JSON.stringify(script.parameters), + returnType: script.returnType, + }, + }) +} diff --git a/frontends/nextjs/src/lib/db/lua-scripts/delete-lua-script.ts b/frontends/nextjs/src/lib/db/lua-scripts/delete-lua-script.ts new file mode 100644 index 000000000..0bddebb8e --- /dev/null +++ b/frontends/nextjs/src/lib/db/lua-scripts/delete-lua-script.ts @@ -0,0 +1,8 @@ +import { prisma } from '../prisma' + +/** + * Delete a Lua script by ID + */ +export async function deleteLuaScript(scriptId: string): Promise { + await prisma.luaScript.delete({ where: { id: scriptId } }) +} diff --git a/frontends/nextjs/src/lib/db/lua-scripts/get-lua-scripts.ts b/frontends/nextjs/src/lib/db/lua-scripts/get-lua-scripts.ts new file mode 100644 index 000000000..c81fe27d8 --- /dev/null +++ b/frontends/nextjs/src/lib/db/lua-scripts/get-lua-scripts.ts @@ -0,0 +1,17 @@ +import { prisma } from '../prisma' +import type { LuaScript } from '../../level-types' + +/** + * Get all Lua scripts + */ +export async function getLuaScripts(): Promise { + const scripts = await prisma.luaScript.findMany() + return scripts.map((s) => ({ + id: s.id, + name: s.name, + description: s.description || undefined, + code: s.code, + parameters: JSON.parse(s.parameters), + returnType: s.returnType || undefined, + })) +} diff --git a/frontends/nextjs/src/lib/db/lua-scripts/index.ts b/frontends/nextjs/src/lib/db/lua-scripts/index.ts new file mode 100644 index 000000000..89514e013 --- /dev/null +++ b/frontends/nextjs/src/lib/db/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/lua-scripts/set-lua-scripts.ts b/frontends/nextjs/src/lib/db/lua-scripts/set-lua-scripts.ts new file mode 100644 index 000000000..dacb6182c --- /dev/null +++ b/frontends/nextjs/src/lib/db/lua-scripts/set-lua-scripts.ts @@ -0,0 +1,21 @@ +import { prisma } from '../prisma' +import type { LuaScript } from '../../level-types' + +/** + * Set all Lua scripts (replaces existing) + */ +export async function setLuaScripts(scripts: LuaScript[]): Promise { + await prisma.luaScript.deleteMany() + for (const script of scripts) { + await prisma.luaScript.create({ + data: { + id: script.id, + name: script.name, + description: script.description, + code: script.code, + parameters: JSON.stringify(script.parameters), + returnType: script.returnType, + }, + }) + } +} diff --git a/frontends/nextjs/src/lib/db/lua-scripts/update-lua-script.ts b/frontends/nextjs/src/lib/db/lua-scripts/update-lua-script.ts new file mode 100644 index 000000000..4b8371f52 --- /dev/null +++ b/frontends/nextjs/src/lib/db/lua-scripts/update-lua-script.ts @@ -0,0 +1,19 @@ +import { prisma } from '../prisma' +import type { LuaScript } from '../../level-types' + +/** + * Update a Lua script by ID + */ +export async function updateLuaScript(scriptId: string, updates: Partial): Promise { + const data: any = {} + if (updates.name !== undefined) data.name = updates.name + if (updates.description !== undefined) data.description = updates.description + if (updates.code !== undefined) data.code = updates.code + if (updates.parameters !== undefined) data.parameters = JSON.stringify(updates.parameters) + if (updates.returnType !== undefined) data.returnType = updates.returnType + + await prisma.luaScript.update({ + where: { id: scriptId }, + data, + }) +} diff --git a/frontends/nextjs/src/lib/db/pages/add-page.ts b/frontends/nextjs/src/lib/db/pages/add-page.ts new file mode 100644 index 000000000..bb870cb74 --- /dev/null +++ b/frontends/nextjs/src/lib/db/pages/add-page.ts @@ -0,0 +1,19 @@ +import { prisma } from '../prisma' +import type { PageConfig } from '../../level-types' + +/** + * Add a page + */ +export async function addPage(page: PageConfig): Promise { + await prisma.pageConfig.create({ + data: { + id: page.id, + path: page.path, + title: page.title, + level: page.level, + componentTree: JSON.stringify(page.componentTree), + requiresAuth: page.requiresAuth, + requiredRole: page.requiredRole, + }, + }) +} diff --git a/frontends/nextjs/src/lib/db/pages/delete-page.ts b/frontends/nextjs/src/lib/db/pages/delete-page.ts new file mode 100644 index 000000000..9813b967b --- /dev/null +++ b/frontends/nextjs/src/lib/db/pages/delete-page.ts @@ -0,0 +1,8 @@ +import { prisma } from '../prisma' + +/** + * Delete a page by ID + */ +export async function deletePage(pageId: string): Promise { + await prisma.pageConfig.delete({ where: { id: pageId } }) +} diff --git a/frontends/nextjs/src/lib/db/pages/get-pages.ts b/frontends/nextjs/src/lib/db/pages/get-pages.ts new file mode 100644 index 000000000..811c4ccd3 --- /dev/null +++ b/frontends/nextjs/src/lib/db/pages/get-pages.ts @@ -0,0 +1,18 @@ +import { prisma } from '../prisma' +import type { PageConfig } from '../../level-types' + +/** + * Get all pages + */ +export async function getPages(): Promise { + const pages = await prisma.pageConfig.findMany() + return pages.map((p) => ({ + id: p.id, + path: p.path, + title: p.title, + level: p.level as any, + componentTree: JSON.parse(p.componentTree), + requiresAuth: p.requiresAuth, + requiredRole: (p.requiredRole as any) || undefined, + })) +} diff --git a/frontends/nextjs/src/lib/db/pages/index.ts b/frontends/nextjs/src/lib/db/pages/index.ts new file mode 100644 index 000000000..87ebb83eb --- /dev/null +++ b/frontends/nextjs/src/lib/db/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/pages/set-pages.ts b/frontends/nextjs/src/lib/db/pages/set-pages.ts new file mode 100644 index 000000000..61b60bd71 --- /dev/null +++ b/frontends/nextjs/src/lib/db/pages/set-pages.ts @@ -0,0 +1,22 @@ +import { prisma } from '../prisma' +import type { PageConfig } from '../../level-types' + +/** + * Set all pages (replaces existing) + */ +export async function setPages(pages: PageConfig[]): Promise { + await prisma.pageConfig.deleteMany() + for (const page of pages) { + await prisma.pageConfig.create({ + data: { + id: page.id, + path: page.path, + title: page.title, + level: page.level, + componentTree: JSON.stringify(page.componentTree), + requiresAuth: page.requiresAuth, + requiredRole: page.requiredRole, + }, + }) + } +} diff --git a/frontends/nextjs/src/lib/db/pages/update-page.ts b/frontends/nextjs/src/lib/db/pages/update-page.ts new file mode 100644 index 000000000..a35e90444 --- /dev/null +++ b/frontends/nextjs/src/lib/db/pages/update-page.ts @@ -0,0 +1,20 @@ +import { prisma } from '../prisma' +import type { PageConfig } from '../../level-types' + +/** + * Update a page by ID + */ +export async function updatePage(pageId: string, updates: Partial): Promise { + const data: any = {} + if (updates.path !== undefined) data.path = updates.path + if (updates.title !== undefined) data.title = updates.title + if (updates.level !== undefined) data.level = updates.level + if (updates.componentTree !== undefined) data.componentTree = JSON.stringify(updates.componentTree) + if (updates.requiresAuth !== undefined) data.requiresAuth = updates.requiresAuth + if (updates.requiredRole !== undefined) data.requiredRole = updates.requiredRole + + await prisma.pageConfig.update({ + where: { id: pageId }, + data, + }) +} diff --git a/frontends/nextjs/src/lib/db/prisma.ts b/frontends/nextjs/src/lib/db/prisma.ts new file mode 100644 index 000000000..909ab84ed --- /dev/null +++ b/frontends/nextjs/src/lib/db/prisma.ts @@ -0,0 +1,4 @@ +/** + * Symlink to actual prisma client for db folder imports + */ +export { prisma } from '../prisma' diff --git a/frontends/nextjs/src/lib/db/schemas/add-schema.ts b/frontends/nextjs/src/lib/db/schemas/add-schema.ts new file mode 100644 index 000000000..25d1bb936 --- /dev/null +++ b/frontends/nextjs/src/lib/db/schemas/add-schema.ts @@ -0,0 +1,21 @@ +import { prisma } from '../prisma' +import type { ModelSchema } from '../../schema-types' + +/** + * Add a schema + */ +export async function addSchema(schema: ModelSchema): Promise { + await prisma.modelSchema.create({ + data: { + name: schema.name, + label: schema.label, + labelPlural: schema.labelPlural, + icon: schema.icon, + fields: JSON.stringify(schema.fields), + listDisplay: schema.listDisplay ? JSON.stringify(schema.listDisplay) : null, + listFilter: schema.listFilter ? JSON.stringify(schema.listFilter) : null, + searchFields: schema.searchFields ? JSON.stringify(schema.searchFields) : null, + ordering: schema.ordering ? JSON.stringify(schema.ordering) : null, + }, + }) +} diff --git a/frontends/nextjs/src/lib/db/schemas/delete-schema.ts b/frontends/nextjs/src/lib/db/schemas/delete-schema.ts new file mode 100644 index 000000000..edff94836 --- /dev/null +++ b/frontends/nextjs/src/lib/db/schemas/delete-schema.ts @@ -0,0 +1,8 @@ +import { prisma } from '../prisma' + +/** + * Delete a schema by name + */ +export async function deleteSchema(schemaName: string): Promise { + await prisma.modelSchema.delete({ where: { name: schemaName } }) +} diff --git a/frontends/nextjs/src/lib/db/schemas/get-schemas.ts b/frontends/nextjs/src/lib/db/schemas/get-schemas.ts new file mode 100644 index 000000000..24a52ee74 --- /dev/null +++ b/frontends/nextjs/src/lib/db/schemas/get-schemas.ts @@ -0,0 +1,20 @@ +import { prisma } from '../prisma' +import type { ModelSchema } from '../../schema-types' + +/** + * Get all schemas + */ +export async function getSchemas(): Promise { + const schemas = await prisma.modelSchema.findMany() + return schemas.map((s) => ({ + name: s.name, + label: s.label || undefined, + labelPlural: s.labelPlural || undefined, + icon: s.icon || undefined, + fields: JSON.parse(s.fields), + listDisplay: s.listDisplay ? JSON.parse(s.listDisplay) : undefined, + listFilter: s.listFilter ? JSON.parse(s.listFilter) : undefined, + searchFields: s.searchFields ? JSON.parse(s.searchFields) : undefined, + ordering: s.ordering ? JSON.parse(s.ordering) : undefined, + })) +} diff --git a/frontends/nextjs/src/lib/db/schemas/index.ts b/frontends/nextjs/src/lib/db/schemas/index.ts new file mode 100644 index 000000000..a5c44cf69 --- /dev/null +++ b/frontends/nextjs/src/lib/db/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/schemas/set-schemas.ts b/frontends/nextjs/src/lib/db/schemas/set-schemas.ts new file mode 100644 index 000000000..0a5fbff09 --- /dev/null +++ b/frontends/nextjs/src/lib/db/schemas/set-schemas.ts @@ -0,0 +1,24 @@ +import { prisma } from '../prisma' +import type { ModelSchema } from '../../schema-types' + +/** + * Set all schemas (replaces existing) + */ +export async function setSchemas(schemas: ModelSchema[]): Promise { + await prisma.modelSchema.deleteMany() + for (const schema of schemas) { + await prisma.modelSchema.create({ + data: { + name: schema.name, + label: schema.label, + labelPlural: schema.labelPlural, + icon: schema.icon, + fields: JSON.stringify(schema.fields), + listDisplay: schema.listDisplay ? JSON.stringify(schema.listDisplay) : null, + listFilter: schema.listFilter ? JSON.stringify(schema.listFilter) : null, + searchFields: schema.searchFields ? JSON.stringify(schema.searchFields) : null, + ordering: schema.ordering ? JSON.stringify(schema.ordering) : null, + }, + }) + } +} diff --git a/frontends/nextjs/src/lib/db/schemas/update-schema.ts b/frontends/nextjs/src/lib/db/schemas/update-schema.ts new file mode 100644 index 000000000..8823a5b9b --- /dev/null +++ b/frontends/nextjs/src/lib/db/schemas/update-schema.ts @@ -0,0 +1,22 @@ +import { prisma } from '../prisma' +import type { ModelSchema } from '../../schema-types' + +/** + * Update a schema by name + */ +export async function updateSchema(schemaName: string, updates: Partial): Promise { + const data: any = {} + if (updates.label !== undefined) data.label = updates.label + if (updates.labelPlural !== undefined) data.labelPlural = updates.labelPlural + if (updates.icon !== undefined) data.icon = updates.icon + if (updates.fields !== undefined) data.fields = JSON.stringify(updates.fields) + if (updates.listDisplay !== undefined) data.listDisplay = JSON.stringify(updates.listDisplay) + if (updates.listFilter !== undefined) data.listFilter = JSON.stringify(updates.listFilter) + if (updates.searchFields !== undefined) data.searchFields = JSON.stringify(updates.searchFields) + if (updates.ordering !== undefined) data.ordering = JSON.stringify(updates.ordering) + + await prisma.modelSchema.update({ + where: { name: schemaName }, + data, + }) +} diff --git a/frontends/nextjs/src/lib/db/types.ts b/frontends/nextjs/src/lib/db/types.ts new file mode 100644 index 000000000..f5b613eef --- /dev/null +++ b/frontends/nextjs/src/lib/db/types.ts @@ -0,0 +1,98 @@ +/** + * CSS category configuration + */ +export interface CssCategory { + name: string + classes: string[] +} + +/** + * Dropdown configuration + */ +export interface DropdownConfig { + id: string + name: string + label: string + options: Array<{ value: string; label: string }> +} + +/** + * Component node in hierarchy + */ +export interface ComponentNode { + id: string + type: string + parentId?: string + childIds: string[] + order: number + pageId: string +} + +/** + * Component configuration + */ +export interface ComponentConfig { + id: string + componentId: string + props: Record + styles: Record + events: Record + conditionalRendering?: { + condition: string + luaScriptId?: string + } +} + +/** + * Full database schema type + */ +export interface DatabaseSchema { + users: import('../level-types').User[] + credentials: Record + workflows: import('../level-types').Workflow[] + luaScripts: import('../level-types').LuaScript[] + pages: import('../level-types').PageConfig[] + schemas: import('../schema-types').ModelSchema[] + appConfig: import('../level-types').AppConfiguration + comments: import('../level-types').Comment[] + componentHierarchy: Record + componentConfigs: Record + godCredentialsExpiry: number + passwordChangeTimestamps: Record + firstLoginFlags: Record + godCredentialsExpiryDuration: number + cssClasses: CssCategory[] + dropdownConfigs: DropdownConfig[] + tenants: import('../level-types').Tenant[] + powerTransferRequests: import('../level-types').PowerTransferRequest[] + smtpConfig: import('../password').SMTPConfig + passwordResetTokens: Record +} + +/** + * Database keys enum + */ +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 diff --git a/frontends/nextjs/src/lib/db/users/add-user.ts b/frontends/nextjs/src/lib/db/users/add-user.ts new file mode 100644 index 000000000..ba0f95ed9 --- /dev/null +++ b/frontends/nextjs/src/lib/db/users/add-user.ts @@ -0,0 +1,21 @@ +import { prisma } from '../prisma' +import type { User } from '../level-types' + +/** + * Add a single user + */ +export async function addUser(user: User): Promise { + await prisma.user.create({ + data: { + id: user.id, + username: user.username, + email: user.email, + role: user.role, + profilePicture: user.profilePicture, + bio: user.bio, + createdAt: BigInt(user.createdAt), + tenantId: user.tenantId, + isInstanceOwner: user.isInstanceOwner ?? false, + }, + }) +} diff --git a/frontends/nextjs/src/lib/db/users/delete-user.ts b/frontends/nextjs/src/lib/db/users/delete-user.ts new file mode 100644 index 000000000..191d9f6b7 --- /dev/null +++ b/frontends/nextjs/src/lib/db/users/delete-user.ts @@ -0,0 +1,8 @@ +import { prisma } from '../prisma' + +/** + * Delete a user by ID + */ +export async function deleteUser(userId: string): Promise { + await prisma.user.delete({ where: { id: userId } }) +} diff --git a/frontends/nextjs/src/lib/db/users/get-super-god.ts b/frontends/nextjs/src/lib/db/users/get-super-god.ts new file mode 100644 index 000000000..fe71bc12f --- /dev/null +++ b/frontends/nextjs/src/lib/db/users/get-super-god.ts @@ -0,0 +1,25 @@ +import { prisma } from '../prisma' +import type { User } from '../level-types' + +/** + * Get the SuperGod user (instance owner) + */ +export async function getSuperGod(): Promise { + const user = await prisma.user.findFirst({ + where: { isInstanceOwner: true }, + }) + + 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, + } +} diff --git a/frontends/nextjs/src/lib/db/users/get-users.ts b/frontends/nextjs/src/lib/db/users/get-users.ts new file mode 100644 index 000000000..3f1c8ee24 --- /dev/null +++ b/frontends/nextjs/src/lib/db/users/get-users.ts @@ -0,0 +1,20 @@ +import { prisma } from '../prisma' +import type { User } from '../level-types' + +/** + * Get all users from database + */ +export async function getUsers(): Promise { + const users = await prisma.user.findMany() + return users.map((u) => ({ + id: u.id, + username: u.username, + email: u.email, + role: u.role as any, + profilePicture: u.profilePicture || undefined, + bio: u.bio || undefined, + createdAt: Number(u.createdAt), + tenantId: u.tenantId || undefined, + isInstanceOwner: u.isInstanceOwner, + })) +} diff --git a/frontends/nextjs/src/lib/db/users/index.ts b/frontends/nextjs/src/lib/db/users/index.ts new file mode 100644 index 000000000..43185da2b --- /dev/null +++ b/frontends/nextjs/src/lib/db/users/index.ts @@ -0,0 +1,7 @@ +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' +export { getSuperGod } from './get-super-god' +export { transferSuperGodPower } from './transfer-super-god-power' diff --git a/frontends/nextjs/src/lib/db/users/set-users.ts b/frontends/nextjs/src/lib/db/users/set-users.ts new file mode 100644 index 000000000..91f9c74e1 --- /dev/null +++ b/frontends/nextjs/src/lib/db/users/set-users.ts @@ -0,0 +1,26 @@ +import { prisma } from '../prisma' +import type { User } from '../level-types' + +/** + * Set all users (replaces existing) + */ +export async function setUsers(users: User[]): Promise { + await prisma.$transaction(async (tx) => { + await tx.user.deleteMany() + for (const user of users) { + await tx.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, + }, + }) + } + }) +} diff --git a/frontends/nextjs/src/lib/db/users/transfer-super-god-power.ts b/frontends/nextjs/src/lib/db/users/transfer-super-god-power.ts new file mode 100644 index 000000000..0714db159 --- /dev/null +++ b/frontends/nextjs/src/lib/db/users/transfer-super-god-power.ts @@ -0,0 +1,17 @@ +import { prisma } from '../prisma' + +/** + * Transfer SuperGod power from one user to another + */ +export async function transferSuperGodPower(fromUserId: string, toUserId: string): Promise { + await prisma.$transaction([ + prisma.user.update({ + where: { id: fromUserId }, + data: { isInstanceOwner: false, role: 'god' }, + }), + prisma.user.update({ + where: { id: toUserId }, + data: { isInstanceOwner: true, role: 'supergod' }, + }), + ]) +} diff --git a/frontends/nextjs/src/lib/db/users/update-user.ts b/frontends/nextjs/src/lib/db/users/update-user.ts new file mode 100644 index 000000000..d4de8606b --- /dev/null +++ b/frontends/nextjs/src/lib/db/users/update-user.ts @@ -0,0 +1,20 @@ +import { prisma } from '../prisma' +import type { User } from '../level-types' + +/** + * Update a user by ID + */ +export async function updateUser(userId: string, updates: Partial): Promise { + await prisma.user.update({ + where: { id: userId }, + data: { + username: updates.username, + email: updates.email, + role: updates.role, + profilePicture: updates.profilePicture, + bio: updates.bio, + tenantId: updates.tenantId, + isInstanceOwner: updates.isInstanceOwner, + }, + }) +} diff --git a/frontends/nextjs/src/lib/db/verify-password.ts b/frontends/nextjs/src/lib/db/verify-password.ts new file mode 100644 index 000000000..a479ccb81 --- /dev/null +++ b/frontends/nextjs/src/lib/db/verify-password.ts @@ -0,0 +1,9 @@ +import { hashPassword } from './hash-password' + +/** + * Verify a password against a hash + */ +export async function verifyPassword(password: string, hash: string): Promise { + const inputHash = await hashPassword(password) + return inputHash === hash +} diff --git a/frontends/nextjs/src/lib/db/workflows/add-workflow.ts b/frontends/nextjs/src/lib/db/workflows/add-workflow.ts new file mode 100644 index 000000000..5fe1a9257 --- /dev/null +++ b/frontends/nextjs/src/lib/db/workflows/add-workflow.ts @@ -0,0 +1,18 @@ +import { prisma } from '../prisma' +import type { Workflow } from '../../level-types' + +/** + * Add a workflow + */ +export async function addWorkflow(workflow: Workflow): Promise { + await prisma.workflow.create({ + data: { + id: workflow.id, + name: workflow.name, + description: workflow.description, + nodes: JSON.stringify(workflow.nodes), + edges: JSON.stringify(workflow.edges), + enabled: workflow.enabled, + }, + }) +} diff --git a/frontends/nextjs/src/lib/db/workflows/delete-workflow.ts b/frontends/nextjs/src/lib/db/workflows/delete-workflow.ts new file mode 100644 index 000000000..1c63ec494 --- /dev/null +++ b/frontends/nextjs/src/lib/db/workflows/delete-workflow.ts @@ -0,0 +1,8 @@ +import { prisma } from '../prisma' + +/** + * Delete a workflow by ID + */ +export async function deleteWorkflow(workflowId: string): Promise { + await prisma.workflow.delete({ where: { id: workflowId } }) +} diff --git a/frontends/nextjs/src/lib/db/workflows/get-workflows.ts b/frontends/nextjs/src/lib/db/workflows/get-workflows.ts new file mode 100644 index 000000000..ec8aa5f73 --- /dev/null +++ b/frontends/nextjs/src/lib/db/workflows/get-workflows.ts @@ -0,0 +1,17 @@ +import { prisma } from '../prisma' +import type { Workflow } from '../../level-types' + +/** + * Get all workflows + */ +export async function getWorkflows(): Promise { + const workflows = await prisma.workflow.findMany() + return workflows.map((w) => ({ + id: w.id, + name: w.name, + description: w.description || undefined, + nodes: JSON.parse(w.nodes), + edges: JSON.parse(w.edges), + enabled: w.enabled, + })) +} diff --git a/frontends/nextjs/src/lib/db/workflows/index.ts b/frontends/nextjs/src/lib/db/workflows/index.ts new file mode 100644 index 000000000..35db1eaaa --- /dev/null +++ b/frontends/nextjs/src/lib/db/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/workflows/set-workflows.ts b/frontends/nextjs/src/lib/db/workflows/set-workflows.ts new file mode 100644 index 000000000..c4e41706c --- /dev/null +++ b/frontends/nextjs/src/lib/db/workflows/set-workflows.ts @@ -0,0 +1,21 @@ +import { prisma } from '../prisma' +import type { Workflow } from '../../level-types' + +/** + * Set all workflows (replaces existing) + */ +export async function setWorkflows(workflows: Workflow[]): Promise { + await prisma.workflow.deleteMany() + for (const workflow of workflows) { + await prisma.workflow.create({ + data: { + id: workflow.id, + name: workflow.name, + description: workflow.description, + nodes: JSON.stringify(workflow.nodes), + edges: JSON.stringify(workflow.edges), + enabled: workflow.enabled, + }, + }) + } +} diff --git a/frontends/nextjs/src/lib/db/workflows/update-workflow.ts b/frontends/nextjs/src/lib/db/workflows/update-workflow.ts new file mode 100644 index 000000000..cade461ae --- /dev/null +++ b/frontends/nextjs/src/lib/db/workflows/update-workflow.ts @@ -0,0 +1,19 @@ +import { prisma } from '../prisma' +import type { Workflow } from '../../level-types' + +/** + * Update a workflow by ID + */ +export async function updateWorkflow(workflowId: string, updates: Partial): Promise { + const data: any = {} + if (updates.name !== undefined) data.name = updates.name + if (updates.description !== undefined) data.description = updates.description + if (updates.nodes !== undefined) data.nodes = JSON.stringify(updates.nodes) + if (updates.edges !== undefined) data.edges = JSON.stringify(updates.edges) + if (updates.enabled !== undefined) data.enabled = updates.enabled + + await prisma.workflow.update({ + where: { id: workflowId }, + data, + }) +} diff --git a/frontends/nextjs/src/lib/package-loader.ts b/frontends/nextjs/src/lib/package-loader.ts index d6dbbfab8..1ee9df9d6 100644 --- a/frontends/nextjs/src/lib/package-loader.ts +++ b/frontends/nextjs/src/lib/package-loader.ts @@ -1,123 +1,15 @@ /** * Package Loader Module - * - * Handles the initialization and loading of the modular package system. - * Discovers packages from the /packages directory, builds a registry, - * and exports seed data for components, scripts, and metadata. - * - * Supports both modular packages (new) and legacy packages from catalog. + * @deprecated Import from '@/lib/package-loader' folder instead */ -import { PACKAGE_CATALOG } from './package-catalog' -import { loadPackageComponents } from './declarative-component-renderer' -import { buildPackageRegistry, exportAllPackagesForSeed, type PackageRegistry } from './package-glue' - -// Track initialization state to prevent duplicate loading -let isInitialized = false -// Cache the package registry after first load -let packageRegistry: PackageRegistry | null = null -type ModularPackageSeedData = { - components: any[] - scripts: any[] - packages: any[] -} - -const emptyModularPackageSeedData: ModularPackageSeedData = { - components: [], - scripts: [], - packages: [] -} - -let modularPackageSeedData: ModularPackageSeedData | null = null - -/** - * Initializes the package system by loading all available packages - * This function is idempotent - calling multiple times is safe - * - * Steps: - * 1. Check if already initialized (return early if so) - * 2. Build package registry from /packages directory - * 3. Extract and export seed data - * 4. Load package components into renderer - * 5. Load legacy packages from catalog - * - * @async - * @returns {Promise} - */ -export async function initializePackageSystem() { - if (isInitialized) return - - // Load modular packages from /packages folder structure - try { - // Build registry with all packages found in /packages - packageRegistry = await buildPackageRegistry() - - // Extract seed data from modular packages (components, scripts, metadata) - const seedData = exportAllPackagesForSeed(packageRegistry) - modularPackageSeedData = seedData - - // TODO: Replace with proper persistent storage (currently no-op) - // Modular package data would be stored in database or KV store - // await Database.setModularPackageComponents(seedData.components) - // await Database.setModularPackageScripts(seedData.scripts) - // await Database.setModularPackageMetadata(seedData.packages) - console.log('Loaded modular package data:', { - components: seedData.components?.length || 0, - scripts: seedData.scripts?.length || 0, - packages: seedData.packages?.length || 0 - }) - - console.log(`✅ Loaded ${seedData.packages.length} modular packages:`, - seedData.packages.map(p => p.name).join(', ')) - } catch (error) { - console.warn('⚠️ Could not load modular packages:', error) - } - - // Load legacy packages from catalog for backward compatibility - Object.values(PACKAGE_CATALOG).forEach(pkg => { - if (pkg.content) { - loadPackageComponents(pkg.content) - } - }) - - isInitialized = true -} - -export function getInstalledPackageIds(): string[] { - return Object.keys(PACKAGE_CATALOG) -} - -export function getPackageContent(packageId: string) { - const pkg = PACKAGE_CATALOG[packageId] - return pkg ? pkg.content : null -} - -export function getPackageManifest(packageId: string) { - const pkg = PACKAGE_CATALOG[packageId] - return pkg ? pkg.manifest : null -} - -export function getPackageRegistry(): PackageRegistry | null { - return packageRegistry -} - -export async function getModularPackageComponents() { - // TODO: Replace with proper database query - // return await Database.getModularPackageComponents() || [] - await initializePackageSystem() - return (modularPackageSeedData ?? emptyModularPackageSeedData).components -} - -export async function getModularPackageScripts() { - // TODO: Replace with proper database query - // return await Database.getModularPackageScripts() || [] - await initializePackageSystem() - return (modularPackageSeedData ?? emptyModularPackageSeedData).scripts -} - -export async function getModularPackageMetadata() { - // TODO: Replace with proper database query - // return await Database.getModularPackageMetadata() || [] - await initializePackageSystem() - return (modularPackageSeedData ?? emptyModularPackageSeedData).packages -} +export { + initializePackageSystem, + getInstalledPackageIds, + getPackageContent, + getPackageManifest, + getPackageRegistry, + getModularPackageComponents, + getModularPackageScripts, + getModularPackageMetadata, +} from './package-loader/index' diff --git a/frontends/nextjs/src/lib/package-loader/get-installed-package-ids.ts b/frontends/nextjs/src/lib/package-loader/get-installed-package-ids.ts new file mode 100644 index 000000000..ed3bee05a --- /dev/null +++ b/frontends/nextjs/src/lib/package-loader/get-installed-package-ids.ts @@ -0,0 +1,8 @@ +import { PACKAGE_CATALOG } from '../package-catalog' + +/** + * Get list of all installed package IDs from the catalog + */ +export function getInstalledPackageIds(): string[] { + return Object.keys(PACKAGE_CATALOG) +} diff --git a/frontends/nextjs/src/lib/package-loader/get-modular-package-components.ts b/frontends/nextjs/src/lib/package-loader/get-modular-package-components.ts new file mode 100644 index 000000000..3b1ca7428 --- /dev/null +++ b/frontends/nextjs/src/lib/package-loader/get-modular-package-components.ts @@ -0,0 +1,9 @@ +import { initializePackageSystem, getModularSeedData } from './initialize-package-system' + +/** + * Get modular package components + */ +export async function getModularPackageComponents(): Promise { + await initializePackageSystem() + return getModularSeedData().components +} diff --git a/frontends/nextjs/src/lib/package-loader/get-modular-package-metadata.ts b/frontends/nextjs/src/lib/package-loader/get-modular-package-metadata.ts new file mode 100644 index 000000000..73d797823 --- /dev/null +++ b/frontends/nextjs/src/lib/package-loader/get-modular-package-metadata.ts @@ -0,0 +1,9 @@ +import { initializePackageSystem, getModularSeedData } from './initialize-package-system' + +/** + * Get modular package metadata + */ +export async function getModularPackageMetadata(): Promise { + await initializePackageSystem() + return getModularSeedData().packages +} diff --git a/frontends/nextjs/src/lib/package-loader/get-modular-package-scripts.ts b/frontends/nextjs/src/lib/package-loader/get-modular-package-scripts.ts new file mode 100644 index 000000000..f37a452df --- /dev/null +++ b/frontends/nextjs/src/lib/package-loader/get-modular-package-scripts.ts @@ -0,0 +1,9 @@ +import { initializePackageSystem, getModularSeedData } from './initialize-package-system' + +/** + * Get modular package scripts + */ +export async function getModularPackageScripts(): Promise { + await initializePackageSystem() + return getModularSeedData().scripts +} diff --git a/frontends/nextjs/src/lib/package-loader/get-package-content.ts b/frontends/nextjs/src/lib/package-loader/get-package-content.ts new file mode 100644 index 000000000..1bb6295d9 --- /dev/null +++ b/frontends/nextjs/src/lib/package-loader/get-package-content.ts @@ -0,0 +1,9 @@ +import { PACKAGE_CATALOG } from '../package-catalog' + +/** + * Get the content of a package by its ID + */ +export function getPackageContent(packageId: string) { + const pkg = PACKAGE_CATALOG[packageId] + return pkg ? pkg.content : null +} diff --git a/frontends/nextjs/src/lib/package-loader/get-package-manifest.ts b/frontends/nextjs/src/lib/package-loader/get-package-manifest.ts new file mode 100644 index 000000000..ff201aad7 --- /dev/null +++ b/frontends/nextjs/src/lib/package-loader/get-package-manifest.ts @@ -0,0 +1,9 @@ +import { PACKAGE_CATALOG } from '../package-catalog' + +/** + * Get the manifest of a package by its ID + */ +export function getPackageManifest(packageId: string) { + const pkg = PACKAGE_CATALOG[packageId] + return pkg ? pkg.manifest : null +} diff --git a/frontends/nextjs/src/lib/package-loader/get-package-registry.ts b/frontends/nextjs/src/lib/package-loader/get-package-registry.ts new file mode 100644 index 000000000..7e90e2724 --- /dev/null +++ b/frontends/nextjs/src/lib/package-loader/get-package-registry.ts @@ -0,0 +1,20 @@ +import type { PackageRegistry } from '../package-glue' + +/** + * Cached package registry singleton + */ +let packageRegistry: PackageRegistry | null = null + +/** + * Get the current package registry + */ +export function getPackageRegistry(): PackageRegistry | null { + return packageRegistry +} + +/** + * Set the package registry (called during initialization) + */ +export function setPackageRegistry(registry: PackageRegistry | null): void { + packageRegistry = registry +} diff --git a/frontends/nextjs/src/lib/package-loader/index.ts b/frontends/nextjs/src/lib/package-loader/index.ts new file mode 100644 index 000000000..a11a79853 --- /dev/null +++ b/frontends/nextjs/src/lib/package-loader/index.ts @@ -0,0 +1,9 @@ +export { initializePackageSystem } from './initialize-package-system' +export { getInstalledPackageIds } from './get-installed-package-ids' +export { getPackageContent } from './get-package-content' +export { getPackageManifest } from './get-package-manifest' +export { getPackageRegistry } from './get-package-registry' +export { getModularPackageComponents } from './get-modular-package-components' +export { getModularPackageScripts } from './get-modular-package-scripts' +export { getModularPackageMetadata } from './get-modular-package-metadata' +export type { ModularPackageSeedData } from './modular-package-seed-data' diff --git a/frontends/nextjs/src/lib/package-loader/initialize-package-system.ts b/frontends/nextjs/src/lib/package-loader/initialize-package-system.ts new file mode 100644 index 000000000..e8475bdfe --- /dev/null +++ b/frontends/nextjs/src/lib/package-loader/initialize-package-system.ts @@ -0,0 +1,54 @@ +import { PACKAGE_CATALOG } from '../package-catalog' +import { loadPackageComponents } from '../declarative-component-renderer' +import { buildPackageRegistry, exportAllPackagesForSeed } from '../package-glue' +import { setPackageRegistry } from './get-package-registry' +import { emptyModularPackageSeedData, type ModularPackageSeedData } from './modular-package-seed-data' + +let isInitialized = false +let modularPackageSeedData: ModularPackageSeedData | null = null + +/** + * Get cached modular package seed data + */ +export function getModularSeedData(): ModularPackageSeedData { + return modularPackageSeedData ?? emptyModularPackageSeedData +} + +/** + * Initializes the package system by loading all available packages + * This function is idempotent - calling multiple times is safe + */ +export async function initializePackageSystem(): Promise { + if (isInitialized) return + + // Load modular packages from /packages folder structure + try { + const packageRegistry = await buildPackageRegistry() + setPackageRegistry(packageRegistry) + + const seedData = exportAllPackagesForSeed(packageRegistry) + modularPackageSeedData = seedData + + console.log('Loaded modular package data:', { + components: seedData.components?.length || 0, + scripts: seedData.scripts?.length || 0, + packages: seedData.packages?.length || 0, + }) + + console.log( + `✅ Loaded ${seedData.packages.length} modular packages:`, + seedData.packages.map((p) => p.name).join(', ') + ) + } catch (error) { + console.warn('⚠️ Could not load modular packages:', error) + } + + // Load legacy packages from catalog for backward compatibility + Object.values(PACKAGE_CATALOG).forEach((pkg) => { + if (pkg.content) { + loadPackageComponents(pkg.content) + } + }) + + isInitialized = true +} diff --git a/frontends/nextjs/src/lib/package-loader/modular-package-seed-data.ts b/frontends/nextjs/src/lib/package-loader/modular-package-seed-data.ts new file mode 100644 index 000000000..f5374ef91 --- /dev/null +++ b/frontends/nextjs/src/lib/package-loader/modular-package-seed-data.ts @@ -0,0 +1,17 @@ +/** + * Type for modular package seed data + */ +export type ModularPackageSeedData = { + components: any[] + scripts: any[] + packages: any[] +} + +/** + * Empty seed data default + */ +export const emptyModularPackageSeedData: ModularPackageSeedData = { + components: [], + scripts: [], + packages: [], +} diff --git a/frontends/nextjs/src/lib/password-utils.ts b/frontends/nextjs/src/lib/password-utils.ts index 012561b7a..7cf73abff 100644 --- a/frontends/nextjs/src/lib/password-utils.ts +++ b/frontends/nextjs/src/lib/password-utils.ts @@ -1,78 +1,10 @@ -export function generateScrambledPassword(length: number = 16): string { - const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*' - const array = new Uint8Array(length) - crypto.getRandomValues(array) - - let password = '' - for (let i = 0; i < length; i++) { - password += charset[array[i] % charset.length] - } - - return password -} - -export function generateDeterministicScrambledPassword(seed: string, length: number = 16): string { - const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*' - let hash = 0x811c9dc5 - - for (let i = 0; i < seed.length; i++) { - hash ^= seed.charCodeAt(i) - hash = Math.imul(hash, 0x01000193) >>> 0 - } - - let state = hash || 1 - let password = '' - - for (let i = 0; i < length; i++) { - state = (state + 0x6d2b79f5) >>> 0 - let t = state - t = Math.imul(t ^ (t >>> 15), t | 1) - t ^= t + Math.imul(t ^ (t >>> 7), t | 61) - const rand = ((t ^ (t >>> 14)) >>> 0) / 4294967296 - password += charset[Math.floor(rand * charset.length)] - } - - return password -} - -export async function simulateEmailSend( - to: string, - subject: string, - body: string, - smtpConfig?: SMTPConfig -): Promise<{ success: boolean; message: string }> { - console.log('=== EMAIL SIMULATION ===') - console.log(`To: ${to}`) - console.log(`Subject: ${subject}`) - console.log(`Body:\n${body}`) - if (smtpConfig) { - console.log(`SMTP Host: ${smtpConfig.host}:${smtpConfig.port}`) - console.log(`From: ${smtpConfig.fromEmail}`) - } - console.log('========================') - - return { - success: true, - message: 'Email simulated successfully (check console)' - } -} - -export interface SMTPConfig { - host: string - port: number - secure: boolean - username: string - password: string - fromEmail: string - fromName: string -} - -export const DEFAULT_SMTP_CONFIG: SMTPConfig = { - host: 'smtp.example.com', - port: 587, - secure: false, - username: '', - password: '', - fromEmail: 'noreply@metabuilder.com', - fromName: 'MetaBuilder System', -} +/** + * @deprecated Import from '@/lib/password' instead + */ +export { + generateScrambledPassword, + generateDeterministicScrambledPassword, + simulateEmailSend, + DEFAULT_SMTP_CONFIG, +} from './password' +export type { SMTPConfig } from './password' diff --git a/frontends/nextjs/src/lib/password/default-smtp-config.ts b/frontends/nextjs/src/lib/password/default-smtp-config.ts new file mode 100644 index 000000000..6defcee2c --- /dev/null +++ b/frontends/nextjs/src/lib/password/default-smtp-config.ts @@ -0,0 +1,14 @@ +import type { SMTPConfig } from './smtp-config' + +/** + * Default SMTP configuration + */ +export const DEFAULT_SMTP_CONFIG: SMTPConfig = { + host: 'smtp.example.com', + port: 587, + secure: false, + username: '', + password: '', + fromEmail: 'noreply@metabuilder.com', + fromName: 'MetaBuilder System', +} diff --git a/frontends/nextjs/src/lib/password/generate-deterministic-password.ts b/frontends/nextjs/src/lib/password/generate-deterministic-password.ts new file mode 100644 index 000000000..eeb90a587 --- /dev/null +++ b/frontends/nextjs/src/lib/password/generate-deterministic-password.ts @@ -0,0 +1,26 @@ +/** + * Generate a deterministic password from a seed string using FNV-1a hash + */ +export function generateDeterministicScrambledPassword(seed: string, length: number = 16): string { + const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*' + let hash = 0x811c9dc5 + + for (let i = 0; i < seed.length; i++) { + hash ^= seed.charCodeAt(i) + hash = Math.imul(hash, 0x01000193) >>> 0 + } + + let state = hash || 1 + let password = '' + + for (let i = 0; i < length; i++) { + state = (state + 0x6d2b79f5) >>> 0 + let t = state + t = Math.imul(t ^ (t >>> 15), t | 1) + t ^= t + Math.imul(t ^ (t >>> 7), t | 61) + const rand = ((t ^ (t >>> 14)) >>> 0) / 4294967296 + password += charset[Math.floor(rand * charset.length)] + } + + return password +} diff --git a/frontends/nextjs/src/lib/password/generate-scrambled-password.ts b/frontends/nextjs/src/lib/password/generate-scrambled-password.ts new file mode 100644 index 000000000..3b73af4d9 --- /dev/null +++ b/frontends/nextjs/src/lib/password/generate-scrambled-password.ts @@ -0,0 +1,15 @@ +/** + * Generate a cryptographically secure random password + */ +export function generateScrambledPassword(length: number = 16): string { + const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*' + const array = new Uint8Array(length) + crypto.getRandomValues(array) + + let password = '' + for (let i = 0; i < length; i++) { + password += charset[array[i] % charset.length] + } + + return password +} diff --git a/frontends/nextjs/src/lib/password/index.ts b/frontends/nextjs/src/lib/password/index.ts new file mode 100644 index 000000000..466341367 --- /dev/null +++ b/frontends/nextjs/src/lib/password/index.ts @@ -0,0 +1,5 @@ +export { generateScrambledPassword } from './generate-scrambled-password' +export { generateDeterministicScrambledPassword } from './generate-deterministic-password' +export type { SMTPConfig } from './smtp-config' +export { DEFAULT_SMTP_CONFIG } from './default-smtp-config' +export { simulateEmailSend } from './simulate-email-send' diff --git a/frontends/nextjs/src/lib/password/simulate-email-send.ts b/frontends/nextjs/src/lib/password/simulate-email-send.ts new file mode 100644 index 000000000..36d8b9a57 --- /dev/null +++ b/frontends/nextjs/src/lib/password/simulate-email-send.ts @@ -0,0 +1,26 @@ +import type { SMTPConfig } from './smtp-config' + +/** + * Simulate sending an email (logs to console) + */ +export async function simulateEmailSend( + to: string, + subject: string, + body: string, + smtpConfig?: SMTPConfig +): Promise<{ success: boolean; message: string }> { + console.log('=== EMAIL SIMULATION ===') + console.log(`To: ${to}`) + console.log(`Subject: ${subject}`) + console.log(`Body:\n${body}`) + if (smtpConfig) { + console.log(`SMTP Host: ${smtpConfig.host}:${smtpConfig.port}`) + console.log(`From: ${smtpConfig.fromEmail}`) + } + console.log('========================') + + return { + success: true, + message: 'Email simulated successfully (check console)', + } +} diff --git a/frontends/nextjs/src/lib/password/smtp-config.ts b/frontends/nextjs/src/lib/password/smtp-config.ts new file mode 100644 index 000000000..a4be4c01a --- /dev/null +++ b/frontends/nextjs/src/lib/password/smtp-config.ts @@ -0,0 +1,12 @@ +/** + * SMTP configuration interface + */ +export interface SMTPConfig { + host: string + port: number + secure: boolean + username: string + password: string + fromEmail: string + fromName: string +} diff --git a/frontends/nextjs/src/lib/workflow-engine.ts b/frontends/nextjs/src/lib/workflow-engine.ts index c8ce1d709..6dd5774a5 100644 --- a/frontends/nextjs/src/lib/workflow-engine.ts +++ b/frontends/nextjs/src/lib/workflow-engine.ts @@ -1,234 +1,19 @@ -import { createSandboxedLuaEngine, type SandboxedLuaResult } from './sandboxed-lua-engine' -import type { Workflow, WorkflowNode, LuaScript } from './level-types' +/** + * Workflow Engine Module + * @deprecated Import from '@/lib/workflow' instead + */ -export interface WorkflowExecutionContext { - data: any - user?: any - scripts?: LuaScript[] -} +export { + executeWorkflow, + executeNode, + createWorkflowState, + logToWorkflow, + WorkflowEngine, + createWorkflowEngine, +} from './workflow' -export interface WorkflowExecutionResult { - success: boolean - outputs: Record - logs: string[] - error?: string - securityWarnings?: string[] -} - -export class WorkflowEngine { - // Unit tests for workflow execution paths are in workflow-engine.test.ts - // Coverage includes: node errors, condition short-circuit, Lua security warnings - private logs: string[] = [] - private securityWarnings: string[] = [] - - async executeWorkflow( - workflow: Workflow, - context: WorkflowExecutionContext - ): Promise { - this.logs = [] - this.securityWarnings = [] - const outputs: Record = {} - let currentData = context.data - - try { - this.log(`Starting workflow: ${workflow.name}`) - - for (let i = 0; i < workflow.nodes.length; i++) { - const node = workflow.nodes[i] - this.log(`Executing node ${i + 1}: ${node.label} (${node.type})`) - - const nodeResult = await this.executeNode(node, currentData, context) - - if (!nodeResult.success) { - return { - success: false, - outputs, - logs: this.logs, - error: `Node "${node.label}" failed: ${nodeResult.error}`, - securityWarnings: this.securityWarnings, - } - } - - outputs[node.id] = nodeResult.output - currentData = nodeResult.output - - if (node.type === 'condition' && nodeResult.output === false) { - this.log(`Condition node returned false, stopping workflow`) - break - } - } - - this.log(`Workflow completed successfully`) - - return { - success: true, - outputs, - logs: this.logs, - securityWarnings: this.securityWarnings, - } - } catch (error) { - return { - success: false, - outputs, - logs: this.logs, - error: error instanceof Error ? error.message : String(error), - securityWarnings: this.securityWarnings, - } - } - } - - private async executeNode( - node: WorkflowNode, - data: any, - context: WorkflowExecutionContext - ): Promise<{ success: boolean; output?: any; error?: string }> { - try { - switch (node.type) { - case 'trigger': - return { success: true, output: data } - - case 'action': - return await this.executeActionNode(node, data, context) - - case 'condition': - return await this.executeConditionNode(node, data, context) - - case 'lua': - return await this.executeLuaNode(node, data, context) - - case 'transform': - return await this.executeTransformNode(node, data, context) - - default: - return { - success: false, - error: `Unknown node type: ${node.type}`, - } - } - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : String(error), - } - } - } - - private async executeActionNode( - node: WorkflowNode, - data: any, - context: WorkflowExecutionContext - ): Promise<{ success: boolean; output?: any; error?: string }> { - this.log(`Action: ${node.config.action || 'default'}`) - return { success: true, output: data } - } - - private async executeConditionNode( - node: WorkflowNode, - data: any, - context: WorkflowExecutionContext - ): Promise<{ success: boolean; output?: any; error?: string }> { - const condition = node.config.condition || 'true' - - try { - const result = new Function('data', 'context', `return ${condition}`)( - data, - context - ) - this.log(`Condition evaluated to: ${result}`) - return { success: true, output: result } - } catch (error) { - return { - success: false, - error: `Condition evaluation failed: ${error instanceof Error ? error.message : String(error)}`, - } - } - } - - private async executeLuaNode( - node: WorkflowNode, - data: any, - context: WorkflowExecutionContext - ): Promise<{ success: boolean; output?: any; error?: string }> { - const scriptId = node.config.scriptId - - if (!scriptId || !context.scripts) { - const luaCode = node.config.code || 'return context.data' - return await this.executeLuaCode(luaCode, data, context) - } - - const script = context.scripts.find((s) => s.id === scriptId) - if (!script) { - return { - success: false, - error: `Script not found: ${scriptId}`, - } - } - - return await this.executeLuaCode(script.code, data, context) - } - - private async executeLuaCode( - code: string, - data: any, - context: WorkflowExecutionContext - ): Promise<{ success: boolean; output?: any; error?: string }> { - const engine = createSandboxedLuaEngine() - - try { - const luaContext = { - data, - user: context.user, - log: (...args: any[]) => this.log(...args), - } - - const result: SandboxedLuaResult = await engine.executeWithSandbox(code, luaContext) - - if (result.security.severity === 'critical' || result.security.severity === 'high') { - this.securityWarnings.push(`Security issues detected: ${result.security.issues.map(i => i.message).join(', ')}`) - } - - result.execution.logs.forEach((log) => this.log(`[Lua] ${log}`)) - - if (!result.execution.success) { - return { - success: false, - error: result.execution.error, - } - } - - return { success: true, output: result.execution.result } - } finally { - engine.destroy() - } - } - - private async executeTransformNode( - node: WorkflowNode, - data: any, - context: WorkflowExecutionContext - ): Promise<{ success: boolean; output?: any; error?: string }> { - const transform = node.config.transform || 'data' - - try { - const result = new Function('data', 'context', `return ${transform}`)( - data, - context - ) - this.log(`Transform result: ${JSON.stringify(result)}`) - return { success: true, output: result } - } catch (error) { - return { - success: false, - error: `Transform failed: ${error instanceof Error ? error.message : String(error)}`, - } - } - } - - private log(...args: any[]) { - this.logs.push(args.map((arg) => String(arg)).join(' ')) - } -} - -export function createWorkflowEngine(): WorkflowEngine { - return new WorkflowEngine() -} +export type { + WorkflowExecutionContext, + WorkflowExecutionResult, + WorkflowState, +} from './workflow' diff --git a/frontends/nextjs/src/lib/workflow/execute-action-node.ts b/frontends/nextjs/src/lib/workflow/execute-action-node.ts new file mode 100644 index 000000000..0e1573034 --- /dev/null +++ b/frontends/nextjs/src/lib/workflow/execute-action-node.ts @@ -0,0 +1,17 @@ +import type { WorkflowNode } from '../level-types' +import type { WorkflowExecutionContext } from './workflow-execution-context' +import type { WorkflowState } from './workflow-state' +import { logToWorkflow } from './log-to-workflow' + +/** + * Execute an action node + */ +export async function executeActionNode( + node: WorkflowNode, + data: any, + _context: WorkflowExecutionContext, + state: WorkflowState +): Promise<{ success: boolean; output?: any; error?: string }> { + logToWorkflow(state, `Action: ${node.config.action || 'default'}`) + return { success: true, output: data } +} diff --git a/frontends/nextjs/src/lib/workflow/execute-condition-node.ts b/frontends/nextjs/src/lib/workflow/execute-condition-node.ts new file mode 100644 index 000000000..7faf57af8 --- /dev/null +++ b/frontends/nextjs/src/lib/workflow/execute-condition-node.ts @@ -0,0 +1,27 @@ +import type { WorkflowNode } from '../level-types' +import type { WorkflowExecutionContext } from './workflow-execution-context' +import type { WorkflowState } from './workflow-state' +import { logToWorkflow } from './log-to-workflow' + +/** + * Execute a condition node + */ +export async function executeConditionNode( + node: WorkflowNode, + data: any, + context: WorkflowExecutionContext, + state: WorkflowState +): Promise<{ success: boolean; output?: any; error?: string }> { + const condition = node.config.condition || 'true' + + try { + const result = new Function('data', 'context', `return ${condition}`)(data, context) + logToWorkflow(state, `Condition evaluated to: ${result}`) + return { success: true, output: result } + } catch (error) { + return { + success: false, + error: `Condition evaluation failed: ${error instanceof Error ? error.message : String(error)}`, + } + } +} diff --git a/frontends/nextjs/src/lib/workflow/execute-lua-code.ts b/frontends/nextjs/src/lib/workflow/execute-lua-code.ts new file mode 100644 index 000000000..ce094a089 --- /dev/null +++ b/frontends/nextjs/src/lib/workflow/execute-lua-code.ts @@ -0,0 +1,45 @@ +import { createSandboxedLuaEngine, type SandboxedLuaResult } from '../sandboxed-lua-engine' +import type { WorkflowExecutionContext } from './workflow-execution-context' +import type { WorkflowState } from './workflow-state' +import { logToWorkflow } from './log-to-workflow' + +/** + * Execute Lua code in sandbox + */ +export async function executeLuaCode( + code: string, + data: any, + context: WorkflowExecutionContext, + state: WorkflowState +): Promise<{ success: boolean; output?: any; error?: string }> { + const engine = createSandboxedLuaEngine() + + try { + const luaContext = { + data, + user: context.user, + log: (...args: any[]) => logToWorkflow(state, ...args), + } + + const result: SandboxedLuaResult = await engine.executeWithSandbox(code, luaContext) + + if (result.security.severity === 'critical' || result.security.severity === 'high') { + state.securityWarnings.push( + `Security issues detected: ${result.security.issues.map((i) => i.message).join(', ')}` + ) + } + + result.execution.logs.forEach((log) => logToWorkflow(state, `[Lua] ${log}`)) + + if (!result.execution.success) { + return { + success: false, + error: result.execution.error, + } + } + + return { success: true, output: result.execution.result } + } finally { + engine.destroy() + } +} diff --git a/frontends/nextjs/src/lib/workflow/execute-lua-node.ts b/frontends/nextjs/src/lib/workflow/execute-lua-node.ts new file mode 100644 index 000000000..1cf5f2922 --- /dev/null +++ b/frontends/nextjs/src/lib/workflow/execute-lua-node.ts @@ -0,0 +1,31 @@ +import type { WorkflowNode } from '../level-types' +import type { WorkflowExecutionContext } from './workflow-execution-context' +import type { WorkflowState } from './workflow-state' +import { executeLuaCode } from './execute-lua-code' + +/** + * Execute a Lua script node + */ +export async function executeLuaNode( + node: WorkflowNode, + data: any, + context: WorkflowExecutionContext, + state: WorkflowState +): Promise<{ success: boolean; output?: any; error?: string }> { + const scriptId = node.config.scriptId + + if (!scriptId || !context.scripts) { + const luaCode = node.config.code || 'return context.data' + return await executeLuaCode(luaCode, data, context, state) + } + + const script = context.scripts.find((s) => s.id === scriptId) + if (!script) { + return { + success: false, + error: `Script not found: ${scriptId}`, + } + } + + return await executeLuaCode(script.code, data, context, state) +} diff --git a/frontends/nextjs/src/lib/workflow/execute-node.ts b/frontends/nextjs/src/lib/workflow/execute-node.ts new file mode 100644 index 000000000..4523eb4f6 --- /dev/null +++ b/frontends/nextjs/src/lib/workflow/execute-node.ts @@ -0,0 +1,47 @@ +import type { WorkflowNode } from '../level-types' +import type { WorkflowExecutionContext } from './workflow-execution-context' +import type { WorkflowState } from './workflow-state' +import { executeActionNode } from './execute-action-node' +import { executeConditionNode } from './execute-condition-node' +import { executeLuaNode } from './execute-lua-node' +import { executeTransformNode } from './execute-transform-node' + +/** + * Execute a single workflow node + */ +export async function executeNode( + node: WorkflowNode, + data: any, + context: WorkflowExecutionContext, + state: WorkflowState +): Promise<{ success: boolean; output?: any; error?: string }> { + try { + switch (node.type) { + case 'trigger': + return { success: true, output: data } + + case 'action': + return await executeActionNode(node, data, context, state) + + case 'condition': + return await executeConditionNode(node, data, context, state) + + case 'lua': + return await executeLuaNode(node, data, context, state) + + case 'transform': + return await executeTransformNode(node, data, context, state) + + default: + return { + success: false, + error: `Unknown node type: ${node.type}`, + } + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + } + } +} diff --git a/frontends/nextjs/src/lib/workflow/execute-transform-node.ts b/frontends/nextjs/src/lib/workflow/execute-transform-node.ts new file mode 100644 index 000000000..82b49fb7d --- /dev/null +++ b/frontends/nextjs/src/lib/workflow/execute-transform-node.ts @@ -0,0 +1,27 @@ +import type { WorkflowNode } from '../level-types' +import type { WorkflowExecutionContext } from './workflow-execution-context' +import type { WorkflowState } from './workflow-state' +import { logToWorkflow } from './log-to-workflow' + +/** + * Execute a transform node + */ +export async function executeTransformNode( + node: WorkflowNode, + data: any, + context: WorkflowExecutionContext, + state: WorkflowState +): Promise<{ success: boolean; output?: any; error?: string }> { + const transform = node.config.transform || 'data' + + try { + const result = new Function('data', 'context', `return ${transform}`)(data, context) + logToWorkflow(state, `Transform result: ${JSON.stringify(result)}`) + return { success: true, output: result } + } catch (error) { + return { + success: false, + error: `Transform failed: ${error instanceof Error ? error.message : String(error)}`, + } + } +} diff --git a/frontends/nextjs/src/lib/workflow/execute-workflow.ts b/frontends/nextjs/src/lib/workflow/execute-workflow.ts new file mode 100644 index 000000000..4253a188e --- /dev/null +++ b/frontends/nextjs/src/lib/workflow/execute-workflow.ts @@ -0,0 +1,64 @@ +import type { Workflow } from '../level-types' +import type { WorkflowExecutionContext } from './workflow-execution-context' +import type { WorkflowExecutionResult } from './workflow-execution-result' +import { createWorkflowState } from './workflow-state' +import { executeNode } from './execute-node' +import { logToWorkflow } from './log-to-workflow' + +/** + * Execute a complete workflow + */ +export async function executeWorkflow( + workflow: Workflow, + context: WorkflowExecutionContext +): Promise { + const state = createWorkflowState() + const outputs: Record = {} + let currentData = context.data + + try { + logToWorkflow(state, `Starting workflow: ${workflow.name}`) + + for (let i = 0; i < workflow.nodes.length; i++) { + const node = workflow.nodes[i] + logToWorkflow(state, `Executing node ${i + 1}: ${node.label} (${node.type})`) + + const nodeResult = await executeNode(node, currentData, context, state) + + if (!nodeResult.success) { + return { + success: false, + outputs, + logs: state.logs, + error: `Node "${node.label}" failed: ${nodeResult.error}`, + securityWarnings: state.securityWarnings, + } + } + + outputs[node.id] = nodeResult.output + currentData = nodeResult.output + + if (node.type === 'condition' && nodeResult.output === false) { + logToWorkflow(state, `Condition node returned false, stopping workflow`) + break + } + } + + logToWorkflow(state, `Workflow completed successfully`) + + return { + success: true, + outputs, + logs: state.logs, + securityWarnings: state.securityWarnings, + } + } catch (error) { + return { + success: false, + outputs, + logs: state.logs, + error: error instanceof Error ? error.message : String(error), + securityWarnings: state.securityWarnings, + } + } +} diff --git a/frontends/nextjs/src/lib/workflow/index.ts b/frontends/nextjs/src/lib/workflow/index.ts new file mode 100644 index 000000000..058a476b8 --- /dev/null +++ b/frontends/nextjs/src/lib/workflow/index.ts @@ -0,0 +1,24 @@ +// Types +export type { WorkflowExecutionContext } from './workflow-execution-context' +export type { WorkflowExecutionResult } from './workflow-execution-result' +export type { WorkflowState } from './workflow-state' + +// State +export { createWorkflowState } from './workflow-state' + +// Core functions +export { executeWorkflow } from './execute-workflow' +export { executeNode } from './execute-node' + +// Node executors +export { executeActionNode } from './execute-action-node' +export { executeConditionNode } from './execute-condition-node' +export { executeLuaNode } from './execute-lua-node' +export { executeTransformNode } from './execute-transform-node' +export { executeLuaCode } from './execute-lua-code' + +// Utilities +export { logToWorkflow } from './log-to-workflow' + +// Namespace class (groups lambdas, no state) +export { WorkflowEngine, createWorkflowEngine } from './workflow-engine-class' diff --git a/frontends/nextjs/src/lib/workflow/log-to-workflow.ts b/frontends/nextjs/src/lib/workflow/log-to-workflow.ts new file mode 100644 index 000000000..87e6f9d71 --- /dev/null +++ b/frontends/nextjs/src/lib/workflow/log-to-workflow.ts @@ -0,0 +1,8 @@ +import type { WorkflowState } from './workflow-state' + +/** + * Log a message to workflow state + */ +export function logToWorkflow(state: WorkflowState, ...args: any[]): void { + state.logs.push(args.map((arg) => String(arg)).join(' ')) +} diff --git a/frontends/nextjs/src/lib/workflow/workflow-engine-class.ts b/frontends/nextjs/src/lib/workflow/workflow-engine-class.ts new file mode 100644 index 000000000..c0e14c7de --- /dev/null +++ b/frontends/nextjs/src/lib/workflow/workflow-engine-class.ts @@ -0,0 +1,44 @@ +import type { Workflow } from '../level-types' +import type { WorkflowExecutionContext } from './workflow-execution-context' +import type { WorkflowExecutionResult } from './workflow-execution-result' +import type { WorkflowState } from './workflow-state' +import { executeWorkflow } from './execute-workflow' +import { executeNode } from './execute-node' +import { executeActionNode } from './execute-action-node' +import { executeConditionNode } from './execute-condition-node' +import { executeLuaNode } from './execute-lua-node' +import { executeTransformNode } from './execute-transform-node' +import { executeLuaCode } from './execute-lua-code' +import { createWorkflowState } from './workflow-state' +import { logToWorkflow } from './log-to-workflow' + +/** + * Workflow execution functions grouped as static methods + * Class is a namespace container - no instance state + */ +export class WorkflowEngine { + static execute = executeWorkflow + static executeNode = executeNode + static executeActionNode = executeActionNode + static executeConditionNode = executeConditionNode + static executeLuaNode = executeLuaNode + static executeTransformNode = executeTransformNode + static executeLuaCode = executeLuaCode + static createState = createWorkflowState + static log = logToWorkflow + + // Convenience instance method for legacy compatibility + async executeWorkflow( + workflow: Workflow, + context: WorkflowExecutionContext + ): Promise { + return executeWorkflow(workflow, context) + } +} + +/** + * @deprecated Use WorkflowEngine.execute() directly + */ +export function createWorkflowEngine(): WorkflowEngine { + return new WorkflowEngine() +} diff --git a/frontends/nextjs/src/lib/workflow/workflow-execution-context.ts b/frontends/nextjs/src/lib/workflow/workflow-execution-context.ts new file mode 100644 index 000000000..eda234173 --- /dev/null +++ b/frontends/nextjs/src/lib/workflow/workflow-execution-context.ts @@ -0,0 +1,10 @@ +import type { LuaScript } from '../level-types' + +/** + * Context passed to workflow execution + */ +export interface WorkflowExecutionContext { + data: any + user?: any + scripts?: LuaScript[] +} diff --git a/frontends/nextjs/src/lib/workflow/workflow-execution-result.ts b/frontends/nextjs/src/lib/workflow/workflow-execution-result.ts new file mode 100644 index 000000000..d4c12a8dc --- /dev/null +++ b/frontends/nextjs/src/lib/workflow/workflow-execution-result.ts @@ -0,0 +1,10 @@ +/** + * Result of workflow execution + */ +export interface WorkflowExecutionResult { + success: boolean + outputs: Record + logs: string[] + error?: string + securityWarnings?: string[] +} diff --git a/frontends/nextjs/src/lib/workflow/workflow-state.ts b/frontends/nextjs/src/lib/workflow/workflow-state.ts new file mode 100644 index 000000000..15c3d8c7e --- /dev/null +++ b/frontends/nextjs/src/lib/workflow/workflow-state.ts @@ -0,0 +1,17 @@ +/** + * Shared state for workflow execution + */ +export interface WorkflowState { + logs: string[] + securityWarnings: string[] +} + +/** + * Create initial workflow state + */ +export function createWorkflowState(): WorkflowState { + return { + logs: [], + securityWarnings: [], + } +}