diff --git a/netlify/edge-functions/bff-proxy.js b/netlify/edge-functions/bff-proxy.js index a6cecb4..28dfa89 100644 --- a/netlify/edge-functions/bff-proxy.js +++ b/netlify/edge-functions/bff-proxy.js @@ -14,7 +14,8 @@ const BFF_ROUTES = new Map([ ['auto-activate-esim', ['POST', 'OPTIONS']], ['qrcode-generate', ['POST', 'OPTIONS']], ['verify-cookie', ['POST', 'OPTIONS']], - ['public-config', ['GET', 'OPTIONS']] + ['public-config', ['GET', 'OPTIONS']], + ['health', ['GET', 'OPTIONS']] ]); // 注意:此文件运行在 Deno(Edge Function),无法直接引用 Node.js 的 _shared/cors.js。 @@ -80,7 +81,7 @@ export default async (request, context) => { const sameOrigin = requestOrigin === url.origin; const configuredOrigin = allowedOrigins.includes(requestOrigin); const corsOrigin = sameOrigin || configuredOrigin ? requestOrigin : ''; - const allowMissingOriginForPublicGet = targetName === 'public-config' && request.method === 'GET'; + const allowMissingOriginForPublicGet = (targetName === 'public-config' || targetName === 'health') && request.method === 'GET'; const fallbackCorsOrigin = allowedOrigins[0] || DEFAULT_ALLOWED_ORIGIN; const errorCorsHeaders = buildCorsHeaders(corsOrigin || fallbackCorsOrigin); diff --git a/netlify/functions/health.js b/netlify/functions/health.js index 1cb6bcb..32fc293 100644 --- a/netlify/functions/health.js +++ b/netlify/functions/health.js @@ -1,48 +1,73 @@ +'use strict'; + /** * Netlify Function: Health Check - * 提供健康检查端点,用于监控服务状态 + * 提供分层健康检查端点,用于监控服务状态 + * + * 公开层(无需认证):返回基础状态信息 + * 私有层(通过 BFF + ACCESS_KEY 认证):返回环境变量检查计数(隐藏变量名) */ -exports.handler = async (event, context) => { - const health = { - status: 'healthy', - timestamp: new Date().toISOString(), - service: 'eSIM-Tools', - version: process.env.APP_VERSION || '2.0.0', - environment: process.env.NODE_ENV || 'production', - uptime: process.uptime(), - checks: { - accessKey: !!process.env.ACCESS_KEY, - giffgaffClientId: !!process.env.GIFFGAFF_CLIENT_ID, - giffgaffClientSecret: !!process.env.GIFFGAFF_CLIENT_SECRET - } - }; +const { withAuth } = require('./_shared/middleware'); - // 检查关键环境变量 - const missingConfigs = []; - if (!process.env.ACCESS_KEY) { - missingConfigs.push('ACCESS_KEY'); - } - if (!process.env.GIFFGAFF_CLIENT_ID) { - missingConfigs.push('GIFFGAFF_CLIENT_ID'); - } - if (!process.env.GIFFGAFF_CLIENT_SECRET) { - missingConfigs.push('GIFFGAFF_CLIENT_SECRET'); - } +// 需要检查的关键环境变量列表(仅内部使用,不对外暴露) +const CRITICAL_ENV_VARS = [ + 'ACCESS_KEY', + 'ALLOWED_ORIGIN', + 'GIFFGAFF_CLIENT_ID', + 'GIFFGAFF_CLIENT_SECRET' +]; - if (missingConfigs.length > 0) { - health.status = 'degraded'; - health.warnings = missingConfigs.map(key => `${key} not configured`); - } +const OPTIONAL_ENV_VARS = [ + 'SENTRY_DSN', + 'CAPTCHA_PROVIDER' +]; - const statusCode = health.status === 'healthy' ? 200 : 503; +/** + * 检查环境变量配置状态 + * @returns {{ total: number, configured: number, missing: number, optional: { total: number, configured: number } }} + */ +function checkEnvStatus() { + const criticalMissing = CRITICAL_ENV_VARS.filter(key => !process.env[key]); + const optionalConfigured = OPTIONAL_ENV_VARS.filter(key => !!process.env[key]); return { - statusCode, - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-cache, no-store, must-revalidate' - }, + total: CRITICAL_ENV_VARS.length, + configured: CRITICAL_ENV_VARS.length - criticalMissing.length, + missing: criticalMissing.length, + optional: { + total: OPTIONAL_ENV_VARS.length, + configured: optionalConfigured.length + } + }; +} + +const handler = async (event, context, { auth }) => { + // 基础健康信息(公开) + const envStatus = checkEnvStatus(); + const isHealthy = envStatus.missing === 0; + const health = { + status: isHealthy ? 'healthy' : 'degraded', + service: 'eSIM-Tools', + timestamp: new Date().toISOString() + }; + + // 私有层:通过 BFF 认证后返回详细检查(BFF 已注入 ACCESS_KEY 做认证) + const isDetailRequest = event.queryStringParameters && event.queryStringParameters.detail === 'true'; + if (isDetailRequest) { + health.version = process.env.APP_VERSION || '2.0.0'; + health.environment = process.env.NODE_ENV || 'production'; + health.uptime = Math.floor(process.uptime()); + health.checks = { + env: envStatus + }; + } + + return { + statusCode: isHealthy ? 200 : 503, body: JSON.stringify(health, null, 2) }; }; + +// 公开端点:无需 ACCESS_KEY,仅做 CORS 校验 +exports.handler = withAuth(handler, { requireAuth: false }); diff --git a/netlify/functions/notifications-internal.js b/netlify/functions/notifications-internal.js deleted file mode 100644 index 297c65d..0000000 --- a/netlify/functions/notifications-internal.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -/** - * 内部通知API(兼容层) - * 已合并至 notifications.js,此文件仅为向后兼容保留路由。 - * 前端应迁移至 /.netlify/functions/notifications - */ - -module.exports = require('./notifications');