consolidation of auth mechanism

This commit is contained in:
overcuriousity 2025-07-24 12:30:59 +02:00
parent 32fca8a06f
commit 72bcc04309
5 changed files with 107 additions and 176 deletions

View File

@ -19,7 +19,13 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
<meta name="description" content={description}> <meta name="description" content={description}>
<title>{title} - CC24-Guide</title> <title>{title} - CC24-Guide</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico"> <link rel="icon" type="image/x-icon" href="/favicon.ico">
<!-- CONSOLIDATED: Load theme script -->
<script src="/src/scripts/theme.js"></script> <script src="/src/scripts/theme.js"></script>
<!-- CONSOLIDATED: Load client-side auth utilities -->
<script src="/src/scripts/client-auth.js"></script>
<script> <script>
// Initialize theme immediately to prevent flash // Initialize theme immediately to prevent flash
(window as any).themeUtils?.initTheme(); (window as any).themeUtils?.initTheme();

View File

@ -6,7 +6,6 @@ import ToolMatrix from '../components/ToolMatrix.astro';
import AIQueryInterface from '../components/AIQueryInterface.astro'; import AIQueryInterface from '../components/AIQueryInterface.astro';
import { getToolsData } from '../utils/dataService.js'; import { getToolsData } from '../utils/dataService.js';
// Load tools data // Load tools data
const data = await getToolsData(); const data = await getToolsData();
const tools = data.tools; const tools = data.tools;
@ -53,8 +52,8 @@ const tools = data.tools;
KI befragen KI befragen
</button> </button>
<!-- NEW: Contribution Button --> <!-- Contribution Button - FIXED: Use data-contribute-button -->
<a href="/contribute" class="btn" style="padding: 0.75rem 1.5rem; background-color: var(--color-warning); color: white; border-color: var(--color-warning);" data-contribute-button="new"> <a href="/contribute" class="btn" style="padding: 0.75rem 1.5rem; background-color: var(--color-warning); color: white; border-color: var(--color-warning);" data-contribute-button>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/> <path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="8.5" cy="7" r="4"/> <circle cx="8.5" cy="7" r="4"/>
@ -78,13 +77,12 @@ const tools = data.tools;
<!-- Filters Section --> <!-- Filters Section -->
<section id="filters-section" style="padding: 2rem 0;"> <section id="filters-section" style="padding: 2rem 0;">
<ToolFilters /> <ToolFilters data={data} />
</section> </section>
<!-- AI Query Interface --> <!-- AI Query Interface -->
<AIQueryInterface /> <AIQueryInterface />
<!-- Tools Grid --> <!-- Tools Grid -->
<section id="tools-grid" style="padding-bottom: 2rem;"> <section id="tools-grid" style="padding-bottom: 2rem;">
<div class="grid-auto-fit" id="tools-container"> <div class="grid-auto-fit" id="tools-container">
@ -100,36 +98,22 @@ const tools = data.tools;
</section> </section>
<!-- Matrix View --> <!-- Matrix View -->
<ToolMatrix /> <ToolMatrix data={data} />
</BaseLayout> </BaseLayout>
<script> <script define:vars={{ toolsData: data.tools }}>
// Extend Window interface for custom properties // Store tools data globally
declare global { window.toolsData = toolsData;
interface Window {
toolsData: any[];
showToolDetails: (toolName: string, modalType?: string) => void;
hideToolDetails: (modalType?: string) => void;
hideAllToolDetails: () => void;
clearAllFilters?: () => void;
restoreAIResults?: () => void;
switchToAIView?: () => void;
showShareDialog: (shareButton: HTMLElement) => void;
navigateToGrid: (toolName: string) => void;
navigateToMatrix: (toolName: string) => void;
}
}
import { requireClientAuth } from '../utils/auth.js';
// Handle view changes and filtering // Handle view changes and filtering
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const toolsContainer = document.getElementById('tools-container') as HTMLElement; const toolsContainer = document.getElementById('tools-container');
const toolsGrid = document.getElementById('tools-grid') as HTMLElement; const toolsGrid = document.getElementById('tools-grid');
const matrixContainer = document.getElementById('matrix-container') as HTMLElement; const matrixContainer = document.getElementById('matrix-container');
const aiInterface = document.getElementById('ai-interface') as HTMLElement; const aiInterface = document.getElementById('ai-interface');
const filtersSection = document.getElementById('filters-section') as HTMLElement; const filtersSection = document.getElementById('filters-section');
const noResults = document.getElementById('no-results') as HTMLElement; const noResults = document.getElementById('no-results');
const aiQueryBtn = document.getElementById('ai-query-btn') as HTMLButtonElement; const aiQueryBtn = document.getElementById('ai-query-btn');
// Guard against null elements // Guard against null elements
if (!toolsContainer || !toolsGrid || !matrixContainer || !noResults || !aiInterface || !filtersSection) { if (!toolsContainer || !toolsGrid || !matrixContainer || !noResults || !aiInterface || !filtersSection) {
@ -138,7 +122,7 @@ const tools = data.tools;
} }
// Simple sorting function // Simple sorting function
function sortTools(tools: any[], sortBy = 'default') { function sortTools(tools, sortBy = 'default') {
const sorted = [...tools]; const sorted = [...tools];
switch (sortBy) { switch (sortBy) {
@ -147,12 +131,12 @@ const tools = data.tools;
case 'difficulty': case 'difficulty':
const difficultyOrder = { 'novice': 0, 'beginner': 1, 'intermediate': 2, 'advanced': 3, 'expert': 4 }; const difficultyOrder = { 'novice': 0, 'beginner': 1, 'intermediate': 2, 'advanced': 3, 'expert': 4 };
return sorted.sort((a, b) => return sorted.sort((a, b) =>
(difficultyOrder[a.skillLevel as keyof typeof difficultyOrder] || 999) - (difficultyOrder[b.skillLevel as keyof typeof difficultyOrder] || 999) (difficultyOrder[a.skillLevel] || 999) - (difficultyOrder[b.skillLevel] || 999)
); );
case 'type': case 'type':
const typeOrder = { 'concept': 0, 'method': 1, 'software': 2 }; const typeOrder = { 'concept': 0, 'method': 1, 'software': 2 };
return sorted.sort((a, b) => return sorted.sort((a, b) =>
(typeOrder[a.type as keyof typeof typeOrder] || 999) - (typeOrder[b.type as keyof typeof typeOrder] || 999) (typeOrder[a.type] || 999) - (typeOrder[b.type] || 999)
); );
case 'default': case 'default':
default: default:
@ -160,34 +144,22 @@ const tools = data.tools;
} }
} }
// Authentication check function // FIXED: AI Query Button Handler using global client-side auth function
async function checkAuthentication() {
try {
const response = await fetch('/api/auth/status');
const data = await response.json();
return {
authenticated: data.authenticated,
authRequired: data.authRequired
};
} catch (error) {
console.error('Auth check failed:', error);
return {
authenticated: false,
authRequired: true
};
}
}
// AI Query Button Handler
if (aiQueryBtn) { if (aiQueryBtn) {
aiQueryBtn.addEventListener('click', async () => { aiQueryBtn.addEventListener('click', async () => {
await requireClientAuth(() => switchToView('ai'), `${window.location.pathname}?view=ai`); // Wait for client-side auth functions to be available
if (typeof window.requireClientAuth === 'function') {
await window.requireClientAuth(() => switchToView('ai'), `${window.location.pathname}?view=ai`);
} else {
console.error('requireClientAuth not available - client-auth.js may not be loaded');
// Fallback - try switching anyway
switchToView('ai');
}
}); });
} }
// Function to switch between different views // Function to switch between different views
function switchToView(view: string) { function switchToView(view) {
// Hide all views first // Hide all views first
toolsGrid.style.display = 'none'; toolsGrid.style.display = 'none';
matrixContainer.style.display = 'none'; matrixContainer.style.display = 'none';
@ -209,7 +181,7 @@ const tools = data.tools;
if (window.restoreAIResults) { if (window.restoreAIResults) {
window.restoreAIResults(); window.restoreAIResults();
} }
const aiInput = document.getElementById('ai-query-input') as HTMLTextAreaElement; const aiInput = document.getElementById('ai-query-input');
if (aiInput) { if (aiInput) {
setTimeout(() => aiInput.focus(), 100); setTimeout(() => aiInput.focus(), 100);
} }
@ -243,19 +215,19 @@ const tools = data.tools;
]; ];
elements.forEach(selector => { elements.forEach(selector => {
const element = document.querySelector(selector) as HTMLElement; const element = document.querySelector(selector);
if (element) element.style.display = 'none'; if (element) element.style.display = 'none';
}); });
const allInputs = filtersSection.querySelectorAll('input, select, textarea'); const allInputs = filtersSection.querySelectorAll('input, select, textarea');
allInputs.forEach(input => (input as HTMLElement).style.display = 'none'); allInputs.forEach(input => input.style.display = 'none');
} }
function showFilterControls() { function showFilterControls() {
const domainPhaseContainer = document.querySelector('.domain-phase-container') as HTMLElement; const domainPhaseContainer = document.querySelector('.domain-phase-container');
const searchInput = document.getElementById('search-input') as HTMLElement; const searchInput = document.getElementById('search-input');
const tagCloud = document.querySelector('.tag-cloud') as HTMLElement; const tagCloud = document.querySelector('.tag-cloud');
const tagHeader = document.querySelector('.tag-header') as HTMLElement; const tagHeader = document.querySelector('.tag-header');
const checkboxWrappers = document.querySelectorAll('.checkbox-wrapper'); const checkboxWrappers = document.querySelectorAll('.checkbox-wrapper');
const allInputs = filtersSection.querySelectorAll('input, select, textarea'); const allInputs = filtersSection.querySelectorAll('input, select, textarea');
@ -264,12 +236,12 @@ const tools = data.tools;
if (tagCloud) tagCloud.style.display = 'flex'; if (tagCloud) tagCloud.style.display = 'flex';
if (tagHeader) tagHeader.style.display = 'flex'; if (tagHeader) tagHeader.style.display = 'flex';
allInputs.forEach(input => (input as HTMLElement).style.display = 'block'); allInputs.forEach(input => input.style.display = 'block');
checkboxWrappers.forEach(wrapper => (wrapper as HTMLElement).style.display = 'flex'); checkboxWrappers.forEach(wrapper => wrapper.style.display = 'flex');
} }
// Create tool slug from name // Create tool slug from name
function createToolSlug(toolName: string): string { function createToolSlug(toolName) {
return toolName.toLowerCase() return toolName.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '') .replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-') .replace(/\s+/g, '-')
@ -278,15 +250,15 @@ const tools = data.tools;
} }
// Find tool by name or slug // Find tool by name or slug
function findTool(identifier: string) { function findTool(identifier) {
return window.toolsData.find(tool => return window.toolsData.find(tool =>
tool.name === identifier || tool.name === identifier ||
createToolSlug(tool.name) === identifier.toLowerCase() createToolSlug(tool.name) === identifier.toLowerCase()
); );
} }
// Navigation functions for sharing // RESTORED: Navigation functions for sharing - EXACT ORIGINAL VERSIONS
window.navigateToGrid = function(toolName: string) { window.navigateToGrid = function(toolName) {
console.log('Navigating to grid for tool:', toolName); console.log('Navigating to grid for tool:', toolName);
// Switch to grid view first // Switch to grid view first
@ -302,7 +274,7 @@ const tools = data.tools;
// Wait for filters to clear and re-render // Wait for filters to clear and re-render
setTimeout(() => { setTimeout(() => {
const toolCards = document.querySelectorAll('.tool-card'); const toolCards = document.querySelectorAll('.tool-card');
let targetCard: Element | null = null; let targetCard = null;
toolCards.forEach(card => { toolCards.forEach(card => {
const cardTitle = card.querySelector('h3'); const cardTitle = card.querySelector('h3');
@ -317,13 +289,12 @@ const tools = data.tools;
if (targetCard) { if (targetCard) {
console.log('Found target card, scrolling...'); console.log('Found target card, scrolling...');
// Cast to Element to fix TypeScript issue targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
(targetCard as Element).scrollIntoView({ behavior: 'smooth', block: 'center' }); targetCard.style.animation = 'highlight-flash 2s ease-out';
(targetCard as HTMLElement).style.animation = 'highlight-flash 2s ease-out';
setTimeout(() => { setTimeout(() => {
if (targetCard) { if (targetCard) {
(targetCard as HTMLElement).style.animation = ''; targetCard.style.animation = '';
} }
}, 2000); }, 2000);
} else { } else {
@ -333,7 +304,7 @@ const tools = data.tools;
}, 200); }, 200);
}; };
window.navigateToMatrix = function(toolName: string) { window.navigateToMatrix = function(toolName) {
console.log('Navigating to matrix for tool:', toolName); console.log('Navigating to matrix for tool:', toolName);
// Switch to matrix view // Switch to matrix view
@ -342,7 +313,7 @@ const tools = data.tools;
// Wait for view switch and matrix to render // Wait for view switch and matrix to render
setTimeout(() => { setTimeout(() => {
const toolChips = document.querySelectorAll('.tool-chip'); const toolChips = document.querySelectorAll('.tool-chip');
let firstMatch: Element | null = null; let firstMatch = null;
let matchCount = 0; let matchCount = 0;
toolChips.forEach(chip => { toolChips.forEach(chip => {
@ -350,7 +321,7 @@ const tools = data.tools;
const chipText = chip.textContent?.replace(/📖/g, '').replace(/[^\w\s\-\.]/g, '').trim(); const chipText = chip.textContent?.replace(/📖/g, '').replace(/[^\w\s\-\.]/g, '').trim();
if (chipText === toolName) { if (chipText === toolName) {
// Highlight this occurrence // Highlight this occurrence
(chip as HTMLElement).style.animation = 'highlight-flash 2s ease-out'; chip.style.animation = 'highlight-flash 2s ease-out';
matchCount++; matchCount++;
// Remember the first match for scrolling // Remember the first match for scrolling
@ -360,22 +331,21 @@ const tools = data.tools;
// Clean up animation after it completes // Clean up animation after it completes
setTimeout(() => { setTimeout(() => {
(chip as HTMLElement).style.animation = ''; chip.style.animation = '';
}, 8000); }, 8000);
} }
}); });
if (firstMatch) { if (firstMatch) {
console.log(`Found ${matchCount} occurrences of tool, highlighting all and scrolling to first`); console.log(`Found ${matchCount} occurrences of tool, highlighting all and scrolling to first`);
// Cast to Element to fix TypeScript issue firstMatch.scrollIntoView({ behavior: 'smooth', block: 'center' });
(firstMatch as Element).scrollIntoView({ behavior: 'smooth', block: 'center' });
} else { } else {
console.warn('Tool chip not found in matrix:', toolName); console.warn('Tool chip not found in matrix:', toolName);
} }
}, 500); }, 500);
}; };
// Handle URL parameters on page load // RESTORED: Handle URL parameters on page load - EXACT ORIGINAL VERSION
function handleSharedURL() { function handleSharedURL() {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const toolParam = urlParams.get('tool'); const toolParam = urlParams.get('tool');
@ -425,8 +395,7 @@ const tools = data.tools;
// Handle filtered results // Handle filtered results
window.addEventListener('toolsFiltered', (event) => { window.addEventListener('toolsFiltered', (event) => {
const customEvent = event as CustomEvent; const filtered = event.detail;
const filtered = customEvent.detail;
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view'); const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
if (currentView === 'matrix' || currentView === 'ai') { if (currentView === 'matrix' || currentView === 'ai') {
@ -443,7 +412,7 @@ const tools = data.tools;
const sortedTools = sortTools(filtered, 'default'); const sortedTools = sortTools(filtered, 'default');
sortedTools.forEach((tool: any) => { sortedTools.forEach((tool) => {
const toolCard = createToolCard(tool); const toolCard = createToolCard(tool);
toolsContainer.appendChild(toolCard); toolsContainer.appendChild(toolCard);
}); });
@ -452,8 +421,7 @@ const tools = data.tools;
// Handle view changes // Handle view changes
window.addEventListener('viewChanged', (event) => { window.addEventListener('viewChanged', (event) => {
const customEvent = event as CustomEvent; const view = event.detail;
const view = customEvent.detail;
switchToView(view); switchToView(view);
}); });
@ -461,7 +429,7 @@ const tools = data.tools;
window.switchToAIView = () => switchToView('ai'); window.switchToAIView = () => switchToView('ai');
// Tool card creation function // Tool card creation function
function createToolCard(tool: any): HTMLElement { function createToolCard(tool) {
const isMethod = tool.type === 'method'; const isMethod = tool.type === 'method';
const isConcept = tool.type === 'concept'; const isConcept = tool.type === 'concept';
const hasValidProjectUrl = tool.projectUrl !== undefined && const hasValidProjectUrl = tool.projectUrl !== undefined &&
@ -545,7 +513,7 @@ const tools = data.tools;
</div> </div>
<div class="tool-tags-container"> <div class="tool-tags-container">
${(tool.tags || []).slice(0, 8).map((tag: string) => `<span class="tag">${tag}</span>`).join('')} ${(tool.tags || []).slice(0, 8).map((tag) => `<span class="tag">${tag}</span>`).join('')}
</div> </div>
<div class="tool-card-buttons" onclick="event.stopPropagation();"> <div class="tool-card-buttons" onclick="event.stopPropagation();">
@ -577,7 +545,7 @@ const tools = data.tools;
return cardDiv; return cardDiv;
} }
// Initialize URL handling // RESTORED: Initialize URL handling - EXACT ORIGINAL
handleSharedURL(); handleSharedURL();
}); });
</script> </script>

View File

@ -1,30 +0,0 @@
// src/scripts/auth-utils.js
export async function checkAuthAndRedirect(targetUrl) {
try {
const response = await fetch('/api/auth/status');
const data = await response.json();
if (data.authRequired && !data.authenticated) {
const returnUrl = encodeURIComponent(targetUrl);
window.location.href = `/api/auth/login?returnTo=${returnUrl}`;
return false;
} else {
window.location.href = targetUrl;
return true;
}
} catch (error) {
console.error('Auth check failed:', error);
window.location.href = targetUrl; // Fallback
return true;
}
}
export function setupAuthButtons(selector = '[data-contribute-button]') {
document.addEventListener('click', async (e) => {
const button = e.target.closest(selector);
if (!button) return;
e.preventDefault();
await checkAuthAndRedirect(button.href);
});
}

View File

@ -1,7 +1,8 @@
// src/scripts/client-auth.js - Client-side auth utilities // src/scripts/client-auth.js - CONSOLIDATED client-side auth utilities
// This file REPLACES auth-utils.js and any client-side auth functions
/** /**
* Consolidated client-side auth status check * Check authentication status
*/ */
async function checkClientAuth() { async function checkClientAuth() {
try { try {
@ -30,8 +31,12 @@ async function requireClientAuth(callback, returnUrl) {
if (authStatus.authRequired && !authStatus.authenticated) { if (authStatus.authRequired && !authStatus.authenticated) {
const targetUrl = returnUrl || window.location.href; const targetUrl = returnUrl || window.location.href;
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(targetUrl)}`; window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(targetUrl)}`;
return false;
} else { } else {
callback(); if (typeof callback === 'function') {
callback();
}
return true;
} }
} }
@ -49,7 +54,30 @@ async function showIfAuthenticated(selector) {
} }
} }
/**
* Handle contribute button clicks with auth check
*/
function setupAuthButtons(selector = '[data-contribute-button]') {
document.addEventListener('click', async (e) => {
const button = e.target.closest(selector);
if (!button) return;
e.preventDefault();
await requireClientAuth(() => {
window.location.href = button.href;
}, button.href);
});
}
// Make functions available globally // Make functions available globally
window.checkClientAuth = checkClientAuth; window.checkClientAuth = checkClientAuth;
window.requireClientAuth = requireClientAuth; window.requireClientAuth = requireClientAuth;
window.showIfAuthenticated = showIfAuthenticated; window.showIfAuthenticated = showIfAuthenticated;
window.setupAuthButtons = setupAuthButtons;
// Auto-setup contribute buttons when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
setupAuthButtons('[data-contribute-button]');
});
console.log('Client auth utilities loaded');

View File

@ -1,9 +1,8 @@
// src/utils/auth.ts - Enhanced with Email Support // src/utils/auth.ts - SERVER-SIDE ONLY (remove client-side functions)
import { SignJWT, jwtVerify, type JWTPayload } from 'jose'; import { SignJWT, jwtVerify, type JWTPayload } from 'jose';
import { serialize, parse } from 'cookie'; import { serialize, parse } from 'cookie';
import { config } from 'dotenv'; import { config } from 'dotenv';
import type { AstroGlobal, APIRoute } from 'astro'; import type { AstroGlobal } from 'astro';
// Load environment variables // Load environment variables
config(); config();
@ -210,8 +209,9 @@ export interface AuthContext {
} }
/** /**
* Consolidated auth check for Astro pages * CONSOLIDATED: Replace repeated auth patterns in .astro pages
* Replaces repeated auth patterns in contribute pages * Usage: const authResult = await withAuth(Astro);
* if (authResult instanceof Response) return authResult;
*/ */
export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Response> { export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Response> {
const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false'; const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
@ -254,10 +254,15 @@ export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Respon
} }
/** /**
* Consolidated auth check for API endpoints * CONSOLIDATED: Replace repeated auth patterns in API endpoints
* Replaces repeated auth patterns in API routes * Usage: const authResult = await withAPIAuth(request);
* if (!authResult.authenticated) return createAuthErrorResponse();
*/ */
export async function withAPIAuth(request: Request): Promise<{ authenticated: boolean; userId: string; session?: SessionData }> { export async function withAPIAuth(request: Request): Promise<{
authenticated: boolean;
userId: string;
session?: SessionData
}> {
const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false'; const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
if (!authRequired) { if (!authRequired) {
@ -293,49 +298,3 @@ export function createAuthErrorResponse(message: string = 'Authentication requir
headers: { 'Content-Type': 'application/json' } 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';
}
}