feat: homelab deployment - NPM + IONOS DNS + monitoring + NAS backup

- Restructured docker-compose for Nginx Proxy Manager (no custom nginx)
- Added domain wordly.art configuration
- Added Prometheus + Grafana monitoring stack with pre-configured dashboards
- Added PostgreSQL backup script to NAS (daily/weekly/monthly rotation)
- Added alert rules for backend, system, and Docker metrics
- Updated deployment guide for NPM + IONOS DNS homelab setup
- Added marketing plan document
- PDF translator and watermark support
- Enhanced middleware, routes, and translator modules

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 11:43:28 +02:00
parent 16ac7ca2b9
commit ce8e150a61
110 changed files with 6935 additions and 4301 deletions

View File

@@ -2,6 +2,7 @@
import { useState } from 'react';
import Link from 'next/link';
import { useI18n } from '@/lib/i18n';
import {
Eye,
EyeOff,
@@ -34,7 +35,7 @@ function validatePassword(password: string) {
}
function getPasswordStrength(password: string) {
if (password.length === 0) return { score: 0, label: '', color: '' };
if (password.length === 0) return { score: 0, labelKey: '', color: '' };
let score = 0;
if (password.length >= 8) score++;
if (password.length >= 12) score++;
@@ -43,9 +44,9 @@ function getPasswordStrength(password: string) {
if (/[0-9]/.test(password)) score++;
if (/[^A-Za-z0-9]/.test(password)) score++;
if (score <= 2) return { score, label: 'Faible', color: 'bg-destructive' };
if (score <= 4) return { score, label: 'Moyen', color: 'bg-yellow-500' };
return { score, label: 'Fort', color: 'bg-green-500' };
if (score <= 2) return { score, labelKey: 'register.password.strength.weak', color: 'bg-destructive' };
if (score <= 4) return { score, labelKey: 'register.password.strength.medium', color: 'bg-yellow-500' };
return { score, labelKey: 'register.password.strength.strong', color: 'bg-green-500' };
}
function PasswordToggleIcon({ visible, onToggle, label }: { visible: boolean; onToggle: () => void; label: string }) {
@@ -72,21 +73,22 @@ export function RegisterForm() {
const [touched, setTouched] = useState({ name: false, email: false, password: false, confirmPassword: false });
const registerMutation = useRegister();
const { t } = useI18n();
const nameError = touched.name && name.length > 0 && name.length < 2
? 'Le nom doit contenir au moins 2 caractères'
? t('register.name.error')
: undefined;
const emailError = touched.email && email.length > 0 && !validateEmail(email)
? 'Adresse email invalide'
? t('register.email.error')
: undefined;
const passwordError = touched.password && password.length > 0 && !validatePassword(password)
? 'Le mot de passe doit contenir au moins 8 caractères, une majuscule, une minuscule et un chiffre'
? t('register.password.error')
: undefined;
const confirmError = touched.confirmPassword && confirmPassword.length > 0 && password !== confirmPassword
? 'Les mots de passe ne correspondent pas'
? t('register.confirmPassword.error')
: undefined;
const passwordStrength = getPasswordStrength(password);
@@ -112,7 +114,7 @@ export function RegisterForm() {
<PasswordToggleIcon
visible={showConfirm}
onToggle={() => setShowConfirm(!showConfirm)}
label={showConfirm ? 'Masquer' : 'Afficher'}
label={showConfirm ? t('register.confirmPassword.hide') : t('register.confirmPassword.show')}
/>
);
};
@@ -129,8 +131,8 @@ export function RegisterForm() {
</span>
</Link>
<CardTitle className="text-2xl font-bold">Créer un compte</CardTitle>
<CardDescription>Commencez à traduire gratuitement</CardDescription>
<CardTitle className="text-2xl font-bold">{t('register.title')}</CardTitle>
<CardDescription>{t('register.subtitle')}</CardDescription>
</CardHeader>
<CardContent className="space-y-5">
@@ -139,7 +141,7 @@ export function RegisterForm() {
<div className="flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-destructive flex-shrink-0 mt-0.5" />
<p className="text-sm text-destructive">
{registerMutation.error?.message || "L'inscription a échoué"}
{registerMutation.error?.message || t('register.error.failed')}
</p>
</div>
</div>
@@ -147,11 +149,11 @@ export function RegisterForm() {
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Nom</Label>
<Label htmlFor="name">{t('register.name.label')}</Label>
<Input
id="name"
type="text"
placeholder="Votre nom"
placeholder={t('register.name.placeholder')}
value={name}
onChange={(e) => setName(e.target.value)}
onBlur={() => setTouched((t) => ({ ...t, name: true }))}
@@ -170,11 +172,11 @@ export function RegisterForm() {
</div>
<div className="space-y-2">
<Label htmlFor="email">Adresse email</Label>
<Label htmlFor="email">{t('register.email.label')}</Label>
<Input
id="email"
type="email"
placeholder="vous@exemple.com"
placeholder={t('register.email.placeholder')}
value={email}
onChange={(e) => setEmail(e.target.value)}
onBlur={() => setTouched((t) => ({ ...t, email: true }))}
@@ -193,7 +195,7 @@ export function RegisterForm() {
</div>
<div className="space-y-2">
<Label htmlFor="password">Mot de passe</Label>
<Label htmlFor="password">{t('register.password.label')}</Label>
<Input
id="password"
type={showPassword ? 'text' : 'password'}
@@ -206,7 +208,7 @@ export function RegisterForm() {
<PasswordToggleIcon
visible={showPassword}
onToggle={() => setShowPassword(!showPassword)}
label={showPassword ? 'Masquer le mot de passe' : 'Afficher le mot de passe'}
label={showPassword ? t('register.password.hide') : t('register.password.show')}
/>
}
error={passwordError}
@@ -230,14 +232,14 @@ export function RegisterForm() {
))}
</div>
<p className={cn('text-xs', passwordStrength.score <= 2 ? 'text-destructive' : passwordStrength.score <= 4 ? 'text-muted-foreground' : 'text-green-500')}>
Force : {passwordStrength.label}
{t('register.password.strengthLabel')} {passwordStrength.labelKey ? t(passwordStrength.labelKey) : ''}
</p>
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirmer le mot de passe</Label>
<Label htmlFor="confirmPassword">{t('register.confirmPassword.label')}</Label>
<Input
id="confirmPassword"
type={showConfirm ? 'text' : 'password'}
@@ -263,30 +265,30 @@ export function RegisterForm() {
>
{registerMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Création du compte...
<Loader2 className="me-2 h-4 w-4 animate-spin" />
{t('register.submit.creating')}
</>
) : (
<>
<UserPlus className="mr-2 h-4 w-4" />
Créer mon compte
<ArrowRight className="ml-2 h-4 w-4" />
<UserPlus className="me-2 h-4 w-4" />
{t('register.submit.create')}
<ArrowRight className="ms-2 h-4 w-4" />
</>
)}
</Button>
</form>
<p className="text-center text-sm text-muted-foreground">
Vous avez déjà un compte ?{' '}
{t('register.hasAccount')}{' '}
<Link href="/auth/login" className="text-primary hover:underline font-medium">
Se connecter
{t('register.login')}
</Link>
</p>
<p className="text-center text-xs text-muted-foreground">
En créant un compte, vous acceptez notre{' '}
{t('register.terms.prefix')}{' '}
<span className="text-muted-foreground">
utilisation du service
{t('register.terms.link')}
</span>
.
</p>