consolidation and unification
This commit is contained in:
		
							parent
							
								
									72bcc04309
								
							
						
					
					
						commit
						f92219f61f
					
				@ -16,6 +16,7 @@
 | 
			
		||||
    "dotenv": "^16.4.5",
 | 
			
		||||
    "jose": "^5.2.0",
 | 
			
		||||
    "js-yaml": "^4.1.0",
 | 
			
		||||
    "jsonwebtoken": "^9.0.2",
 | 
			
		||||
    "zod": "^3.25.76"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
 | 
			
		||||
@ -1,102 +0,0 @@
 | 
			
		||||
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 {
 | 
			
		||||
    if (process.env.NODE_ENV === 'development') {
 | 
			
		||||
      console.log('Auth callback processing...');
 | 
			
		||||
      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' }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@ -28,7 +28,7 @@ export const GET: APIRoute = async ({ url, redirect }) => {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    logAuthEvent('Login failed', { error: error.message });
 | 
			
		||||
    logAuthEvent('Login failed', { error: error instanceof Error ? error.message : 'Unknown error' });
 | 
			
		||||
    return new Response('Authentication error', { status: 500 });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@ -1,99 +1,61 @@
 | 
			
		||||
// src/pages/api/auth/process.ts - Fixed Email Support
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { parse } from 'cookie';
 | 
			
		||||
import { 
 | 
			
		||||
  verifyAuthState,
 | 
			
		||||
  exchangeCodeForTokens, 
 | 
			
		||||
  getUserInfo, 
 | 
			
		||||
  createSession, 
 | 
			
		||||
  createSessionCookie, 
 | 
			
		||||
  createSessionWithCookie,
 | 
			
		||||
  logAuthEvent,
 | 
			
		||||
  getUserEmail
 | 
			
		||||
  createBadRequestResponse,
 | 
			
		||||
  createSuccessResponse
 | 
			
		||||
} 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);
 | 
			
		||||
    
 | 
			
		||||
    // Parse request body
 | 
			
		||||
    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' }
 | 
			
		||||
      });
 | 
			
		||||
      return createBadRequestResponse('Invalid 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' }
 | 
			
		||||
      });
 | 
			
		||||
      return createBadRequestResponse('Missing required parameters');
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 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' }
 | 
			
		||||
      });
 | 
			
		||||
    // 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');
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Exchange code for tokens
 | 
			
		||||
    console.log('Exchanging code for tokens...');
 | 
			
		||||
    // Exchange code for tokens and get user info
 | 
			
		||||
    const tokens = await exchangeCodeForTokens(code);
 | 
			
		||||
    
 | 
			
		||||
    // Get user info
 | 
			
		||||
    console.log('Getting user info...');
 | 
			
		||||
    const userInfo = await getUserInfo(tokens.access_token);
 | 
			
		||||
    
 | 
			
		||||
    // Extract user details
 | 
			
		||||
    const userId = userInfo.sub || userInfo.preferred_username || 'unknown';
 | 
			
		||||
    const userEmail = getUserEmail(userInfo);
 | 
			
		||||
    
 | 
			
		||||
    // Create session with email
 | 
			
		||||
    const sessionToken = await createSession(userId, userEmail);
 | 
			
		||||
    const sessionCookie = createSessionCookie(sessionToken);
 | 
			
		||||
    // CONSOLIDATED: Single function call replaces 10+ lines of session creation
 | 
			
		||||
    const sessionResult = await createSessionWithCookie(userInfo);
 | 
			
		||||
    
 | 
			
		||||
    logAuthEvent('Authentication successful', { 
 | 
			
		||||
      userId: userId,
 | 
			
		||||
      email: userEmail 
 | 
			
		||||
      userId: sessionResult.userId,
 | 
			
		||||
      email: sessionResult.userEmail 
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Clear auth state cookie
 | 
			
		||||
    const clearStateCookie = 'auth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0';
 | 
			
		||||
    const returnTo = storedStateData.returnTo || '/';
 | 
			
		||||
    
 | 
			
		||||
    // Build response with cookies
 | 
			
		||||
    const headers = new Headers();
 | 
			
		||||
    headers.append('Content-Type', 'application/json');
 | 
			
		||||
    headers.append('Set-Cookie', sessionCookie);
 | 
			
		||||
    headers.append('Set-Cookie', clearStateCookie);
 | 
			
		||||
    headers.append('Set-Cookie', sessionResult.sessionCookie);
 | 
			
		||||
    headers.append('Set-Cookie', sessionResult.clearStateCookie);
 | 
			
		||||
    
 | 
			
		||||
    return new Response(JSON.stringify({ 
 | 
			
		||||
      success: true, 
 | 
			
		||||
      redirectTo: returnTo 
 | 
			
		||||
      redirectTo: stateVerification.stateData.returnTo 
 | 
			
		||||
    }), {
 | 
			
		||||
      status: 200,
 | 
			
		||||
      headers: headers
 | 
			
		||||
@ -101,10 +63,9 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
    
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Authentication processing failed:', error);
 | 
			
		||||
    logAuthEvent('Authentication processing failed', { error: error instanceof Error ? error.message : 'Unknown error' });
 | 
			
		||||
    return new Response(JSON.stringify({ success: false }), {
 | 
			
		||||
      status: 500,
 | 
			
		||||
      headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
    logAuthEvent('Authentication processing failed', { 
 | 
			
		||||
      error: error instanceof Error ? error.message : 'Unknown error' 
 | 
			
		||||
    });
 | 
			
		||||
    return createBadRequestResponse('Authentication processing failed');
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@ -1,56 +1,24 @@
 | 
			
		||||
// src/pages/api/auth/status.ts
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
 | 
			
		||||
import { withAPIAuth, createAPIResponse } from '../../../utils/auth.js';
 | 
			
		||||
 | 
			
		||||
export const prerender = false;
 | 
			
		||||
 | 
			
		||||
export const GET: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // Check if authentication is required
 | 
			
		||||
    const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
 | 
			
		||||
    // CONSOLIDATED: Single function call replaces 35+ lines
 | 
			
		||||
    const authResult = await withAPIAuth(request);
 | 
			
		||||
    
 | 
			
		||||
    if (!authRequired) {
 | 
			
		||||
      // If authentication is not required, always return authenticated
 | 
			
		||||
      return new Response(JSON.stringify({ 
 | 
			
		||||
        authenticated: true,
 | 
			
		||||
        authRequired: false
 | 
			
		||||
      }), {
 | 
			
		||||
        status: 200,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const sessionToken = getSessionFromRequest(request);
 | 
			
		||||
    
 | 
			
		||||
    if (!sessionToken) {
 | 
			
		||||
      return new Response(JSON.stringify({ 
 | 
			
		||||
        authenticated: false,
 | 
			
		||||
        authRequired: true
 | 
			
		||||
      }), {
 | 
			
		||||
        status: 200,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    const session = await verifySession(sessionToken);
 | 
			
		||||
    
 | 
			
		||||
    return new Response(JSON.stringify({ 
 | 
			
		||||
      authenticated: session !== null,
 | 
			
		||||
      authRequired: true,
 | 
			
		||||
      expires: session?.exp ? new Date(session.exp * 1000).toISOString() : null
 | 
			
		||||
    }), {
 | 
			
		||||
      status: 200,
 | 
			
		||||
      headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
    return createAPIResponse({
 | 
			
		||||
      authenticated: authResult.authenticated,
 | 
			
		||||
      authRequired: authResult.authRequired,
 | 
			
		||||
      expires: authResult.session?.exp ? new Date(authResult.session.exp * 1000).toISOString() : null
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    return new Response(JSON.stringify({ 
 | 
			
		||||
    return createAPIResponse({
 | 
			
		||||
      authenticated: false,
 | 
			
		||||
      authRequired: process.env.AUTHENTICATION_NECESSARY !== 'false',
 | 
			
		||||
      error: 'Session verification failed'
 | 
			
		||||
    }), {
 | 
			
		||||
      status: 200,
 | 
			
		||||
      headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
// src/pages/api/contribute/knowledgebase.ts
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
 | 
			
		||||
import { withAPIAuth, createAuthErrorResponse } from '../../../utils/auth.js';
 | 
			
		||||
import { GitContributionManager } from '../../../utils/gitContributions.js';
 | 
			
		||||
import { z } from 'zod';
 | 
			
		||||
 | 
			
		||||
@ -270,26 +270,13 @@ ${data.article.uploadedFiles.map((file: any) => `- ${file.name} (${file.url})`).
 | 
			
		||||
export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // Check authentication
 | 
			
		||||
    const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
 | 
			
		||||
    
 | 
			
		||||
    if (authRequired) {
 | 
			
		||||
      const sessionToken = getSessionFromRequest(request);
 | 
			
		||||
      if (!sessionToken) {
 | 
			
		||||
        return new Response(JSON.stringify({ error: 'Authentication required' }), {
 | 
			
		||||
          status: 401,
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    const authResult = await withAPIAuth(request);
 | 
			
		||||
    if (authResult.authRequired && !authResult.authenticated) {
 | 
			
		||||
      return createAuthErrorResponse('Authentication required');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      const session = await verifySession(sessionToken);
 | 
			
		||||
      if (!session) {
 | 
			
		||||
        return new Response(JSON.stringify({ error: 'Invalid session' }), {
 | 
			
		||||
          status: 401,
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    const userEmail = authResult.session?.email || 'anonymous@example.com';
 | 
			
		||||
 | 
			
		||||
      const userEmail = session.email;
 | 
			
		||||
 | 
			
		||||
      // Rate limiting
 | 
			
		||||
      if (!checkRateLimit(userEmail)) {
 | 
			
		||||
@ -383,14 +370,6 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      return new Response(JSON.stringify({ 
 | 
			
		||||
        error: 'Authentication is disabled' 
 | 
			
		||||
      }), {
 | 
			
		||||
        status: 501,
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Knowledgebase contribution API error:', error);
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
// src/pages/api/contribute/tool.ts
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
 | 
			
		||||
import { withAPIAuth, createAuthErrorResponse } from '../../../utils/auth.js';
 | 
			
		||||
import { GitContributionManager, type ContributionData } from '../../../utils/gitContributions.js';
 | 
			
		||||
import { z } from 'zod';
 | 
			
		||||
 | 
			
		||||
@ -189,39 +189,21 @@ async function validateToolData(tool: any, action: 'add' | 'edit'): Promise<{ va
 | 
			
		||||
export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // Check if authentication is required
 | 
			
		||||
    const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
 | 
			
		||||
    let userId = 'anonymous';
 | 
			
		||||
    let userEmail = 'anonymous@example.com';
 | 
			
		||||
 | 
			
		||||
    if (authRequired) {
 | 
			
		||||
      // Authentication check
 | 
			
		||||
      const sessionToken = getSessionFromRequest(request);
 | 
			
		||||
      if (!sessionToken) {
 | 
			
		||||
        return new Response(JSON.stringify({ 
 | 
			
		||||
          success: false, 
 | 
			
		||||
          error: 'Authentication required' 
 | 
			
		||||
        }), {
 | 
			
		||||
          status: 401,
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const session = await verifySession(sessionToken);
 | 
			
		||||
      if (!session) {
 | 
			
		||||
        return new Response(JSON.stringify({ 
 | 
			
		||||
          success: false, 
 | 
			
		||||
          error: 'Invalid session' 
 | 
			
		||||
        }), {
 | 
			
		||||
          status: 401,
 | 
			
		||||
          headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      userId = session.userId;
 | 
			
		||||
      // In a real implementation, you might want to fetch user email from session or OIDC
 | 
			
		||||
      userEmail = `${userId}@cc24.dev`;
 | 
			
		||||
    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' }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const userId = authResult.session?.userId || 'anonymous';
 | 
			
		||||
    const userEmail = authResult.session?.email || 'anonymous@example.com';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // Rate limiting
 | 
			
		||||
    if (!checkRateLimit(userId)) {
 | 
			
		||||
      return new Response(JSON.stringify({ 
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
// src/pages/api/upload/media.ts
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
 | 
			
		||||
import { getSessionFromRequest, verifySession, withAPIAuth, createAuthErrorResponse } from '../../../utils/auth.js';
 | 
			
		||||
import { NextcloudUploader, isNextcloudConfigured } from '../../../utils/nextcloud.js';
 | 
			
		||||
import { promises as fs } from 'fs';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
@ -169,28 +169,13 @@ async function uploadToNextcloud(file: File, category: string): Promise<UploadRe
 | 
			
		||||
export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // Check authentication
 | 
			
		||||
    const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
 | 
			
		||||
    let userEmail = 'anonymous';
 | 
			
		||||
    
 | 
			
		||||
    if (authRequired) {
 | 
			
		||||
      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' }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      userEmail = session.email;
 | 
			
		||||
    const authResult = await withAPIAuth(request);
 | 
			
		||||
    if (authResult.authRequired && !authResult.authenticated) {
 | 
			
		||||
      return createAuthErrorResponse('Authentication required');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const userEmail = authResult.session?.email || 'anonymous';
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Rate limiting
 | 
			
		||||
    if (!checkUploadRateLimit(userEmail)) {
 | 
			
		||||
@ -279,28 +264,14 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
export const GET: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // Check authentication
 | 
			
		||||
    const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
 | 
			
		||||
    
 | 
			
		||||
    if (authRequired) {
 | 
			
		||||
      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' }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    const authResult = await withAPIAuth(request);
 | 
			
		||||
    if (authResult.authRequired && !authResult.authenticated) {
 | 
			
		||||
      return createAuthErrorResponse('Authentication required');
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // Return upload configuration and status
 | 
			
		||||
    const nextcloudConfigured = isNextcloudConfigured();
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Check local upload directory
 | 
			
		||||
    let localStorageAvailable = false;
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import { SignJWT, jwtVerify, type JWTPayload } from 'jose';
 | 
			
		||||
import { serialize, parse } from 'cookie';
 | 
			
		||||
import { config } from 'dotenv';
 | 
			
		||||
import type { AstroGlobal } from 'astro';
 | 
			
		||||
import jwt from 'jsonwebtoken';
 | 
			
		||||
 | 
			
		||||
// Load environment variables
 | 
			
		||||
config();
 | 
			
		||||
@ -31,12 +32,25 @@ export interface SessionData {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UserInfo {
 | 
			
		||||
  sub?: string;
 | 
			
		||||
  sub: string;
 | 
			
		||||
  preferred_username?: string;
 | 
			
		||||
  email?: string;
 | 
			
		||||
  name?: 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;
 | 
			
		||||
@ -97,7 +111,7 @@ export function createSessionCookie(token: string): string {
 | 
			
		||||
  return serialize('session', token, {
 | 
			
		||||
    httpOnly: true,
 | 
			
		||||
    secure: isSecure,
 | 
			
		||||
    sameSite: 'strict', // More secure than 'lax'
 | 
			
		||||
    sameSite: 'lax',
 | 
			
		||||
    maxAge: SESSION_DURATION,
 | 
			
		||||
    path: '/'
 | 
			
		||||
  });
 | 
			
		||||
@ -199,13 +213,91 @@ export function getUserEmail(userInfo: UserInfo): string {
 | 
			
		||||
         `${userInfo.preferred_username || userInfo.sub || 'unknown'}@cc24.dev`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// === CONSOLIDATION: Server-side Auth Helpers ===
 | 
			
		||||
/**
 | 
			
		||||
 * CONSOLIDATED: Parse and validate auth state from cookies
 | 
			
		||||
 * Replaces duplicated cookie parsing in callback.ts and process.ts
 | 
			
		||||
 */
 | 
			
		||||
export function parseAuthState(request: Request): { 
 | 
			
		||||
  isValid: boolean; 
 | 
			
		||||
  stateData: AuthStateData | null; 
 | 
			
		||||
  error?: string 
 | 
			
		||||
} {
 | 
			
		||||
  try {
 | 
			
		||||
    const cookieHeader = request.headers.get('cookie');
 | 
			
		||||
    const cookies = cookieHeader ? parse(cookieHeader) : {};
 | 
			
		||||
    
 | 
			
		||||
    if (!cookies.auth_state) {
 | 
			
		||||
      return { isValid: false, stateData: null, error: 'No auth state cookie' };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    const stateData = JSON.parse(decodeURIComponent(cookies.auth_state));
 | 
			
		||||
    
 | 
			
		||||
    if (!stateData.state || !stateData.returnTo) {
 | 
			
		||||
      return { isValid: false, stateData: null, error: 'Invalid state data structure' };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    return { isValid: true, stateData };
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    return { isValid: false, stateData: null, error: 'Failed to parse auth state' };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuthContext {
 | 
			
		||||
  authenticated: boolean;
 | 
			
		||||
  session: SessionData | null;
 | 
			
		||||
  userEmail: string;
 | 
			
		||||
/**
 | 
			
		||||
 * CONSOLIDATED: Verify state parameter against stored state
 | 
			
		||||
 * Replaces duplicated verification logic in callback.ts and process.ts
 | 
			
		||||
 */
 | 
			
		||||
export function verifyAuthState(request: Request, receivedState: string): {
 | 
			
		||||
  isValid: boolean;
 | 
			
		||||
  stateData: AuthStateData | null;
 | 
			
		||||
  error?: string;
 | 
			
		||||
} {
 | 
			
		||||
  const { isValid, stateData, error } = parseAuthState(request);
 | 
			
		||||
  
 | 
			
		||||
  if (!isValid || !stateData) {
 | 
			
		||||
    logAuthEvent('State parsing failed', { error });
 | 
			
		||||
    return { isValid: false, stateData: null, error };
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (stateData.state !== receivedState) {
 | 
			
		||||
    logAuthEvent('State mismatch', { 
 | 
			
		||||
      received: receivedState, 
 | 
			
		||||
      stored: stateData.state 
 | 
			
		||||
    });
 | 
			
		||||
    return { 
 | 
			
		||||
      isValid: false, 
 | 
			
		||||
      stateData: null, 
 | 
			
		||||
      error: 'State parameter mismatch' 
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return { isValid: true, stateData };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * CONSOLIDATED: Create session with cookie headers
 | 
			
		||||
 * Replaces duplicated session creation in callback.ts and process.ts
 | 
			
		||||
 */
 | 
			
		||||
export async function createSessionWithCookie(userInfo: UserInfo): Promise<{
 | 
			
		||||
  sessionToken: string;
 | 
			
		||||
  sessionCookie: string;
 | 
			
		||||
  clearStateCookie: string;
 | 
			
		||||
  userId: string;
 | 
			
		||||
  userEmail: string;
 | 
			
		||||
}> {
 | 
			
		||||
  const userId = userInfo.sub || userInfo.preferred_username || 'unknown';
 | 
			
		||||
  const userEmail = getUserEmail(userInfo);
 | 
			
		||||
  
 | 
			
		||||
  const sessionToken = await createSession(userId, userEmail);
 | 
			
		||||
  const sessionCookie = createSessionCookie(sessionToken);
 | 
			
		||||
  const clearStateCookie = 'auth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0';
 | 
			
		||||
  
 | 
			
		||||
  return {
 | 
			
		||||
    sessionToken,
 | 
			
		||||
    sessionCookie,
 | 
			
		||||
    clearStateCookie,
 | 
			
		||||
    userId,
 | 
			
		||||
    userEmail
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -255,37 +347,47 @@ export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Respon
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * CONSOLIDATED: Replace repeated auth patterns in API endpoints
 | 
			
		||||
 * Usage: const authResult = await withAPIAuth(request);
 | 
			
		||||
 *        if (!authResult.authenticated) return createAuthErrorResponse();
 | 
			
		||||
 * Enhanced version with better return structure
 | 
			
		||||
 */
 | 
			
		||||
export async function withAPIAuth(request: Request): Promise<{ 
 | 
			
		||||
  authenticated: boolean; 
 | 
			
		||||
  userId: string; 
 | 
			
		||||
  session?: SessionData 
 | 
			
		||||
  session?: SessionData;
 | 
			
		||||
  authRequired: boolean; 
 | 
			
		||||
}> {
 | 
			
		||||
  const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
 | 
			
		||||
  
 | 
			
		||||
  if (!authRequired) {
 | 
			
		||||
    return {
 | 
			
		||||
      authenticated: true,
 | 
			
		||||
      userId: 'anonymous'
 | 
			
		||||
      userId: 'anonymous',
 | 
			
		||||
      authRequired: false 
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const sessionToken = getSessionFromRequest(request);
 | 
			
		||||
  if (!sessionToken) {
 | 
			
		||||
    return { authenticated: false, userId: '' };
 | 
			
		||||
    return { 
 | 
			
		||||
      authenticated: false, 
 | 
			
		||||
      userId: '', 
 | 
			
		||||
      authRequired: true 
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const session = await verifySession(sessionToken);
 | 
			
		||||
  if (!session) {
 | 
			
		||||
    return { authenticated: false, userId: '' };
 | 
			
		||||
    return { 
 | 
			
		||||
      authenticated: false, 
 | 
			
		||||
      userId: '', 
 | 
			
		||||
      authRequired: true  
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    authenticated: true,
 | 
			
		||||
    userId: session.userId,
 | 
			
		||||
    session
 | 
			
		||||
    session,
 | 
			
		||||
    authRequired: true  
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -297,4 +399,22 @@ export function createAuthErrorResponse(message: string = 'Authentication requir
 | 
			
		||||
    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