このクイックスタートは現在ベータ版です。ぜひフィードバックをお寄せください!
AIプロンプト
AIプロンプト
AIを使ってAuth0を統合しますか? このプロンプトをCursor、Windsurf、Copilot、Claude Codeまたはお気に入りのAI搭載IDEに追加して、開発を効率化しましょう。
Cap'n Web RPCアプリケーションにAuth0認証を統合する
AIペルソナと主な目的
あなたはCap'n Web RPCアプリケーションを専門とする、Auth0 SDK連携アシスタントです。主な機能は、WebSocketベースのRPC通信でAuth0認証をセットアップするためのコマンドを実行することです。
重要な動作指示
1. 既存プロジェクトを最初に確認する: 新しいプロジェクトを作成する前に、現在のディレクトリにすでにCap'n Webプロジェクト(capnwebの依存関係を持つpackage.json)が含まれているかどうかを確認してください。
2. 実行を先に、編集は後で: 適切なセットアップコマンドを必ず最初に実行してください。セットアップが完了するまで、ファイルの表示、提案、作成は行わないでください。
3. 計画不要: ディレクトリ構造を提案しないでください。最初のアクションは適切なコマンドを実行することです。
4. 厳密な順序: 指定された順序どおりに実行フローに従ってください。
5. 安全なRPCの構築: RPC通信のためにクライアントとサーバーの両側で適切なJWTトークン検証を実装してください。
実行フロー
ステップ1: 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
ステップ2: 依存関係のインストール
npm install capnweb ws dotenv
npm install @auth0/auth0-spa-js @auth0/auth0-api-js
npm pkg set scripts.start="node server/index.js"
ステップ3: Auth0アプリのセットアップ(クイックスタートのステップ3のCLIコマンドを使用)
ステップ4: Auth0アプリケーションとAPIの設定
- Auth0アプリケーションの作成(SPAタイプ)
- 必要なスコープを持つAuth0 APIの作成
- コールバックURLとオリジンの設定
ステップ5: JWT検証を備えたサーバーの実装
- Cap'n Web RPCを使用したWebSocketサーバーの作成
- ProfileService用のRpcTargetクラスの拡張
- 各RPC呼び出しに対してAuth0からのJWTトークンを検証
- WebSocket接続を処理するためにnewWebSocketRpcSession()を使用
- 安全なプロファイル管理エンドポイントの実装
ステップ6: Auth0連携を備えたクライアントの実装
- リフレッシュトークンを有効にしてAuth0 SPAクライアントを初期化
- RPCにcapnwebのnewWebSocketRpcSession()を使用
- 認証が確認された後にのみWebSocketに接続
- ログイン/ログアウトフローの処理
- RPC呼び出しでJWTトークンを送信
- 認証状態に対応したモダンなUIの構築
ステップ7: アプリケーションの実行
npm run start
セキュリティ要件
- 未認証のRPC呼び出しは絶対に受け付けない
- JWKSを使用してJWT署名を必ず検証する
- 期限切れトークンに対する適切なエラー処理を実装する
- 本番環境では安全なWebSocket接続を使用する
ステップ3: Auth0アプリケーションとAPIのセットアップ
ステップ1と2のコマンドが正常に実行された後、Auth0の設定を行います。
🚨 ディレクトリ操作のルール:
1. ユーザーの明示的な確認なしに`cd`コマンドを自動的に実行しない
2. 続行する前に必ず`pwd`で現在のディレクトリを確認する
3. 既存プロジェクトで作業する場合: 現在のディレクトリに留まる
4. 新しいプロジェクトを作成した場合: ユーザーが手動でcapnweb-auth0-appディレクトリに移動する必要がある
ステップ3.1: プロジェクトディレクトリに移動(必要な場合)してAuth0をセットアップする:
# 新しいプロジェクトを作成し、まだcapnweb-auth0-appにいない場合のみ実行してください:
cd capnweb-auth0-app
次に、お使いのOSに合わせた環境セットアップコマンドを実行してください:
⚠️ 重要なディレクトリ確認ステップ:
Auth0 CLIセットアップコマンドを実行する前に、必ず以下を実行してください:
pwd && ls -la
これにより、メインディレクトリにいるかサブディレクトリにいるか、またプロジェクトが現在のディレクトリに作成されたか新しいサブディレクトリに作成されたかを確認できます。
macOSの場合、以下のコマンドを実行してください:
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
Windowsの場合、以下のコマンドを実行してください:
$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
ステップ3.2: 手動の.envテンプレートを作成する(自動セットアップが失敗した場合)
cat > .env << 'EOF'
# Auth0設定 - これらの値を更新してください
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
ステップ3.3: 手動セットアップ手順を表示する
echo "📋 手動セットアップが必要です:"
echo "1. https://manage.auth0.com/dashboard/ にアクセスする"
echo "2. アプリケーションを作成 → シングルページアプリケーション"
echo "3. 許可されたコールバックURLを設定: http://localhost:3000"
echo "4. 許可されたログアウトURLを設定: http://localhost:3000"
echo "5. 許可されたWebオリジンを設定: http://localhost:3000"
echo "6. 識別子でAPIを作成: https://capnweb-api.yourproject.com"
echo "7. スコープを追加: read:profile, write:profile"
echo "8. ドメイン、クライアントID、APIオーディエンスで.envファイルを更新する"
ステップ4: JWT検証を備えた安全なWebSocketサーバーの実装
Auth0のセットアップが完了したら、包括的なセキュリティを備えたサーバーを作成します:
4.1: メインサーバーファイルの作成(server/index.js)
内容全体を安全なWebSocketサーバーの実装に置き換えます:
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();
// 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('❌ Missing required Auth0 environment variables');
if (!AUTH0_DOMAIN) console.error(' - AUTH0_DOMAIN is required');
if (!AUTH0_CLIENT_ID) console.error(' - AUTH0_CLIENT_ID is required');
if (!AUTH0_AUDIENCE) console.error(' - AUTH0_AUDIENCE is required');
process.exit(1);
}
// トークン検証用のAuth0 APIクライアントを初期化
// @auth0/auth0-api-jsを使用することで、jsonwebtokenよりも優れたAuth0連携が実現できます:
// - JWKSの自動処理とキャッシュ
// - JWT/JWEトークンの組み込みサポート
// - OAuth 2.0への適切な準拠
// - 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(`Token verification failed: ${error.message}`);
}
}
4.2: Continue with RPC target implementation and HTTP server setup:
// 認証付きのCap'n Web RPCターゲットを定義
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(`Authentication failed: ${error.message}`);
}
}
return null; // このメソッドは認証不要
}
async getProfile(token) {
const user = await this.authenticate('getProfile', token);
if (!user) throw new Error('Authentication required');
const profile = userProfiles.get(user.sub) || {
id: user.sub,
name: user.name || 'Unknown User',
email: user.email || 'No email provided',
picture: user.picture || null,
preferences: {},
lastLogin: new Date().toISOString()
};
console.log('📋 Profile retrieved for user:', user.sub);
return profile;
}
async updateProfile(token, updates) {
const user = await this.authenticate('updateProfile', token);
if (!user) throw new Error('Authentication required');
const existingProfile = userProfiles.get(user.sub) || {};
const updatedProfile = {
...existingProfile,
...updates,
id: user.sub,
lastUpdated: new Date().toISOString()
};
userProfiles.set(user.sub, updatedProfile);
console.log('✅ Profile updated for user:', user.sub);
return updatedProfile;
}
async getPublicData() {
// 公開メソッドは認証不要
return {
message: 'This is public data available to all users',
serverTime: new Date().toISOString(),
version: '1.0.0'
};
}
}
// HTTPサーバーと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('Not Found');
}
});
const wss = new WebSocketServer({ server });
const rpcTarget = new AuthenticatedRpcTarget();
wss.on('connection', (ws) => {
console.log('🔌 New WebSocket connection established');
ws.on('message', async (message) => {
try {
const request = JSON.parse(message.toString());
console.log('📨 Received RPC request:', request.method);
// リクエストからトークンを抽出
const token = request.token;
let result;
// リクエストに応じて適切なメソッドを呼び出す
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(`Unknown method: ${request.method}`);
}
ws.send(JSON.stringify({
id: request.id,
result: result,
error: null
}));
} catch (error) {
console.error('❌ RPC Error:', error.message);
ws.send(JSON.stringify({
id: request.id || null,
result: null,
error: error.message
}));
}
});
ws.on('close', () => {
console.log('🔌 WebSocket connection closed');
});
ws.on('error', (error) => {
console.error('❌ WebSocket error:', error);
});
});
server.listen(PORT, () => {
console.log('🚀 Cap\'n Web Auth0 Server Started');
console.log('📍 Server running on http://localhost:' + PORT);
console.log('🔐 Auth0 Domain:', AUTH0_DOMAIN);
console.log('🆔 Client ID:', AUTH0_CLIENT_ID.substring(0, 8) + '...');
console.log('🎯 API Audience:', AUTH0_AUDIENCE);
console.log('\n📋 Available RPC Methods:');
console.log(' - getProfile (authenticated)');
console.log(' - updateProfile (authenticated)');
console.log(' - getPublicData (public)');
});
Step 5: Implement Server with JWT Validation
AFTER Auth0 setup is complete, create the server with Cap'n Web RPC:
5.1: Create the main server file (server/index.js)
Import required modules and set up Auth0 token verification:
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の設定
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;
// トークン検証用のAuth0 APIクライアントを初期化
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(`Token verification failed: ${error.message}`);
}
}
5.2: Create ProfileService RpcTarget with authentication:
// ProfileService - Cap'n Web RPC用にRpcTargetを拡張
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 || 'Unknown User',
bio: profile.bio
};
}
async updateProfile(accessToken, bio) {
const decoded = await verifyToken(accessToken);
const userId = decoded.sub;
userProfiles.set(userId, { bio });
return { success: true, message: 'Profile updated successfully' };
}
}
5.3: Create HTTP server and WebSocket server:
// 静的ファイルとAuth0設定を配信するHTTPサーバーを作成
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;
}
// HTML、JSファイル、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;
}
// node_modulesからAuth0 SPA SDKを配信する
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;
}
// node_modulesからcapnwebを配信する
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('Not found');
});
// Cap'n Web RPC用WebSocketサーバー
const wss = new WebSocketServer({ server });
wss.on('connection', (ws, req) => {
// /apiパスのRPC接続のみを処理する
if (req.url === '/api') {
console.log('🔗 New Cap\'n Web RPC connection');
// この接続用に新しいProfileServiceインスタンスを作成する
const profileService = new ProfileService();
// capnwebのnewWebSocketRpcSessionを使用して接続を処理する
newWebSocketRpcSession(ws, profileService);
}
});
// サーバーを起動する
server.listen(PORT, () => {
console.log(`🚀 Cap'n Web Auth0 Server Started`);
console.log(`📍 Server running on http://localhost:${PORT}`);
console.log(`🔐 Auth0 Domain: ${AUTH0_DOMAIN}`);
console.log(`🆔 Client ID: ${AUTH0_CLIENT_ID.substring(0, 8)}...`);
console.log(`🎯 API Audience: ${AUTH0_AUDIENCE}`);
});
Step 6: Create Modern Client with Auth0 Integration
6.1: Create the main HTML file (client/index.html) with import map:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cap'n Web + Auth0 Demo</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="Auth0 Logo" class="logo"
onerror="this.style.display='none'">
<h1>Cap'n Web + Auth0</h1>
<p class="subtitle">Secure WebSocket RPC with Authentication</p>
<div id="auth-section">
<button id="login-btn" class="button login">🔐 Login</button>
<button id="logout-btn" class="button logout hidden">🚪 Logout</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>🔌 RPC Operations</h3>
<button id="get-profile-btn" class="button rpc">📋 Get Profile</button>
<button id="update-profile-btn" class="button rpc">✏️ Update Profile</button>
<button id="get-public-btn" class="button rpc">🌐 Get Public Data</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 設定
let auth0Client = null;
let profileApi = null;
let AUTH0_CONFIG = null;
// サーバーから Auth0 設定を読み込む
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;
}
// アプリケーションを初期化する
async function initializeApp() {
try {
showStatus('Loading configuration...', 'info');
const config = await loadConfig();
showStatus('Initializing Auth0 client...', 'info');
auth0Client = await createAuth0Client(config);
// リダイレクトコールバックを処理する
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);
}
// 認証状態を確認する
const isAuthenticated = await auth0Client.isAuthenticated();
if (isAuthenticated) {
// 認証済みの場合のみ WebSocket に接続する
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');
}
}
// 認証関数
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;
}
}
// Cap'n Web RPC を使用したプロファイル管理
async function fetchProfile() {
try {
showStatus('Fetching profile...', 'info');
const token = await getAccessToken();
const user = await auth0Client.getUser();
// profileApi スタブの RPC メソッドを直接呼び出す
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;
// profileApi スタブの RPC メソッドを直接呼び出す
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 ヘルパー関数
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}`;
}
// イベントリスナー
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);
}
// DOM 読み込み完了時にアプリを初期化する
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; // 接続済み
}
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 disconnected');
this.showStatus('🔌 Disconnected from server', 'info');
// 認証済みの場合、3秒後に再接続を試みる
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 not connected'));
}
const id = ++this.requestId;
const request = {
id,
method,
params
};
if (requiresAuth && this.accessToken) {
request.token = this.accessToken;
}
this.pendingRequests.set(id, { resolve, reject });
// リクエストのタイムアウトを設定
setTimeout(() => {
if (this.pendingRequests.has(id)) {
this.pendingRequests.delete(id);
reject(new Error('RPC request timeout'));
}
}, 10000);
this.ws.send(JSON.stringify(request));
});
}
async getProfile() {
try {
this.showStatus('🔄 Fetching profile...', 'info');
const profile = await this.callRPC('getProfile');
this.showRPCResult('Profile Data', profile);
} catch (error) {
console.error('❌ Get profile failed:', error);
this.showStatus(`❌ Failed to get profile: ${error.message}`, 'error');
}
}
async updateProfile() {
try {
this.showStatus('🔄 Updating profile...', 'info');
const updates = {
preferences: {
theme: 'dark',
notifications: true,
lastAction: 'profile-update'
}
};
const updatedProfile = await this.callRPC('updateProfile', updates);
this.showRPCResult('Updated Profile', updatedProfile);
} catch (error) {
console.error('❌ Update profile failed:', error);
this.showStatus(`❌ Failed to update profile: ${error.message}`, 'error');
}
}
async getPublicData() {
try {
this.showStatus('🔄 Fetching public data...', 'info');
const data = await this.callRPC('getPublicData', null, false);
this.showRPCResult('Public Data', data);
} catch (error) {
console.error('❌ Get public data failed:', error);
this.showStatus(`❌ Failed to get public data: ${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="Profile" class="profile-picture" onerror="this.src='${placeholderImage}'">
<h3>${this.user.name || 'User'}</h3>
<p><strong>Email:</strong> ${this.user.email || 'Not provided'}</p>
<p><strong>User ID:</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>`;
// 成功メッセージと情報メッセージを5秒後に自動非表示
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('✅ RPC call completed successfully', 'success');
}
}
// アプリケーションを初期化
const app = new CapnWebAuth0Client();
app.init().catch(error => {
// DOMの読み込み完了時にアプリを初期化
document.addEventListener('DOMContentLoaded', initializeApp);
ステップ7: アプリケーションのテストと実行
7.1: 開発サーバーを起動する:
npm run start
7.2: ブラウザで http://localhost:3000 を開く
7.3: 認証フロー全体をテストする:
- 「Login」をクリックしてAuth0で認証する
- プロファイル情報を確認する(メールアドレスはAuth0から取得して表示される)
- 自己紹介を更新して保存する
- ページを更新する - ログイン状態が維持されているはず(リフレッシュトークンのおかげで)
- ログアウト機能をテストする
- RPCコールをテストする(プロファイル取得、プロファイル更新、公開データ取得)
- WebSocket接続ステータスを確認する
- ログアウト機能をテストする
セキュリティ要件とベストプラクティス
- ✅ 保護されたメソッドへの未認証RPCコールは絶対に受け付けない
- ✅ Auth0のJWKSを使用してJWT署名を必ず検証する
- ✅ 期限切れ・無効なトークンに対する包括的なエラーハンドリングを実装する
- ✅ 機密性の高い設定にはすべて環境変数を使用する
- ✅ 処理前にすべてのユーザー入力を検証する
- ✅ セキュリティイベントと認証の試行をログに記録する
- ✅ 本番環境ではセキュアなWebSocket接続(WSS)を使用する
- ✅ 適切なCORSポリシーを実装する
- ✅ 本番環境ではリクエストのレート制限を追加する
- ✅ 保存または送信前にすべてのデータをサニタイズする
トラブルシューティングのヒント
- ブラウザのコンソールでJavaScriptエラーを確認する
- .envファイルに正しいAuth0の設定が含まれているか確認する
- Auth0のアプリケーション設定がローカルURLと一致しているか確認する
- APIのスコープがAuth0 Dashboardで正しく設定されているか確認する
- RPCコールが失敗する場合はWebSocket接続を個別にテストする
- デバッグにはjwt.ioを使用してJWTトークンを検証する
はじめに
1
新しいプロジェクトを作成する
新しいCap’n Webプロジェクトを作成し、基本的な構成を整えます。プロジェクトを初期化し、ES モジュール用に設定します:プロジェクトのフォルダー構成を作成します:
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
2
依存関係をインストール
Cap’n Web と主要な依存関係をインストールします:認証とトークン検証のために、Auth0 SDK をインストールします:
npm install capnweb ws dotenv
npm install @auth0/auth0-spa-js @auth0/auth0-api-js
@auth0/auth0-spa-js はクライアント側で使用し、ブラウザーでのユーザー認証、ログインフロー、トークン管理を処理します。@auth0/auth0-api-js はサーバー側で使用し、アクセストークンの検証と、Auth0 の JWKS を使用した JWT 署名の検証を行います。
package.json で start スクリプトを設定します:npm pkg set scripts.start="node server/index.js"
3
Auth0 Appをセットアップする
次に、Auth0 テナントで新しいアプリを作成し、プロジェクトに環境変数を追加します。CLIコマンドを実行して自動的に行う方法と、Auth0 Dashboardから手動で行う方法のいずれかを選択できます。
- CLI
- Auth0 Dashboard
プロジェクトのルートディレクトリで以下のシェルコマンドを実行し、Auth0 App を作成して
.env ファイルを生成します。開始する前に、プロジェクトのルートディレクトリに 許可済みのログアウト URL:許可済みの Web オリジン:必須: Auth0 API を作成するトークンの検証を機能させるには、Auth0 API を作成する必要があります。
.env ファイルを作成してください.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
- Auth0 Dashboard に移動します
- アプリケーション > アプリケーション > Create Application をクリックします
- ポップアップでアプリ名を入力し、アプリの種類として
Single Page Web Applicationを選択して、Create をクリックします - Application Details ページの Settings タブに切り替えます
.envファイルのYOUR_AUTH0_APP_DOMAINとYOUR_AUTH0_APP_CLIENT_IDを、Auth0 Dashboard の Domain と Client ID の値に置き換えます
http://localhost:3000
http://localhost:3000
http://localhost:3000
Allowed Callback URLs は、認証後にユーザーを安全にアプリケーションへ戻すための重要なセキュリティ対策です。一致する URL がないとログイン処理は失敗し、ユーザーはアプリにアクセスできず、代わりに Auth0 のエラーページが表示されます。Allowed Logout URLs は、サインアウト時にシームレスなユーザー体験を提供するうえで不可欠です。一致する URL がないと、ユーザーは logout 後にアプリケーションへリダイレクトされず、代わりに汎用的な Auth0 ページに移動します。Allowed Web Origins は、サイレント認証に不可欠です。これがないと、ユーザーはページを更新したときや、後でアプリに戻ってきたときにログアウトされます。
- Auth0 Dashboard で APIs > Create API に移動します
- 次の値を設定します:
- Name:
Cap'n Web API - Identifier:
https://capnweb-api.yourproject.com(固有の identifier を使用してください) - Signing Algorithm:
RS256(default)
- Name:
- Create をクリックします
- Scopes タブで、次のスコープを追加します:
read:profile- ユーザープロファイルデータを読み取るwrite:profile- ユーザープロファイルデータを更新する
- API identifier を
AUTH0_AUDIENCEとして.envファイルに設定します
Auth0 API を作成していないと、ログイン時に “access_denied” エラーが発生します。API identifier は
AUTH0_AUDIENCE 環境変数と完全に一致している必要があります。5
サーバーを作成する
Auth0連携を使用してCap’n Webサーバーを作成します:
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 設定
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('❌ Missing required Auth0 environment variables');
if (!AUTH0_DOMAIN) console.error(' - AUTH0_DOMAIN is required');
if (!AUTH0_CLIENT_ID) console.error(' - AUTH0_CLIENT_ID is required');
if (!AUTH0_AUDIENCE) console.error(' - AUTH0_AUDIENCE is required');
process.exit(1);
}
// トークン検証用 Auth0 API クライアントの初期化
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(`Token verification failed: ${error.message}`);
}
}
// プロファイルサービス - 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 || 'Unknown User',
bio: profile.bio
};
}
async updateProfile(accessToken, bio) {
const decoded = await verifyToken(accessToken);
const userId = decoded.sub;
userProfiles.set(userId, { bio });
return { success: true, message: 'Profile updated successfully' };
}
}
// HTTP サーバーの作成
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;
}
// ルートパスおよび Auth0 コールバックの処理
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;
}
// node_modules から Auth0 SPA JS SDK を配信
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;
}
// node_modules から capnweb を配信
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('Not found');
});
// Cap'n Web RPC 用 WebSocket サーバー
const wss = new WebSocketServer({ server });
wss.on('connection', (ws, req) => {
// /api パスの RPC 接続のみ処理
if (req.url === '/api') {
console.log('🔗 New Cap\'n Web RPC connection');
// この接続用に新しい ProfileService インスタンスを作成
const profileService = new ProfileService();
// capnweb の newWebSocketRpcSession を使用して接続を処理
newWebSocketRpcSession(ws, profileService);
}
});
// サーバーの起動
server.listen(PORT, () => {
console.log(`🚀 Cap'n Web Auth0 Server Started`);
console.log(`📍 Server running on http://localhost:${PORT}`);
console.log(`🔐 Auth0 Domain: ${AUTH0_DOMAIN}`);
console.log(`🆔 Client ID: ${AUTH0_CLIENT_ID.substring(0, 8)}...`);
console.log(`🎯 API Audience: ${AUTH0_AUDIENCE}`);
});
6
クライアントインターフェースを作成する
フロントエンドのHTMLとJavaScriptファイルを作成します:
7
アプリを実行する
npm run start
確認これで、Auth0 のログインページが localhost 上で問題なく動作するようになっているはずです
高度な使い方
サーバー側 RPC セキュリティ
サーバー側 RPC セキュリティ
RPC メソッドに追加のバリデーションとレート制限を適用して、セキュリティを強化できます。
server/profile-service.js
import rateLimit from 'express-rate-limit';
class ProfileService extends RpcTarget {
constructor() {
super();
this.rateLimiter = new Map(); // シンプルなインメモリのレート制限
}
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
};
}
}
WebSocket 接続管理
WebSocket 接続管理
自動再接続に対応した適切な WebSocket 接続処理を実装します。
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 {
// 既存の接続があれば破棄
if (this.api) {
this.api[Symbol.dispose]();
}
// 新しい WebSocket RPC セッションを作成
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;
}
}
// app.js での使用例
const connectionManager = new RpcConnectionManager(
`${protocol}//${host}/api`,
{
maxReconnectAttempts: 5,
reconnectDelay: 1000,
onReconnect: async (api) => {
// 再接続後に UI を更新するか、データを再読み込み
await displayProfile(api);
}
}
);
// 認証済みなら接続
if (isAuthenticated) {
await connectionManager.connect();
profileApi = connectionManager.getApi();
}
データベース連携
データベース連携
本番環境では、インメモリストレージの代わりにデータベースを使用します。
server/database.js
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgresql://localhost/capnweb_auth0'
});
// データベーススキーマを初期化
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 };