diff --git a/dbal/development/src/core/client.ts b/dbal/development/src/core/client.ts new file mode 100644 index 000000000..789cabfc1 --- /dev/null +++ b/dbal/development/src/core/client.ts @@ -0,0 +1,8 @@ +import type { DBALConfig } from '../runtime/config' +import { DBALClient } from './client/client' +export { buildAdapter, buildEntityOperations } from './client/builders' +export { normalizeClientConfig, validateClientConfig } from './client/mappers' + +export const createDBALClient = (config: DBALConfig) => new DBALClient(config) + +export { DBALClient } diff --git a/dbal/development/src/core/client/builders.ts b/dbal/development/src/core/client/builders.ts new file mode 100644 index 000000000..56fcf095f --- /dev/null +++ b/dbal/development/src/core/client/builders.ts @@ -0,0 +1,24 @@ +import type { DBALAdapter } from '../../adapters/adapter' +import type { DBALConfig } from '../../runtime/config' +import { createAdapter } from './adapter-factory' +import { + createComponentOperations, + createLuaScriptOperations, + createPackageOperations, + createPageOperations, + createSessionOperations, + createUserOperations, + createWorkflowOperations +} from '../entities' + +export const buildAdapter = (config: DBALConfig): DBALAdapter => createAdapter(config) + +export const buildEntityOperations = (adapter: DBALAdapter) => ({ + users: createUserOperations(adapter), + pages: createPageOperations(adapter), + components: createComponentOperations(adapter), + workflows: createWorkflowOperations(adapter), + luaScripts: createLuaScriptOperations(adapter), + packages: createPackageOperations(adapter), + sessions: createSessionOperations(adapter) +}) diff --git a/dbal/development/src/core/client/client.ts b/dbal/development/src/core/client/client.ts index b57eb6ea9..6c9a98691 100644 --- a/dbal/development/src/core/client/client.ts +++ b/dbal/development/src/core/client/client.ts @@ -1,7 +1,7 @@ /** * @file client.ts * @description DBAL Client - Main interface for database operations - * + * * Provides CRUD operations for all entities through modular operation handlers. * Each entity type has its own dedicated operations module following the * single-responsibility pattern. @@ -9,82 +9,67 @@ import type { DBALConfig } from '../../runtime/config' import type { DBALAdapter } from '../../adapters/adapter' -import { createAdapter } from './adapter-factory' -import { - createUserOperations, - createPageOperations, - createComponentOperations, - createWorkflowOperations, - createLuaScriptOperations, - createPackageOperations, - createSessionOperations, -} from '../entities' +import { buildAdapter, buildEntityOperations } from './builders' +import { normalizeClientConfig, validateClientConfig } from './mappers' export class DBALClient { private adapter: DBALAdapter private config: DBALConfig + private operations: ReturnType constructor(config: DBALConfig) { - this.config = config - - // Validate configuration - if (!config.adapter) { - throw new Error('Adapter type must be specified') - } - if (config.mode !== 'production' && !config.database?.url) { - throw new Error('Database URL must be specified for non-production mode') - } - - this.adapter = createAdapter(config) + this.config = normalizeClientConfig(validateClientConfig(config)) + this.adapter = buildAdapter(this.config) + this.operations = buildEntityOperations(this.adapter) } /** * User entity operations */ get users() { - return createUserOperations(this.adapter) + return this.operations.users } /** * Page entity operations */ get pages() { - return createPageOperations(this.adapter) + return this.operations.pages } /** * Component hierarchy entity operations */ get components() { - return createComponentOperations(this.adapter) + return this.operations.components } /** * Workflow entity operations */ get workflows() { - return createWorkflowOperations(this.adapter) + return this.operations.workflows } /** * Lua script entity operations */ get luaScripts() { - return createLuaScriptOperations(this.adapter) + return this.operations.luaScripts } /** * Package entity operations */ get packages() { - return createPackageOperations(this.adapter) + return this.operations.packages } /** * Session entity operations */ get sessions() { - return createSessionOperations(this.adapter) + return this.operations.sessions } /** diff --git a/dbal/development/src/core/client/mappers.ts b/dbal/development/src/core/client/mappers.ts new file mode 100644 index 000000000..b9abc9661 --- /dev/null +++ b/dbal/development/src/core/client/mappers.ts @@ -0,0 +1,25 @@ +import type { DBALConfig } from '../../runtime/config' +import { DBALError } from '../foundation/errors' + +export const validateClientConfig = (config: DBALConfig): DBALConfig => { + if (!config.adapter) { + throw DBALError.validationError('Adapter type must be specified', []) + } + + if (config.mode !== 'production' && !config.database?.url) { + throw DBALError.validationError('Database URL must be specified for non-production mode', []) + } + + return config +} + +export const normalizeClientConfig = (config: DBALConfig): DBALConfig => ({ + ...config, + security: { + sandbox: config.security?.sandbox ?? 'strict', + enableAuditLog: config.security?.enableAuditLog ?? true + }, + performance: { + ...config.performance + } +}) diff --git a/dbal/development/src/index.ts b/dbal/development/src/index.ts index 7acf658e0..e98734f17 100644 --- a/dbal/development/src/index.ts +++ b/dbal/development/src/index.ts @@ -1,4 +1,4 @@ -export { DBALClient } from './core/client/client' +export { DBALClient, createDBALClient } from './core/client' export type { DBALConfig } from './runtime/config' export type * from './core/foundation/types' export { DBALError, DBALErrorCode } from './core/foundation/errors' diff --git a/detection/detectors/class-detector.ts b/detection/detectors/class-detector.ts new file mode 100644 index 000000000..2fb08b909 --- /dev/null +++ b/detection/detectors/class-detector.ts @@ -0,0 +1,64 @@ +import * as ts from 'typescript' +import { Detector, DetectionFinding, DetectorContext } from '..' + +const getLocation = (sourceFile: ts.SourceFile, node: ts.Node) => { + const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()) + + return { + line: line + 1, + column: character + 1 + } +} + +const getClassName = ( + node: ts.ClassDeclaration | ts.ClassExpression, + sourceFile: ts.SourceFile +): string => { + if (node.name) { + return node.name.getText(sourceFile) + } + + const parent = node.parent + + if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) { + return parent.name.text + } + + return 'anonymous' +} + +const collectClasses = (context: DetectorContext): DetectionFinding[] => { + const sourceFile = ts.createSourceFile( + context.filePath, + context.source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX + ) + + const findings: DetectionFinding[] = [] + + const visit = (node: ts.Node) => { + if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { + const name = getClassName(node, sourceFile) + findings.push({ + detectorId: 'class-detector', + name, + message: `Class detected: ${name}`, + location: getLocation(sourceFile, node) + }) + } + + ts.forEachChild(node, visit) + } + + visit(sourceFile) + + return findings +} + +export const classDetector: Detector = { + id: 'class-detector', + description: 'Detects class declarations and expressions within a TypeScript/TSX source file.', + detect: collectClasses +} diff --git a/detection/detectors/function-detector.ts b/detection/detectors/function-detector.ts new file mode 100644 index 000000000..7bfffafd1 --- /dev/null +++ b/detection/detectors/function-detector.ts @@ -0,0 +1,78 @@ +import * as ts from 'typescript' +import { Detector, DetectionFinding, DetectorContext } from '..' + +type FunctionLike = + | ts.FunctionDeclaration + | ts.FunctionExpression + | ts.ArrowFunction + | ts.MethodDeclaration + | ts.ConstructorDeclaration + +const getLocation = (sourceFile: ts.SourceFile, node: ts.Node) => { + const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()) + + return { + line: line + 1, + column: character + 1 + } +} + +const getFunctionName = (node: FunctionLike, sourceFile: ts.SourceFile): string => { + if ('name' in node && node.name) { + return node.name.getText(sourceFile) + } + + const parent = node.parent + + if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) { + return parent.name.text + } + + if (ts.isPropertyAssignment(parent) && ts.isIdentifier(parent.name)) { + return parent.name.text + } + + return 'anonymous' +} + +const collectFunctions = (context: DetectorContext): DetectionFinding[] => { + const sourceFile = ts.createSourceFile( + context.filePath, + context.source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX + ) + + const findings: DetectionFinding[] = [] + + const visit = (node: ts.Node) => { + if ( + ts.isFunctionDeclaration(node) || + ts.isFunctionExpression(node) || + ts.isArrowFunction(node) || + ts.isMethodDeclaration(node) || + ts.isConstructorDeclaration(node) + ) { + const name = getFunctionName(node, sourceFile) + findings.push({ + detectorId: 'function-detector', + name, + message: `Function detected: ${name}`, + location: getLocation(sourceFile, node) + }) + } + + ts.forEachChild(node, visit) + } + + visit(sourceFile) + + return findings +} + +export const functionDetector: Detector = { + id: 'function-detector', + description: 'Detects functions and methods within a TypeScript/TSX source file.', + detect: collectFunctions +} diff --git a/detection/index.ts b/detection/index.ts new file mode 100644 index 000000000..7f5eb21f4 --- /dev/null +++ b/detection/index.ts @@ -0,0 +1,45 @@ +import { classDetector } from './detectors/class-detector' +import { functionDetector } from './detectors/function-detector' + +export type DetectorContext = { + filePath: string + source: string +} + +export type DetectionFinding = { + detectorId: string + name: string + message: string + location?: { + line: number + column: number + } +} + +export interface Detector { + id: string + description: string + detect: (context: DetectorContext) => DetectionFinding[] +} + +export class DetectorRegistry { + private readonly detectors: Detector[] = [] + + register(detector: Detector): void { + this.detectors.push(detector) + } + + list(): Detector[] { + return [...this.detectors] + } + + run(context: DetectorContext): DetectionFinding[] { + return this.detectors.flatMap((detector) => detector.detect(context)) + } +} + +export const registry = new DetectorRegistry() + +const builtInDetectors: Detector[] = [functionDetector, classDetector] + +builtInDetectors.forEach((detector) => registry.register(detector)) diff --git a/frontends/nextjs/src/components/auth/god-credentials/Form.tsx b/frontends/nextjs/src/components/auth/god-credentials/Form.tsx new file mode 100644 index 000000000..88700c036 --- /dev/null +++ b/frontends/nextjs/src/components/auth/god-credentials/Form.tsx @@ -0,0 +1,83 @@ +import { Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui' + +export interface GodCredentialsFormProps { + duration: number + unit: 'minutes' | 'hours' + onDurationChange: (value: number) => void + onUnitChange: (unit: 'minutes' | 'hours') => void + onSave: () => void + onResetExpiry: () => void + onClearExpiry: () => void +} + +export function GodCredentialsForm({ + duration, + unit, + onDurationChange, + onUnitChange, + onSave, + onResetExpiry, + onClearExpiry, +}: GodCredentialsFormProps) { + return ( +
+
+
+ +
+ onDurationChange(Number(e.target.value))} + className="flex-1" + /> + +
+

+ Set the duration for how long credentials are visible (1 minute to 24 hours) +

+
+ +
+ +
+
+ +
+
+ +

+ Reset or clear the current expiry timer +

+
+ +
+ + +
+ +

+ Reset Timer: Restart the countdown using the configured duration
+ Clear Expiry: Remove expiry time (credentials will show on next page load) +

+
+
+ ) +} diff --git a/frontends/nextjs/src/components/auth/god-credentials/Summary.tsx b/frontends/nextjs/src/components/auth/god-credentials/Summary.tsx new file mode 100644 index 000000000..84cba9aa9 --- /dev/null +++ b/frontends/nextjs/src/components/auth/god-credentials/Summary.tsx @@ -0,0 +1,42 @@ +import { Alert, AlertDescription, Badge } from '@/components/ui' +import { CheckCircle, WarningCircle } from '@phosphor-icons/react' + +export interface GodCredentialsSummaryProps { + isActive: boolean + expiryTime: number + timeRemaining: string +} + +export function GodCredentialsSummary({ isActive, expiryTime, timeRemaining }: GodCredentialsSummaryProps) { + if (isActive) { + return ( + + + +
+

+ God credentials are currently visible + Active +

+

+ Time remaining: {timeRemaining} +

+
+
+
+ ) + } + + if (!isActive && expiryTime > 0) { + return ( + + + +

God credentials have expired or been hidden

+
+
+ ) + } + + return null +} diff --git a/frontends/nextjs/src/components/auth/unified-login/LoginForm.tsx b/frontends/nextjs/src/components/auth/unified-login/LoginForm.tsx new file mode 100644 index 000000000..9e3a81491 --- /dev/null +++ b/frontends/nextjs/src/components/auth/unified-login/LoginForm.tsx @@ -0,0 +1,48 @@ +import { Button, Input, Label, Alert, AlertDescription } from '@/components/ui' +import { SignIn } from '@phosphor-icons/react' + +export interface LoginFormProps { + username: string + password: string + onUsernameChange: (value: string) => void + onPasswordChange: (value: string) => void + onSubmit: () => void +} + +export function LoginForm({ username, password, onUsernameChange, onPasswordChange, onSubmit }: LoginFormProps) { + return ( +
+
+ + onUsernameChange(e.target.value)} + placeholder="Enter username" + onKeyDown={(e) => e.key === 'Enter' && onSubmit()} + /> +
+
+ + onPasswordChange(e.target.value)} + placeholder="Enter password" + onKeyDown={(e) => e.key === 'Enter' && onSubmit()} + /> +
+ + + +

Test Credentials:

+

Check browser console for default user passwords (they are scrambled on first run)

+
+
+
+ ) +} diff --git a/frontends/nextjs/src/components/auth/unified-login/ProviderList.tsx b/frontends/nextjs/src/components/auth/unified-login/ProviderList.tsx new file mode 100644 index 000000000..3bfd01527 --- /dev/null +++ b/frontends/nextjs/src/components/auth/unified-login/ProviderList.tsx @@ -0,0 +1,50 @@ +import { Button, Separator } from '@/components/ui' +import { GoogleLogo, GithubLogo, IconProps } from '@phosphor-icons/react' + +export interface Provider { + name: string + description?: string + icon?: React.ComponentType +} + +export interface ProviderListProps { + providers: Provider[] + onSelect?: (provider: Provider) => void +} + +const FALLBACK_PROVIDERS: Provider[] = [ + { name: 'Google', description: 'Use your Google Workspace account', icon: GoogleLogo }, + { name: 'GitHub', description: 'Developer SSO via GitHub', icon: GithubLogo }, +] + +export function ProviderList({ providers, onSelect }: ProviderListProps) { + const entries = providers.length > 0 ? providers : FALLBACK_PROVIDERS + + return ( +
+ +

Or continue with

+
+ {entries.map((provider) => { + const Icon = provider.icon + return ( + + ) + })} +
+
+ ) +} diff --git a/frontends/nextjs/src/components/managers/package/PackageDetailsDialog.tsx b/frontends/nextjs/src/components/managers/package/PackageDetailsDialog.tsx new file mode 100644 index 000000000..14b63509e --- /dev/null +++ b/frontends/nextjs/src/components/managers/package/PackageDetailsDialog.tsx @@ -0,0 +1,185 @@ +import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, ScrollArea, Separator, Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui' +import type { PackageCatalogData } from '@/lib/packages/core/package-catalog' +import type { InstalledPackage } from '@/lib/package-types' +import { Download, Star, Tag, Trash, User } from '@phosphor-icons/react' +import { DependenciesTab } from './tabs/DependenciesTab' +import { ScriptsTab } from './tabs/ScriptsTab' + +interface PackageDetailsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + selectedPackage: PackageCatalogData | null + installing: boolean + onInstall: (packageId: string) => void + onUninstall: (packageId: string) => void + installedPackages: InstalledPackage[] + getCatalogEntry: (packageId: string) => PackageCatalogData | undefined +} + +export function PackageDetailsDialog({ + open, + onOpenChange, + selectedPackage, + installing, + onInstall, + onUninstall, + installedPackages, + getCatalogEntry, +}: PackageDetailsDialogProps) { + if (!selectedPackage) return null + + const { manifest, content } = selectedPackage + + return ( + + + +
+
+ {manifest.icon} +
+
+ {manifest.name} + {manifest.description} +
+ {manifest.category} +
+ + {manifest.downloadCount.toLocaleString()} +
+
+ + {manifest.rating} +
+
+
+
+
+ + + + +
+ + Overview + Dependencies + Scripts + +
+ + + +
+
+
+

Author

+
+ + {manifest.author} +
+
+
+

Version

+

{manifest.version}

+
+
+ +
+

Tags

+
+ {manifest.tags.map(tag => ( + + + {tag} + + ))} +
+
+ +
+

Includes

+
+
+
Data Models
+
{content.schemas.length}
+
+
+
Pages
+
{content.pages.length}
+
+
+
Workflows
+
{content.workflows.length}
+
+
+
Scripts
+
{content.luaScripts.length}
+
+
+
+ + {content.schemas.length > 0 && ( +
+

Data Models

+
+ {content.schemas.map(schema => ( +
+
{schema.displayName || schema.name}
+
{schema.fields.length} fields
+
+ ))} +
+
+ )} + + {content.pages.length > 0 && ( +
+

Pages

+
+ {content.pages.map(page => ( +
+
{page.title}
+
{page.path}
+
+ ))} +
+
+ )} +
+
+
+ + + + + + + + + + + + +
+ + + {manifest.installed ? ( + + ) : ( + + )} + +
+
+ ) +} diff --git a/frontends/nextjs/src/components/managers/package/PackageManager.tsx b/frontends/nextjs/src/components/managers/package/PackageManager.tsx index 5bdeda96e..ea5300946 100644 --- a/frontends/nextjs/src/components/managers/package/PackageManager.tsx +++ b/frontends/nextjs/src/components/managers/package/PackageManager.tsx @@ -1,12 +1,12 @@ import { useState } from 'react' -import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, ScrollArea, Separator } from '@/components/ui' -import { toast } from 'sonner' -import { installPackage, togglePackageEnabled, uninstallPackage } from '@/lib/api/packages' +import { Button } from '@/components/ui' import type { PackageCatalogData } from '@/lib/packages/core/package-catalog' -import { ArrowSquareIn, Download, Export, Package, Star, Tag, Trash, User } from '@phosphor-icons/react' +import { ArrowSquareIn, Export, Package } from '@phosphor-icons/react' +import { PackageDetailsDialog } from './PackageDetailsDialog' import { PackageImportExport } from './PackageImportExport' import { PackageFilters } from './package-manager/PackageFilters' import { PackageTabs } from './package-manager/PackageTabs' +import { usePackageActions } from './package-manager/usePackageActions' import { usePackages } from './package-manager/usePackages' interface PackageManagerProps { @@ -31,61 +31,12 @@ export function PackageManager({ onClose }: PackageManagerProps) { } = usePackages() const [selectedPackage, setSelectedPackage] = useState(null) const [showDetails, setShowDetails] = useState(false) - const [installing, setInstalling] = useState(false) const [showImportExport, setShowImportExport] = useState(false) const [importExportMode, setImportExportMode] = useState<'import' | 'export'>('export') - - const handleInstallPackage = async (packageId: string) => { - setInstalling(true) - try { - const packageEntry = getCatalogEntry(packageId) - if (!packageEntry) { - toast.error('Package not found') - return - } - - await installPackage(packageId) - - toast.success(`${packageEntry.manifest.name} installed successfully!`) - await loadPackages() - setShowDetails(false) - } catch (error) { - console.error('Installation error:', error) - toast.error('Failed to install package') - } finally { - setInstalling(false) - } - } - - const handleUninstallPackage = async (packageId: string) => { - try { - const packageEntry = getCatalogEntry(packageId) - if (!packageEntry) { - toast.error('Package not found') - return - } - - await uninstallPackage(packageId) - - toast.success(`${packageEntry.manifest.name} uninstalled successfully!`) - await loadPackages() - setShowDetails(false) - } catch (error) { - console.error('Uninstallation error:', error) - toast.error('Failed to uninstall package') - } - } - - const handleTogglePackage = async (packageId: string, enabled: boolean) => { - try { - await togglePackageEnabled(packageId, enabled) - toast.success(enabled ? 'Package enabled' : 'Package disabled') - await loadPackages() - } catch (error) { - console.error('Toggle error:', error) - toast.error('Failed to toggle package') - } - } + const { installing, handleInstallPackage, handleUninstallPackage, handleTogglePackage } = usePackageActions({ + loadPackages, + getCatalogEntry, + }) const openPackageDetails = (packageId: string) => { const catalogEntry = getCatalogEntry(packageId) @@ -162,131 +113,16 @@ export function PackageManager({ onClose }: PackageManagerProps) { /> - - - {selectedPackage && ( - <> - -
-
- {selectedPackage.manifest.icon} -
-
- {selectedPackage.manifest.name} - {selectedPackage.manifest.description} -
- {selectedPackage.manifest.category} -
- - {selectedPackage.manifest.downloadCount.toLocaleString()} -
-
- - {selectedPackage.manifest.rating} -
-
-
-
-
- - - - -
-
-

Author

-
- - {selectedPackage.manifest.author} -
-
- -
-

Version

-

{selectedPackage.manifest.version}

-
- -
-

Tags

-
- {selectedPackage.manifest.tags.map(tag => ( - - - {tag} - - ))} -
-
- -
-

Includes

-
-
-
Data Models
-
{selectedPackage.content.schemas.length}
-
-
-
Pages
-
{selectedPackage.content.pages.length}
-
-
-
Workflows
-
{selectedPackage.content.workflows.length}
-
-
-
Scripts
-
{selectedPackage.content.luaScripts.length}
-
-
-
- - {selectedPackage.content.schemas.length > 0 && ( -
-

Data Models

-
- {selectedPackage.content.schemas.map(schema => ( -
-
{schema.displayName || schema.name}
-
{schema.fields.length} fields
-
- ))} -
-
- )} - - {selectedPackage.content.pages.length > 0 && ( -
-

Pages

-
- {selectedPackage.content.pages.map(page => ( -
-
{page.title}
-
{page.path}
-
- ))} -
-
- )} -
-
- - - {selectedPackage.manifest.installed ? ( - - ) : ( - - )} - - - )} -
-
+ handleInstallPackage(packageId, () => setShowDetails(false))} + onUninstall={(packageId) => handleUninstallPackage(packageId, () => setShowDetails(false))} + installedPackages={installedPackages} + getCatalogEntry={getCatalogEntry} + /> -
-
- - setManifest(prev => ({ ...prev, name: e.target.value }))} - /> -
- -
-
- - setManifest(prev => ({ ...prev, version: e.target.value }))} - /> -
- -
- - setManifest(prev => ({ ...prev, author: e.target.value }))} - /> -
-
- -
- -