mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
Merge branch 'main' into codex/add-javascript-injection-and-xss-modules
This commit is contained in:
@@ -1,308 +1,6 @@
|
||||
import type { SchemaConfig } from '../types/schema-types'
|
||||
import { defaultApps } from './default/components'
|
||||
|
||||
export const defaultSchema: SchemaConfig = {
|
||||
apps: [
|
||||
{
|
||||
name: 'blog',
|
||||
label: 'Blog',
|
||||
models: [
|
||||
{
|
||||
name: 'post',
|
||||
label: 'Post',
|
||||
labelPlural: 'Posts',
|
||||
icon: 'Article',
|
||||
listDisplay: ['title', 'author', 'status', 'publishedAt'],
|
||||
listFilter: ['status', 'author'],
|
||||
searchFields: ['title', 'content'],
|
||||
ordering: ['-publishedAt'],
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
label: 'ID',
|
||||
required: true,
|
||||
unique: true,
|
||||
editable: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
label: 'Title',
|
||||
required: true,
|
||||
validation: {
|
||||
minLength: 3,
|
||||
maxLength: 200,
|
||||
},
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'string',
|
||||
label: 'Slug',
|
||||
required: true,
|
||||
unique: true,
|
||||
helpText: 'URL-friendly version of the title',
|
||||
validation: {
|
||||
pattern: '^[a-z0-9-]+$',
|
||||
},
|
||||
listDisplay: false,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'text',
|
||||
label: 'Content',
|
||||
required: true,
|
||||
helpText: 'Main post content',
|
||||
listDisplay: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'excerpt',
|
||||
type: 'text',
|
||||
label: 'Excerpt',
|
||||
required: false,
|
||||
helpText: ['Short summary of the post', 'Used in list views and previews'],
|
||||
validation: {
|
||||
maxLength: 500,
|
||||
},
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
type: 'relation',
|
||||
label: 'Author',
|
||||
required: true,
|
||||
relatedModel: 'author',
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
label: 'Status',
|
||||
required: true,
|
||||
default: 'draft',
|
||||
choices: [
|
||||
{ value: 'draft', label: 'Draft' },
|
||||
{ value: 'published', label: 'Published' },
|
||||
{ value: 'archived', label: 'Archived' },
|
||||
],
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'featured',
|
||||
type: 'boolean',
|
||||
label: 'Featured',
|
||||
default: false,
|
||||
helpText: 'Display on homepage',
|
||||
listDisplay: true,
|
||||
},
|
||||
{
|
||||
name: 'publishedAt',
|
||||
type: 'datetime',
|
||||
label: 'Published At',
|
||||
required: false,
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
type: 'json',
|
||||
label: 'Tags',
|
||||
required: false,
|
||||
helpText: 'JSON array of tag strings',
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'views',
|
||||
type: 'number',
|
||||
label: 'Views',
|
||||
default: 0,
|
||||
validation: {
|
||||
min: 0,
|
||||
},
|
||||
listDisplay: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
label: 'Author',
|
||||
labelPlural: 'Authors',
|
||||
icon: 'User',
|
||||
listDisplay: ['name', 'email', 'active', 'createdAt'],
|
||||
listFilter: ['active'],
|
||||
searchFields: ['name', 'email'],
|
||||
ordering: ['name'],
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
label: 'ID',
|
||||
required: true,
|
||||
unique: true,
|
||||
editable: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
label: 'Name',
|
||||
required: true,
|
||||
validation: {
|
||||
minLength: 2,
|
||||
maxLength: 100,
|
||||
},
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
label: 'Email',
|
||||
required: true,
|
||||
unique: true,
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'bio',
|
||||
type: 'text',
|
||||
label: 'Bio',
|
||||
required: false,
|
||||
helpText: 'Author biography',
|
||||
validation: {
|
||||
maxLength: 1000,
|
||||
},
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
type: 'url',
|
||||
label: 'Website',
|
||||
required: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'active',
|
||||
type: 'boolean',
|
||||
label: 'Active',
|
||||
default: true,
|
||||
listDisplay: true,
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
type: 'datetime',
|
||||
label: 'Created At',
|
||||
required: true,
|
||||
editable: false,
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ecommerce',
|
||||
label: 'E-Commerce',
|
||||
models: [
|
||||
{
|
||||
name: 'product',
|
||||
label: 'Product',
|
||||
labelPlural: 'Products',
|
||||
icon: 'ShoppingCart',
|
||||
listDisplay: ['name', 'price', 'stock', 'available'],
|
||||
listFilter: ['available', 'category'],
|
||||
searchFields: ['name', 'description'],
|
||||
ordering: ['name'],
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
label: 'ID',
|
||||
required: true,
|
||||
unique: true,
|
||||
editable: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
label: 'Product Name',
|
||||
required: true,
|
||||
validation: {
|
||||
minLength: 3,
|
||||
maxLength: 200,
|
||||
},
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
label: 'Description',
|
||||
required: false,
|
||||
helpText: 'Product description',
|
||||
listDisplay: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
type: 'number',
|
||||
label: 'Price',
|
||||
required: true,
|
||||
validation: {
|
||||
min: 0,
|
||||
},
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'stock',
|
||||
type: 'number',
|
||||
label: 'Stock',
|
||||
required: true,
|
||||
default: 0,
|
||||
validation: {
|
||||
min: 0,
|
||||
},
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'category',
|
||||
type: 'select',
|
||||
label: 'Category',
|
||||
required: true,
|
||||
choices: [
|
||||
{ value: 'electronics', label: 'Electronics' },
|
||||
{ value: 'clothing', label: 'Clothing' },
|
||||
{ value: 'books', label: 'Books' },
|
||||
{ value: 'home', label: 'Home & Garden' },
|
||||
{ value: 'toys', label: 'Toys' },
|
||||
],
|
||||
listDisplay: false,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'available',
|
||||
type: 'boolean',
|
||||
label: 'Available',
|
||||
default: true,
|
||||
listDisplay: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
apps: defaultApps,
|
||||
}
|
||||
|
||||
54
frontends/nextjs/src/lib/schema/default/components.ts
Normal file
54
frontends/nextjs/src/lib/schema/default/components.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { AppSchema, ModelSchema } from '../../types/schema-types'
|
||||
import { authorFields, postFields, productFields } from './forms'
|
||||
|
||||
export const blogModels: ModelSchema[] = [
|
||||
{
|
||||
name: 'post',
|
||||
label: 'Post',
|
||||
labelPlural: 'Posts',
|
||||
icon: 'Article',
|
||||
listDisplay: ['title', 'author', 'status', 'publishedAt'],
|
||||
listFilter: ['status', 'author'],
|
||||
searchFields: ['title', 'content'],
|
||||
ordering: ['-publishedAt'],
|
||||
fields: postFields,
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
label: 'Author',
|
||||
labelPlural: 'Authors',
|
||||
icon: 'User',
|
||||
listDisplay: ['name', 'email', 'active', 'createdAt'],
|
||||
listFilter: ['active'],
|
||||
searchFields: ['name', 'email'],
|
||||
ordering: ['name'],
|
||||
fields: authorFields,
|
||||
},
|
||||
]
|
||||
|
||||
export const ecommerceModels: ModelSchema[] = [
|
||||
{
|
||||
name: 'product',
|
||||
label: 'Product',
|
||||
labelPlural: 'Products',
|
||||
icon: 'ShoppingCart',
|
||||
listDisplay: ['name', 'price', 'stock', 'available'],
|
||||
listFilter: ['available', 'category'],
|
||||
searchFields: ['name', 'description'],
|
||||
ordering: ['name'],
|
||||
fields: productFields,
|
||||
},
|
||||
]
|
||||
|
||||
export const defaultApps: AppSchema[] = [
|
||||
{
|
||||
name: 'blog',
|
||||
label: 'Blog',
|
||||
models: blogModels,
|
||||
},
|
||||
{
|
||||
name: 'ecommerce',
|
||||
label: 'E-Commerce',
|
||||
models: ecommerceModels,
|
||||
},
|
||||
]
|
||||
244
frontends/nextjs/src/lib/schema/default/forms.ts
Normal file
244
frontends/nextjs/src/lib/schema/default/forms.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import type { FieldSchema } from '../../types/schema-types'
|
||||
import { authorValidations, postValidations, productValidations } from './validation'
|
||||
|
||||
export const postFields: FieldSchema[] = [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
label: 'ID',
|
||||
required: true,
|
||||
unique: true,
|
||||
editable: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
label: 'Title',
|
||||
required: true,
|
||||
validation: postValidations.title,
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'string',
|
||||
label: 'Slug',
|
||||
required: true,
|
||||
unique: true,
|
||||
helpText: 'URL-friendly version of the title',
|
||||
validation: postValidations.slug,
|
||||
listDisplay: false,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'text',
|
||||
label: 'Content',
|
||||
required: true,
|
||||
helpText: 'Main post content',
|
||||
listDisplay: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'excerpt',
|
||||
type: 'text',
|
||||
label: 'Excerpt',
|
||||
required: false,
|
||||
helpText: ['Short summary of the post', 'Used in list views and previews'],
|
||||
validation: postValidations.excerpt,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
type: 'relation',
|
||||
label: 'Author',
|
||||
required: true,
|
||||
relatedModel: 'author',
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
label: 'Status',
|
||||
required: true,
|
||||
default: 'draft',
|
||||
choices: [
|
||||
{ value: 'draft', label: 'Draft' },
|
||||
{ value: 'published', label: 'Published' },
|
||||
{ value: 'archived', label: 'Archived' },
|
||||
],
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'featured',
|
||||
type: 'boolean',
|
||||
label: 'Featured',
|
||||
default: false,
|
||||
helpText: 'Display on homepage',
|
||||
listDisplay: true,
|
||||
},
|
||||
{
|
||||
name: 'publishedAt',
|
||||
type: 'datetime',
|
||||
label: 'Published At',
|
||||
required: false,
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
type: 'json',
|
||||
label: 'Tags',
|
||||
required: false,
|
||||
helpText: 'JSON array of tag strings',
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'views',
|
||||
type: 'number',
|
||||
label: 'Views',
|
||||
default: 0,
|
||||
validation: postValidations.views,
|
||||
listDisplay: false,
|
||||
},
|
||||
]
|
||||
|
||||
export const authorFields: FieldSchema[] = [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
label: 'ID',
|
||||
required: true,
|
||||
unique: true,
|
||||
editable: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
label: 'Name',
|
||||
required: true,
|
||||
validation: authorValidations.name,
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
label: 'Email',
|
||||
required: true,
|
||||
unique: true,
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'bio',
|
||||
type: 'text',
|
||||
label: 'Bio',
|
||||
required: false,
|
||||
helpText: 'Author biography',
|
||||
validation: authorValidations.bio,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
type: 'url',
|
||||
label: 'Website',
|
||||
required: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'active',
|
||||
type: 'boolean',
|
||||
label: 'Active',
|
||||
default: true,
|
||||
listDisplay: true,
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
type: 'datetime',
|
||||
label: 'Created At',
|
||||
required: true,
|
||||
editable: false,
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
]
|
||||
|
||||
export const productFields: FieldSchema[] = [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
label: 'ID',
|
||||
required: true,
|
||||
unique: true,
|
||||
editable: false,
|
||||
listDisplay: false,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
label: 'Product Name',
|
||||
required: true,
|
||||
validation: productValidations.name,
|
||||
listDisplay: true,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
label: 'Description',
|
||||
required: false,
|
||||
helpText: 'Product description',
|
||||
listDisplay: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
type: 'number',
|
||||
label: 'Price',
|
||||
required: true,
|
||||
validation: productValidations.price,
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'stock',
|
||||
type: 'number',
|
||||
label: 'Stock',
|
||||
required: true,
|
||||
default: 0,
|
||||
validation: productValidations.stock,
|
||||
listDisplay: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'category',
|
||||
type: 'select',
|
||||
label: 'Category',
|
||||
required: true,
|
||||
choices: [
|
||||
{ value: 'electronics', label: 'Electronics' },
|
||||
{ value: 'clothing', label: 'Clothing' },
|
||||
{ value: 'books', label: 'Books' },
|
||||
{ value: 'home', label: 'Home & Garden' },
|
||||
{ value: 'toys', label: 'Toys' },
|
||||
],
|
||||
listDisplay: false,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'available',
|
||||
type: 'boolean',
|
||||
label: 'Available',
|
||||
default: true,
|
||||
listDisplay: true,
|
||||
},
|
||||
]
|
||||
19
frontends/nextjs/src/lib/schema/default/validation.ts
Normal file
19
frontends/nextjs/src/lib/schema/default/validation.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { FieldSchema } from '../../types/schema-types'
|
||||
|
||||
export const postValidations: Record<string, FieldSchema['validation']> = {
|
||||
title: { minLength: 3, maxLength: 200 },
|
||||
slug: { pattern: '^[a-z0-9-]+$' },
|
||||
excerpt: { maxLength: 500 },
|
||||
views: { min: 0 },
|
||||
}
|
||||
|
||||
export const authorValidations: Record<string, FieldSchema['validation']> = {
|
||||
name: { minLength: 2, maxLength: 100 },
|
||||
bio: { maxLength: 1000 },
|
||||
}
|
||||
|
||||
export const productValidations: Record<string, FieldSchema['validation']> = {
|
||||
name: { minLength: 3, maxLength: 200 },
|
||||
price: { min: 0 },
|
||||
stock: { min: 0 },
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// Schema utilities exports
|
||||
export * from './schema-utils'
|
||||
export { defaultSchema } from './default-schema'
|
||||
export * from './default/components'
|
||||
export * from './default/forms'
|
||||
export * from './default/validation'
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { scanForVulnerabilities, securityScanner } from '@/lib/security-scanner'
|
||||
|
||||
describe('security-scanner detection', () => {
|
||||
describe('scanJavaScript', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag eval usage as critical',
|
||||
code: ['const safe = true;', 'const result = eval("1 + 1")'].join('\n'),
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
expectedIssueType: 'dangerous',
|
||||
expectedIssuePattern: 'eval',
|
||||
expectedLine: 2,
|
||||
},
|
||||
{
|
||||
name: 'warn on localStorage usage but stay safe',
|
||||
code: 'localStorage.setItem("k", "v")',
|
||||
expectedSeverity: 'low',
|
||||
expectedSafe: true,
|
||||
expectedIssueType: 'warning',
|
||||
expectedIssuePattern: 'localStorage',
|
||||
},
|
||||
{
|
||||
name: 'return safe for benign code',
|
||||
code: 'const sum = (a, b) => a + b',
|
||||
expectedSeverity: 'safe',
|
||||
expectedSafe: true,
|
||||
},
|
||||
])(
|
||||
'should $name',
|
||||
({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern, expectedLine }) => {
|
||||
const result = securityScanner.scanJavaScript(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()
|
||||
if (expectedLine !== undefined) {
|
||||
expect(issue?.line).toBe(expectedLine)
|
||||
}
|
||||
} else {
|
||||
expect(result.issues.length).toBe(0)
|
||||
}
|
||||
|
||||
if (expectedSafe) {
|
||||
expect(result.sanitizedCode).toBe(code)
|
||||
} else {
|
||||
expect(result.sanitizedCode).toBeUndefined()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('scanLua', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag os.execute usage as critical',
|
||||
code: 'os.execute("rm -rf /")',
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
expectedIssueType: 'malicious',
|
||||
expectedIssuePattern: 'os.execute',
|
||||
},
|
||||
{
|
||||
name: 'return safe for simple Lua function',
|
||||
code: 'function add(a, b) return a + b end',
|
||||
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)
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('scanJSON', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag invalid JSON as medium severity',
|
||||
json: '{"value": }',
|
||||
expectedSeverity: 'medium',
|
||||
expectedSafe: false,
|
||||
expectedIssuePattern: 'JSON parse error',
|
||||
},
|
||||
{
|
||||
name: 'flag prototype pollution in JSON as critical',
|
||||
json: '{"__proto__": {"polluted": true}}',
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
expectedIssuePattern: '__proto__',
|
||||
},
|
||||
{
|
||||
name: 'return safe for valid JSON',
|
||||
json: '{"ok": true}',
|
||||
expectedSeverity: 'safe',
|
||||
expectedSafe: true,
|
||||
},
|
||||
])('should $name', ({ json, expectedSeverity, expectedSafe, expectedIssuePattern }) => {
|
||||
const result = securityScanner.scanJSON(json)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
expect(result.safe).toBe(expectedSafe)
|
||||
|
||||
if (expectedIssuePattern) {
|
||||
expect(result.issues.some(issue => issue.pattern.includes(expectedIssuePattern))).toBe(true)
|
||||
} else {
|
||||
expect(result.issues.length).toBe(0)
|
||||
}
|
||||
|
||||
if (expectedSafe) {
|
||||
expect(result.sanitizedCode).toBe(json)
|
||||
} else {
|
||||
expect(result.sanitizedCode).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('scanHTML', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag script tags as critical',
|
||||
html: '<div><script>alert(1)</script></div>',
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
},
|
||||
{
|
||||
name: 'flag inline handlers as high',
|
||||
html: '<button onclick="alert(1)">Click</button>',
|
||||
expectedSeverity: 'high',
|
||||
expectedSafe: false,
|
||||
},
|
||||
{
|
||||
name: 'return safe for plain markup',
|
||||
html: '<div><span>Safe</span></div>',
|
||||
expectedSeverity: 'safe',
|
||||
expectedSafe: true,
|
||||
},
|
||||
])('should $name', ({ html, expectedSeverity, expectedSafe }) => {
|
||||
const result = securityScanner.scanHTML(html)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
expect(result.safe).toBe(expectedSafe)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sanitizeInput', () => {
|
||||
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>',
|
||||
type: 'text' as const,
|
||||
shouldExclude: ['<script', 'onclick', 'javascript:'],
|
||||
},
|
||||
{
|
||||
name: 'remove data html URIs from html',
|
||||
input: '<img src="data:text/html;base64,abc"><script>alert(1)</script>',
|
||||
type: 'html' as const,
|
||||
shouldExclude: ['data:text/html', '<script'],
|
||||
},
|
||||
{
|
||||
name: 'neutralize prototype pollution in json',
|
||||
input: '{"__proto__": {"polluted": true}, "note": "constructor[\\"prototype\\"]"}',
|
||||
type: 'json' as const,
|
||||
shouldInclude: ['_proto_'],
|
||||
shouldExclude: ['__proto__', 'constructor["prototype"]'],
|
||||
},
|
||||
])('should $name', ({ input, type, shouldExclude = [], shouldInclude = [] }) => {
|
||||
const sanitized = securityScanner.sanitizeInput(input, type)
|
||||
shouldExclude.forEach(value => {
|
||||
expect(sanitized).not.toContain(value)
|
||||
})
|
||||
shouldInclude.forEach(value => {
|
||||
expect(sanitized).toContain(value)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('scanForVulnerabilities', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'auto-detects JSON and flags prototype pollution',
|
||||
code: '{"__proto__": {"polluted": true}}',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'auto-detects Lua when function/end present',
|
||||
code: 'function dangerous() os.execute("rm -rf /") end',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'auto-detects HTML and flags script tags',
|
||||
code: '<div><script>alert(1)</script></div>',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'falls back to JavaScript scanning',
|
||||
code: 'const result = eval("1 + 1")',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'honors explicit type parameter',
|
||||
code: 'return 1',
|
||||
type: 'lua' as const,
|
||||
expectedSeverity: 'safe',
|
||||
},
|
||||
])('should $name', ({ code, type, expectedSeverity }) => {
|
||||
const result = scanForVulnerabilities(code, type)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getSeverityColor, getSeverityIcon } from '@/lib/security-scanner'
|
||||
|
||||
describe('security-scanner reporting', () => {
|
||||
describe('getSeverityColor', () => {
|
||||
it.each([
|
||||
{ severity: 'critical', expected: 'error' },
|
||||
{ severity: 'high', expected: 'warning' },
|
||||
{ severity: 'medium', expected: 'info' },
|
||||
{ severity: 'low', expected: 'secondary' },
|
||||
{ severity: 'safe', expected: 'success' },
|
||||
])('should map $severity to expected classes', ({ severity, expected }) => {
|
||||
expect(getSeverityColor(severity)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSeverityIcon', () => {
|
||||
it.each([
|
||||
{ severity: 'critical', expected: '\u{1F6A8}' },
|
||||
{ severity: 'high', expected: '\u26A0\uFE0F' },
|
||||
{ severity: 'medium', expected: '\u26A1' },
|
||||
{ severity: 'low', expected: '\u2139\uFE0F' },
|
||||
{ severity: 'safe', expected: '\u2713' },
|
||||
])('should map $severity to expected icon', ({ severity, expected }) => {
|
||||
expect(getSeverityIcon(severity)).toBe(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,257 +1,2 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { securityScanner, scanForVulnerabilities, getSeverityColor, getSeverityIcon } from '@/lib/security-scanner'
|
||||
|
||||
describe('security-scanner', () => {
|
||||
describe('scanJavaScript', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag eval usage as critical',
|
||||
code: ['const safe = true;', 'const result = eval("1 + 1")'].join('\n'),
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
expectedIssueType: 'dangerous',
|
||||
expectedIssuePattern: 'eval',
|
||||
expectedLine: 2,
|
||||
},
|
||||
{
|
||||
name: 'warn on localStorage usage but stay safe',
|
||||
code: 'localStorage.setItem("k", "v")',
|
||||
expectedSeverity: 'low',
|
||||
expectedSafe: true,
|
||||
expectedIssueType: 'warning',
|
||||
expectedIssuePattern: 'localStorage',
|
||||
},
|
||||
{
|
||||
name: 'return safe for benign code',
|
||||
code: 'const sum = (a, b) => a + b',
|
||||
expectedSeverity: 'safe',
|
||||
expectedSafe: true,
|
||||
},
|
||||
])(
|
||||
'should $name',
|
||||
({ code, expectedSeverity, expectedSafe, expectedIssueType, expectedIssuePattern, expectedLine }) => {
|
||||
const result = securityScanner.scanJavaScript(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()
|
||||
if (expectedLine !== undefined) {
|
||||
expect(issue?.line).toBe(expectedLine)
|
||||
}
|
||||
} else {
|
||||
expect(result.issues.length).toBe(0)
|
||||
}
|
||||
|
||||
if (expectedSafe) {
|
||||
expect(result.sanitizedCode).toBe(code)
|
||||
} else {
|
||||
expect(result.sanitizedCode).toBeUndefined()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('scanLua', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag os.execute usage as critical',
|
||||
code: 'os.execute("rm -rf /")',
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
expectedIssueType: 'malicious',
|
||||
expectedIssuePattern: 'os.execute',
|
||||
},
|
||||
{
|
||||
name: 'return safe for simple Lua function',
|
||||
code: 'function add(a, b) return a + b end',
|
||||
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)
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('scanJSON', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag invalid JSON as medium severity',
|
||||
json: '{"value": }',
|
||||
expectedSeverity: 'medium',
|
||||
expectedSafe: false,
|
||||
expectedIssuePattern: 'JSON parse error',
|
||||
},
|
||||
{
|
||||
name: 'flag prototype pollution in JSON as critical',
|
||||
json: '{"__proto__": {"polluted": true}}',
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
expectedIssuePattern: '__proto__',
|
||||
},
|
||||
{
|
||||
name: 'return safe for valid JSON',
|
||||
json: '{"ok": true}',
|
||||
expectedSeverity: 'safe',
|
||||
expectedSafe: true,
|
||||
},
|
||||
])('should $name', ({ json, expectedSeverity, expectedSafe, expectedIssuePattern }) => {
|
||||
const result = securityScanner.scanJSON(json)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
expect(result.safe).toBe(expectedSafe)
|
||||
|
||||
if (expectedIssuePattern) {
|
||||
expect(result.issues.some(issue => issue.pattern.includes(expectedIssuePattern))).toBe(true)
|
||||
} else {
|
||||
expect(result.issues.length).toBe(0)
|
||||
}
|
||||
|
||||
if (expectedSafe) {
|
||||
expect(result.sanitizedCode).toBe(json)
|
||||
} else {
|
||||
expect(result.sanitizedCode).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('scanHTML', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'flag script tags as critical',
|
||||
html: '<div><script>alert(1)</script></div>',
|
||||
expectedSeverity: 'critical',
|
||||
expectedSafe: false,
|
||||
},
|
||||
{
|
||||
name: 'flag inline handlers as high',
|
||||
html: '<button onclick="alert(1)">Click</button>',
|
||||
expectedSeverity: 'high',
|
||||
expectedSafe: false,
|
||||
},
|
||||
{
|
||||
name: 'return safe for plain markup',
|
||||
html: '<div><span>Safe</span></div>',
|
||||
expectedSeverity: 'safe',
|
||||
expectedSafe: true,
|
||||
},
|
||||
])('should $name', ({ html, expectedSeverity, expectedSafe }) => {
|
||||
const result = securityScanner.scanHTML(html)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
expect(result.safe).toBe(expectedSafe)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sanitizeInput', () => {
|
||||
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>',
|
||||
type: 'text' as const,
|
||||
shouldExclude: ['<script', 'onclick', 'javascript:'],
|
||||
},
|
||||
{
|
||||
name: 'remove data html URIs from html',
|
||||
input: '<img src="data:text/html;base64,abc"><script>alert(1)</script>',
|
||||
type: 'html' as const,
|
||||
shouldExclude: ['data:text/html', '<script'],
|
||||
},
|
||||
{
|
||||
name: 'neutralize prototype pollution in json',
|
||||
input: '{"__proto__": {"polluted": true}, "note": "constructor[\\"prototype\\"]"}',
|
||||
type: 'json' as const,
|
||||
shouldInclude: ['_proto_'],
|
||||
shouldExclude: ['__proto__', 'constructor["prototype"]'],
|
||||
},
|
||||
])('should $name', ({ input, type, shouldExclude = [], shouldInclude = [] }) => {
|
||||
const sanitized = securityScanner.sanitizeInput(input, type)
|
||||
shouldExclude.forEach(value => {
|
||||
expect(sanitized).not.toContain(value)
|
||||
})
|
||||
shouldInclude.forEach(value => {
|
||||
expect(sanitized).toContain(value)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSeverityColor', () => {
|
||||
it.each([
|
||||
{ severity: 'critical', expected: 'error' },
|
||||
{ severity: 'high', expected: 'warning' },
|
||||
{ severity: 'medium', expected: 'info' },
|
||||
{ severity: 'low', expected: 'secondary' },
|
||||
{ severity: 'safe', expected: 'success' },
|
||||
])('should map $severity to expected classes', ({ severity, expected }) => {
|
||||
expect(getSeverityColor(severity)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSeverityIcon', () => {
|
||||
it.each([
|
||||
{ severity: 'critical', expected: '\u{1F6A8}' },
|
||||
{ severity: 'high', expected: '\u26A0\uFE0F' },
|
||||
{ severity: 'medium', expected: '\u26A1' },
|
||||
{ severity: 'low', expected: '\u2139\uFE0F' },
|
||||
{ severity: 'safe', expected: '\u2713' },
|
||||
])('should map $severity to expected icon', ({ severity, expected }) => {
|
||||
expect(getSeverityIcon(severity)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('scanForVulnerabilities', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'auto-detects JSON and flags prototype pollution',
|
||||
code: '{"__proto__": {"polluted": true}}',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'auto-detects Lua when function/end present',
|
||||
code: 'function dangerous() os.execute("rm -rf /") end',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'auto-detects HTML and flags script tags',
|
||||
code: '<div><script>alert(1)</script></div>',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'falls back to JavaScript scanning',
|
||||
code: 'const result = eval("1 + 1")',
|
||||
expectedSeverity: 'critical',
|
||||
},
|
||||
{
|
||||
name: 'honors explicit type parameter',
|
||||
code: 'return 1',
|
||||
type: 'lua' as const,
|
||||
expectedSeverity: 'safe',
|
||||
},
|
||||
])('should $name', ({ code, type, expectedSeverity }) => {
|
||||
const result = scanForVulnerabilities(code, type)
|
||||
expect(result.severity).toBe(expectedSeverity)
|
||||
})
|
||||
})
|
||||
})
|
||||
import './__tests__/security-scanner.detection.test'
|
||||
import './__tests__/security-scanner.reporting.test'
|
||||
|
||||
71
frontends/nextjs/src/theme/types/components.d.ts
vendored
Normal file
71
frontends/nextjs/src/theme/types/components.d.ts
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
import '@mui/material/styles'
|
||||
import '@mui/material/Typography'
|
||||
import '@mui/material/Button'
|
||||
import '@mui/material/Chip'
|
||||
import '@mui/material/IconButton'
|
||||
import '@mui/material/Badge'
|
||||
import '@mui/material/Alert'
|
||||
|
||||
// Typography variants and component overrides
|
||||
declare module '@mui/material/styles' {
|
||||
interface TypographyVariants {
|
||||
code: React.CSSProperties
|
||||
kbd: React.CSSProperties
|
||||
label: React.CSSProperties
|
||||
}
|
||||
|
||||
interface TypographyVariantsOptions {
|
||||
code?: React.CSSProperties
|
||||
kbd?: React.CSSProperties
|
||||
label?: React.CSSProperties
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/Typography' {
|
||||
interface TypographyPropsVariantOverrides {
|
||||
code: true
|
||||
kbd: true
|
||||
label: true
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/Button' {
|
||||
interface ButtonPropsVariantOverrides {
|
||||
soft: true
|
||||
ghost: true
|
||||
}
|
||||
|
||||
interface ButtonPropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/Chip' {
|
||||
interface ChipPropsVariantOverrides {
|
||||
soft: true
|
||||
}
|
||||
|
||||
interface ChipPropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/IconButton' {
|
||||
interface IconButtonPropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/Badge' {
|
||||
interface BadgePropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/Alert' {
|
||||
interface AlertPropsVariantOverrides {
|
||||
soft: true
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
70
frontends/nextjs/src/theme/types/layout.d.ts
vendored
Normal file
70
frontends/nextjs/src/theme/types/layout.d.ts
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
import '@mui/material/styles'
|
||||
|
||||
// Custom theme properties for layout and design tokens
|
||||
declare module '@mui/material/styles' {
|
||||
interface Theme {
|
||||
custom: {
|
||||
fonts: {
|
||||
body: string
|
||||
heading: string
|
||||
mono: string
|
||||
}
|
||||
borderRadius: {
|
||||
none: number
|
||||
sm: number
|
||||
md: number
|
||||
lg: number
|
||||
xl: number
|
||||
full: number
|
||||
}
|
||||
contentWidth: {
|
||||
sm: string
|
||||
md: string
|
||||
lg: string
|
||||
xl: string
|
||||
full: string
|
||||
}
|
||||
sidebar: {
|
||||
width: number
|
||||
collapsedWidth: number
|
||||
}
|
||||
header: {
|
||||
height: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ThemeOptions {
|
||||
custom?: {
|
||||
fonts?: {
|
||||
body?: string
|
||||
heading?: string
|
||||
mono?: string
|
||||
}
|
||||
borderRadius?: {
|
||||
none?: number
|
||||
sm?: number
|
||||
md?: number
|
||||
lg?: number
|
||||
xl?: number
|
||||
full?: number
|
||||
}
|
||||
contentWidth?: {
|
||||
sm?: string
|
||||
md?: string
|
||||
lg?: string
|
||||
xl?: string
|
||||
full?: string
|
||||
}
|
||||
sidebar?: {
|
||||
width?: number
|
||||
collapsedWidth?: number
|
||||
}
|
||||
header?: {
|
||||
height?: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
38
frontends/nextjs/src/theme/types/palette.d.ts
vendored
Normal file
38
frontends/nextjs/src/theme/types/palette.d.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
import '@mui/material/styles'
|
||||
|
||||
// Extend palette with custom neutral colors
|
||||
declare module '@mui/material/styles' {
|
||||
interface Palette {
|
||||
neutral: {
|
||||
50: string
|
||||
100: string
|
||||
200: string
|
||||
300: string
|
||||
400: string
|
||||
500: string
|
||||
600: string
|
||||
700: string
|
||||
800: string
|
||||
900: string
|
||||
950: string
|
||||
}
|
||||
}
|
||||
|
||||
interface PaletteOptions {
|
||||
neutral?: {
|
||||
50?: string
|
||||
100?: string
|
||||
200?: string
|
||||
300?: string
|
||||
400?: string
|
||||
500?: string
|
||||
600?: string
|
||||
700?: string
|
||||
800?: string
|
||||
900?: string
|
||||
950?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
202
frontends/nextjs/src/theme/types/theme.d.ts
vendored
202
frontends/nextjs/src/theme/types/theme.d.ts
vendored
@@ -1,200 +1,10 @@
|
||||
/**
|
||||
* MUI Theme Type Extensions
|
||||
*
|
||||
* This file extends Material-UI's theme interface with custom properties.
|
||||
* All custom design tokens and component variants should be declared here.
|
||||
*
|
||||
* This file aggregates the theme augmentation modules to keep the
|
||||
* main declaration lightweight while still exposing all custom tokens.
|
||||
*/
|
||||
|
||||
import '@mui/material/styles'
|
||||
import '@mui/material/Typography'
|
||||
import '@mui/material/Button'
|
||||
|
||||
// ============================================================================
|
||||
// Custom Palette Extensions
|
||||
// ============================================================================
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
// Extend palette with custom neutral colors
|
||||
interface Palette {
|
||||
neutral: {
|
||||
50: string
|
||||
100: string
|
||||
200: string
|
||||
300: string
|
||||
400: string
|
||||
500: string
|
||||
600: string
|
||||
700: string
|
||||
800: string
|
||||
900: string
|
||||
950: string
|
||||
}
|
||||
}
|
||||
|
||||
interface PaletteOptions {
|
||||
neutral?: {
|
||||
50?: string
|
||||
100?: string
|
||||
200?: string
|
||||
300?: string
|
||||
400?: string
|
||||
500?: string
|
||||
600?: string
|
||||
700?: string
|
||||
800?: string
|
||||
900?: string
|
||||
950?: string
|
||||
}
|
||||
}
|
||||
|
||||
// Custom typography variants
|
||||
interface TypographyVariants {
|
||||
code: React.CSSProperties
|
||||
kbd: React.CSSProperties
|
||||
label: React.CSSProperties
|
||||
}
|
||||
|
||||
interface TypographyVariantsOptions {
|
||||
code?: React.CSSProperties
|
||||
kbd?: React.CSSProperties
|
||||
label?: React.CSSProperties
|
||||
}
|
||||
|
||||
// Custom theme properties
|
||||
interface Theme {
|
||||
custom: {
|
||||
fonts: {
|
||||
body: string
|
||||
heading: string
|
||||
mono: string
|
||||
}
|
||||
borderRadius: {
|
||||
none: number
|
||||
sm: number
|
||||
md: number
|
||||
lg: number
|
||||
xl: number
|
||||
full: number
|
||||
}
|
||||
contentWidth: {
|
||||
sm: string
|
||||
md: string
|
||||
lg: string
|
||||
xl: string
|
||||
full: string
|
||||
}
|
||||
sidebar: {
|
||||
width: number
|
||||
collapsedWidth: number
|
||||
}
|
||||
header: {
|
||||
height: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ThemeOptions {
|
||||
custom?: {
|
||||
fonts?: {
|
||||
body?: string
|
||||
heading?: string
|
||||
mono?: string
|
||||
}
|
||||
borderRadius?: {
|
||||
none?: number
|
||||
sm?: number
|
||||
md?: number
|
||||
lg?: number
|
||||
xl?: number
|
||||
full?: number
|
||||
}
|
||||
contentWidth?: {
|
||||
sm?: string
|
||||
md?: string
|
||||
lg?: string
|
||||
xl?: string
|
||||
full?: string
|
||||
}
|
||||
sidebar?: {
|
||||
width?: number
|
||||
collapsedWidth?: number
|
||||
}
|
||||
header?: {
|
||||
height?: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Typography Variant Mapping
|
||||
// ============================================================================
|
||||
|
||||
declare module '@mui/material/Typography' {
|
||||
interface TypographyPropsVariantOverrides {
|
||||
code: true
|
||||
kbd: true
|
||||
label: true
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Button Variants & Colors
|
||||
// ============================================================================
|
||||
|
||||
declare module '@mui/material/Button' {
|
||||
interface ButtonPropsVariantOverrides {
|
||||
soft: true
|
||||
ghost: true
|
||||
}
|
||||
|
||||
interface ButtonPropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Chip Variants
|
||||
// ============================================================================
|
||||
|
||||
declare module '@mui/material/Chip' {
|
||||
interface ChipPropsVariantOverrides {
|
||||
soft: true
|
||||
}
|
||||
|
||||
interface ChipPropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// IconButton Colors
|
||||
// ============================================================================
|
||||
|
||||
declare module '@mui/material/IconButton' {
|
||||
interface IconButtonPropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Badge Colors
|
||||
// ============================================================================
|
||||
|
||||
declare module '@mui/material/Badge' {
|
||||
interface BadgePropsColorOverrides {
|
||||
neutral: true
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Alert Variants
|
||||
// ============================================================================
|
||||
|
||||
declare module '@mui/material/Alert' {
|
||||
interface AlertPropsVariantOverrides {
|
||||
soft: true
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
export * from './palette'
|
||||
export * from './layout'
|
||||
export * from './components'
|
||||
|
||||
Reference in New Issue
Block a user