fix(docker-watcher): address final review findings

Security:
- Move config export behind auth middleware
- Validate OIDC callback token before storing in localStorage
- Use constant-time comparison for webhook secret
- Encrypt OIDC client secret at rest (like registry tokens)

Performance:
- Make TriggerDeploy async from HTTP handlers (return deploy ID
  immediately, run pipeline in background goroutine)

Robustness:
- Wrap api.ts res.json() in try/catch for non-JSON responses

i18n:
- Replace ~20 hardcoded English validation messages with $t() calls
- Localize ConfirmDialog cancel button, InstanceCard confirm titles,
  ProjectCard instance/instances pluralization
- Add validation keys to both en.json and ru.json
This commit is contained in:
2026-03-28 00:14:53 +03:00
parent a3aa5912d9
commit 1f81ca9eb0
17 changed files with 178 additions and 40 deletions
+5 -5
View File
@@ -22,27 +22,27 @@
let errors = $state<Record<string, string>>({});
function validateDomain(value: string): string {
if (!value.trim()) return 'Domain is required';
if (!/^[a-zA-Z0-9][a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/.test(value.trim())) return 'Invalid domain format';
if (!value.trim()) return $t('validation.required', { field: 'Domain' });
if (!/^[a-zA-Z0-9][a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/.test(value.trim())) return $t('validation.invalidDomain');
return '';
}
function validateIp(value: string): string {
if (!value.trim()) return '';
if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(value.trim())) return 'Invalid IP address format';
if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(value.trim())) return $t('validation.invalidIp');
return '';
}
function validatePollingInterval(value: string): string {
if (!value.trim()) return '';
const num = parseInt(value, 10);
if (isNaN(num) || num < 10 || num > 86400) return 'Polling interval must be between 10 and 86400 seconds';
if (isNaN(num) || num < 10 || num > 86400) return $t('validation.invalidPollingInterval');
return '';
}
function validateUrl(value: string): string {
if (!value.trim()) return '';
try { new URL(value.trim()); return ''; } catch { return 'Invalid URL format'; }
try { new URL(value.trim()); return ''; } catch { return $t('validation.invalidUrl'); }
}
function validateAll(): boolean {
@@ -17,9 +17,9 @@
function validateNpmForm(): boolean {
const newErrors: Record<string, string> = {};
if (!npmUrl.trim()) { newErrors.npmUrl = 'NPM URL is required'; } else { try { new URL(npmUrl.trim()); } catch { newErrors.npmUrl = 'Invalid URL format'; } }
if (!npmEmail.trim()) { newErrors.npmEmail = 'Email is required'; } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(npmEmail.trim())) { newErrors.npmEmail = 'Invalid email format'; }
if (editingNpm && !npmPassword.trim()) { newErrors.npmPassword = 'Password is required when updating credentials'; }
if (!npmUrl.trim()) { newErrors.npmUrl = $t('validation.required', { field: 'NPM URL' }); } else { try { new URL(npmUrl.trim()); } catch { newErrors.npmUrl = $t('validation.invalidUrl'); } }
if (!npmEmail.trim()) { newErrors.npmEmail = $t('validation.required', { field: 'Email' }); } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(npmEmail.trim())) { newErrors.npmEmail = $t('validation.invalidEmail'); }
if (editingNpm && !npmPassword.trim()) { newErrors.npmPassword = $t('validation.requiredWhenUpdating', { field: 'Password' }); }
errors = newErrors;
return Object.keys(newErrors).length === 0;
}
@@ -24,9 +24,9 @@
function validateForm(): boolean {
const newErrors: Record<string, string> = {};
if (!formName.trim()) newErrors.name = 'Name is required';
if (!formUrl.trim()) { newErrors.url = 'URL is required'; } else { try { new URL(formUrl.trim()); } catch { newErrors.url = 'Invalid URL format'; } }
if (!formToken.trim() && !editingId) newErrors.token = 'Token is required for new registries';
if (!formName.trim()) newErrors.name = $t('validation.required', { field: 'Name' });
if (!formUrl.trim()) { newErrors.url = $t('validation.required', { field: 'URL' }); } else { try { new URL(formUrl.trim()); } catch { newErrors.url = $t('validation.invalidUrl'); } }
if (!formToken.trim() && !editingId) newErrors.token = $t('validation.requiredForNew', { field: 'Token' });
errors = newErrors;
return Object.keys(newErrors).length === 0;
}