diff --git a/frontend/nginx/nginx.conf b/frontend/nginx/nginx.conf index a6bf1af..ffe449d 100644 --- a/frontend/nginx/nginx.conf +++ b/frontend/nginx/nginx.conf @@ -14,11 +14,6 @@ http { sendfile on; keepalive_timeout 65; - # Content Security Policy - organized for better readability - map $request_uri $csp_policy { - default "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src * data: blob:; connect-src 'self' http://127.0.0.1:8000 http://localhost:8000 ws: wss: https://api.paylibo.com; font-src 'self' data: https://fonts.gstatic.com"; - } - server { listen 80; server_name _; @@ -32,7 +27,7 @@ http { location / { try_files $uri /index.html; # Ensure CSP is present on SPA document responses too - add_header Content-Security-Policy $csp_policy always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://api.paylibo.com; connect-src 'self' http://127.0.0.1:8000 http://localhost:8000 ws: wss: https://api.paylibo.com; font-src 'self' data:" always; } # ------------------------- @@ -64,7 +59,7 @@ http { client_max_body_size 50m; # Ensure CSP is also present on proxied responses - add_header Content-Security-Policy $csp_policy always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://api.paylibo.com; connect-src 'self' http://127.0.0.1:8000 http://localhost:8000 ws: wss: https://api.paylibo.com; font-src 'self' data:" always; } # ------------------------- @@ -74,10 +69,7 @@ http { add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; - # CSP Policy - Centrally defined above for better maintainability - # To add new domains, update the $csp_policy map above - # Development: More permissive for external resources - # Production: Should be more restrictive and use nonces/hashes where possible - add_header Content-Security-Policy $csp_policy always; + # Minimal, valid CSP for development (apply on all responses) + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://api.paylibo.com; connect-src 'self' http://127.0.0.1:8000 http://localhost:8000 ws: wss: https://api.paylibo.com; font-src 'self' data:" always; } } diff --git a/frontend/package.json b/frontend/package.json index 3662bf5..1b64e53 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,33 +8,48 @@ "build": "tsc -b && tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", - "api:gen": "orval --config src/orval.config.ts" + "api:gen": "orval --config src/orval.config.ts && node scripts/generate-choice-labels.cjs" }, "dependencies": { + "@fortawesome/free-solid-svg-icons": "^7.1.0", + "@fortawesome/react-fontawesome": "^3.1.1", + "@headlessui/react": "^2.2.9", + "@heroicons/react": "^2.2.0", + "@mantine/core": "^8.3.11", + "@mantine/dates": "^8.3.11", + "@mantine/hooks": "^8.3.11", + "@radix-ui/react-switch": "^1.2.6", + "@tabler/icons-react": "^3.36.1", "@tailwindcss/vite": "^4.1.16", "@tanstack/react-query": "^5.90.12", + "@tanstack/react-table": "^8.21.3", "@types/react-router": "^5.1.20", "axios": "^1.13.0", - "dotenv": "^17.2.3", + "dayjs": "^1.11.19", + "framer-motion": "^12.25.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-hook-form": "^7.70.0", "react-icons": "^5.5.0", "react-router-dom": "^7.8.1", + "react-toastify": "^11.0.5", "tailwindcss": "^4.1.16" }, "devDependencies": { "@eslint/js": "^9.33.0", "@tailwindcss/postcss": "^4.1.17", "@types/node": "^24.10.4", - "@types/react": "^19.1.10", + "@types/react": "^19.2.7", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^5.0.0", "autoprefixer": "^10.4.21", + "babel-plugin-react-compiler": "^1.0.0", + "dotenv": "^17.2.3", "eslint": "^9.33.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", - "orval": "^7.13.2", + "orval": "^8.0.2", "prettier": "^3.7.4", "typescript": "~5.8.3", "typescript-eslint": "^8.39.1", diff --git a/frontend/scripts/fetch-openapi.js b/frontend/scripts/fetch-openapi.js deleted file mode 100644 index 533dd59..0000000 --- a/frontend/scripts/fetch-openapi.js +++ /dev/null @@ -1,25 +0,0 @@ -import fs from "fs"; -import path from "path"; -import axios from "axios"; - -// Single config point -const config = { schemaUrl: "/api/schema/", baseUrl: "/api/" }; - -async function main() { - const outDir = path.resolve("./src/openapi"); - const outFile = path.join(outDir, "schema.json"); - const base = process.env.VITE_API_BASE_URL || "http://localhost:8000"; - const url = new URL(config.schemaUrl, base).toString(); - - console.log(`[openapi] Fetching schema from ${url}`); - const res = await axios.get(url, { headers: { Accept: "application/json" } }); - - await fs.promises.mkdir(outDir, { recursive: true }); - await fs.promises.writeFile(outFile, JSON.stringify(res.data, null, 2), "utf8"); - console.log(`[openapi] Wrote ${outFile}`); -} - -main().catch((err) => { - console.error("[openapi] Failed to fetch schema:", err?.message || err); - process.exit(1); -}); diff --git a/frontend/scripts/generate-choice-labels.cjs b/frontend/scripts/generate-choice-labels.cjs new file mode 100644 index 0000000..50b15af --- /dev/null +++ b/frontend/scripts/generate-choice-labels.cjs @@ -0,0 +1,212 @@ +#!/usr/bin/env node + +/** + * Generates TypeScript file with choice labels from Orval-generated enum files + * + * Usage: node scripts/generate-choice-labels.js + * + * This script reads the TypeScript enum files generated by Orval and extracts + * choice labels from JSDoc comments (format: * `value` - Label) and generates + * a TypeScript file with runtime-accessible label mappings. + */ + +const fs = require('fs'); +const path = require('path'); + +// Configuration +const MODELS_DIRS = [ + path.join(__dirname, '../src/api/generated/private/models'), + path.join(__dirname, '../src/api/generated/public/models') +]; +const OUTPUT_PATH = path.join(__dirname, '../src/constants/choiceLabels.ts'); + +// Convert camelCase to UPPER_SNAKE_CASE +function camelToSnakeCase(str) { + return str + .replace(/([A-Z])/g, '_$1') + .replace(/^_/, '') // Remove leading underscore + .toUpperCase(); +} + +// Extract enum labels from TypeScript file comments +function parseEnumFile(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const filename = path.basename(filePath, '.ts'); + + // Extract all JSDoc comment blocks + // Match all: /** ... */ + const commentRegex = /\/\*\*\s*\n([\s\S]*?)\*\//g; + const comments = []; + let match; + + while ((match = commentRegex.exec(content)) !== null) { + comments.push(match[1]); + } + + if (comments.length === 0) { + return null; + } + + // Try to find labels in all comment blocks + // Parse labels from comment lines like: * `value` - Label + // Note: The asterisk might have 0 or more spaces after it + const labelPattern = /\*\s*`([^`]+)`\s*-\s*(.+)/g; + const labels = {}; + + for (const comment of comments) { + let labelMatch; + while ((labelMatch = labelPattern.exec(comment)) !== null) { + const [, value, label] = labelMatch; + labels[value] = label.trim(); + } + } + + // Only return if we found labels + if (Object.keys(labels).length === 0) { + return null; + } + + // Extract enum name from the type definition + // Match: export type EnumName = ... + const typeMatch = content.match(/export\s+type\s+(\w+Enum)\s*=/); + const enumName = typeMatch ? typeMatch[1] : null; + + if (!enumName) { + return null; + } + + return { enumName, labels }; +} + +// Main function +function generateLabels() { + console.log('πŸ” Scanning Orval-generated enum files...'); + + const enums = {}; + let totalEnumFiles = 0; + + // Check each models directory + for (const MODELS_DIR of MODELS_DIRS) { + const dirName = path.basename(path.dirname(MODELS_DIR)); + + if (!fs.existsSync(MODELS_DIR)) { + console.warn(`⚠️ Models directory not found: ${MODELS_DIR}`); + continue; + } + + console.log(`πŸ“ Scanning ${dirName}/models...`); + + // Read all enum files + const files = fs.readdirSync(MODELS_DIR); + const enumFiles = files.filter(f => f.endsWith('Enum.ts')); + + console.log(` Found ${enumFiles.length} enum files`); + totalEnumFiles += enumFiles.length; + + // Parse each enum file + for (const file of enumFiles) { + const filePath = path.join(MODELS_DIR, file); + const result = parseEnumFile(filePath); + + if (result) { + // Check if enum already exists from another directory + if (enums[result.enumName]) { + // Compare labels to see if they're the same + const existing = JSON.stringify(enums[result.enumName]); + const current = JSON.stringify(result.labels); + + if (existing !== current) { + console.warn(` ⚠️ ${result.enumName}: Different labels found in ${dirName}, keeping first version`); + } + } else { + enums[result.enumName] = result.labels; + console.log(` βœ“ ${result.enumName}: ${Object.keys(result.labels).length} labels`); + } + } + } + } + + if (totalEnumFiles === 0) { + console.error('❌ No enum files found in any models directory'); + console.error(' Run "npm run api:gen" first to generate API client'); + process.exit(1); + } + + if (Object.keys(enums).length === 0) { + console.error('❌ No enum labels found in generated files'); + process.exit(1); + } + + // Generate TypeScript file + console.log('πŸ“ Generating TypeScript file...'); + + const tsContent = `/** + * Auto-generated choice labels from Orval-generated enum files + * Generated by: scripts/generate-choice-labels.cjs + * + * DO NOT EDIT MANUALLY - Run \`npm run api:gen\` to regenerate + */ + +${Object.entries(enums).map(([enumName, labels]) => { + const enumBaseName = enumName.replace(/Enum$/, ''); + const constantName = camelToSnakeCase(enumBaseName) + '_LABELS'; + + return `/** + * Labels for ${enumName} + * ${Object.entries(labels).map(([value, label]) => `* ${value}: ${label}`).join('\n * ')} + */ +export const ${constantName} = ${JSON.stringify(labels, null, 2)} as const; +`; +}).join('\n')} +/** + * Type-safe helper to get choice label + */ +export function getChoiceLabel( + labels: Record, + value: T | undefined | null +): string { + if (!value) return ''; + return labels[value] || value; +} + +/** + * Get options array for select dropdowns + */ +export function getChoiceOptions( + labels: Record +): Array<{ value: T; label: string }> { + return Object.entries(labels).map(([value, label]) => ({ + value: value as T, + label: label as string, + })); +} + +// Auto-generate all OPTIONS exports +${Object.entries(enums).map(([enumName]) => { + const enumBaseName = enumName.replace(/Enum$/, ''); + const labelsConstName = camelToSnakeCase(enumBaseName) + '_LABELS'; + const optionsConstName = camelToSnakeCase(enumBaseName) + '_OPTIONS'; + return `export const ${optionsConstName} = getChoiceOptions(${labelsConstName});`; +}).join('\n')} +`; + + // Ensure output directory exists + const outputDir = path.dirname(OUTPUT_PATH); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Write file + fs.writeFileSync(OUTPUT_PATH, tsContent, 'utf8'); + + console.log(`βœ… Generated ${OUTPUT_PATH}`); + console.log(` Found ${Object.keys(enums).length} enum types with labels`); +} + +// Run the script +try { + generateLabels(); +} catch (error) { + console.error('❌ Error generating choice labels:', error.message); + process.exit(1); +} diff --git a/frontend/src/orval.config.ts b/frontend/src/orval.config.ts index 58c26fd..2f32600 100644 --- a/frontend/src/orval.config.ts +++ b/frontend/src/orval.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from "orval"; -const backendUrl = process.env.VITE_BACKEND_URL || "http://localhost:8000"; +const backendUrl = + process.env.VITE_BACKEND_URL || 'http://localhost:8000'; // mΕ―ΕΎe se hodit pokud nechceme pΕ™i buildu generovat klienta (nechat false pro produkci nebo vynechat) const SKIP_ORVAL = process.env.SKIP_ORVAL === "true";