diff --git a/eslint.config.js b/eslint.config.js index be84740d..e33d0c5c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -11,20 +11,20 @@ export default [ '.venv/**/*', 'node_modules/**/*', 'dist/**/*', - '*.md', '*.yml', '*.yaml', - '*.json', '*.toml', '*.sh', 'Dockerfile*', '.git/**/*', - '.github/**/*' - ] + '.github/**/*', + ], }, // Apply frontend config to frontend files only - ...frontendConfig.map(config => ({ + ...frontendConfig.map((config) => ({ ...config, - files: config.files ? config.files.map(pattern => `frontend/${pattern}`) : ['frontend/**/*.{ts,tsx,js,jsx}'] - })) + files: config.files + ? config.files.map((pattern) => `frontend/${pattern}`) + : ['frontend/**/*.{ts,tsx,js,jsx}'], + })), ]; diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 7c6faeb5..b1090e8a 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -2,20 +2,196 @@ import js from '@eslint/js'; import tseslint from 'typescript-eslint'; import reactRefresh from 'eslint-plugin-react-refresh'; import reactHooks from 'eslint-plugin-react-hooks'; +import jsxA11y from 'eslint-plugin-jsx-a11y'; +import importX from 'eslint-plugin-import-x'; +import unusedImports from 'eslint-plugin-unused-imports'; +import promise from 'eslint-plugin-promise'; +import unicorn from 'eslint-plugin-unicorn'; +import sonarjs from 'eslint-plugin-sonarjs'; +import security from 'eslint-plugin-security'; +import noUnsanitized from 'eslint-plugin-no-unsanitized'; +import reactCompiler from 'eslint-plugin-react-compiler'; +import testingLibrary from 'eslint-plugin-testing-library'; +import vitest from 'eslint-plugin-vitest'; +import css from '@eslint/css'; +import json from '@eslint/json'; +import markdown from '@eslint/markdown'; export default tseslint.config( { ignores: ['dist/**', 'node_modules/**', 'coverage/**'] }, - js.configs.recommended, - ...tseslint.configs.recommended, + + // ── Base configs (scoped to JS/TS to avoid breaking non-JS parsers) ── + { ...js.configs.recommended, files: ['**/*.{ts,tsx,js,jsx,mjs,cjs}'] }, + ...tseslint.configs.recommended.map(config => ({ + ...config, + files: config.files ?? ['**/*.{ts,tsx,js,jsx,mjs,cjs}'], + })), + + // ── TypeScript + React (main source files) ──────────────────────────── { files: ['**/*.{ts,tsx}'], - plugins: { 'react-refresh': reactRefresh, 'react-hooks': reactHooks }, + plugins: { + 'react-refresh': reactRefresh, + 'react-hooks': reactHooks, + 'jsx-a11y': jsxA11y, + 'import-x': importX, + 'unused-imports': unusedImports, + promise, + unicorn, + sonarjs, + security, + 'no-unsanitized': noUnsanitized, + 'react-compiler': reactCompiler, + }, + settings: { + 'import-x/resolver': { + typescript: true, + node: true, + }, + }, rules: { + // ── React ── 'react-refresh/only-export-components': 'warn', 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', + 'react-compiler/react-compiler': 'warn', + + // ── TypeScript ── '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': 'warn' - } + '@typescript-eslint/no-unused-vars': 'off', // handled by unused-imports + '@typescript-eslint/consistent-type-imports': [ + 'warn', + { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, + ], + + // ── Unused imports ── + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': [ + 'warn', + { + vars: 'all', + varsIgnorePattern: '^_', + args: 'after-used', + argsIgnorePattern: '^_', + }, + ], + + // ── Import organization ── + 'import-x/order': [ + 'warn', + { + groups: [ + 'builtin', + 'external', + 'internal', + ['parent', 'sibling', 'index'], + 'type', + ], + 'newlines-between': 'always', + alphabetize: { order: 'asc', caseInsensitive: true }, + }, + ], + 'import-x/no-duplicates': ['warn', { 'prefer-inline': true }], + 'import-x/no-cycle': ['warn', { maxDepth: 4 }], + 'import-x/no-self-import': 'error', + + // ── Accessibility ── + ...jsxA11y.flatConfigs.recommended.rules, + 'jsx-a11y/label-has-associated-control': 'warn', + 'jsx-a11y/no-static-element-interactions': 'warn', + 'jsx-a11y/click-events-have-key-events': 'warn', + 'jsx-a11y/no-autofocus': 'warn', + 'jsx-a11y/role-has-required-aria-props': 'warn', + 'jsx-a11y/heading-has-content': 'warn', + + // ── Promises ── + 'promise/always-return': 'warn', + 'promise/no-return-wrap': 'error', + 'promise/catch-or-return': 'warn', + 'promise/no-nesting': 'warn', + + // ── Unicorn (cherry-picked) ── + 'unicorn/prefer-node-protocol': 'error', + 'unicorn/no-array-for-each': 'warn', + 'unicorn/prefer-array-find': 'warn', + 'unicorn/prefer-array-flat-map': 'warn', + 'unicorn/prefer-array-some': 'warn', + 'unicorn/prefer-includes': 'warn', + 'unicorn/prefer-string-starts-ends-with': 'warn', + 'unicorn/no-useless-spread': 'warn', + 'unicorn/no-useless-undefined': 'warn', + 'unicorn/prefer-optional-catch-binding': 'warn', + 'unicorn/prefer-ternary': ['warn', 'only-single-line'], + 'unicorn/no-lonely-if': 'warn', + + // ── Sonar (code smells) ── + 'sonarjs/no-identical-functions': 'warn', + 'sonarjs/no-duplicated-branches': 'warn', + 'sonarjs/no-collapsible-if': 'warn', + 'sonarjs/prefer-immediate-return': 'warn', + + // ── Security ── + 'security/detect-object-injection': 'off', // too noisy for frontend + 'security/detect-non-literal-regexp': 'warn', + 'security/detect-unsafe-regex': 'warn', + 'no-unsanitized/method': 'error', + 'no-unsanitized/property': 'error', + }, + }, + + // ── Test files ──────────────────────────────────────────────────────── + { + files: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', '**/__tests__/**/*.{ts,tsx}'], + plugins: { + 'testing-library': testingLibrary, + vitest, + }, + rules: { + ...testingLibrary.configs['flat/react'].rules, + ...vitest.configs.recommended.rules, + // relax rules that are too noisy for the existing test suite + '@typescript-eslint/no-explicit-any': 'off', + 'sonarjs/no-identical-functions': 'off', + 'testing-library/no-node-access': 'warn', + 'testing-library/prefer-find-by': 'warn', + 'testing-library/no-container': 'warn', + 'testing-library/no-wait-for-multiple-assertions': 'warn', + 'testing-library/no-unnecessary-act': 'warn', + 'testing-library/no-manual-cleanup': 'warn', + 'testing-library/render-result-naming-convention': 'warn', + 'vitest/expect-expect': 'warn', + }, + }, + + // ── CSS files ───────────────────────────────────────────────────────── + { + files: ['**/*.css'], + language: 'css/css', + plugins: { css }, + rules: { + 'css/no-duplicate-imports': 'error', + 'css/no-empty-blocks': 'warn', + }, + }, + + // ── JSON files ──────────────────────────────────────────────────────── + { + files: ['**/*.json'], + ignores: ['package-lock.json', 'tsconfig*.json'], + language: 'json/json', + plugins: { json }, + rules: { + 'json/no-duplicate-keys': 'error', + }, + }, + + // ── Markdown files ──────────────────────────────────────────────────── + { + files: ['**/*.md'], + plugins: { markdown }, + language: 'markdown/gfm', + rules: { + 'markdown/no-html': 'off', + }, } ); diff --git a/frontend/src/components/CredentialManager.tsx b/frontend/src/components/CredentialManager.tsx index 1e2c4c5f..fb865278 100644 --- a/frontend/src/components/CredentialManager.tsx +++ b/frontend/src/components/CredentialManager.tsx @@ -1,6 +1,7 @@ +import { Plus, Edit, Trash2, CheckCircle, XCircle, TestTube } from 'lucide-react' import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { Plus, Edit, Trash2, CheckCircle, XCircle, TestTube } from 'lucide-react' + import { Dialog, DialogContent, @@ -22,9 +23,10 @@ import { type DNSProviderCredential, type CredentialRequest, } from '../hooks/useCredentials' -import type { DNSProvider, DNSProviderTypeInfo } from '../api/dnsProviders' import { toast } from '../utils/toast' +import type { DNSProvider, DNSProviderTypeInfo } from '../api/dnsProviders' + interface CredentialManagerProps { open: boolean onOpenChange: (open: boolean) => void @@ -369,7 +371,6 @@ function CredentialForm({ } } setErrors((prev) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars const { zone_filter: _, ...rest } = prev return rest }) @@ -393,13 +394,12 @@ function CredentialForm({ // Check required credential fields const missingFields: string[] = [] - providerTypeInfo?.fields - .filter((f) => f.required) - .forEach((field) => { + for (const field of (providerTypeInfo?.fields ?? []) + .filter((f) => f.required)) { if (!credentials[field.name]) { missingFields.push(field.label) } - }) + } if (missingFields.length > 0 && !credential) { // Only enforce for new credentials diff --git a/frontend/src/locales/de/translation.json b/frontend/src/locales/de/translation.json index 55259dd7..780acece 100644 --- a/frontend/src/locales/de/translation.json +++ b/frontend/src/locales/de/translation.json @@ -74,23 +74,6 @@ "expandSidebar": "Seitenleiste erweitern", "collapseSidebar": "Seitenleiste einklappen" }, - "dashboard": { - "title": "Dashboard", - "description": "Übersicht Ihres Charon-Reverse-Proxys", - "proxyHosts": "Proxy-Hosts", - "remoteServers": "Remote-Server", - "certificates": "Zertifikate", - "accessLists": "Zugriffslisten", - "systemStatus": "Systemstatus", - "healthy": "Gesund", - "unhealthy": "Ungesund", - "pendingCertificates": "Ausstehende Zertifikate", - "allCertificatesValid": "Alle Zertifikate gültig", - "activeHosts": "{{count}} aktiv", - "activeServers": "{{count}} aktiv", - "activeLists": "{{count}} aktiv", - "validCerts": "{{count}} gültig" - }, "settings": { "title": "Einstellungen", "description": "Konfigurieren Sie Ihre Charon-Instanz", diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index 726dd69f..7e282e39 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -83,23 +83,6 @@ "admin": "Admin", "plugins": "Plugins" }, - "dashboard": { - "title": "Dashboard", - "description": "Overview of your Charon reverse proxy", - "proxyHosts": "Proxy Hosts", - "remoteServers": "Remote Servers", - "certificates": "Certificates", - "accessLists": "Access Lists", - "systemStatus": "System Status", - "healthy": "Healthy", - "unhealthy": "Unhealthy", - "pendingCertificates": "Pending certificates", - "allCertificatesValid": "All certificates valid", - "activeHosts": "{{count}} active", - "activeServers": "{{count}} active", - "activeLists": "{{count}} active", - "validCerts": "{{count}} valid" - }, "settings": { "title": "Settings", "description": "Configure your Charon instance", @@ -323,10 +306,7 @@ "layer3": "Layer 3", "layer4": "Layer 4", "ids": "IDS", - "acl": "ACL", - "waf": "WAF", "rate": "Rate", - "crowdsec": "CrowdSec", "crowdsecDescription": "IP Reputation & Threat Intelligence", "crowdsecProtects": "Protects against: Known attackers, botnets, brute-force", "crowdsecDisabledDescription": "Intrusion Prevention System powered by community threat intelligence", @@ -1294,7 +1274,6 @@ }, "plugins": { "title": "DNS Provider Plugins", - "description": "Manage built-in and external DNS provider plugins for certificate automation", "note": "Note", "noteText": "External plugins extend Charon with custom DNS providers. Only install plugins from trusted sources.", "builtInPlugins": "Built-in Providers", diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index 0271f797..61093f21 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -74,23 +74,6 @@ "expandSidebar": "Expandir barra lateral", "collapseSidebar": "Colapsar barra lateral" }, - "dashboard": { - "title": "Panel de Control", - "description": "Resumen de tu proxy inverso Charon", - "proxyHosts": "Hosts Proxy", - "remoteServers": "Servidores Remotos", - "certificates": "Certificados", - "accessLists": "Listas de Acceso", - "systemStatus": "Estado del Sistema", - "healthy": "Saludable", - "unhealthy": "No Saludable", - "pendingCertificates": "Certificados pendientes", - "allCertificatesValid": "Todos los certificados válidos", - "activeHosts": "{{count}} activo", - "activeServers": "{{count}} activo", - "activeLists": "{{count}} activo", - "validCerts": "{{count}} válido" - }, "settings": { "title": "Configuración", "description": "Configura tu instancia de Charon", diff --git a/frontend/src/locales/fr/translation.json b/frontend/src/locales/fr/translation.json index 73512630..0c1c302a 100644 --- a/frontend/src/locales/fr/translation.json +++ b/frontend/src/locales/fr/translation.json @@ -74,23 +74,6 @@ "expandSidebar": "Développer la barre latérale", "collapseSidebar": "Réduire la barre latérale" }, - "dashboard": { - "title": "Tableau de bord", - "description": "Vue d'ensemble de votre proxy inverse Charon", - "proxyHosts": "Hôtes Proxy", - "remoteServers": "Serveurs Distants", - "certificates": "Certificats", - "accessLists": "Listes d'Accès", - "systemStatus": "État du Système", - "healthy": "En bonne santé", - "unhealthy": "Pas en bonne santé", - "pendingCertificates": "Certificats en attente", - "allCertificatesValid": "Tous les certificats sont valides", - "activeHosts": "{{count}} actif", - "activeServers": "{{count}} actif", - "activeLists": "{{count}} actif", - "validCerts": "{{count}} valide" - }, "settings": { "title": "Paramètres", "description": "Configurez votre instance Charon", diff --git a/frontend/src/locales/zh/translation.json b/frontend/src/locales/zh/translation.json index e2c1bf77..4926cbb6 100644 --- a/frontend/src/locales/zh/translation.json +++ b/frontend/src/locales/zh/translation.json @@ -74,23 +74,6 @@ "expandSidebar": "展开侧边栏", "collapseSidebar": "收起侧边栏" }, - "dashboard": { - "title": "仪表板", - "description": "Charon反向代理概览", - "proxyHosts": "代理主机", - "remoteServers": "远程服务器", - "certificates": "证书", - "accessLists": "访问列表", - "systemStatus": "系统状态", - "healthy": "健康", - "unhealthy": "不健康", - "pendingCertificates": "待处理证书", - "allCertificatesValid": "所有证书有效", - "activeHosts": "{{count}} 个活动", - "activeServers": "{{count}} 个活动", - "activeLists": "{{count}} 个活动", - "validCerts": "{{count}} 个有效" - }, "settings": { "title": "设置", "description": "配置您的Charon实例",