auth splitting

This commit is contained in:
overcuriousity 2025-07-26 14:07:18 +02:00
parent a4f4e03cba
commit d2fdeccce3
13 changed files with 87 additions and 69 deletions

View File

@ -7,7 +7,10 @@ AUTH_SECRET=change-this-to-a-strong-secret-key-in-production
OIDC_ENDPOINT=https://your-oidc-provider.com
OIDC_CLIENT_ID=your-oidc-client-id
OIDC_CLIENT_SECRET=your-oidc-client-secret
AUTHENTICATION_NECESSARY=true
# Auth Scopes - set to true in prod
AUTHENTICATION_NECESSARY_CONTRIBUTIONS=true
AUTHENTICATION_NECESSARY_AI=true
# Application Configuration (Required)
PUBLIC_BASE_URL=https://your-domain.com

View File

@ -64,7 +64,7 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
Ihre Anfrage wird an mistral.ai übertragen und unterliegt deren
Ihre Anfrage wird über die kostenlose API von mistral.ai übertragen, wird für KI-Training verwendet und unterliegt deren
<a href="https://mistral.ai/privacy-policy/" target="_blank" rel="noopener noreferrer" style="color: var(--color-primary); text-decoration: underline;">Datenschutzrichtlinien</a>
</p>
</div>

View File

@ -112,15 +112,31 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
(window as any).isToolHosted = isToolHosted;
// Client-side auth functions (consolidated from client-auth.js)
async function checkClientAuth() {
async function checkClientAuth(context = 'general') {
try {
const response = await fetch('/api/auth/status');
const data = await response.json();
return {
authenticated: data.authenticated,
authRequired: data.authRequired,
expires: data.expires
};
switch (context) {
case 'contributions':
return {
authenticated: data.contributionAuthenticated,
authRequired: data.contributionAuthRequired,
expires: data.expires
};
case 'ai':
return {
authenticated: data.aiAuthenticated,
authRequired: data.aiAuthRequired,
expires: data.expires
};
default:
return {
authenticated: data.authenticated,
authRequired: data.contributionAuthRequired || data.aiAuthRequired,
expires: data.expires
};
}
} catch (error) {
console.error('Auth check failed:', error);
return {
@ -130,8 +146,8 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
}
}
async function requireClientAuth(callback, returnUrl) {
const authStatus = await checkClientAuth();
async function requireClientAuth(callback, returnUrl, context = 'general') {
const authStatus = await checkClientAuth(context);
if (authStatus.authRequired && !authStatus.authenticated) {
const targetUrl = returnUrl || window.location.href;
@ -145,8 +161,8 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
}
}
async function showIfAuthenticated(selector) {
const authStatus = await checkClientAuth();
async function showIfAuthenticated(selector, context = 'general') {
const authStatus = await checkClientAuth(context);
const element = document.querySelector(selector);
if (element) {
@ -157,11 +173,9 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
}
function setupAuthButtons(selector = '[data-contribute-button]') {
// Use event delegation on document for dynamic content support
document.addEventListener('click', async (e) => {
if (!e.target) return;
// FIXED: Properly cast EventTarget to Element for closest() method
const button = (e.target as Element).closest(selector);
if (!button) return;
@ -169,10 +183,11 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
console.log('[AUTH] Contribute button clicked:', button.getAttribute('data-contribute-button'));
// ENHANCED: Use contributions context
await requireClientAuth(() => {
console.log('[AUTH] Navigation approved, redirecting to:', (button as HTMLAnchorElement).href);
window.location.href = (button as HTMLAnchorElement).href;
}, (button as HTMLAnchorElement).href);
}, (button as HTMLAnchorElement).href, 'contributions');
});
}
@ -187,7 +202,7 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
setupAuthButtons('[data-contribute-button]');
const initAIButton = async () => {
await showIfAuthenticated('#ai-view-toggle');
await showIfAuthenticated('#ai-view-toggle', 'ai');
};
initAIButton();

View File

@ -278,7 +278,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 (UNCHANGED)
const authResult = await withAPIAuth(request);
const authResult = await withAPIAuth(request, 'ai');
if (!authResult.authenticated) {
return createAuthErrorResponse();
}

View File

@ -1,4 +1,4 @@
// src/pages/api/auth/status.ts (FIXED - Updated imports and consolidated)
// src/pages/api/auth/status.ts
import type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js';
import { apiResponse, handleAPIRequest } from '../../../utils/api.js';
@ -7,14 +7,16 @@ export const prerender = false;
export const GET: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => {
// CONSOLIDATED: Single function call replaces 35+ lines
const authResult = await withAPIAuth(request);
const contributionAuth = await withAPIAuth(request, 'contributions');
const aiAuth = await withAPIAuth(request, 'ai');
return apiResponse.success({
authenticated: authResult.authenticated,
authRequired: authResult.authRequired,
expires: authResult.session?.exp ? new Date(authResult.session.exp * 1000).toISOString() : null
authenticated: contributionAuth.authenticated || aiAuth.authenticated,
contributionAuthRequired: contributionAuth.authRequired,
aiAuthRequired: aiAuth.authRequired,
contributionAuthenticated: contributionAuth.authenticated,
aiAuthenticated: aiAuth.authenticated,
expires: contributionAuth.session?.exp ? new Date(contributionAuth.session.exp * 1000).toISOString() : null
});
}, 'Status check failed');
};

View File

@ -84,12 +84,12 @@ function validateKnowledgebaseData(data: KnowledgebaseContributionData): { valid
export const POST: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => {
// Check authentication
const authResult = await withAPIAuth(request);
const authResult = await withAPIAuth(request, 'contributions');
if (authResult.authRequired && !authResult.authenticated) {
return apiError.unauthorized();
}
const userEmail = authResult.session?.email || 'anonymous@example.com';
const userEmail = authResult.session?.email || 'anon@anon.anon';
// Rate limiting
if (!checkRateLimit(userEmail)) {

View File

@ -127,13 +127,13 @@ async function validateToolData(tool: any, action: string): Promise<{ valid: boo
export const POST: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => {
// Authentication check
const authResult = await withAPIAuth(request);
const authResult = await withAPIAuth(request, 'contributions');
if (authResult.authRequired && !authResult.authenticated) {
return apiError.unauthorized();
}
const userId = authResult.session?.userId || 'anonymous';
const userEmail = authResult.session?.email || 'anonymous@example.com';
const userEmail = authResult.session?.email || 'anon@anon.anon';
// Rate limiting
if (!checkRateLimit(userId)) {

View File

@ -139,23 +139,19 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
}
}
// POST endpoint for file uploads
export const POST: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => {
// Authentication check
const authResult = await withAPIAuth(request);
const authResult = await withAPIAuth(request, 'contributions');
if (authResult.authRequired && !authResult.authenticated) {
return apiError.unauthorized();
}
const userEmail = authResult.session?.email || 'anonymous@example.com';
const userEmail = authResult.session?.email || 'anon@anon.anon';
// Rate limiting
if (!checkUploadRateLimit(userEmail)) {
return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.');
}
// Parse multipart form data
let formData;
try {
formData = await request.formData();

View File

@ -6,7 +6,7 @@ import { withAuth } from '../../utils/auth.js'; // Note: .js extension!
export const prerender = false;
// CONSOLIDATED: Replace 15+ lines with single function call
const authResult = await withAuth(Astro);
const authResult = await withAuth(Astro, 'contributions');
if (authResult instanceof Response) {
return authResult; // Redirect to login
}

View File

@ -7,7 +7,7 @@ import { getToolsData } from '../../utils/dataService.js';
export const prerender = false;
// Check authentication
const authResult = await withAuth(Astro);
const authResult = await withAuth(Astro, 'contributions');
if (authResult instanceof Response) {
return authResult;
}

View File

@ -7,7 +7,7 @@ import { getToolsData } from '../../utils/dataService.js';
export const prerender = false;
// Check authentication
const authResult = await withAuth(Astro);
const authResult = await withAuth(Astro, 'contributions');
if (authResult instanceof Response) {
return authResult;
}

View File

@ -121,15 +121,13 @@ const tools = data.tools;
return;
}
// AI Query Button Handler using consolidated auth system
if (aiQueryBtn) {
aiQueryBtn.addEventListener('click', async () => {
// Use the global auth system consistently
if (typeof window.requireClientAuth === 'function') {
await window.requireClientAuth(() => switchToView('ai'), `${window.location.pathname}?view=ai`);
// ENHANCED: Use AI-specific authentication
await window.requireClientAuth(() => switchToView('ai'), `${window.location.pathname}?view=ai`, 'ai');
} else {
// Better fallback logging
console.warn('[AUTH] requireClientAuth not available - client-auth.js may not be loaded properly');
console.warn('[AUTH] requireClientAuth not available');
switchToView('ai');
}
});

View File

@ -30,6 +30,8 @@ export interface AuthContext {
userId: string;
}
export type AuthContextType = 'contributions' | 'ai' | 'general';
export interface UserInfo {
sub?: string;
preferred_username?: string;
@ -265,10 +267,19 @@ export function verifyAuthState(request: Request, receivedState: string): {
return { isValid: true, stateData };
}
/**
* CONSOLIDATED: Create session with cookie headers
* Replaces duplicated session creation in callback.ts and process.ts
*/
function getAuthRequirement(context: AuthContextType): boolean {
switch (context) {
case 'contributions':
return process.env.AUTHENTICATION_NECESSARY_CONTRIBUTIONS !== 'false';
case 'ai':
return process.env.AUTHENTICATION_NECESSARY_AI !== 'false';
case 'general':
return process.env.AUTHENTICATION_NECESSARY !== 'false';
default:
return true;
}
}
export async function createSessionWithCookie(userInfo: UserInfo): Promise<{
sessionToken: string;
sessionCookie: string;
@ -292,27 +303,20 @@ export async function createSessionWithCookie(userInfo: UserInfo): Promise<{
};
}
/**
* CONSOLIDATED: Replace repeated auth patterns in .astro pages
* Usage: const authResult = await withAuth(Astro);
* if (authResult instanceof Response) return authResult;
*/
export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Response> {
const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
console.log('[DEBUG PAGE] Auth required:', authRequired);
export async function withAuth(Astro: AstroGlobal, context: AuthContextType = 'general'): Promise<AuthContext | Response> {
const authRequired = getAuthRequirement(context);
console.log(`[DEBUG PAGE] Auth required for ${context}:`, authRequired);
console.log('[DEBUG PAGE] Request URL:', Astro.url.toString());
// If auth not required, return mock context
if (!authRequired) {
return {
authenticated: true,
session: null,
userEmail: 'anonymous@example.com',
userEmail: 'anon@anon.anon',
userId: 'anonymous'
};
}
// Check session
const sessionToken = getSessionFromRequest(Astro.request);
console.log('[DEBUG PAGE] Session token found:', !!sessionToken);
@ -337,7 +341,7 @@ export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Respon
});
}
console.log('[DEBUG PAGE] Page authentication successful for user:', session.userId);
console.log(`[DEBUG PAGE] Page authentication successful for ${context}:`, session.userId);
return {
authenticated: true,
session,
@ -346,17 +350,13 @@ export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Respon
};
}
/**
* CONSOLIDATED: Replace repeated auth patterns in API endpoints
* Enhanced version with better return structure
*/
export async function withAPIAuth(request: Request): Promise<{
export async function withAPIAuth(request: Request, context: AuthContextType = 'general'): Promise<{
authenticated: boolean;
userId: string;
session?: SessionData;
authRequired: boolean;
}> {
const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
const authRequired = getAuthRequirement(context);
if (!authRequired) {
return {
@ -367,10 +367,10 @@ export async function withAPIAuth(request: Request): Promise<{
}
const sessionToken = getSessionFromRequest(request);
console.log('[DEBUG] Session token found:', !!sessionToken);
console.log(`[DEBUG API] Session token found for ${context}:`, !!sessionToken);
if (!sessionToken) {
console.log('[DEBUG] No session token found');
console.log(`[DEBUG API] No session token found for ${context}`);
return {
authenticated: false,
userId: '',
@ -379,10 +379,10 @@ export async function withAPIAuth(request: Request): Promise<{
}
const session = await verifySession(sessionToken);
console.log('[DEBUG] Session verification result:', !!session);
console.log(`[DEBUG API] Session verification result for ${context}:`, !!session);
if (!session) {
console.log('[DEBUG] Session verification failed');
console.log(`[DEBUG API] Session verification failed for ${context}`);
return {
authenticated: false,
userId: '',
@ -390,11 +390,15 @@ export async function withAPIAuth(request: Request): Promise<{
};
}
console.log('[DEBUG] Authentication successful for user:', session.userId);
console.log(`[DEBUG API] Authentication successful for ${context}:`, session.userId);
return {
authenticated: true,
userId: session.userId,
session,
authRequired: true
};
}
export function getAuthRequirementForContext(context: AuthContextType): boolean {
return getAuthRequirement(context);
}