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