134 lines
3.7 KiB
TypeScript
134 lines
3.7 KiB
TypeScript
import { applyCachePolicy, isContentMutation } from '$lib/server/cache';
|
|
import { purgePublicCache } from '$lib/server/cloudflare';
|
|
import { dev } from '$app/environment';
|
|
import { verifyAccessJwt } from '$lib/server/access';
|
|
import type { Handle } from '@sveltejs/kit';
|
|
|
|
function unauthorized(): Response {
|
|
return new Response('身份验证失败:Access 凭证校验未通过', {
|
|
status: 401,
|
|
headers: {
|
|
'Cache-Control': 'no-store',
|
|
'Content-Type': 'text/plain; charset=utf-8'
|
|
}
|
|
});
|
|
}
|
|
|
|
function accessNotConfigured(): Response {
|
|
return new Response('后台认证尚未配置', {
|
|
status: 503,
|
|
headers: {
|
|
'Cache-Control': 'no-store',
|
|
'Content-Type': 'text/plain; charset=utf-8'
|
|
}
|
|
});
|
|
}
|
|
|
|
const authenticate: Handle = async ({ event, resolve }) => {
|
|
if (event.url.pathname.startsWith('/api/v1/')) {
|
|
const authHeader = event.request.headers.get('Authorization');
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
return new Response(JSON.stringify({ error: 'Unauthorized: Missing or invalid token' }), {
|
|
status: 401,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}
|
|
|
|
const token = authHeader.slice('Bearer '.length).trim();
|
|
const db = event.platform?.env.DB;
|
|
if (!db) {
|
|
return new Response(JSON.stringify({ error: 'Database not available' }), {
|
|
status: 500,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}
|
|
|
|
try {
|
|
const result = await db
|
|
.prepare("SELECT value FROM settings WHERE key = 'agent.api_token'")
|
|
.first<{ value: string }>();
|
|
|
|
const validToken = result?.value;
|
|
if (!validToken || token !== validToken) {
|
|
return new Response(JSON.stringify({ error: 'Unauthorized: Invalid token' }), {
|
|
status: 401,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error('API token verification failed', err);
|
|
return new Response(JSON.stringify({ error: 'Internal Server Error' }), {
|
|
status: 500,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}
|
|
|
|
return resolve(event);
|
|
}
|
|
|
|
if (!event.url.pathname.startsWith('/admin')) {
|
|
return resolve(event);
|
|
}
|
|
|
|
if (dev) {
|
|
event.locals.user = {
|
|
email: event.request.headers.get('cf-access-authenticated-user-email') || 'dev@local.host'
|
|
};
|
|
return resolve(event);
|
|
}
|
|
|
|
const token = event.request.headers.get('cf-access-jwt-assertion');
|
|
const teamDomain = event.platform?.env.CF_ACCESS_TEAM_DOMAIN;
|
|
const audience = event.platform?.env.CF_ACCESS_AUD;
|
|
|
|
if (!teamDomain || !audience) {
|
|
console.error(
|
|
JSON.stringify({
|
|
message: 'Cloudflare Access configuration is missing',
|
|
path: event.url.pathname,
|
|
hasTeamDomain: Boolean(teamDomain),
|
|
hasAudience: Boolean(audience)
|
|
})
|
|
);
|
|
return accessNotConfigured();
|
|
}
|
|
|
|
if (!token) {
|
|
console.error(
|
|
JSON.stringify({
|
|
message: 'Cloudflare Access JWT is missing',
|
|
path: event.url.pathname
|
|
})
|
|
);
|
|
return unauthorized();
|
|
}
|
|
|
|
try {
|
|
event.locals.user = {
|
|
email: await verifyAccessJwt(token, teamDomain, audience)
|
|
};
|
|
} catch (cause) {
|
|
console.error(
|
|
JSON.stringify({
|
|
message: 'Cloudflare Access JWT verification failed',
|
|
path: event.url.pathname,
|
|
error: cause instanceof Error ? cause.message : String(cause)
|
|
})
|
|
);
|
|
return unauthorized();
|
|
}
|
|
|
|
return resolve(event);
|
|
};
|
|
|
|
export const handle: Handle = async (input) => {
|
|
const response = await authenticate(input);
|
|
const result = applyCachePolicy(input.event, response);
|
|
if (!dev && isContentMutation(input.event) && response.status < 400) {
|
|
// Await invalidation before completing the save. Failed purges do not undo saved content.
|
|
const error = await purgePublicCache(input.event.platform);
|
|
result.headers.set('X-Blogflare-Cache-Purge', error ? 'failed' : 'accepted');
|
|
}
|
|
return result;
|
|
};
|