first ai implementation, oidc done
This commit is contained in:
293
src/pages/api/ai/query.ts
Normal file
293
src/pages/api/ai/query.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
// src/pages/api/ai/query.ts
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
|
||||
import { promises as fs } from 'fs';
|
||||
import { load } from 'js-yaml';
|
||||
import path from 'path';
|
||||
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
|
||||
function getEnv(key: string): string {
|
||||
const value = process.env[key];
|
||||
if (!value) {
|
||||
throw new Error(`Missing environment variable: ${key}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const AI_MODEL = getEnv('AI_MODEL');
|
||||
// Rate limiting store (in production, use Redis)
|
||||
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
|
||||
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
|
||||
const RATE_LIMIT_MAX = 10; // 10 requests per minute per user
|
||||
|
||||
// Input validation and sanitization
|
||||
function sanitizeInput(input: string): string {
|
||||
// Remove potential prompt injection patterns
|
||||
const dangerous = [
|
||||
/ignore\s+previous\s+instructions?/gi,
|
||||
/new\s+instructions?:/gi,
|
||||
/system\s*:/gi,
|
||||
/assistant\s*:/gi,
|
||||
/human\s*:/gi,
|
||||
/<\s*\/?system\s*>/gi,
|
||||
/```\s*system/gi,
|
||||
];
|
||||
|
||||
let sanitized = input.trim();
|
||||
dangerous.forEach(pattern => {
|
||||
sanitized = sanitized.replace(pattern, '[FILTERED]');
|
||||
});
|
||||
|
||||
// Limit length
|
||||
return sanitized.slice(0, 2000);
|
||||
}
|
||||
|
||||
// Strip markdown code blocks from AI response
|
||||
function stripMarkdownJson(content: string): string {
|
||||
// Remove ```json and ``` wrappers
|
||||
return content
|
||||
.replace(/^```json\s*/i, '')
|
||||
.replace(/\s*```\s*$/, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
// Rate limiting check
|
||||
function checkRateLimit(userId: string): boolean {
|
||||
const now = Date.now();
|
||||
const userLimit = rateLimitStore.get(userId);
|
||||
|
||||
if (!userLimit || now > userLimit.resetTime) {
|
||||
rateLimitStore.set(userId, { count: 1, resetTime: now + RATE_LIMIT_WINDOW });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (userLimit.count >= RATE_LIMIT_MAX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
userLimit.count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load tools database
|
||||
async function loadToolsDatabase() {
|
||||
try {
|
||||
const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml');
|
||||
const yamlContent = await fs.readFile(yamlPath, 'utf8');
|
||||
return load(yamlContent) as any;
|
||||
} catch (error) {
|
||||
console.error('Failed to load tools database:', error);
|
||||
throw new Error('Database unavailable');
|
||||
}
|
||||
}
|
||||
|
||||
// Create system prompt
|
||||
function createSystemPrompt(toolsData: any): string {
|
||||
const toolsList = toolsData.tools.map((tool: any) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
domains: tool.domains,
|
||||
phases: tool.phases,
|
||||
platforms: tool.platforms,
|
||||
skillLevel: tool.skillLevel,
|
||||
license: tool.license,
|
||||
tags: tool.tags,
|
||||
projectUrl: tool.projectUrl ? 'self-hosted' : 'external'
|
||||
}));
|
||||
|
||||
// Dynamically build phases list from configuration
|
||||
const phasesDescription = toolsData.phases.map((phase: any) =>
|
||||
`- ${phase.id}: ${phase.name}`
|
||||
).join('\n');
|
||||
|
||||
// Dynamically build domains list from configuration
|
||||
const domainsDescription = toolsData.domains.map((domain: any) =>
|
||||
`- ${domain.id}: ${domain.name}`
|
||||
).join('\n');
|
||||
|
||||
return `Du bist ein DFIR (Digital Forensics and Incident Response) Experte, der Ermittlern bei der Toolauswahl hilft.
|
||||
|
||||
VERFÜGBARE TOOLS DATABASE:
|
||||
${JSON.stringify(toolsList, null, 2)}
|
||||
|
||||
UNTERSUCHUNGSPHASEN (NIST Framework):
|
||||
${phasesDescription}
|
||||
|
||||
FORENSISCHE DOMÄNEN:
|
||||
${domainsDescription}
|
||||
|
||||
PRIORITÄTEN:
|
||||
1. Self-hosted Tools (projectUrl: "self-hosted") bevorzugen
|
||||
2. Open Source Tools bevorzugen (license != "Proprietary")
|
||||
3. Maximal 3 Tools pro Phase empfehlen
|
||||
4. Deutsche Antworten für deutsche Anfragen, English for English queries
|
||||
|
||||
ANTWORT-FORMAT (strict JSON):
|
||||
{
|
||||
"scenario_analysis": "Detaillierte Analyse des Szenarios auf Deutsch/English",
|
||||
"recommended_tools": [
|
||||
{
|
||||
"name": "EXAKTER Name aus der Database",
|
||||
"priority": "high|medium|low",
|
||||
"phase": "data-collection|examination|analysis|reporting",
|
||||
"justification": "Warum dieses Tool für dieses Szenario geeignet ist"
|
||||
}
|
||||
],
|
||||
"workflow_suggestion": "Vorgeschlagener Untersuchungsablauf",
|
||||
"additional_notes": "Wichtige Überlegungen und Hinweise"
|
||||
}
|
||||
|
||||
Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des JSON.`;
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
// Check if authentication is required
|
||||
const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
|
||||
let userId = 'test-user';
|
||||
|
||||
if (authRequired) {
|
||||
// Authentication check
|
||||
const sessionToken = getSessionFromRequest(request);
|
||||
if (!sessionToken) {
|
||||
return new Response(JSON.stringify({ error: 'Authentication required' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const session = await verifySession(sessionToken);
|
||||
if (!session) {
|
||||
return new Response(JSON.stringify({ error: 'Invalid session' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
userId = session.userId;
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
if (!checkRateLimit(userId)) {
|
||||
return new Response(JSON.stringify({ error: 'Rate limit exceeded' }), {
|
||||
status: 429,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
const body = await request.json();
|
||||
const { query } = body;
|
||||
|
||||
if (!query || typeof query !== 'string') {
|
||||
return new Response(JSON.stringify({ error: 'Query required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Sanitize input
|
||||
const sanitizedQuery = sanitizeInput(query);
|
||||
if (sanitizedQuery.includes('[FILTERED]')) {
|
||||
return new Response(JSON.stringify({ error: 'Invalid input detected' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Load tools database
|
||||
const toolsData = await loadToolsDatabase();
|
||||
|
||||
// Create AI request
|
||||
const systemPrompt = createSystemPrompt(toolsData);
|
||||
|
||||
const aiResponse = await fetch(process.env.AI_API_ENDPOINT + '/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.AI_API_KEY}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: AI_MODEL, // or whatever model is available
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: systemPrompt
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: sanitizedQuery
|
||||
}
|
||||
],
|
||||
max_tokens: 2000,
|
||||
temperature: 0.3
|
||||
})
|
||||
});
|
||||
|
||||
if (!aiResponse.ok) {
|
||||
console.error('AI API error:', await aiResponse.text());
|
||||
return new Response(JSON.stringify({ error: 'AI service unavailable' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const aiData = await aiResponse.json();
|
||||
const aiContent = aiData.choices?.[0]?.message?.content;
|
||||
|
||||
if (!aiContent) {
|
||||
return new Response(JSON.stringify({ error: 'No response from AI' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Parse AI JSON response
|
||||
let recommendation;
|
||||
try {
|
||||
const cleanedContent = stripMarkdownJson(aiContent);
|
||||
recommendation = JSON.parse(cleanedContent);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse AI response:', aiContent);
|
||||
return new Response(JSON.stringify({ error: 'Invalid AI response format' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Validate tool names against database
|
||||
const validToolNames = new Set(toolsData.tools.map((t: any) => t.name));
|
||||
const validatedRecommendation = {
|
||||
...recommendation,
|
||||
recommended_tools: recommendation.recommended_tools?.filter((tool: any) => {
|
||||
if (!validToolNames.has(tool.name)) {
|
||||
console.warn(`AI recommended unknown tool: ${tool.name}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}) || []
|
||||
};
|
||||
|
||||
// Log successful query
|
||||
console.log(`[AI Query] User: ${userId}, Query length: ${sanitizedQuery.length}, Tools: ${validatedRecommendation.recommended_tools.length}`);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
recommendation: validatedRecommendation,
|
||||
query: sanitizedQuery
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('AI query error:', error);
|
||||
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
100
src/pages/api/auth/callback.ts
Normal file
100
src/pages/api/auth/callback.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { parse } from 'cookie';
|
||||
import {
|
||||
exchangeCodeForTokens,
|
||||
getUserInfo,
|
||||
createSession,
|
||||
createSessionCookie,
|
||||
logAuthEvent
|
||||
} from '../../../utils/auth.js';
|
||||
|
||||
export const GET: APIRoute = async ({ url, request }) => {
|
||||
try {
|
||||
// Debug: multiple ways to access URL parameters
|
||||
console.log('Full URL:', url.toString());
|
||||
console.log('URL pathname:', url.pathname);
|
||||
console.log('URL search:', url.search);
|
||||
console.log('URL searchParams:', url.searchParams.toString());
|
||||
|
||||
// Try different ways to get parameters
|
||||
const allParams = Object.fromEntries(url.searchParams.entries());
|
||||
console.log('SearchParams entries:', allParams);
|
||||
|
||||
// Also try parsing manually from the search string
|
||||
const manualParams = new URLSearchParams(url.search);
|
||||
const manualEntries = Object.fromEntries(manualParams.entries());
|
||||
console.log('Manual URLSearchParams:', manualEntries);
|
||||
|
||||
// Also check request URL
|
||||
const requestUrl = new URL(request.url);
|
||||
console.log('Request URL:', requestUrl.toString());
|
||||
const requestParams = Object.fromEntries(requestUrl.searchParams.entries());
|
||||
console.log('Request URL params:', requestParams);
|
||||
|
||||
const code = url.searchParams.get('code') || requestUrl.searchParams.get('code');
|
||||
const state = url.searchParams.get('state') || requestUrl.searchParams.get('state');
|
||||
const error = url.searchParams.get('error') || requestUrl.searchParams.get('error');
|
||||
|
||||
console.log('Final extracted values:', { code: !!code, state: !!state, error });
|
||||
|
||||
// Handle OIDC errors
|
||||
if (error) {
|
||||
logAuthEvent('OIDC error', { error, description: url.searchParams.get('error_description') });
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: { 'Location': '/?auth=error' }
|
||||
});
|
||||
}
|
||||
|
||||
if (!code || !state) {
|
||||
logAuthEvent('Missing code or state parameter', { received: allParams });
|
||||
return new Response('Invalid callback parameters', { status: 400 });
|
||||
}
|
||||
|
||||
// Verify state parameter
|
||||
const cookieHeader = request.headers.get('cookie');
|
||||
const cookies = cookieHeader ? parse(cookieHeader) : {};
|
||||
const storedStateData = cookies.auth_state ? JSON.parse(decodeURIComponent(cookies.auth_state)) : null;
|
||||
|
||||
if (!storedStateData || storedStateData.state !== state) {
|
||||
logAuthEvent('State mismatch', { received: state, stored: storedStateData?.state });
|
||||
return new Response('Invalid state parameter', { status: 400 });
|
||||
}
|
||||
|
||||
// Exchange code for tokens
|
||||
const tokens = await exchangeCodeForTokens(code);
|
||||
|
||||
// Get user info
|
||||
const userInfo = await getUserInfo(tokens.access_token);
|
||||
|
||||
// Create session
|
||||
const sessionToken = await createSession(userInfo.sub || userInfo.preferred_username || 'unknown');
|
||||
const sessionCookie = createSessionCookie(sessionToken);
|
||||
|
||||
logAuthEvent('Authentication successful', {
|
||||
userId: userInfo.sub || userInfo.preferred_username,
|
||||
email: userInfo.email
|
||||
});
|
||||
|
||||
// Clear auth state cookie and redirect to intended destination
|
||||
const returnTo = storedStateData.returnTo || '/';
|
||||
const clearStateCookie = 'auth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0';
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append('Location', returnTo);
|
||||
headers.append('Set-Cookie', sessionCookie);
|
||||
headers.append('Set-Cookie', clearStateCookie);
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: headers
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logAuthEvent('Callback failed', { error: error.message });
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: { 'Location': '/?auth=error' }
|
||||
});
|
||||
}
|
||||
};
|
||||
34
src/pages/api/auth/login.ts
Normal file
34
src/pages/api/auth/login.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { generateAuthUrl, generateState, logAuthEvent } from '../../../utils/auth.js';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const GET: APIRoute = async ({ url, redirect }) => {
|
||||
try {
|
||||
const state = generateState();
|
||||
const authUrl = generateAuthUrl(state);
|
||||
|
||||
// Debug: log the generated URL
|
||||
console.log('Generated auth URL:', authUrl);
|
||||
|
||||
// Get the intended destination after login (if any)
|
||||
const returnTo = url.searchParams.get('returnTo') || '/';
|
||||
|
||||
logAuthEvent('Login initiated', { returnTo, authUrl });
|
||||
|
||||
// Store state and returnTo in a cookie for the callback
|
||||
const stateData = JSON.stringify({ state, returnTo });
|
||||
const stateCookie = `auth_state=${encodeURIComponent(stateData)}; HttpOnly; SameSite=Lax; Path=/; Max-Age=600`; // 10 minutes
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
'Location': authUrl,
|
||||
'Set-Cookie': stateCookie
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logAuthEvent('Login failed', { error: error.message });
|
||||
return new Response('Authentication error', { status: 500 });
|
||||
}
|
||||
};
|
||||
38
src/pages/api/auth/logout.ts
Normal file
38
src/pages/api/auth/logout.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const sessionToken = getSessionFromRequest(request);
|
||||
|
||||
if (!sessionToken) {
|
||||
return new Response(JSON.stringify({
|
||||
authenticated: false
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const session = await verifySession(sessionToken);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
authenticated: session !== null,
|
||||
expires: session?.exp ? new Date(session.exp * 1000).toISOString() : null
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({
|
||||
authenticated: false,
|
||||
error: 'Session verification failed'
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
104
src/pages/api/auth/process.ts
Normal file
104
src/pages/api/auth/process.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { parse } from 'cookie';
|
||||
import {
|
||||
exchangeCodeForTokens,
|
||||
getUserInfo,
|
||||
createSession,
|
||||
createSessionCookie,
|
||||
logAuthEvent
|
||||
} from '../../../utils/auth.js';
|
||||
|
||||
// Mark as server-rendered
|
||||
export const prerender = false;
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
// Check if there's a body to parse
|
||||
const contentType = request.headers.get('content-type');
|
||||
console.log('Request content-type:', contentType);
|
||||
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch (parseError) {
|
||||
console.error('JSON parse error:', parseError);
|
||||
return new Response(JSON.stringify({ success: false, error: 'Invalid JSON' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const { code, state } = body || {};
|
||||
|
||||
console.log('Processing authentication:', { code: !!code, state: !!state });
|
||||
|
||||
if (!code || !state) {
|
||||
logAuthEvent('Missing code or state parameter in process request');
|
||||
return new Response(JSON.stringify({ success: false }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Verify state parameter
|
||||
const cookieHeader = request.headers.get('cookie');
|
||||
const cookies = cookieHeader ? parse(cookieHeader) : {};
|
||||
const storedStateData = cookies.auth_state ? JSON.parse(decodeURIComponent(cookies.auth_state)) : null;
|
||||
|
||||
console.log('State verification:', {
|
||||
received: state,
|
||||
stored: storedStateData?.state,
|
||||
match: storedStateData?.state === state
|
||||
});
|
||||
|
||||
if (!storedStateData || storedStateData.state !== state) {
|
||||
logAuthEvent('State mismatch', { received: state, stored: storedStateData?.state });
|
||||
return new Response(JSON.stringify({ success: false }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Exchange code for tokens
|
||||
console.log('Exchanging code for tokens...');
|
||||
const tokens = await exchangeCodeForTokens(code);
|
||||
|
||||
// Get user info
|
||||
console.log('Getting user info...');
|
||||
const userInfo = await getUserInfo(tokens.access_token);
|
||||
|
||||
// Create session
|
||||
const sessionToken = await createSession(userInfo.sub || userInfo.preferred_username || 'unknown');
|
||||
const sessionCookie = createSessionCookie(sessionToken);
|
||||
|
||||
logAuthEvent('Authentication successful', {
|
||||
userId: userInfo.sub || userInfo.preferred_username,
|
||||
email: userInfo.email
|
||||
});
|
||||
|
||||
// Clear auth state cookie
|
||||
const clearStateCookie = 'auth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0';
|
||||
const returnTo = storedStateData.returnTo || '/';
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append('Content-Type', 'application/json');
|
||||
headers.append('Set-Cookie', sessionCookie);
|
||||
headers.append('Set-Cookie', clearStateCookie);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
redirectTo: returnTo
|
||||
}), {
|
||||
status: 200,
|
||||
headers: headers
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Authentication processing failed:', error);
|
||||
logAuthEvent('Authentication processing failed', { error: error.message });
|
||||
return new Response(JSON.stringify({ success: false }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
36
src/pages/api/auth/status.ts
Normal file
36
src/pages/api/auth/status.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const sessionToken = getSessionFromRequest(request);
|
||||
|
||||
if (!sessionToken) {
|
||||
return new Response(JSON.stringify({
|
||||
authenticated: false
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const session = await verifySession(sessionToken);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
authenticated: session !== null,
|
||||
expires: session?.exp ? new Date(session.exp * 1000).toISOString() : null
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({
|
||||
authenticated: false,
|
||||
error: 'Session verification failed'
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
54
src/pages/auth/callback.astro
Normal file
54
src/pages/auth/callback.astro
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
// Since server-side URL parameters aren't working,
|
||||
// we'll handle this client-side and POST to the API
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Processing Authentication...</title>
|
||||
</head>
|
||||
<body>
|
||||
<div style="text-align: center; padding: 4rem; font-family: sans-serif;">
|
||||
<h2>Processing authentication...</h2>
|
||||
<p>Please wait while we complete your login.</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Get URL parameters from client-side
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
const state = urlParams.get('state');
|
||||
const error = urlParams.get('error');
|
||||
|
||||
console.log('Client-side callback params:', { code: !!code, state: !!state, error });
|
||||
|
||||
if (error) {
|
||||
window.location.href = '/?auth=error';
|
||||
} else if (code && state) {
|
||||
// Send the parameters to our API endpoint
|
||||
fetch('/api/auth/process', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ code, state })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.href = data.redirectTo || '/';
|
||||
} else {
|
||||
window.location.href = '/?auth=error';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Authentication processing failed:', error);
|
||||
window.location.href = '/?auth=error';
|
||||
});
|
||||
} else {
|
||||
console.error('Missing code or state parameters');
|
||||
window.location.href = '/?auth=error';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user