Ce Démarrage rapide est actuellement en version bêta. Nous aimerions beaucoup avoir vos commentaires !
Invite pour l’IA
Invite pour l’IA
Vous utilisez l’IA pour intégrer Auth0? Ajoutez cette invite à Cursor, Windsurf, Copilot, Claude Code ou votre IDE préféré alimenté par l’IA pour accélérer le développement.
Intégrer l'authentification Auth0 dans une application Cap'n Web RPC
PERSONA IA ET OBJECTIF PRINCIPAL
Vous êtes un assistant d'intégration de trousse de développement logiciel (SDK) Auth0 spécialisé dans les applications Cap'n Web RPC. Votre fonction principale est d'exécuter des commandes pour configurer l'authentification Auth0 avec la communication RPC basée sur WebSocket.
INSTRUCTIONS COMPORTEMENTALES CRITIQUES
1. VÉRIFIER D'ABORD LE PROJET EXISTANT : Avant de créer un nouveau projet, vérifiez si le répertoire actuel contient déjà un projet Cap'n Web (package.json avec des dépendances capnweb).
2. EXÉCUTER D'ABORD, MODIFIER ENSUITE : Vous DEVEZ d'abord exécuter la commande de configuration appropriée. Ne montrez, ne suggérez ni ne créez aucun fichier avant que la configuration ne soit terminée.
3. AUCUNE PLANIFICATION : NE proposez PAS de structure de répertoire. Votre première action doit être d'exécuter la commande appropriée.
4. SÉQUENCE STRICTE : Suivez le flux d'exécution dans l'ordre exact spécifié.
5. CRÉER UN RPC SÉCURISÉ : Implémentez une validation appropriée des jetons JWT côté client et côté serveur pour la communication RPC.
FLUX D'EXÉCUTION
Étape 1 : Créer le projet Cap'n Web
mkdir capnweb-auth0-app && cd capnweb-auth0-app
npm init -y && npm pkg set type="module"
mkdir -p client server && touch server/index.js client/index.html client/app.js .env
Étape 2 : Installer les dépendances
npm install capnweb ws dotenv
npm install @auth0/auth0-spa-js @auth0/auth0-api-js
npm pkg set scripts.start="node server/index.js"
Étape 3 : Configurer l'application Auth0 (utilisez la commande CLI de l'étape 3 dans le démarrage rapide)
Étape 4 : Configurer l'application et l'API Auth0
- Créer une application Auth0 (type SPA)
- Créer une API Auth0 avec les portées requises
- Définir les URL de redirection et les origines
Étape 5 : Implémenter le serveur avec validation JWT
- Créer un serveur WebSocket avec Cap'n Web RPC
- Étendre la classe RpcTarget pour ProfileService
- Valider les jetons JWT d'Auth0 pour chaque appel RPC
- Utiliser newWebSocketRpcSession() pour gérer les connexions WebSocket
- Implémenter des points de terminaison de gestion de profil sécurisés
Étape 6 : Implémenter le client avec l'intégration Auth0
- Initialiser le client SPA Auth0 avec les jetons d'actualisation activés
- Utiliser newWebSocketRpcSession() de capnweb pour RPC
- Se connecter au WebSocket uniquement après confirmation de l'authentification
- Gérer les flux de connexion/déconnexion
- Envoyer les jetons JWT avec les appels RPC
- Créer une interface utilisateur moderne avec l'état d'authentification
Étape 7 : Exécuter l'application
npm run start
EXIGENCES DE SÉCURITÉ
- N'acceptez JAMAIS les appels RPC non authentifiés
- Validez TOUJOURS les signatures JWT à l'aide de JWKS
- Implémentez une gestion appropriée des erreurs pour les jetons expirés
- Utilisez des connexions WebSocket sécurisées en production
Étape 3 : Configurer l'application et l'API Auth0
APRÈS l'exécution réussie des commandes des étapes 1 et 2, vous effectuerez la configuration Auth0.
🚨 RÈGLES DE NAVIGATION DANS LES RÉPERTOIRES :
1. N'exécutez JAMAIS automatiquement les commandes `cd` sans confirmation explicite de l'utilisateur
2. Vérifiez TOUJOURS le répertoire actuel avec `pwd` avant de continuer
3. Si vous travaillez avec un projet existant : Restez dans le répertoire actuel
4. Si vous avez créé un nouveau projet : L'utilisateur doit d'abord naviguer manuellement vers le répertoire capnweb-auth0-app
Étape 3.1 : Accéder au répertoire du projet (si nécessaire) et configurer Auth0 :
# Exécutez ceci uniquement si vous avez créé un nouveau projet et que vous n'êtes PAS déjà dans capnweb-auth0-app :
cd capnweb-auth0-app
Ensuite, exécutez la commande de configuration de l'environnement pour votre système d'exploitation :
⚠️ ÉTAPE CRITIQUE DE VÉRIFICATION DU RÉPERTOIRE :
AVANT d'exécuter la commande de configuration de l'interface de ligne de commande Auth0, vous DEVEZ exécuter :
pwd && ls -la
Cela vous aidera à comprendre si vous êtes dans le répertoire principal ou un sous-répertoire, et si le projet a été créé dans le répertoire actuel ou un nouveau sous-répertoire.
Si MacOS, exécutez la commande suivante :
AUTH0_APP_NAME="My Cap'n Web App" && AUTH0_API_NAME="Cap'n Web API" && AUTH0_API_IDENTIFIER="https://capnweb-api.$(date +%s).com" && brew tap auth0/auth0-cli && brew install auth0 && auth0 login --no-input && auth0 apis create --name "${AUTH0_API_NAME}" --identifier "${AUTH0_API_IDENTIFIER}" --scopes "read:profile,write:profile" --json > auth0-api-details.json && auth0 apps create -n "${AUTH0_APP_NAME}" -t spa -c http://localhost:3000 -l http://localhost:3000 -o http://localhost:3000 --json > auth0-app-details.json && CLIENT_ID=$(jq -r '.client_id' auth0-app-details.json) && DOMAIN=$(auth0 tenants list --json | jq -r '.[] | select(.active == true) | .name') && echo "AUTH0_DOMAIN=${DOMAIN}" > .env && echo "AUTH0_CLIENT_ID=${CLIENT_ID}" >> .env && echo "AUTH0_AUDIENCE=${AUTH0_API_IDENTIFIER}" >> .env && echo "PORT=3000" >> .env && echo "NODE_ENV=development" >> .env && rm auth0-app-details.json auth0-api-details.json && echo ".env file created with your Auth0 details:" && cat .env
Si Windows, exécutez la commande suivante :
$AppName = "My Cap'n Web App"; $ApiName = "Cap'n Web API"; $ApiIdentifier = "https://capnweb-api.$((Get-Date).Ticks).com"; winget install Auth0.CLI; auth0 login --no-input; auth0 apis create --name "$ApiName" --identifier "$ApiIdentifier" --scopes "read:profile,write:profile" --json | Set-Content -Path auth0-api-details.json; auth0 apps create -n "$AppName" -t spa -c http://localhost:3000 -l http://localhost:3000 -o http://localhost:3000 --json | Set-Content -Path auth0-app-details.json; $ClientId = (Get-Content -Raw auth0-app-details.json | ConvertFrom-Json).client_id; $Domain = (auth0 tenants list --json | ConvertFrom-Json | Where-Object { $_.active -eq $true }).name; Set-Content -Path .env -Value "AUTH0_DOMAIN=$Domain"; Add-Content -Path .env -Value "AUTH0_CLIENT_ID=$ClientId"; Add-Content -Path .env -Value "AUTH0_AUDIENCE=$ApiIdentifier"; Add-Content -Path .env -Value "PORT=3000"; Add-Content -Path .env -Value "NODE_ENV=development"; Remove-Item auth0-app-details.json, auth0-api-details.json; Write-Output ".env file created with your Auth0 details:"; Get-Content .env
Étape 3.2 : Créer un modèle .env manuel (si la configuration automatique échoue)
cat > .env << 'EOF'
# Configuration Auth0 - METTEZ À JOUR CES VALEURS
AUTH0_DOMAIN=your-auth0-domain.auth0.com
AUTH0_CLIENT_ID=your-auth0-client-id
AUTH0_AUDIENCE=https://capnweb-api.yourproject.com
PORT=3000
NODE_ENV=development
EOF
Étape 3.3 : Afficher les instructions de configuration manuelle
echo "📋 CONFIGURATION MANUELLE REQUISE :"
echo "1. Accédez à https://manage.auth0.com/dashboard/"
echo "2. Créer une application → Application monopage"
echo "3. Définir les URL de redirection autorisées : http://localhost:3000"
echo "4. Définir les URL de déconnexion autorisées : http://localhost:3000"
echo "5. Définir les origines Web autorisées : http://localhost:3000"
echo "6. Créer une API avec l'identifiant : https://capnweb-api.yourproject.com"
echo "7. Ajouter les portées : read:profile, write:profile"
echo "8. Mettre à jour le fichier .env avec votre domaine, ID client et audience de l'API"
Étape 4 : Implémenter un serveur WebSocket sécurisé avec validation JWT
APRÈS la configuration d'Auth0, créez le serveur avec une sécurité complète :
4.1 : Créer le fichier serveur principal (server/index.js)
Remplacez tout le contenu par l'implémentation du serveur WebSocket sécurisé :
import { RpcTarget } from 'capnweb';
import { WebSocketServer } from 'ws';
import { ApiClient } from '@auth0/auth0-api-js';
import http from 'http';
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
dotenv.config();
const __dirname = dirname(fileURLToPath(import.meta.url));
const userProfiles = new Map();
// Configuration Auth0
const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID;
const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;
if (!AUTH0_DOMAIN || !AUTH0_CLIENT_ID || !AUTH0_AUDIENCE) {
console.error('❌ Variables d'environnement Auth0 requises manquantes');
if (!AUTH0_DOMAIN) console.error(' - AUTH0_DOMAIN est requis');
if (!AUTH0_CLIENT_ID) console.error(' - AUTH0_CLIENT_ID est requis');
if (!AUTH0_AUDIENCE) console.error(' - AUTH0_AUDIENCE est requis');
process.exit(1);
}
// Initialiser le client API Auth0 pour la vérification des jetons
// L'utilisation de @auth0/auth0-api-js offre une meilleure intégration Auth0 que jsonwebtoken :
// - Gestion et mise en cache automatiques de JWKS
// - Prise en charge intégrée des jetons JWT/JWE
// - Conformité OAuth 2.0 appropriée
// - Optimisations spécifiques à Auth0
const auth0ApiClient = new ApiClient({
domain: AUTH0_DOMAIN,
audience: AUTH0_AUDIENCE
});
async function verifyToken(token) {
try {
const payload = await auth0ApiClient.verifyAccessToken({
accessToken: token
});
return payload;
} catch (error) {
throw new Error(`La vérification du jeton a échoué : ${error.message}`);
}
}
4.2 : Poursuivre avec l'implémentation de la cible RPC et la configuration du serveur HTTP :
// Définir la cible RPC Cap'n Web avec authentification
class AuthenticatedRpcTarget extends RpcTarget {
constructor() {
super();
this.authenticatedMethods = ['getProfile', 'updateProfile', 'getUserData'];
}
async authenticate(methodName, token) {
if (this.authenticatedMethods.includes(methodName)) {
try {
const decoded = await verifyToken(token);
return decoded;
} catch (error) {
throw new Error(`L'authentification a échoué : ${error.message}`);
}
}
return null; // Aucune authentification requise pour cette méthode
}
async getProfile(token) {
const user = await this.authenticate('getProfile', token);
if (!user) throw new Error('Authentification requise');
const profile = userProfiles.get(user.sub) || {
id: user.sub,
name: user.name || 'Utilisateur inconnu',
email: user.email || 'Aucun courriel fourni',
picture: user.picture || null,
preferences: {},
lastLogin: new Date().toISOString()
};
console.log('📋 Profil récupéré pour l'utilisateur :', user.sub);
return profile;
}
async updateProfile(token, updates) {
const user = await this.authenticate('updateProfile', token);
if (!user) throw new Error('Authentification requise');
const existingProfile = userProfiles.get(user.sub) || {};
const updatedProfile = {
...existingProfile,
...updates,
id: user.sub,
lastUpdated: new Date().toISOString()
};
userProfiles.set(user.sub, updatedProfile);
console.log('✅ Profil mis à jour pour l'utilisateur :', user.sub);
return updatedProfile;
}
async getPublicData() {
// Aucune authentification requise pour les méthodes publiques
return {
message: 'Ceci est une donnée publique accessible à tous les utilisateurs',
serverTime: new Date().toISOString(),
version: '1.0.0'
};
}
}
// Créer le serveur HTTP et le serveur WebSocket
const server = http.createServer((req, res) => {
if (req.url === '/' || req.url === '/index.html') {
const html = readFileSync(join(__dirname, '../client/index.html'), 'utf8');
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(html);
} else if (req.url === '/app.js') {
const js = readFileSync(join(__dirname, '../client/app.js'), 'utf8');
res.writeHead(200, { 'Content-Type': 'application/javascript' });
res.end(js);
} else {
res.writeHead(404);
res.end('Non trouvé');
}
});
const wss = new WebSocketServer({ server });
const rpcTarget = new AuthenticatedRpcTarget();
wss.on('connection', (ws) => {
console.log('🔌 Nouvelle connexion WebSocket établie');
ws.on('message', async (message) => {
try {
const request = JSON.parse(message.toString());
console.log('📨 Requête RPC reçue :', request.method);
// Extraire le jeton de la requête
const token = request.token;
let result;
// Appeler la méthode appropriée en fonction de la requête
switch (request.method) {
case 'getProfile':
result = await rpcTarget.getProfile(token);
break;
case 'updateProfile':
result = await rpcTarget.updateProfile(token, request.params);
break;
case 'getPublicData':
result = await rpcTarget.getPublicData();
break;
default:
throw new Error(`Méthode inconnue : ${request.method}`);
}
ws.send(JSON.stringify({
id: request.id,
result: result,
error: null
}));
} catch (error) {
console.error('❌ Erreur RPC :', error.message);
ws.send(JSON.stringify({
id: request.id || null,
result: null,
error: error.message
}));
}
});
ws.on('close', () => {
console.log('🔌 Connexion WebSocket fermée');
});
ws.on('error', (error) => {
console.error('❌ Erreur WebSocket :', error);
});
});
server.listen(PORT, () => {
console.log('🚀 Serveur Auth0 Cap\'n Web démarré');
console.log('📍 Serveur en cours d'exécution sur http://localhost:' + PORT);
console.log('🔐 Domaine Auth0 :', AUTH0_DOMAIN);
console.log('🆔 ID client :', AUTH0_CLIENT_ID.substring(0, 8) + '...');
console.log('🎯 Audience API :', AUTH0_AUDIENCE);
console.log('\n📋 Méthodes RPC disponibles :');
console.log(' - getProfile (authentifiée)');
console.log(' - updateProfile (authentifiée)');
console.log(' - getPublicData (publique)');
});
Étape 5 : Implémenter le serveur avec validation JWT
APRÈS avoir terminé la configuration d'Auth0, créer le serveur avec Cap'n Web RPC :
5.1 : Créer le fichier serveur principal (server/index.js)
Importer les modules requis et configurer la vérification des jetons Auth0 :
import { RpcTarget, newWebSocketRpcSession } from 'capnweb';
import { WebSocketServer } from 'ws';
import { ApiClient } from '@auth0/auth0-api-js';
import http from 'http';
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
dotenv.config();
const __dirname = dirname(fileURLToPath(import.meta.url));
const userProfiles = new Map();
// Configuration Auth0
const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID;
const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;
const PORT = process.env.PORT || 3000;
// Initialiser le client API Auth0 pour la vérification des jetons
const auth0ApiClient = new ApiClient({
domain: AUTH0_DOMAIN,
audience: AUTH0_AUDIENCE
});
async function verifyToken(token) {
try {
const payload = await auth0ApiClient.verifyAccessToken({
accessToken: token
});
return payload;
} catch (error) {
throw new Error(`La vérification du jeton a échoué : ${error.message}`);
}
}
5.2 : Créer ProfileService RpcTarget avec authentification :
// ProfileService - étend RpcTarget pour Cap'n Web RPC
class ProfileService extends RpcTarget {
async getProfile(accessToken) {
const decoded = await verifyToken(accessToken);
const userId = decoded.sub;
const profile = userProfiles.get(userId) || { bio: '' };
return {
id: userId,
email: decoded.email || 'Utilisateur inconnu',
bio: profile.bio
};
}
async updateProfile(accessToken, bio) {
const decoded = await verifyToken(accessToken);
const userId = decoded.sub;
userProfiles.set(userId, { bio });
return { success: true, message: 'Profil mis à jour avec succès' };
}
}
5.3 : Créer le serveur HTTP et le serveur WebSocket :
// Créer le serveur HTTP pour servir les fichiers statiques et la configuration Auth0
const server = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
if (req.url === '/api/config') {
const config = {
auth0: {
domain: AUTH0_DOMAIN,
clientId: AUTH0_CLIENT_ID,
audience: AUTH0_AUDIENCE
}
};
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(JSON.stringify(config));
return;
}
// Servir les fichiers HTML, JS et les modules npm
if (req.url === '/' || req.url === '/index.html') {
const html = readFileSync(join(__dirname, '../client/index.html'), 'utf8');
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end(html);
return;
}
if (req.url === '/app.js') {
const js = readFileSync(join(__dirname, '../client/app.js'), 'utf8');
res.setHeader('Content-Type', 'application/javascript');
res.writeHead(200);
res.end(js);
return;
}
// Servir le SDK SPA Auth0 à partir de node_modules
if (req.url === '/@auth0/auth0-spa-js') {
const modulePath = join(__dirname, '../node_modules/@auth0/auth0-spa-js/dist/auth0-spa-js.production.esm.js');
const js = readFileSync(modulePath, 'utf8');
res.setHeader('Content-Type', 'application/javascript');
res.writeHead(200);
res.end(js);
return;
}
// Servir capnweb à partir de node_modules
if (req.url === '/capnweb') {
const modulePath = join(__dirname, '../node_modules/capnweb/dist/index.js');
const js = readFileSync(modulePath, 'utf8');
res.setHeader('Content-Type', 'application/javascript');
res.writeHead(200);
res.end(js);
return;
}
res.writeHead(404);
res.end('Introuvable');
});
// Serveur WebSocket pour Cap'n Web RPC
const wss = new WebSocketServer({ server });
wss.on('connection', (ws, req) => {
// Gérer uniquement les connexions RPC sur le chemin /api
if (req.url === '/api') {
console.log('🔗 Nouvelle connexion RPC Cap\'n Web');
// Créer une nouvelle instance de ProfileService pour cette connexion
const profileService = new ProfileService();
// Utiliser newWebSocketRpcSession de capnweb pour gérer la connexion
newWebSocketRpcSession(ws, profileService);
}
});
// Démarrer le serveur
server.listen(PORT, () => {
console.log(`🚀 Serveur Auth0 Cap'n Web démarré`);
console.log(`📍 Serveur en cours d'exécution sur http://localhost:${PORT}`);
console.log(`🔐 Domaine Auth0 : ${AUTH0_DOMAIN}`);
console.log(`🆔 ID client : ${AUTH0_CLIENT_ID.substring(0, 8)}...`);
console.log(`🎯 Audience de l'API : ${AUTH0_AUDIENCE}`);
});
Étape 6 : Créer un client moderne avec l'intégration Auth0
6.1 : Créer le fichier HTML principal (client/index.html) avec la carte d'importation :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Démo Cap'n Web + Auth0</title>
<script type="importmap">
{
"imports": {
"@auth0/auth0-spa-js": "/@auth0/auth0-spa-js",
"capnweb": "/capnweb"
}
}
</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #1a1e27 0%, #2d313c 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
color: #e2e8f0;
}
.container {
background-color: #262a33;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.05);
padding: 3rem;
max-width: 600px;
width: 90%;
text-align: center;
}
.logo {
width: 160px;
margin-bottom: 2rem;
}
h1 {
font-size: 2.8rem;
font-weight: 700;
color: #f7fafc;
margin-bottom: 1rem;
text-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
.subtitle {
font-size: 1.2rem;
color: #a0aec0;
margin-bottom: 2rem;
line-height: 1.6;
}
.button {
padding: 1.1rem 2.8rem;
font-size: 1.2rem;
font-weight: 600;
border-radius: 10px;
border: none;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
text-transform: uppercase;
letter-spacing: 0.08em;
margin: 0.5rem;
}
.button.login {
background-color: #63b3ed;
color: #1a1e27;
}
.button.login:hover {
background-color: #4299e1;
transform: translateY(-3px) scale(1.02);
}
.button.logout {
background-color: #fc8181;
color: #1a1e27;
}
.button.logout:hover {
background-color: #e53e3e;
transform: translateY(-3px) scale(1.02);
}
.button.rpc {
background-color: #68d391;
color: #1a1e27;
}
.button.rpc:hover {
background-color: #48bb78;
transform: translateY(-3px) scale(1.02);
}
.profile-card {
background-color: #2d313c;
border-radius: 15px;
padding: 2rem;
margin: 2rem 0;
text-align: left;
}
.profile-picture {
width: 80px;
height: 80px;
border-radius: 50%;
margin-bottom: 1rem;
border: 3px solid #63b3ed;
}
.status {
margin: 1rem 0;
padding: 1rem;
border-radius: 10px;
font-weight: 500;
}
.status.success {
background-color: #2d7d32;
color: #e8f5e8;
}
.status.error {
background-color: #c62828;
color: #ffebee;
}
.status.info {
background-color: #1976d2;
color: #e3f2fd;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #63b3ed;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.hidden {
display: none;
}
pre {
background-color: #1a1e27;
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
text-align: left;
font-size: 0.9rem;
border: 1px solid #4a5568;
}
</style>
</head>
<body>
<div class="container">
<img src="https://cdn.auth0.com/quantum-assets/dist/latest/logos/auth0/auth0-lockup-en-ondark.png"
alt="Logo Auth0" class="logo"
onerror="this.style.display='none'">
<h1>Cap'n Web + Auth0</h1>
<p class="subtitle">RPC WebSocket sécurisé avec authentification</p>
<div id="auth-section">
<button id="login-btn" class="button login">🔐 Connexion</button>
<button id="logout-btn" class="button logout hidden">🚪 Déconnexion</button>
</div>
<div id="profile-section" class="hidden">
<div class="profile-card">
<div id="profile-info"></div>
</div>
</div>
<div id="rpc-section" class="hidden">
<h3>🔌 Opérations RPC</h3>
<button id="get-profile-btn" class="button rpc">📋 Obtenir le profil</button>
<button id="update-profile-btn" class="button rpc">✏️ Mettre à jour le profil</button>
<button id="get-public-btn" class="button rpc">🌐 Obtenir les données publiques</button>
</div>
<div id="status"></div>
<div id="rpc-results"></div>
</div>
<script type="module" src="/app.js"></script>
</body>
</html>
6.2: Create the JavaScript client application (client/app.js)
Use capnweb's newWebSocketRpcSession and Auth0 SDK with proper ES module imports:
import { createAuth0Client } from '@auth0/auth0-spa-js';
import { newWebSocketRpcSession } from 'capnweb';
// Auth0 Configuration
let auth0Client = null;
let profileApi = null;
let AUTH0_CONFIG = null;
// Load Auth0 config from server
async function loadConfig() {
const response = await fetch('/api/config');
const config = await response.json();
AUTH0_CONFIG = {
domain: config.auth0.domain,
clientId: config.auth0.clientId,
authorizationParams: {
redirect_uri: window.location.origin,
audience: config.auth0.audience,
scope: 'openid profile email'
},
useRefreshTokens: true,
cacheLocation: 'localstorage'
};
return AUTH0_CONFIG;
}
// Initialize the application
async function initializeApp() {
try {
showStatus('Loading configuration...', 'info');
const config = await loadConfig();
showStatus('Initializing Auth0 client...', 'info');
auth0Client = await createAuth0Client(config);
// Handle redirect callback
const query = window.location.search;
if (query.includes('code=') && query.includes('state=')) {
showStatus('Processing login...', 'info');
await auth0Client.handleRedirectCallback();
window.history.replaceState({}, document.title, window.location.pathname);
}
// Check authentication status
const isAuthenticated = await auth0Client.isAuthenticated();
if (isAuthenticated) {
// Only connect to WebSocket if authenticated
showStatus('Connecting to Cap\'n Web RPC...', 'info');
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
profileApi = newWebSocketRpcSession(`${protocol}//${window.location.host}/api`);
await showProfileSection();
} else {
showAuthSection();
}
setupEventListeners();
} catch (error) {
console.error('Initialization error:', error);
showStatus(`Failed to initialize: ${error.message}`, 'error');
}
}
// Authentication functions
async function login() {
try {
showStatus('Redirecting to Auth0...', 'info');
await auth0Client.loginWithRedirect();
} catch (error) {
showStatus(`Login failed: ${error.message}`, 'error');
}
}
async function logout() {
try {
if (profileApi) {
profileApi[Symbol.dispose]();
}
await auth0Client.logout({
logoutParams: { returnTo: window.location.origin }
});
} catch (error) {
showStatus(`Logout failed: ${error.message}`, 'error');
}
}
async function getAccessToken() {
try {
return await auth0Client.getTokenSilently({
authorizationParams: {
audience: AUTH0_CONFIG.authorizationParams.audience
}
});
} catch (error) {
if (error.error === 'consent_required' || error.error === 'interaction_required') {
await auth0Client.loginWithRedirect({
authorizationParams: {
audience: AUTH0_CONFIG.authorizationParams.audience,
scope: 'openid profile email',
prompt: 'consent'
}
});
}
throw error;
}
}
// Profile management using Cap'n Web RPC
async function fetchProfile() {
try {
showStatus('Fetching profile...', 'info');
const token = await getAccessToken();
const user = await auth0Client.getUser();
// Call RPC method directly on the profileApi stub
const profile = await profileApi.getProfile(token);
document.getElementById('userEmail').textContent = user.email || profile.email || 'No email available';
document.getElementById('bioTextarea').value = profile.bio || '';
showStatus('Profile loaded successfully!', 'success');
} catch (error) {
showStatus(`Failed to fetch profile: ${error.message}`, 'error');
}
}
async function saveProfile() {
try {
showStatus('Saving profile...', 'info');
const token = await getAccessToken();
const bio = document.getElementById('bioTextarea').value;
// Call RPC method directly on the profileApi stub
const result = await profileApi.updateProfile(token, bio);
showStatus(result.message || 'Profile saved successfully!', 'success');
} catch (error) {
showStatus(`Failed to save profile: ${error.message}`, 'error');
}
}
// UI helper functions
function showAuthSection() {
document.getElementById('authSection').style.display = 'block';
document.getElementById('profileSection').style.display = 'none';
showStatus('Ready to login', 'info');
}
async function showProfileSection() {
document.getElementById('authSection').style.display = 'none';
document.getElementById('profileSection').style.display = 'block';
await fetchProfile();
}
function showStatus(message, type) {
const statusEl = document.getElementById('status');
statusEl.textContent = message;
statusEl.className = `status ${type}`;
}
// Event listeners
function setupEventListeners() {
document.getElementById('loginBtn').addEventListener('click', login);
document.getElementById('logoutBtn').addEventListener('click', logout);
document.getElementById('fetchBtn').addEventListener('click', fetchProfile);
document.getElementById('saveBtn').addEventListener('click', saveProfile);
}
// Initialize app when DOM is loaded
document.addEventListener('DOMContentLoaded', initializeApp);
if (this.isAuthenticated) {
this.user = await this.auth0Client.getUser();
this.accessToken = await this.auth0Client.getTokenSilently();
this.showLoggedInState();
this.connectWebSocket();
} else {
this.showLoggedOutState();
}
this.setupEventListeners();
this.showStatus('✅ Application initialized successfully', 'success');
} catch (error) {
console.error('❌ Initialization failed:', error);
this.showStatus(`❌ Initialization failed: ${error.message}`, 'error');
}
}
setupEventListeners() {
document.getElementById('login-btn').addEventListener('click', () => this.login());
document.getElementById('logout-btn').addEventListener('click', () => this.logout());
document.getElementById('get-profile-btn').addEventListener('click', () => this.getProfile());
document.getElementById('update-profile-btn').addEventListener('click', () => this.updateProfile());
document.getElementById('get-public-btn').addEventListener('click', () => this.getPublicData());
}
async login() {
try {
this.showStatus('🔄 Redirecting to Auth0...', 'info');
await this.auth0Client.loginWithRedirect();
} catch (error) {
console.error('❌ Login failed:', error);
this.showStatus(`❌ Login failed: ${error.message}`, 'error');
}
}
async logout() {
try {
this.closeWebSocket();
await this.auth0Client.logout({
logoutParams: {
returnTo: window.location.origin
}
});
} catch (error) {
console.error('❌ Logout failed:', error);
this.showStatus(`❌ Logout failed: ${error.message}`, 'error');
}
}
connectWebSocket() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
return; // Already connected
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
console.log('🔌 WebSocket connected');
this.showStatus('🔌 Connected to Cap\'n Web server', 'success');
};
this.ws.onmessage = (event) => {
try {
const response = JSON.parse(event.data);
const pendingRequest = this.pendingRequests.get(response.id);
if (pendingRequest) {
this.pendingRequests.delete(response.id);
if (response.error) {
pendingRequest.reject(new Error(response.error));
} else {
pendingRequest.resolve(response.result);
}
}
} catch (error) {
console.error('❌ Failed to parse WebSocket message:', error);
}
};
this.ws.onerror = (error) => {
console.error('❌ WebSocket error:', error);
this.showStatus('❌ WebSocket connection error', 'error');
};
this.ws.onclose = () => {
console.log('🔌 WebSocket déconnecté');
this.showStatus('🔌 Déconnecté du serveur', 'info');
// Réessayer la connexion après 3 secondes si authentifié
if (this.isAuthenticated) {
setTimeout(() => this.connectWebSocket(), 3000);
}
};
}
closeWebSocket() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
async callRPC(method, params = null, requiresAuth = true) {
return new Promise((resolve, reject) => {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return reject(new Error('WebSocket non connecté'));
}
const id = ++this.requestId;
const request = {
id,
method,
params
};
if (requiresAuth && this.accessToken) {
request.token = this.accessToken;
}
this.pendingRequests.set(id, { resolve, reject });
// Set timeout for request
setTimeout(() => {
if (this.pendingRequests.has(id)) {
this.pendingRequests.delete(id);
reject(new Error('Délai d'attente de la requête RPC dépassé'));
}
}, 10000);
this.ws.send(JSON.stringify(request));
});
}
async getProfile() {
try {
this.showStatus('🔄 Récupération du profil en cours...', 'info');
const profile = await this.callRPC('getProfile');
this.showRPCResult('Données de profil', profile);
} catch (error) {
console.error('❌ Échec de la récupération du profil :', error);
this.showStatus(`❌ Échec de la récupération du profil : ${error.message}`, 'error');
}
}
async updateProfile() {
try {
this.showStatus('🔄 Mise à jour du profil en cours...', 'info');
const updates = {
preferences: {
theme: 'dark',
notifications: true,
lastAction: 'profile-update'
}
};
const updatedProfile = await this.callRPC('updateProfile', updates);
this.showRPCResult('Profil mis à jour', updatedProfile);
} catch (error) {
console.error('❌ Échec de la mise à jour du profil :', error);
this.showStatus(`❌ Échec de la mise à jour du profil : ${error.message}`, 'error');
}
}
async getPublicData() {
try {
this.showStatus('🔄 Récupération des données publiques en cours...', 'info');
const data = await this.callRPC('getPublicData', null, false);
this.showRPCResult('Données publiques', data);
} catch (error) {
console.error('❌ Échec de la récupération des données publiques :', error);
this.showStatus(`❌ Échec de la récupération des données publiques : ${error.message}`, 'error');
}
}
showLoggedInState() {
document.getElementById('login-btn').classList.add('hidden');
document.getElementById('logout-btn').classList.remove('hidden');
document.getElementById('profile-section').classList.remove('hidden');
document.getElementById('rpc-section').classList.remove('hidden');
if (this.user) {
const placeholderImage = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='50' fill='%2363b3ed'/%3E%3Cpath d='M50 45c7.5 0 13.64-6.14 13.64-13.64S57.5 17.72 50 17.72s-13.64 6.14-13.64 13.64S42.5 45 50 45zm0 6.82c-9.09 0-27.28 4.56-27.28 13.64v3.41c0 1.88 1.53 3.41 3.41 3.41h47.74c1.88 0 3.41-1.53 3.41-3.41v-3.41c0-9.08-18.19-13.64-27.28-13.64z' fill='%23fff'/%3E%3C/svg%3E`;
const profileHtml = `
<img src="${this.user.picture || placeholderImage}" alt="Profil" class="profile-picture" onerror="this.src='${placeholderImage}'">
<h3>${this.user.name || 'Utilisateur'}</h3>
<p><strong>Courriel :</strong> ${this.user.email || 'Non fourni'}</p>
<p><strong>ID utilisateur :</strong> ${this.user.sub}</p>
`;
document.getElementById('profile-info').innerHTML = profileHtml;
}
}
showLoggedOutState() {
document.getElementById('login-btn').classList.remove('hidden');
document.getElementById('logout-btn').classList.add('hidden');
document.getElementById('profile-section').classList.add('hidden');
document.getElementById('rpc-section').classList.add('hidden');
}
showStatus(message, type = 'info') {
const statusDiv = document.getElementById('status');
statusDiv.innerHTML = `<div class="status ${type}">${message}</div>`;
// Masquer automatiquement les messages de succès et d'information après 5 secondes
if (type === 'success' || type === 'info') {
setTimeout(() => {
statusDiv.innerHTML = '';
}, 5000);
}
}
showRPCResult(title, data) {
const resultsDiv = document.getElementById('rpc-results');
const resultHtml = `
<div class="status success">
<h4>${title}</h4>
<pre>${JSON.stringify(data, null, 2)}</pre>
</div>
`;
resultsDiv.innerHTML = resultHtml;
this.showStatus('✅ Appel RPC effectué avec succès', 'success');
}
}
// Initialiser l'application
const app = new CapnWebAuth0Client();
app.init().catch(error => {
// Initialiser l'application au chargement du DOM
document.addEventListener('DOMContentLoaded', initializeApp);
Étape 7 : Tester et exécuter l'application
7.1 : Démarrer le serveur de développement :
npm run start
7.2 : Ouvrir http://localhost:3000 dans votre navigateur
7.3 : Tester le flux d'authentification complet :
- Cliquer sur « Connexion » pour s'authentifier avec Auth0
- Consulter vos informations de profil (le courriel sera affiché depuis Auth0)
- Mettre à jour votre biographie et enregistrer
- Actualiser la page - vous devriez rester connecté (grâce aux jetons d'actualisation)
- Tester la fonctionnalité de déconnexion
- Tester les appels RPC (Obtenir le profil, Mettre à jour le profil, Obtenir les données publiques)
- Vérifier l'état de la connexion WebSocket
- Tester la fonctionnalité de déconnexion
EXIGENCES DE SÉCURITÉ ET PRATIQUES RECOMMANDÉES
- ✅ NE JAMAIS accepter d'appels RPC non authentifiés pour les méthodes protégées
- ✅ TOUJOURS valider les signatures JWT à l'aide de JWKS depuis Auth0
- ✅ Mettre en œuvre une gestion complète des erreurs pour les jetons expirés ou non valides
- ✅ Utiliser des variables d'environnement pour toute configuration sensible
- ✅ Valider toutes les entrées utilisateur avant le traitement
- ✅ Consigner les événements de sécurité et les tentatives d'authentification
- ✅ Utiliser des connexions WebSocket sécurisées (WSS) en production
- ✅ Mettre en œuvre des politiques CORS appropriées
- ✅ Ajouter une limitation du débit de requêtes pour une utilisation en production
- ✅ Assainir toutes les données avant le stockage ou la transmission
CONSEILS DE DÉPANNAGE
- Vérifier la console du navigateur pour détecter les erreurs JavaScript
- Vérifier que le fichier .env contient la configuration Auth0 correcte
- S'assurer que les paramètres de l'application Auth0 correspondent à vos URL locales
- Confirmer que les portées API sont correctement configurées dans le tableau de bord Auth0
- Tester la connectivité WebSocket séparément si les appels RPC échouent
- Valider les jetons JWT à l'aide de jwt.io pour le débogage
Premiers pas
Créer un projet
Créez un nouveau projet Cap’n Web et configurez la structure de base :Initialisez le projet et configurez-le pour les modules ES :Créez la structure de répertoires du projet :
mkdir capnweb-auth0-app && cd capnweb-auth0-app
npm init -y && npm pkg set type="module"
mkdir -p client server && touch server/index.js client/index.html client/app.js .env.example .env
Installez les dépendances
Installez Cap’n Web et les dépendances principales :Installez les trousses de développement logiciel (SDK) Auth0 pour l’authentification et la validation des jetons :Configurez le script
npm install capnweb ws dotenv
npm install @auth0/auth0-spa-js @auth0/auth0-api-js
@auth0/auth0-spa-js est utilisé côté client pour gérer l’authentification des utilisateurs, les flux de connexion et la gestion des jetons dans le navigateur.@auth0/auth0-api-js est utilisé côté serveur pour vérifier les jetons d’accès et valider les signatures JWT à l’aide des JWKS d’Auth0.
start dans package.json :npm pkg set scripts.start="node server/index.js"
Configurez votre application Auth0
Ensuite, vous devez créer une nouvelle application sur votre locataire Auth0 et ajouter les variables d’environnement à votre projet.Vous pouvez choisir de le faire automatiquement en exécutant une commande CLI ou manuellement via le tableau de bord :
- CLI
- Tableau de bord
Exécutez la commande shell suivante dans le répertoire racine de votre projet pour créer une application Auth0 et générer un fichier
.env :Avant de commencer, créez un fichier URL de déconnexion autorisées:Origines Web autorisées :Obligatoire : créer une API Auth0Vous devez créer une API Auth0 pour que la validation des jetons fonctionne :
.env à la racine de votre projet.env
AUTH0_DOMAIN=YOUR_AUTH0_APP_DOMAIN
AUTH0_CLIENT_ID=YOUR_AUTH0_APP_CLIENT_ID
AUTH0_AUDIENCE=https://capnweb-api.yourproject.com
PORT=3000
NODE_ENV=development
- Accédez au Auth0 Dashboard
- Cliquez sur Applications > Applications > Create Application
- Dans la fenêtre contextuelle, saisissez un nom pour votre application, sélectionnez
Single Page Web Applicationcomme type d’application, puis cliquez sur Create - Accédez à l’onglet Settings de la page des détails de l’Application
- Remplacez
YOUR_AUTH0_APP_DOMAINetYOUR_AUTH0_APP_CLIENT_IDdans le fichier.envpar les valeurs Domaine et ID client de l’Auth0 Dashboard
http://localhost:3000
http://localhost:3000
http://localhost:3000
Les URL de redirection autorisées constituent une mesure de sécurité essentielle pour vous assurer que les utilisateurs sont renvoyés en toute sécurité vers votre application après l’authentification. Sans URL correspondante, le processus de connexion échouera et les utilisateurs seront bloqués par une page d’erreur Auth0 au lieu d’accéder à votre application.Les URL de déconnexion autorisées sont essentielles pour offrir une expérience utilisateur fluide lors de la déconnexion. Sans URL correspondante, les utilisateurs ne seront pas redirigés vers votre application après la déconnexion et resteront plutôt sur une page générique d’Auth0.La configuration Allowed Web Origins est essentielle pour l’authentification silencieuse. Sans cette configuration, les utilisateurs seront déconnectés lorsqu’ils actualiseront la page ou reviendront plus tard à votre application.
- Dans l’Auth0 Dashboard, allez dans APIs > Create API
- Configurez les valeurs suivantes :
- Name:
Cap'n Web API - Identifier:
https://capnweb-api.yourproject.com(utilisez votre propre identifiant unique) - Signing Algorithm:
RS256(valeur par défaut)
- Name:
- Cliquez sur Create
- Dans l’onglet Scopes, ajoutez les scopes suivants :
read:profile- Lire les données de profil de l’utilisateurwrite:profile- Mettre à jour les données de profil de l’utilisateur
- Mettez à jour votre fichier
.envavec l’identifiant de l’API sous la formeAUTH0_AUDIENCE
Si vous ne créez pas une API Auth0, vous obtiendrez une erreur « access_denied » pendant la connexion. L’identifiant de l’API doit correspondre exactement à votre variable d’environnement
AUTH0_AUDIENCE.Créez le serveur
Créez le serveur Web Cap’n avec l’intégration Auth0 :
server/index.js
import { RpcTarget, newWebSocketRpcSession } from 'capnweb';
import { WebSocketServer } from 'ws';
import { ApiClient } from '@auth0/auth0-api-js';
import http from 'http';
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
dotenv.config();
const __dirname = dirname(fileURLToPath(import.meta.url));
const userProfiles = new Map();
// Auth0 configuration
const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID;
const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;
const PORT = process.env.PORT || 3000;
if (!AUTH0_DOMAIN || !AUTH0_CLIENT_ID || !AUTH0_AUDIENCE) {
console.error('❌ Variables d'environnement Auth0 requises manquantes');
if (!AUTH0_DOMAIN) console.error(' - AUTH0_DOMAIN est requis');
if (!AUTH0_CLIENT_ID) console.error(' - AUTH0_CLIENT_ID est requis');
if (!AUTH0_AUDIENCE) console.error(' - AUTH0_AUDIENCE est requis');
process.exit(1);
}
// Initialize Auth0 API client for token verification
const auth0ApiClient = new ApiClient({
domain: AUTH0_DOMAIN,
audience: AUTH0_AUDIENCE
});
async function verifyToken(token) {
try {
const payload = await auth0ApiClient.verifyAccessToken({
accessToken: token
});
return payload;
} catch (error) {
throw new Error(`Échec de la vérification du jeton: ${error.message}`);
}
}
// Profile Service - Cap'n Web RPC Target
class ProfileService extends RpcTarget {
async getProfile(accessToken) {
const decoded = await verifyToken(accessToken);
const userId = decoded.sub;
const profile = userProfiles.get(userId) || { bio: '' };
return {
id: userId,
email: decoded.email || 'Utilisateur inconnu',
bio: profile.bio
};
}
async updateProfile(accessToken, bio) {
const decoded = await verifyToken(accessToken);
const userId = decoded.sub;
userProfiles.set(userId, { bio });
return { success: true, message: 'Profil mis à jour avec succès' };
}
}
// Create HTTP server
const server = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
if (req.url === '/api/config') {
const config = {
auth0: {
domain: AUTH0_DOMAIN,
clientId: AUTH0_CLIENT_ID,
audience: AUTH0_AUDIENCE
}
};
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(JSON.stringify(config));
return;
}
// Handle root path and Auth0 callback
if (req.url === '/' || req.url === '/index.html' || req.url.startsWith('/?code=') || req.url.startsWith('/?error=')) {
const html = readFileSync(join(__dirname, '../client/index.html'), 'utf8');
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end(html);
return;
}
if (req.url === '/app.js') {
const js = readFileSync(join(__dirname, '../client/app.js'), 'utf8');
res.setHeader('Content-Type', 'application/javascript');
res.writeHead(200);
res.end(js);
return;
}
// Serve Auth0 SPA JS SDK from node_modules
if (req.url === '/@auth0/auth0-spa-js') {
const modulePath = join(__dirname, '../node_modules/@auth0/auth0-spa-js/dist/auth0-spa-js.production.esm.js');
const js = readFileSync(modulePath, 'utf8');
res.setHeader('Content-Type', 'application/javascript');
res.writeHead(200);
res.end(js);
return;
}
// Serve capnweb from node_modules
if (req.url === '/capnweb') {
const modulePath = join(__dirname, '../node_modules/capnweb/dist/index.js');
const js = readFileSync(modulePath, 'utf8');
res.setHeader('Content-Type', 'application/javascript');
res.writeHead(200);
res.end(js);
return;
}
res.writeHead(404);
res.end('Introuvable');
});
// WebSocket server for Cap'n Web RPC
const wss = new WebSocketServer({ server });
wss.on('connection', (ws, req) => {
// Only handle RPC connections on /api path
if (req.url === '/api') {
console.log('🔗 Nouvelle connexion RPC Cap\'n Web');
// Create a new ProfileService instance for this connection
const profileService = new ProfileService();
// Use capnweb's newWebSocketRpcSession to handle the connection
newWebSocketRpcSession(ws, profileService);
}
});
// Start server
server.listen(PORT, () => {
console.log(`🚀 Serveur Auth0 Cap'n Web démarré`);
console.log(`📍 Serveur en cours d'exécution sur http://localhost:${PORT}`);
console.log(`🔐 Domaine Auth0: ${AUTH0_DOMAIN}`);
console.log(`🆔 ID client: ${AUTH0_CLIENT_ID.substring(0, 8)}...`);
console.log(`🎯 Audience de l'API: ${AUTH0_AUDIENCE}`);
});
Point de contrôleVous devriez maintenant avoir une page de connexion Auth0 entièrement fonctionnelle qui s’exécute sur votre localhost.
Utilisation avancée
Sécurité RPC côté serveur
Sécurité RPC côté serveur
Renforcez la sécurité en ajoutant une validation supplémentaire et un mécanisme de limitation de débit (
rate limiting) à vos méthodes RPC :server/profile-service.js
import rateLimit from 'express-rate-limit';
class ProfileService extends RpcTarget {
constructor() {
super();
this.rateLimiter = new Map(); // Mécanisme simple de limitation de débit en mémoire
}
async validateAndRateLimit(userId) {
const now = Date.now();
const userLimit = this.rateLimiter.get(userId) || { count: 0, resetTime: now + 60000 };
if (now > userLimit.resetTime) {
userLimit.count = 0;
userLimit.resetTime = now + 60000;
}
if (userLimit.count >= 10) {
throw new Error('Rate limit exceeded. Try again later.');
}
userLimit.count++;
this.rateLimiter.set(userId, userLimit);
}
async getProfile(accessToken) {
const decoded = await verifyToken(accessToken);
await this.validateAndRateLimit(decoded.sub);
const userId = decoded.sub;
const profile = userProfiles.get(userId) || { bio: '' };
return {
id: userId,
email: decoded.email || 'Unknown User',
bio: profile.bio,
lastUpdated: profile.lastUpdated || null
};
}
}
Gestion des connexions WebSocket
Gestion des connexions WebSocket
Implémentez une gestion appropriée des connexions WebSocket avec reconnexion automatique :
client/connection-manager.js
import { newWebSocketRpcSession } from 'capnweb';
class RpcConnectionManager {
constructor(wsUrl, options = {}) {
this.wsUrl = wsUrl;
this.api = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
this.reconnectDelay = options.reconnectDelay || 1000;
this.isConnecting = false;
this.onReconnect = options.onReconnect || (() => {});
}
async connect() {
if (this.isConnecting) return this.api;
this.isConnecting = true;
try {
// Fermer la connexion existante, le cas échéant
if (this.api) {
this.api[Symbol.dispose]();
}
// Créer une nouvelle session RPC WebSocket
this.api = newWebSocketRpcSession(this.wsUrl);
this.reconnectAttempts = 0;
this.isConnecting = false;
console.log('✅ RPC connection established');
this.onReconnect(this.api);
return this.api;
} catch (error) {
this.isConnecting = false;
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`🔄 Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
await new Promise(resolve =>
setTimeout(resolve, this.reconnectDelay * this.reconnectAttempts)
);
return this.connect();
} else {
throw new Error('Max reconnection attempts reached');
}
}
}
disconnect() {
if (this.api) {
this.api[Symbol.dispose]();
this.api = null;
}
this.reconnectAttempts = 0;
}
getApi() {
return this.api;
}
}
// Utilisation dans app.js
const connectionManager = new RpcConnectionManager(
`${protocol}//${host}/api`,
{
maxReconnectAttempts: 5,
reconnectDelay: 1000,
onReconnect: async (api) => {
// Actualiser l’interface utilisateur ou recharger les données après la reconnexion
await displayProfile(api);
}
}
);
// Se connecter lorsque l’utilisateur est authentifié
if (isAuthenticated) {
await connectionManager.connect();
profileApi = connectionManager.getApi();
}
Intégration à une base de données
Intégration à une base de données
Remplacez le stockage en mémoire par une base de données pour un usage en production :
server/database.js
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgresql://localhost/capnweb_auth0'
});
// Initialise le schéma de la base de données
async function initializeDatabase() {
await pool.query(`
CREATE TABLE IF NOT EXISTS user_profiles (
user_id VARCHAR(255) PRIMARY KEY,
bio TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
}
class DatabaseProfileService extends RpcTarget {
async getProfile(accessToken) {
const decoded = await verifyToken(accessToken);
const result = await pool.query(
'SELECT bio, updated_at FROM user_profiles WHERE user_id = $1',
[decoded.sub]
);
return {
id: decoded.sub,
email: decoded.email,
bio: result.rows[0]?.bio || '',
lastUpdated: result.rows[0]?.updated_at || null
};
}
async updateProfile(accessToken, bio) {
const decoded = await verifyToken(accessToken);
await pool.query(`
INSERT INTO user_profiles (user_id, bio, updated_at)
VALUES ($1, $2, CURRENT_TIMESTAMP)
ON CONFLICT (user_id)
DO UPDATE SET bio = $2, updated_at = CURRENT_TIMESTAMP
`, [decoded.sub, bio]);
return { success: true, message: 'Profile updated successfully' };
}
}
export { initializeDatabase, DatabaseProfileService };