| 76 | * Returns null if authorized, or a NextResponse with error if unauthorized |
| 77 | */ |
| 78 | export function verifyCronAuth(request: NextRequest, context?: string): NextResponse | null { |
| 79 | if (!env.CRON_SECRET) { |
| 80 | const contextInfo = context ? ` for ${context}` : '' |
| 81 | logger.warn(`CRON endpoint accessed but CRON_SECRET is not configured${contextInfo}`, { |
| 82 | ip: getClientIp(request), |
| 83 | userAgent: request.headers.get('user-agent') ?? 'unknown', |
| 84 | context, |
| 85 | }) |
| 86 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) |
| 87 | } |
| 88 | |
| 89 | const authHeader = request.headers.get('authorization') |
| 90 | const expectedAuth = `Bearer ${env.CRON_SECRET}` |
| 91 | const isValid = authHeader !== null && safeCompare(authHeader, expectedAuth) |
| 92 | if (!isValid) { |
| 93 | const contextInfo = context ? ` for ${context}` : '' |
| 94 | logger.warn(`Unauthorized CRON access attempt${contextInfo}`, { |
| 95 | hasAuthorizationHeader: authHeader !== null, |
| 96 | ip: getClientIp(request), |
| 97 | userAgent: request.headers.get('user-agent') ?? 'unknown', |
| 98 | context, |
| 99 | }) |
| 100 | |
| 101 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) |
| 102 | } |
| 103 | |
| 104 | return null |
| 105 | } |