Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/app/api/revalidate/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { headers } from 'next/headers'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest'

import { _handleRevalidateRequest } from './route'
import { _handleRevalidateRequest } from './route.utils'

// Mock Next.js modules
vi.mock('next/cache', () => ({
Expand Down
102 changes: 1 addition & 101 deletions apps/docs/app/api/revalidate/route.ts
Original file line number Diff line number Diff line change
@@ -1,109 +1,9 @@
import { createClient } from '@supabase/supabase-js'
import { type Database } from 'common'
import { revalidateTag } from 'next/cache'
import { headers } from 'next/headers'
import { type NextRequest } from 'next/server'
import { z } from 'zod'
import { VALID_REVALIDATION_TAGS } from '~/features/helpers.fetch'

enum AuthorizationLevel {
Unauthorized,
Basic,
Override,
}

const requestBodySchema = z.object({
tags: z.array(z.enum(VALID_REVALIDATION_TAGS)),
})
import { _handleRevalidateRequest } from './route.utils'

export const POST = handleError(_handleRevalidateRequest)

export async function _handleRevalidateRequest(request: NextRequest) {
const requestHeaders = await headers()
const authorization = requestHeaders.get('Authorization')
if (!authorization) {
return new Response('Missing Authorization header', { status: 401 })
}

const basicKeys = process.env.DOCS_REVALIDATION_KEYS?.split(/\s*,\s*/) ?? []
const overrideKeys = process.env.DOCS_REVALIDATION_OVERRIDE_KEYS?.split(/\s*,\s*/) ?? []
if (basicKeys.length === 0 && overrideKeys.length === 0) {
console.error('No keys configured for revalidation')
return new Response('Internal server error', {
status: 500,
})
}

let authorizationLevel = AuthorizationLevel.Unauthorized
const token = authorization.replace(/^Bearer /, '')
if (overrideKeys.includes(token)) {
authorizationLevel = AuthorizationLevel.Override
} else if (basicKeys.includes(token)) {
authorizationLevel = AuthorizationLevel.Basic
}
if (authorizationLevel === AuthorizationLevel.Unauthorized) {
return new Response('Invalid Authorization header', { status: 401 })
}

let result: z.infer<typeof requestBodySchema>
try {
result = requestBodySchema.parse(await request.json())
} catch (error) {
console.error(error)
return new Response(
'Malformed request body: should be a JSON object with a "tags" array of strings.',
{ status: 400 }
)
}

const supabaseAdmin = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SECRET_KEY!
)

if (authorizationLevel === AuthorizationLevel.Basic) {
const { data: lastRevalidation, error } = await supabaseAdmin.rpc(
'get_last_revalidation_for_tags',
{
tags: result.tags,
}
)
if (error) {
console.error(error)
return new Response('Internal server error', { status: 500 })
}

const sixHoursAgo = new Date()
sixHoursAgo.setHours(sixHoursAgo.getHours() - 6)
if (lastRevalidation?.some((revalidation) => new Date(revalidation.created_at) > sixHoursAgo)) {
return new Response(
'Your request includes a tag that has been revalidated within the last 6 hours. You can override this limit by authenticating with Override permissions.',
{
status: 429,
}
)
}
}

const { error } = await supabaseAdmin
.from('validation_history')
.insert(result.tags.map((tag) => ({ tag })))
if (error) {
console.error('Failed to update revalidation table: %o', error)
}

result.tags.forEach((tag) => {
revalidateTag(tag)
})

return new Response(null, {
status: 204,
headers: {
'Cache-Control': 'no-cache',
},
})
}

function handleError(handleRequest: (request: NextRequest) => Promise<Response>) {
return async function (request: NextRequest) {
try {
Expand Down
103 changes: 103 additions & 0 deletions apps/docs/app/api/revalidate/route.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { createClient } from '@supabase/supabase-js'
import { type Database } from 'common'
import { revalidateTag } from 'next/cache'
import { headers } from 'next/headers'
import { type NextRequest } from 'next/server'
import { z } from 'zod'
import { VALID_REVALIDATION_TAGS } from '~/features/helpers.fetch'

enum AuthorizationLevel {
Unauthorized,
Basic,
Override,
}

const requestBodySchema = z.object({
tags: z.array(z.enum(VALID_REVALIDATION_TAGS)),
})

export async function _handleRevalidateRequest(request: NextRequest) {
const requestHeaders = await headers()
const authorization = requestHeaders.get('Authorization')
if (!authorization) {
return new Response('Missing Authorization header', { status: 401 })
}

const basicKeys = process.env.DOCS_REVALIDATION_KEYS?.split(/\s*,\s*/) ?? []
const overrideKeys = process.env.DOCS_REVALIDATION_OVERRIDE_KEYS?.split(/\s*,\s*/) ?? []
if (basicKeys.length === 0 && overrideKeys.length === 0) {
console.error('No keys configured for revalidation')
return new Response('Internal server error', {
status: 500,
})
}

let authorizationLevel = AuthorizationLevel.Unauthorized
const token = authorization.replace(/^Bearer /, '')
if (overrideKeys.includes(token)) {
authorizationLevel = AuthorizationLevel.Override
} else if (basicKeys.includes(token)) {
authorizationLevel = AuthorizationLevel.Basic
}
if (authorizationLevel === AuthorizationLevel.Unauthorized) {
return new Response('Invalid Authorization header', { status: 401 })
}

let result: z.infer<typeof requestBodySchema>
try {
result = requestBodySchema.parse(await request.json())
} catch (error) {
console.error(error)
return new Response(
'Malformed request body: should be a JSON object with a "tags" array of strings.',
{ status: 400 }
)
}

const supabaseAdmin = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SECRET_KEY!
)

if (authorizationLevel === AuthorizationLevel.Basic) {
const { data: lastRevalidation, error } = await supabaseAdmin.rpc(
'get_last_revalidation_for_tags',
{
tags: result.tags,
}
)
if (error) {
console.error(error)
return new Response('Internal server error', { status: 500 })
}

const sixHoursAgo = new Date()
sixHoursAgo.setHours(sixHoursAgo.getHours() - 6)
if (lastRevalidation?.some((revalidation) => new Date(revalidation.created_at) > sixHoursAgo)) {
return new Response(
'Your request includes a tag that has been revalidated within the last 6 hours. You can override this limit by authenticating with Override permissions.',
{
status: 429,
}
)
}
}

const { error } = await supabaseAdmin
.from('validation_history')
.insert(result.tags.map((tag) => ({ tag })))
if (error) {
console.error('Failed to update revalidation table: %o', error)
}

result.tags.forEach((tag) => {
revalidateTag(tag)
})

return new Response(null, {
status: 204,
headers: {
'Cache-Control': 'no-cache',
},
})
}
Loading
Loading