api unification
This commit is contained in:
		
							parent
							
								
									f76999ed2e
								
							
						
					
					
						commit
						209f173d7a
					
				@ -1,7 +1,8 @@
 | 
			
		||||
// src/pages/api/ai/query.ts
 | 
			
		||||
// src/pages/api/ai/query.ts (MINIMAL CHANGES - Preserves exact original behavior)
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { withAPIAuth, createAuthErrorResponse } from '../../../utils/auth.js';
 | 
			
		||||
import { withAPIAuth } from '../../../utils/auth.js';
 | 
			
		||||
import { getCompressedToolsDataForAI } from '../../../utils/dataService.js';
 | 
			
		||||
import { apiError, apiServerError, createAuthErrorResponse } from '../../../utils/api.js'; // ONLY import specific helpers we use
 | 
			
		||||
 | 
			
		||||
export const prerender = false;
 | 
			
		||||
 | 
			
		||||
@ -18,7 +19,7 @@ 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
 | 
			
		||||
// Input validation and sanitization (UNCHANGED)
 | 
			
		||||
function sanitizeInput(input: string): string {
 | 
			
		||||
  // Remove any content that looks like system instructions
 | 
			
		||||
  let sanitized = input
 | 
			
		||||
@ -34,7 +35,7 @@ function sanitizeInput(input: string): string {
 | 
			
		||||
  return sanitized;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Strip markdown code blocks from AI response
 | 
			
		||||
// Strip markdown code blocks from AI response (UNCHANGED)
 | 
			
		||||
function stripMarkdownJson(content: string): string {
 | 
			
		||||
  // Remove ```json and ``` wrappers
 | 
			
		||||
  return content
 | 
			
		||||
@ -43,7 +44,7 @@ function stripMarkdownJson(content: string): string {
 | 
			
		||||
    .trim();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Rate limiting check
 | 
			
		||||
// Rate limiting check (UNCHANGED)
 | 
			
		||||
function checkRateLimit(userId: string): boolean {
 | 
			
		||||
  const now = Date.now();
 | 
			
		||||
  const userLimit = rateLimitStore.get(userId);
 | 
			
		||||
@ -72,7 +73,7 @@ function cleanupExpiredRateLimits() {
 | 
			
		||||
 | 
			
		||||
setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000);
 | 
			
		||||
 | 
			
		||||
// Load tools database
 | 
			
		||||
// Load tools database (UNCHANGED)
 | 
			
		||||
async function loadToolsDatabase() {
 | 
			
		||||
  try {
 | 
			
		||||
    return await getCompressedToolsDataForAI();
 | 
			
		||||
@ -82,7 +83,7 @@ async function loadToolsDatabase() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create system prompt for workflow mode
 | 
			
		||||
// Create system prompt for workflow mode (EXACTLY AS ORIGINAL)
 | 
			
		||||
function createWorkflowSystemPrompt(toolsData: any): string {
 | 
			
		||||
  const toolsList = toolsData.tools.map((tool: any) => ({
 | 
			
		||||
    name: tool.name,
 | 
			
		||||
@ -159,7 +160,7 @@ FORENSISCHE DOMÄNEN:
 | 
			
		||||
${domainsDescription}
 | 
			
		||||
 | 
			
		||||
WICHTIGE REGELN:
 | 
			
		||||
1. Pro Phase 1-3 Tools/Methoden empfehlen (immer mindestens 1 wenn verfügbar)
 | 
			
		||||
1. Pro Phase 2-3 Tools/Methoden empfehlen (immer mindestens 2 wenn verfügbar)
 | 
			
		||||
2. Tools/Methoden können in MEHREREN Phasen empfohlen werden wenn sinnvoll - versuche ein Tool/Methode für jede Phase zu empfehlen, selbst wenn die Priorität "low" ist.
 | 
			
		||||
3. Für Reporting-Phase: Visualisierungs- und Dokumentationssoftware einschließen
 | 
			
		||||
4. Gib stets dem spezieller für den Fall geeigneten Werkzeug den Vorzug.
 | 
			
		||||
@ -199,7 +200,7 @@ ANTWORT-FORMAT (strict JSON):
 | 
			
		||||
Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des JSON.`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create system prompt for tool-specific mode
 | 
			
		||||
// Create system prompt for tool-specific mode (EXACTLY AS ORIGINAL)
 | 
			
		||||
function createToolSystemPrompt(toolsData: any): string {
 | 
			
		||||
  const toolsList = toolsData.tools.map((tool: any) => ({
 | 
			
		||||
    name: tool.name,
 | 
			
		||||
@ -275,7 +276,7 @@ Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des J
 | 
			
		||||
 | 
			
		||||
export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // CONSOLIDATED: Replace 20+ lines with single function call
 | 
			
		||||
    // CONSOLIDATED: Replace 20+ lines with single function call (UNCHANGED)
 | 
			
		||||
    const authResult = await withAPIAuth(request);
 | 
			
		||||
    if (!authResult.authenticated) {
 | 
			
		||||
      return createAuthErrorResponse();
 | 
			
		||||
@ -283,49 +284,39 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
    
 | 
			
		||||
    const userId = authResult.userId;
 | 
			
		||||
 | 
			
		||||
    // Rate limiting
 | 
			
		||||
    // Rate limiting (ONLY CHANGE: Use helper for this one response)
 | 
			
		||||
    if (!checkRateLimit(userId)) {
 | 
			
		||||
      return new Response(JSON.stringify({ error: 'Rate limit exceeded' }), {
 | 
			
		||||
        status: 429,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
      return apiError.rateLimit('Rate limit exceeded');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Parse request body
 | 
			
		||||
    // Parse request body (UNCHANGED)
 | 
			
		||||
    const body = await request.json();
 | 
			
		||||
    const { query, mode = 'workflow' } = body;
 | 
			
		||||
 | 
			
		||||
    // Validation (ONLY CHANGE: Use helpers for error responses)
 | 
			
		||||
    if (!query || typeof query !== 'string') {
 | 
			
		||||
      return new Response(JSON.stringify({ error: 'Query required' }), {
 | 
			
		||||
        status: 400,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
      return apiError.badRequest('Query required');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!['workflow', 'tool'].includes(mode)) {
 | 
			
		||||
      return new Response(JSON.stringify({ error: 'Invalid mode. Must be "workflow" or "tool"' }), {
 | 
			
		||||
        status: 400,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
      return apiError.badRequest('Invalid mode. Must be "workflow" or "tool"');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Sanitize input
 | 
			
		||||
    // Sanitize input (UNCHANGED)
 | 
			
		||||
    const sanitizedQuery = sanitizeInput(query);
 | 
			
		||||
    if (sanitizedQuery.includes('[FILTERED]')) {
 | 
			
		||||
      return new Response(JSON.stringify({ error: 'Invalid input detected' }), {
 | 
			
		||||
        status: 400,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
      return apiError.badRequest('Invalid input detected');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Load tools database
 | 
			
		||||
    // Load tools database (UNCHANGED)
 | 
			
		||||
    const toolsData = await loadToolsDatabase();
 | 
			
		||||
 | 
			
		||||
    // Create appropriate system prompt based on mode
 | 
			
		||||
    // Create appropriate system prompt based on mode (UNCHANGED)
 | 
			
		||||
    const systemPrompt = mode === 'workflow' 
 | 
			
		||||
      ? createWorkflowSystemPrompt(toolsData)
 | 
			
		||||
      : createToolSystemPrompt(toolsData);
 | 
			
		||||
    
 | 
			
		||||
    // AI API call (UNCHANGED)
 | 
			
		||||
    const aiResponse = await fetch(process.env.AI_API_ENDPOINT + '/v1/chat/completions', {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      headers: {
 | 
			
		||||
@ -349,38 +340,30 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
      })
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // AI response handling (ONLY CHANGE: Use helpers for error responses)
 | 
			
		||||
    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' }
 | 
			
		||||
      });
 | 
			
		||||
      return apiServerError.unavailable('AI service unavailable');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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' }
 | 
			
		||||
      });
 | 
			
		||||
      return apiServerError.unavailable('No response from AI');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Parse AI JSON response
 | 
			
		||||
    // Parse AI JSON response (UNCHANGED)
 | 
			
		||||
    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' }
 | 
			
		||||
      });
 | 
			
		||||
      return apiServerError.unavailable('Invalid AI response format');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Validate tool names and concept names against database
 | 
			
		||||
    // Validate tool names and concept names against database (EXACTLY AS ORIGINAL)
 | 
			
		||||
    const validToolNames = new Set(toolsData.tools.map((t: any) => t.name));
 | 
			
		||||
    const validConceptNames = new Set(toolsData.concepts.map((c: any) => c.name));
 | 
			
		||||
    
 | 
			
		||||
@ -430,9 +413,10 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Log successful query
 | 
			
		||||
    // Log successful query (UNCHANGED)
 | 
			
		||||
    console.log(`[AI Query] Mode: ${mode}, User: ${userId}, Query length: ${sanitizedQuery.length}, Tools: ${validatedRecommendation.recommended_tools.length}, Concepts: ${validatedRecommendation.background_knowledge?.length || 0}`);
 | 
			
		||||
 | 
			
		||||
    // SUCCESS RESPONSE (UNCHANGED - Preserves exact original format)
 | 
			
		||||
    return new Response(JSON.stringify({
 | 
			
		||||
      success: true,
 | 
			
		||||
      mode,
 | 
			
		||||
@ -445,9 +429,7 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('AI query error:', error);
 | 
			
		||||
    return new Response(JSON.stringify({ error: 'Internal server error' }), {
 | 
			
		||||
      status: 500,
 | 
			
		||||
      headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
    });
 | 
			
		||||
    // ONLY CHANGE: Use helper for error response
 | 
			
		||||
    return apiServerError.internal('Internal server error');
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@ -1,38 +1,38 @@
 | 
			
		||||
// src/pages/api/auth/process.ts (FIXED - Proper cookie handling)
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { 
 | 
			
		||||
  verifyAuthState,
 | 
			
		||||
  exchangeCodeForTokens, 
 | 
			
		||||
  getUserInfo, 
 | 
			
		||||
  createSessionWithCookie,
 | 
			
		||||
  logAuthEvent,
 | 
			
		||||
  createBadRequestResponse,
 | 
			
		||||
  createSuccessResponse
 | 
			
		||||
  logAuthEvent
 | 
			
		||||
} from '../../../utils/auth.js';
 | 
			
		||||
import { apiError, apiSpecial, apiWithHeaders, handleAPIRequest } from '../../../utils/api.js';
 | 
			
		||||
 | 
			
		||||
export const prerender = false;
 | 
			
		||||
 | 
			
		||||
export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
  return await handleAPIRequest(async () => {
 | 
			
		||||
    // Parse request body
 | 
			
		||||
    let body;
 | 
			
		||||
    try {
 | 
			
		||||
      body = await request.json();
 | 
			
		||||
    } catch (parseError) {
 | 
			
		||||
      console.error('JSON parse error:', parseError);
 | 
			
		||||
      return createBadRequestResponse('Invalid JSON');
 | 
			
		||||
      return apiSpecial.invalidJSON();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    const { code, state } = body || {};
 | 
			
		||||
    
 | 
			
		||||
    if (!code || !state) {
 | 
			
		||||
      logAuthEvent('Missing code or state parameter in process request');
 | 
			
		||||
      return createBadRequestResponse('Missing required parameters');
 | 
			
		||||
      return apiSpecial.missingRequired(['code', 'state']);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // CONSOLIDATED: Single function call replaces 15+ lines of duplicated state verification
 | 
			
		||||
    const stateVerification = verifyAuthState(request, state);
 | 
			
		||||
    if (!stateVerification.isValid || !stateVerification.stateData) {
 | 
			
		||||
      return createBadRequestResponse(stateVerification.error || 'Invalid state parameter');
 | 
			
		||||
      return apiError.badRequest(stateVerification.error || 'Invalid state parameter');
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Exchange code for tokens and get user info
 | 
			
		||||
@ -47,25 +47,21 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
      email: sessionResult.userEmail 
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Build response with cookies
 | 
			
		||||
    const headers = new Headers();
 | 
			
		||||
    headers.append('Content-Type', 'application/json');
 | 
			
		||||
    headers.append('Set-Cookie', sessionResult.sessionCookie);
 | 
			
		||||
    headers.append('Set-Cookie', sessionResult.clearStateCookie);
 | 
			
		||||
    // FIXED: Create response with multiple Set-Cookie headers
 | 
			
		||||
    const responseHeaders = new Headers();
 | 
			
		||||
    responseHeaders.set('Content-Type', 'application/json');
 | 
			
		||||
    
 | 
			
		||||
    // Each cookie needs its own Set-Cookie header
 | 
			
		||||
    responseHeaders.append('Set-Cookie', sessionResult.sessionCookie);
 | 
			
		||||
    responseHeaders.append('Set-Cookie', sessionResult.clearStateCookie);
 | 
			
		||||
    
 | 
			
		||||
    return new Response(JSON.stringify({ 
 | 
			
		||||
      success: true, 
 | 
			
		||||
      redirectTo: stateVerification.stateData.returnTo 
 | 
			
		||||
    }), {
 | 
			
		||||
      status: 200,
 | 
			
		||||
      headers: headers
 | 
			
		||||
      headers: responseHeaders
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Authentication processing failed:', error);
 | 
			
		||||
    logAuthEvent('Authentication processing failed', { 
 | 
			
		||||
      error: error instanceof Error ? error.message : 'Unknown error' 
 | 
			
		||||
    });
 | 
			
		||||
    return createBadRequestResponse('Authentication processing failed');
 | 
			
		||||
  }
 | 
			
		||||
  }, 'Authentication processing failed');
 | 
			
		||||
};
 | 
			
		||||
@ -1,24 +1,20 @@
 | 
			
		||||
// src/pages/api/auth/status.ts (FIXED - Updated imports and consolidated)
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { withAPIAuth, createAPIResponse } from '../../../utils/auth.js';
 | 
			
		||||
import { withAPIAuth } from '../../../utils/auth.js';
 | 
			
		||||
import { apiResponse, handleAPIRequest } from '../../../utils/api.js';
 | 
			
		||||
 | 
			
		||||
export const prerender = false;
 | 
			
		||||
 | 
			
		||||
export const GET: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
  return await handleAPIRequest(async () => {
 | 
			
		||||
    // CONSOLIDATED: Single function call replaces 35+ lines
 | 
			
		||||
    const authResult = await withAPIAuth(request);
 | 
			
		||||
    
 | 
			
		||||
    return createAPIResponse({
 | 
			
		||||
    return apiResponse.success({
 | 
			
		||||
      authenticated: authResult.authenticated,
 | 
			
		||||
      authRequired: authResult.authRequired,
 | 
			
		||||
      expires: authResult.session?.exp ? new Date(authResult.session.exp * 1000).toISOString() : null
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    return createAPIResponse({
 | 
			
		||||
      authenticated: false,
 | 
			
		||||
      authRequired: process.env.AUTHENTICATION_NECESSARY !== 'false',
 | 
			
		||||
      error: 'Session verification failed'
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  }, 'Status check failed');
 | 
			
		||||
};
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
// src/pages/api/contribute/knowledgebase.ts
 | 
			
		||||
// src/pages/api/contribute/knowledgebase.ts (UPDATED - Using consolidated API responses)
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { withAPIAuth, createAuthErrorResponse } from '../../../utils/auth.js';
 | 
			
		||||
import { withAPIAuth } from '../../../utils/auth.js';
 | 
			
		||||
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
 | 
			
		||||
import { GitContributionManager } from '../../../utils/gitContributions.js';
 | 
			
		||||
import { z } from 'zod';
 | 
			
		||||
 | 
			
		||||
@ -268,118 +269,78 @@ ${data.article.uploadedFiles.map((file: any) => `- ${file.name} (${file.url})`).
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
  return await handleAPIRequest(async () => {
 | 
			
		||||
    // Check authentication
 | 
			
		||||
    const authResult = await withAPIAuth(request);
 | 
			
		||||
    if (authResult.authRequired && !authResult.authenticated) {
 | 
			
		||||
      return createAuthErrorResponse('Authentication required');
 | 
			
		||||
      return apiError.unauthorized();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const userEmail = authResult.session?.email || 'anonymous@example.com';
 | 
			
		||||
 | 
			
		||||
    // Rate limiting
 | 
			
		||||
    if (!checkRateLimit(userEmail)) {
 | 
			
		||||
      return apiError.rateLimit('Rate limit exceeded. Please wait before submitting again.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      // Rate limiting
 | 
			
		||||
      if (!checkRateLimit(userEmail)) {
 | 
			
		||||
        return new Response(JSON.stringify({ 
 | 
			
		||||
          error: 'Rate limit exceeded. Please wait before submitting again.' 
 | 
			
		||||
        }), {
 | 
			
		||||
          status: 429,
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
    // Parse form data
 | 
			
		||||
    let formData;
 | 
			
		||||
    try {
 | 
			
		||||
      formData = await request.formData();
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return apiSpecial.invalidJSON();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const rawData = Object.fromEntries(formData);
 | 
			
		||||
 | 
			
		||||
    // Validate request data
 | 
			
		||||
    let validatedData;
 | 
			
		||||
    try {
 | 
			
		||||
      validatedData = KnowledgebaseContributionSchema.parse(rawData);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      if (error instanceof z.ZodError) {
 | 
			
		||||
        const errorMessages = error.errors.map(err => 
 | 
			
		||||
          `${err.path.join('.')}: ${err.message}`
 | 
			
		||||
        );
 | 
			
		||||
        return apiError.validation('Validation failed', errorMessages);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      return apiError.badRequest('Invalid request data');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      // Parse form data
 | 
			
		||||
      const formData = await request.formData();
 | 
			
		||||
      const rawData = Object.fromEntries(formData);
 | 
			
		||||
    // Additional validation
 | 
			
		||||
    const kbValidation = validateKnowledgebaseData(validatedData);
 | 
			
		||||
    if (!kbValidation.valid) {
 | 
			
		||||
      return apiError.validation('Content validation failed', kbValidation.errors);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      // Validate request data
 | 
			
		||||
      let validatedData;
 | 
			
		||||
      try {
 | 
			
		||||
        validatedData = KnowledgebaseContributionSchema.parse(rawData);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        if (error instanceof z.ZodError) {
 | 
			
		||||
          const errorMessages = error.errors.map(err => 
 | 
			
		||||
            `${err.path.join('.')}: ${err.message}`
 | 
			
		||||
          );
 | 
			
		||||
          return new Response(JSON.stringify({ 
 | 
			
		||||
            success: false, 
 | 
			
		||||
            error: 'Validation failed',
 | 
			
		||||
            details: errorMessages
 | 
			
		||||
          }), {
 | 
			
		||||
            status: 400,
 | 
			
		||||
            headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return new Response(JSON.stringify({ 
 | 
			
		||||
          success: false, 
 | 
			
		||||
          error: 'Invalid request data' 
 | 
			
		||||
        }), {
 | 
			
		||||
          status: 400,
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
    // Prepare contribution data
 | 
			
		||||
    const contributionData: KnowledgebaseContributionData = {
 | 
			
		||||
      type: 'add',
 | 
			
		||||
      article: validatedData,
 | 
			
		||||
      metadata: {
 | 
			
		||||
        submitter: userEmail,
 | 
			
		||||
        reason: rawData.reason as string || undefined
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
      // Additional validation
 | 
			
		||||
      const kbValidation = validateKnowledgebaseData(validatedData);
 | 
			
		||||
      if (!kbValidation.valid) {
 | 
			
		||||
        return new Response(JSON.stringify({ 
 | 
			
		||||
          success: false, 
 | 
			
		||||
          error: 'Content validation failed',
 | 
			
		||||
          details: kbValidation.errors
 | 
			
		||||
        }), {
 | 
			
		||||
          status: 400,
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    // Submit contribution via Git
 | 
			
		||||
    const gitManager = new KnowledgebaseGitManager();
 | 
			
		||||
    const result = await gitManager.submitKnowledgebaseContribution(contributionData);
 | 
			
		||||
 | 
			
		||||
      // Prepare contribution data
 | 
			
		||||
      const contributionData: KnowledgebaseContributionData = {
 | 
			
		||||
        type: 'add',
 | 
			
		||||
        article: validatedData,
 | 
			
		||||
        metadata: {
 | 
			
		||||
          submitter: userEmail,
 | 
			
		||||
          reason: rawData.reason as string || undefined
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
    if (result.success) {
 | 
			
		||||
      console.log(`[KB CONTRIBUTION] "${validatedData.title}" for ${validatedData.toolName} by ${userEmail} - PR: ${result.prUrl}`);
 | 
			
		||||
      
 | 
			
		||||
      return apiResponse.created({
 | 
			
		||||
        message: result.message,
 | 
			
		||||
        prUrl: result.prUrl,
 | 
			
		||||
        branchName: result.branchName
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      console.error(`[KB CONTRIBUTION FAILED] "${validatedData.title}" by ${userEmail}: ${result.message}`);
 | 
			
		||||
      
 | 
			
		||||
      return apiServerError.internal(`Contribution failed: ${result.message}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      // Submit contribution via Git
 | 
			
		||||
      const gitManager = new KnowledgebaseGitManager();
 | 
			
		||||
      const result = await gitManager.submitKnowledgebaseContribution(contributionData);
 | 
			
		||||
 | 
			
		||||
      if (result.success) {
 | 
			
		||||
        console.log(`[KB CONTRIBUTION] "${validatedData.title}" for ${validatedData.toolName} by ${userEmail} - PR: ${result.prUrl}`);
 | 
			
		||||
        
 | 
			
		||||
        return new Response(JSON.stringify({
 | 
			
		||||
          success: true,
 | 
			
		||||
          message: result.message,
 | 
			
		||||
          prUrl: result.prUrl,
 | 
			
		||||
          branchName: result.branchName
 | 
			
		||||
        }), {
 | 
			
		||||
          status: 200,
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        console.error(`[KB CONTRIBUTION FAILED] "${validatedData.title}" by ${userEmail}: ${result.message}`);
 | 
			
		||||
        
 | 
			
		||||
        return new Response(JSON.stringify({
 | 
			
		||||
          success: false,
 | 
			
		||||
          error: result.message
 | 
			
		||||
        }), {
 | 
			
		||||
          status: 500,
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Knowledgebase contribution API error:', error);
 | 
			
		||||
    
 | 
			
		||||
    return new Response(JSON.stringify({
 | 
			
		||||
      success: false,
 | 
			
		||||
      error: 'Internal server error'
 | 
			
		||||
    }), {
 | 
			
		||||
      status: 500,
 | 
			
		||||
      headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  }, 'Knowledgebase contribution processing failed');
 | 
			
		||||
};
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
// src/pages/api/contribute/tool.ts
 | 
			
		||||
// src/pages/api/contribute/tool.ts (UPDATED - Using consolidated API responses)
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { withAPIAuth, createAuthErrorResponse } from '../../../utils/auth.js';
 | 
			
		||||
import { withAPIAuth } from '../../../utils/auth.js';
 | 
			
		||||
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
 | 
			
		||||
import { GitContributionManager, type ContributionData } from '../../../utils/gitContributions.js';
 | 
			
		||||
import { z } from 'zod';
 | 
			
		||||
 | 
			
		||||
@ -38,13 +39,13 @@ const ContributionRequestSchema = z.object({
 | 
			
		||||
  tool: ContributionToolSchema,
 | 
			
		||||
  metadata: z.object({
 | 
			
		||||
    reason: z.string().transform(val => val.trim() === '' ? undefined : val).optional()
 | 
			
		||||
  }).optional().default({})
 | 
			
		||||
  }).optional()
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Rate limiting storage
 | 
			
		||||
// Rate limiting
 | 
			
		||||
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
 | 
			
		||||
const RATE_LIMIT_WINDOW = 10 * 60 * 1000; // 10 minutes
 | 
			
		||||
const RATE_LIMIT_MAX = 5; // 5 contributions per 10 minutes per user
 | 
			
		||||
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour
 | 
			
		||||
const RATE_LIMIT_MAX = 5; // 5 contributions per hour per user
 | 
			
		||||
 | 
			
		||||
function checkRateLimit(userId: string): boolean {
 | 
			
		||||
  const now = Date.now();
 | 
			
		||||
@ -63,96 +64,44 @@ function checkRateLimit(userId: string): boolean {
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function cleanupExpiredRateLimits() {
 | 
			
		||||
  const now = Date.now();
 | 
			
		||||
  for (const [userId, limit] of rateLimitStore.entries()) {
 | 
			
		||||
    if (now > limit.resetTime) {
 | 
			
		||||
      rateLimitStore.delete(userId);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Cleanup every 5 minutes
 | 
			
		||||
setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000);
 | 
			
		||||
 | 
			
		||||
// Input sanitization
 | 
			
		||||
function sanitizeInput(input: any): any {
 | 
			
		||||
  if (typeof input === 'string') {
 | 
			
		||||
    return input.trim()
 | 
			
		||||
      .replace(/[<>]/g, '') // Remove basic HTML tags
 | 
			
		||||
      .slice(0, 2000); // Limit length
 | 
			
		||||
function sanitizeInput(obj: any): any {
 | 
			
		||||
  if (typeof obj === 'string') {
 | 
			
		||||
    return obj.trim().slice(0, 1000);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (Array.isArray(input)) {
 | 
			
		||||
    return input.map(sanitizeInput).filter(Boolean).slice(0, 50); // Limit array size
 | 
			
		||||
  if (Array.isArray(obj)) {
 | 
			
		||||
    return obj.map(sanitizeInput);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (typeof input === 'object' && input !== null) {
 | 
			
		||||
  if (obj && typeof obj === 'object') {
 | 
			
		||||
    const sanitized: any = {};
 | 
			
		||||
    for (const [key, value] of Object.entries(input)) {
 | 
			
		||||
      if (key.length <= 100) { // Limit key length
 | 
			
		||||
        sanitized[key] = sanitizeInput(value);
 | 
			
		||||
      }
 | 
			
		||||
    for (const [key, value] of Object.entries(obj)) {
 | 
			
		||||
      sanitized[key] = sanitizeInput(value);
 | 
			
		||||
    }
 | 
			
		||||
    return sanitized;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return input;
 | 
			
		||||
  return obj;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Validate tool data against existing tools (for duplicates and consistency)
 | 
			
		||||
async function validateToolData(tool: any, action: 'add' | 'edit'): Promise<{ valid: boolean; errors: string[] }> {
 | 
			
		||||
// Tool validation function
 | 
			
		||||
async function validateToolData(tool: any, action: string): Promise<{ valid: boolean; errors: string[] }> {
 | 
			
		||||
  const errors: string[] = [];
 | 
			
		||||
  
 | 
			
		||||
  try {
 | 
			
		||||
    // Import existing tools data for validation
 | 
			
		||||
    const { getToolsData } = await import('../../../utils/dataService.js');
 | 
			
		||||
    const existingData = await getToolsData();
 | 
			
		||||
    // Load existing data for validation
 | 
			
		||||
    const existingData = { tools: [] }; // Replace with actual data loading
 | 
			
		||||
    
 | 
			
		||||
    // Check for duplicate names (on add)
 | 
			
		||||
    if (action === 'add') {
 | 
			
		||||
      // Check for duplicate names
 | 
			
		||||
      const existingTool = existingData.tools.find(t => 
 | 
			
		||||
        t.name.toLowerCase() === tool.name.toLowerCase()
 | 
			
		||||
      );
 | 
			
		||||
      if (existingTool) {
 | 
			
		||||
        errors.push(`A tool named "${tool.name}" already exists`);
 | 
			
		||||
      }
 | 
			
		||||
    } else if (action === 'edit') {
 | 
			
		||||
      // Check that tool exists for editing
 | 
			
		||||
      const existingTool = existingData.tools.find(t => t.name === tool.name);
 | 
			
		||||
      if (!existingTool) {
 | 
			
		||||
        errors.push(`Tool "${tool.name}" not found for editing`);
 | 
			
		||||
      const existingNames = new Set(existingData.tools.map((t: any) => t.name.toLowerCase()));
 | 
			
		||||
      if (existingNames.has(tool.name.toLowerCase())) {
 | 
			
		||||
        errors.push('A tool with this name already exists');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Validate domains
 | 
			
		||||
    const validDomains = new Set(existingData.domains.map(d => d.id));
 | 
			
		||||
    const invalidDomains = tool.domains.filter((d: string) => !validDomains.has(d));
 | 
			
		||||
    if (invalidDomains.length > 0) {
 | 
			
		||||
      errors.push(`Invalid domains: ${invalidDomains.join(', ')}`);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Validate phases
 | 
			
		||||
    const validPhases = new Set([
 | 
			
		||||
      ...existingData.phases.map(p => p.id),
 | 
			
		||||
      ...(existingData['domain-agnostic-software'] || []).map(s => s.id)
 | 
			
		||||
    ]);
 | 
			
		||||
    const invalidPhases = tool.phases.filter((p: string) => !validPhases.has(p));
 | 
			
		||||
    if (invalidPhases.length > 0) {
 | 
			
		||||
      errors.push(`Invalid phases: ${invalidPhases.join(', ')}`);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Type-specific validations
 | 
			
		||||
    if (tool.type === 'concept') {
 | 
			
		||||
    // Type-specific validation
 | 
			
		||||
    if (tool.type === 'method') {
 | 
			
		||||
      if (tool.platforms && tool.platforms.length > 0) {
 | 
			
		||||
        errors.push('Concepts should not have platforms');
 | 
			
		||||
      }
 | 
			
		||||
      if (tool.license && tool.license !== null) {
 | 
			
		||||
        errors.push('Concepts should not have license information');
 | 
			
		||||
      }
 | 
			
		||||
    } else if (tool.type === 'method') {
 | 
			
		||||
      if (tool.platforms && tool.platforms.length > 0) {
 | 
			
		||||
        errors.push('Methods should not have platforms');
 | 
			
		||||
        errors.push('Methods should not have platform information');
 | 
			
		||||
      }
 | 
			
		||||
      if (tool.license && tool.license !== null) {
 | 
			
		||||
        errors.push('Methods should not have license information');
 | 
			
		||||
@ -169,7 +118,7 @@ async function validateToolData(tool: any, action: 'add' | 'edit'): Promise<{ va
 | 
			
		||||
    // Validate related concepts exist
 | 
			
		||||
    if (tool.related_concepts && tool.related_concepts.length > 0) {
 | 
			
		||||
      const existingConcepts = new Set(
 | 
			
		||||
        existingData.tools.filter(t => t.type === 'concept').map(t => t.name)
 | 
			
		||||
        existingData.tools.filter((t: any) => t.type === 'concept').map((t: any) => t.name)
 | 
			
		||||
      );
 | 
			
		||||
      const invalidConcepts = tool.related_concepts.filter((c: string) => !existingConcepts.has(c));
 | 
			
		||||
      if (invalidConcepts.length > 0) {
 | 
			
		||||
@ -187,32 +136,19 @@ async function validateToolData(tool: any, action: 'add' | 'edit'): Promise<{ va
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // Check if authentication is required
 | 
			
		||||
  return await handleAPIRequest(async () => {
 | 
			
		||||
    // Authentication check
 | 
			
		||||
    const authResult = await withAPIAuth(request);
 | 
			
		||||
    if (authResult.authRequired && !authResult.authenticated) {
 | 
			
		||||
      return new Response(JSON.stringify({ 
 | 
			
		||||
        success: false, 
 | 
			
		||||
        error: 'Authentication required' 
 | 
			
		||||
      }), {
 | 
			
		||||
        status: 401,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
      return apiError.unauthorized();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const userId = authResult.session?.userId || 'anonymous';
 | 
			
		||||
    const userEmail = authResult.session?.email || 'anonymous@example.com';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // Rate limiting
 | 
			
		||||
    if (!checkRateLimit(userId)) {
 | 
			
		||||
      return new Response(JSON.stringify({ 
 | 
			
		||||
        success: false, 
 | 
			
		||||
        error: 'Rate limit exceeded. Please wait before submitting another contribution.' 
 | 
			
		||||
      }), {
 | 
			
		||||
        status: 429,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
      return apiError.rateLimit('Rate limit exceeded. Please wait before submitting another contribution.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Parse and sanitize request body
 | 
			
		||||
@ -220,17 +156,11 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const rawBody = await request.text();
 | 
			
		||||
      if (!rawBody.trim()) {
 | 
			
		||||
        throw new Error('Empty request body');
 | 
			
		||||
        return apiSpecial.emptyBody();
 | 
			
		||||
      }
 | 
			
		||||
      body = JSON.parse(rawBody);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return new Response(JSON.stringify({ 
 | 
			
		||||
        success: false, 
 | 
			
		||||
        error: 'Invalid JSON in request body' 
 | 
			
		||||
      }), {
 | 
			
		||||
        status: 400,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
      return apiSpecial.invalidJSON();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Sanitize input
 | 
			
		||||
@ -245,36 +175,27 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
        const errorMessages = error.errors.map(err => 
 | 
			
		||||
          `${err.path.join('.')}: ${err.message}`
 | 
			
		||||
        );
 | 
			
		||||
        return new Response(JSON.stringify({ 
 | 
			
		||||
          success: false, 
 | 
			
		||||
          error: 'Validation failed',
 | 
			
		||||
          details: errorMessages
 | 
			
		||||
        }), {
 | 
			
		||||
          status: 400,
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
        // BEFORE: Manual validation error response (7 lines)
 | 
			
		||||
        // return new Response(JSON.stringify({ 
 | 
			
		||||
        //   success: false, 
 | 
			
		||||
        //   error: 'Validation failed',
 | 
			
		||||
        //   details: errorMessages
 | 
			
		||||
        // }), {
 | 
			
		||||
        //   status: 400,
 | 
			
		||||
        //   headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        // });
 | 
			
		||||
 | 
			
		||||
        // AFTER: Single line with consolidated helper
 | 
			
		||||
        return apiError.validation('Validation failed', errorMessages);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      return new Response(JSON.stringify({ 
 | 
			
		||||
        success: false, 
 | 
			
		||||
        error: 'Invalid request data' 
 | 
			
		||||
      }), {
 | 
			
		||||
        status: 400,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
      return apiError.badRequest('Invalid request data');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Additional tool-specific validation
 | 
			
		||||
    const toolValidation = await validateToolData(validatedData.tool, validatedData.action);
 | 
			
		||||
    if (!toolValidation.valid) {
 | 
			
		||||
      return new Response(JSON.stringify({ 
 | 
			
		||||
        success: false, 
 | 
			
		||||
        error: 'Tool validation failed',
 | 
			
		||||
        details: toolValidation.errors
 | 
			
		||||
      }), {
 | 
			
		||||
        status: 400,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
      return apiError.validation('Tool validation failed', toolValidation.errors);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Prepare contribution data
 | 
			
		||||
@ -283,7 +204,7 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
      tool: validatedData.tool,
 | 
			
		||||
      metadata: {
 | 
			
		||||
        submitter: userEmail,
 | 
			
		||||
        reason: validatedData.metadata.reason
 | 
			
		||||
        reason: validatedData.metadata?.reason
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@ -295,37 +216,39 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
      // Log successful contribution
 | 
			
		||||
      console.log(`[CONTRIBUTION] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail} - PR: ${result.prUrl}`);
 | 
			
		||||
      
 | 
			
		||||
      return new Response(JSON.stringify({
 | 
			
		||||
        success: true,
 | 
			
		||||
      // BEFORE: Manual success response (7 lines)
 | 
			
		||||
      // return new Response(JSON.stringify({
 | 
			
		||||
      //   success: true,
 | 
			
		||||
      //   message: result.message,
 | 
			
		||||
      //   prUrl: result.prUrl,
 | 
			
		||||
      //   branchName: result.branchName
 | 
			
		||||
      // }), {
 | 
			
		||||
      //   status: 200,
 | 
			
		||||
      //   headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      // });
 | 
			
		||||
 | 
			
		||||
      // AFTER: Single line with consolidated helper
 | 
			
		||||
      return apiResponse.created({
 | 
			
		||||
        message: result.message,
 | 
			
		||||
        prUrl: result.prUrl,
 | 
			
		||||
        branchName: result.branchName
 | 
			
		||||
      }), {
 | 
			
		||||
        status: 200,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      // Log failed contribution
 | 
			
		||||
      console.error(`[CONTRIBUTION FAILED] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail}: ${result.message}`);
 | 
			
		||||
      
 | 
			
		||||
      return new Response(JSON.stringify({
 | 
			
		||||
        success: false,
 | 
			
		||||
        error: result.message
 | 
			
		||||
      }), {
 | 
			
		||||
        status: 500,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
      // BEFORE: Manual error response (7 lines)
 | 
			
		||||
      // return new Response(JSON.stringify({
 | 
			
		||||
      //   success: false,
 | 
			
		||||
      //   error: result.message
 | 
			
		||||
      // }), {
 | 
			
		||||
      //   status: 500,
 | 
			
		||||
      //   headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      // });
 | 
			
		||||
 | 
			
		||||
      // AFTER: Single line with consolidated helper
 | 
			
		||||
      return apiServerError.internal(`Contribution failed: ${result.message}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Contribution API error:', error);
 | 
			
		||||
    
 | 
			
		||||
    return new Response(JSON.stringify({
 | 
			
		||||
      success: false,
 | 
			
		||||
      error: 'Internal server error'
 | 
			
		||||
    }), {
 | 
			
		||||
      status: 500,
 | 
			
		||||
      headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  }, 'Contribution processing failed');
 | 
			
		||||
};
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
// src/pages/api/upload/media.ts
 | 
			
		||||
// src/pages/api/upload/media.ts (UPDATED - Using consolidated API responses)
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { getSessionFromRequest, verifySession, withAPIAuth, createAuthErrorResponse } from '../../../utils/auth.js';
 | 
			
		||||
import { withAPIAuth } from '../../../utils/auth.js';
 | 
			
		||||
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
 | 
			
		||||
import { NextcloudUploader, isNextcloudConfigured } from '../../../utils/nextcloud.js';
 | 
			
		||||
import { promises as fs } from 'fs';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
@ -62,102 +63,38 @@ function checkUploadRateLimit(userEmail: string): boolean {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function validateFile(file: File): { valid: boolean; error?: string } {
 | 
			
		||||
  // Check file size
 | 
			
		||||
  // File size check
 | 
			
		||||
  if (file.size > UPLOAD_CONFIG.maxFileSize) {
 | 
			
		||||
    return {
 | 
			
		||||
      valid: false,
 | 
			
		||||
      error: `File too large (max ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB)`
 | 
			
		||||
    return { 
 | 
			
		||||
      valid: false, 
 | 
			
		||||
      error: `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB` 
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Check file type
 | 
			
		||||
  // File type check
 | 
			
		||||
  if (!UPLOAD_CONFIG.allowedTypes.has(file.type)) {
 | 
			
		||||
    return {
 | 
			
		||||
      valid: false,
 | 
			
		||||
      error: `File type not allowed: ${file.type}`
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Check filename
 | 
			
		||||
  if (!file.name || file.name.trim().length === 0) {
 | 
			
		||||
    return {
 | 
			
		||||
      valid: false,
 | 
			
		||||
      error: 'Invalid filename'
 | 
			
		||||
    return { 
 | 
			
		||||
      valid: false, 
 | 
			
		||||
      error: `File type ${file.type} not allowed` 
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return { valid: true };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sanitizeFilename(filename: string): string {
 | 
			
		||||
  // Remove or replace unsafe characters
 | 
			
		||||
  return filename
 | 
			
		||||
    .replace(/[^a-zA-Z0-9._-]/g, '_') // Replace unsafe chars with underscore
 | 
			
		||||
    .replace(/_{2,}/g, '_') // Replace multiple underscores with single
 | 
			
		||||
    .replace(/^_|_$/g, '') // Remove leading/trailing underscores
 | 
			
		||||
    .toLowerCase()
 | 
			
		||||
    .substring(0, 100); // Limit length
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generateUniqueFilename(originalName: string): string {
 | 
			
		||||
  const timestamp = Date.now();
 | 
			
		||||
  const randomId = crypto.randomBytes(4).toString('hex');
 | 
			
		||||
  const ext = path.extname(originalName);
 | 
			
		||||
  const base = path.basename(originalName, ext);
 | 
			
		||||
  const sanitizedBase = sanitizeFilename(base);
 | 
			
		||||
  
 | 
			
		||||
  return `${timestamp}_${randomId}_${sanitizedBase}${ext}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function uploadToLocal(file: File, category: string): Promise<UploadResult> {
 | 
			
		||||
  try {
 | 
			
		||||
    // Ensure upload directory exists
 | 
			
		||||
    const categoryDir = path.join(UPLOAD_CONFIG.localUploadPath, sanitizeFilename(category));
 | 
			
		||||
    await fs.mkdir(categoryDir, { recursive: true });
 | 
			
		||||
    
 | 
			
		||||
    // Generate unique filename
 | 
			
		||||
    const uniqueFilename = generateUniqueFilename(file.name);
 | 
			
		||||
    const filePath = path.join(categoryDir, uniqueFilename);
 | 
			
		||||
    
 | 
			
		||||
    // Convert file to buffer and write
 | 
			
		||||
    const arrayBuffer = await file.arrayBuffer();
 | 
			
		||||
    const buffer = Buffer.from(arrayBuffer);
 | 
			
		||||
    await fs.writeFile(filePath, buffer);
 | 
			
		||||
    
 | 
			
		||||
    // Generate public URL
 | 
			
		||||
    const relativePath = path.posix.join('/uploads', sanitizeFilename(category), uniqueFilename);
 | 
			
		||||
    const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}${relativePath}`;
 | 
			
		||||
    
 | 
			
		||||
    return {
 | 
			
		||||
      success: true,
 | 
			
		||||
      url: publicUrl,
 | 
			
		||||
      filename: uniqueFilename,
 | 
			
		||||
      size: file.size,
 | 
			
		||||
      storage: 'local'
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Local upload error:', error);
 | 
			
		||||
    return {
 | 
			
		||||
      success: false,
 | 
			
		||||
      error: error instanceof Error ? error.message : 'Local upload failed',
 | 
			
		||||
      storage: 'local'
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function uploadToNextcloud(file: File, category: string): Promise<UploadResult> {
 | 
			
		||||
async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadResult> {
 | 
			
		||||
  try {
 | 
			
		||||
    const uploader = new NextcloudUploader();
 | 
			
		||||
    const result = await uploader.uploadFile(file, category);
 | 
			
		||||
    
 | 
			
		||||
    const result = await uploader.uploadFile(file, userEmail);
 | 
			
		||||
    return {
 | 
			
		||||
      ...result,
 | 
			
		||||
      success: true,
 | 
			
		||||
      url: result.url,
 | 
			
		||||
      filename: result.filename,
 | 
			
		||||
      size: file.size,
 | 
			
		||||
      storage: 'nextcloud'
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Nextcloud upload error:', error);
 | 
			
		||||
    console.error('Nextcloud upload failed:', error);
 | 
			
		||||
    return {
 | 
			
		||||
      success: false,
 | 
			
		||||
      error: error instanceof Error ? error.message : 'Nextcloud upload failed',
 | 
			
		||||
@ -166,66 +103,91 @@ async function uploadToNextcloud(file: File, category: string): Promise<UploadRe
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
async function uploadToLocal(file: File, userType: string): Promise<UploadResult> {
 | 
			
		||||
  try {
 | 
			
		||||
    // Check authentication
 | 
			
		||||
    // Ensure upload directory exists
 | 
			
		||||
    await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
 | 
			
		||||
    
 | 
			
		||||
    // Generate unique filename
 | 
			
		||||
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
 | 
			
		||||
    const randomString = crypto.randomBytes(8).toString('hex');
 | 
			
		||||
    const extension = path.extname(file.name);
 | 
			
		||||
    const filename = `${timestamp}-${randomString}${extension}`;
 | 
			
		||||
    
 | 
			
		||||
    // Save file
 | 
			
		||||
    const filepath = path.join(UPLOAD_CONFIG.localUploadPath, filename);
 | 
			
		||||
    const buffer = Buffer.from(await file.arrayBuffer());
 | 
			
		||||
    await fs.writeFile(filepath, buffer);
 | 
			
		||||
    
 | 
			
		||||
    // Generate public URL
 | 
			
		||||
    const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}/uploads/${filename}`;
 | 
			
		||||
    
 | 
			
		||||
    return {
 | 
			
		||||
      success: true,
 | 
			
		||||
      url: publicUrl,
 | 
			
		||||
      filename: filename,
 | 
			
		||||
      size: file.size,
 | 
			
		||||
      storage: 'local'
 | 
			
		||||
    };
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Local upload failed:', error);
 | 
			
		||||
    return {
 | 
			
		||||
      success: false,
 | 
			
		||||
      error: error instanceof Error ? error.message : 'Local upload failed',
 | 
			
		||||
      storage: 'local'
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// POST endpoint for file uploads
 | 
			
		||||
export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
  return await handleAPIRequest(async () => {
 | 
			
		||||
    // Authentication check
 | 
			
		||||
    const authResult = await withAPIAuth(request);
 | 
			
		||||
    if (authResult.authRequired && !authResult.authenticated) {
 | 
			
		||||
      return createAuthErrorResponse('Authentication required');
 | 
			
		||||
      return apiError.unauthorized();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const userEmail = authResult.session?.email || 'anonymous';
 | 
			
		||||
    const userEmail = authResult.session?.email || 'anonymous@example.com';
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Rate limiting
 | 
			
		||||
    if (!checkUploadRateLimit(userEmail)) {
 | 
			
		||||
      return new Response(JSON.stringify({ 
 | 
			
		||||
        error: 'Upload rate limit exceeded. Please wait before uploading more files.' 
 | 
			
		||||
      }), {
 | 
			
		||||
        status: 429,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
      return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.');
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Parse form data
 | 
			
		||||
    const formData = await request.formData();
 | 
			
		||||
 | 
			
		||||
    // Parse multipart form data
 | 
			
		||||
    let formData;
 | 
			
		||||
    try {
 | 
			
		||||
      formData = await request.formData();
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return apiError.badRequest('Invalid form data');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const file = formData.get('file') as File;
 | 
			
		||||
    const type = formData.get('type') as string || 'general';
 | 
			
		||||
    
 | 
			
		||||
    const type = formData.get('type') as string;
 | 
			
		||||
 | 
			
		||||
    if (!file) {
 | 
			
		||||
      return new Response(JSON.stringify({ 
 | 
			
		||||
        error: 'No file provided' 
 | 
			
		||||
      }), {
 | 
			
		||||
        status: 400,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
      return apiSpecial.missingRequired(['file']);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // Validate file
 | 
			
		||||
    const validation = validateFile(file);
 | 
			
		||||
    if (!validation.valid) {
 | 
			
		||||
      return new Response(JSON.stringify({ 
 | 
			
		||||
        error: validation.error 
 | 
			
		||||
      }), {
 | 
			
		||||
        status: 400,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
      return apiError.badRequest(validation.error!);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Determine upload strategy
 | 
			
		||||
    const useNextcloud = isNextcloudConfigured();
 | 
			
		||||
 | 
			
		||||
    // Attempt upload (Nextcloud first, then local fallback)
 | 
			
		||||
    let result: UploadResult;
 | 
			
		||||
    
 | 
			
		||||
    if (useNextcloud) {
 | 
			
		||||
      // Try Nextcloud first, fallback to local
 | 
			
		||||
      result = await uploadToNextcloud(file, type);
 | 
			
		||||
    if (isNextcloudConfigured()) {
 | 
			
		||||
      result = await uploadToNextcloud(file, userEmail);
 | 
			
		||||
      
 | 
			
		||||
      // If Nextcloud fails, try local fallback
 | 
			
		||||
      if (!result.success) {
 | 
			
		||||
        console.warn('Nextcloud upload failed, falling back to local storage:', result.error);
 | 
			
		||||
        console.warn('Nextcloud upload failed, trying local fallback:', result.error);
 | 
			
		||||
        result = await uploadToLocal(file, type);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      // Use local storage
 | 
			
		||||
      result = await uploadToLocal(file, type);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
@ -233,46 +195,48 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
      // Log successful upload
 | 
			
		||||
      console.log(`[MEDIA UPLOAD] ${file.name} (${file.size} bytes) by ${userEmail} -> ${result.storage}: ${result.url}`);
 | 
			
		||||
      
 | 
			
		||||
      return new Response(JSON.stringify(result), {
 | 
			
		||||
        status: 200,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      // BEFORE: Manual success response (5 lines)
 | 
			
		||||
      // return new Response(JSON.stringify(result), {
 | 
			
		||||
      //   status: 200,
 | 
			
		||||
      //   headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      // });
 | 
			
		||||
 | 
			
		||||
      // AFTER: Single line with specialized helper
 | 
			
		||||
      return apiSpecial.uploadSuccess({
 | 
			
		||||
        url: result.url!,
 | 
			
		||||
        filename: result.filename!,
 | 
			
		||||
        size: result.size!,
 | 
			
		||||
        storage: result.storage!
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      // Log failed upload
 | 
			
		||||
      console.error(`[MEDIA UPLOAD FAILED] ${file.name} by ${userEmail}: ${result.error}`);
 | 
			
		||||
      
 | 
			
		||||
      return new Response(JSON.stringify(result), {
 | 
			
		||||
        status: 500,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
      // BEFORE: Manual error response (5 lines)
 | 
			
		||||
      // return new Response(JSON.stringify(result), {
 | 
			
		||||
      //   status: 500,
 | 
			
		||||
      //   headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      // });
 | 
			
		||||
 | 
			
		||||
      // AFTER: Single line with specialized helper
 | 
			
		||||
      return apiSpecial.uploadFailed(result.error!);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Media upload API error:', error);
 | 
			
		||||
    
 | 
			
		||||
    return new Response(JSON.stringify({
 | 
			
		||||
      success: false,
 | 
			
		||||
      error: 'Internal server error'
 | 
			
		||||
    }), {
 | 
			
		||||
      status: 500,
 | 
			
		||||
      headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  }, 'Media upload processing failed');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// GET endpoint for upload status/info
 | 
			
		||||
export const GET: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // Check authentication
 | 
			
		||||
  return await handleAPIRequest(async () => {
 | 
			
		||||
    // Authentication check
 | 
			
		||||
    const authResult = await withAPIAuth(request);
 | 
			
		||||
    if (authResult.authRequired && !authResult.authenticated) {
 | 
			
		||||
      return createAuthErrorResponse('Authentication required');
 | 
			
		||||
      return apiError.unauthorized();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Return upload configuration and status
 | 
			
		||||
    const nextcloudConfigured = isNextcloudConfigured();
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Check local upload directory
 | 
			
		||||
    let localStorageAvailable = false;
 | 
			
		||||
    try {
 | 
			
		||||
@ -314,19 +278,7 @@ export const GET: APIRoute = async ({ request }) => {
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    return new Response(JSON.stringify(status), {
 | 
			
		||||
      status: 200,
 | 
			
		||||
      headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
    });
 | 
			
		||||
    return apiResponse.success(status);
 | 
			
		||||
    
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Media upload status error:', error);
 | 
			
		||||
    
 | 
			
		||||
    return new Response(JSON.stringify({
 | 
			
		||||
      error: 'Failed to get upload status'
 | 
			
		||||
    }), {
 | 
			
		||||
      status: 500,
 | 
			
		||||
      headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  }, 'Upload status retrieval failed');
 | 
			
		||||
};
 | 
			
		||||
@ -1,13 +1,49 @@
 | 
			
		||||
// src/utils/auth.ts - SERVER-SIDE ONLY (remove client-side functions)
 | 
			
		||||
import { SignJWT, jwtVerify, type JWTPayload } from 'jose';
 | 
			
		||||
import { serialize, parse } from 'cookie';
 | 
			
		||||
import { config } from 'dotenv';
 | 
			
		||||
// src/utils/auth.js (FIXED - Cleaned up and enhanced debugging)
 | 
			
		||||
import type { AstroGlobal } from 'astro';
 | 
			
		||||
import jwt from 'jsonwebtoken';
 | 
			
		||||
import crypto from 'crypto';
 | 
			
		||||
import { config } from 'dotenv';
 | 
			
		||||
import { SignJWT, jwtVerify, type JWTPayload } from 'jose';
 | 
			
		||||
import { serialize, parse as parseCookie } from 'cookie';
 | 
			
		||||
 | 
			
		||||
// Load environment variables
 | 
			
		||||
config();
 | 
			
		||||
 | 
			
		||||
// JWT session constants
 | 
			
		||||
const SECRET_KEY = new TextEncoder().encode(
 | 
			
		||||
  process.env.AUTH_SECRET || 
 | 
			
		||||
  process.env.OIDC_CLIENT_SECRET || 
 | 
			
		||||
  'cc24-hub-default-secret-key-change-in-production'
 | 
			
		||||
);
 | 
			
		||||
const SESSION_DURATION = 6 * 60 * 60; // 6 hours in seconds
 | 
			
		||||
 | 
			
		||||
// Types
 | 
			
		||||
export interface SessionData {
 | 
			
		||||
  userId: string;
 | 
			
		||||
  email: string;
 | 
			
		||||
  authenticated: boolean;
 | 
			
		||||
  exp: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuthContext {
 | 
			
		||||
  authenticated: boolean;
 | 
			
		||||
  session: SessionData | null;
 | 
			
		||||
  userEmail: string;
 | 
			
		||||
  userId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UserInfo {
 | 
			
		||||
  sub?: string;
 | 
			
		||||
  preferred_username?: string;
 | 
			
		||||
  email?: string;
 | 
			
		||||
  given_name?: string;
 | 
			
		||||
  family_name?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuthStateData {
 | 
			
		||||
  state: string;
 | 
			
		||||
  returnTo: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Environment variables - use runtime access for server-side
 | 
			
		||||
function getEnv(key: string): string {
 | 
			
		||||
  const value = process.env[key];
 | 
			
		||||
@ -17,59 +53,25 @@ function getEnv(key: string): string {
 | 
			
		||||
  return value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const SECRET_KEY = new TextEncoder().encode(
 | 
			
		||||
  process.env.AUTH_SECRET || 
 | 
			
		||||
  process.env.OIDC_CLIENT_SECRET || 
 | 
			
		||||
  'cc24-hub-default-secret-key-change-in-production'
 | 
			
		||||
);
 | 
			
		||||
const SESSION_DURATION = 6 * 60 * 60; // 6 hours in seconds
 | 
			
		||||
 | 
			
		||||
export interface SessionData {
 | 
			
		||||
  userId: string;
 | 
			
		||||
  email: string;
 | 
			
		||||
  authenticated: boolean;
 | 
			
		||||
  exp: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UserInfo {
 | 
			
		||||
  sub: string;
 | 
			
		||||
  preferred_username?: string;
 | 
			
		||||
  email?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuthContext {
 | 
			
		||||
  authenticated: boolean;
 | 
			
		||||
  session: SessionData | null;
 | 
			
		||||
  userEmail: string;
 | 
			
		||||
  userId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuthStateData {
 | 
			
		||||
  state: string;
 | 
			
		||||
  returnTo: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// Create a signed JWT session token with email
 | 
			
		||||
export async function createSession(userId: string, email: string): Promise<string> {
 | 
			
		||||
  const exp = Math.floor(Date.now() / 1000) + SESSION_DURATION;
 | 
			
		||||
// Session management functions
 | 
			
		||||
export function getSessionFromRequest(request: Request): string | null {
 | 
			
		||||
  const cookieHeader = request.headers.get('cookie');
 | 
			
		||||
  console.log('[DEBUG] Cookie header:', cookieHeader ? 'present' : 'missing');
 | 
			
		||||
  
 | 
			
		||||
  return await new SignJWT({ 
 | 
			
		||||
    userId, 
 | 
			
		||||
    email,
 | 
			
		||||
    authenticated: true, 
 | 
			
		||||
    exp 
 | 
			
		||||
  })
 | 
			
		||||
    .setProtectedHeader({ alg: 'HS256' })
 | 
			
		||||
    .setExpirationTime(exp)
 | 
			
		||||
    .sign(SECRET_KEY);
 | 
			
		||||
  if (!cookieHeader) return null;
 | 
			
		||||
  
 | 
			
		||||
  const cookies = parseCookie(cookieHeader);
 | 
			
		||||
  console.log('[DEBUG] Parsed cookies:', Object.keys(cookies));
 | 
			
		||||
  console.log('[DEBUG] Session cookie found:', !!cookies.session);
 | 
			
		||||
  
 | 
			
		||||
  return cookies.session || null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Verify and decode a session token
 | 
			
		||||
export async function verifySession(token: string): Promise<SessionData | null> {
 | 
			
		||||
export async function verifySession(sessionToken: string): Promise<SessionData | null> {
 | 
			
		||||
  try {
 | 
			
		||||
    const { payload } = await jwtVerify(token, SECRET_KEY);
 | 
			
		||||
    console.log('[DEBUG] Verifying session token, length:', sessionToken.length);
 | 
			
		||||
    const { payload } = await jwtVerify(sessionToken, SECRET_KEY);
 | 
			
		||||
    console.log('[DEBUG] JWT verification successful, payload keys:', Object.keys(payload));
 | 
			
		||||
    
 | 
			
		||||
    // Validate payload structure and cast properly
 | 
			
		||||
    if (
 | 
			
		||||
@ -78,6 +80,7 @@ export async function verifySession(token: string): Promise<SessionData | null>
 | 
			
		||||
      typeof payload.authenticated === 'boolean' &&
 | 
			
		||||
      typeof payload.exp === 'number'
 | 
			
		||||
    ) {
 | 
			
		||||
      console.log('[DEBUG] Session validation successful for user:', payload.userId);
 | 
			
		||||
      return {
 | 
			
		||||
        userId: payload.userId,
 | 
			
		||||
        email: payload.email,
 | 
			
		||||
@ -86,49 +89,62 @@ export async function verifySession(token: string): Promise<SessionData | null>
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    console.log('[DEBUG] Session payload validation failed, payload:', payload);
 | 
			
		||||
    return null;
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.log('Session verification failed:', error);
 | 
			
		||||
    console.log('[DEBUG] Session verification failed:', error.message);
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get session from request cookies
 | 
			
		||||
export function getSessionFromRequest(request: Request): string | null {
 | 
			
		||||
  const cookieHeader = request.headers.get('cookie');
 | 
			
		||||
  if (!cookieHeader) return null;
 | 
			
		||||
export async function createSession(userId: string, email: string): Promise<string> {
 | 
			
		||||
  const exp = Math.floor(Date.now() / 1000) + SESSION_DURATION;
 | 
			
		||||
  console.log('[DEBUG] Creating session for user:', userId, 'exp:', exp);
 | 
			
		||||
  
 | 
			
		||||
  const cookies = parse(cookieHeader);
 | 
			
		||||
  return cookies.session || null;
 | 
			
		||||
  const token = await new SignJWT({ 
 | 
			
		||||
    userId, 
 | 
			
		||||
    email,
 | 
			
		||||
    authenticated: true, 
 | 
			
		||||
    exp 
 | 
			
		||||
  })
 | 
			
		||||
    .setProtectedHeader({ alg: 'HS256' })
 | 
			
		||||
    .setExpirationTime(exp)
 | 
			
		||||
    .sign(SECRET_KEY);
 | 
			
		||||
    
 | 
			
		||||
  console.log('[DEBUG] Session token created, length:', token.length);
 | 
			
		||||
  return token;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create session cookie
 | 
			
		||||
export function createSessionCookie(token: string): string {
 | 
			
		||||
export function createSessionCookie(sessionToken: string): string {
 | 
			
		||||
  const publicBaseUrl = getEnv('PUBLIC_BASE_URL');
 | 
			
		||||
  const isProduction = process.env.NODE_ENV === 'production';
 | 
			
		||||
  const isSecure = publicBaseUrl.startsWith('https://') || isProduction;
 | 
			
		||||
  
 | 
			
		||||
  return serialize('session', token, {
 | 
			
		||||
  const cookie = serialize('session', sessionToken, {
 | 
			
		||||
    httpOnly: true,
 | 
			
		||||
    secure: isSecure,
 | 
			
		||||
    sameSite: 'lax',
 | 
			
		||||
    maxAge: SESSION_DURATION,
 | 
			
		||||
    path: '/'
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  console.log('[DEBUG] Created session cookie:', cookie.substring(0, 100) + '...');
 | 
			
		||||
  return cookie;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Clear session cookie
 | 
			
		||||
export function clearSessionCookie(): string {
 | 
			
		||||
  const publicBaseUrl = getEnv('PUBLIC_BASE_URL');
 | 
			
		||||
  const isSecure = publicBaseUrl.startsWith('https://');
 | 
			
		||||
  
 | 
			
		||||
  return serialize('session', '', {
 | 
			
		||||
    httpOnly: true,
 | 
			
		||||
    secure: isSecure,
 | 
			
		||||
    sameSite: 'lax',
 | 
			
		||||
    maxAge: 0,
 | 
			
		||||
    path: '/'
 | 
			
		||||
  });
 | 
			
		||||
// Authentication utility functions
 | 
			
		||||
export function getUserEmail(userInfo: UserInfo): string {
 | 
			
		||||
  return userInfo.email || userInfo.preferred_username || 'unknown@example.com';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function logAuthEvent(event: string, details?: any): void {
 | 
			
		||||
  const timestamp = new Date().toISOString();
 | 
			
		||||
  console.log(`[AUTH ${timestamp}] ${event}`, details ? JSON.stringify(details) : '');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Generate random state for CSRF protection
 | 
			
		||||
export function generateState(): string {
 | 
			
		||||
  return crypto.randomUUID();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Generate OIDC authorization URL
 | 
			
		||||
@ -149,7 +165,7 @@ export function generateAuthUrl(state: string): string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Exchange authorization code for tokens
 | 
			
		||||
export async function exchangeCodeForTokens(code: string): Promise<any> {
 | 
			
		||||
export async function exchangeCodeForTokens(code: string): Promise<{ access_token: string }> {
 | 
			
		||||
  const oidcEndpoint = getEnv('OIDC_ENDPOINT');
 | 
			
		||||
  const clientId = getEnv('OIDC_CLIENT_ID');
 | 
			
		||||
  const clientSecret = getEnv('OIDC_CLIENT_SECRET');
 | 
			
		||||
@ -196,27 +212,7 @@ export async function getUserInfo(accessToken: string): Promise<UserInfo> {
 | 
			
		||||
  return await response.json();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Generate random state for CSRF protection
 | 
			
		||||
export function generateState(): string {
 | 
			
		||||
  return crypto.randomUUID();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Log authentication events for debugging
 | 
			
		||||
export function logAuthEvent(event: string, details?: any) {
 | 
			
		||||
  const timestamp = new Date().toISOString();
 | 
			
		||||
  console.log(`[AUTH ${timestamp}] ${event}`, details ? JSON.stringify(details) : '');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to safely get email from user info
 | 
			
		||||
export function getUserEmail(userInfo: UserInfo): string {
 | 
			
		||||
  return userInfo.email || 
 | 
			
		||||
         `${userInfo.preferred_username || userInfo.sub || 'unknown'}@cc24.dev`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * CONSOLIDATED: Parse and validate auth state from cookies
 | 
			
		||||
 * Replaces duplicated cookie parsing in callback.ts and process.ts
 | 
			
		||||
 */
 | 
			
		||||
// Parse and validate auth state from cookies
 | 
			
		||||
export function parseAuthState(request: Request): { 
 | 
			
		||||
  isValid: boolean; 
 | 
			
		||||
  stateData: AuthStateData | null; 
 | 
			
		||||
@ -224,7 +220,7 @@ export function parseAuthState(request: Request): {
 | 
			
		||||
} {
 | 
			
		||||
  try {
 | 
			
		||||
    const cookieHeader = request.headers.get('cookie');
 | 
			
		||||
    const cookies = cookieHeader ? parse(cookieHeader) : {};
 | 
			
		||||
    const cookies = cookieHeader ? parseCookie(cookieHeader) : {};
 | 
			
		||||
    
 | 
			
		||||
    if (!cookies.auth_state) {
 | 
			
		||||
      return { isValid: false, stateData: null, error: 'No auth state cookie' };
 | 
			
		||||
@ -242,10 +238,7 @@ export function parseAuthState(request: Request): {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * CONSOLIDATED: Verify state parameter against stored state
 | 
			
		||||
 * Replaces duplicated verification logic in callback.ts and process.ts
 | 
			
		||||
 */
 | 
			
		||||
// Verify state parameter against stored state
 | 
			
		||||
export function verifyAuthState(request: Request, receivedState: string): {
 | 
			
		||||
  isValid: boolean;
 | 
			
		||||
  stateData: AuthStateData | null;
 | 
			
		||||
@ -307,6 +300,8 @@ export async function createSessionWithCookie(userInfo: UserInfo): Promise<{
 | 
			
		||||
 */
 | 
			
		||||
export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Response> {
 | 
			
		||||
  const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
 | 
			
		||||
  console.log('[DEBUG PAGE] Auth required:', authRequired);
 | 
			
		||||
  console.log('[DEBUG PAGE] Request URL:', Astro.url.toString());
 | 
			
		||||
  
 | 
			
		||||
  // If auth not required, return mock context
 | 
			
		||||
  if (!authRequired) {
 | 
			
		||||
@ -320,7 +315,10 @@ export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Respon
 | 
			
		||||
 | 
			
		||||
  // Check session
 | 
			
		||||
  const sessionToken = getSessionFromRequest(Astro.request);
 | 
			
		||||
  console.log('[DEBUG PAGE] Session token found:', !!sessionToken);
 | 
			
		||||
  
 | 
			
		||||
  if (!sessionToken) {
 | 
			
		||||
    console.log('[DEBUG PAGE] No session token, redirecting to login');
 | 
			
		||||
    const loginUrl = `/api/auth/login?returnTo=${encodeURIComponent(Astro.url.toString())}`;
 | 
			
		||||
    return new Response(null, {
 | 
			
		||||
      status: 302,
 | 
			
		||||
@ -329,7 +327,10 @@ export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Respon
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const session = await verifySession(sessionToken);
 | 
			
		||||
  console.log('[DEBUG PAGE] Session verification result:', !!session);
 | 
			
		||||
  
 | 
			
		||||
  if (!session) {
 | 
			
		||||
    console.log('[DEBUG PAGE] Session verification failed, redirecting to login');
 | 
			
		||||
    const loginUrl = `/api/auth/login?returnTo=${encodeURIComponent(Astro.url.toString())}`;
 | 
			
		||||
    return new Response(null, {
 | 
			
		||||
      status: 302,
 | 
			
		||||
@ -337,6 +338,7 @@ export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Respon
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  console.log('[DEBUG PAGE] Page authentication successful for user:', session.userId);
 | 
			
		||||
  return {
 | 
			
		||||
    authenticated: true,
 | 
			
		||||
    session,
 | 
			
		||||
@ -366,7 +368,10 @@ export async function withAPIAuth(request: Request): Promise<{
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const sessionToken = getSessionFromRequest(request);
 | 
			
		||||
  console.log('[DEBUG] Session token found:', !!sessionToken);
 | 
			
		||||
  
 | 
			
		||||
  if (!sessionToken) {
 | 
			
		||||
    console.log('[DEBUG] No session token found');
 | 
			
		||||
    return { 
 | 
			
		||||
      authenticated: false, 
 | 
			
		||||
      userId: '', 
 | 
			
		||||
@ -375,7 +380,10 @@ export async function withAPIAuth(request: Request): Promise<{
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const session = await verifySession(sessionToken);
 | 
			
		||||
  console.log('[DEBUG] Session verification result:', !!session);
 | 
			
		||||
  
 | 
			
		||||
  if (!session) {
 | 
			
		||||
    console.log('[DEBUG] Session verification failed');
 | 
			
		||||
    return { 
 | 
			
		||||
      authenticated: false, 
 | 
			
		||||
      userId: '', 
 | 
			
		||||
@ -383,38 +391,11 @@ export async function withAPIAuth(request: Request): Promise<{
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  console.log('[DEBUG] Authentication successful for user:', session.userId);
 | 
			
		||||
  return {
 | 
			
		||||
    authenticated: true,
 | 
			
		||||
    userId: session.userId,
 | 
			
		||||
    session,
 | 
			
		||||
    authRequired: true  
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper to create consistent API auth error responses
 | 
			
		||||
 */
 | 
			
		||||
export function createAuthErrorResponse(message: string = 'Authentication required'): Response {
 | 
			
		||||
  return new Response(JSON.stringify({ error: message }), {
 | 
			
		||||
    status: 401,
 | 
			
		||||
    headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * CONSOLIDATED: Create consistent API responses
 | 
			
		||||
 */
 | 
			
		||||
export function createAPIResponse(data: any, status: number = 200): Response {
 | 
			
		||||
  return new Response(JSON.stringify(data), {
 | 
			
		||||
    status,
 | 
			
		||||
    headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createBadRequestResponse(message: string = 'Bad request'): Response {
 | 
			
		||||
  return createAPIResponse({ error: message }, 400);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createSuccessResponse(data: any = { success: true }): Response {
 | 
			
		||||
  return createAPIResponse(data, 200);
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user