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 AdminEmailTemplates from "./pages/AdminEmailTemplates";
|
||||||
import AdminEmailConfig from "./pages/AdminEmailConfig";
|
import AdminEmailConfig from "./pages/AdminEmailConfig";
|
||||||
import Inscription from "./pages/Inscription";
|
import Inscription from "./pages/Inscription";
|
||||||
|
import Login from "./pages/Login";
|
||||||
|
import Register from "./pages/Register";
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
|
<Route path={"/login"} component={Login} />
|
||||||
|
<Route path={"/register"} component={Register} />
|
||||||
<Route path={"/"} component={Home} />
|
<Route path={"/"} component={Home} />
|
||||||
<Route path={"/admin/rapport-public-cible"} component={AdminRapportPublicCible} />
|
<Route path={"/admin/rapport-public-cible"} component={AdminRapportPublicCible} />
|
||||||
<Route path={"/inscription/:lien"} component={Inscription} />
|
<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";
|
export const APP_LOGO = "https://placehold.co/128x128/E1E7EF/1F2937?text=App";
|
||||||
|
|
||||||
// Generate login URL at runtime so redirect URI reflects the current origin.
|
// Retourne l'URL de la page de connexion locale
|
||||||
export const getLoginUrl = () => {
|
export const getLoginUrl = () => "/login";
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|||||||
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,
|
"when": 1763448844826,
|
||||||
"tag": "0009_polite_senator_kelly",
|
"tag": "0009_polite_senator_kelly",
|
||||||
"breakpoints": true
|
"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.
|
* Use this for relations between tables.
|
||||||
*/
|
*/
|
||||||
id: int("id").autoincrement().primaryKey(),
|
id: int("id").autoincrement().primaryKey(),
|
||||||
/** Manus OAuth identifier (openId) returned from the OAuth callback. Unique per user. */
|
/** Email unique pour la connexion */
|
||||||
openId: varchar("openId", { length: 64 }).notNull().unique(),
|
email: varchar("email", { length: 320 }).notNull().unique(),
|
||||||
|
/** Mot de passe hashé avec bcrypt */
|
||||||
|
password: varchar("password", { length: 255 }).notNull(),
|
||||||
name: text("name"),
|
name: text("name"),
|
||||||
email: varchar("email", { length: 320 }),
|
/** Email vérifié ou non */
|
||||||
loginMethod: varchar("loginMethod", { length: 64 }),
|
emailVerified: boolean("emailVerified").default(false).notNull(),
|
||||||
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
|
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
|
||||||
/** Statut du compte (actif/inactif) */
|
/** Statut du compte (actif/inactif) */
|
||||||
isActive: boolean("isActive").default(true).notNull(),
|
isActive: boolean("isActive").default(true).notNull(),
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"@trpc/react-query": "^11.6.0",
|
"@trpc/react-query": "^11.6.0",
|
||||||
"@trpc/server": "^11.6.0",
|
"@trpc/server": "^11.6.0",
|
||||||
"axios": "^1.12.0",
|
"axios": "^1.12.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -85,6 +86,7 @@
|
|||||||
"@builder.io/vite-plugin-jsx-loc": "^0.1.1",
|
"@builder.io/vite-plugin-jsx-loc": "^0.1.1",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@tailwindcss/vite": "^4.1.3",
|
"@tailwindcss/vite": "^4.1.3",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/express": "4.17.21",
|
"@types/express": "4.17.21",
|
||||||
"@types/google.maps": "^3.58.1",
|
"@types/google.maps": "^3.58.1",
|
||||||
"@types/node": "^24.7.0",
|
"@types/node": "^24.7.0",
|
||||||
|
|||||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@@ -115,6 +115,9 @@ importers:
|
|||||||
axios:
|
axios:
|
||||||
specifier: ^1.12.0
|
specifier: ^1.12.0
|
||||||
version: 1.12.2
|
version: 1.12.2
|
||||||
|
bcrypt:
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -227,6 +230,9 @@ importers:
|
|||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.1.3
|
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))
|
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':
|
'@types/express':
|
||||||
specifier: 4.17.21
|
specifier: 4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
@@ -2157,6 +2163,9 @@ packages:
|
|||||||
'@types/babel__traverse@7.28.0':
|
'@types/babel__traverse@7.28.0':
|
||||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
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':
|
'@types/body-parser@1.19.6':
|
||||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||||
|
|
||||||
@@ -2443,6 +2452,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==}
|
resolution: {integrity: sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
bcrypt@6.0.0:
|
||||||
|
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
body-parser@1.20.3:
|
body-parser@1.20.3:
|
||||||
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
|
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
|
||||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
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: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
react-dom: ^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:
|
node-domexception@1.0.0:
|
||||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
engines: {node: '>=10.5.0'}
|
engines: {node: '>=10.5.0'}
|
||||||
@@ -3716,6 +3733,10 @@ packages:
|
|||||||
encoding:
|
encoding:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
node-gyp-build@4.8.4:
|
||||||
|
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
node-releases@2.0.23:
|
node-releases@2.0.23:
|
||||||
resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==}
|
resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==}
|
||||||
|
|
||||||
@@ -6641,6 +6662,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.28.4
|
'@babel/types': 7.28.4
|
||||||
|
|
||||||
|
'@types/bcrypt@6.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.7.0
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
@@ -6973,6 +6998,11 @@ snapshots:
|
|||||||
|
|
||||||
baseline-browser-mapping@2.8.12: {}
|
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:
|
body-parser@1.20.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
@@ -8496,12 +8526,16 @@ snapshots:
|
|||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
react-dom: 19.2.0(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-domexception@1.0.0: {}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
|
|
||||||
|
node-gyp-build@4.8.4: {}
|
||||||
|
|
||||||
node-releases@2.0.23: {}
|
node-releases@2.0.23: {}
|
||||||
|
|
||||||
normalize-range@0.1.2: {}
|
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 { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
||||||
import type { User } from "../../drizzle/schema";
|
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 = {
|
export type TrpcContext = {
|
||||||
req: CreateExpressContextOptions["req"];
|
req: CreateExpressContextOptions["req"];
|
||||||
@@ -14,7 +16,18 @@ export async function createContext(
|
|||||||
let user: User | null = null;
|
let user: User | null = null;
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
// Authentication is optional for public procedures.
|
// Authentication is optional for public procedures.
|
||||||
user = null;
|
user = null;
|
||||||
|
|||||||
111
server/db.ts
111
server/db.ts
@@ -42,77 +42,72 @@ export async function getDb() {
|
|||||||
return _db;
|
return _db;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function upsertUser(user: InsertUser): Promise<void> {
|
/**
|
||||||
if (!user.openId) {
|
* Crée un nouvel utilisateur avec email/mot de passe
|
||||||
throw new Error("User openId is required for upsert");
|
*/
|
||||||
|
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();
|
const db = await getDb();
|
||||||
if (!db) {
|
if (!db) {
|
||||||
console.warn("[Database] Cannot upsert user: database not available");
|
throw new Error("Database not available");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const values: InsertUser = {
|
const result = await db.insert(users).values(user);
|
||||||
openId: user.openId,
|
return Number(result[0].insertId);
|
||||||
};
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Database] Failed to upsert user:", error);
|
console.error("[Database] Failed to create user:", error);
|
||||||
throw 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();
|
const db = await getDb();
|
||||||
if (!db) {
|
if (!db) {
|
||||||
console.warn("[Database] Cannot get user: database not available");
|
console.warn("[Database] Cannot get user: database not available");
|
||||||
return undefined;
|
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;
|
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 ====================
|
// ==================== FORMATIONS ====================
|
||||||
|
|
||||||
export async function createFormation(data: InsertFormation) {
|
export async function createFormation(data: InsertFormation) {
|
||||||
@@ -383,14 +378,6 @@ export async function deleteInscription(id: number) {
|
|||||||
|
|
||||||
// ==================== GESTION DES UTILISATEURS ====================
|
// ==================== 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() {
|
export async function getAllUsers() {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
if (!db) return [];
|
if (!db) return [];
|
||||||
@@ -398,14 +385,6 @@ export async function getAllUsers() {
|
|||||||
return await db.select().from(users);
|
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>) {
|
export async function updateUser(id: number, data: Partial<InsertUser>) {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
if (!db) throw new Error("Database not available");
|
if (!db) throw new Error("Database not available");
|
||||||
|
|||||||
@@ -21,6 +21,83 @@ export const appRouter = router({
|
|||||||
system: systemRouter,
|
system: systemRouter,
|
||||||
auth: router({
|
auth: router({
|
||||||
me: publicProcedure.query(opts => opts.ctx.user),
|
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 }) => {
|
logout: publicProcedure.mutation(({ ctx }) => {
|
||||||
const cookieOptions = getSessionCookieOptions(ctx.req);
|
const cookieOptions = getSessionCookieOptions(ctx.req);
|
||||||
ctx.res.clearCookie(COOKIE_NAME, { ...cookieOptions, maxAge: -1 });
|
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 basculement simulation/production
|
||||||
- [x] Ajouter le lien dans le menu de navigation
|
- [x] Ajouter le lien dans le menu de navigation
|
||||||
- [x] Tester la configuration et l'envoi d'emails
|
- [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