mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-29 16:24:58 +00:00
stuff
This commit is contained in:
@@ -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<string, string> = 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<UserRole, number> = {
|
||||
public: 1,
|
||||
user: 2,
|
||||
admin: 3,
|
||||
god: 4,
|
||||
supergod: 5,
|
||||
}
|
||||
|
||||
return roleHierarchy[userRole] >= level
|
||||
}
|
||||
|
||||
export function getRoleDisplayName(role: UserRole): string {
|
||||
const names: Record<UserRole, string> = {
|
||||
public: 'Public',
|
||||
user: 'User',
|
||||
admin: 'Administrator',
|
||||
god: 'System Architect',
|
||||
supergod: 'Supreme Administrator',
|
||||
}
|
||||
return names[role]
|
||||
}
|
||||
|
||||
19
frontends/nextjs/src/lib/auth/can-access-level.ts
Normal file
19
frontends/nextjs/src/lib/auth/can-access-level.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { UserRole } from '../level-types'
|
||||
|
||||
/**
|
||||
* Role hierarchy mapping roles to their numeric permission levels
|
||||
*/
|
||||
const roleHierarchy: Record<UserRole, number> = {
|
||||
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
|
||||
}
|
||||
6
frontends/nextjs/src/lib/auth/default-credentials.ts
Normal file
6
frontends/nextjs/src/lib/auth/default-credentials.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { SCRAMBLED_PASSWORDS } from './scrambled-passwords'
|
||||
|
||||
/**
|
||||
* Maps usernames to their scrambled passwords for authentication
|
||||
*/
|
||||
export const DEFAULT_CREDENTIALS: Record<string, string> = SCRAMBLED_PASSWORDS
|
||||
40
frontends/nextjs/src/lib/auth/default-users.ts
Normal file
40
frontends/nextjs/src/lib/auth/default-users.ts
Normal file
@@ -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(),
|
||||
},
|
||||
]
|
||||
19
frontends/nextjs/src/lib/auth/get-role-display-name.ts
Normal file
19
frontends/nextjs/src/lib/auth/get-role-display-name.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { UserRole } from '../level-types'
|
||||
|
||||
/**
|
||||
* Human-readable display names for user roles
|
||||
*/
|
||||
const roleDisplayNames: Record<UserRole, string> = {
|
||||
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]
|
||||
}
|
||||
8
frontends/nextjs/src/lib/auth/get-scrambled-password.ts
Normal file
8
frontends/nextjs/src/lib/auth/get-scrambled-password.ts
Normal file
@@ -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] || ''
|
||||
}
|
||||
6
frontends/nextjs/src/lib/auth/index.ts
Normal file
6
frontends/nextjs/src/lib/auth/index.ts
Normal file
@@ -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'
|
||||
30
frontends/nextjs/src/lib/auth/scrambled-passwords.ts
Normal file
30
frontends/nextjs/src/lib/auth/scrambled-passwords.ts
Normal file
@@ -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),
|
||||
}
|
||||
16
frontends/nextjs/src/lib/db/app-config/get-app-config.ts
Normal file
16
frontends/nextjs/src/lib/db/app-config/get-app-config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { AppConfiguration } from '../../level-types'
|
||||
|
||||
export async function getAppConfig(): Promise<AppConfiguration | null> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
2
frontends/nextjs/src/lib/db/app-config/index.ts
Normal file
2
frontends/nextjs/src/lib/db/app-config/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { getAppConfig } from './get-app-config'
|
||||
export { setAppConfig } from './set-app-config'
|
||||
17
frontends/nextjs/src/lib/db/app-config/set-app-config.ts
Normal file
17
frontends/nextjs/src/lib/db/app-config/set-app-config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { AppConfiguration } from '../../level-types'
|
||||
|
||||
export async function setAppConfig(config: AppConfiguration): Promise<void> {
|
||||
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),
|
||||
},
|
||||
})
|
||||
}
|
||||
15
frontends/nextjs/src/lib/db/comments/add-comment.ts
Normal file
15
frontends/nextjs/src/lib/db/comments/add-comment.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { Comment } from '../../level-types'
|
||||
|
||||
export async function addComment(comment: Comment): Promise<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
5
frontends/nextjs/src/lib/db/comments/delete-comment.ts
Normal file
5
frontends/nextjs/src/lib/db/comments/delete-comment.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { prisma } from '../prisma'
|
||||
|
||||
export async function deleteComment(commentId: string): Promise<void> {
|
||||
await prisma.comment.delete({ where: { id: commentId } })
|
||||
}
|
||||
14
frontends/nextjs/src/lib/db/comments/get-comments.ts
Normal file
14
frontends/nextjs/src/lib/db/comments/get-comments.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { Comment } from '../../level-types'
|
||||
|
||||
export async function getComments(): Promise<Comment[]> {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
5
frontends/nextjs/src/lib/db/comments/index.ts
Normal file
5
frontends/nextjs/src/lib/db/comments/index.ts
Normal file
@@ -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'
|
||||
18
frontends/nextjs/src/lib/db/comments/set-comments.ts
Normal file
18
frontends/nextjs/src/lib/db/comments/set-comments.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { Comment } from '../../level-types'
|
||||
|
||||
export async function setComments(comments: Comment[]): Promise<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
9
frontends/nextjs/src/lib/db/comments/update-comment.ts
Normal file
9
frontends/nextjs/src/lib/db/comments/update-comment.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { Comment } from '../../level-types'
|
||||
|
||||
export async function updateComment(commentId: string, updates: Partial<Comment>): Promise<void> {
|
||||
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 })
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { ComponentConfig } from '../types'
|
||||
|
||||
export async function addComponentConfig(config: ComponentConfig): Promise<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
15
frontends/nextjs/src/lib/db/components/add-component-node.ts
Normal file
15
frontends/nextjs/src/lib/db/components/add-component-node.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { ComponentNode } from '../types'
|
||||
|
||||
export async function addComponentNode(node: ComponentNode): Promise<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { prisma } from '../prisma'
|
||||
|
||||
export async function deleteComponentConfig(configId: string): Promise<void> {
|
||||
await prisma.componentConfig.delete({ where: { id: configId } })
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { prisma } from '../prisma'
|
||||
|
||||
export async function deleteComponentNode(nodeId: string): Promise<void> {
|
||||
await prisma.componentNode.delete({ where: { id: nodeId } })
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { ComponentConfig } from '../types'
|
||||
|
||||
export async function getComponentConfigs(): Promise<Record<string, ComponentConfig>> {
|
||||
const configs = await prisma.componentConfig.findMany()
|
||||
const result: Record<string, ComponentConfig> = {}
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { ComponentNode } from '../types'
|
||||
|
||||
export async function getComponentHierarchy(): Promise<Record<string, ComponentNode>> {
|
||||
const nodes = await prisma.componentNode.findMany()
|
||||
const result: Record<string, ComponentNode> = {}
|
||||
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
|
||||
}
|
||||
10
frontends/nextjs/src/lib/db/components/index.ts
Normal file
10
frontends/nextjs/src/lib/db/components/index.ts
Normal file
@@ -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'
|
||||
@@ -0,0 +1,18 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { ComponentConfig } from '../types'
|
||||
|
||||
export async function setComponentConfigs(configs: Record<string, ComponentConfig>): Promise<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { ComponentNode } from '../types'
|
||||
|
||||
export async function setComponentHierarchy(hierarchy: Record<string, ComponentNode>): Promise<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { ComponentConfig } from '../types'
|
||||
|
||||
export async function updateComponentConfig(configId: string, updates: Partial<ComponentConfig>): Promise<void> {
|
||||
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 })
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { ComponentNode } from '../types'
|
||||
|
||||
export async function updateComponentNode(nodeId: string, updates: Partial<ComponentNode>): Promise<void> {
|
||||
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 })
|
||||
}
|
||||
@@ -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<void> {
|
||||
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) },
|
||||
})
|
||||
}
|
||||
13
frontends/nextjs/src/lib/db/credentials/get-credentials.ts
Normal file
13
frontends/nextjs/src/lib/db/credentials/get-credentials.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { prisma } from '../prisma'
|
||||
|
||||
/**
|
||||
* Get all credentials as a username->passwordHash map
|
||||
*/
|
||||
export async function getCredentials(): Promise<Record<string, string>> {
|
||||
const credentials = await prisma.credential.findMany()
|
||||
const result: Record<string, string> = {}
|
||||
for (const cred of credentials) {
|
||||
result[cred.username] = cred.passwordHash
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { prisma } from '../prisma'
|
||||
|
||||
/**
|
||||
* Get password change timestamps for all users
|
||||
*/
|
||||
export async function getPasswordChangeTimestamps(): Promise<Record<string, number>> {
|
||||
const users = await prisma.user.findMany({
|
||||
where: { passwordChangeTimestamp: { not: null } },
|
||||
select: { username: true, passwordChangeTimestamp: true },
|
||||
})
|
||||
const result: Record<string, number> = {}
|
||||
for (const user of users) {
|
||||
if (user.passwordChangeTimestamp) {
|
||||
result[user.username] = Number(user.passwordChangeTimestamp)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { prisma } from '../prisma'
|
||||
|
||||
/**
|
||||
* Get password reset tokens
|
||||
*/
|
||||
export async function getPasswordResetTokens(): Promise<Record<string, string>> {
|
||||
const kv = await prisma.keyValue.findUnique({ where: { key: 'db_password_reset_tokens' } })
|
||||
return kv ? JSON.parse(kv.value) : {}
|
||||
}
|
||||
8
frontends/nextjs/src/lib/db/credentials/index.ts
Normal file
8
frontends/nextjs/src/lib/db/credentials/index.ts
Normal file
@@ -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'
|
||||
17
frontends/nextjs/src/lib/db/credentials/set-credential.ts
Normal file
17
frontends/nextjs/src/lib/db/credentials/set-credential.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { prisma } from '../prisma'
|
||||
|
||||
/**
|
||||
* Set or update a user's credential
|
||||
*/
|
||||
export async function setCredential(username: string, passwordHash: string): Promise<void> {
|
||||
await prisma.credential.upsert({
|
||||
where: { username },
|
||||
update: { passwordHash },
|
||||
create: { username, passwordHash },
|
||||
})
|
||||
|
||||
await prisma.user.update({
|
||||
where: { username },
|
||||
data: { passwordChangeTimestamp: BigInt(Date.now()) },
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { prisma } from '../prisma'
|
||||
|
||||
/**
|
||||
* Set password change timestamps for users
|
||||
*/
|
||||
export async function setPasswordChangeTimestamps(timestamps: Record<string, number>): Promise<void> {
|
||||
for (const [username, timestamp] of Object.entries(timestamps)) {
|
||||
await prisma.user.update({
|
||||
where: { username },
|
||||
data: { passwordChangeTimestamp: BigInt(timestamp) },
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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) },
|
||||
})
|
||||
}
|
||||
@@ -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<boolean> {
|
||||
const credential = await prisma.credential.findUnique({ where: { username } })
|
||||
if (!credential) return false
|
||||
return await verifyPassword(password, credential.passwordHash)
|
||||
}
|
||||
11
frontends/nextjs/src/lib/db/hash-password.ts
Normal file
11
frontends/nextjs/src/lib/db/hash-password.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Hash a password using SHA-512
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
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
|
||||
}
|
||||
84
frontends/nextjs/src/lib/db/index.ts
Normal file
84
frontends/nextjs/src/lib/db/index.ts
Normal file
@@ -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
|
||||
}
|
||||
14
frontends/nextjs/src/lib/db/initialize-database.ts
Normal file
14
frontends/nextjs/src/lib/db/initialize-database.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { prisma } from '../prisma'
|
||||
|
||||
/**
|
||||
* Initialize database connection
|
||||
*/
|
||||
export async function initializeDatabase(): Promise<void> {
|
||||
try {
|
||||
await prisma.$connect()
|
||||
console.log('Database initialized successfully')
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize database:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
18
frontends/nextjs/src/lib/db/lua-scripts/add-lua-script.ts
Normal file
18
frontends/nextjs/src/lib/db/lua-scripts/add-lua-script.ts
Normal file
@@ -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<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { prisma } from '../prisma'
|
||||
|
||||
/**
|
||||
* Delete a Lua script by ID
|
||||
*/
|
||||
export async function deleteLuaScript(scriptId: string): Promise<void> {
|
||||
await prisma.luaScript.delete({ where: { id: scriptId } })
|
||||
}
|
||||
17
frontends/nextjs/src/lib/db/lua-scripts/get-lua-scripts.ts
Normal file
17
frontends/nextjs/src/lib/db/lua-scripts/get-lua-scripts.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { LuaScript } from '../../level-types'
|
||||
|
||||
/**
|
||||
* Get all Lua scripts
|
||||
*/
|
||||
export async function getLuaScripts(): Promise<LuaScript[]> {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
5
frontends/nextjs/src/lib/db/lua-scripts/index.ts
Normal file
5
frontends/nextjs/src/lib/db/lua-scripts/index.ts
Normal file
@@ -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'
|
||||
21
frontends/nextjs/src/lib/db/lua-scripts/set-lua-scripts.ts
Normal file
21
frontends/nextjs/src/lib/db/lua-scripts/set-lua-scripts.ts
Normal file
@@ -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<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
19
frontends/nextjs/src/lib/db/lua-scripts/update-lua-script.ts
Normal file
19
frontends/nextjs/src/lib/db/lua-scripts/update-lua-script.ts
Normal file
@@ -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<LuaScript>): Promise<void> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
19
frontends/nextjs/src/lib/db/pages/add-page.ts
Normal file
19
frontends/nextjs/src/lib/db/pages/add-page.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { PageConfig } from '../../level-types'
|
||||
|
||||
/**
|
||||
* Add a page
|
||||
*/
|
||||
export async function addPage(page: PageConfig): Promise<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
8
frontends/nextjs/src/lib/db/pages/delete-page.ts
Normal file
8
frontends/nextjs/src/lib/db/pages/delete-page.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { prisma } from '../prisma'
|
||||
|
||||
/**
|
||||
* Delete a page by ID
|
||||
*/
|
||||
export async function deletePage(pageId: string): Promise<void> {
|
||||
await prisma.pageConfig.delete({ where: { id: pageId } })
|
||||
}
|
||||
18
frontends/nextjs/src/lib/db/pages/get-pages.ts
Normal file
18
frontends/nextjs/src/lib/db/pages/get-pages.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { PageConfig } from '../../level-types'
|
||||
|
||||
/**
|
||||
* Get all pages
|
||||
*/
|
||||
export async function getPages(): Promise<PageConfig[]> {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
5
frontends/nextjs/src/lib/db/pages/index.ts
Normal file
5
frontends/nextjs/src/lib/db/pages/index.ts
Normal file
@@ -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'
|
||||
22
frontends/nextjs/src/lib/db/pages/set-pages.ts
Normal file
22
frontends/nextjs/src/lib/db/pages/set-pages.ts
Normal file
@@ -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<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
20
frontends/nextjs/src/lib/db/pages/update-page.ts
Normal file
20
frontends/nextjs/src/lib/db/pages/update-page.ts
Normal file
@@ -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<PageConfig>): Promise<void> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
4
frontends/nextjs/src/lib/db/prisma.ts
Normal file
4
frontends/nextjs/src/lib/db/prisma.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Symlink to actual prisma client for db folder imports
|
||||
*/
|
||||
export { prisma } from '../prisma'
|
||||
21
frontends/nextjs/src/lib/db/schemas/add-schema.ts
Normal file
21
frontends/nextjs/src/lib/db/schemas/add-schema.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { ModelSchema } from '../../schema-types'
|
||||
|
||||
/**
|
||||
* Add a schema
|
||||
*/
|
||||
export async function addSchema(schema: ModelSchema): Promise<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
8
frontends/nextjs/src/lib/db/schemas/delete-schema.ts
Normal file
8
frontends/nextjs/src/lib/db/schemas/delete-schema.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { prisma } from '../prisma'
|
||||
|
||||
/**
|
||||
* Delete a schema by name
|
||||
*/
|
||||
export async function deleteSchema(schemaName: string): Promise<void> {
|
||||
await prisma.modelSchema.delete({ where: { name: schemaName } })
|
||||
}
|
||||
20
frontends/nextjs/src/lib/db/schemas/get-schemas.ts
Normal file
20
frontends/nextjs/src/lib/db/schemas/get-schemas.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { ModelSchema } from '../../schema-types'
|
||||
|
||||
/**
|
||||
* Get all schemas
|
||||
*/
|
||||
export async function getSchemas(): Promise<ModelSchema[]> {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
5
frontends/nextjs/src/lib/db/schemas/index.ts
Normal file
5
frontends/nextjs/src/lib/db/schemas/index.ts
Normal file
@@ -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'
|
||||
24
frontends/nextjs/src/lib/db/schemas/set-schemas.ts
Normal file
24
frontends/nextjs/src/lib/db/schemas/set-schemas.ts
Normal file
@@ -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<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
22
frontends/nextjs/src/lib/db/schemas/update-schema.ts
Normal file
22
frontends/nextjs/src/lib/db/schemas/update-schema.ts
Normal file
@@ -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<ModelSchema>): Promise<void> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
98
frontends/nextjs/src/lib/db/types.ts
Normal file
98
frontends/nextjs/src/lib/db/types.ts
Normal file
@@ -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<string, any>
|
||||
styles: Record<string, any>
|
||||
events: Record<string, string>
|
||||
conditionalRendering?: {
|
||||
condition: string
|
||||
luaScriptId?: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Full database schema type
|
||||
*/
|
||||
export interface DatabaseSchema {
|
||||
users: import('../level-types').User[]
|
||||
credentials: Record<string, string>
|
||||
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<string, ComponentNode>
|
||||
componentConfigs: Record<string, ComponentConfig>
|
||||
godCredentialsExpiry: number
|
||||
passwordChangeTimestamps: Record<string, number>
|
||||
firstLoginFlags: Record<string, boolean>
|
||||
godCredentialsExpiryDuration: number
|
||||
cssClasses: CssCategory[]
|
||||
dropdownConfigs: DropdownConfig[]
|
||||
tenants: import('../level-types').Tenant[]
|
||||
powerTransferRequests: import('../level-types').PowerTransferRequest[]
|
||||
smtpConfig: import('../password').SMTPConfig
|
||||
passwordResetTokens: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
21
frontends/nextjs/src/lib/db/users/add-user.ts
Normal file
21
frontends/nextjs/src/lib/db/users/add-user.ts
Normal file
@@ -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<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
8
frontends/nextjs/src/lib/db/users/delete-user.ts
Normal file
8
frontends/nextjs/src/lib/db/users/delete-user.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { prisma } from '../prisma'
|
||||
|
||||
/**
|
||||
* Delete a user by ID
|
||||
*/
|
||||
export async function deleteUser(userId: string): Promise<void> {
|
||||
await prisma.user.delete({ where: { id: userId } })
|
||||
}
|
||||
25
frontends/nextjs/src/lib/db/users/get-super-god.ts
Normal file
25
frontends/nextjs/src/lib/db/users/get-super-god.ts
Normal file
@@ -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<User | null> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
20
frontends/nextjs/src/lib/db/users/get-users.ts
Normal file
20
frontends/nextjs/src/lib/db/users/get-users.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { User } from '../level-types'
|
||||
|
||||
/**
|
||||
* Get all users from database
|
||||
*/
|
||||
export async function getUsers(): Promise<User[]> {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
7
frontends/nextjs/src/lib/db/users/index.ts
Normal file
7
frontends/nextjs/src/lib/db/users/index.ts
Normal file
@@ -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'
|
||||
26
frontends/nextjs/src/lib/db/users/set-users.ts
Normal file
26
frontends/nextjs/src/lib/db/users/set-users.ts
Normal file
@@ -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<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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<void> {
|
||||
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' },
|
||||
}),
|
||||
])
|
||||
}
|
||||
20
frontends/nextjs/src/lib/db/users/update-user.ts
Normal file
20
frontends/nextjs/src/lib/db/users/update-user.ts
Normal file
@@ -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<User>): Promise<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
9
frontends/nextjs/src/lib/db/verify-password.ts
Normal file
9
frontends/nextjs/src/lib/db/verify-password.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { hashPassword } from './hash-password'
|
||||
|
||||
/**
|
||||
* Verify a password against a hash
|
||||
*/
|
||||
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||
const inputHash = await hashPassword(password)
|
||||
return inputHash === hash
|
||||
}
|
||||
18
frontends/nextjs/src/lib/db/workflows/add-workflow.ts
Normal file
18
frontends/nextjs/src/lib/db/workflows/add-workflow.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { Workflow } from '../../level-types'
|
||||
|
||||
/**
|
||||
* Add a workflow
|
||||
*/
|
||||
export async function addWorkflow(workflow: Workflow): Promise<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
8
frontends/nextjs/src/lib/db/workflows/delete-workflow.ts
Normal file
8
frontends/nextjs/src/lib/db/workflows/delete-workflow.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { prisma } from '../prisma'
|
||||
|
||||
/**
|
||||
* Delete a workflow by ID
|
||||
*/
|
||||
export async function deleteWorkflow(workflowId: string): Promise<void> {
|
||||
await prisma.workflow.delete({ where: { id: workflowId } })
|
||||
}
|
||||
17
frontends/nextjs/src/lib/db/workflows/get-workflows.ts
Normal file
17
frontends/nextjs/src/lib/db/workflows/get-workflows.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { prisma } from '../prisma'
|
||||
import type { Workflow } from '../../level-types'
|
||||
|
||||
/**
|
||||
* Get all workflows
|
||||
*/
|
||||
export async function getWorkflows(): Promise<Workflow[]> {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
5
frontends/nextjs/src/lib/db/workflows/index.ts
Normal file
5
frontends/nextjs/src/lib/db/workflows/index.ts
Normal file
@@ -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'
|
||||
21
frontends/nextjs/src/lib/db/workflows/set-workflows.ts
Normal file
21
frontends/nextjs/src/lib/db/workflows/set-workflows.ts
Normal file
@@ -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<void> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
19
frontends/nextjs/src/lib/db/workflows/update-workflow.ts
Normal file
19
frontends/nextjs/src/lib/db/workflows/update-workflow.ts
Normal file
@@ -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<Workflow>): Promise<void> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -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<void>}
|
||||
*/
|
||||
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'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { initializePackageSystem, getModularSeedData } from './initialize-package-system'
|
||||
|
||||
/**
|
||||
* Get modular package components
|
||||
*/
|
||||
export async function getModularPackageComponents(): Promise<any[]> {
|
||||
await initializePackageSystem()
|
||||
return getModularSeedData().components
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { initializePackageSystem, getModularSeedData } from './initialize-package-system'
|
||||
|
||||
/**
|
||||
* Get modular package metadata
|
||||
*/
|
||||
export async function getModularPackageMetadata(): Promise<any[]> {
|
||||
await initializePackageSystem()
|
||||
return getModularSeedData().packages
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { initializePackageSystem, getModularSeedData } from './initialize-package-system'
|
||||
|
||||
/**
|
||||
* Get modular package scripts
|
||||
*/
|
||||
export async function getModularPackageScripts(): Promise<any[]> {
|
||||
await initializePackageSystem()
|
||||
return getModularSeedData().scripts
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
9
frontends/nextjs/src/lib/package-loader/index.ts
Normal file
9
frontends/nextjs/src/lib/package-loader/index.ts
Normal file
@@ -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'
|
||||
@@ -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<void> {
|
||||
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
|
||||
}
|
||||
@@ -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: [],
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
14
frontends/nextjs/src/lib/password/default-smtp-config.ts
Normal file
14
frontends/nextjs/src/lib/password/default-smtp-config.ts
Normal file
@@ -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',
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
5
frontends/nextjs/src/lib/password/index.ts
Normal file
5
frontends/nextjs/src/lib/password/index.ts
Normal file
@@ -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'
|
||||
26
frontends/nextjs/src/lib/password/simulate-email-send.ts
Normal file
26
frontends/nextjs/src/lib/password/simulate-email-send.ts
Normal file
@@ -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)',
|
||||
}
|
||||
}
|
||||
12
frontends/nextjs/src/lib/password/smtp-config.ts
Normal file
12
frontends/nextjs/src/lib/password/smtp-config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* SMTP configuration interface
|
||||
*/
|
||||
export interface SMTPConfig {
|
||||
host: string
|
||||
port: number
|
||||
secure: boolean
|
||||
username: string
|
||||
password: string
|
||||
fromEmail: string
|
||||
fromName: string
|
||||
}
|
||||
@@ -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<string, any>
|
||||
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<WorkflowExecutionResult> {
|
||||
this.logs = []
|
||||
this.securityWarnings = []
|
||||
const outputs: Record<string, any> = {}
|
||||
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'
|
||||
|
||||
17
frontends/nextjs/src/lib/workflow/execute-action-node.ts
Normal file
17
frontends/nextjs/src/lib/workflow/execute-action-node.ts
Normal file
@@ -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 }
|
||||
}
|
||||
27
frontends/nextjs/src/lib/workflow/execute-condition-node.ts
Normal file
27
frontends/nextjs/src/lib/workflow/execute-condition-node.ts
Normal file
@@ -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)}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
45
frontends/nextjs/src/lib/workflow/execute-lua-code.ts
Normal file
45
frontends/nextjs/src/lib/workflow/execute-lua-code.ts
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
31
frontends/nextjs/src/lib/workflow/execute-lua-node.ts
Normal file
31
frontends/nextjs/src/lib/workflow/execute-lua-node.ts
Normal file
@@ -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)
|
||||
}
|
||||
47
frontends/nextjs/src/lib/workflow/execute-node.ts
Normal file
47
frontends/nextjs/src/lib/workflow/execute-node.ts
Normal file
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
27
frontends/nextjs/src/lib/workflow/execute-transform-node.ts
Normal file
27
frontends/nextjs/src/lib/workflow/execute-transform-node.ts
Normal file
@@ -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)}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user