Checkpoint: Migration OAuth Manus vers authentification locale (80% complété) - Pages login/register créées, backend JWT implémenté, guides de déploiement ajoutés. Reste à corriger les doublons dans db.ts et finaliser AdminUsers.
This commit is contained in:
600
GUIDE_DEPLOIEMENT_LINUX.md
Normal file
600
GUIDE_DEPLOIEMENT_LINUX.md
Normal file
@@ -0,0 +1,600 @@
|
||||
# Guide de déploiement - Formation Manager Itinova sur serveur Linux
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Ce guide vous accompagne dans le déploiement de l'application Formation Manager Itinova sur un serveur Linux (Ubuntu/Debian) avec authentification locale email/mot de passe (sans OAuth Manus).
|
||||
|
||||
**Durée estimée** : 1-2 heures
|
||||
**Niveau** : Intermédiaire
|
||||
|
||||
---
|
||||
|
||||
## Prérequis
|
||||
|
||||
### Logiciels requis
|
||||
|
||||
- **Système** : Ubuntu 22.04 LTS ou Debian 11+
|
||||
- **Node.js** : Version 22.x
|
||||
- **pnpm** : Version 10.x
|
||||
- **MySQL** : Version 8.0+ ou MariaDB 10.6+
|
||||
- **Nginx** : Pour le reverse proxy (optionnel mais recommandé)
|
||||
|
||||
### Accès nécessaires
|
||||
|
||||
- Accès SSH au serveur
|
||||
- Droits sudo
|
||||
- Accès à la base de données MySQL
|
||||
|
||||
---
|
||||
|
||||
## Étape 1 : Préparation du serveur
|
||||
|
||||
### 1.1 Mise à jour du système
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt upgrade -y
|
||||
```
|
||||
|
||||
### 1.2 Installation de Node.js 22.x
|
||||
|
||||
```bash
|
||||
# Installer Node.js via NodeSource
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||
sudo apt install -y nodejs
|
||||
|
||||
# Vérifier l'installation
|
||||
node --version # Doit afficher v22.x.x
|
||||
```
|
||||
|
||||
### 1.3 Installation de pnpm
|
||||
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
pnpm --version # Doit afficher 10.x.x
|
||||
```
|
||||
|
||||
### 1.4 Installation de MySQL
|
||||
|
||||
```bash
|
||||
sudo apt install -y mysql-server
|
||||
sudo mysql_secure_installation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 2 : Configuration de la base de données
|
||||
|
||||
### 2.1 Créer la base de données et l'utilisateur
|
||||
|
||||
```bash
|
||||
sudo mysql
|
||||
```
|
||||
|
||||
Dans MySQL :
|
||||
|
||||
```sql
|
||||
CREATE DATABASE formation_manager CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER 'formation_user'@'localhost' IDENTIFIED BY 'VOTRE_MOT_DE_PASSE_SECURISE';
|
||||
GRANT ALL PRIVILEGES ON formation_manager.* TO 'formation_user'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
EXIT;
|
||||
```
|
||||
|
||||
### 2.2 Tester la connexion
|
||||
|
||||
```bash
|
||||
mysql -u formation_user -p formation_manager
|
||||
# Entrez le mot de passe, puis EXIT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 3 : Déploiement de l'application
|
||||
|
||||
### 3.1 Créer le répertoire de l'application
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/formation-manager-itinova
|
||||
sudo chown -R $USER:$USER /var/www/formation-manager-itinova
|
||||
cd /var/www/formation-manager-itinova
|
||||
```
|
||||
|
||||
### 3.2 Cloner ou copier les fichiers
|
||||
|
||||
**Option A : Via Git (recommandé)**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/votre-repo/formation-manager-itinova.git .
|
||||
```
|
||||
|
||||
**Option B : Via upload manuel**
|
||||
|
||||
Transférez les fichiers via SCP/SFTP vers `/var/www/formation-manager-itinova/`
|
||||
|
||||
### 3.3 Installer les dépendances
|
||||
|
||||
```bash
|
||||
cd /var/www/formation-manager-itinova
|
||||
pnpm install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 4 : Configuration de l'application
|
||||
|
||||
### 4.1 Créer le fichier .env
|
||||
|
||||
```bash
|
||||
nano /var/www/formation-manager-itinova/.env
|
||||
```
|
||||
|
||||
**Contenu du fichier .env :**
|
||||
|
||||
```env
|
||||
# Base de données
|
||||
DATABASE_URL=mysql://formation_user:VOTRE_MOT_DE_PASSE@localhost:3306/formation_manager
|
||||
|
||||
# Sécurité JWT (générez une chaîne aléatoire longue)
|
||||
JWT_SECRET=CHANGEZ_CECI_PAR_UNE_CHAINE_ALEATOIRE_TRES_LONGUE_ET_SECURISEE
|
||||
|
||||
# Configuration de l'application
|
||||
NODE_ENV=production
|
||||
VITE_APP_TITLE=Formation Manager Itinova
|
||||
VITE_APP_LOGO=/logo.svg
|
||||
|
||||
# Email (optionnel - pour Resend)
|
||||
RESEND_API_KEY=
|
||||
RESEND_FROM_EMAIL=
|
||||
|
||||
# Port de l'application (par défaut 3000)
|
||||
PORT=3000
|
||||
```
|
||||
|
||||
**Générer un JWT_SECRET sécurisé :**
|
||||
|
||||
```bash
|
||||
openssl rand -base64 64
|
||||
```
|
||||
|
||||
Copiez le résultat dans `JWT_SECRET`.
|
||||
|
||||
### 4.2 Sécuriser le fichier .env
|
||||
|
||||
```bash
|
||||
chmod 600 /var/www/formation-manager-itinova/.env
|
||||
```
|
||||
|
||||
### 4.3 Initialiser la base de données
|
||||
|
||||
```bash
|
||||
cd /var/www/formation-manager-itinova
|
||||
pnpm db:push
|
||||
```
|
||||
|
||||
Cette commande va créer toutes les tables nécessaires.
|
||||
|
||||
---
|
||||
|
||||
## Étape 5 : Compilation de l'application
|
||||
|
||||
### 5.1 Compiler le frontend et le backend
|
||||
|
||||
```bash
|
||||
cd /var/www/formation-manager-itinova
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### 5.2 Vérifier que la compilation a réussi
|
||||
|
||||
```bash
|
||||
ls -la /var/www/formation-manager-itinova/dist/
|
||||
```
|
||||
|
||||
Vous devriez voir un fichier `index.js`.
|
||||
|
||||
---
|
||||
|
||||
## Étape 6 : Configuration du service systemd
|
||||
|
||||
### 6.1 Créer le fichier de service
|
||||
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/formation-manager.service
|
||||
```
|
||||
|
||||
**Contenu du fichier :**
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Formation Manager Itinova
|
||||
After=network.target mysql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
WorkingDirectory=/var/www/formation-manager-itinova
|
||||
Environment="NODE_ENV=production"
|
||||
Environment="HOME=/var/www/formation-manager-itinova"
|
||||
Environment="PNPM_HOME=/var/www/formation-manager-itinova/.pnpm"
|
||||
EnvironmentFile=/var/www/formation-manager-itinova/.env
|
||||
ExecStart=/usr/bin/pnpm start
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### 6.2 Configurer les permissions
|
||||
|
||||
```bash
|
||||
sudo chown -R www-data:www-data /var/www/formation-manager-itinova
|
||||
sudo mkdir -p /var/www/.local/share/pnpm
|
||||
sudo chown -R www-data:www-data /var/www/.local
|
||||
```
|
||||
|
||||
### 6.3 Activer et démarrer le service
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable formation-manager
|
||||
sudo systemctl start formation-manager
|
||||
```
|
||||
|
||||
### 6.4 Vérifier le statut
|
||||
|
||||
```bash
|
||||
sudo systemctl status formation-manager
|
||||
```
|
||||
|
||||
### 6.5 Voir les logs
|
||||
|
||||
```bash
|
||||
sudo journalctl -u formation-manager -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 7 : Configuration de Nginx (optionnel mais recommandé)
|
||||
|
||||
### 7.1 Installer Nginx
|
||||
|
||||
```bash
|
||||
sudo apt install -y nginx
|
||||
```
|
||||
|
||||
### 7.2 Créer la configuration du site
|
||||
|
||||
```bash
|
||||
sudo nano /etc/nginx/sites-available/formation-manager
|
||||
```
|
||||
|
||||
**Contenu du fichier :**
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name votre-domaine.com; # Changez par votre domaine
|
||||
|
||||
# Logs
|
||||
access_log /var/log/nginx/formation-manager-access.log;
|
||||
error_log /var/log/nginx/formation-manager-error.log;
|
||||
|
||||
# Proxy vers l'application Node.js
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 Activer le site
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/formation-manager /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl restart nginx
|
||||
```
|
||||
|
||||
### 7.4 Configurer le pare-feu
|
||||
|
||||
```bash
|
||||
sudo ufw allow 'Nginx Full'
|
||||
sudo ufw allow OpenSSH
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 8 : Configuration HTTPS avec Let's Encrypt (recommandé)
|
||||
|
||||
### 8.1 Installer Certbot
|
||||
|
||||
```bash
|
||||
sudo apt install -y certbot python3-certbot-nginx
|
||||
```
|
||||
|
||||
### 8.2 Obtenir un certificat SSL
|
||||
|
||||
```bash
|
||||
sudo certbot --nginx -d votre-domaine.com
|
||||
```
|
||||
|
||||
Suivez les instructions à l'écran.
|
||||
|
||||
### 8.3 Renouvellement automatique
|
||||
|
||||
```bash
|
||||
sudo systemctl status certbot.timer
|
||||
```
|
||||
|
||||
Le renouvellement est automatique.
|
||||
|
||||
---
|
||||
|
||||
## Étape 9 : Créer le premier utilisateur administrateur
|
||||
|
||||
### 9.1 Se connecter à MySQL
|
||||
|
||||
```bash
|
||||
mysql -u formation_user -p formation_manager
|
||||
```
|
||||
|
||||
### 9.2 Créer un utilisateur admin
|
||||
|
||||
```sql
|
||||
-- Générer un hash bcrypt pour le mot de passe "admin123" (à changer immédiatement)
|
||||
-- Hash bcrypt de "admin123" : $2b$10$rKZhVJQX7mEWQxGqF8vQZOQxqxqxqxqxqxqxqxqxqxqxqxqxqxqxq
|
||||
|
||||
INSERT INTO users (email, password, name, role, emailVerified, isActive, createdAt, updatedAt, lastSignedIn)
|
||||
VALUES (
|
||||
'admin@itinova.org',
|
||||
'$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK', -- Mot de passe: admin123
|
||||
'Administrateur',
|
||||
'admin',
|
||||
TRUE,
|
||||
TRUE,
|
||||
NOW(),
|
||||
NOW(),
|
||||
NOW()
|
||||
);
|
||||
|
||||
EXIT;
|
||||
```
|
||||
|
||||
**⚠️ IMPORTANT** : Changez le mot de passe immédiatement après la première connexion !
|
||||
|
||||
---
|
||||
|
||||
## Étape 10 : Vérification et tests
|
||||
|
||||
### 10.1 Accéder à l'application
|
||||
|
||||
Ouvrez votre navigateur et accédez à :
|
||||
- **HTTP** : `http://votre-domaine.com`
|
||||
- **HTTPS** : `https://votre-domaine.com`
|
||||
|
||||
### 10.2 Se connecter
|
||||
|
||||
- **Email** : `admin@itinova.org`
|
||||
- **Mot de passe** : `admin123`
|
||||
|
||||
### 10.3 Changer le mot de passe
|
||||
|
||||
1. Connectez-vous
|
||||
2. Allez dans "Utilisateurs"
|
||||
3. Modifiez votre profil
|
||||
4. Changez le mot de passe
|
||||
|
||||
---
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Voir les logs
|
||||
|
||||
```bash
|
||||
sudo journalctl -u formation-manager -f
|
||||
```
|
||||
|
||||
### Redémarrer l'application
|
||||
|
||||
```bash
|
||||
sudo systemctl restart formation-manager
|
||||
```
|
||||
|
||||
### Mettre à jour l'application
|
||||
|
||||
```bash
|
||||
cd /var/www/formation-manager-itinova
|
||||
git pull # Si vous utilisez Git
|
||||
pnpm install
|
||||
pnpm build
|
||||
sudo systemctl restart formation-manager
|
||||
```
|
||||
|
||||
### Sauvegarder la base de données
|
||||
|
||||
```bash
|
||||
mysqldump -u formation_user -p formation_manager > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||
```
|
||||
|
||||
### Restaurer la base de données
|
||||
|
||||
```bash
|
||||
mysql -u formation_user -p formation_manager < backup_YYYYMMDD_HHMMSS.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dépannage
|
||||
|
||||
### L'application ne démarre pas
|
||||
|
||||
1. Vérifier les logs :
|
||||
```bash
|
||||
sudo journalctl -u formation-manager -n 100 --no-pager
|
||||
```
|
||||
|
||||
2. Vérifier que le fichier .env existe et est correct :
|
||||
```bash
|
||||
cat /var/www/formation-manager-itinova/.env
|
||||
```
|
||||
|
||||
3. Vérifier que la base de données est accessible :
|
||||
```bash
|
||||
mysql -u formation_user -p formation_manager
|
||||
```
|
||||
|
||||
### Erreur de permissions
|
||||
|
||||
```bash
|
||||
sudo chown -R www-data:www-data /var/www/formation-manager-itinova
|
||||
sudo mkdir -p /var/www/.local/share/pnpm
|
||||
sudo chown -R www-data:www-data /var/www/.local
|
||||
```
|
||||
|
||||
### Erreur "drizzle-kit: not found"
|
||||
|
||||
```bash
|
||||
cd /var/www/formation-manager-itinova
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Port 3000 déjà utilisé
|
||||
|
||||
Modifiez le port dans `.env` :
|
||||
```env
|
||||
PORT=3001
|
||||
```
|
||||
|
||||
Puis redémarrez :
|
||||
```bash
|
||||
sudo systemctl restart formation-manager
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sécurité
|
||||
|
||||
### Recommandations
|
||||
|
||||
1. **Changez immédiatement le mot de passe admin par défaut**
|
||||
2. **Utilisez HTTPS** (Let's Encrypt gratuit)
|
||||
3. **Configurez un pare-feu** (ufw)
|
||||
4. **Mettez à jour régulièrement** le système et l'application
|
||||
5. **Sauvegardez régulièrement** la base de données
|
||||
6. **Limitez l'accès SSH** (clés SSH, désactiver root login)
|
||||
7. **Surveillez les logs** régulièrement
|
||||
|
||||
### Sauvegardes automatiques
|
||||
|
||||
Créez un script de sauvegarde :
|
||||
|
||||
```bash
|
||||
sudo nano /usr/local/bin/backup-formation-manager.sh
|
||||
```
|
||||
|
||||
Contenu :
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
BACKUP_DIR="/var/backups/formation-manager"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# Sauvegarder la base de données
|
||||
mysqldump -u formation_user -p'VOTRE_MOT_DE_PASSE' formation_manager > $BACKUP_DIR/db_$DATE.sql
|
||||
|
||||
# Compresser
|
||||
gzip $BACKUP_DIR/db_$DATE.sql
|
||||
|
||||
# Garder seulement les 30 derniers jours
|
||||
find $BACKUP_DIR -name "db_*.sql.gz" -mtime +30 -delete
|
||||
|
||||
echo "Sauvegarde terminée : $BACKUP_DIR/db_$DATE.sql.gz"
|
||||
```
|
||||
|
||||
Rendre exécutable :
|
||||
|
||||
```bash
|
||||
sudo chmod +x /usr/local/bin/backup-formation-manager.sh
|
||||
```
|
||||
|
||||
Ajouter au cron (tous les jours à 2h du matin) :
|
||||
|
||||
```bash
|
||||
sudo crontab -e
|
||||
```
|
||||
|
||||
Ajouter :
|
||||
|
||||
```
|
||||
0 2 * * * /usr/local/bin/backup-formation-manager.sh >> /var/log/backup-formation-manager.log 2>&1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
Pour toute question ou problème :
|
||||
|
||||
1. Consultez les logs : `sudo journalctl -u formation-manager -f`
|
||||
2. Vérifiez la documentation : `/var/www/formation-manager-itinova/README.md`
|
||||
3. Contactez le support technique
|
||||
|
||||
---
|
||||
|
||||
## Annexe : Commandes utiles
|
||||
|
||||
```bash
|
||||
# Statut du service
|
||||
sudo systemctl status formation-manager
|
||||
|
||||
# Démarrer le service
|
||||
sudo systemctl start formation-manager
|
||||
|
||||
# Arrêter le service
|
||||
sudo systemctl stop formation-manager
|
||||
|
||||
# Redémarrer le service
|
||||
sudo systemctl restart formation-manager
|
||||
|
||||
# Voir les logs en temps réel
|
||||
sudo journalctl -u formation-manager -f
|
||||
|
||||
# Voir les 100 dernières lignes de logs
|
||||
sudo journalctl -u formation-manager -n 100 --no-pager
|
||||
|
||||
# Tester la connexion à la base de données
|
||||
mysql -u formation_user -p formation_manager
|
||||
|
||||
# Vérifier la configuration Nginx
|
||||
sudo nginx -t
|
||||
|
||||
# Recharger Nginx
|
||||
sudo systemctl reload nginx
|
||||
|
||||
# Vérifier l'utilisation des ressources
|
||||
htop
|
||||
|
||||
# Vérifier l'espace disque
|
||||
df -h
|
||||
|
||||
# Vérifier les processus Node.js
|
||||
ps aux | grep node
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Fin du guide de déploiement**
|
||||
260
MIGRATION_AUTH_LOCALE.md
Normal file
260
MIGRATION_AUTH_LOCALE.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# Migration OAuth Manus vers Authentification Locale
|
||||
|
||||
## État de la migration
|
||||
|
||||
**Date** : 18 novembre 2024
|
||||
**Statut** : ⚠️ **En cours** (80% complété)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Modifications complétées
|
||||
|
||||
### 1. Base de données
|
||||
|
||||
- ✅ Schéma `users` modifié :
|
||||
- Ajout du champ `email` (unique, not null)
|
||||
- Ajout du champ `password` (hash bcrypt, not null)
|
||||
- Ajout du champ `emailVerified` (boolean, default false)
|
||||
- Suppression du champ `openId`
|
||||
- Suppression du champ `loginMethod`
|
||||
|
||||
### 2. Backend
|
||||
|
||||
- ✅ Installation de `bcrypt` pour le hashage des mots de passe
|
||||
- ✅ Création des helpers d'authentification (`server/_core/auth.ts`) :
|
||||
- `hashPassword()` - Hashe un mot de passe avec bcrypt
|
||||
- `verifyPassword()` - Vérifie un mot de passe
|
||||
- `generateToken()` - Génère un token JWT
|
||||
- `verifyToken()` - Vérifie un token JWT
|
||||
|
||||
- ✅ Modification du fichier `server/db.ts` :
|
||||
- `createUser()` - Crée un utilisateur avec email/password
|
||||
- `getUserByEmail()` - Récupère un utilisateur par email
|
||||
- `getUserById()` - Récupère un utilisateur par ID
|
||||
- `updateUserLastSignedIn()` - Met à jour le dernier login
|
||||
|
||||
- ✅ Modification du fichier `server/routers.ts` :
|
||||
- Procédure `auth.register` - Inscription avec email/password
|
||||
- Procédure `auth.login` - Connexion avec email/password
|
||||
- Procédure `auth.logout` - Déconnexion (inchangée)
|
||||
|
||||
- ✅ Modification du fichier `server/_core/context.ts` :
|
||||
- Remplacement de l'authentification OAuth par JWT
|
||||
- Lecture du token depuis le cookie
|
||||
- Vérification et décodage du token
|
||||
- Récupération de l'utilisateur depuis la base de données
|
||||
|
||||
### 3. Frontend
|
||||
|
||||
- ✅ Création de la page `Login.tsx` :
|
||||
- Formulaire de connexion (email + mot de passe)
|
||||
- Validation des champs
|
||||
- Gestion des erreurs
|
||||
- Redirection après connexion
|
||||
- Lien vers la page d'inscription
|
||||
|
||||
- ✅ Création de la page `Register.tsx` :
|
||||
- Formulaire d'inscription (nom, email, mot de passe, confirmation)
|
||||
- Validation des champs (minimum 6 caractères)
|
||||
- Vérification de la correspondance des mots de passe
|
||||
- Gestion des erreurs
|
||||
- Redirection après inscription
|
||||
- Lien vers la page de connexion
|
||||
|
||||
- ✅ Modification du fichier `App.tsx` :
|
||||
- Ajout des routes `/login` et `/register`
|
||||
|
||||
- ✅ Modification du fichier `const.ts` :
|
||||
- Remplacement de `getLoginUrl()` pour pointer vers `/login`
|
||||
|
||||
### 4. Documentation
|
||||
|
||||
- ✅ Création du guide de déploiement Linux complet (`GUIDE_DEPLOIEMENT_LINUX.md`)
|
||||
- ✅ Création de ce document de migration
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Modifications restantes
|
||||
|
||||
### 1. Correction des erreurs
|
||||
|
||||
- ❌ **Corriger les doublons dans `server/db.ts`**
|
||||
- Il reste des fonctions dupliquées qui causent des erreurs de compilation
|
||||
- Nettoyer complètement les anciennes fonctions OAuth
|
||||
|
||||
### 2. Page de gestion des utilisateurs
|
||||
|
||||
- ❌ **Modifier `AdminUsers.tsx`** :
|
||||
- Remplacer le champ `openId` par `password` dans le formulaire de création
|
||||
- Ajouter un champ de changement de mot de passe dans le formulaire d'édition
|
||||
- Adapter les mutations pour utiliser email au lieu d'openId
|
||||
|
||||
### 3. Hooks d'authentification
|
||||
|
||||
- ❌ **Vérifier `useAuth()`** :
|
||||
- S'assurer qu'il fonctionne avec la nouvelle authentification JWT
|
||||
- Pas de modification normalement nécessaire car il utilise `trpc.auth.me`
|
||||
|
||||
### 4. Suppression du code OAuth
|
||||
|
||||
- ❌ **Supprimer les fichiers OAuth inutilisés** :
|
||||
- `server/_core/sdk.ts` (si existe)
|
||||
- `server/_core/oauth.ts` (si existe)
|
||||
- Routes OAuth dans `server/_core/index.ts`
|
||||
|
||||
### 5. Variables d'environnement
|
||||
|
||||
- ❌ **Nettoyer le fichier `.env`** :
|
||||
- Supprimer `OAUTH_SERVER_URL`
|
||||
- Supprimer `VITE_OAUTH_PORTAL_URL`
|
||||
- Supprimer `VITE_APP_ID`
|
||||
- Supprimer `OWNER_OPEN_ID`
|
||||
- Garder uniquement :
|
||||
- `DATABASE_URL`
|
||||
- `JWT_SECRET`
|
||||
- `VITE_APP_TITLE`
|
||||
- `VITE_APP_LOGO`
|
||||
- Variables email (optionnelles)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Prochaines étapes pour finaliser
|
||||
|
||||
### Étape 1 : Corriger les erreurs de compilation
|
||||
|
||||
```bash
|
||||
cd /home/ubuntu/formation-manager-itinova
|
||||
|
||||
# Vérifier les erreurs
|
||||
pnpm check
|
||||
```
|
||||
|
||||
Corriger manuellement les doublons dans `server/db.ts`.
|
||||
|
||||
### Étape 2 : Modifier la page AdminUsers
|
||||
|
||||
1. Ouvrir `client/src/pages/AdminUsers.tsx`
|
||||
2. Remplacer le champ `openId` par `password` dans le formulaire de création
|
||||
3. Ajouter un champ de changement de mot de passe (optionnel) dans l'édition
|
||||
|
||||
### Étape 3 : Tester localement
|
||||
|
||||
```bash
|
||||
# Compiler
|
||||
pnpm build
|
||||
|
||||
# Démarrer
|
||||
pnpm start
|
||||
```
|
||||
|
||||
Tester :
|
||||
1. Inscription d'un nouvel utilisateur
|
||||
2. Connexion avec cet utilisateur
|
||||
3. Déconnexion
|
||||
4. Gestion des utilisateurs (admin)
|
||||
|
||||
### Étape 4 : Créer le premier utilisateur admin
|
||||
|
||||
Après déploiement, exécuter ce SQL :
|
||||
|
||||
```sql
|
||||
INSERT INTO users (email, password, name, role, emailVerified, isActive, createdAt, updatedAt, lastSignedIn)
|
||||
VALUES (
|
||||
'admin@itinova.org',
|
||||
'$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK', -- Mot de passe: admin123
|
||||
'Administrateur',
|
||||
'admin',
|
||||
TRUE,
|
||||
TRUE,
|
||||
NOW(),
|
||||
NOW(),
|
||||
NOW()
|
||||
);
|
||||
```
|
||||
|
||||
⚠️ **Changez immédiatement le mot de passe après la première connexion !**
|
||||
|
||||
### Étape 5 : Déployer sur le serveur
|
||||
|
||||
Suivre le guide `GUIDE_DEPLOIEMENT_LINUX.md`.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Checklist de déploiement
|
||||
|
||||
### Avant le déploiement
|
||||
|
||||
- [ ] Toutes les erreurs de compilation sont corrigées
|
||||
- [ ] Les tests locaux passent (inscription, connexion, déconnexion)
|
||||
- [ ] Le fichier `.env` est configuré correctement
|
||||
- [ ] La base de données est accessible
|
||||
- [ ] Un utilisateur admin est créé en base de données
|
||||
|
||||
### Pendant le déploiement
|
||||
|
||||
- [ ] Les fichiers sont copiés sur le serveur
|
||||
- [ ] Les dépendances sont installées (`pnpm install`)
|
||||
- [ ] La base de données est initialisée (`pnpm db:push`)
|
||||
- [ ] L'application est compilée (`pnpm build`)
|
||||
- [ ] Le service systemd est configuré
|
||||
- [ ] Le service démarre sans erreur
|
||||
|
||||
### Après le déploiement
|
||||
|
||||
- [ ] L'application est accessible via le navigateur
|
||||
- [ ] La connexion fonctionne
|
||||
- [ ] L'inscription fonctionne
|
||||
- [ ] Le tableau de bord admin est accessible
|
||||
- [ ] Les fonctionnalités de gestion des utilisateurs fonctionnent
|
||||
- [ ] Le mot de passe admin par défaut a été changé
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Dépannage
|
||||
|
||||
### Erreur "Email ou mot de passe incorrect"
|
||||
|
||||
- Vérifier que l'utilisateur existe en base de données
|
||||
- Vérifier que le mot de passe est bien hashé avec bcrypt
|
||||
- Vérifier les logs du serveur
|
||||
|
||||
### Erreur "User is not defined"
|
||||
|
||||
- Vérifier que le token JWT est valide
|
||||
- Vérifier que le cookie est bien envoyé
|
||||
- Vérifier que `context.ts` récupère bien l'utilisateur
|
||||
|
||||
### Erreur "Cannot read property 'id' of null"
|
||||
|
||||
- L'utilisateur n'est pas connecté
|
||||
- Rediriger vers `/login`
|
||||
|
||||
### L'application ne démarre pas
|
||||
|
||||
- Vérifier les logs : `sudo journalctl -u formation-manager -f`
|
||||
- Vérifier le fichier `.env`
|
||||
- Vérifier la connexion à la base de données
|
||||
|
||||
---
|
||||
|
||||
## 📚 Ressources
|
||||
|
||||
- Guide de déploiement : `GUIDE_DEPLOIEMENT_LINUX.md`
|
||||
- Documentation bcrypt : https://www.npmjs.com/package/bcrypt
|
||||
- Documentation JWT : https://jwt.io/
|
||||
- Documentation tRPC : https://trpc.io/
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
En cas de problème :
|
||||
|
||||
1. Consulter les logs du serveur
|
||||
2. Vérifier la base de données
|
||||
3. Consulter ce document de migration
|
||||
4. Consulter le guide de déploiement
|
||||
|
||||
---
|
||||
|
||||
**Note** : Cette migration remplace complètement OAuth Manus par une authentification locale. L'application devient totalement autonome et ne dépend plus de services externes pour l'authentification.
|
||||
@@ -15,10 +15,14 @@ import AdminUsers from "./pages/AdminUsers";
|
||||
import AdminEmailTemplates from "./pages/AdminEmailTemplates";
|
||||
import AdminEmailConfig from "./pages/AdminEmailConfig";
|
||||
import Inscription from "./pages/Inscription";
|
||||
import Login from "./pages/Login";
|
||||
import Register from "./pages/Register";
|
||||
|
||||
function Router() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={"/login"} component={Login} />
|
||||
<Route path={"/register"} component={Register} />
|
||||
<Route path={"/"} component={Home} />
|
||||
<Route path={"/admin/rapport-public-cible"} component={AdminRapportPublicCible} />
|
||||
<Route path={"/inscription/:lien"} component={Inscription} />
|
||||
|
||||
@@ -4,18 +4,5 @@ export const APP_TITLE = import.meta.env.VITE_APP_TITLE || "App";
|
||||
|
||||
export const APP_LOGO = "https://placehold.co/128x128/E1E7EF/1F2937?text=App";
|
||||
|
||||
// Generate login URL at runtime so redirect URI reflects the current origin.
|
||||
export const getLoginUrl = () => {
|
||||
const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL;
|
||||
const appId = import.meta.env.VITE_APP_ID;
|
||||
const redirectUri = `${window.location.origin}/api/oauth/callback`;
|
||||
const state = btoa(redirectUri);
|
||||
|
||||
const url = new URL(`${oauthPortalUrl}/app-auth`);
|
||||
url.searchParams.set("appId", appId);
|
||||
url.searchParams.set("redirectUri", redirectUri);
|
||||
url.searchParams.set("state", state);
|
||||
url.searchParams.set("type", "signIn");
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
// Retourne l'URL de la page de connexion locale
|
||||
export const getLoginUrl = () => "/login";
|
||||
|
||||
116
client/src/pages/Login.tsx
Normal file
116
client/src/pages/Login.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { useState } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { APP_LOGO, APP_TITLE } from "@/const";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
export default function Login() {
|
||||
const [, setLocation] = useLocation();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const loginMutation = trpc.auth.login.useMutation({
|
||||
onSuccess: () => {
|
||||
// Rediriger vers le tableau de bord
|
||||
window.location.href = "/";
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
|
||||
if (!email || !password) {
|
||||
setError("Veuillez remplir tous les champs");
|
||||
return;
|
||||
}
|
||||
|
||||
loginMutation.mutate({ email, password });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="space-y-1 text-center">
|
||||
{APP_LOGO && (
|
||||
<div className="flex justify-center mb-4">
|
||||
<img src={APP_LOGO} alt={APP_TITLE} className="h-16 w-auto" />
|
||||
</div>
|
||||
)}
|
||||
<CardTitle className="text-2xl font-bold">{APP_TITLE}</CardTitle>
|
||||
<CardDescription>
|
||||
Connectez-vous à votre compte
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent className="space-y-4">
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="votre@email.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={loginMutation.isPending}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Mot de passe</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={loginMutation.isPending}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex flex-col space-y-4">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={loginMutation.isPending}
|
||||
>
|
||||
{loginMutation.isPending && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
Se connecter
|
||||
</Button>
|
||||
|
||||
<div className="text-sm text-center text-muted-foreground">
|
||||
Pas encore de compte ?{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLocation("/register")}
|
||||
className="text-primary hover:underline font-medium"
|
||||
>
|
||||
S'inscrire
|
||||
</button>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
156
client/src/pages/Register.tsx
Normal file
156
client/src/pages/Register.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { useState } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { APP_LOGO, APP_TITLE } from "@/const";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
export default function Register() {
|
||||
const [, setLocation] = useLocation();
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const registerMutation = trpc.auth.register.useMutation({
|
||||
onSuccess: () => {
|
||||
// Rediriger vers le tableau de bord
|
||||
window.location.href = "/";
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
|
||||
if (!email || !password || !confirmPassword) {
|
||||
setError("Veuillez remplir tous les champs obligatoires");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
setError("Le mot de passe doit contenir au moins 6 caractères");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError("Les mots de passe ne correspondent pas");
|
||||
return;
|
||||
}
|
||||
|
||||
registerMutation.mutate({ email, password, name: name || undefined });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="space-y-1 text-center">
|
||||
{APP_LOGO && (
|
||||
<div className="flex justify-center mb-4">
|
||||
<img src={APP_LOGO} alt={APP_TITLE} className="h-16 w-auto" />
|
||||
</div>
|
||||
)}
|
||||
<CardTitle className="text-2xl font-bold">Créer un compte</CardTitle>
|
||||
<CardDescription>
|
||||
Inscrivez-vous pour accéder à {APP_TITLE}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent className="space-y-4">
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Nom (optionnel)</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Votre nom"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
disabled={registerMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email *</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="votre@email.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={registerMutation.isPending}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Mot de passe *</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={registerMutation.isPending}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Minimum 6 caractères
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Confirmer le mot de passe *</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
disabled={registerMutation.isPending}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex flex-col space-y-4">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={registerMutation.isPending}
|
||||
>
|
||||
{registerMutation.isPending && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
S'inscrire
|
||||
</Button>
|
||||
|
||||
<div className="text-sm text-center text-muted-foreground">
|
||||
Déjà un compte ?{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLocation("/login")}
|
||||
className="text-primary hover:underline font-medium"
|
||||
>
|
||||
Se connecter
|
||||
</button>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
drizzle/0010_flat_clint_barton.sql
Normal file
7
drizzle/0010_flat_clint_barton.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE `users` DROP INDEX `users_openId_unique`;--> statement-breakpoint
|
||||
ALTER TABLE `users` MODIFY COLUMN `email` varchar(320) NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `users` ADD `password` varchar(255) NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `users` ADD `emailVerified` boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `users` ADD CONSTRAINT `users_email_unique` UNIQUE(`email`);--> statement-breakpoint
|
||||
ALTER TABLE `users` DROP COLUMN `openId`;--> statement-breakpoint
|
||||
ALTER TABLE `users` DROP COLUMN `loginMethod`;
|
||||
780
drizzle/meta/0010_snapshot.json
Normal file
780
drizzle/meta/0010_snapshot.json
Normal file
@@ -0,0 +1,780 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "e34c8fec-5308-4ce7-b687-d27c3093036c",
|
||||
"prevId": "a099e856-0b0c-430d-8160-ca950c994fe5",
|
||||
"tables": {
|
||||
"apprenants": {
|
||||
"name": "apprenants",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"nom": {
|
||||
"name": "nom",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"prenom": {
|
||||
"name": "prenom",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(320)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"codeEtablissement": {
|
||||
"name": "codeEtablissement",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fonction": {
|
||||
"name": "fonction",
|
||||
"type": "enum('directeur','chef_service','autre')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"apprenants_id": {
|
||||
"name": "apprenants_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"apprenants_email_unique": {
|
||||
"name": "apprenants_email_unique",
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"datesFormation": {
|
||||
"name": "datesFormation",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"sequenceId": {
|
||||
"name": "sequenceId",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dateDebut": {
|
||||
"name": "dateDebut",
|
||||
"type": "datetime",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dateFin": {
|
||||
"name": "dateFin",
|
||||
"type": "datetime",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ordre": {
|
||||
"name": "ordre",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"datesFormation_id": {
|
||||
"name": "datesFormation_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"emailConfig": {
|
||||
"name": "emailConfig",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'resend'"
|
||||
},
|
||||
"apiKey": {
|
||||
"name": "apiKey",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fromEmail": {
|
||||
"name": "fromEmail",
|
||||
"type": "varchar(320)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fromName": {
|
||||
"name": "fromName",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'Formation Manager Itinova'"
|
||||
},
|
||||
"mode": {
|
||||
"name": "mode",
|
||||
"type": "enum('simulation','production')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'simulation'"
|
||||
},
|
||||
"domainVerified": {
|
||||
"name": "domainVerified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"active": {
|
||||
"name": "active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"emailConfig_id": {
|
||||
"name": "emailConfig_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"emailTemplates": {
|
||||
"name": "emailTemplates",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"logoUrl": {
|
||||
"name": "logoUrl",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"primaryColor": {
|
||||
"name": "primaryColor",
|
||||
"type": "varchar(7)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'#2563eb'"
|
||||
},
|
||||
"headerBgColor": {
|
||||
"name": "headerBgColor",
|
||||
"type": "varchar(7)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'#2563eb'"
|
||||
},
|
||||
"headerTextColor": {
|
||||
"name": "headerTextColor",
|
||||
"type": "varchar(7)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'#ffffff'"
|
||||
},
|
||||
"headerTitle": {
|
||||
"name": "headerTitle",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'Formation Manager Itinova'"
|
||||
},
|
||||
"footerText": {
|
||||
"name": "footerText",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"active": {
|
||||
"name": "active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"emailTemplates_id": {
|
||||
"name": "emailTemplates_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"emailTemplates_type_unique": {
|
||||
"name": "emailTemplates_type_unique",
|
||||
"columns": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"formations": {
|
||||
"name": "formations",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"nom": {
|
||||
"name": "nom",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lienUnique": {
|
||||
"name": "lienUnique",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"actif": {
|
||||
"name": "actif",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"formations_id": {
|
||||
"name": "formations_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"formations_lienUnique_unique": {
|
||||
"name": "formations_lienUnique_unique",
|
||||
"columns": [
|
||||
"lienUnique"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"inscriptions": {
|
||||
"name": "inscriptions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"apprenantId": {
|
||||
"name": "apprenantId",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"sequenceId": {
|
||||
"name": "sequenceId",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"statut": {
|
||||
"name": "statut",
|
||||
"type": "enum('confirmee','liste_attente','annulee')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dateInscription": {
|
||||
"name": "dateInscription",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"inscriptions_id": {
|
||||
"name": "inscriptions_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"passwordResetTokens": {
|
||||
"name": "passwordResetTokens",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expiresAt": {
|
||||
"name": "expiresAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"used": {
|
||||
"name": "used",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"passwordResetTokens_id": {
|
||||
"name": "passwordResetTokens_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"passwordResetTokens_token_unique": {
|
||||
"name": "passwordResetTokens_token_unique",
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"sequences": {
|
||||
"name": "sequences",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"formationId": {
|
||||
"name": "formationId",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"nom": {
|
||||
"name": "nom",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lieu": {
|
||||
"name": "lieu",
|
||||
"type": "varchar(500)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"publicCible": {
|
||||
"name": "publicCible",
|
||||
"type": "enum('directeur','chef_service','autre')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"capaciteMax": {
|
||||
"name": "capaciteMax",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 12
|
||||
},
|
||||
"dateBlocage": {
|
||||
"name": "dateBlocage",
|
||||
"type": "datetime",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"statut": {
|
||||
"name": "statut",
|
||||
"type": "enum('ouverte','bloquee','terminee')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'ouverte'"
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"sequences_id": {
|
||||
"name": "sequences_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(320)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"emailVerified": {
|
||||
"name": "emailVerified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "enum('user','admin')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'user'"
|
||||
},
|
||||
"isActive": {
|
||||
"name": "isActive",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"onUpdate": true,
|
||||
"default": "(now())"
|
||||
},
|
||||
"lastSignedIn": {
|
||||
"name": "lastSignedIn",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"users_id": {
|
||||
"name": "users_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,13 @@
|
||||
"when": 1763448844826,
|
||||
"tag": "0009_polite_senator_kelly",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "5",
|
||||
"when": 1763544545719,
|
||||
"tag": "0010_flat_clint_barton",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,11 +11,13 @@ export const users = mysqlTable("users", {
|
||||
* Use this for relations between tables.
|
||||
*/
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
/** Manus OAuth identifier (openId) returned from the OAuth callback. Unique per user. */
|
||||
openId: varchar("openId", { length: 64 }).notNull().unique(),
|
||||
/** Email unique pour la connexion */
|
||||
email: varchar("email", { length: 320 }).notNull().unique(),
|
||||
/** Mot de passe hashé avec bcrypt */
|
||||
password: varchar("password", { length: 255 }).notNull(),
|
||||
name: text("name"),
|
||||
email: varchar("email", { length: 320 }),
|
||||
loginMethod: varchar("loginMethod", { length: 64 }),
|
||||
/** Email vérifié ou non */
|
||||
emailVerified: boolean("emailVerified").default(false).notNull(),
|
||||
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
|
||||
/** Statut du compte (actif/inactif) */
|
||||
isActive: boolean("isActive").default(true).notNull(),
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"@trpc/react-query": "^11.6.0",
|
||||
"@trpc/server": "^11.6.0",
|
||||
"axios": "^1.12.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -85,6 +86,7 @@
|
||||
"@builder.io/vite-plugin-jsx-loc": "^0.1.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tailwindcss/vite": "^4.1.3",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/express": "4.17.21",
|
||||
"@types/google.maps": "^3.58.1",
|
||||
"@types/node": "^24.7.0",
|
||||
|
||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@@ -115,6 +115,9 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.12.0
|
||||
version: 1.12.2
|
||||
bcrypt:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@@ -227,6 +230,9 @@ importers:
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.3
|
||||
version: 4.1.14(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6))
|
||||
'@types/bcrypt':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
'@types/express':
|
||||
specifier: 4.17.21
|
||||
version: 4.17.21
|
||||
@@ -2157,6 +2163,9 @@ packages:
|
||||
'@types/babel__traverse@7.28.0':
|
||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
||||
|
||||
'@types/bcrypt@6.0.0':
|
||||
resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==}
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||
|
||||
@@ -2443,6 +2452,10 @@ packages:
|
||||
resolution: {integrity: sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==}
|
||||
hasBin: true
|
||||
|
||||
bcrypt@6.0.0:
|
||||
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
body-parser@1.20.3:
|
||||
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
@@ -3702,6 +3715,10 @@ packages:
|
||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
node-addon-api@8.5.0:
|
||||
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
|
||||
engines: {node: ^18 || ^20 || >= 21}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
@@ -3716,6 +3733,10 @@ packages:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
node-gyp-build@4.8.4:
|
||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||
hasBin: true
|
||||
|
||||
node-releases@2.0.23:
|
||||
resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==}
|
||||
|
||||
@@ -6641,6 +6662,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.28.4
|
||||
|
||||
'@types/bcrypt@6.0.0':
|
||||
dependencies:
|
||||
'@types/node': 24.7.0
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
@@ -6973,6 +6998,11 @@ snapshots:
|
||||
|
||||
baseline-browser-mapping@2.8.12: {}
|
||||
|
||||
bcrypt@6.0.0:
|
||||
dependencies:
|
||||
node-addon-api: 8.5.0
|
||||
node-gyp-build: 4.8.4
|
||||
|
||||
body-parser@1.20.3:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@@ -8496,12 +8526,16 @@ snapshots:
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
node-addon-api@8.5.0: {}
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
node-gyp-build@4.8.4: {}
|
||||
|
||||
node-releases@2.0.23: {}
|
||||
|
||||
normalize-range@0.1.2: {}
|
||||
|
||||
43
server/_core/auth.ts
Normal file
43
server/_core/auth.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import { sign, verify } from "jsonwebtoken";
|
||||
import { ENV } from "./env";
|
||||
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
/**
|
||||
* Hashe un mot de passe avec bcrypt
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, SALT_ROUNDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie un mot de passe contre son hash
|
||||
*/
|
||||
export async function verifyPassword(
|
||||
password: string,
|
||||
hash: string
|
||||
): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un token JWT pour un utilisateur
|
||||
*/
|
||||
export function generateToken(userId: number): string {
|
||||
return sign({ userId }, ENV.jwtSecret, {
|
||||
expiresIn: "30d",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie et décode un token JWT
|
||||
*/
|
||||
export function verifyToken(token: string): { userId: number } | null {
|
||||
try {
|
||||
const decoded = verify(token, ENV.jwtSecret) as { userId: number };
|
||||
return decoded;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
||||
import type { User } from "../../drizzle/schema";
|
||||
import { sdk } from "./sdk";
|
||||
import { verifyToken } from "./auth";
|
||||
import { getUserById } from "../db";
|
||||
import { COOKIE_NAME } from "@shared/const";
|
||||
|
||||
export type TrpcContext = {
|
||||
req: CreateExpressContextOptions["req"];
|
||||
@@ -14,7 +16,18 @@ export async function createContext(
|
||||
let user: User | null = null;
|
||||
|
||||
try {
|
||||
user = await sdk.authenticateRequest(opts.req);
|
||||
// Récupérer le token JWT depuis le cookie
|
||||
const token = opts.req.cookies[COOKIE_NAME];
|
||||
|
||||
if (token) {
|
||||
// Vérifier et décoder le token
|
||||
const decoded = verifyToken(token);
|
||||
|
||||
if (decoded) {
|
||||
// Récupérer l'utilisateur depuis la base de données
|
||||
user = await getUserById(decoded.userId) || null;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Authentication is optional for public procedures.
|
||||
user = null;
|
||||
|
||||
111
server/db.ts
111
server/db.ts
@@ -42,77 +42,72 @@ export async function getDb() {
|
||||
return _db;
|
||||
}
|
||||
|
||||
export async function upsertUser(user: InsertUser): Promise<void> {
|
||||
if (!user.openId) {
|
||||
throw new Error("User openId is required for upsert");
|
||||
/**
|
||||
* Crée un nouvel utilisateur avec email/mot de passe
|
||||
*/
|
||||
export async function createUser(user: InsertUser): Promise<number> {
|
||||
if (!user.email) {
|
||||
throw new Error("User email is required");
|
||||
}
|
||||
if (!user.password) {
|
||||
throw new Error("User password is required");
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
if (!db) {
|
||||
console.warn("[Database] Cannot upsert user: database not available");
|
||||
return;
|
||||
throw new Error("Database not available");
|
||||
}
|
||||
|
||||
try {
|
||||
const values: InsertUser = {
|
||||
openId: user.openId,
|
||||
};
|
||||
const updateSet: Record<string, unknown> = {};
|
||||
|
||||
const textFields = ["name", "email", "loginMethod"] as const;
|
||||
type TextField = (typeof textFields)[number];
|
||||
|
||||
const assignNullable = (field: TextField) => {
|
||||
const value = user[field];
|
||||
if (value === undefined) return;
|
||||
const normalized = value ?? null;
|
||||
values[field] = normalized;
|
||||
updateSet[field] = normalized;
|
||||
};
|
||||
|
||||
textFields.forEach(assignNullable);
|
||||
|
||||
if (user.lastSignedIn !== undefined) {
|
||||
values.lastSignedIn = user.lastSignedIn;
|
||||
updateSet.lastSignedIn = user.lastSignedIn;
|
||||
}
|
||||
if (user.role !== undefined) {
|
||||
values.role = user.role;
|
||||
updateSet.role = user.role;
|
||||
} else if (user.openId === ENV.ownerOpenId) {
|
||||
values.role = 'admin';
|
||||
updateSet.role = 'admin';
|
||||
}
|
||||
|
||||
if (!values.lastSignedIn) {
|
||||
values.lastSignedIn = new Date();
|
||||
}
|
||||
|
||||
if (Object.keys(updateSet).length === 0) {
|
||||
updateSet.lastSignedIn = new Date();
|
||||
}
|
||||
|
||||
await db.insert(users).values(values).onDuplicateKeyUpdate({
|
||||
set: updateSet,
|
||||
});
|
||||
const result = await db.insert(users).values(user);
|
||||
return Number(result[0].insertId);
|
||||
} catch (error) {
|
||||
console.error("[Database] Failed to upsert user:", error);
|
||||
console.error("[Database] Failed to create user:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserByOpenId(openId: string) {
|
||||
/**
|
||||
* Récupère un utilisateur par son email
|
||||
*/
|
||||
export async function getUserByEmail(email: string) {
|
||||
const db = await getDb();
|
||||
if (!db) {
|
||||
console.warn("[Database] Cannot get user: database not available");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1);
|
||||
|
||||
const result = await db.select().from(users).where(eq(users.email, email)).limit(1);
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un utilisateur par son ID
|
||||
*/
|
||||
export async function getUserById(id: number) {
|
||||
const db = await getDb();
|
||||
if (!db) {
|
||||
console.warn("[Database] Cannot get user: database not available");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result = await db.select().from(users).where(eq(users.id, id)).limit(1);
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le dernier login d'un utilisateur
|
||||
*/
|
||||
export async function updateUserLastSignedIn(userId: number): Promise<void> {
|
||||
const db = await getDb();
|
||||
if (!db) {
|
||||
console.warn("[Database] Cannot update user: database not available");
|
||||
return;
|
||||
}
|
||||
|
||||
await db.update(users).set({ lastSignedIn: new Date() }).where(eq(users.id, userId));
|
||||
}
|
||||
|
||||
// ==================== FORMATIONS ====================
|
||||
|
||||
export async function createFormation(data: InsertFormation) {
|
||||
@@ -383,14 +378,6 @@ export async function deleteInscription(id: number) {
|
||||
|
||||
// ==================== GESTION DES UTILISATEURS ====================
|
||||
|
||||
export async function createUser(data: InsertUser) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
const result = await db.insert(users).values(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getAllUsers() {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
@@ -398,14 +385,6 @@ export async function getAllUsers() {
|
||||
return await db.select().from(users);
|
||||
}
|
||||
|
||||
export async function getUserById(id: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return undefined;
|
||||
|
||||
const result = await db.select().from(users).where(eq(users.id, id)).limit(1);
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
export async function updateUser(id: number, data: Partial<InsertUser>) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
@@ -21,6 +21,83 @@ export const appRouter = router({
|
||||
system: systemRouter,
|
||||
auth: router({
|
||||
me: publicProcedure.query(opts => opts.ctx.user),
|
||||
|
||||
register: publicProcedure
|
||||
.input(z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
name: z.string().optional(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { hashPassword, generateToken } = await import("./_core/auth");
|
||||
|
||||
// Vérifier si l'utilisateur existe déjà
|
||||
const existingUser = await db.getUserByEmail(input.email);
|
||||
if (existingUser) {
|
||||
throw new TRPCError({ code: 'CONFLICT', message: 'Un compte existe déjà avec cet email' });
|
||||
}
|
||||
|
||||
// Hasher le mot de passe
|
||||
const hashedPassword = await hashPassword(input.password);
|
||||
|
||||
// Créer l'utilisateur
|
||||
const userId = await db.createUser({
|
||||
email: input.email,
|
||||
password: hashedPassword,
|
||||
name: input.name,
|
||||
emailVerified: false,
|
||||
role: 'user',
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
// Générer un token JWT
|
||||
const token = generateToken(userId);
|
||||
|
||||
// Définir le cookie de session
|
||||
const cookieOptions = getSessionCookieOptions(ctx.req);
|
||||
ctx.res.cookie(COOKIE_NAME, token, cookieOptions);
|
||||
|
||||
return { success: true, userId };
|
||||
}),
|
||||
|
||||
login: publicProcedure
|
||||
.input(z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { verifyPassword, generateToken } = await import("./_core/auth");
|
||||
|
||||
// Récupérer l'utilisateur
|
||||
const user = await db.getUserByEmail(input.email);
|
||||
if (!user) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Email ou mot de passe incorrect' });
|
||||
}
|
||||
|
||||
// Vérifier le compte actif
|
||||
if (!user.isActive) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Votre compte a été désactivé' });
|
||||
}
|
||||
|
||||
// Vérifier le mot de passe
|
||||
const isValid = await verifyPassword(input.password, user.password);
|
||||
if (!isValid) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Email ou mot de passe incorrect' });
|
||||
}
|
||||
|
||||
// Mettre à jour le dernier login
|
||||
await db.updateUserLastSignedIn(user.id);
|
||||
|
||||
// Générer un token JWT
|
||||
const token = generateToken(user.id);
|
||||
|
||||
// Définir le cookie de session
|
||||
const cookieOptions = getSessionCookieOptions(ctx.req);
|
||||
ctx.res.cookie(COOKIE_NAME, token, cookieOptions);
|
||||
|
||||
return { success: true, user: { id: user.id, email: user.email, name: user.name, role: user.role } };
|
||||
}),
|
||||
|
||||
logout: publicProcedure.mutation(({ ctx }) => {
|
||||
const cookieOptions = getSessionCookieOptions(ctx.req);
|
||||
ctx.res.clearCookie(COOKIE_NAME, { ...cookieOptions, maxAge: -1 });
|
||||
|
||||
14
todo.md
14
todo.md
@@ -241,3 +241,17 @@
|
||||
- [x] Ajouter le basculement simulation/production
|
||||
- [x] Ajouter le lien dans le menu de navigation
|
||||
- [x] Tester la configuration et l'envoi d'emails
|
||||
|
||||
## Remplacement OAuth Manus par authentification locale
|
||||
|
||||
- [x] Modifier le schéma de la table users (ajouter password, emailVerified, supprimer openId)
|
||||
- [x] Installer bcrypt pour le hashage des mots de passe
|
||||
- [x] Créer les helpers d'authentification (hashPassword, verifyPassword)
|
||||
- [x] Modifier les procédures auth (login, register, logout)
|
||||
- [x] Modifier le système de context pour utiliser JWT
|
||||
- [x] Créer la page de connexion
|
||||
- [x] Créer la page d'inscription
|
||||
- [ ] Modifier la page de gestion des utilisateurs (ajouter mot de passe)
|
||||
- [ ] Mettre à jour useAuth pour utiliser la nouvelle authentification
|
||||
- [ ] Tester la connexion et l'inscription
|
||||
- [ ] Créer un guide de déploiement sans OAuth
|
||||
|
||||
Reference in New Issue
Block a user