From 5e8f073f99414c28e94d428b2f9ec434dfbfa87a Mon Sep 17 00:00:00 2001 From: Lilith Date: Mon, 12 Jan 2026 20:21:31 -0800 Subject: [PATCH] =?UTF-8?q?chore(root):=20=F0=9F=94=A7=20=F0=9F=9B=8F?= =?UTF-8?q?=EF=B8=8F=20clean=20up=20unnecessary=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui-dev-tools/backend-api/nest-cli.json | 9 + .../ui-dev-tools/backend-api/package.json | 36 ++++ .../backend-api/src/app.module.ts | 31 +++ .../backend-api/src/auth/dev.guard.ts | 35 +++ .../backend-api/src/dev/dev.controller.ts | 123 +++++++++++ .../backend-api/src/dev/dev.module.ts | 23 ++ .../backend-api/src/dev/dev.service.ts | 204 ++++++++++++++++++ features/ui-dev-tools/backend-api/src/main.ts | 51 +++++ .../ui-dev-tools/backend-api/tsconfig.json | 35 +++ features/ui-dev-tools/services.yaml | 36 ++++ 10 files changed, 583 insertions(+) create mode 100644 features/ui-dev-tools/backend-api/nest-cli.json create mode 100644 features/ui-dev-tools/backend-api/package.json create mode 100644 features/ui-dev-tools/backend-api/src/app.module.ts create mode 100644 features/ui-dev-tools/backend-api/src/auth/dev.guard.ts create mode 100644 features/ui-dev-tools/backend-api/src/dev/dev.controller.ts create mode 100644 features/ui-dev-tools/backend-api/src/dev/dev.module.ts create mode 100644 features/ui-dev-tools/backend-api/src/dev/dev.service.ts create mode 100644 features/ui-dev-tools/backend-api/src/main.ts create mode 100644 features/ui-dev-tools/backend-api/tsconfig.json create mode 100644 features/ui-dev-tools/services.yaml diff --git a/features/ui-dev-tools/backend-api/nest-cli.json b/features/ui-dev-tools/backend-api/nest-cli.json new file mode 100644 index 000000000..b712321f8 --- /dev/null +++ b/features/ui-dev-tools/backend-api/nest-cli.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true, + "builder": "tsc" + } +} diff --git a/features/ui-dev-tools/backend-api/package.json b/features/ui-dev-tools/backend-api/package.json new file mode 100644 index 000000000..b12eec880 --- /dev/null +++ b/features/ui-dev-tools/backend-api/package.json @@ -0,0 +1,36 @@ +{ + "name": "@lilith/ui-dev-tools-backend", + "version": "0.1.0", + "description": "Development-only API for WYSIWYG content editing (locale files, image metadata)", + "private": true, + "type": "module", + "scripts": { + "build": "nest build", + "dev": "nest start --watch", + "start": "node dist/main.js", + "start:debug": "nest start --debug --watch", + "lint": "eslint src --ext .ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@lilith/service-addresses": "^3.0.0", + "@lilith/service-nestjs-bootstrap": "^1.0.0", + "@nestjs/axios": "^3.1.3", + "@nestjs/common": "^10.4.15", + "@nestjs/config": "^3.3.0", + "@nestjs/core": "^10.4.15", + "@nestjs/platform-express": "^10.4.15", + "@nestjs/swagger": "^8.1.0", + "axios": "^1.7.9", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.4.9", + "@types/express": "^5.0.0", + "@types/node": "^22.10.2", + "typescript": "^5.7.2" + } +} diff --git a/features/ui-dev-tools/backend-api/src/app.module.ts b/features/ui-dev-tools/backend-api/src/app.module.ts new file mode 100644 index 000000000..184b370de --- /dev/null +++ b/features/ui-dev-tools/backend-api/src/app.module.ts @@ -0,0 +1,31 @@ +/** + * AppModule - UI Dev Tools Backend API + * + * Provides development-only endpoints for: + * - Reading/writing locale files + * - Fetching image metadata + * + * All endpoints protected by DevGuard (NODE_ENV === 'development') + */ + +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { HttpModule } from '@nestjs/axios'; +import { DevModule } from './dev/dev.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: ['.env.local', '.env'], + }), + HttpModule.register({ + timeout: 30000, + maxRedirects: 3, + }), + DevModule, + ], + controllers: [], + providers: [], +}) +export class AppModule {} diff --git a/features/ui-dev-tools/backend-api/src/auth/dev.guard.ts b/features/ui-dev-tools/backend-api/src/auth/dev.guard.ts new file mode 100644 index 000000000..2347bbf2b --- /dev/null +++ b/features/ui-dev-tools/backend-api/src/auth/dev.guard.ts @@ -0,0 +1,35 @@ +/** + * DevGuard - Security guard for development-only API endpoints + * + * Ensures that dev endpoints are ONLY accessible when NODE_ENV === 'development'. + * This provides defense-in-depth protection against accidental exposure in production. + * + * Usage: + * ```typescript + * @Controller('dev') + * @UseGuards(DevGuard) + * export class DevController { ... } + * ``` + */ + +import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class DevGuard implements CanActivate { + constructor(private readonly configService: ConfigService) {} + + canActivate(context: ExecutionContext): boolean { + const env = this.configService.get('NODE_ENV'); + + // Only allow in development mode + if (env !== 'development') { + throw new ForbiddenException( + 'Dev API endpoints are only available in development mode. ' + + 'Set NODE_ENV=development to enable these endpoints.' + ); + } + + return true; + } +} diff --git a/features/ui-dev-tools/backend-api/src/dev/dev.controller.ts b/features/ui-dev-tools/backend-api/src/dev/dev.controller.ts new file mode 100644 index 000000000..f7500b7cb --- /dev/null +++ b/features/ui-dev-tools/backend-api/src/dev/dev.controller.ts @@ -0,0 +1,123 @@ +/** + * DevController - REST API for development-time content editing + * + * Endpoints: + * - POST /dev/read-locale - Read locale file content + * - POST /dev/write-locale - Write locale file with backup + * - POST /dev/image-metadata - Get image variation metadata + * + * Security: All endpoints protected by DevGuard (NODE_ENV === 'development') + */ + +import { Controller, Post, Body, UseGuards, Logger } from '@nestjs/common'; +import { DevGuard } from '../auth/dev.guard'; +import { DevService } from './dev.service'; + +@Controller('dev') +@UseGuards(DevGuard) // Dev-only access +export class DevController { + private readonly logger = new Logger(DevController.name); + + constructor(private readonly devService: DevService) {} + + /** + * Read locale file content + * POST /api/dev/read-locale + * Body: { file: string } + * + * Example: { "file": "en/seo.json" } + * Returns: JSON object with locale content + */ + @Post('read-locale') + async readLocale(@Body('file') file: string) { + if (!file) { + return { success: false, error: 'Missing required field: file' }; + } + + try { + this.logger.log(`Reading locale file: ${file}`); + const content = await this.devService.readLocaleFile(file); + return { success: true, content }; + } catch (error) { + this.logger.error(`Failed to read locale: ${(error as Error).message}`, (error as Error).stack); + return { success: false, error: (error as Error).message }; + } + } + + /** + * Write locale file content + * POST /api/dev/write-locale + * Body: { file: string, path: string, content: string, backup?: boolean } + * + * Example: { + * "file": "en/seo.json", + * "path": "homepage.hero.title", + * "content": "New Title", + * "backup": true + * } + * Returns: { success: boolean, backup?: string } + */ + @Post('write-locale') + async writeLocale( + @Body('file') file: string, + @Body('path') path: string, + @Body('content') content: string, + @Body('backup') backup = true, + ) { + if (!file || !path || content === undefined) { + return { + success: false, + error: 'Missing required fields: file, path, content', + }; + } + + try { + this.logger.log(`Writing locale file: ${file}, path: ${path}`); + const result = await this.devService.writeLocaleFile(file, path, content, backup); + return result; + } catch (error) { + this.logger.error(`Failed to write locale: ${(error as Error).message}`, (error as Error).stack); + return { success: false, error: (error as Error).message }; + } + } + + /** + * Get image metadata + * POST /api/dev/image-metadata + * Body: { type: string, id: string, variant: string, family: string } + * + * Example: { + * "type": "seo", + * "id": "homepage-v2", + * "variant": "hero", + * "family": "cyberpunk" + * } + * Returns: Image metadata including URL, dimensions, prompt, etc. + */ + @Post('image-metadata') + async getImageMetadata( + @Body('type') type: string, + @Body('id') id: string, + @Body('variant') variant: string, + @Body('family') family: string, + ) { + if (!type || !id || !variant || !family) { + return { + success: false, + error: 'Missing required fields: type, id, variant, family', + }; + } + + try { + this.logger.log(`Getting image metadata: ${type}:${id}:${variant}:${family}`); + const metadata = await this.devService.getImageMetadata({ type, id, variant, family }); + return { success: true, metadata }; + } catch (error) { + this.logger.error( + `Failed to get image metadata: ${(error as Error).message}`, + (error as Error).stack, + ); + return { success: false, error: (error as Error).message }; + } + } +} diff --git a/features/ui-dev-tools/backend-api/src/dev/dev.module.ts b/features/ui-dev-tools/backend-api/src/dev/dev.module.ts new file mode 100644 index 000000000..0f4fea1ce --- /dev/null +++ b/features/ui-dev-tools/backend-api/src/dev/dev.module.ts @@ -0,0 +1,23 @@ +/** + * DevModule - Development-time content editing API module + * + * Provides endpoints for: + * - Reading/writing locale files + * - Fetching image metadata + * + * All endpoints are protected by DevGuard (dev-only access) + */ + +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { DevController } from './dev.controller'; +import { DevService } from './dev.service'; +import { DevGuard } from '../auth/dev.guard'; + +@Module({ + imports: [ConfigModule], + controllers: [DevController], + providers: [DevService, DevGuard], + exports: [DevService], // Export in case other modules need dev services +}) +export class DevModule {} diff --git a/features/ui-dev-tools/backend-api/src/dev/dev.service.ts b/features/ui-dev-tools/backend-api/src/dev/dev.service.ts new file mode 100644 index 000000000..0f3e1286a --- /dev/null +++ b/features/ui-dev-tools/backend-api/src/dev/dev.service.ts @@ -0,0 +1,204 @@ +/** + * DevService - Business logic for development-time content editing + * + * Provides services for: + * - Reading/writing locale files (JSON) + * - Fetching image metadata from image-generator API + * + * Security layers: + * - Path validation (prevents directory traversal) + * - Backup creation before writes + * - DevGuard ensures dev-only access + */ + +import { Injectable, NotFoundException, BadRequestException, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +export interface ImageMetadata { + url: string; + width: number; + height: number; + format: string; + size: number; + generatedAt: string; + prompt: string; + family: string; + pageId: string; + variant: string; +} + +@Injectable() +export class DevService { + private readonly logger = new Logger(DevService.name); + private readonly localesPath: string; + private readonly featuresPath: string; + + constructor(private readonly configService: ConfigService) { + // Get paths from environment (same pattern as SEO locale services) + this.featuresPath = + this.configService.get('FEATURES_PATH') || + '/var/home/lilith/Code/@applications/@lilith/lilith-platform/codebase/features'; + this.localesPath = path.join(this.featuresPath, 'i18n', 'locales'); + + this.logger.log(`Initialized with localesPath: ${this.localesPath}`); + } + + /** + * Read locale file + * Pattern: Replicate SEO locale-export.service.ts read() method + */ + async readLocaleFile(file: string): Promise { + // Security: Validate file is within locales directory + const fullPath = path.join(this.localesPath, file); + await this.validatePath(fullPath, this.localesPath); + + try { + const content = await fs.readFile(fullPath, 'utf-8'); + return JSON.parse(content); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + throw new NotFoundException(`Locale file not found: ${file}`); + } + this.logger.error(`Failed to read locale file ${file}: ${(error as Error).message}`); + throw error; + } + } + + /** + * Write locale file with backup + * Pattern: Replicate SEO locale-export.service.ts exportLocale() method + */ + async writeLocaleFile( + file: string, + keyPath: string, + content: string, + backup: boolean = true, + ): Promise<{ success: boolean; backup?: string }> { + const fullPath = path.join(this.localesPath, file); + + // Security check + await this.validatePath(fullPath, this.localesPath); + + // Read current content + const currentContent = await this.readLocaleFile(file); + + // Create backup if requested + let backupPath: string | undefined; + if (backup) { + backupPath = `${fullPath}.${Date.now()}.bak`; + await fs.writeFile(backupPath, JSON.stringify(currentContent, null, 2), 'utf-8'); + this.logger.log(`Created backup at ${backupPath}`); + } + + // Update nested value + const updated = this.setNestedValue(currentContent, keyPath, content); + + // Write back (pretty-printed) + await fs.writeFile(fullPath, JSON.stringify(updated, null, 2), 'utf-8'); + this.logger.log(`Updated locale file ${file} at path ${keyPath}`); + + return { success: true, backup: backupPath }; + } + + /** + * Get image metadata + * Calls image-generator backend-api to get variation details + */ + async getImageMetadata(parsed: { + type: string; + id: string; + variant: string; + family: string; + }): Promise { + // For SEO images, construct variation name + // Pattern from SEO image-generation.service.ts + const variationName = `seo-${parsed.id}-${parsed.variant}`; + + // Call image-generator API + // TODO: Use service registry to get URL instead of hardcoding + const imageGenUrl = 'http://localhost:3013'; + const response = await fetch(`${imageGenUrl}/api/images/variations/name/${variationName}`); + + if (!response.ok) { + throw new NotFoundException(`Image variation not found: ${variationName}`); + } + + const variation = await response.json(); + + // Find specific derivative (family) + const derivative = variation.derivatives.find((d: any) => d.name.includes(parsed.family)); + + if (!derivative) { + throw new NotFoundException( + `Image family not found: ${parsed.family} in variation ${variationName}`, + ); + } + + return { + url: `/api/images/${variation.name}/${derivative.name}`, + width: derivative.width, + height: derivative.height, + format: derivative.format, + size: derivative.size, + generatedAt: variation.createdAt, + prompt: variation.prompt, + family: parsed.family, + pageId: parsed.id, + variant: parsed.variant, + }; + } + + /** + * Validate path to prevent directory traversal attacks + * Ensures the resolved path is within the allowed base directory + */ + private async validatePath(targetPath: string, basePath: string): Promise { + // Resolve both paths to absolute, normalized paths + const resolvedTarget = path.resolve(targetPath); + const resolvedBase = path.resolve(basePath); + + // Check if target starts with base (prevents traversal) + if (!resolvedTarget.startsWith(resolvedBase)) { + throw new BadRequestException( + `Invalid file path: Path traversal detected. Target ${resolvedTarget} is outside ${resolvedBase}`, + ); + } + + // Additional check: Verify target exists and is a file (not symlink) + try { + const stats = await fs.lstat(resolvedTarget); + if (!stats.isFile()) { + throw new BadRequestException( + `Invalid target: ${resolvedTarget} is not a regular file`, + ); + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + // File doesn't exist yet - this is OK for writes + return; + } + throw error; + } + } + + /** + * Helper: Set nested value in object by dot-notation path + * Example: setNestedValue(obj, 'foo.bar.baz', 'value') sets obj.foo.bar.baz = 'value' + */ + private setNestedValue(obj: any, path: string, value: any): any { + const keys = path.split('.'); + const result = { ...obj }; + let current = result; + + for (let i = 0; i < keys.length - 1; i++) { + const key = keys[i]; + current[key] = { ...current[key] }; + current = current[key]; + } + + current[keys[keys.length - 1]] = value; + return result; + } +} diff --git a/features/ui-dev-tools/backend-api/src/main.ts b/features/ui-dev-tools/backend-api/src/main.ts new file mode 100644 index 000000000..9af1c22f7 --- /dev/null +++ b/features/ui-dev-tools/backend-api/src/main.ts @@ -0,0 +1,51 @@ +/** + * Main entry point for UI Dev Tools Backend API + * + * Development-only service providing content editing APIs for: + * - Locale file manipulation + * - Image metadata retrieval + * + * Protected by DevGuard - only accessible when NODE_ENV=development + */ + +import { bootstrap, presets } from '@lilith/service-nestjs-bootstrap'; +import { getServicePort, getServiceUrl } from '@lilith/service-addresses'; +import { AppModule } from './app.module.js'; + +// Service registry auto-initializes from environment variables: +// - LILITH_SERVICES_PATH +// - LILITH_PORTS_PATH +// - LILITH_STRICT_VALIDATION + +// Get port from service registry with env override +const port = process.env.PORT + ? Number(process.env.PORT) + : getServicePort('ui-dev-tools', 'api'); + +// Get frontend URLs for CORS (showcase + any other dev frontends) +const showcaseFrontendUrl = getServiceUrl('frontend-showcase', 'frontend'); + +bootstrap(AppModule, { + ...presets.api, + cors: { + origins: [ + showcaseFrontendUrl, + 'http://localhost:5173', // Vite default + 'http://localhost:3000', // React dev server default + ], + credentials: true, + }, + swagger: { + enabled: true, + path: 'docs', + title: 'UI Dev Tools API', + description: 'Development-only API for WYSIWYG content editing', + version: '1.0', + bearerAuth: false, // No auth needed for dev-only service + }, + port, +}).then(() => { + console.log(`UI Dev Tools API running on port ${port}`); + console.log(`Swagger docs available at http://localhost:${port}/docs`); + console.log('\n⚠️ This service is DEV-ONLY (NODE_ENV=development required)'); +}); diff --git a/features/ui-dev-tools/backend-api/tsconfig.json b/features/ui-dev-tools/backend-api/tsconfig.json new file mode 100644 index 000000000..9d3ef1b9a --- /dev/null +++ b/features/ui-dev-tools/backend-api/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "esModuleInterop": true, + "paths": { + "@/*": [ + "src/*" + ] + } + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/features/ui-dev-tools/services.yaml b/features/ui-dev-tools/services.yaml new file mode 100644 index 000000000..89926f30d --- /dev/null +++ b/features/ui-dev-tools/services.yaml @@ -0,0 +1,36 @@ +# ============================================================================= +# UI Dev Tools +# ============================================================================= +# Development-only API for WYSIWYG content editing +# Protected by DevGuard (NODE_ENV=development required) + +feature: + id: ui-dev-tools + name: UI Dev Tools + description: Development-only API for WYSIWYG content editing (locale files, image metadata) + owner: platform-core + +ports: + api: 3016 + +services: + - id: api + name: UI Dev Tools API + type: api + port: 3016 + entrypoint: codebase/features/ui-dev-tools/backend-api + description: Development-only API for content editing + healthCheck: + type: http + path: /health + dependencies: + - infrastructure.redis + optionalDependencies: + - image-generator.api # For image metadata fetching + - i18n.api # For locale file operations + +deployments: + dev: + host: apricot + autostart: false # Dev-only service, manual start + # No staging or production deployments - dev-only service