mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
Refactor page rendering functions for consistency and readability
- Adjusted indentation and formatting in get-pages.ts for consistency. - Enhanced readability in build-feature-card.ts by formatting parameters and adding commas. - Updated build-features-component.ts to maintain consistent formatting and added missing commas. - Improved formatting in build-hero-component.ts for better readability. - Standardized formatting in build-level1-homepage.ts by adding commas. - Refactored initialize-default-pages.ts for consistent indentation and added commas. - Cleaned up PageRendererUtils.ts by ensuring consistent argument handling. - Streamlined check-permissions.ts for better readability and consistency. - Refined execute-lua-script.ts for consistent error handling and formatting. - Enhanced get-page.ts and get-pages-by-level.ts with consistent return formatting. - Improved load-pages.ts for better readability and consistent formatting. - Standardized on-page-load.ts and on-page-unload.ts for consistent formatting and readability.
This commit is contained in:
@@ -34,7 +34,9 @@
|
||||
"Bash(bun run format:*)",
|
||||
"Bash(while read file)",
|
||||
"Bash(do eslint:*)",
|
||||
"Bash(done)"
|
||||
"Bash(done)",
|
||||
"Bash(eslint:*)",
|
||||
"Bash(bunx eslint:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
377
docs/architecture/LUA_UI_MIGRATION_STRATEGY.md
Normal file
377
docs/architecture/LUA_UI_MIGRATION_STRATEGY.md
Normal file
@@ -0,0 +1,377 @@
|
||||
# Lua UI Migration Strategy
|
||||
|
||||
## Vision
|
||||
Transform MetaBuilder into a **lean framework that loads Lua code** by migrating UI boilerplate from React/TypeScript to Lua packages.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Statistics
|
||||
- **401 React components** in the codebase
|
||||
- **0 Lua UI definition files** currently
|
||||
- **UI already defined in packages** (TypeScript schemas)
|
||||
- **Lua engine operational** with Fengari runtime
|
||||
|
||||
### Existing Architecture
|
||||
```
|
||||
packages/
|
||||
└── core/
|
||||
└── package-definitions/
|
||||
├── set-a/ (forum, guestbook, spotify, youtube)
|
||||
└── set-b/ (ecommerce, irc-webchat, retro-games)
|
||||
└── irc-webchat/
|
||||
├── schema/layout.ts (UI config)
|
||||
├── actions/commands.ts
|
||||
├── actions/events.ts
|
||||
└── validation.ts
|
||||
```
|
||||
|
||||
Currently UI is defined in TypeScript but **already structured for package-based loading**.
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Phase 1: Create Lua UI Definition Format (Weeks 1-2)
|
||||
|
||||
#### 1.1 Define Lua UI Schema
|
||||
Create a Lua DSL for defining UI components:
|
||||
|
||||
```lua
|
||||
-- packages/ui/irc-webchat.lua
|
||||
return {
|
||||
metadata = {
|
||||
id = "irc-webchat",
|
||||
version = "1.0.0",
|
||||
name = "IRC Webchat",
|
||||
description = "Real-time chat interface"
|
||||
},
|
||||
|
||||
pages = {
|
||||
{
|
||||
id = "page_chat",
|
||||
path = "/chat",
|
||||
title = "IRC Webchat",
|
||||
level = 2,
|
||||
requiresAuth = true,
|
||||
requiredRole = "user",
|
||||
|
||||
components = {
|
||||
{
|
||||
id = "comp_chat_root",
|
||||
type = "IRCWebchat",
|
||||
props = {
|
||||
channelName = "general",
|
||||
maxMessages = 100,
|
||||
enableEmoji = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
components = {
|
||||
IRCWebchat = {
|
||||
defaultProps = {
|
||||
channelName = "general",
|
||||
theme = "dark"
|
||||
},
|
||||
validation = {
|
||||
channelName = { type = "string", required = true },
|
||||
maxMessages = { type = "number", min = 1, max = 1000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 Create Lua-to-React Bridge
|
||||
**New:** `src/lib/lua/ui/lua-ui-loader.ts`
|
||||
|
||||
```typescript
|
||||
import { executeLuaScript } from '@/lib/lua/engine/execute'
|
||||
import type { PackageContent } from '@/lib/packages/package-types'
|
||||
|
||||
export async function loadLuaUIPackage(
|
||||
luaSource: string
|
||||
): Promise<Pick<PackageContent, 'pages' | 'componentConfigs'>> {
|
||||
// Execute Lua and convert to TypeScript types
|
||||
const result = await executeLuaScript(luaSource, {
|
||||
sandbox: true,
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
return convertLuaToUISchema(result)
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 TypeScript Type Definitions for Lua UI
|
||||
**New:** `src/lib/lua/ui/types.ts`
|
||||
|
||||
```typescript
|
||||
export interface LuaUIMetadata {
|
||||
id: string
|
||||
version: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface LuaUIPage {
|
||||
id: string
|
||||
path: string
|
||||
title: string
|
||||
level: number
|
||||
requiresAuth?: boolean
|
||||
requiredRole?: string
|
||||
components: LuaUIComponent[]
|
||||
}
|
||||
|
||||
export interface LuaUIComponent {
|
||||
id: string
|
||||
type: string
|
||||
props: Record<string, unknown>
|
||||
children?: LuaUIComponent[]
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: Package Storage System (Weeks 2-3)
|
||||
|
||||
#### 2.1 Lua Package Store in Database
|
||||
Add Lua packages to the database schema:
|
||||
|
||||
```typescript
|
||||
// New table in Prisma schema
|
||||
model LuaPackage {
|
||||
id String @id @default(uuid())
|
||||
packageId String @unique
|
||||
version String
|
||||
name String
|
||||
category String // 'ui', 'action', 'validation'
|
||||
luaSource String @db.Text // Lua code
|
||||
metadata Json
|
||||
|
||||
tenantId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([tenantId, category])
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 Package Installation Flow
|
||||
```typescript
|
||||
// src/lib/packages/lua/install-lua-package.ts
|
||||
export async function installLuaPackage(params: {
|
||||
packageId: string
|
||||
luaSource: string
|
||||
category: 'ui' | 'action' | 'validation'
|
||||
tenantId: string
|
||||
}) {
|
||||
// 1. Validate Lua syntax
|
||||
await validateLuaSyntax(params.luaSource)
|
||||
|
||||
// 2. Execute in sandbox to extract metadata
|
||||
const metadata = await extractPackageMetadata(params.luaSource)
|
||||
|
||||
// 3. Store in database
|
||||
await db.luaPackage.create({
|
||||
data: {
|
||||
packageId: params.packageId,
|
||||
luaSource: params.luaSource,
|
||||
category: params.category,
|
||||
metadata,
|
||||
tenantId: params.tenantId,
|
||||
version: metadata.version,
|
||||
name: metadata.name
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Migration of Existing Packages (Weeks 3-5)
|
||||
|
||||
#### Priority Order
|
||||
|
||||
**Tier 1: Simple UI Packages (Week 3)**
|
||||
- Guestbook Retro
|
||||
- Forum Classic
|
||||
- Spotify Clone UI
|
||||
|
||||
**Tier 2: Medium Complexity (Week 4)**
|
||||
- IRC Webchat
|
||||
- YouTube Clone
|
||||
- Ecommerce Basic
|
||||
|
||||
**Tier 3: Complex Packages (Week 5)**
|
||||
- Retro Games
|
||||
- Package Manager UI
|
||||
- Nerd Mode IDE
|
||||
|
||||
#### Migration Template
|
||||
|
||||
For each package:
|
||||
1. Create `packages/lua-ui/{package-name}.lua`
|
||||
2. Convert TypeScript UI schema to Lua DSL
|
||||
3. Write migration tests
|
||||
4. Update package loader to support both formats
|
||||
5. Gradually deprecate TypeScript version
|
||||
|
||||
### Phase 4: Component Abstraction Layer (Weeks 5-6)
|
||||
|
||||
#### 4.1 Core React Components Become "Primitives"
|
||||
|
||||
Keep minimal React components as building blocks:
|
||||
- Form controls (Input, Button, Select)
|
||||
- Layout containers (Box, Stack, Grid)
|
||||
- Data display (Table, List, Card)
|
||||
|
||||
All composition defined in Lua.
|
||||
|
||||
#### 4.2 Component Registry
|
||||
```lua
|
||||
-- Lua can reference registered React components
|
||||
local form = Component.create("Form", {
|
||||
children = {
|
||||
Component.create("Input", { name = "username", label = "Username" }),
|
||||
Component.create("Button", { type = "submit", text = "Login" })
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Maps to React component registry:
|
||||
```typescript
|
||||
const ComponentRegistry = {
|
||||
Form: FormPrimitive,
|
||||
Input: InputPrimitive,
|
||||
Button: ButtonPrimitive,
|
||||
// ... primitives only
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 5: Runtime Package Loading (Weeks 6-7)
|
||||
|
||||
#### 5.1 Dynamic Package Loader
|
||||
```typescript
|
||||
// src/lib/packages/lua/runtime-loader.ts
|
||||
export async function loadPackageAtRuntime(packageId: string) {
|
||||
// 1. Fetch from database or cache
|
||||
const luaPackage = await fetchLuaPackage(packageId)
|
||||
|
||||
// 2. Execute Lua to get UI definition
|
||||
const uiDef = await loadLuaUIPackage(luaPackage.luaSource)
|
||||
|
||||
// 3. Generate React components on-the-fly
|
||||
return generateComponentTree(uiDef)
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.2 Hot Reload Support
|
||||
Lua packages can be updated without rebuilding the framework:
|
||||
- Update Lua code in database
|
||||
- Invalidate cache
|
||||
- UI automatically reflects changes
|
||||
|
||||
### Phase 6: Developer Experience (Weeks 7-8)
|
||||
|
||||
#### 6.1 Lua Package Development Environment
|
||||
- **Lua Editor** with syntax highlighting (Monaco)
|
||||
- **Live Preview** of UI changes
|
||||
- **Validation** feedback
|
||||
- **Version Control** for packages
|
||||
|
||||
#### 6.2 Package Publishing Flow
|
||||
1. Write Lua UI package in browser
|
||||
2. Test in sandbox
|
||||
3. Publish to tenant
|
||||
4. Package becomes available to all users
|
||||
|
||||
## Benefits
|
||||
|
||||
### 1. **Radical Reduction in Build Artifacts**
|
||||
- From 401 React components → ~20-30 primitive components
|
||||
- UI defined in database, not compiled code
|
||||
- Faster builds, smaller bundles
|
||||
|
||||
### 2. **Runtime Flexibility**
|
||||
- Update UI without deploying
|
||||
- Per-tenant customization
|
||||
- A/B testing at the package level
|
||||
|
||||
### 3. **User Empowerment**
|
||||
- Advanced users can create packages
|
||||
- Share packages across tenants
|
||||
- Package marketplace potential
|
||||
|
||||
### 4. **True Multi-Tenancy**
|
||||
- Each tenant can have different package versions
|
||||
- Custom branding via Lua
|
||||
- Isolated package updates
|
||||
|
||||
### 5. **Simplified Architecture**
|
||||
```
|
||||
BEFORE:
|
||||
TypeScript Components → Build → Bundle → Deploy
|
||||
|
||||
AFTER:
|
||||
Lua Packages → Database → Runtime Load → Render
|
||||
```
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Foundation
|
||||
- [ ] Create Lua UI DSL specification
|
||||
- [ ] Implement Lua-to-React bridge
|
||||
- [ ] Add LuaPackage database model
|
||||
- [ ] Create package validation system
|
||||
|
||||
### Core Functionality
|
||||
- [ ] Implement runtime package loader
|
||||
- [ ] Create component registry system
|
||||
- [ ] Build package installation API
|
||||
- [ ] Add package version management
|
||||
|
||||
### Migration
|
||||
- [ ] Migrate 3 simple packages (Tier 1)
|
||||
- [ ] Migrate 3 medium packages (Tier 2)
|
||||
- [ ] Migrate complex packages (Tier 3)
|
||||
- [ ] Remove deprecated TypeScript packages
|
||||
|
||||
### Developer Experience
|
||||
- [ ] Lua package editor UI
|
||||
- [ ] Live preview system
|
||||
- [ ] Package testing framework
|
||||
- [ ] Documentation & examples
|
||||
|
||||
### Production Readiness
|
||||
- [ ] Performance optimization
|
||||
- [ ] Caching strategy
|
||||
- [ ] Error handling & recovery
|
||||
- [ ] Security review & sandboxing
|
||||
|
||||
## Success Metrics
|
||||
|
||||
1. **Code Reduction:** 401 → 30 components (-92%)
|
||||
2. **Package Count:** 8 packages successfully running from Lua
|
||||
3. **Build Time:** Reduce by >50%
|
||||
4. **Bundle Size:** Reduce by >60%
|
||||
5. **Deploy Frequency:** UI updates without deploy
|
||||
|
||||
## Timeline
|
||||
|
||||
**8 weeks total** for full migration:
|
||||
- Weeks 1-2: Foundation & DSL
|
||||
- Weeks 3-5: Package migration
|
||||
- Weeks 6-7: Runtime & hot reload
|
||||
- Week 8: DX & polish
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
1. **Gradual Migration:** Both systems run in parallel
|
||||
2. **Feature Flags:** Toggle Lua/TypeScript per package
|
||||
3. **Rollback Plan:** Keep TypeScript packages until Lua proven
|
||||
4. **Testing:** Comprehensive e2e tests for each migrated package
|
||||
5. **Performance:** Monitor Lua execution times, add caching
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Get approval for Lua UI DSL spec
|
||||
2. Implement proof-of-concept with IRC Webchat
|
||||
3. Measure performance & user experience
|
||||
4. Decide on full migration vs. hybrid approach
|
||||
100
frontends/nextjs/src/lib/lua/ui/generate-component-tree.tsx
Normal file
100
frontends/nextjs/src/lib/lua/ui/generate-component-tree.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { LuaUIComponent } from './types/lua-ui-package'
|
||||
|
||||
// Component Registry - Maps Lua component types to React components
|
||||
import { Box, Stack, Typography, Button, TextField } from '@mui/material'
|
||||
|
||||
const ComponentRegistry: Record<string, React.ComponentType<any>> = {
|
||||
Box,
|
||||
Stack,
|
||||
Typography,
|
||||
Button,
|
||||
Input: TextField,
|
||||
TextArea: TextField,
|
||||
Form: 'form' as any, // Use native HTML form
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate React component tree from Lua UI definition
|
||||
* This is the bridge between Lua-defined UI and React rendering
|
||||
*/
|
||||
export function generateComponentTree(
|
||||
luaComponent: LuaUIComponent,
|
||||
key?: string | number
|
||||
): React.ReactElement {
|
||||
const Component = ComponentRegistry[luaComponent.type]
|
||||
|
||||
if (!Component) {
|
||||
console.warn(`Unknown component type: ${luaComponent.type}`)
|
||||
return <div key={key}>Unknown component: {luaComponent.type}</div>
|
||||
}
|
||||
|
||||
// Convert Lua props to React props
|
||||
const reactProps = convertLuaPropsToReact(luaComponent.props, luaComponent.type)
|
||||
|
||||
// Handle children
|
||||
const children = luaComponent.children?.map((child, index) => generateComponentTree(child, index))
|
||||
|
||||
return React.createElement(Component, { ...reactProps, key }, children)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Lua component props to React component props
|
||||
* Handles special cases like MUI component props
|
||||
*/
|
||||
function convertLuaPropsToReact(
|
||||
luaProps: Record<string, unknown>,
|
||||
componentType: string
|
||||
): Record<string, unknown> {
|
||||
const reactProps: Record<string, unknown> = { ...luaProps }
|
||||
|
||||
// Special handling for different component types
|
||||
switch (componentType) {
|
||||
case 'Input':
|
||||
case 'TextArea':
|
||||
// Convert generic props to MUI TextField props
|
||||
if (luaProps.type === 'email') {
|
||||
reactProps.type = 'email'
|
||||
}
|
||||
if (luaProps.rows) {
|
||||
reactProps.multiline = true
|
||||
reactProps.rows = luaProps.rows
|
||||
}
|
||||
break
|
||||
|
||||
case 'Button':
|
||||
// Handle button text
|
||||
if (luaProps.text) {
|
||||
// Text will be rendered as children
|
||||
delete reactProps.text
|
||||
}
|
||||
break
|
||||
|
||||
case 'Typography':
|
||||
// Handle typography text
|
||||
if (luaProps.text) {
|
||||
delete reactProps.text
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return reactProps
|
||||
}
|
||||
|
||||
/**
|
||||
* Render text content for components
|
||||
*/
|
||||
function renderComponentContent(
|
||||
componentType: string,
|
||||
props: Record<string, unknown>
|
||||
): React.ReactNode {
|
||||
switch (componentType) {
|
||||
case 'Button':
|
||||
case 'Typography':
|
||||
return props.text as string
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
66
frontends/nextjs/src/lib/lua/ui/load-lua-ui-package.ts
Normal file
66
frontends/nextjs/src/lib/lua/ui/load-lua-ui-package.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { executeLuaCode } from '@/lib/lua/functions/execution/execute-lua-code'
|
||||
import { createLuaEngine } from '@/lib/lua/engine/core/create-lua-engine'
|
||||
import type { LuaUIPackage } from './types/lua-ui-package'
|
||||
import type { LuaValue } from '@/types/utility-types'
|
||||
|
||||
/**
|
||||
* Load a Lua UI package from Lua source code
|
||||
* Executes the Lua code using existing Fengari engine and converts result to TypeScript types
|
||||
*/
|
||||
export async function loadLuaUIPackage(luaSource: string): Promise<LuaUIPackage> {
|
||||
// Create Lua engine instance using existing infrastructure
|
||||
const engine = createLuaEngine()
|
||||
|
||||
// Execute Lua code and get the returned table
|
||||
const executionResult = await executeLuaCode(engine.L, luaSource, {}, [])
|
||||
|
||||
if (!executionResult.success) {
|
||||
engine.destroy()
|
||||
throw new Error(`Lua execution failed: ${executionResult.error}`)
|
||||
}
|
||||
|
||||
const result = executionResult.result
|
||||
|
||||
// Validate result structure
|
||||
if (!isLuaUIPackage(result)) {
|
||||
engine.destroy()
|
||||
throw new Error('Invalid Lua UI package: missing required fields')
|
||||
}
|
||||
|
||||
// Clean up engine
|
||||
engine.destroy()
|
||||
|
||||
return convertLuaToUIPackage(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if value is a valid Lua UI package
|
||||
*/
|
||||
function isLuaUIPackage(value: unknown): value is LuaValue {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const pkg = value as Record<string, unknown>
|
||||
|
||||
// Check required fields
|
||||
if (!pkg.metadata || typeof pkg.metadata !== 'object') return false
|
||||
if (!pkg.pages || !Array.isArray(pkg.pages)) return false
|
||||
|
||||
const metadata = pkg.metadata as Record<string, unknown>
|
||||
if (!metadata.id || typeof metadata.id !== 'string') return false
|
||||
if (!metadata.version || typeof metadata.version !== 'string') return false
|
||||
if (!metadata.name || typeof metadata.name !== 'string') return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Lua value to TypeScript UI package
|
||||
* Uses existing Lua-to-JS converters from the Fengari engine
|
||||
*/
|
||||
function convertLuaToUIPackage(luaValue: LuaValue): LuaUIPackage {
|
||||
// The fromLua converter already handles this conversion in executeLuaCode
|
||||
// We just need to cast to the correct TypeScript type
|
||||
return luaValue as unknown as LuaUIPackage
|
||||
}
|
||||
61
frontends/nextjs/src/lib/lua/ui/types/lua-ui-package.ts
Normal file
61
frontends/nextjs/src/lib/lua/ui/types/lua-ui-package.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* TypeScript types for Lua UI packages
|
||||
* These types define the structure of UI packages written in Lua
|
||||
*/
|
||||
|
||||
export interface LuaUIMetadata {
|
||||
id: string
|
||||
version: string
|
||||
name: string
|
||||
description: string
|
||||
author?: string
|
||||
category: 'ui' | 'action' | 'validation' | 'workflow'
|
||||
dependencies?: string[]
|
||||
}
|
||||
|
||||
export interface LuaUIPage {
|
||||
id: string
|
||||
path: string
|
||||
title: string
|
||||
level: number
|
||||
requiresAuth?: boolean
|
||||
requiredRole?: string
|
||||
layout: LuaUIComponent
|
||||
}
|
||||
|
||||
export interface LuaUIComponent {
|
||||
type: string
|
||||
props: Record<string, unknown>
|
||||
children?: LuaUIComponent[]
|
||||
}
|
||||
|
||||
export interface LuaUIAction {
|
||||
name: string
|
||||
handler: string | Function
|
||||
}
|
||||
|
||||
export interface LuaUIValidation {
|
||||
[fieldName: string]: {
|
||||
type?: string
|
||||
required?: boolean
|
||||
minLength?: number
|
||||
maxLength?: number
|
||||
min?: number
|
||||
max?: number
|
||||
pattern?: string
|
||||
format?: string
|
||||
custom?: string | Function
|
||||
}
|
||||
}
|
||||
|
||||
export interface LuaUIPackage {
|
||||
metadata: LuaUIMetadata
|
||||
pages: LuaUIPage[]
|
||||
actions?: Record<string, Function>
|
||||
validation?: LuaUIValidation
|
||||
hooks?: {
|
||||
onMount?: Function
|
||||
onUnmount?: Function
|
||||
beforeRender?: Function
|
||||
}
|
||||
}
|
||||
135
frontends/nextjs/src/lib/packages/lua-ui/example-form.lua
Normal file
135
frontends/nextjs/src/lib/packages/lua-ui/example-form.lua
Normal file
@@ -0,0 +1,135 @@
|
||||
-- Example: Simple Form UI Package
|
||||
-- This demonstrates how UI can be defined in Lua instead of TypeScript/React
|
||||
|
||||
return {
|
||||
metadata = {
|
||||
id = "example-form",
|
||||
version = "1.0.0",
|
||||
name = "Example Form Package",
|
||||
description = "A simple form demonstrating Lua UI definition",
|
||||
author = "MetaBuilder",
|
||||
category = "ui"
|
||||
},
|
||||
|
||||
pages = {
|
||||
{
|
||||
id = "page_example_form",
|
||||
path = "/example-form",
|
||||
title = "Example Form",
|
||||
level = 2,
|
||||
requiresAuth = false,
|
||||
|
||||
layout = {
|
||||
type = "Stack",
|
||||
props = {
|
||||
spacing = 3,
|
||||
padding = 4
|
||||
},
|
||||
|
||||
children = {
|
||||
-- Header
|
||||
{
|
||||
type = "Typography",
|
||||
props = {
|
||||
variant = "h4",
|
||||
text = "Example Form"
|
||||
}
|
||||
},
|
||||
|
||||
-- Form
|
||||
{
|
||||
type = "Form",
|
||||
props = {
|
||||
id = "example_form",
|
||||
onSubmit = "handleFormSubmit"
|
||||
},
|
||||
|
||||
children = {
|
||||
-- Name Field
|
||||
{
|
||||
type = "Input",
|
||||
props = {
|
||||
name = "name",
|
||||
label = "Your Name",
|
||||
required = true,
|
||||
placeholder = "Enter your name"
|
||||
}
|
||||
},
|
||||
|
||||
-- Email Field
|
||||
{
|
||||
type = "Input",
|
||||
props = {
|
||||
name = "email",
|
||||
label = "Email Address",
|
||||
type = "email",
|
||||
required = true,
|
||||
validation = {
|
||||
pattern = "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
|
||||
message = "Please enter a valid email"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
-- Message Field
|
||||
{
|
||||
type = "TextArea",
|
||||
props = {
|
||||
name = "message",
|
||||
label = "Message",
|
||||
rows = 4,
|
||||
placeholder = "Enter your message"
|
||||
}
|
||||
},
|
||||
|
||||
-- Submit Button
|
||||
{
|
||||
type = "Button",
|
||||
props = {
|
||||
type = "submit",
|
||||
variant = "contained",
|
||||
color = "primary",
|
||||
text = "Submit"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
actions = {
|
||||
handleFormSubmit = function(formData)
|
||||
-- This Lua function handles form submission
|
||||
print("Form submitted with data:")
|
||||
for key, value in pairs(formData) do
|
||||
print(string.format(" %s = %s", key, value))
|
||||
end
|
||||
|
||||
-- Return success response
|
||||
return {
|
||||
success = true,
|
||||
message = "Form submitted successfully!"
|
||||
}
|
||||
end
|
||||
},
|
||||
|
||||
validation = {
|
||||
name = {
|
||||
type = "string",
|
||||
minLength = 2,
|
||||
maxLength = 100,
|
||||
required = true
|
||||
},
|
||||
email = {
|
||||
type = "string",
|
||||
format = "email",
|
||||
required = true
|
||||
},
|
||||
message = {
|
||||
type = "string",
|
||||
maxLength = 500
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,15 @@ import type { ComponentInstance } from '@/lib/types/builder-types'
|
||||
import type { User } from '@/lib/types/level-types'
|
||||
|
||||
export async function registerPage(page: PageDefinition): Promise<void> {
|
||||
this.pages.set(page.id, page)
|
||||
const pageConfig = {
|
||||
id: page.id,
|
||||
path: `/_page_${page.id}`,
|
||||
title: page.title,
|
||||
level: page.level,
|
||||
componentTree: page.components,
|
||||
requiresAuth: page.permissions?.requiresAuth || false,
|
||||
requiredRole: page.permissions?.requiredRole as any
|
||||
}
|
||||
await Database.addPage(pageConfig)
|
||||
this.pages.set(page.id, page)
|
||||
const pageConfig = {
|
||||
id: page.id,
|
||||
path: `/_page_${page.id}`,
|
||||
title: page.title,
|
||||
level: page.level,
|
||||
componentTree: page.components,
|
||||
requiresAuth: page.permissions?.requiresAuth || false,
|
||||
requiredRole: page.permissions?.requiredRole as any,
|
||||
}
|
||||
await Database.addPage(pageConfig)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { ComponentInstance } from '@/lib/types/builder-types'
|
||||
import type { User, UserRole } from '@/lib/types/level-types'
|
||||
import type { PageDefinition } from './page-renderer'
|
||||
|
||||
export function createMockPage(
|
||||
id: string,
|
||||
options: Partial<PageDefinition> = {}
|
||||
): PageDefinition {
|
||||
export function createMockPage(id: string, options: Partial<PageDefinition> = {}): PageDefinition {
|
||||
return {
|
||||
id,
|
||||
level: options.level ?? 1,
|
||||
|
||||
@@ -76,8 +76,18 @@ describe('declarative-component-renderer evaluation', () => {
|
||||
{ name: 'empty string condition', condition: '', context: {}, expected: true },
|
||||
{ name: 'null condition', condition: null as any, context: {}, expected: true },
|
||||
{ name: 'undefined condition', condition: undefined as any, context: {}, expected: true },
|
||||
{ name: 'truthy context value', condition: 'isActive', context: { isActive: true }, expected: true },
|
||||
{ name: 'falsy context value', condition: 'isActive', context: { isActive: false }, expected: false },
|
||||
{
|
||||
name: 'truthy context value',
|
||||
condition: 'isActive',
|
||||
context: { isActive: true },
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: 'falsy context value',
|
||||
condition: 'isActive',
|
||||
context: { isActive: false },
|
||||
expected: false,
|
||||
},
|
||||
{ name: 'missing context key', condition: 'missing', context: {}, expected: false },
|
||||
{ name: 'truthy string value', condition: 'name', context: { name: 'test' }, expected: true },
|
||||
{ name: 'empty string value', condition: 'name', context: { name: '' }, expected: false },
|
||||
@@ -117,8 +127,16 @@ describe('declarative-component-renderer evaluation', () => {
|
||||
{
|
||||
name: 'object array data source',
|
||||
dataSource: 'users',
|
||||
context: { users: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] },
|
||||
expected: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }],
|
||||
context: {
|
||||
users: [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
],
|
||||
},
|
||||
expected: [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
],
|
||||
},
|
||||
])('should resolve $name', ({ dataSource, context, expected }) => {
|
||||
expect(renderer.resolveDataSource(dataSource, context)).toEqual(expected)
|
||||
|
||||
@@ -127,7 +127,10 @@ describe('declarative-component-renderer lifecycle', () => {
|
||||
})
|
||||
|
||||
it('should load Lua scripts from package', () => {
|
||||
const luaExecuteSpy = vi.spyOn(DeclarativeComponentRenderer.prototype as any, 'executeLuaScript')
|
||||
const luaExecuteSpy = vi.spyOn(
|
||||
DeclarativeComponentRenderer.prototype as any,
|
||||
'executeLuaScript'
|
||||
)
|
||||
|
||||
loadPackageComponents({
|
||||
luaScripts: [
|
||||
|
||||
@@ -9,10 +9,13 @@ describe('schema-utils migration', () => {
|
||||
{ appName: 'MyApp', modelName: 'User', expected: 'MyApp_User' },
|
||||
{ appName: 'app-v2', modelName: 'User_Profile', expected: 'app-v2_User_Profile' },
|
||||
{ appName: '', modelName: 'Model', expected: '_Model' },
|
||||
])('should generate key "$expected" for app=$appName, model=$modelName', ({ appName, modelName, expected }) => {
|
||||
const result = getModelKey(appName, modelName)
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
])(
|
||||
'should generate key "$expected" for app=$appName, model=$modelName',
|
||||
({ appName, modelName, expected }) => {
|
||||
const result = getModelKey(appName, modelName)
|
||||
expect(result).toBe(expected)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('getRecordsKey', () => {
|
||||
|
||||
@@ -17,8 +17,16 @@ describe('schema-utils serialization', () => {
|
||||
describe('getFieldLabel', () => {
|
||||
it.each([
|
||||
{ field: createMockField(), expected: 'Email Address', description: 'custom label' },
|
||||
{ field: { name: 'email', type: 'email' }, expected: 'Email', description: 'auto-capitalized field name' },
|
||||
{ field: { name: 'firstName', type: 'string' }, expected: 'FirstName', description: 'multi-word field name' },
|
||||
{
|
||||
field: { name: 'email', type: 'email' },
|
||||
expected: 'Email',
|
||||
description: 'auto-capitalized field name',
|
||||
},
|
||||
{
|
||||
field: { name: 'firstName', type: 'string' },
|
||||
expected: 'FirstName',
|
||||
description: 'multi-word field name',
|
||||
},
|
||||
])('should return $description', ({ field, expected }) => {
|
||||
const result = getFieldLabel(field as FieldSchema)
|
||||
expect(result).toBe(expected)
|
||||
@@ -94,12 +102,20 @@ describe('schema-utils serialization', () => {
|
||||
|
||||
describe('getDefaultValue', () => {
|
||||
it.each([
|
||||
{ field: { name: 'count', type: 'number', default: 42 }, expected: 42, description: 'custom default' },
|
||||
{
|
||||
field: { name: 'count', type: 'number', default: 42 },
|
||||
expected: 42,
|
||||
description: 'custom default',
|
||||
},
|
||||
{ field: { name: 'text', type: 'string' }, expected: '', description: 'string type' },
|
||||
{ field: { name: 'count', type: 'number' }, expected: 0, description: 'number type' },
|
||||
{ field: { name: 'active', type: 'boolean' }, expected: false, description: 'boolean type' },
|
||||
{ field: { name: 'birthDate', type: 'date' }, expected: null, description: 'date type' },
|
||||
{ field: { name: 'createdAt', type: 'datetime' }, expected: null, description: 'datetime type' },
|
||||
{
|
||||
field: { name: 'createdAt', type: 'datetime' },
|
||||
expected: null,
|
||||
description: 'datetime type',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
name: 'status',
|
||||
@@ -198,7 +214,12 @@ describe('schema-utils serialization', () => {
|
||||
{ searchTerm: 'ALICE', searchFields: ['name'], filters: {}, expectedCount: 1 },
|
||||
{ searchTerm: 'bob', searchFields: ['name', 'email'], filters: {}, expectedCount: 1 },
|
||||
{ searchTerm: '', searchFields: ['name'], filters: { status: 'active' }, expectedCount: 2 },
|
||||
{ searchTerm: 'charlie', searchFields: ['name'], filters: { status: 'active' }, expectedCount: 1 },
|
||||
{
|
||||
searchTerm: 'charlie',
|
||||
searchFields: ['name'],
|
||||
filters: { status: 'active' },
|
||||
expectedCount: 1,
|
||||
},
|
||||
{ searchTerm: '', searchFields: ['name'], filters: {}, expectedCount: 3 },
|
||||
])(
|
||||
'should filter with searchTerm=$searchTerm, fields=$searchFields, filters=$filters',
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { FieldSchema } from '@/lib/schema-types'
|
||||
*/
|
||||
export const getDefaultValue = (field: FieldSchema): any => {
|
||||
if (field.default !== undefined) return field.default
|
||||
|
||||
|
||||
switch (field.type) {
|
||||
case 'string':
|
||||
case 'text':
|
||||
|
||||
@@ -27,7 +27,12 @@ export const validateField = (field: FieldSchema, value: any): string | null =>
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'string' || field.type === 'text' || field.type === 'email' || field.type === 'url') {
|
||||
if (
|
||||
field.type === 'string' ||
|
||||
field.type === 'text' ||
|
||||
field.type === 'email' ||
|
||||
field.type === 'url'
|
||||
) {
|
||||
const strValue = String(value)
|
||||
if (minLength !== undefined && strValue.length < minLength) {
|
||||
return `${getFieldLabel(field)} must be at least ${minLength} characters`
|
||||
|
||||
@@ -5,19 +5,15 @@
|
||||
* @param direction - Sort direction ('asc' or 'desc')
|
||||
* @returns Sorted copy of the records array
|
||||
*/
|
||||
export const sortRecords = (
|
||||
records: any[],
|
||||
field: string,
|
||||
direction: 'asc' | 'desc'
|
||||
): any[] => {
|
||||
export const sortRecords = (records: any[], field: string, direction: 'asc' | 'desc'): any[] => {
|
||||
return [...records].sort((a, b) => {
|
||||
const aVal = a[field]
|
||||
const bVal = b[field]
|
||||
|
||||
|
||||
if (aVal === bVal) return 0
|
||||
if (aVal === null || aVal === undefined) return 1
|
||||
if (bVal === null || bVal === undefined) return -1
|
||||
|
||||
|
||||
const comparison = aVal < bVal ? -1 : 1
|
||||
return direction === 'asc' ? comparison : -comparison
|
||||
})
|
||||
|
||||
@@ -7,10 +7,7 @@ import { validateField } from '../field/validate-field'
|
||||
* @param record - The record to validate
|
||||
* @returns Object mapping field names to error messages
|
||||
*/
|
||||
export const validateRecord = (
|
||||
model: ModelSchema,
|
||||
record: any
|
||||
): Record<string, string> => {
|
||||
export const validateRecord = (model: ModelSchema, record: any): Record<string, string> => {
|
||||
const errors: Record<string, string> = {}
|
||||
|
||||
for (const field of model.fields) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/**
|
||||
* Schema utilities - Re-exports from individual function files
|
||||
*
|
||||
*
|
||||
* This file maintains backward compatibility.
|
||||
* Prefer importing from SchemaUtils class or individual functions:
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // Class pattern (recommended)
|
||||
* import { SchemaUtils } from '@/lib/schema/SchemaUtils'
|
||||
* SchemaUtils.validateField(field, value)
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // Individual function import
|
||||
* import { validateField } from '@/lib/schema/functions/validate-field'
|
||||
|
||||
@@ -22,9 +22,10 @@ export async function requestScreenshotAnalysis(
|
||||
const data = (await response.json().catch(() => null)) as unknown
|
||||
|
||||
if (!response.ok || !isValidResult(data)) {
|
||||
const message = (typeof data === 'object' && data !== null && 'error' in data && typeof data.error === 'string')
|
||||
? data.error
|
||||
: 'Analysis failed'
|
||||
const message =
|
||||
typeof data === 'object' && data !== null && 'error' in data && typeof data.error === 'string'
|
||||
? data.error
|
||||
: 'Analysis failed'
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ const basePayload = {
|
||||
url: 'https://example.com',
|
||||
viewport: { width: 1280, height: 720 },
|
||||
textSample: 'Hello world from the screenshot analyzer.',
|
||||
htmlSample: '<h1>Title</h1><h2>Subtitle</h2><img src="x" alt="desc" /><a href="#">Link</a><button>Go</button>',
|
||||
htmlSample:
|
||||
'<h1>Title</h1><h2>Subtitle</h2><img src="x" alt="desc" /><a href="#">Link</a><button>Go</button>',
|
||||
}
|
||||
|
||||
describe('ScreenshotAnalysisService', () => {
|
||||
@@ -32,7 +33,7 @@ describe('ScreenshotAnalysisService', () => {
|
||||
htmlSample: '<img src="x" /><button>Save</button>',
|
||||
})
|
||||
|
||||
expect(result.warnings.some((warning) => warning.includes('No H1'))).toBe(true)
|
||||
expect(result.warnings.some((warning) => warning.includes('missing alt'))).toBe(true)
|
||||
expect(result.warnings.some(warning => warning.includes('No H1'))).toBe(true)
|
||||
expect(result.warnings.some(warning => warning.includes('missing alt'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,7 +33,7 @@ export class ScreenshotAnalysisService {
|
||||
h2Count,
|
||||
h3Count,
|
||||
imgCount: imgTags.length,
|
||||
imgMissingAltCount: imgTags.filter((tag) => !/\salt=/.test(tag)).length,
|
||||
imgMissingAltCount: imgTags.filter(tag => !/\salt=/.test(tag)).length,
|
||||
linkCount: this.countMatches(/<a\b[^>]*>/gi, htmlSample),
|
||||
buttonCount: this.countMatches(/<button\b[^>]*>/gi, htmlSample),
|
||||
formCount: this.countMatches(/<form\b[^>]*>/gi, htmlSample),
|
||||
@@ -96,7 +96,7 @@ export class ScreenshotAnalysisService {
|
||||
if (warnings.length === 0) {
|
||||
lines.push('- None detected')
|
||||
} else {
|
||||
warnings.forEach((warning) => lines.push(`- ${warning}`))
|
||||
warnings.forEach(warning => lines.push(`- ${warning}`))
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
|
||||
@@ -11,5 +11,5 @@ import { JAVASCRIPT_XSS_PATTERNS } from './javascript/xss'
|
||||
export const JAVASCRIPT_PATTERNS: SecurityPattern[] = [
|
||||
...JAVASCRIPT_INJECTION_PATTERNS,
|
||||
...JAVASCRIPT_XSS_PATTERNS,
|
||||
...JAVASCRIPT_MISC_PATTERNS
|
||||
...JAVASCRIPT_MISC_PATTERNS,
|
||||
]
|
||||
|
||||
@@ -6,48 +6,49 @@ export const JAVASCRIPT_INJECTION_PATTERNS: SecurityPattern[] = [
|
||||
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'
|
||||
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'
|
||||
recommendation: 'Avoid dynamic code generation or use with extreme caution',
|
||||
},
|
||||
{
|
||||
pattern: /import\s+.*\s+from\s+['"]https?:/gi,
|
||||
type: 'dangerous',
|
||||
severity: 'critical',
|
||||
message: 'Remote code import detected',
|
||||
recommendation: 'Only import from trusted, local sources'
|
||||
recommendation: 'Only import from trusted, local sources',
|
||||
},
|
||||
{
|
||||
pattern: /setTimeout\s*\(\s*['"`]/gi,
|
||||
type: 'dangerous',
|
||||
severity: 'high',
|
||||
message: 'setTimeout with string argument detected',
|
||||
recommendation: 'Use setTimeout with function reference instead'
|
||||
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'
|
||||
recommendation: 'Use setInterval with function reference instead',
|
||||
},
|
||||
{
|
||||
pattern: /require\s*\(\s*[^'"`]/gi,
|
||||
type: 'dangerous',
|
||||
severity: 'high',
|
||||
message: 'Dynamic require() detected',
|
||||
recommendation: 'Use static imports only'
|
||||
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'
|
||||
}
|
||||
recommendation: 'This is not allowed in browser environment',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -6,76 +6,76 @@ export const JAVASCRIPT_MISC_PATTERNS: SecurityPattern[] = [
|
||||
type: 'suspicious',
|
||||
severity: 'medium',
|
||||
message: 'Calling functions with window context',
|
||||
recommendation: 'Be careful with context manipulation'
|
||||
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'
|
||||
recommendation: 'Be careful with context manipulation',
|
||||
},
|
||||
{
|
||||
pattern: /__proto__/gi,
|
||||
type: 'dangerous',
|
||||
severity: 'critical',
|
||||
message: 'Prototype pollution attempt detected',
|
||||
recommendation: 'Never manipulate __proto__ directly'
|
||||
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'
|
||||
recommendation: 'Use Object.create() or proper class syntax',
|
||||
},
|
||||
{
|
||||
pattern: /localStorage|sessionStorage/gi,
|
||||
type: 'warning',
|
||||
severity: 'low',
|
||||
message: 'Local/session storage usage detected',
|
||||
recommendation: 'Use useKV hook for persistent data instead'
|
||||
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'
|
||||
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'
|
||||
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'
|
||||
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'
|
||||
recommendation: 'Use React Router or validate URLs carefully',
|
||||
},
|
||||
{
|
||||
pattern: /fs\.|path\.|os\./gi,
|
||||
type: 'malicious',
|
||||
severity: 'critical',
|
||||
message: 'Node.js system module usage detected',
|
||||
recommendation: 'File system access not allowed in browser'
|
||||
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'
|
||||
}
|
||||
recommendation: 'Not applicable in browser environment',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -6,48 +6,48 @@ export const JAVASCRIPT_XSS_PATTERNS: SecurityPattern[] = [
|
||||
type: 'dangerous',
|
||||
severity: 'high',
|
||||
message: 'innerHTML assignment detected - XSS vulnerability risk',
|
||||
recommendation: 'Use textContent, createElement, or React JSX instead'
|
||||
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'
|
||||
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'
|
||||
recommendation: 'Use DOM manipulation methods instead',
|
||||
},
|
||||
{
|
||||
pattern: /<script[^>]*>/gi,
|
||||
type: 'dangerous',
|
||||
severity: 'critical',
|
||||
message: 'Script tag injection detected',
|
||||
recommendation: 'Never inject script tags dynamically'
|
||||
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'
|
||||
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'
|
||||
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'
|
||||
}
|
||||
recommendation: 'Avoid data URIs with executable content',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -11,76 +11,76 @@ export const LUA_PATTERNS: SecurityPattern[] = [
|
||||
type: 'malicious',
|
||||
severity: 'critical',
|
||||
message: 'Lua OS module system call detected',
|
||||
recommendation: 'OS module access is disabled for security'
|
||||
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'
|
||||
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'
|
||||
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'
|
||||
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'
|
||||
recommendation: 'Limited debug functionality available',
|
||||
},
|
||||
{
|
||||
pattern: /loadstring\s*\(/gi,
|
||||
type: 'dangerous',
|
||||
severity: 'high',
|
||||
message: 'Lua dynamic code execution detected',
|
||||
recommendation: 'Use with extreme caution'
|
||||
recommendation: 'Use with extreme caution',
|
||||
},
|
||||
{
|
||||
pattern: /\.\.\s*[[\]]/gi,
|
||||
type: 'suspicious',
|
||||
severity: 'medium',
|
||||
message: 'Potential Lua table manipulation',
|
||||
recommendation: 'Ensure proper validation'
|
||||
recommendation: 'Ensure proper validation',
|
||||
},
|
||||
{
|
||||
pattern: /_G\s*\[/gi,
|
||||
type: 'suspicious',
|
||||
severity: 'high',
|
||||
message: 'Global environment manipulation detected',
|
||||
recommendation: 'Avoid modifying global environment'
|
||||
recommendation: 'Avoid modifying global environment',
|
||||
},
|
||||
{
|
||||
pattern: /getmetatable|setmetatable/gi,
|
||||
type: 'suspicious',
|
||||
severity: 'medium',
|
||||
message: 'Metatable manipulation detected',
|
||||
recommendation: 'Use carefully to avoid security issues'
|
||||
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'
|
||||
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'
|
||||
}
|
||||
recommendation: 'Ensure recursion has proper termination',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -11,27 +11,27 @@ export const SQL_INJECTION_PATTERNS: SecurityPattern[] = [
|
||||
type: 'malicious',
|
||||
severity: 'critical',
|
||||
message: 'SQL injection attempt detected',
|
||||
recommendation: 'Use parameterized queries'
|
||||
recommendation: 'Use parameterized queries',
|
||||
},
|
||||
{
|
||||
pattern: /UNION\s+SELECT/gi,
|
||||
type: 'malicious',
|
||||
severity: 'critical',
|
||||
message: 'SQL UNION injection attempt',
|
||||
recommendation: 'Use parameterized queries'
|
||||
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'
|
||||
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'
|
||||
}
|
||||
recommendation: 'May indicate SQL injection attempt',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -23,7 +23,7 @@ export const scanHTML = (html: string): SecurityScanResult => {
|
||||
severity: 'critical',
|
||||
message: 'Script tag detected in HTML',
|
||||
pattern: match[0].substring(0, 50) + '...',
|
||||
recommendation: 'Remove script tags or use proper React components'
|
||||
recommendation: 'Remove script tags or use proper React components',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export const scanHTML = (html: string): SecurityScanResult => {
|
||||
severity: 'high',
|
||||
message: 'Inline event handler in HTML',
|
||||
pattern: match[0],
|
||||
recommendation: 'Use React event handlers instead'
|
||||
recommendation: 'Use React event handlers instead',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export const scanHTML = (html: string): SecurityScanResult => {
|
||||
severity: 'critical',
|
||||
message: 'javascript: protocol in href',
|
||||
pattern: 'javascript:',
|
||||
recommendation: 'Use proper URLs or event handlers'
|
||||
recommendation: 'Use proper URLs or event handlers',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export const scanHTML = (html: string): SecurityScanResult => {
|
||||
severity: 'medium',
|
||||
message: 'Iframe without sandbox attribute',
|
||||
pattern: match[0],
|
||||
recommendation: 'Add sandbox attribute to iframes for security'
|
||||
recommendation: 'Add sandbox attribute to iframes for security',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,6 @@ export const scanHTML = (html: string): SecurityScanResult => {
|
||||
return {
|
||||
safe,
|
||||
severity,
|
||||
issues
|
||||
issues,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export const scanJavaScript = (code: string): SecurityScanResult => {
|
||||
message: pattern.message,
|
||||
pattern: match[0],
|
||||
line: lineNumber,
|
||||
recommendation: pattern.recommendation
|
||||
recommendation: pattern.recommendation,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export const scanJavaScript = (code: string): SecurityScanResult => {
|
||||
message: pattern.message,
|
||||
pattern: match[0],
|
||||
line: lineNumber,
|
||||
recommendation: pattern.recommendation
|
||||
recommendation: pattern.recommendation,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,6 @@ export const scanJavaScript = (code: string): SecurityScanResult => {
|
||||
safe,
|
||||
severity,
|
||||
issues,
|
||||
sanitizedCode: safe ? code : undefined
|
||||
sanitizedCode: safe ? code : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const scanJSON = (jsonString: string): SecurityScanResult => {
|
||||
severity: 'medium',
|
||||
message: 'Invalid JSON format',
|
||||
pattern: 'JSON parse error',
|
||||
recommendation: 'Ensure JSON is properly formatted'
|
||||
recommendation: 'Ensure JSON is properly formatted',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export const scanJSON = (jsonString: string): SecurityScanResult => {
|
||||
severity: 'critical',
|
||||
message: 'Prototype pollution attempt in JSON',
|
||||
pattern: '__proto__',
|
||||
recommendation: 'Remove prototype manipulation from JSON'
|
||||
recommendation: 'Remove prototype manipulation from JSON',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const scanJSON = (jsonString: string): SecurityScanResult => {
|
||||
severity: 'critical',
|
||||
message: 'Script tag in JSON data',
|
||||
pattern: '<script>',
|
||||
recommendation: 'Remove all HTML/script content from JSON'
|
||||
recommendation: 'Remove all HTML/script content from JSON',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,6 +57,6 @@ export const scanJSON = (jsonString: string): SecurityScanResult => {
|
||||
safe,
|
||||
severity,
|
||||
issues,
|
||||
sanitizedCode: safe ? jsonString : undefined
|
||||
sanitizedCode: safe ? jsonString : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const scanLua = (code: string): SecurityScanResult => {
|
||||
message: pattern.message,
|
||||
pattern: match[0],
|
||||
line: lineNumber,
|
||||
recommendation: pattern.recommendation
|
||||
recommendation: pattern.recommendation,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,6 @@ export const scanLua = (code: string): SecurityScanResult => {
|
||||
safe,
|
||||
severity,
|
||||
issues,
|
||||
sanitizedCode: safe ? code : undefined
|
||||
sanitizedCode: safe ? code : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* @returns Sanitized string
|
||||
*/
|
||||
export const sanitizeInput = (
|
||||
input: string,
|
||||
input: string,
|
||||
type: 'text' | 'html' | 'json' | 'javascript' | 'lua' = 'text'
|
||||
): string => {
|
||||
let sanitized = input
|
||||
|
||||
@@ -24,6 +24,6 @@ export const calculateOverallSeverity = (
|
||||
if (hasHigh) return 'high'
|
||||
if (hasMedium) return 'medium'
|
||||
if (hasLow) return 'low'
|
||||
|
||||
|
||||
return 'safe'
|
||||
}
|
||||
|
||||
@@ -30,7 +30,14 @@ describe('security-scanner detection', () => {
|
||||
},
|
||||
])(
|
||||
'should $name',
|
||||
({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern, expectedLine }) => {
|
||||
({
|
||||
code,
|
||||
expectedSeverity,
|
||||
expectedSafe,
|
||||
expectedIssueType,
|
||||
expectedIssuePattern,
|
||||
expectedLine,
|
||||
}) => {
|
||||
const result = securityScanner.scanJavaScript(code)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
expect(result.safe).toBe(expectedSafe)
|
||||
@@ -38,7 +45,9 @@ describe('security-scanner detection', () => {
|
||||
if (expectedIssueType || expectedIssuePattern) {
|
||||
const issue = result.issues.find(item => {
|
||||
const matchesType = expectedIssueType ? item.type === expectedIssueType : true
|
||||
const matchesPattern = expectedIssuePattern ? item.pattern.includes(expectedIssuePattern) : true
|
||||
const matchesPattern = expectedIssuePattern
|
||||
? item.pattern.includes(expectedIssuePattern)
|
||||
: true
|
||||
return matchesType && matchesPattern
|
||||
})
|
||||
expect(issue).toBeDefined()
|
||||
@@ -74,28 +83,33 @@ describe('security-scanner detection', () => {
|
||||
expectedSeverity: 'safe',
|
||||
expectedSafe: true,
|
||||
},
|
||||
])('should $name', ({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern }) => {
|
||||
const result = securityScanner.scanLua(code)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
expect(result.safe).toBe(expectedSafe)
|
||||
])(
|
||||
'should $name',
|
||||
({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern }) => {
|
||||
const result = securityScanner.scanLua(code)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
expect(result.safe).toBe(expectedSafe)
|
||||
|
||||
if (expectedIssueType || expectedIssuePattern) {
|
||||
const issue = result.issues.find(item => {
|
||||
const matchesType = expectedIssueType ? item.type === expectedIssueType : true
|
||||
const matchesPattern = expectedIssuePattern ? item.pattern.includes(expectedIssuePattern) : true
|
||||
return matchesType && matchesPattern
|
||||
})
|
||||
expect(issue).toBeDefined()
|
||||
} else {
|
||||
expect(result.issues.length).toBe(0)
|
||||
}
|
||||
if (expectedIssueType || expectedIssuePattern) {
|
||||
const issue = result.issues.find(item => {
|
||||
const matchesType = expectedIssueType ? item.type === expectedIssueType : true
|
||||
const matchesPattern = expectedIssuePattern
|
||||
? item.pattern.includes(expectedIssuePattern)
|
||||
: true
|
||||
return matchesType && matchesPattern
|
||||
})
|
||||
expect(issue).toBeDefined()
|
||||
} else {
|
||||
expect(result.issues.length).toBe(0)
|
||||
}
|
||||
|
||||
if (expectedSafe) {
|
||||
expect(result.sanitizedCode).toBe(code)
|
||||
} else {
|
||||
expect(result.sanitizedCode).toBeUndefined()
|
||||
if (expectedSafe) {
|
||||
expect(result.sanitizedCode).toBe(code)
|
||||
} else {
|
||||
expect(result.sanitizedCode).toBeUndefined()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('scanJSON', () => {
|
||||
@@ -170,7 +184,8 @@ describe('security-scanner detection', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'remove script tags and inline handlers from text',
|
||||
input: '<div onclick="alert(1)">Click</div><script>alert(2)</script><a href="javascript:alert(3)">x</a>',
|
||||
input:
|
||||
'<div onclick="alert(1)">Click</div><script>alert(2)</script><a href="javascript:alert(3)">x</a>',
|
||||
type: 'text' as const,
|
||||
shouldExclude: ['<script', 'onclick', 'javascript:'],
|
||||
},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Security Scanner
|
||||
*
|
||||
*
|
||||
* Wraps individual scan functions into a unified class interface.
|
||||
* Each scanning function is implemented as 1 lambda/function per file
|
||||
* in the functions/ directory.
|
||||
*
|
||||
*
|
||||
* Pattern: Lambda wrapper class - keeps this file short while maintaining
|
||||
* a convenient class-based API.
|
||||
*/
|
||||
@@ -24,7 +24,7 @@ import { getSeverityIcon } from '../functions/helpers/get-severity-icon'
|
||||
|
||||
/**
|
||||
* SecurityScanner - Wrapper class for security scanning functions
|
||||
*
|
||||
*
|
||||
* All methods delegate to individual lambda functions in functions/scanners/
|
||||
* This keeps the main class file small while providing a unified API.
|
||||
*/
|
||||
@@ -45,11 +45,4 @@ export const securityScanner = new SecurityScanner()
|
||||
export { getSeverityColor, getSeverityIcon }
|
||||
|
||||
// Re-export individual functions for direct use
|
||||
export {
|
||||
scanJavaScript,
|
||||
scanLua,
|
||||
scanJSON,
|
||||
scanHTML,
|
||||
scanForVulnerabilities,
|
||||
sanitizeInput
|
||||
}
|
||||
export { scanJavaScript, scanLua, scanJSON, scanHTML, scanForVulnerabilities, sanitizeInput }
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function logOperation(
|
||||
errorMessage,
|
||||
ipAddress: ctx.ipAddress,
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
addAuditLog(log)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
// Types
|
||||
export type {
|
||||
OperationType,
|
||||
ResourceType,
|
||||
AuditLog,
|
||||
SecurityContext,
|
||||
AccessRule
|
||||
} from './types'
|
||||
export type { OperationType, ResourceType, AuditLog, SecurityContext, AccessRule } from './types'
|
||||
|
||||
// Core functions
|
||||
export { ACCESS_RULES } from './query/access-rules'
|
||||
@@ -76,7 +70,7 @@ export class SecureDatabase {
|
||||
static sanitizeInput = sanitizeInput
|
||||
static logOperation = logOperation
|
||||
static executeQuery = executeQuery
|
||||
|
||||
|
||||
// Operations
|
||||
static getUsers = getUsers
|
||||
static getUserById = getUserById
|
||||
|
||||
@@ -9,9 +9,7 @@ export interface LoginSecurityContextParams {
|
||||
requestId?: string
|
||||
}
|
||||
|
||||
export const createLoginSecurityContext = (
|
||||
params: LoginSecurityContextParams
|
||||
): SecurityContext => {
|
||||
export const createLoginSecurityContext = (params: LoginSecurityContextParams): SecurityContext => {
|
||||
const sanitizedIdentifier = sanitizeInput(params.identifier).trim()
|
||||
const loginKey = sanitizedIdentifier.length > 0 ? sanitizedIdentifier : 'public'
|
||||
|
||||
|
||||
@@ -17,7 +17,10 @@ const parsePositiveInt = (value: string | undefined, fallback: number): number =
|
||||
|
||||
const lockoutConfig = {
|
||||
windowMs: parsePositiveInt(process.env.MB_AUTH_LOCKOUT_WINDOW_MS, DEFAULT_AUTH_LOCKOUT_WINDOW_MS),
|
||||
maxAttempts: parsePositiveInt(process.env.MB_AUTH_LOCKOUT_MAX_ATTEMPTS, DEFAULT_AUTH_LOCKOUT_MAX_ATTEMPTS),
|
||||
maxAttempts: parsePositiveInt(
|
||||
process.env.MB_AUTH_LOCKOUT_MAX_ATTEMPTS,
|
||||
DEFAULT_AUTH_LOCKOUT_MAX_ATTEMPTS
|
||||
),
|
||||
lockoutMs: parsePositiveInt(process.env.MB_AUTH_LOCKOUT_MS, DEFAULT_AUTH_LOCKOUT_MS),
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import { loginAttemptTracker } from '../login-attempt-tracker'
|
||||
* Verify user credentials with security checks
|
||||
*/
|
||||
export async function verifyCredentials(
|
||||
ctx: SecurityContext,
|
||||
username: string,
|
||||
ctx: SecurityContext,
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<boolean> {
|
||||
const sanitizedUsername = sanitizeInput(username).trim()
|
||||
|
||||
@@ -7,11 +7,5 @@ import { executeQuery } from '../../execute-query'
|
||||
* Get comments with security checks
|
||||
*/
|
||||
export async function getComments(ctx: SecurityContext): Promise<Comment[]> {
|
||||
return executeQuery(
|
||||
ctx,
|
||||
'comment',
|
||||
'READ',
|
||||
async () => fetchComments(),
|
||||
'all_comments'
|
||||
)
|
||||
return executeQuery(ctx, 'comment', 'READ', async () => fetchComments(), 'all_comments')
|
||||
}
|
||||
|
||||
@@ -7,11 +7,5 @@ import { executeQuery } from '../../execute-query'
|
||||
* Get Lua scripts with security checks
|
||||
*/
|
||||
export async function getLuaScripts(ctx: SecurityContext): Promise<LuaScript[]> {
|
||||
return executeQuery(
|
||||
ctx,
|
||||
'luaScript',
|
||||
'READ',
|
||||
async () => fetchLuaScripts(),
|
||||
'all_lua_scripts'
|
||||
)
|
||||
return executeQuery(ctx, 'luaScript', 'READ', async () => fetchLuaScripts(), 'all_lua_scripts')
|
||||
}
|
||||
|
||||
@@ -7,11 +7,5 @@ import { executeQuery } from '../../execute-query'
|
||||
* Get model schemas with security checks
|
||||
*/
|
||||
export async function getModelSchemas(ctx: SecurityContext): Promise<ModelSchema[]> {
|
||||
return executeQuery(
|
||||
ctx,
|
||||
'modelSchema',
|
||||
'READ',
|
||||
async () => fetchSchemas(),
|
||||
'all_model_schemas'
|
||||
)
|
||||
return executeQuery(ctx, 'modelSchema', 'READ', async () => fetchSchemas(), 'all_model_schemas')
|
||||
}
|
||||
|
||||
@@ -7,11 +7,5 @@ import { executeQuery } from '../../execute-query'
|
||||
* Get page configs with security checks
|
||||
*/
|
||||
export async function getPageConfigs(ctx: SecurityContext): Promise<PageConfig[]> {
|
||||
return executeQuery(
|
||||
ctx,
|
||||
'pageConfig',
|
||||
'READ',
|
||||
async () => fetchPages(),
|
||||
'all_page_configs'
|
||||
)
|
||||
return executeQuery(ctx, 'pageConfig', 'READ', async () => fetchPages(), 'all_page_configs')
|
||||
}
|
||||
|
||||
@@ -7,11 +7,5 @@ import { executeQuery } from '../../execute-query'
|
||||
* Get workflows with security checks
|
||||
*/
|
||||
export async function getWorkflows(ctx: SecurityContext): Promise<Workflow[]> {
|
||||
return executeQuery(
|
||||
ctx,
|
||||
'workflow',
|
||||
'READ',
|
||||
async () => fetchWorkflows(),
|
||||
'all_workflows'
|
||||
)
|
||||
return executeQuery(ctx, 'workflow', 'READ', async () => fetchWorkflows(), 'all_workflows')
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { sanitizeInput } from '../sanitize-input'
|
||||
* Create a new user with security checks
|
||||
*/
|
||||
export async function createUser(
|
||||
ctx: SecurityContext,
|
||||
ctx: SecurityContext,
|
||||
userData: Omit<User, 'id' | 'createdAt'>
|
||||
): Promise<User> {
|
||||
const sanitized = sanitizeInput(userData)
|
||||
@@ -29,7 +29,7 @@ export async function createUser(
|
||||
tenantId: tenantId ?? sanitized.tenantId,
|
||||
isInstanceOwner: sanitized.isInstanceOwner ?? false,
|
||||
}
|
||||
|
||||
|
||||
return executeQuery(
|
||||
ctx,
|
||||
'user',
|
||||
|
||||
@@ -12,10 +12,7 @@ export async function getUserById(ctx: SecurityContext, userId: string): Promise
|
||||
'user',
|
||||
'READ',
|
||||
async () =>
|
||||
Database.getUserById(
|
||||
userId,
|
||||
ctx.user.tenantId ? { tenantId: ctx.user.tenantId } : undefined
|
||||
),
|
||||
Database.getUserById(userId, ctx.user.tenantId ? { tenantId: ctx.user.tenantId } : undefined),
|
||||
userId
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import { sanitizeInput } from '../sanitize-input'
|
||||
* Update a user with security checks
|
||||
*/
|
||||
export async function updateUser(
|
||||
ctx: SecurityContext,
|
||||
userId: string,
|
||||
ctx: SecurityContext,
|
||||
userId: string,
|
||||
updates: Partial<User>
|
||||
): Promise<User> {
|
||||
const sanitized = sanitizeInput(updates)
|
||||
@@ -18,7 +18,7 @@ export async function updateUser(
|
||||
if (tenantId && sanitized.tenantId && sanitized.tenantId !== tenantId) {
|
||||
throw new Error('Access denied. Cannot change user tenant.')
|
||||
}
|
||||
|
||||
|
||||
return executeQuery(
|
||||
ctx,
|
||||
'user',
|
||||
@@ -32,10 +32,7 @@ export async function updateUser(
|
||||
}
|
||||
|
||||
await Database.updateUser(userId, sanitized)
|
||||
const updated = await Database.getUserById(
|
||||
userId,
|
||||
tenantId ? { tenantId } : undefined
|
||||
)
|
||||
const updated = await Database.getUserById(userId, tenantId ? { tenantId } : undefined)
|
||||
if (!updated) {
|
||||
throw new Error('User not found after update')
|
||||
}
|
||||
|
||||
@@ -1,42 +1,74 @@
|
||||
import type { AccessRule } from './types'
|
||||
|
||||
export const ACCESS_RULES: AccessRule[] = [
|
||||
{ resource: 'user', operation: 'READ', allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'] },
|
||||
{
|
||||
resource: 'user',
|
||||
operation: 'READ',
|
||||
allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'],
|
||||
},
|
||||
{ resource: 'user', operation: 'CREATE', allowedRoles: ['god', 'supergod'] },
|
||||
{ resource: 'user', operation: 'UPDATE', allowedRoles: ['admin', 'god', 'supergod'] },
|
||||
{ resource: 'user', operation: 'DELETE', allowedRoles: ['god', 'supergod'] },
|
||||
|
||||
|
||||
{ resource: 'workflow', operation: 'READ', allowedRoles: ['admin', 'god', 'supergod'] },
|
||||
{ resource: 'workflow', operation: 'CREATE', allowedRoles: ['god', 'supergod'] },
|
||||
{ resource: 'workflow', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] },
|
||||
{ resource: 'workflow', operation: 'DELETE', allowedRoles: ['god', 'supergod'] },
|
||||
|
||||
|
||||
{ resource: 'luaScript', operation: 'READ', allowedRoles: ['god', 'supergod'] },
|
||||
{ resource: 'luaScript', operation: 'CREATE', allowedRoles: ['god', 'supergod'] },
|
||||
{ resource: 'luaScript', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] },
|
||||
{ resource: 'luaScript', operation: 'DELETE', allowedRoles: ['god', 'supergod'] },
|
||||
|
||||
{ resource: 'pageConfig', operation: 'READ', allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'] },
|
||||
|
||||
{
|
||||
resource: 'pageConfig',
|
||||
operation: 'READ',
|
||||
allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'],
|
||||
},
|
||||
{ resource: 'pageConfig', operation: 'CREATE', allowedRoles: ['god', 'supergod'] },
|
||||
{ resource: 'pageConfig', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] },
|
||||
{ resource: 'pageConfig', operation: 'DELETE', allowedRoles: ['god', 'supergod'] },
|
||||
|
||||
|
||||
{ resource: 'modelSchema', operation: 'READ', allowedRoles: ['admin', 'god', 'supergod'] },
|
||||
{ resource: 'modelSchema', operation: 'CREATE', allowedRoles: ['god', 'supergod'] },
|
||||
{ resource: 'modelSchema', operation: 'UPDATE', allowedRoles: ['god', 'supergod'] },
|
||||
{ resource: 'modelSchema', operation: 'DELETE', allowedRoles: ['god', 'supergod'] },
|
||||
|
||||
{ resource: 'comment', operation: 'READ', allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'] },
|
||||
{ resource: 'comment', operation: 'CREATE', allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'] },
|
||||
{ resource: 'comment', operation: 'UPDATE', allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'] },
|
||||
{ resource: 'comment', operation: 'DELETE', allowedRoles: ['moderator', 'admin', 'god', 'supergod'] },
|
||||
|
||||
|
||||
{
|
||||
resource: 'comment',
|
||||
operation: 'READ',
|
||||
allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'],
|
||||
},
|
||||
{
|
||||
resource: 'comment',
|
||||
operation: 'CREATE',
|
||||
allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'],
|
||||
},
|
||||
{
|
||||
resource: 'comment',
|
||||
operation: 'UPDATE',
|
||||
allowedRoles: ['user', 'moderator', 'admin', 'god', 'supergod'],
|
||||
},
|
||||
{
|
||||
resource: 'comment',
|
||||
operation: 'DELETE',
|
||||
allowedRoles: ['moderator', 'admin', 'god', 'supergod'],
|
||||
},
|
||||
|
||||
{ resource: 'smtpConfig', operation: 'READ', allowedRoles: ['god', 'supergod'] },
|
||||
{ resource: 'smtpConfig', operation: 'UPDATE', allowedRoles: ['supergod'] },
|
||||
|
||||
{ resource: 'credential', operation: 'READ', allowedRoles: ['public', 'user', 'admin', 'god', 'supergod'] },
|
||||
{ resource: 'credential', operation: 'UPDATE', allowedRoles: ['user', 'admin', 'god', 'supergod'] },
|
||||
|
||||
|
||||
{
|
||||
resource: 'credential',
|
||||
operation: 'READ',
|
||||
allowedRoles: ['public', 'user', 'admin', 'god', 'supergod'],
|
||||
},
|
||||
{
|
||||
resource: 'credential',
|
||||
operation: 'UPDATE',
|
||||
allowedRoles: ['user', 'admin', 'god', 'supergod'],
|
||||
},
|
||||
|
||||
{ resource: 'tenant', operation: 'READ', allowedRoles: ['god', 'supergod'] },
|
||||
{ resource: 'tenant', operation: 'CREATE', allowedRoles: ['supergod'] },
|
||||
{ resource: 'tenant', operation: 'UPDATE', allowedRoles: ['supergod'] },
|
||||
|
||||
@@ -10,21 +10,19 @@ export async function checkAccess(
|
||||
operation: OperationType,
|
||||
resourceId?: string
|
||||
): Promise<boolean> {
|
||||
const rule = ACCESS_RULES.find(
|
||||
r => r.resource === resource && r.operation === operation
|
||||
)
|
||||
|
||||
const rule = ACCESS_RULES.find(r => r.resource === resource && r.operation === operation)
|
||||
|
||||
if (!rule) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if (!rule.allowedRoles.includes(ctx.user.role)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if (rule.customCheck) {
|
||||
return await rule.customCheck(ctx, resourceId)
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -7,15 +7,13 @@ export function checkRateLimit(userId: string): boolean {
|
||||
const { windowMs, maxRequests } = getRateLimitConfig()
|
||||
const now = Date.now()
|
||||
const userRequests = rateLimitMap.get(userId) || []
|
||||
|
||||
const recentRequests = userRequests.filter(
|
||||
timestamp => now - timestamp < windowMs
|
||||
)
|
||||
|
||||
|
||||
const recentRequests = userRequests.filter(timestamp => now - timestamp < windowMs)
|
||||
|
||||
if (recentRequests.length >= maxRequests) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
recentRequests.push(now)
|
||||
rateLimitMap.set(userId, recentRequests)
|
||||
return true
|
||||
|
||||
@@ -15,7 +15,10 @@ const parsePositiveInt = (value: string | null | undefined, fallback: number): n
|
||||
|
||||
const getEnvRateLimitConfig = () => ({
|
||||
windowMs: parsePositiveInt(process.env.MB_RATE_LIMIT_WINDOW_MS, DEFAULT_RATE_LIMIT_WINDOW_MS),
|
||||
maxRequests: parsePositiveInt(process.env.MB_RATE_LIMIT_MAX_REQUESTS, DEFAULT_MAX_REQUESTS_PER_WINDOW),
|
||||
maxRequests: parsePositiveInt(
|
||||
process.env.MB_RATE_LIMIT_MAX_REQUESTS,
|
||||
DEFAULT_MAX_REQUESTS_PER_WINDOW
|
||||
),
|
||||
})
|
||||
|
||||
let rateLimitConfig = getEnvRateLimitConfig()
|
||||
@@ -31,7 +34,8 @@ export async function loadRateLimitConfig(): Promise<void> {
|
||||
configLoadPromise = (async () => {
|
||||
try {
|
||||
const envConfig = getEnvRateLimitConfig()
|
||||
const { getSystemConfigValue } = await import('@/lib/db/system-config/get-system-config-value')
|
||||
const { getSystemConfigValue } =
|
||||
await import('@/lib/db/system-config/get-system-config-value')
|
||||
const [windowValue, maxRequestsValue] = await Promise.all([
|
||||
getSystemConfigValue(RATE_LIMIT_WINDOW_KEY),
|
||||
getSystemConfigValue(MAX_REQUESTS_KEY),
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import type { User } from '../../types/level-types'
|
||||
|
||||
export type OperationType = 'CREATE' | 'READ' | 'UPDATE' | 'DELETE'
|
||||
export type ResourceType = 'user' | 'workflow' | 'luaScript' | 'pageConfig' |
|
||||
'modelSchema' | 'comment' | 'componentNode' | 'componentConfig' | 'cssCategory' |
|
||||
'dropdownConfig' | 'tenant' | 'powerTransfer' | 'smtpConfig' | 'credential'
|
||||
export type ResourceType =
|
||||
| 'user'
|
||||
| 'workflow'
|
||||
| 'luaScript'
|
||||
| 'pageConfig'
|
||||
| 'modelSchema'
|
||||
| 'comment'
|
||||
| 'componentNode'
|
||||
| 'componentConfig'
|
||||
| 'cssCategory'
|
||||
| 'dropdownConfig'
|
||||
| 'tenant'
|
||||
| 'powerTransfer'
|
||||
| 'smtpConfig'
|
||||
| 'credential'
|
||||
|
||||
export interface AuditLog {
|
||||
id: string
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Seed Data Module
|
||||
*
|
||||
*
|
||||
* Initializes the database with seed data including:
|
||||
* - Default users at each permission level
|
||||
* - Initial configuration
|
||||
* - Package metadata
|
||||
* - System templates
|
||||
*
|
||||
*
|
||||
* This runs once during application initialization
|
||||
*/
|
||||
|
||||
@@ -14,11 +14,11 @@ import { initializeAllSeedData } from '@/seed-data'
|
||||
|
||||
/**
|
||||
* Seeds the database with initial data
|
||||
*
|
||||
*
|
||||
* This function should be called during app initialization
|
||||
* to populate the database with default users, configurations,
|
||||
* and system data needed for the application to function.
|
||||
*
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* ComponentType - All supported UI component types in the builder
|
||||
* @description Union type of all available components
|
||||
* Supports: Layout (Flex, Grid, Container), Input (Input, Select, Switch),
|
||||
* Supports: Layout (Flex, Grid, Container), Input (Input, Select, Switch),
|
||||
* Display (Text, Heading, Badge), Interactive (Button, Dialog, Tabs)
|
||||
*/
|
||||
export type ComponentType =
|
||||
|
||||
@@ -53,8 +53,16 @@ describe('Type Guards', () => {
|
||||
|
||||
describe('getErrorMessage', () => {
|
||||
it.each([
|
||||
{ value: new Error('error message'), expected: 'error message', description: 'Error instance' },
|
||||
{ value: { message: 'custom error' }, expected: 'custom error', description: 'error-like object' },
|
||||
{
|
||||
value: new Error('error message'),
|
||||
expected: 'error message',
|
||||
description: 'Error instance',
|
||||
},
|
||||
{
|
||||
value: { message: 'custom error' },
|
||||
expected: 'custom error',
|
||||
description: 'error-like object',
|
||||
},
|
||||
{ value: 'string error', expected: 'string error', description: 'string' },
|
||||
{ value: null, expected: 'An unknown error occurred', description: 'null' },
|
||||
{ value: undefined, expected: 'An unknown error occurred', description: 'undefined' },
|
||||
@@ -68,11 +76,21 @@ describe('Type Guards', () => {
|
||||
describe('hasProperty', () => {
|
||||
it.each([
|
||||
{ obj: { name: 'test' }, key: 'name', expected: true, description: 'object with property' },
|
||||
{ obj: { name: 'test' }, key: 'age', expected: false, description: 'object without property' },
|
||||
{
|
||||
obj: { name: 'test' },
|
||||
key: 'age',
|
||||
expected: false,
|
||||
description: 'object without property',
|
||||
},
|
||||
{ obj: {}, key: 'name', expected: false, description: 'empty object' },
|
||||
{ obj: null, key: 'name', expected: false, description: 'null' },
|
||||
{ obj: undefined, key: 'name', expected: false, description: 'undefined' },
|
||||
{ obj: 'string', key: 'length', expected: false, description: 'string (primitive, not object)' },
|
||||
{
|
||||
obj: 'string',
|
||||
key: 'length',
|
||||
expected: false,
|
||||
description: 'string (primitive, not object)',
|
||||
},
|
||||
])('should return $expected for $description', ({ obj, key, expected }) => {
|
||||
expect(hasProperty(obj, key)).toBe(expected)
|
||||
})
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
/**
|
||||
* Check if value has a specific property
|
||||
*/
|
||||
export function hasProperty<K extends string>(
|
||||
obj: unknown,
|
||||
key: K
|
||||
): obj is Record<K, unknown> {
|
||||
export function hasProperty<K extends string>(obj: unknown, key: K): obj is Record<K, unknown> {
|
||||
return typeof obj === 'object' && obj !== null && key in obj
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface Comment {
|
||||
|
||||
/**
|
||||
* WorkflowNode - Individual step in a workflow
|
||||
* @property type - Node type: trigger (event), action (do something),
|
||||
* @property type - Node type: trigger (event), action (do something),
|
||||
* condition (if/then), lua (execute script), transform (map data)
|
||||
* @property position - X/Y coordinates for visual editor
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { clsx, type ClassValue } from 'clsx'
|
||||
|
||||
/**
|
||||
* Utility function to merge class names
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('workflow-engine errors', () => {
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toContain('Transform failed')
|
||||
expect(result.logs.filter((log) => log.includes('Retrying node'))).toHaveLength(1)
|
||||
expect(result.logs.filter(log => log.includes('Retrying node'))).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('propagates Lua script resolution errors', async () => {
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('workflow-engine execution', () => {
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.outputs.action).toBeUndefined()
|
||||
expect(Object.keys(result.outputs)).toHaveLength(2)
|
||||
expect(result.logs.some((log) => log.includes('Condition node returned false'))).toBe(true)
|
||||
expect(result.logs.some(log => log.includes('Condition node returned false'))).toBe(true)
|
||||
})
|
||||
|
||||
it('passes user context through to Lua nodes', async () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('workflow-engine persistence', () => {
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.outputs.trigger).toEqual({ value: 3 })
|
||||
expect(result.outputs.action).toBeUndefined()
|
||||
expect(result.logs.some((log) => log.includes('Break here'))).toBe(true)
|
||||
expect(result.logs.some(log => log.includes('Break here'))).toBe(true)
|
||||
expect(result.error).toContain('Transform failed')
|
||||
})
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('workflow-engine persistence', () => {
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.outputs.lua).toBe(99)
|
||||
expect(result.securityWarnings).toContain('Security issues detected: uses os')
|
||||
expect(result.logs.some((log) => log.includes('[Lua] lua log'))).toBe(true)
|
||||
expect(result.logs.some(log => log.includes('[Lua] lua log'))).toBe(true)
|
||||
expect(mockEngine.destroy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,10 +48,7 @@ function normalizeRetryConfig(config: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
function calculateRetryDelayMs(
|
||||
attempt: number,
|
||||
config: ReturnType<typeof normalizeRetryConfig>
|
||||
) {
|
||||
function calculateRetryDelayMs(attempt: number, config: ReturnType<typeof normalizeRetryConfig>) {
|
||||
if (config.delayMs <= 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ import type { WorkflowState } from './workflow-state'
|
||||
* Log a message to workflow state
|
||||
*/
|
||||
export function logToWorkflow(state: WorkflowState, ...args: any[]): void {
|
||||
state.logs.push(args.map((arg) => String(arg)).join(' '))
|
||||
state.logs.push(args.map(arg => String(arg)).join(' '))
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ export async function executeLuaCode(
|
||||
|
||||
if (result.security.severity === 'critical' || result.security.severity === 'high') {
|
||||
state.securityWarnings.push(
|
||||
`Security issues detected: ${result.security.issues.map((i) => i.message).join(', ')}`
|
||||
`Security issues detected: ${result.security.issues.map(i => i.message).join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
result.execution.logs.forEach((log) => logToWorkflow(state, `[Lua] ${log}`))
|
||||
result.execution.logs.forEach(log => logToWorkflow(state, `[Lua] ${log}`))
|
||||
|
||||
if (!result.execution.success) {
|
||||
return {
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function executeLuaNode(
|
||||
return await executeLuaCode(luaCode, data, context, state)
|
||||
}
|
||||
|
||||
const script = context.scripts.find((s) => s.id === scriptId)
|
||||
const script = context.scripts.find(s => s.id === scriptId)
|
||||
if (!script) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -19,9 +19,7 @@ end
|
||||
|
||||
return generateWelcome
|
||||
`,
|
||||
parameters: [
|
||||
{ name: 'username', type: 'string' }
|
||||
],
|
||||
parameters: [{ name: 'username', type: 'string' }],
|
||||
returnType: 'string',
|
||||
},
|
||||
{
|
||||
@@ -37,9 +35,7 @@ end
|
||||
|
||||
return formatDate
|
||||
`,
|
||||
parameters: [
|
||||
{ name: 'timestamp', type: 'number' }
|
||||
],
|
||||
parameters: [{ name: 'timestamp', type: 'number' }],
|
||||
returnType: 'string',
|
||||
},
|
||||
{
|
||||
@@ -55,9 +51,7 @@ end
|
||||
|
||||
return validateEmail
|
||||
`,
|
||||
parameters: [
|
||||
{ name: 'email', type: 'string' }
|
||||
],
|
||||
parameters: [{ name: 'email', type: 'string' }],
|
||||
returnType: 'boolean',
|
||||
},
|
||||
{
|
||||
@@ -85,7 +79,7 @@ return checkPermission
|
||||
`,
|
||||
parameters: [
|
||||
{ name: 'userRole', type: 'string' },
|
||||
{ name: 'requiredRole', type: 'string' }
|
||||
{ name: 'requiredRole', type: 'string' },
|
||||
],
|
||||
returnType: 'boolean',
|
||||
},
|
||||
@@ -104,7 +98,7 @@ return logPageView
|
||||
parameters: [
|
||||
{ name: 'pageId', type: 'string' },
|
||||
{ name: 'userId', type: 'string' },
|
||||
{ name: 'timestamp', type: 'number' }
|
||||
{ name: 'timestamp', type: 'number' },
|
||||
],
|
||||
returnType: 'boolean',
|
||||
},
|
||||
|
||||
@@ -19,27 +19,27 @@ export async function initializeWorkflows() {
|
||||
type: 'condition',
|
||||
label: 'Validate Input',
|
||||
config: { action: 'validate_email' },
|
||||
position: { x: 100, y: 100 }
|
||||
position: { x: 100, y: 100 },
|
||||
},
|
||||
{
|
||||
id: 'node_create',
|
||||
type: 'action',
|
||||
label: 'Create User',
|
||||
config: { action: 'create_user' },
|
||||
position: { x: 100, y: 200 }
|
||||
position: { x: 100, y: 200 },
|
||||
},
|
||||
{
|
||||
id: 'node_notify',
|
||||
type: 'action',
|
||||
label: 'Send Welcome Email',
|
||||
config: { action: 'send_email' },
|
||||
position: { x: 100, y: 300 }
|
||||
}
|
||||
position: { x: 100, y: 300 },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'edge_1', source: 'node_validate', target: 'node_create', label: 'valid' },
|
||||
{ id: 'edge_2', source: 'node_create', target: 'node_notify', label: 'success' }
|
||||
]
|
||||
{ id: 'edge_2', source: 'node_create', target: 'node_notify', label: 'success' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'workflow_page_access',
|
||||
@@ -52,27 +52,27 @@ export async function initializeWorkflows() {
|
||||
type: 'condition',
|
||||
label: 'Check Authentication',
|
||||
config: { action: 'check_auth' },
|
||||
position: { x: 100, y: 100 }
|
||||
position: { x: 100, y: 100 },
|
||||
},
|
||||
{
|
||||
id: 'node_perm_check',
|
||||
type: 'lua',
|
||||
label: 'Check Permission',
|
||||
config: { scriptId: 'script_permission_check' },
|
||||
position: { x: 100, y: 200 }
|
||||
position: { x: 100, y: 200 },
|
||||
},
|
||||
{
|
||||
id: 'node_log',
|
||||
type: 'action',
|
||||
label: 'Log Access',
|
||||
config: { action: 'log_page_view' },
|
||||
position: { x: 100, y: 300 }
|
||||
}
|
||||
position: { x: 100, y: 300 },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'edge_1', source: 'node_auth_check', target: 'node_perm_check', label: 'authorized' },
|
||||
{ id: 'edge_2', source: 'node_perm_check', target: 'node_log', label: 'allowed' }
|
||||
]
|
||||
{ id: 'edge_2', source: 'node_perm_check', target: 'node_log', label: 'allowed' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'workflow_comment_submission',
|
||||
@@ -85,27 +85,37 @@ export async function initializeWorkflows() {
|
||||
type: 'condition',
|
||||
label: 'Validate Comment',
|
||||
config: { action: 'validate_content' },
|
||||
position: { x: 100, y: 100 }
|
||||
position: { x: 100, y: 100 },
|
||||
},
|
||||
{
|
||||
id: 'node_save_comment',
|
||||
type: 'action',
|
||||
label: 'Save to Database',
|
||||
config: { action: 'create_comment' },
|
||||
position: { x: 100, y: 200 }
|
||||
position: { x: 100, y: 200 },
|
||||
},
|
||||
{
|
||||
id: 'node_notify_success',
|
||||
type: 'action',
|
||||
label: 'Show Success',
|
||||
config: { action: 'show_toast' },
|
||||
position: { x: 100, y: 300 }
|
||||
}
|
||||
position: { x: 100, y: 300 },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'edge_1', source: 'node_validate_comment', target: 'node_save_comment', label: 'valid' },
|
||||
{ id: 'edge_2', source: 'node_save_comment', target: 'node_notify_success', label: 'saved' }
|
||||
]
|
||||
{
|
||||
id: 'edge_1',
|
||||
source: 'node_validate_comment',
|
||||
target: 'node_save_comment',
|
||||
label: 'valid',
|
||||
},
|
||||
{
|
||||
id: 'edge_2',
|
||||
source: 'node_save_comment',
|
||||
target: 'node_notify_success',
|
||||
label: 'saved',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'workflow_package_export',
|
||||
@@ -118,28 +128,28 @@ export async function initializeWorkflows() {
|
||||
type: 'condition',
|
||||
label: 'Validate Package',
|
||||
config: { action: 'validate_package' },
|
||||
position: { x: 100, y: 100 }
|
||||
position: { x: 100, y: 100 },
|
||||
},
|
||||
{
|
||||
id: 'node_build_zip',
|
||||
type: 'action',
|
||||
label: 'Build Zip',
|
||||
config: { action: 'build_package_zip' },
|
||||
position: { x: 100, y: 200 }
|
||||
position: { x: 100, y: 200 },
|
||||
},
|
||||
{
|
||||
id: 'node_publish_package',
|
||||
type: 'action',
|
||||
label: 'Publish Artifact',
|
||||
config: { action: 'publish_package' },
|
||||
position: { x: 100, y: 300 }
|
||||
}
|
||||
position: { x: 100, y: 300 },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'edge_1', source: 'node_validate_package', target: 'node_build_zip', label: 'valid' },
|
||||
{ id: 'edge_2', source: 'node_build_zip', target: 'node_publish_package', label: 'built' }
|
||||
]
|
||||
}
|
||||
{ id: 'edge_2', source: 'node_build_zip', target: 'node_publish_package', label: 'built' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
for (const workflow of workflows) {
|
||||
|
||||
@@ -12,7 +12,7 @@ const packages = [
|
||||
dataTableMetadata,
|
||||
formBuilderMetadata,
|
||||
navMenuMetadata,
|
||||
notificationCenterMetadata
|
||||
notificationCenterMetadata,
|
||||
]
|
||||
|
||||
describe('Package System Integration', () => {
|
||||
@@ -63,14 +63,14 @@ describe('Package System Integration', () => {
|
||||
throw new Error(`Circular dependency detected: ${pkgId}`)
|
||||
}
|
||||
visited.add(pkgId)
|
||||
|
||||
|
||||
const pkg = packages.find(p => p.packageId === pkgId)
|
||||
if (!pkg) return visited
|
||||
|
||||
|
||||
pkg.dependencies.forEach((depId: string) => {
|
||||
getDependencies(depId, new Set(visited))
|
||||
})
|
||||
|
||||
|
||||
return visited
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('Package System Integration', () => {
|
||||
|
||||
it('should have all dependencies reference valid packages', () => {
|
||||
const allPackageIds = packages.map(pkg => pkg.packageId)
|
||||
|
||||
|
||||
packages.forEach(pkg => {
|
||||
pkg.dependencies.forEach((depId: string) => {
|
||||
expect(allPackageIds).toContain(depId)
|
||||
|
||||
@@ -24,8 +24,16 @@ export const colors = {
|
||||
dark: { main: '#34d399', light: '#6ee7b7', dark: '#10b981' },
|
||||
},
|
||||
neutral: {
|
||||
50: '#fafafa', 100: '#f4f4f5', 200: '#e4e4e7', 300: '#d4d4d8',
|
||||
400: '#a1a1aa', 500: '#71717a', 600: '#52525b', 700: '#3f3f46',
|
||||
800: '#27272a', 900: '#18181b', 950: '#09090b',
|
||||
50: '#fafafa',
|
||||
100: '#f4f4f5',
|
||||
200: '#e4e4e7',
|
||||
300: '#d4d4d8',
|
||||
400: '#a1a1aa',
|
||||
500: '#71717a',
|
||||
600: '#52525b',
|
||||
700: '#3f3f46',
|
||||
800: '#27272a',
|
||||
900: '#18181b',
|
||||
950: '#09090b',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,8 +2,20 @@ import { fonts } from './fonts'
|
||||
|
||||
export const typography = {
|
||||
fontFamily: fonts.body,
|
||||
h1: { fontFamily: fonts.heading, fontWeight: 700, fontSize: '3rem', lineHeight: 1.2, letterSpacing: '-0.02em' },
|
||||
h2: { fontFamily: fonts.heading, fontWeight: 700, fontSize: '2.25rem', lineHeight: 1.25, letterSpacing: '-0.01em' },
|
||||
h1: {
|
||||
fontFamily: fonts.heading,
|
||||
fontWeight: 700,
|
||||
fontSize: '3rem',
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: '-0.02em',
|
||||
},
|
||||
h2: {
|
||||
fontFamily: fonts.heading,
|
||||
fontWeight: 700,
|
||||
fontSize: '2.25rem',
|
||||
lineHeight: 1.25,
|
||||
letterSpacing: '-0.01em',
|
||||
},
|
||||
h3: { fontFamily: fonts.heading, fontWeight: 600, fontSize: '1.875rem', lineHeight: 1.3 },
|
||||
h4: { fontFamily: fonts.heading, fontWeight: 600, fontSize: '1.5rem', lineHeight: 1.35 },
|
||||
h5: { fontFamily: fonts.heading, fontWeight: 600, fontSize: '1.25rem', lineHeight: 1.4 },
|
||||
@@ -13,9 +25,27 @@ export const typography = {
|
||||
body1: { fontSize: '1rem', lineHeight: 1.6 },
|
||||
body2: { fontSize: '0.875rem', lineHeight: 1.5 },
|
||||
caption: { fontSize: '0.75rem', lineHeight: 1.4 },
|
||||
overline: { fontWeight: 600, fontSize: '0.75rem', letterSpacing: '0.08em', textTransform: 'uppercase' as const },
|
||||
overline: {
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase' as const,
|
||||
},
|
||||
button: { fontWeight: 500, fontSize: '0.875rem', textTransform: 'none' as const },
|
||||
code: { fontFamily: fonts.mono, fontSize: '0.875rem', backgroundColor: 'rgba(0,0,0,0.06)', padding: '2px 6px', borderRadius: 4 },
|
||||
kbd: { fontFamily: fonts.mono, fontSize: '0.75rem', fontWeight: 500, padding: '2px 8px', borderRadius: 4, border: '1px solid' },
|
||||
code: {
|
||||
fontFamily: fonts.mono,
|
||||
fontSize: '0.875rem',
|
||||
backgroundColor: 'rgba(0,0,0,0.06)',
|
||||
padding: '2px 6px',
|
||||
borderRadius: 4,
|
||||
},
|
||||
kbd: {
|
||||
fontFamily: fonts.mono,
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 500,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 4,
|
||||
border: '1px solid',
|
||||
},
|
||||
label: { fontSize: '0.875rem', fontWeight: 500, lineHeight: 1.5 },
|
||||
}
|
||||
|
||||
@@ -9,11 +9,18 @@ export const getComponentOverrides = (mode: 'light' | 'dark'): ThemeOptions['com
|
||||
return {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
':root': { '--font-body': fonts.body, '--font-heading': fonts.heading, '--font-mono': fonts.mono },
|
||||
':root': {
|
||||
'--font-body': fonts.body,
|
||||
'--font-heading': fonts.heading,
|
||||
'--font-mono': fonts.mono,
|
||||
},
|
||||
html: { scrollBehavior: 'smooth' },
|
||||
body: { WebkitFontSmoothing: 'antialiased', MozOsxFontSmoothing: 'grayscale' },
|
||||
'code, pre, kbd': { fontFamily: fonts.mono },
|
||||
'::selection': { backgroundColor: isDark ? colors.primary.dark.main : colors.primary.light.main, color: '#fff' },
|
||||
'::selection': {
|
||||
backgroundColor: isDark ? colors.primary.dark.main : colors.primary.light.main,
|
||||
color: '#fff',
|
||||
},
|
||||
'::-webkit-scrollbar': { width: 8, height: 8 },
|
||||
'::-webkit-scrollbar-track': { backgroundColor: isDark ? n[800] : n[100] },
|
||||
'::-webkit-scrollbar-thumb': { backgroundColor: isDark ? n[600] : n[400], borderRadius: 4 },
|
||||
@@ -25,12 +32,22 @@ export const getComponentOverrides = (mode: 'light' | 'dark'): ThemeOptions['com
|
||||
root: { borderRadius: 8, textTransform: 'none', fontWeight: 500, padding: '8px 16px' },
|
||||
sizeSmall: { padding: '4px 12px', fontSize: '0.8125rem' },
|
||||
sizeLarge: { padding: '12px 24px', fontSize: '0.9375rem' },
|
||||
containedPrimary: { '&:hover': { boxShadow: `0 4px 12px ${alpha(isDark ? colors.primary.dark.main : colors.primary.light.main, 0.4)}` } },
|
||||
containedPrimary: {
|
||||
'&:hover': {
|
||||
boxShadow: `0 4px 12px ${alpha(isDark ? colors.primary.dark.main : colors.primary.light.main, 0.4)}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
defaultProps: { elevation: 0 },
|
||||
styleOverrides: { root: { borderRadius: 12, border: `1px solid ${isDark ? n[800] : n[200]}`, backgroundImage: 'none' } },
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${isDark ? n[800] : n[200]}`,
|
||||
backgroundImage: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCardHeader: {
|
||||
styleOverrides: {
|
||||
@@ -39,56 +56,177 @@ export const getComponentOverrides = (mode: 'light' | 'dark'): ThemeOptions['com
|
||||
subheader: { fontSize: '0.875rem', color: isDark ? n[400] : n[600] },
|
||||
},
|
||||
},
|
||||
MuiCardContent: { styleOverrides: { root: { padding: '16px 24px', '&:last-child': { paddingBottom: 24 } } } },
|
||||
MuiCardActions: { styleOverrides: { root: { padding: '16px 24px', borderTop: `1px solid ${isDark ? n[800] : n[200]}` } } },
|
||||
MuiPaper: { defaultProps: { elevation: 0 }, styleOverrides: { root: { backgroundImage: 'none' }, outlined: { borderColor: isDark ? n[800] : n[200] } } },
|
||||
MuiCardContent: {
|
||||
styleOverrides: { root: { padding: '16px 24px', '&:last-child': { paddingBottom: 24 } } },
|
||||
},
|
||||
MuiCardActions: {
|
||||
styleOverrides: {
|
||||
root: { padding: '16px 24px', borderTop: `1px solid ${isDark ? n[800] : n[200]}` },
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
defaultProps: { elevation: 0 },
|
||||
styleOverrides: {
|
||||
root: { backgroundImage: 'none' },
|
||||
outlined: { borderColor: isDark ? n[800] : n[200] },
|
||||
},
|
||||
},
|
||||
MuiTextField: {
|
||||
defaultProps: { variant: 'outlined', size: 'small' },
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiOutlinedInput-root': { borderRadius: 8, backgroundColor: isDark ? n[900] : '#fff' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 8,
|
||||
backgroundColor: isDark ? n[900] : '#fff',
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': { borderColor: isDark ? n[700] : n[300] },
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiOutlinedInput: { styleOverrides: { root: { borderRadius: 8 } } },
|
||||
MuiSelect: { styleOverrides: { root: { borderRadius: 8 } } },
|
||||
MuiChip: { styleOverrides: { root: { borderRadius: 6, fontWeight: 500 }, sizeSmall: { height: 24 } } },
|
||||
MuiChip: {
|
||||
styleOverrides: { root: { borderRadius: 6, fontWeight: 500 }, sizeSmall: { height: 24 } },
|
||||
},
|
||||
MuiAlert: {
|
||||
styleOverrides: {
|
||||
root: { borderRadius: 8, alignItems: 'center' },
|
||||
standardSuccess: { backgroundColor: alpha(isDark ? colors.success.dark.main : colors.success.light.main, isDark ? 0.15 : 0.1) },
|
||||
standardError: { backgroundColor: alpha(isDark ? colors.error.dark.main : colors.error.light.main, isDark ? 0.15 : 0.1) },
|
||||
standardWarning: { backgroundColor: alpha(isDark ? colors.warning.dark.main : colors.warning.light.main, isDark ? 0.15 : 0.1) },
|
||||
standardInfo: { backgroundColor: alpha(isDark ? colors.info.dark.main : colors.info.light.main, isDark ? 0.15 : 0.1) },
|
||||
standardSuccess: {
|
||||
backgroundColor: alpha(
|
||||
isDark ? colors.success.dark.main : colors.success.light.main,
|
||||
isDark ? 0.15 : 0.1
|
||||
),
|
||||
},
|
||||
standardError: {
|
||||
backgroundColor: alpha(
|
||||
isDark ? colors.error.dark.main : colors.error.light.main,
|
||||
isDark ? 0.15 : 0.1
|
||||
),
|
||||
},
|
||||
standardWarning: {
|
||||
backgroundColor: alpha(
|
||||
isDark ? colors.warning.dark.main : colors.warning.light.main,
|
||||
isDark ? 0.15 : 0.1
|
||||
),
|
||||
},
|
||||
standardInfo: {
|
||||
backgroundColor: alpha(
|
||||
isDark ? colors.info.dark.main : colors.info.light.main,
|
||||
isDark ? 0.15 : 0.1
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDialog: { styleOverrides: { paper: { borderRadius: 16 } } },
|
||||
MuiDialogTitle: { styleOverrides: { root: { fontFamily: fonts.heading, fontWeight: 600, fontSize: '1.25rem', padding: '24px 24px 16px' } } },
|
||||
MuiDialogTitle: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
fontFamily: fonts.heading,
|
||||
fontWeight: 600,
|
||||
fontSize: '1.25rem',
|
||||
padding: '24px 24px 16px',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDialogContent: { styleOverrides: { root: { padding: '16px 24px' } } },
|
||||
MuiDialogActions: { styleOverrides: { root: { padding: '16px 24px 24px', gap: 8 } } },
|
||||
MuiTable: { styleOverrides: { root: { borderCollapse: 'separate', borderSpacing: 0 } } },
|
||||
MuiTableHead: { styleOverrides: { root: { '& .MuiTableCell-root': { fontWeight: 600, backgroundColor: isDark ? n[900] : n[50], borderBottom: `2px solid ${isDark ? n[700] : n[300]}` } } } },
|
||||
MuiTableCell: { styleOverrides: { root: { borderBottom: `1px solid ${isDark ? n[800] : n[200]}`, padding: '12px 16px' } } },
|
||||
MuiTableRow: { styleOverrides: { root: { '&:hover': { backgroundColor: isDark ? alpha(n[700], 0.3) : alpha(n[100], 0.5) } } } },
|
||||
MuiTabs: { styleOverrides: { root: { minHeight: 48 }, indicator: { height: 3, borderRadius: '3px 3px 0 0' } } },
|
||||
MuiTab: { styleOverrides: { root: { textTransform: 'none', fontWeight: 500, minHeight: 48, padding: '12px 16px' } } },
|
||||
MuiTooltip: { styleOverrides: { tooltip: { backgroundColor: isDark ? n[700] : n[900], fontSize: '0.75rem', padding: '6px 12px', borderRadius: 6 }, arrow: { color: isDark ? n[700] : n[900] } } },
|
||||
MuiMenu: { styleOverrides: { paper: { borderRadius: 8, border: `1px solid ${isDark ? n[800] : n[200]}` } } },
|
||||
MuiMenuItem: { styleOverrides: { root: { padding: '8px 16px', borderRadius: 4, margin: '2px 8px' } } },
|
||||
MuiDrawer: { styleOverrides: { paper: { borderRight: `1px solid ${isDark ? n[800] : n[200]}` } } },
|
||||
MuiAppBar: { defaultProps: { elevation: 0 }, styleOverrides: { root: { backgroundColor: isDark ? n[900] : '#fff', borderBottom: `1px solid ${isDark ? n[800] : n[200]}` } } },
|
||||
MuiTableHead: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiTableCell-root': {
|
||||
fontWeight: 600,
|
||||
backgroundColor: isDark ? n[900] : n[50],
|
||||
borderBottom: `2px solid ${isDark ? n[700] : n[300]}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: { borderBottom: `1px solid ${isDark ? n[800] : n[200]}`, padding: '12px 16px' },
|
||||
},
|
||||
},
|
||||
MuiTableRow: {
|
||||
styleOverrides: {
|
||||
root: { '&:hover': { backgroundColor: isDark ? alpha(n[700], 0.3) : alpha(n[100], 0.5) } },
|
||||
},
|
||||
},
|
||||
MuiTabs: {
|
||||
styleOverrides: {
|
||||
root: { minHeight: 48 },
|
||||
indicator: { height: 3, borderRadius: '3px 3px 0 0' },
|
||||
},
|
||||
},
|
||||
MuiTab: {
|
||||
styleOverrides: {
|
||||
root: { textTransform: 'none', fontWeight: 500, minHeight: 48, padding: '12px 16px' },
|
||||
},
|
||||
},
|
||||
MuiTooltip: {
|
||||
styleOverrides: {
|
||||
tooltip: {
|
||||
backgroundColor: isDark ? n[700] : n[900],
|
||||
fontSize: '0.75rem',
|
||||
padding: '6px 12px',
|
||||
borderRadius: 6,
|
||||
},
|
||||
arrow: { color: isDark ? n[700] : n[900] },
|
||||
},
|
||||
},
|
||||
MuiMenu: {
|
||||
styleOverrides: {
|
||||
paper: { borderRadius: 8, border: `1px solid ${isDark ? n[800] : n[200]}` },
|
||||
},
|
||||
},
|
||||
MuiMenuItem: {
|
||||
styleOverrides: { root: { padding: '8px 16px', borderRadius: 4, margin: '2px 8px' } },
|
||||
},
|
||||
MuiDrawer: {
|
||||
styleOverrides: { paper: { borderRight: `1px solid ${isDark ? n[800] : n[200]}` } },
|
||||
},
|
||||
MuiAppBar: {
|
||||
defaultProps: { elevation: 0 },
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: isDark ? n[900] : '#fff',
|
||||
borderBottom: `1px solid ${isDark ? n[800] : n[200]}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDivider: { styleOverrides: { root: { borderColor: isDark ? n[800] : n[200] } } },
|
||||
MuiAvatar: { styleOverrides: { root: { fontFamily: fonts.heading, fontWeight: 600 } } },
|
||||
MuiBadge: { styleOverrides: { badge: { fontWeight: 600, fontSize: '0.6875rem' } } },
|
||||
MuiLinearProgress: { styleOverrides: { root: { borderRadius: 4, height: 6 } } },
|
||||
MuiSkeleton: { styleOverrides: { root: { borderRadius: 4, backgroundColor: isDark ? n[800] : n[200] } } },
|
||||
MuiAccordion: { defaultProps: { elevation: 0 }, styleOverrides: { root: { border: `1px solid ${isDark ? n[800] : n[200]}`, borderRadius: 8, '&:before': { display: 'none' } } } },
|
||||
MuiAccordionSummary: { styleOverrides: { root: { padding: '0 16px', minHeight: 56 }, content: { fontWeight: 500 } } },
|
||||
MuiSkeleton: {
|
||||
styleOverrides: { root: { borderRadius: 4, backgroundColor: isDark ? n[800] : n[200] } },
|
||||
},
|
||||
MuiAccordion: {
|
||||
defaultProps: { elevation: 0 },
|
||||
styleOverrides: {
|
||||
root: {
|
||||
border: `1px solid ${isDark ? n[800] : n[200]}`,
|
||||
borderRadius: 8,
|
||||
'&:before': { display: 'none' },
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAccordionSummary: {
|
||||
styleOverrides: { root: { padding: '0 16px', minHeight: 56 }, content: { fontWeight: 500 } },
|
||||
},
|
||||
MuiAccordionDetails: { styleOverrides: { root: { padding: '0 16px 16px' } } },
|
||||
MuiSwitch: { styleOverrides: { root: { width: 46, height: 26, padding: 0 }, switchBase: { padding: 2, '&.Mui-checked': { transform: 'translateX(20px)' } }, thumb: { width: 22, height: 22 }, track: { borderRadius: 13, opacity: 1, backgroundColor: isDark ? n[700] : n[300] } } },
|
||||
MuiSwitch: {
|
||||
styleOverrides: {
|
||||
root: { width: 46, height: 26, padding: 0 },
|
||||
switchBase: { padding: 2, '&.Mui-checked': { transform: 'translateX(20px)' } },
|
||||
thumb: { width: 22, height: 22 },
|
||||
track: { borderRadius: 13, opacity: 1, backgroundColor: isDark ? n[700] : n[300] },
|
||||
},
|
||||
},
|
||||
MuiIconButton: { styleOverrides: { root: { borderRadius: 8 } } },
|
||||
MuiListItemButton: { styleOverrides: { root: { borderRadius: 8, margin: '2px 8px', padding: '8px 12px' } } },
|
||||
MuiListItemButton: {
|
||||
styleOverrides: { root: { borderRadius: 8, margin: '2px 8px', padding: '8px 12px' } },
|
||||
},
|
||||
MuiSnackbar: { styleOverrides: { root: { '& .MuiPaper-root': { borderRadius: 8 } } } },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,24 @@ import { layout } from './layout'
|
||||
import { typography } from './typography'
|
||||
import { getComponentOverrides } from './components'
|
||||
|
||||
const custom = { fonts, borderRadius: layout.borderRadius, contentWidth: layout.contentWidth, sidebar: layout.sidebar, header: layout.header }
|
||||
const shadows = (o: number): Shadows => ['none', `0 1px 2px rgba(0,0,0,${o/2})`, `0 1px 3px rgba(0,0,0,${o})`, `0 4px 6px rgba(0,0,0,${o})`, `0 10px 15px rgba(0,0,0,${o})`, `0 20px 25px rgba(0,0,0,${o})`, `0 25px 50px rgba(0,0,0,${o*2.5})`, ...Array(18).fill(`0 25px 50px rgba(0,0,0,${o*2.5})`)] as Shadows
|
||||
const custom = {
|
||||
fonts,
|
||||
borderRadius: layout.borderRadius,
|
||||
contentWidth: layout.contentWidth,
|
||||
sidebar: layout.sidebar,
|
||||
header: layout.header,
|
||||
}
|
||||
const shadows = (o: number): Shadows =>
|
||||
[
|
||||
'none',
|
||||
`0 1px 2px rgba(0,0,0,${o / 2})`,
|
||||
`0 1px 3px rgba(0,0,0,${o})`,
|
||||
`0 4px 6px rgba(0,0,0,${o})`,
|
||||
`0 10px 15px rgba(0,0,0,${o})`,
|
||||
`0 20px 25px rgba(0,0,0,${o})`,
|
||||
`0 25px 50px rgba(0,0,0,${o * 2.5})`,
|
||||
...Array(18).fill(`0 25px 50px rgba(0,0,0,${o * 2.5})`),
|
||||
] as Shadows
|
||||
|
||||
export const darkTheme = createTheme({
|
||||
palette: {
|
||||
@@ -22,9 +38,19 @@ export const darkTheme = createTheme({
|
||||
success: colors.success.dark,
|
||||
neutral: colors.neutral,
|
||||
background: { default: colors.neutral[950], paper: colors.neutral[900] },
|
||||
text: { primary: colors.neutral[100], secondary: colors.neutral[400], disabled: colors.neutral[600] },
|
||||
text: {
|
||||
primary: colors.neutral[100],
|
||||
secondary: colors.neutral[400],
|
||||
disabled: colors.neutral[600],
|
||||
},
|
||||
divider: colors.neutral[800],
|
||||
action: { active: colors.neutral[300], hover: alpha(colors.neutral[400], 0.12), selected: alpha(colors.primary.dark.main, 0.2), disabled: colors.neutral[600], disabledBackground: colors.neutral[800] },
|
||||
action: {
|
||||
active: colors.neutral[300],
|
||||
hover: alpha(colors.neutral[400], 0.12),
|
||||
selected: alpha(colors.primary.dark.main, 0.2),
|
||||
disabled: colors.neutral[600],
|
||||
disabledBackground: colors.neutral[800],
|
||||
},
|
||||
},
|
||||
typography,
|
||||
spacing: layout.spacing,
|
||||
|
||||
@@ -8,4 +8,3 @@ export { lightTheme } from './light-theme'
|
||||
export { darkTheme } from './dark-theme'
|
||||
export { useTheme, styled, alpha } from '@mui/material/styles'
|
||||
export type { Theme, SxProps } from '@mui/material/styles'
|
||||
|
||||
|
||||
@@ -8,8 +8,24 @@ import { layout } from './layout'
|
||||
import { typography } from './typography'
|
||||
import { getComponentOverrides } from './components'
|
||||
|
||||
const custom = { fonts, borderRadius: layout.borderRadius, contentWidth: layout.contentWidth, sidebar: layout.sidebar, header: layout.header }
|
||||
const shadows = (o: number): Shadows => ['none', `0 1px 2px rgba(0,0,0,${o/2})`, `0 1px 3px rgba(0,0,0,${o})`, `0 4px 6px rgba(0,0,0,${o})`, `0 10px 15px rgba(0,0,0,${o})`, `0 20px 25px rgba(0,0,0,${o})`, `0 25px 50px rgba(0,0,0,${o*2.5})`, ...Array(18).fill(`0 25px 50px rgba(0,0,0,${o*2.5})`)] as Shadows
|
||||
const custom = {
|
||||
fonts,
|
||||
borderRadius: layout.borderRadius,
|
||||
contentWidth: layout.contentWidth,
|
||||
sidebar: layout.sidebar,
|
||||
header: layout.header,
|
||||
}
|
||||
const shadows = (o: number): Shadows =>
|
||||
[
|
||||
'none',
|
||||
`0 1px 2px rgba(0,0,0,${o / 2})`,
|
||||
`0 1px 3px rgba(0,0,0,${o})`,
|
||||
`0 4px 6px rgba(0,0,0,${o})`,
|
||||
`0 10px 15px rgba(0,0,0,${o})`,
|
||||
`0 20px 25px rgba(0,0,0,${o})`,
|
||||
`0 25px 50px rgba(0,0,0,${o * 2.5})`,
|
||||
...Array(18).fill(`0 25px 50px rgba(0,0,0,${o * 2.5})`),
|
||||
] as Shadows
|
||||
|
||||
export const lightTheme = createTheme({
|
||||
palette: {
|
||||
@@ -22,9 +38,19 @@ export const lightTheme = createTheme({
|
||||
success: colors.success.light,
|
||||
neutral: colors.neutral,
|
||||
background: { default: '#ffffff', paper: colors.neutral[50] },
|
||||
text: { primary: colors.neutral[900], secondary: colors.neutral[600], disabled: colors.neutral[400] },
|
||||
text: {
|
||||
primary: colors.neutral[900],
|
||||
secondary: colors.neutral[600],
|
||||
disabled: colors.neutral[400],
|
||||
},
|
||||
divider: colors.neutral[200],
|
||||
action: { active: colors.neutral[700], hover: alpha(colors.neutral[500], 0.08), selected: alpha(colors.primary.light.main, 0.12), disabled: colors.neutral[400], disabledBackground: colors.neutral[200] },
|
||||
action: {
|
||||
active: colors.neutral[700],
|
||||
hover: alpha(colors.neutral[500], 0.08),
|
||||
selected: alpha(colors.primary.light.main, 0.12),
|
||||
disabled: colors.neutral[400],
|
||||
disabledBackground: colors.neutral[200],
|
||||
},
|
||||
},
|
||||
typography,
|
||||
spacing: layout.spacing,
|
||||
|
||||
@@ -7,8 +7,24 @@ import { layout } from '../layout'
|
||||
import { typography } from '../typography'
|
||||
import { getComponentOverrides } from '../components'
|
||||
|
||||
const custom = { fonts, borderRadius: layout.borderRadius, contentWidth: layout.contentWidth, sidebar: layout.sidebar, header: layout.header }
|
||||
const shadows = (o: number): Shadows => ['none', `0 1px 2px rgba(0,0,0,${o/2})`, `0 1px 3px rgba(0,0,0,${o})`, `0 4px 6px rgba(0,0,0,${o})`, `0 10px 15px rgba(0,0,0,${o})`, `0 20px 25px rgba(0,0,0,${o})`, `0 25px 50px rgba(0,0,0,${o*2.5})`, ...Array(18).fill(`0 25px 50px rgba(0,0,0,${o*2.5})`)] as Shadows
|
||||
const custom = {
|
||||
fonts,
|
||||
borderRadius: layout.borderRadius,
|
||||
contentWidth: layout.contentWidth,
|
||||
sidebar: layout.sidebar,
|
||||
header: layout.header,
|
||||
}
|
||||
const shadows = (o: number): Shadows =>
|
||||
[
|
||||
'none',
|
||||
`0 1px 2px rgba(0,0,0,${o / 2})`,
|
||||
`0 1px 3px rgba(0,0,0,${o})`,
|
||||
`0 4px 6px rgba(0,0,0,${o})`,
|
||||
`0 10px 15px rgba(0,0,0,${o})`,
|
||||
`0 20px 25px rgba(0,0,0,${o})`,
|
||||
`0 25px 50px rgba(0,0,0,${o * 2.5})`,
|
||||
...Array(18).fill(`0 25px 50px rgba(0,0,0,${o * 2.5})`),
|
||||
] as Shadows
|
||||
|
||||
export const darkTheme = createTheme({
|
||||
palette: {
|
||||
@@ -21,9 +37,19 @@ export const darkTheme = createTheme({
|
||||
success: colors.success.dark,
|
||||
neutral: colors.neutral,
|
||||
background: { default: colors.neutral[950], paper: colors.neutral[900] },
|
||||
text: { primary: colors.neutral[100], secondary: colors.neutral[400], disabled: colors.neutral[600] },
|
||||
text: {
|
||||
primary: colors.neutral[100],
|
||||
secondary: colors.neutral[400],
|
||||
disabled: colors.neutral[600],
|
||||
},
|
||||
divider: colors.neutral[800],
|
||||
action: { active: colors.neutral[300], hover: alpha(colors.neutral[400], 0.12), selected: alpha(colors.primary.dark.main, 0.2), disabled: colors.neutral[600], disabledBackground: colors.neutral[800] },
|
||||
action: {
|
||||
active: colors.neutral[300],
|
||||
hover: alpha(colors.neutral[400], 0.12),
|
||||
selected: alpha(colors.primary.dark.main, 0.2),
|
||||
disabled: colors.neutral[600],
|
||||
disabledBackground: colors.neutral[800],
|
||||
},
|
||||
},
|
||||
typography,
|
||||
spacing: layout.spacing,
|
||||
|
||||
@@ -7,8 +7,24 @@ import { layout } from '../layout'
|
||||
import { typography } from '../typography'
|
||||
import { getComponentOverrides } from '../components'
|
||||
|
||||
const custom = { fonts, borderRadius: layout.borderRadius, contentWidth: layout.contentWidth, sidebar: layout.sidebar, header: layout.header }
|
||||
const shadows = (o: number): Shadows => ['none', `0 1px 2px rgba(0,0,0,${o/2})`, `0 1px 3px rgba(0,0,0,${o})`, `0 4px 6px rgba(0,0,0,${o})`, `0 10px 15px rgba(0,0,0,${o})`, `0 20px 25px rgba(0,0,0,${o})`, `0 25px 50px rgba(0,0,0,${o*2.5})`, ...Array(18).fill(`0 25px 50px rgba(0,0,0,${o*2.5})`)] as Shadows
|
||||
const custom = {
|
||||
fonts,
|
||||
borderRadius: layout.borderRadius,
|
||||
contentWidth: layout.contentWidth,
|
||||
sidebar: layout.sidebar,
|
||||
header: layout.header,
|
||||
}
|
||||
const shadows = (o: number): Shadows =>
|
||||
[
|
||||
'none',
|
||||
`0 1px 2px rgba(0,0,0,${o / 2})`,
|
||||
`0 1px 3px rgba(0,0,0,${o})`,
|
||||
`0 4px 6px rgba(0,0,0,${o})`,
|
||||
`0 10px 15px rgba(0,0,0,${o})`,
|
||||
`0 20px 25px rgba(0,0,0,${o})`,
|
||||
`0 25px 50px rgba(0,0,0,${o * 2.5})`,
|
||||
...Array(18).fill(`0 25px 50px rgba(0,0,0,${o * 2.5})`),
|
||||
] as Shadows
|
||||
|
||||
export const lightTheme = createTheme({
|
||||
palette: {
|
||||
@@ -21,9 +37,19 @@ export const lightTheme = createTheme({
|
||||
success: colors.success.light,
|
||||
neutral: colors.neutral,
|
||||
background: { default: '#ffffff', paper: colors.neutral[50] },
|
||||
text: { primary: colors.neutral[900], secondary: colors.neutral[600], disabled: colors.neutral[400] },
|
||||
text: {
|
||||
primary: colors.neutral[900],
|
||||
secondary: colors.neutral[600],
|
||||
disabled: colors.neutral[400],
|
||||
},
|
||||
divider: colors.neutral[200],
|
||||
action: { active: colors.neutral[700], hover: alpha(colors.neutral[500], 0.08), selected: alpha(colors.primary.light.main, 0.12), disabled: colors.neutral[400], disabledBackground: colors.neutral[200] },
|
||||
action: {
|
||||
active: colors.neutral[700],
|
||||
hover: alpha(colors.neutral[500], 0.08),
|
||||
selected: alpha(colors.primary.light.main, 0.12),
|
||||
disabled: colors.neutral[400],
|
||||
disabledBackground: colors.neutral[200],
|
||||
},
|
||||
},
|
||||
typography,
|
||||
spacing: layout.spacing,
|
||||
|
||||
Reference in New Issue
Block a user