From 8b0abf09f778294f2c4083515242fab945c6494e Mon Sep 17 00:00:00 2001 From: PengGao Date: Sun, 31 Aug 2025 21:34:26 +0200 Subject: [PATCH 01/11] fix: correct OAuth token exchange URL in raindrop callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The token exchange URL was incorrectly using the API endpoint instead of the OAuth endpoint. Fixed to use https://raindrop.io/oauth/access_token directly. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .env.local | 4 ++++ src/app/api/auth/raindrop/callback/route.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .env.local diff --git a/.env.local b/.env.local new file mode 100644 index 0000000..1d6b9a2 --- /dev/null +++ b/.env.local @@ -0,0 +1,4 @@ +RAINDROP_CLIENT_ID=6826e3e0149b4706a991e02d +RAINDROP_CLIENT_SECRET=26a1ed7a-17ff-4700-b396-1ba0d989db5e +RAINDROP_ENCRYPTION_KEY=c567419f3383e7c5b0d6148867f81328 +NEXT_PUBLIC_BASE_URL=https://me.deeptoai.com \ No newline at end of file diff --git a/src/app/api/auth/raindrop/callback/route.js b/src/app/api/auth/raindrop/callback/route.js index 3402ea0..8bcd5a3 100644 --- a/src/app/api/auth/raindrop/callback/route.js +++ b/src/app/api/auth/raindrop/callback/route.js @@ -28,7 +28,7 @@ export async function GET(request) { } // 交换授权码获取访问令牌 - const tokenResponse = await fetch(`${RAINDROP_API_URL}/oauth/access_token`, { + const tokenResponse = await fetch('https://raindrop.io/oauth/access_token', { method: 'POST', headers: { 'Content-Type': 'application/json' From 12a998e91357568c83ba91055ad2177fab82fe67 Mon Sep 17 00:00:00 2001 From: PengGao Date: Sun, 31 Aug 2025 21:44:57 +0200 Subject: [PATCH 02/11] fix: use Raindrop.io OAuth v2 API endpoint instead of v1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v1 OAuth endpoint was returning Internal Server Error, while the v2 endpoint works correctly and redirects to the login page as expected. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/app/api/auth/raindrop/route.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/api/auth/raindrop/route.js b/src/app/api/auth/raindrop/route.js index 43d225e..22040ee 100644 --- a/src/app/api/auth/raindrop/route.js +++ b/src/app/api/auth/raindrop/route.js @@ -8,8 +8,8 @@ export async function GET() { return NextResponse.json({ error: 'Missing RAINDROP_CLIENT_ID' }, { status: 500 }) } - // 构建 OAuth2 授权 URL - const authUrl = new URL('https://raindrop.io/oauth/authorize') + // 构建 OAuth2 授权 URL - 使用 v2 API + const authUrl = new URL('https://api.raindrop.io/v2/oauth/authorize') authUrl.searchParams.set('client_id', clientId) authUrl.searchParams.set('redirect_uri', `${baseUrl}/api/auth/raindrop/callback`) authUrl.searchParams.set('response_type', 'code') From eae7d0e6d54814ffd739ed9d650dc742c4f74a25 Mon Sep 17 00:00:00 2001 From: PengGao Date: Sun, 31 Aug 2025 21:56:53 +0200 Subject: [PATCH 03/11] fix: improve OAuth callback error handling and token manager selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add detailed logging for token exchange debugging - Fix token manager selection in callback to use dynamic approach - Add better error reporting for token response validation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/app/api/auth/raindrop/callback/route.js | 23 +++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/app/api/auth/raindrop/callback/route.js b/src/app/api/auth/raindrop/callback/route.js index 8bcd5a3..eef9977 100644 --- a/src/app/api/auth/raindrop/callback/route.js +++ b/src/app/api/auth/raindrop/callback/route.js @@ -1,6 +1,18 @@ import { NextResponse } from 'next/server' -import { tokenManager } from '@/lib/auth/token-manager' +// 动态选择 token manager +function getTokenManager() { + if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) { + const { tokenManager } = require('@/lib/auth/token-manager') + return tokenManager + } + if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUPABASE_ANON_KEY) { + const { getTokenManager } = require('@/lib/auth/supabase-token-manager') + return getTokenManager() + } + const { getTokenManager } = require('@/lib/auth/env-token-manager') + return getTokenManager() +} const RAINDROP_API_URL = 'https://api.raindrop.io/rest/v1' @@ -49,12 +61,19 @@ export async function GET(request) { } const tokenData = await tokenResponse.json() + console.info('Token response from Raindrop:', JSON.stringify(tokenData, null, 2)) if (!tokenData.access_token || !tokenData.refresh_token) { + console.error('Missing tokens in response:', { + hasAccessToken: !!tokenData.access_token, + hasRefreshToken: !!tokenData.refresh_token, + responseKeys: Object.keys(tokenData) + }) throw new Error('Invalid token response') } - // 存储令牌到 KV + // 存储令牌 + const tokenManager = getTokenManager() await tokenManager.storeInitialTokens( tokenData.access_token, tokenData.refresh_token, From 114853f3f424705373d86935025f64b6d03daf1d Mon Sep 17 00:00:00 2001 From: PengGao Date: Sun, 31 Aug 2025 22:07:46 +0200 Subject: [PATCH 04/11] fix: improve token storage debugging and error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add detailed logging for token exchange and storage process - Fix env-token-manager to return encrypted token properly - Add better error reporting for token response validation - Improve debugging information for OAuth callback process 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/app/api/auth/raindrop/callback/route.js | 11 +++++++++-- src/lib/auth/env-token-manager.js | 19 +++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/app/api/auth/raindrop/callback/route.js b/src/app/api/auth/raindrop/callback/route.js index eef9977..4ddddb3 100644 --- a/src/app/api/auth/raindrop/callback/route.js +++ b/src/app/api/auth/raindrop/callback/route.js @@ -56,7 +56,12 @@ export async function GET(request) { if (!tokenResponse.ok) { const errorData = await tokenResponse.text() - console.error('Token exchange failed:', errorData) + console.error('Token exchange failed:', { + status: tokenResponse.status, + statusText: tokenResponse.statusText, + headers: Object.fromEntries(tokenResponse.headers.entries()), + errorData + }) throw new Error(`Token exchange failed: ${tokenResponse.status}`) } @@ -74,11 +79,13 @@ export async function GET(request) { // 存储令牌 const tokenManager = getTokenManager() - await tokenManager.storeInitialTokens( + const storeResult = await tokenManager.storeInitialTokens( tokenData.access_token, tokenData.refresh_token, tokenData.expires_in || 1209600 // 默认14天 ) + + console.info('Token storage result:', storeResult) // 验证令牌是否工作 const testResponse = await fetch(`${RAINDROP_API_URL}/user`, { diff --git a/src/lib/auth/env-token-manager.js b/src/lib/auth/env-token-manager.js index c4a5075..e063dd2 100644 --- a/src/lib/auth/env-token-manager.js +++ b/src/lib/auth/env-token-manager.js @@ -31,12 +31,13 @@ export class EnvTokenManager { async storeTokenInfo(tokenInfo) { try { - encrypt(JSON.stringify(tokenInfo)) - - // 在这种实现中,我们只能输出加密后的token,需要手动添加到环境变量 - // 注意:在生产环境中,应该通过其他方式传递这个信息 - - return true + const encryptedToken = encrypt(JSON.stringify(tokenInfo)) + + console.info('✅ Token encrypted successfully!') + console.info('🔐 Encrypted token (add this to RAINDROP_ENCRYPTED_REFRESH_TOKEN env var):') + console.info(encryptedToken) + + return encryptedToken } catch (error) { console.error('Failed to encrypt token for storage:', error) return false @@ -122,6 +123,12 @@ export class EnvTokenManager { updatedAt: Date.now() } + console.info('Storing initial tokens:', { + hasAccessToken: !!accessToken, + hasRefreshToken: !!refreshToken, + expiresIn + }) + return await this.storeTokenInfo(tokenInfo) } } From b1d4f25e206410a549c40a50c5a652abdb733603 Mon Sep 17 00:00:00 2001 From: PengGao Date: Sun, 31 Aug 2025 22:11:56 +0200 Subject: [PATCH 05/11] feat(oauth): improve OAuth callback debugging and error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add detailed error reporting for OAuth token response - Separate validation for access_token and refresh_token - Add comprehensive request logging for debugging - Improve error messages to identify specific issues 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/app/api/auth/raindrop/callback/route.js | 49 ++++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/src/app/api/auth/raindrop/callback/route.js b/src/app/api/auth/raindrop/callback/route.js index 4ddddb3..d45037a 100644 --- a/src/app/api/auth/raindrop/callback/route.js +++ b/src/app/api/auth/raindrop/callback/route.js @@ -40,18 +40,27 @@ export async function GET(request) { } // 交换授权码获取访问令牌 + const tokenRequest = { + client_id: clientId, + client_secret: clientSecret, + grant_type: 'authorization_code', + code: code, + redirect_uri: `${baseUrl}/api/auth/raindrop/callback` + } + + console.info('Token exchange request:', { + url: 'https://raindrop.io/oauth/access_token', + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: { ...tokenRequest, client_secret: '***' } // 隐藏敏感信息 + }) + const tokenResponse = await fetch('https://raindrop.io/oauth/access_token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - client_id: clientId, - client_secret: clientSecret, - grant_type: 'authorization_code', - code: code, - redirect_uri: `${baseUrl}/api/auth/raindrop/callback` - }) + body: JSON.stringify(tokenRequest) }) if (!tokenResponse.ok) { @@ -68,13 +77,31 @@ export async function GET(request) { const tokenData = await tokenResponse.json() console.info('Token response from Raindrop:', JSON.stringify(tokenData, null, 2)) - if (!tokenData.access_token || !tokenData.refresh_token) { - console.error('Missing tokens in response:', { + // Check if response contains an error + if (tokenData.error) { + console.error('OAuth error in token response:', tokenData) + throw new Error(`OAuth error: ${tokenData.error} - ${tokenData.error_description || 'Unknown error'}`) + } + + // Validate required tokens + if (!tokenData.access_token) { + console.error('Missing access_token in response:', { + hasAccessToken: !!tokenData.access_token, + hasRefreshToken: !!tokenData.refresh_token, + responseKeys: Object.keys(tokenData), + fullResponse: tokenData + }) + throw new Error('Missing access_token in response') + } + + if (!tokenData.refresh_token) { + console.error('Missing refresh_token in response:', { hasAccessToken: !!tokenData.access_token, hasRefreshToken: !!tokenData.refresh_token, - responseKeys: Object.keys(tokenData) + responseKeys: Object.keys(tokenData), + fullResponse: tokenData }) - throw new Error('Invalid token response') + throw new Error('Missing refresh_token in response') } // 存储令牌 From 7c3894c9ae7b0f497f5f75b802cdd18fc7cd2cfa Mon Sep 17 00:00:00 2001 From: PengGao Date: Sun, 31 Aug 2025 22:58:17 +0200 Subject: [PATCH 06/11] debug(oauth): add environment variable validation and Raindrop.io error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add debugging for client ID and secret validation - Improve error handling for Raindrop.io API error format - Show credential lengths and prefixes for debugging 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/app/api/auth/raindrop/callback/route.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/app/api/auth/raindrop/callback/route.js b/src/app/api/auth/raindrop/callback/route.js index d45037a..77562e5 100644 --- a/src/app/api/auth/raindrop/callback/route.js +++ b/src/app/api/auth/raindrop/callback/route.js @@ -35,6 +35,16 @@ export async function GET(request) { const clientSecret = process.env.RAINDROP_CLIENT_SECRET const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000' + // Debug environment variables + console.info('Environment variables check:', { + hasClientId: !!clientId, + clientIdLength: clientId?.length || 0, + hasClientSecret: !!clientSecret, + clientSecretLength: clientSecret?.length || 0, + clientIdFirst4: clientId?.substring(0, 4) || 'missing', + clientSecretFirst4: clientSecret?.substring(0, 4) || 'missing' + }) + if (!clientId || !clientSecret) { throw new Error('Missing RAINDROP_CLIENT_ID or RAINDROP_CLIENT_SECRET') } @@ -77,11 +87,17 @@ export async function GET(request) { const tokenData = await tokenResponse.json() console.info('Token response from Raindrop:', JSON.stringify(tokenData, null, 2)) - // Check if response contains an error + // Check if response contains an error (handle both OAuth standard and Raindrop.io format) if (tokenData.error) { console.error('OAuth error in token response:', tokenData) throw new Error(`OAuth error: ${tokenData.error} - ${tokenData.error_description || 'Unknown error'}`) } + + // Check for Raindrop.io specific error format + if (tokenData.result === false) { + console.error('Raindrop.io API error:', tokenData) + throw new Error(`Raindrop.io API error: ${tokenData.errorMessage || 'Unknown error'} (status: ${tokenData.status})`) + } // Validate required tokens if (!tokenData.access_token) { From 0ab9108fdbe90edcb410fdcbc9909c8fdc2c0735 Mon Sep 17 00:00:00 2001 From: PengGao Date: Sun, 31 Aug 2025 23:17:19 +0200 Subject: [PATCH 07/11] fix(auth): handle authentication gracefully during build time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Prevent build failures when OAuth tokens are not configured - Return empty results instead of throwing errors during static generation - Add specific error handling for authentication-required scenarios - Improve error messages for debugging authentication issues 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/lib/raindrop-with-auth.js | 60 +++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/src/lib/raindrop-with-auth.js b/src/lib/raindrop-with-auth.js index 088e149..1a4227b 100644 --- a/src/lib/raindrop-with-auth.js +++ b/src/lib/raindrop-with-auth.js @@ -26,7 +26,15 @@ const RAINDROP_API_URL = 'https://api.raindrop.io/rest/v1' async function makeAuthenticatedRequest(url, options = {}) { try { const tokenManager = getTokenManager() - const accessToken = await tokenManager.getValidAccessToken() + + let accessToken + try { + accessToken = await tokenManager.getValidAccessToken() + } catch (authError) { + // 在构建时或没有token时,返回null而不是抛出错误 + console.warn('Authentication not available:', authError.message) + throw new Error('Authentication required but not configured') + } const authOptions = { ...options, @@ -41,23 +49,28 @@ async function makeAuthenticatedRequest(url, options = {}) { // 如果token无效,尝试刷新token并重试一次 if (response.status === 401) { - // 获取新的access token (这会触发refresh) - const newAccessToken = await tokenManager.getValidAccessToken() - - const retryOptions = { - ...authOptions, - headers: { - ...authOptions.headers, - Authorization: `Bearer ${newAccessToken}` + try { + // 获取新的access token (这会触发refresh) + const newAccessToken = await tokenManager.getValidAccessToken() + + const retryOptions = { + ...authOptions, + headers: { + ...authOptions.headers, + Authorization: `Bearer ${newAccessToken}` + } } - } - const retryResponse = await fetch(url, retryOptions) - if (!retryResponse.ok) { - throw new Error(`HTTP error after token refresh! status: ${retryResponse.status}`) - } + const retryResponse = await fetch(url, retryOptions) + if (!retryResponse.ok) { + throw new Error(`HTTP error after token refresh! status: ${retryResponse.status}`) + } - return retryResponse + return retryResponse + } catch (refreshError) { + console.error('Token refresh failed:', refreshError) + throw new Error('Authentication expired and refresh failed') + } } if (!response.ok) { @@ -95,6 +108,11 @@ export const getBookmarkItems = async (id, pageIndex = 0) => { const data = await response.json() return data } catch (error) { + // 在构建时或认证未配置时,返回空结果而不是null + if (error.message.includes('Authentication required but not configured')) { + console.warn(`Bookmark items not available during build for collection ${id} - authentication not configured`) + return { items: [] } + } console.error(`Failed to fetch bookmark items for collection ${id}: ${error.message}`) return null } @@ -114,6 +132,11 @@ export const getBookmarks = async () => { const bookmarks = await response.json() return bookmarks.items.filter((bookmark) => COLLECTION_IDS.includes(bookmark._id)) } catch (error) { + // 在构建时或认证未配置时,返回空数组而不是null + if (error.message.includes('Authentication required but not configured')) { + console.warn('Bookmarks not available during build - authentication not configured') + return [] + } console.error(`Failed to fetch bookmarks: ${error.message}`) return null } @@ -124,7 +147,12 @@ export const getBookmark = async (id) => { const response = await makeAuthenticatedRequest(`${RAINDROP_API_URL}/collection/${id}`) return await response.json() } catch (error) { - console.info(error) + // 在构建时或认证未配置时,返回空结果 + if (error.message.includes('Authentication required but not configured')) { + console.warn(`Bookmark not available during build for id ${id} - authentication not configured`) + return { collection: {} } + } + console.error(`Failed to fetch bookmark ${id}:`, error) return null } } From 51d9359d5c5fd0eeb501fbed463de883a7f916be Mon Sep 17 00:00:00 2001 From: PengGao Date: Sun, 31 Aug 2025 23:23:38 +0200 Subject: [PATCH 08/11] debug(oauth): add comprehensive callback logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add detailed logging at callback entry point - Log request URL and parameters for debugging - Ensure we can see when callback is triggered 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/app/api/auth/raindrop/callback/route.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app/api/auth/raindrop/callback/route.js b/src/app/api/auth/raindrop/callback/route.js index 77562e5..b61b692 100644 --- a/src/app/api/auth/raindrop/callback/route.js +++ b/src/app/api/auth/raindrop/callback/route.js @@ -17,9 +17,19 @@ function getTokenManager() { const RAINDROP_API_URL = 'https://api.raindrop.io/rest/v1' export async function GET(request) { + console.info('=== OAuth Callback Started ===') + console.info('Request URL:', request.url) + const { searchParams } = new URL(request.url) const code = searchParams.get('code') const error = searchParams.get('error') + + console.info('URL Parameters:', { + hasCode: !!code, + hasError: !!error, + codeLength: code?.length || 0, + error: error || 'none' + }) if (error) { console.error('OAuth error:', error) @@ -27,6 +37,7 @@ export async function GET(request) { } if (!code) { + console.error('No authorization code found in callback') return NextResponse.json({ error: 'Authorization code not found' }, { status: 400 }) } From c18d876f12b6d52d958e94708d4c30b742ccc48f Mon Sep 17 00:00:00 2001 From: PengGao Date: Sun, 31 Aug 2025 23:49:56 +0200 Subject: [PATCH 09/11] debug(oauth): add detailed environment variable debugging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Show all RAINDROP env keys available - Display actual values for debugging - Check Vercel environment detection - Add error logging for missing credentials 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/app/api/auth/raindrop/callback/route.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/app/api/auth/raindrop/callback/route.js b/src/app/api/auth/raindrop/callback/route.js index b61b692..9a750e0 100644 --- a/src/app/api/auth/raindrop/callback/route.js +++ b/src/app/api/auth/raindrop/callback/route.js @@ -42,10 +42,21 @@ export async function GET(request) { } try { + console.info('=== Starting token exchange process ===') + const clientId = process.env.RAINDROP_CLIENT_ID const clientSecret = process.env.RAINDROP_CLIENT_SECRET const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000' + console.info('Raw environment variables:', { + NODE_ENV: process.env.NODE_ENV, + hasVercelEnv: !!process.env.VERCEL, + allEnvKeys: Object.keys(process.env).filter(key => key.includes('RAINDROP')), + clientIdValue: clientId || 'MISSING', + clientSecretValue: clientSecret ? `${clientSecret.substring(0, 4)}...` : 'MISSING', + baseUrlValue: baseUrl + }) + // Debug environment variables console.info('Environment variables check:', { hasClientId: !!clientId, @@ -57,6 +68,10 @@ export async function GET(request) { }) if (!clientId || !clientSecret) { + console.error('MISSING CREDENTIALS:', { + clientId: clientId || 'UNDEFINED', + clientSecret: clientSecret || 'UNDEFINED' + }) throw new Error('Missing RAINDROP_CLIENT_ID or RAINDROP_CLIENT_SECRET') } From 48c9bbd11f38ce772e0cb2b5042ef0bf2fdccec7 Mon Sep 17 00:00:00 2001 From: PengGao Date: Mon, 1 Sep 2025 00:11:10 +0200 Subject: [PATCH 10/11] fix(auth): use dynamic token manager in status and clear APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix status API to use correct token manager based on environment - Fix clear API to use dynamic token manager selection - Add compatibility for different token manager interfaces - Add debugging for token status checks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/app/api/auth/raindrop/clear/route.js | 23 +++++++++++-- src/app/api/auth/raindrop/status/route.js | 41 +++++++++++++++++++++-- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/app/api/auth/raindrop/clear/route.js b/src/app/api/auth/raindrop/clear/route.js index c109f08..e72b10b 100644 --- a/src/app/api/auth/raindrop/clear/route.js +++ b/src/app/api/auth/raindrop/clear/route.js @@ -1,10 +1,29 @@ import { NextResponse } from 'next/server' -import { tokenManager } from '@/lib/auth/token-manager' +// 动态选择 token manager +function getTokenManager() { + if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) { + const { tokenManager } = require('@/lib/auth/token-manager') + return tokenManager + } + if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUPABASE_ANON_KEY) { + const { getTokenManager } = require('@/lib/auth/supabase-token-manager') + return getTokenManager() + } + const { getTokenManager } = require('@/lib/auth/env-token-manager') + return getTokenManager() +} export async function POST() { try { - await tokenManager.clearTokens() + const tokenManager = getTokenManager() + + // 不同的token manager可能有不同的清除方法 + if (tokenManager.clearTokens) { + await tokenManager.clearTokens() + } else { + console.warn('Token manager does not support clearing tokens') + } return NextResponse.json({ success: true, diff --git a/src/app/api/auth/raindrop/status/route.js b/src/app/api/auth/raindrop/status/route.js index 467dea5..7bb8332 100644 --- a/src/app/api/auth/raindrop/status/route.js +++ b/src/app/api/auth/raindrop/status/route.js @@ -1,10 +1,47 @@ import { NextResponse } from 'next/server' -import { tokenManager } from '@/lib/auth/token-manager' +// 动态选择 token manager +function getTokenManager() { + if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) { + const { tokenManager } = require('@/lib/auth/token-manager') + return tokenManager + } + if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUPABASE_ANON_KEY) { + const { getTokenManager } = require('@/lib/auth/supabase-token-manager') + return getTokenManager() + } + const { getTokenManager } = require('@/lib/auth/env-token-manager') + return getTokenManager() +} export async function GET() { try { - const tokenInfo = await tokenManager.getTokenInfo() + console.info('=== Token Status Check ===') + const tokenManager = getTokenManager() + + // 检查是否有存储的token信息 + let tokenInfo = null + try { + // 尝试获取存储的token信息 + if (tokenManager.getTokenInfo) { + tokenInfo = await tokenManager.getTokenInfo() + } else if (tokenManager.getStoredTokenInfo) { + // env-token-manager 使用不同的方法名 + const storedInfo = await tokenManager.getStoredTokenInfo() + if (storedInfo) { + tokenInfo = { + hasTokens: true, + expiresAt: new Date(storedInfo.accessExpiresAt).toISOString(), + lastRefreshed: new Date(storedInfo.updatedAt).toISOString(), + isExpired: storedInfo.accessExpiresAt < Date.now() + } + } + } + } catch (tokenError) { + console.warn('Could not retrieve token info:', tokenError.message) + } + + console.info('Token status result:', { hasTokens: !!tokenInfo }) if (!tokenInfo) { return NextResponse.json({ hasTokens: false }) From b4915246f9d3fc3291043742b03f974925a9be3d Mon Sep 17 00:00:00 2001 From: PengGao Date: Mon, 1 Sep 2025 00:35:59 +0200 Subject: [PATCH 11/11] docs: add work log and update OAuth setup documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive work log for 2025-08-31 OAuth implementation - Update README with complete OAuth 2.0 setup instructions - Replace outdated token-based auth with OAuth flow documentation - Include troubleshooting guide and multiple storage strategies - Document manual encrypted token setup process 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 119 +++++++++++++++---------- docs/work-log-2025-08-31.md | 169 ++++++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+), 44 deletions(-) create mode 100644 docs/work-log-2025-08-31.md diff --git a/README.md b/README.md index 5e7731c..07c477c 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,11 @@ MUSING_CODE=your_secret_validation_code GITHUB_PAT=ghp_your_github_personal_acce # Analytics & Monitoring NEXT_PUBLIC_TINYBIRD_TOKEN=your_tinybird_analytics_token # Optional: additional analytics -# Bookmarks Integration -NEXT_PUBLIC_RAINDROP_ACCESS_TOKEN=your_raindrop_io_access_token # For Raindrop.io bookmarks feature +# Raindrop.io OAuth Integration (for Bookmarks) +RAINDROP_CLIENT_ID=your_raindrop_oauth_client_id +RAINDROP_CLIENT_SECRET=your_raindrop_oauth_client_secret +RAINDROP_ENCRYPTION_KEY=your_32_character_encryption_key +RAINDROP_ENCRYPTED_REFRESH_TOKEN=generated_after_oauth_completion # Revalidation & Cache Management NEXT_REVALIDATE_SECRET=your_nextjs_revalidation_secret @@ -218,25 +221,22 @@ If you've published new GitHub Issues but the blog doesn't show the latest conte - ISR cache will automatically expire after 24 hours - The first visitor will trigger page regeneration -## Raindrop.io Setup (Bookmarks Feature) +## Raindrop.io OAuth Setup (Bookmarks Feature) -This project includes a bookmarks feature that integrates with [Raindrop.io](https://raindrop.io) to display your saved -bookmarks. Follow these steps to set up the integration properly: +This project includes a bookmarks feature that integrates with [Raindrop.io](https://raindrop.io) using OAuth 2.0 authentication to securely access your saved bookmarks. The system supports multiple token storage strategies and automatic token refresh. ### Prerequisites - A Raindrop.io account (free account works) - Some bookmark collections created in your Raindrop.io account -### Step 1: Create a Raindrop.io Application +### Step 1: Create a Raindrop.io OAuth Application 1. **Login to Raindrop.io**: - - Go to [https://raindrop.io](https://raindrop.io) - Sign in to your account 2. **Access Developer Settings**: - - Click on your profile avatar in the top-right corner - Select "Settings" - In the left sidebar, find and click "Integrations" @@ -245,26 +245,33 @@ bookmarks. Follow these steps to set up the integration properly: 3. **Create New Application**: - Click "Create new app" - Fill in the application details: - - **Name**: `Personal Website` (or any name you prefer) - - **Description**: `For personal website bookmarks display` + - **Name**: `Personal Website OAuth` (or any name you prefer) + - **Description**: `OAuth integration for personal website bookmarks` - **Site**: Your website URL (e.g., `https://yourdomain.com`) - - **Redirect URI**: Your website URL (same as above) + - **Redirect URI**: `https://yourdomain.com/api/auth/raindrop/callback` ⚠️ **Critical: Must be exact** + +### Step 2: Configure Environment Variables + +Add the following variables to your Vercel project settings or `.env` file: + +```bash +# Raindrop.io OAuth Credentials +RAINDROP_CLIENT_ID=your_client_id_from_raindrop_app +RAINDROP_CLIENT_SECRET=your_client_secret_from_raindrop_app -### Step 2: Generate Access Token +# Encryption Key for Token Storage (32 characters) +RAINDROP_ENCRYPTION_KEY=generate_a_32_character_random_string -1. **Get Test Token**: - - After creating the application, you'll see the app details page - - In the "Credentials" section, find the "Test token" row - - Click "Create test token" button - - Copy the generated token (this is your access token) +# Base URL for OAuth Callbacks +NEXT_PUBLIC_BASE_URL=https://yourdomain.com -> **Important**: Use the "Test token", NOT the "Client secret". The Client secret is used for OAuth flows, while the -> Test token is for direct API access. +# Encrypted Tokens (Set after OAuth completion - see Step 4) +RAINDROP_ENCRYPTED_REFRESH_TOKEN=will_be_generated_during_oauth_setup +``` ### Step 3: Configure Collection IDs 1. **Find Your Collection IDs**: - - In Raindrop.io, go to your collections - The collection ID can be found in the URL when viewing a collection - For example, in `https://app.raindrop.io/my/12345678`, the ID is `12345678` @@ -275,44 +282,68 @@ bookmarks. Follow these steps to set up the integration properly: ```javascript export const COLLECTION_IDS = [ 12345678, // Replace with your actual collection IDs - 87654321 - // Add more collection IDs as needed + 87654321 // Add more collection IDs as needed ] ``` -### Step 4: Set Environment Variable +### Step 4: Complete OAuth Setup -Add the access token to your `.env` file: +1. **Access OAuth Setup Page**: + - Navigate to `/admin/raindrop-setup` on your website + - You should see the OAuth setup interface -``` -NEXT_PUBLIC_RAINDROP_ACCESS_TOKEN=your_actual_test_token_here -``` +2. **Start OAuth Flow**: + - Click "开始 OAuth 认证" (Start OAuth Authentication) + - You'll be redirected to Raindrop.io for authorization + - Click "Agree" to authorize your application -### Step 5: Restart Development Server +3. **Copy Encrypted Token**: + - After successful authorization, check your Vercel function logs + - Look for a message like: + ``` + 🔐 Encrypted token (add this to RAINDROP_ENCRYPTED_REFRESH_TOKEN env var): + [long encrypted string] + ``` + - Copy this encrypted string -After updating the environment variables, restart your Next.js development server: +4. **Set Final Environment Variable**: + - In Vercel project settings, add/update: + - `RAINDROP_ENCRYPTED_REFRESH_TOKEN=[paste the encrypted string here]` + - Redeploy or wait for automatic deployment -```bash -npm run dev -# or -bun dev -``` +5. **Verify Setup**: + - Return to `/admin/raindrop-setup` + - Status should now show "已认证" (Authenticated) + - Visit `/bookmarks` to see your bookmark collections -### Troubleshooting +### Token Storage Strategies -- **401 Unauthorized Error**: Ensure you're using the Test token, not the Client secret -- **Empty Bookmarks**: Verify that your collection IDs in `constants.js` match your actual Raindrop.io collections -- **Network Errors**: Check if your Raindrop.io account has the collections you're trying to access +The system automatically selects the best available storage method: -### API Rate Limits +1. **Vercel KV** (if `KV_REST_API_URL` and `KV_REST_API_TOKEN` are set) +2. **Supabase** (if `NEXT_PUBLIC_SUPABASE_URL` and `SUPABASE_ANON_KEY` are set) +3. **Environment Variables** (fallback - requires manual encrypted token setup) + +### Features + +- **Automatic Token Refresh**: Access tokens are automatically refreshed when needed +- **Secure Storage**: All tokens are encrypted before storage +- **Build-time Safe**: Gracefully handles missing authentication during static generation +- **Multiple Storage Options**: Supports different deployment environments +- **Admin Interface**: Web-based setup and status monitoring + +### Troubleshooting -Raindrop.io has API rate limits. The current implementation includes: +- **"未认证" Status**: Complete the OAuth flow and set the encrypted token environment variable +- **Build Failures**: Ensure all environment variables are set correctly +- **Empty Bookmarks**: Verify collection IDs in `constants.js` match your Raindrop.io collections +- **OAuth Errors**: Check that redirect URI exactly matches your application settings -- Cache duration: 2 days for bookmark data -- Request timeout: 10 seconds -- Automatic error handling for failed requests +### API Information -For more information about Raindrop.io API, visit: [https://developer.raindrop.io](https://developer.raindrop.io) +- **Token Lifetime**: Access tokens expire after 2 weeks, automatically refreshed +- **Cache Duration**: Bookmark data is cached for 2 days +- **Rate Limits**: Automatic handling with appropriate timeouts ## Analytics & Monitoring diff --git a/docs/work-log-2025-08-31.md b/docs/work-log-2025-08-31.md new file mode 100644 index 0000000..ebae349 --- /dev/null +++ b/docs/work-log-2025-08-31.md @@ -0,0 +1,169 @@ +# 工作日志 - 2025年8月31日 + +**日期**: 2025-08-31 +**时间**: 20:00 - 23:00 CST +**主要任务**: 修复 Raindrop.io OAuth 认证和书签同步功能 + +## 问题描述 + +用户在成功合并 PR 并部署后发现书签页面(`/bookmarks`)无法显示内容,根本原因是 Raindrop.io OAuth 认证未配置完成。 + +## 解决过程 + +### 1. 问题诊断 (20:00-20:30) + +- **发现**: 书签页面调用 `getBookmarks()` 函数失败 +- **根本原因**: "No refresh token available. Please complete OAuth setup" +- **影响范围**: 所有需要 Raindrop.io API 的功能都无法使用 + +### 2. OAuth 流程修复 (20:30-22:00) + +#### 2.1 修复 OAuth 授权 URL +- **文件**: `src/app/api/auth/raindrop/route.js:12` +- **问题**: 使用了 v1 API 端点导致 500 错误 +- **修复**: 改为 v2 API 端点 `https://api.raindrop.io/v2/oauth/authorize` + +#### 2.2 修复环境变量配置 +- **问题**: Vercel 中的 `RAINDROP_CLIENT_SECRET` 配置错误 +- **解决**: 用户更正了 Vercel 环境变量 + +#### 2.3 修复 Token Manager 选择逻辑 +- **文件**: `src/app/api/auth/raindrop/callback/route.js:4-15` +- **问题**: 回调函数没有使用动态 token manager 选择 +- **修复**: 实现与 `raindrop-with-auth.js` 相同的动态选择逻辑 + +#### 2.4 修复状态检查 API +- **文件**: `src/app/api/auth/raindrop/status/route.js` +- **问题**: 硬编码使用 `token-manager` 而不是动态选择 +- **修复**: 实现动态 token manager 选择和兼容不同接口 + +### 3. 核心问题发现 (22:00-22:30) + +#### 3.1 env-token-manager 工作机制理解 +- **关键发现**: `env-token-manager` 需要手动设置 `RAINDROP_ENCRYPTED_REFRESH_TOKEN` 环境变量 +- **工作流程**: OAuth成功 → 加密token → 输出到日志 → 手动设置环境变量 +- **缺失步骤**: 用户需要将加密的 token 添加到 Vercel 环境变量 + +#### 3.2 最终解决方案 +- **环境变量**: `RAINDROP_ENCRYPTED_REFRESH_TOKEN` +- **值**: 从 OAuth 回调日志中获取的加密字符串 +- **结果**: 认证状态从"未认证"变为"已认证",书签功能正常工作 + +### 4. 构建时认证问题修复 (22:30-23:00) + +#### 4.1 静态生成时的认证错误 +- **文件**: `src/lib/raindrop-with-auth.js:26-85` +- **问题**: 构建时调用认证 API 导致部署失败 +- **修复**: 添加优雅的错误处理,构建时返回空数据而不是抛出错误 + +## 关键代码修改 + +### 1. OAuth 回调增强 (`src/app/api/auth/raindrop/callback/route.js`) + +```javascript +// 添加详细的环境变量调试 +console.info('Raw environment variables:', { + NODE_ENV: process.env.NODE_ENV, + hasVercelEnv: !!process.env.VERCEL, + allEnvKeys: Object.keys(process.env).filter(key => key.includes('RAINDROP')), + clientIdValue: clientId || 'MISSING', + clientSecretValue: clientSecret ? `${clientSecret.substring(0, 4)}...` : 'MISSING', + baseUrlValue: baseUrl +}) + +// 改进的错误处理 +if (tokenData.result === false) { + console.error('Raindrop.io API error:', tokenData) + throw new Error(`Raindrop.io API error: ${tokenData.errorMessage || 'Unknown error'} (status: ${tokenData.status})`) +} +``` + +### 2. 认证请求优雅处理 (`src/lib/raindrop-with-auth.js`) + +```javascript +// 构建时认证处理 +try { + accessToken = await tokenManager.getValidAccessToken() +} catch (authError) { + console.warn('Authentication not available:', authError.message) + throw new Error('Authentication required but not configured') +} + +// 构建时返回空数据 +if (error.message.includes('Authentication required but not configured')) { + console.warn('Bookmarks not available during build - authentication not configured') + return [] +} +``` + +### 3. 动态 Token Manager 选择 + +所有 API 端点现在都使用统一的动态选择逻辑: + +```javascript +function getTokenManager() { + if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) { + const { tokenManager } = require('@/lib/auth/token-manager') + return tokenManager + } + if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUPABASE_ANON_KEY) { + const { getTokenManager } = require('@/lib/auth/supabase-token-manager') + return getTokenManager() + } + const { getTokenManager } = require('@/lib/auth/env-token-manager') + return getTokenManager() +} +``` + +## 环境变量要求 + +### 必需的环境变量 + +```bash +# Raindrop.io 应用凭据 +RAINDROP_CLIENT_ID=6826e3e0149b4706a991e02d +RAINDROP_CLIENT_SECRET=26a1ed7a-17ff-4700-b396-1ba0d989db5e + +# 加密密钥 +RAINDROP_ENCRYPTION_KEY=c567419f3383e7c5b0d6148867f81328 + +# 加密的刷新令牌(OAuth完成后设置) +RAINDROP_ENCRYPTED_REFRESH_TOKEN=[从OAuth回调日志中获取] + +# 基础URL +NEXT_PUBLIC_BASE_URL=https://me.deeptoai.com +``` + +## 经验教训 + +### 关键失误 +1. **对 env-token-manager 工作机制的误解** - 以为是自动存储,实际需要手动设置环境变量 +2. **忽略成功日志中的操作指示** - 日志明确说明需要设置环境变量 +3. **症状治疗而非根本治疗** - 过度修改代码而不是理解现有架构 + +### 正确的诊断顺序 +1. 完整的认证链路检查 (OAuth → Token交换 → Token存储 → Token读取 → API认证) +2. 仔细阅读现有代码逻辑和存储机制 +3. 关注成功日志而不只是错误日志 +4. 理解不同存储策略的工作方式 + +### 优化建议 +- **先理解再修改**: 在添加新代码之前完全理解现有实现 +- **日志是朋友**: 成功日志和错误日志一样重要 +- **最小可行诊断**: 先找到根本原因再进行修复 + +## 最终结果 + +- ✅ OAuth 认证流程完全正常工作 +- ✅ 书签页面正确显示内容 +- ✅ 认证状态正确显示 +- ✅ 构建过程不再因认证问题失败 +- ✅ 所有 API 端点使用统一的 token manager 选择逻辑 + +**总耗时**: 约3小时 +**实际需要时间**: 约10分钟(如果直接识别根本原因) + +--- + +*记录者: Claude Code* +*最后更新: 2025-08-31 23:00* \ No newline at end of file