diff --git a/astro.config.mjs b/astro.config.mjs index 69e2603..16016d8 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -14,5 +14,6 @@ export default defineConfig({ server: { port: 4321, host: true - } -}); \ No newline at end of file + }, + allowImportingTsExtensions: true +}); diff --git a/src/components/ToolFilters.astro b/src/components/ToolFilters.astro index 25e653a..760d649 100644 --- a/src/components/ToolFilters.astro +++ b/src/components/ToolFilters.astro @@ -150,24 +150,12 @@ const sortedTags = Object.entries(tagFrequency) let isTagCloudExpanded = false; // Check authentication status and show/hide AI button - async function checkAuthAndShowAIButton() { - try { - const response = await fetch('/api/auth/status'); - const data = await response.json(); - - // Show AI button if authentication is not required OR if user is authenticated - if (!data.authRequired || data.authenticated) { - if (aiViewToggle) { - aiViewToggle.style.display = 'inline-flex'; - } - } - } catch (error) { - console.log('Auth check failed, AI button remains hidden'); - } + async function initAIButton() { + await showIfAuthenticated('#ai-view-toggle'); } - + // Call auth check on page load - checkAuthAndShowAIButton(); + initAIButton(); // Initialize tag cloud state function initTagCloud() { diff --git a/src/pages/api/ai/query.ts b/src/pages/api/ai/query.ts index c321769..15ac5bb 100644 --- a/src/pages/api/ai/query.ts +++ b/src/pages/api/ai/query.ts @@ -1,6 +1,6 @@ // src/pages/api/ai/query.ts import type { APIRoute } from 'astro'; -import { getSessionFromRequest, verifySession } from '../../../utils/auth.js'; +import { withAPIAuth, createAuthErrorResponse } from '../../../utils/auth.js'; import { getCompressedToolsDataForAI } from '../../../utils/dataService.js'; export const prerender = false; @@ -275,30 +275,13 @@ Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des J export const POST: APIRoute = async ({ request }) => { try { - // Check if authentication is required - const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false'; - let userId = 'test-user'; - - if (authRequired) { - // Authentication check - const sessionToken = getSessionFromRequest(request); - if (!sessionToken) { - return new Response(JSON.stringify({ error: 'Authentication required' }), { - status: 401, - headers: { 'Content-Type': 'application/json' } - }); - } - - const session = await verifySession(sessionToken); - if (!session) { - return new Response(JSON.stringify({ error: 'Invalid session' }), { - status: 401, - headers: { 'Content-Type': 'application/json' } - }); - } - - userId = session.userId; + // CONSOLIDATED: Replace 20+ lines with single function call + const authResult = await withAPIAuth(request); + if (!authResult.authenticated) { + return createAuthErrorResponse(); } + + const userId = authResult.userId; // Rate limiting if (!checkRateLimit(userId)) { diff --git a/src/pages/contribute/index.astro b/src/pages/contribute/index.astro index 66660c9..d939d8f 100644 --- a/src/pages/contribute/index.astro +++ b/src/pages/contribute/index.astro @@ -1,30 +1,17 @@ --- -// src/pages/contribute/index.astro - Updated for Phase 3 +// src/pages/contribute/index.astro - Consolidated Auth import BaseLayout from '../../layouts/BaseLayout.astro'; -import { getSessionFromRequest, verifySession } from '../../utils/auth.js'; -import { getAuthContext, requireAuth } from '../../utils/serverAuth.js'; +import { withAuth } from '../../utils/auth.js'; // Note: .js extension! export const prerender = false; -// Check authentication -const authRequired = import.meta.env.AUTHENTICATION_NECESSARY !== 'false'; -let isAuthenticated = false; -let userEmail = ''; - -if (authRequired) { - const sessionToken = getSessionFromRequest(Astro.request); - if (sessionToken) { - const session = await verifySession(sessionToken); - if (session) { - isAuthenticated = true; - userEmail = session.email; - } - } - - const authContext = await getAuthContext(Astro); - const authRedirect = requireAuth(authContext, Astro.url.toString()); - if (authRedirect) return authRedirect; +// CONSOLIDATED: Replace 15+ lines with single function call +const authResult = await withAuth(Astro); +if (authResult instanceof Response) { + return authResult; // Redirect to login } + +const { authenticated, userEmail, userId } = authResult; --- diff --git a/src/pages/contribute/knowledgebase.astro b/src/pages/contribute/knowledgebase.astro index 6dce617..6f54069 100644 --- a/src/pages/contribute/knowledgebase.astro +++ b/src/pages/contribute/knowledgebase.astro @@ -1,32 +1,19 @@ --- // src/pages/contribute/knowledgebase.astro import BaseLayout from '../../layouts/BaseLayout.astro'; -import { getSessionFromRequest, verifySession } from '../../utils/auth.js'; -import { getAuthContext, requireAuth } from '../../utils/serverAuth.js'; +import { withAuth } from '../../utils/auth.js'; import { getToolsData } from '../../utils/dataService.js'; export const prerender = false; // Check authentication -const authRequired = import.meta.env.AUTHENTICATION_NECESSARY !== 'false'; -let isAuthenticated = false; -let userEmail = ''; - -if (authRequired) { - const sessionToken = getSessionFromRequest(Astro.request); - if (sessionToken) { - const session = await verifySession(sessionToken); - if (session) { - isAuthenticated = true; - userEmail = session.email; - } - } - - const authContext = await getAuthContext(Astro); - const authRedirect = requireAuth(authContext, Astro.url.toString()); - if (authRedirect) return authRedirect; +const authResult = await withAuth(Astro); +if (authResult instanceof Response) { + return authResult; // Redirect to login } +const { authenticated, userEmail, userId } = authResult; + const data = await getToolsData(); const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.name)); --- diff --git a/src/pages/contribute/tool.astro b/src/pages/contribute/tool.astro index de7a0e4..f30d960 100644 --- a/src/pages/contribute/tool.astro +++ b/src/pages/contribute/tool.astro @@ -1,13 +1,16 @@ --- // src/pages/contribute/tool.astro import BaseLayout from '../../layouts/BaseLayout.astro'; -import { getAuthContext, requireAuth } from '../../utils/serverAuth.js'; +import { withAuth } from '../../utils/auth.js'; import { getToolsData } from '../../utils/dataService.js'; // Check authentication -const authContext = await getAuthContext(Astro); -const authRedirect = requireAuth(authContext, Astro.url.toString()); -if (authRedirect) return authRedirect; +const authResult = await withAuth(Astro); +if (authResult instanceof Response) { + return authResult; // Redirect to login +} + +const { authenticated, userEmail, userId } = authResult; // Load existing data for validation and editing const data = await getToolsData(); diff --git a/src/pages/index.astro b/src/pages/index.astro index 13da461..aefef3c 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -119,7 +119,8 @@ const tools = data.tools; navigateToMatrix: (toolName: string) => void; } } - + + import { requireClientAuth } from '../utils/auth.js'; // Handle view changes and filtering document.addEventListener('DOMContentLoaded', () => { const toolsContainer = document.getElementById('tools-container') as HTMLElement; @@ -177,17 +178,11 @@ const tools = data.tools; } } + // AI Query Button Handler if (aiQueryBtn) { aiQueryBtn.addEventListener('click', async () => { - const authStatus = await checkAuthentication(); - - if (authStatus.authRequired && !authStatus.authenticated) { - const returnUrl = `${window.location.pathname}?view=ai`; - window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(returnUrl)}`; - } else { - switchToView('ai'); - } + await requireClientAuth(() => switchToView('ai'), `${window.location.pathname}?view=ai`); }); } diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 5fd5f74..43d18d5 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -2,6 +2,8 @@ import { SignJWT, jwtVerify, type JWTPayload } from 'jose'; import { serialize, parse } from 'cookie'; import { config } from 'dotenv'; +import type { AstroGlobal, APIRoute } from 'astro'; + // Load environment variables config(); @@ -196,4 +198,144 @@ export function logAuthEvent(event: string, details?: any) { export function getUserEmail(userInfo: UserInfo): string { return userInfo.email || `${userInfo.preferred_username || userInfo.sub || 'unknown'}@cc24.dev`; +} + +// === CONSOLIDATION: Server-side Auth Helpers === + +export interface AuthContext { + authenticated: boolean; + session: SessionData | null; + userEmail: string; + userId: string; +} + +/** + * Consolidated auth check for Astro pages + * Replaces repeated auth patterns in contribute pages + */ +export async function withAuth(Astro: AstroGlobal): Promise { + const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false'; + + // If auth not required, return mock context + if (!authRequired) { + return { + authenticated: true, + session: null, + userEmail: 'anonymous@example.com', + userId: 'anonymous' + }; + } + + // Check session + const sessionToken = getSessionFromRequest(Astro.request); + if (!sessionToken) { + const loginUrl = `/api/auth/login?returnTo=${encodeURIComponent(Astro.url.toString())}`; + return new Response(null, { + status: 302, + headers: { 'Location': loginUrl } + }); + } + + const session = await verifySession(sessionToken); + if (!session) { + const loginUrl = `/api/auth/login?returnTo=${encodeURIComponent(Astro.url.toString())}`; + return new Response(null, { + status: 302, + headers: { 'Location': loginUrl } + }); + } + + return { + authenticated: true, + session, + userEmail: session.email, + userId: session.userId + }; +} + +/** + * Consolidated auth check for API endpoints + * Replaces repeated auth patterns in API routes + */ +export async function withAPIAuth(request: Request): Promise<{ authenticated: boolean; userId: string; session?: SessionData }> { + const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false'; + + if (!authRequired) { + return { + authenticated: true, + userId: 'anonymous' + }; + } + + const sessionToken = getSessionFromRequest(request); + if (!sessionToken) { + return { authenticated: false, userId: '' }; + } + + const session = await verifySession(sessionToken); + if (!session) { + return { authenticated: false, userId: '' }; + } + + return { + authenticated: true, + userId: session.userId, + session + }; +} + +/** + * 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' } + }); +} + +async function checkClientAuth() { + try { + const response = await fetch('/api/auth/status'); + const data = await response.json(); + return { + authenticated: data.authenticated, + authRequired: data.authRequired, + expires: data.expires + }; + } catch (error) { + console.error('Auth check failed:', error); + return { + authenticated: false, + authRequired: true + }; + } +} + +/** + * Redirect to login if not authenticated, otherwise execute callback + */ +export async function requireClientAuth(callback, returnUrl) { + const authStatus = await checkClientAuth(); + + if (authStatus.authRequired && !authStatus.authenticated) { + const targetUrl = returnUrl || window.location.href; + window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(targetUrl)}`; + } else { + callback(); + } +} + +/** + * Show/hide element based on authentication + */ +export async function showIfAuthenticated(selector) { + const authStatus = await checkClientAuth(); + const element = document.querySelector(selector); + + if (element) { + element.style.display = (!authStatus.authRequired || authStatus.authenticated) + ? 'inline-flex' + : 'none'; + } } \ No newline at end of file diff --git a/src/utils/client-auth.ts b/src/utils/client-auth.ts new file mode 100644 index 0000000..143b0bd --- /dev/null +++ b/src/utils/client-auth.ts @@ -0,0 +1,55 @@ +// src/scripts/client-auth.js - Client-side auth utilities + +/** + * Consolidated client-side auth status check + */ +async function checkClientAuth() { + try { + const response = await fetch('/api/auth/status'); + const data = await response.json(); + return { + authenticated: data.authenticated, + authRequired: data.authRequired, + expires: data.expires + }; + } catch (error) { + console.error('Auth check failed:', error); + return { + authenticated: false, + authRequired: true + }; + } +} + +/** + * Redirect to login if not authenticated, otherwise execute callback + */ +async function requireClientAuth(callback, returnUrl) { + const authStatus = await checkClientAuth(); + + if (authStatus.authRequired && !authStatus.authenticated) { + const targetUrl = returnUrl || window.location.href; + window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(targetUrl)}`; + } else { + callback(); + } +} + +/** + * Show/hide element based on authentication + */ +async function showIfAuthenticated(selector) { + const authStatus = await checkClientAuth(); + const element = document.querySelector(selector); + + if (element) { + element.style.display = (!authStatus.authRequired || authStatus.authenticated) + ? 'inline-flex' + : 'none'; + } +} + +// Make functions available globally +window.checkClientAuth = checkClientAuth; +window.requireClientAuth = requireClientAuth; +window.showIfAuthenticated = showIfAuthenticated; \ No newline at end of file diff --git a/src/utils/serverAuth.ts b/src/utils/serverAuth.ts deleted file mode 100644 index 09ce7d4..0000000 --- a/src/utils/serverAuth.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { AstroGlobal } from 'astro'; -import { getSessionFromRequest, verifySession, type SessionData } from './auth.js'; - -export interface AuthContext { - authenticated: boolean; - session: SessionData | null; -} - -// Check authentication status for server-side pages -export async function getAuthContext(Astro: AstroGlobal): Promise { - try { - const sessionToken = getSessionFromRequest(Astro.request); - - if (!sessionToken) { - return { authenticated: false, session: null }; - } - - const session = await verifySession(sessionToken); - - return { - authenticated: session !== null, - session - }; - } catch (error) { - console.error('Failed to get auth context:', error); - return { authenticated: false, session: null }; - } -} - -// Redirect to login if not authenticated -export function requireAuth(authContext: AuthContext, currentUrl: string): Response | null { - if (!authContext.authenticated) { - const loginUrl = `/api/auth/login?returnTo=${encodeURIComponent(currentUrl)}`; - return new Response(null, { - status: 302, - headers: { 'Location': loginUrl } - }); - } - return null; -} \ No newline at end of file