From a020fa24d86681276742480051cac61e5ee44ea1 Mon Sep 17 00:00:00 2001 From: syui Date: Thu, 19 Jun 2025 11:34:56 +0900 Subject: [PATCH] fix gh-actions oauth-session --- .github/workflows/cloudflare-pages.yml | 47 +++++++++ oauth_new/CLOUDFLARE_DEPLOY_WITH_CLEANUP.yml | 104 +++++++++++++++++++ oauth_new/OAUTH_FIX.md | 81 +++++++++++++++ oauth_new/cleanup-deployments.yml | 41 ++++++++ oauth_new/src/services/oauth.js | 16 ++- oauth_new/src/utils/avatarFetcher.js | 8 +- 6 files changed, 294 insertions(+), 3 deletions(-) create mode 100644 oauth_new/CLOUDFLARE_DEPLOY_WITH_CLEANUP.yml create mode 100644 oauth_new/OAUTH_FIX.md create mode 100644 oauth_new/cleanup-deployments.yml diff --git a/.github/workflows/cloudflare-pages.yml b/.github/workflows/cloudflare-pages.yml index 0889650..4449f6a 100644 --- a/.github/workflows/cloudflare-pages.yml +++ b/.github/workflows/cloudflare-pages.yml @@ -8,6 +8,7 @@ on: env: OAUTH_DIR: oauth_new + KEEP_DEPLOYMENTS: 5 jobs: deploy: @@ -108,3 +109,49 @@ jobs: directory: my-blog/public gitHubToken: ${{ secrets.GITHUB_TOKEN }} wranglerVersion: '3' + + cleanup: + needs: deploy + runs-on: ubuntu-latest + if: success() + + steps: + - name: Wait for deployment to complete + run: sleep 3 + + - name: Cleanup old deployments + run: | + # Get all deployments + DEPLOYMENTS=$(curl -s -X GET \ + "https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/pages/projects/${{ secrets.CLOUDFLARE_PROJECT_NAME }}/deployments" \ + -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \ + -H "Content-Type: application/json") + + # Extract deployment IDs (skip the latest N deployments) + DEPLOYMENT_IDS=$(echo "$DEPLOYMENTS" | jq -r ".result | sort_by(.created_on) | reverse | .[${{ env.KEEP_DEPLOYMENTS }}:] | .[].id // empty") + + if [ -z "$DEPLOYMENT_IDS" ]; then + echo "No old deployments to delete" + exit 0 + fi + + # Delete old deployments + for ID in $DEPLOYMENT_IDS; do + echo "Deleting deployment: $ID" + RESPONSE=$(curl -s -X DELETE \ + "https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/pages/projects/${{ secrets.CLOUDFLARE_PROJECT_NAME }}/deployments/$ID" \ + -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \ + -H "Content-Type: application/json") + + SUCCESS=$(echo "$RESPONSE" | jq -r '.success') + if [ "$SUCCESS" = "true" ]; then + echo "Successfully deleted deployment: $ID" + else + echo "Failed to delete deployment: $ID" + echo "$RESPONSE" | jq . + fi + + sleep 1 # Rate limiting + done + + echo "Cleanup completed!" diff --git a/oauth_new/CLOUDFLARE_DEPLOY_WITH_CLEANUP.yml b/oauth_new/CLOUDFLARE_DEPLOY_WITH_CLEANUP.yml new file mode 100644 index 0000000..0f36458 --- /dev/null +++ b/oauth_new/CLOUDFLARE_DEPLOY_WITH_CLEANUP.yml @@ -0,0 +1,104 @@ +name: Deploy to Cloudflare Pages + +on: + push: + branches: + - main + workflow_dispatch: + +env: + OAUTH_DIR: oauth_new + KEEP_DEPLOYMENTS: 5 # 保持するデプロイメント数 + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: ${{ env.OAUTH_DIR }}/package-lock.json + + - name: Install dependencies + run: | + cd ${{ env.OAUTH_DIR }} + npm ci + + - name: Build OAuth app + run: | + cd ${{ env.OAUTH_DIR }} + NODE_ENV=production npm run build + env: + VITE_ADMIN: ${{ secrets.VITE_ADMIN }} + VITE_PDS: ${{ secrets.VITE_PDS }} + VITE_HANDLE_LIST: ${{ secrets.VITE_HANDLE_LIST }} + VITE_COLLECTION: ${{ secrets.VITE_COLLECTION }} + VITE_OAUTH_CLIENT_ID: ${{ secrets.VITE_OAUTH_CLIENT_ID }} + VITE_OAUTH_REDIRECT_URI: ${{ secrets.VITE_OAUTH_REDIRECT_URI }} + VITE_ENABLE_TEST_UI: 'false' + VITE_ENABLE_DEBUG: 'false' + + - name: Deploy to Cloudflare Pages + uses: cloudflare/pages-action@v1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }} + directory: ${{ env.OAUTH_DIR }}/dist + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + deploymentName: Production + + cleanup: + needs: deploy + runs-on: ubuntu-latest + if: success() + + steps: + - name: Wait for deployment to complete + run: sleep 30 + + - name: Cleanup old deployments + run: | + # Get all deployments + DEPLOYMENTS=$(curl -s -X GET \ + "https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/pages/projects/${{ secrets.CLOUDFLARE_PROJECT_NAME }}/deployments" \ + -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \ + -H "Content-Type: application/json") + + # Extract deployment IDs (skip the latest N deployments) + DEPLOYMENT_IDS=$(echo "$DEPLOYMENTS" | jq -r ".result | sort_by(.created_on) | reverse | .[${{ env.KEEP_DEPLOYMENTS }}:] | .[].id // empty") + + if [ -z "$DEPLOYMENT_IDS" ]; then + echo "No old deployments to delete" + exit 0 + fi + + # Delete old deployments + for ID in $DEPLOYMENT_IDS; do + echo "Deleting deployment: $ID" + RESPONSE=$(curl -s -X DELETE \ + "https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/pages/projects/${{ secrets.CLOUDFLARE_PROJECT_NAME }}/deployments/$ID" \ + -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \ + -H "Content-Type: application/json") + + SUCCESS=$(echo "$RESPONSE" | jq -r '.success') + if [ "$SUCCESS" = "true" ]; then + echo "Successfully deleted deployment: $ID" + else + echo "Failed to delete deployment: $ID" + echo "$RESPONSE" | jq . + fi + + sleep 1 # Rate limiting + done + + echo "Cleanup completed!" \ No newline at end of file diff --git a/oauth_new/OAUTH_FIX.md b/oauth_new/OAUTH_FIX.md new file mode 100644 index 0000000..82951ac --- /dev/null +++ b/oauth_new/OAUTH_FIX.md @@ -0,0 +1,81 @@ +# OAuth認証の修正案 + +## 現在の問題 + +1. **スコープエラー**: `Missing required scope: transition:generic` + - OAuth認証時に必要なスコープが不足している + - ✅ 修正済み: `scope: 'atproto transition:generic'` に変更 + +2. **401エラー**: PDSへの直接アクセス + - `https://shiitake.us-east.host.bsky.network/xrpc/app.bsky.actor.getProfile` で401エラー + - 原因: 個人のPDSに直接アクセスしているが、これは認証が必要 + - 解決策: 公開APIエンドポイント(`https://public.api.bsky.app`)を使用すべき + +3. **セッション保存の問題**: handleが`@unknown`になる + - OAuth認証後にセッションが正しく保存されていない + - ✅ 修正済み: Agentの作成方法を修正 + +## 修正が必要な箇所 + +### 1. avatarFetcher.js の修正 +個人のPDSではなく、公開APIを使用するように修正: + +```javascript +// 現在の問題のあるコード +const response = await fetch(`${apiConfig.bsky}/xrpc/app.bsky.actor.getProfile?actor=${did}`) + +// 修正案 +// PDSに関係なく、常に公開APIを使用 +const response = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${did}`) +``` + +### 2. セッション復元の改善 +OAuth認証後のコールバック処理で、セッションが正しく復元されていない可能性がある。 + +```javascript +// restoreSession メソッドの改善 +async restoreSession() { + // Try both clients + for (const [name, client] of Object.entries(this.clients)) { + if (!client) continue + + const result = await client.init() + if (result?.session) { + // セッション処理を確実に行う + this.agent = new Agent(result.session) + const sessionInfo = await this.processSession(result.session) + + // セッション情報をログに出力(デバッグ用) + logger.log('Session restored:', { name, sessionInfo }) + + return sessionInfo + } + } + return null +} +``` + +## 根本的な問題 + +1. **PDSアクセスの誤解** + - `app.bsky.actor.getProfile` は公開API(認証不要) + - 個人のPDSサーバーに直接アクセスする必要はない + - 常に `https://public.api.bsky.app` を使用すべき + +2. **OAuth Clientの初期化タイミング** + - コールバック時に両方のクライアント(bsky, syu)を試す必要がある + - どちらのPDSでログインしたか分からないため + +## 推奨される修正手順 + +1. **即座の修正**(401エラー解決) + - `avatarFetcher.js` で公開APIを使用 + - `getProfile` 呼び出しをすべて公開APIに変更 + +2. **セッション管理の改善** + - OAuth認証後のセッション復元を確実に + - エラーハンドリングの強化 + +3. **デバッグ情報の追加** + - セッション復元時のログ追加 + - どのOAuthクライアントが使用されたか確認 \ No newline at end of file diff --git a/oauth_new/cleanup-deployments.yml b/oauth_new/cleanup-deployments.yml new file mode 100644 index 0000000..b97feff --- /dev/null +++ b/oauth_new/cleanup-deployments.yml @@ -0,0 +1,41 @@ +name: Cleanup Old Deployments + +on: + workflow_run: + workflows: ["Deploy to Cloudflare Pages"] + types: + - completed + workflow_dispatch: + +env: + KEEP_DEPLOYMENTS: 5 # 保持するデプロイメント数 + +jobs: + cleanup: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} + + steps: + - name: Cleanup old deployments + run: | + # Get all deployments + DEPLOYMENTS=$(curl -s -X GET \ + "https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/pages/projects/${{ secrets.CLOUDFLARE_PROJECT_NAME }}/deployments" \ + -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \ + -H "Content-Type: application/json") + + # Extract deployment IDs (skip the latest N deployments) + DEPLOYMENT_IDS=$(echo "$DEPLOYMENTS" | jq -r ".result | sort_by(.created_on) | reverse | .[${{ env.KEEP_DEPLOYMENTS }}:] | .[].id") + + # Delete old deployments + for ID in $DEPLOYMENT_IDS; do + echo "Deleting deployment: $ID" + curl -s -X DELETE \ + "https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/pages/projects/${{ secrets.CLOUDFLARE_PROJECT_NAME }}/deployments/$ID" \ + -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \ + -H "Content-Type: application/json" + echo "Deleted deployment: $ID" + sleep 1 # Rate limiting + done + + echo "Cleanup completed!" \ No newline at end of file diff --git a/oauth_new/src/services/oauth.js b/oauth_new/src/services/oauth.js index 51aa7ce..2cff4ac 100644 --- a/oauth_new/src/services/oauth.js +++ b/oauth_new/src/services/oauth.js @@ -66,11 +66,23 @@ export class OAuthService { const did = session.sub || session.did let handle = session.handle || 'unknown' + // Create Agent directly with session (per official docs) + try { + this.agent = new Agent(session) + } catch (err) { + // Fallback to dpopFetch method + this.agent = new Agent({ + service: session.server?.serviceEndpoint || 'https://bsky.social', + fetch: session.dpopFetch + }) + } + this.sessionInfo = { did, handle } // Resolve handle if missing if (handle === 'unknown' && this.agent) { try { + await new Promise(resolve => setTimeout(resolve, 300)) const profile = await this.agent.getProfile({ actor: did }) handle = profile.data.handle this.sessionInfo.handle = handle @@ -86,7 +98,9 @@ export class OAuthService { await this.initialize() const client = isSyuIsHandle(handle) ? this.clients.syu : this.clients.bsky - const authUrl = await client.authorize(handle, { scope: 'atproto' }) + const authUrl = await client.authorize(handle, { + scope: 'atproto transition:generic' + }) window.location.href = authUrl.toString() } diff --git a/oauth_new/src/utils/avatarFetcher.js b/oauth_new/src/utils/avatarFetcher.js index 9c4ed8c..5d52f3b 100644 --- a/oauth_new/src/utils/avatarFetcher.js +++ b/oauth_new/src/utils/avatarFetcher.js @@ -34,11 +34,15 @@ async function getDid(handle) { // DIDからプロフィール情報を取得 async function getProfile(did, handle) { try { + // Determine which public API to use based on handle const pds = await getPdsFromHandle(handle) const apiConfig = getApiConfig(pds) - logger.log('Getting profile for DID:', did, 'using API:', apiConfig.bsky) - const response = await fetch(`${apiConfig.bsky}/xrpc/app.bsky.actor.getProfile?actor=${did}`) + // Use the appropriate public API endpoint + const publicApiUrl = apiConfig.bsky + + logger.log('Getting profile for DID:', did, 'using public API:', publicApiUrl) + const response = await fetch(`${publicApiUrl}/xrpc/app.bsky.actor.getProfile?actor=${did}`) if (!response.ok) { throw new Error(`Profile API error: ${response.status} ${response.statusText}`)