Ajout page simulateur sans accès

This commit is contained in:
odentas 2025-10-15 13:17:03 +02:00
parent 1e9733fd90
commit 0a44dce37a
9 changed files with 2890 additions and 17 deletions

67
FIX_2FA_INPUT_SIZE.md Normal file
View file

@ -0,0 +1,67 @@
# Correction : Encadrés 2FA trop grands sur la page de connexion
## 🐛 Problème
Sur la page de connexion (`/signin`), lors de la saisie du code 2FA (authentification à deux facteurs), les encadrés pour les 6 chiffres étaient beaucoup trop grands par rapport aux encadrés utilisés pour le code OTP par email.
## 🔍 Cause
Les inputs du 2FA utilisaient la classe CSS `flex-1` qui les rendait extensibles pour remplir tout l'espace disponible, combiné avec `justify-between` au lieu de `justify-center`.
**Code problématique** :
```tsx
<div className="flex justify-between gap-1 sm:gap-2">
<input
className="flex-1 h-12 sm:h-14 text-center text-xl sm:text-2xl font-mono border-2 border-[#6366f1]/40 rounded-xl bg-white/30 text-[#171424] focus:outline-none focus:ring-2 focus:ring-[#6366f1] shadow-md transition"
/>
</div>
```
## ✅ Solution
Alignement du style des inputs 2FA sur celui des inputs OTP en utilisant :
- Des largeurs fixes (`w-12 sm:w-14` + style inline `width: 3rem`)
- `justify-center` au lieu de `justify-between`
- Les mêmes classes de style pour une apparence cohérente
### Changements apportés
**Fichier** : `/app/signin/page.tsx`
#### Avant :
```tsx
<div className="flex justify-between gap-1 sm:gap-2">
<input
className="flex-1 h-12 sm:h-14 text-center text-xl sm:text-2xl font-mono border-2 border-[#6366f1]/40 rounded-xl bg-white/30 text-[#171424] focus:outline-none focus:ring-2 focus:ring-[#6366f1] shadow-md transition"
/>
</div>
```
#### Après :
```tsx
<div className="flex justify-center gap-1 sm:gap-2">
<input
className="w-12 sm:w-14 h-14 sm:h-16 text-center text-2xl sm:text-3xl rounded-xl sm:rounded-2xl bg-white/70 border-2 border-[#6366f1]/40 text-[#171424] placeholder-[#171424]/40 focus:outline-none focus:ring-2 focus:ring-[#6366f1]/40 shadow-lg transition flex-shrink-0"
style={{ fontWeight: 700, letterSpacing: "0.1em", width: "3rem", minWidth: "3rem", maxWidth: "3rem" }}
/>
</div>
```
## 🎯 Améliorations
- ✅ Encadrés 2FA de taille fixe (3rem / ~48px)
- ✅ Cohérence visuelle entre OTP et 2FA
- ✅ Centrés sur la page au lieu d'être étirés
- ✅ Même espacement et style que les inputs OTP
- ✅ Meilleure lisibilité et UX
## 📸 Différences visuelles
### Avant
- Inputs 2FA : Largeur flexible (trop large)
- Distribution : `justify-between` (étalés sur toute la largeur)
### Après
- Inputs 2FA : Largeur fixe de 3rem chacun
- Distribution : `justify-center` (centrés et groupés)
- Style identique aux inputs OTP pour cohérence

View file

@ -0,0 +1,68 @@
# Correction : Déconnexion automatique lors de la création d'un mot de passe
## 🐛 Problème
Lorsqu'un utilisateur créait ou mettait à jour son mot de passe via la page **Compte > Sécurité**, il était automatiquement déconnecté de son compte.
## 🔍 Cause
L'API `/api/auth/password-update` utilisait la méthode `admin.auth.admin.updateUserById()` de Supabase (Admin API) pour modifier le mot de passe.
**Comportement de Supabase** : Par défaut, pour des raisons de sécurité, l'Admin API **invalide automatiquement toutes les sessions actives** d'un utilisateur lorsqu'on modifie son mot de passe. Ce comportement assume qu'un changement de mot de passe doit forcer une reconnexion.
## ✅ Solution
Remplacement de l'Admin API par la méthode `supabase.auth.updateUser()` qui permet de mettre à jour le mot de passe **sans invalider la session active**.
### Changements apportés
**Fichier** : `/app/api/auth/password-update/route.ts`
#### Avant :
```typescript
// Utilisation de l'Admin API (invalide la session)
const admin = createClient(url, serviceKey);
const { error: updErr } = await admin.auth.admin.updateUserById(userId, {
password: newPassword,
user_metadata: {
...session.user.user_metadata,
hasPassword: true
}
});
```
#### Après :
```typescript
// Utilisation de updateUser() (préserve la session)
const { error: updErr } = await supabase.auth.updateUser({
password: newPassword,
data: {
...session.user.user_metadata,
hasPassword: true
}
});
```
## 🎯 Résultat
- ✅ L'utilisateur peut créer/modifier son mot de passe sans être déconnecté
- ✅ La session reste active après la mise à jour
- ✅ L'email de confirmation est toujours envoyé
- ✅ Toutes les validations de sécurité du mot de passe sont conservées
## 📝 Notes techniques
- **`updateUser()`** : Met à jour les informations de l'utilisateur authentifié via sa session active
- **`admin.updateUserById()`** : Met à jour un utilisateur via l'Admin API (invalide toutes les sessions pour sécurité)
La méthode `updateUser()` est appropriée ici car :
1. L'utilisateur est authentifié et modifie son propre mot de passe
2. Il n'y a pas de raison de le déconnecter immédiatement
3. C'est une action volontaire de l'utilisateur (pas une récupération de mot de passe compromise)
## 🔒 Sécurité
Le changement de mot de passe reste sécurisé :
- Validation stricte du mot de passe (12+ caractères, majuscules, minuscules, chiffres, caractères spéciaux)
- Email de confirmation envoyé
- L'utilisateur doit être authentifié pour accéder à cette API

View file

@ -0,0 +1,118 @@
"use client";
import React from 'react';
import { usePageTitle } from '@/hooks/usePageTitle';
import { Calculator } from 'lucide-react';
export default function SimulateurPage() {
usePageTitle("Simulateur de paie");
return (
<div className="max-w-[1600px] mx-auto">
<style jsx global>{`
.simulateur-iframe {
width: 100%;
min-height: 1200px;
border: none;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,.06);
background: white;
}
`}</style>
{/* En-tête */}
<div className="mb-6">
<div className="flex items-center gap-3 mb-2">
<Calculator className="w-8 h-8 text-indigo-600" />
<h1 className="text-3xl font-bold text-slate-900">Simulateur de paie intermittent</h1>
</div>
<p className="text-slate-600">
Calculez le coût de recrutement d&apos;un intermittent du spectacle (CDDU)
</p>
</div>
{/* Layout 2 colonnes : simulateur à gauche, cards info à droite */}
<div className="grid grid-cols-1 lg:grid-cols-[1fr_380px] gap-6">
{/* Colonne principale : Simulateur en iframe */}
<div>
<iframe
src="/simulateur-embed.html"
className="simulateur-iframe"
title="Simulateur de paie intermittent"
loading="lazy"
/>
</div>
{/* Colonne droite : Cards d'information modernes */}
<aside className="space-y-4 lg:sticky lg:top-24 lg:self-start" aria-label="Aide et explications">
{/* Card : Mode d'emploi */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 hover:shadow-md transition-shadow">
<div className="flex items-start gap-3 mb-4">
<div className="flex-shrink-0 w-10 h-10 bg-indigo-100 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-slate-900 mb-2">Mode d&apos;emploi</h3>
<p className="text-sm text-slate-600 mb-3">
Calculez le coût de recrutement d&apos;un intermittent du spectacle en CDDU.
</p>
</div>
</div>
<ul className="space-y-2 text-sm text-slate-700">
<li className="flex items-start gap-2">
<span className="flex-shrink-0 w-5 h-5 bg-indigo-50 text-indigo-600 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5">1</span>
<span>Choisissez la Convention Collective et le statut</span>
</li>
<li className="flex items-start gap-2">
<span className="flex-shrink-0 w-5 h-5 bg-indigo-50 text-indigo-600 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5">2</span>
<span>Indiquez les cachets/heures et dates travaillées</span>
</li>
<li className="flex items-start gap-2">
<span className="flex-shrink-0 w-5 h-5 bg-indigo-50 text-indigo-600 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5">3</span>
<span>Saisissez le montant (Brut, Net ou Coût employeur)</span>
</li>
<li className="flex items-start gap-2">
<span className="flex-shrink-0 w-5 h-5 bg-indigo-50 text-indigo-600 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5">4</span>
<span>Consultez les résultats avec le détail des cotisations</span>
</li>
</ul>
<div className="mt-4 pt-4 border-t border-slate-100">
<p className="text-xs text-slate-500">
<span className="font-medium text-slate-700">Taux 2025</span> Contrats multi-mois : nous contacter
</p>
</div>
</div>
{/* Card : Disclaimer */}
<div className="bg-amber-50 border border-amber-200 rounded-xl shadow-sm p-6">
<div className="flex items-start gap-3">
<div className="flex-shrink-0">
<svg className="w-5 h-5 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div className="flex-1">
<h3 className="text-sm font-semibold text-amber-900 mb-2">Limitations & mentions</h3>
<p className="text-xs text-amber-800 leading-relaxed">
Le simulateur ne prévoit pas les cas particuliers : mineurs de moins de 16 ans,
cumul annuel, taxe sur les salaires, taxe d&apos;apprentissage, non-résidents fiscaux,
contrats multi-mois.
</p>
<p className="text-xs text-amber-700 mt-2 italic">
Résultats donnés à titre indicatif, sans valeur contractuelle.
</p>
</div>
</div>
</div>
</aside>
</div>
</div>
);
}

View file

@ -1,7 +1,6 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { createClient } from "@supabase/supabase-js";
import { sendPasswordChangedEmail } from "@/lib/emailMigrationHelpers"; import { sendPasswordChangedEmail } from "@/lib/emailMigrationHelpers";
export async function POST(req: Request) { export async function POST(req: Request) {
@ -45,19 +44,11 @@ export async function POST(req: Request) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
} }
const url = process.env.SUPABASE_URL!; // Mettre à jour le mot de passe via updateUser() qui préserve la session active
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; // Note: Cette méthode n'invalide PAS la session actuelle contrairement à l'Admin API
if (!url || !serviceKey) { const { error: updErr } = await supabase.auth.updateUser({
return NextResponse.json({ error: "Configuration Supabase manquante" }, { status: 500 });
}
const admin = createClient(url, serviceKey);
const userId = session.user.id;
// Mettre à jour le mot de passe via l'Admin API
const { error: updErr } = await admin.auth.admin.updateUserById(userId, {
password: newPassword, password: newPassword,
user_metadata: { data: {
...session.user.user_metadata, ...session.user.user_metadata,
hasPassword: true hasPassword: true
} }

View file

@ -388,7 +388,7 @@ export default function Sidebar({ clientInfo, isStaff = false, mobile = false, o
<DisabledMenuItem <DisabledMenuItem
icon={Calculator} icon={Calculator}
label="Simulateur de paie" label="Simulateur de paie"
tooltipMessage="Le simulateur et les minima seront de retour dans quelques jours." tooltipMessage="Le simulateur de paie est temporairement désactivé."
/> />
</div> </div>
{/* Menu Staff */} {/* Menu Staff */}

View file

@ -410,8 +410,8 @@ export async function sendPasswordChangedEmail(
userEmail: toEmail, userEmail: toEmail,
status: 'Modifié', status: 'Modifié',
eventDate: data.eventDate || new Date().toLocaleString('fr-FR'), eventDate: data.eventDate || new Date().toLocaleString('fr-FR'),
platform: data.platform || 'Odentas Paie', platform: data.platform || 'Espace Paie Odentas',
ctaUrl: `${process.env.NEXT_PUBLIC_BASE_URL || 'https://paie.odentas.fr'}/compte/securite`, ctaUrl: 'https://paie.odentas.fr',
}; };
await sendUniversalEmailV2({ await sendUniversalEmailV2({

View file

@ -178,7 +178,7 @@ const EMAIL_TEMPLATES_V2: Record<EmailTypeV2, EmailTemplateV2> = {
greeting: '{{#if firstName}}Bonjour {{firstName}},{{/if}}', greeting: '{{#if firstName}}Bonjour {{firstName}},{{/if}}',
mainMessage: 'Votre mot de passe a été modifié avec succès.', mainMessage: 'Votre mot de passe a été modifié avec succès.',
closingMessage: 'Si vous nêtes pas à lorigine de cette modification, <a href="{{supportUrl}}" style="color:#0B5FFF; text-decoration:none;">contactez le support</a> immédiatement.', closingMessage: 'Si vous nêtes pas à lorigine de cette modification, <a href="{{supportUrl}}" style="color:#0B5FFF; text-decoration:none;">contactez le support</a> immédiatement.',
ctaText: 'Gérer ma sécurité', ctaText: 'Accès à l\'Espace Paie',
footerText: 'Vous recevez cet e-mail pour confirmer une modification de mot de passe.', footerText: 'Vous recevez cet e-mail pour confirmer une modification de mot de passe.',
preheaderText: 'Mot de passe modifié · Vérifiez la sécurité de votre compte', preheaderText: 'Mot de passe modifié · Vérifiez la sécurité de votre compte',
colors: { colors: {

1374
public/simulateur-embed.html Normal file

File diff suppressed because it is too large Load diff

1255
simulateur.html Normal file

File diff suppressed because it is too large Load diff