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_ENDPOINT=https://your-oidc-provider.com
OIDC_CLIENT_ID=your-oidc-client-id OIDC_CLIENT_ID=your-oidc-client-id
OIDC_CLIENT_SECRET=your-oidc-client-secret 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) # Application Configuration (Required)
PUBLIC_BASE_URL=https://your-domain.com 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="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/> <line x1="12" y1="16" x2="12.01" y2="16"/>
</svg> </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> <a href="https://mistral.ai/privacy-policy/" target="_blank" rel="noopener noreferrer" style="color: var(--color-primary); text-decoration: underline;">Datenschutzrichtlinien</a>
</p> </p>
</div> </div>

View File

@ -112,15 +112,31 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
(window as any).isToolHosted = isToolHosted; (window as any).isToolHosted = isToolHosted;
// Client-side auth functions (consolidated from client-auth.js) // Client-side auth functions (consolidated from client-auth.js)
async function checkClientAuth() { async function checkClientAuth(context = 'general') {
try { try {
const response = await fetch('/api/auth/status'); const response = await fetch('/api/auth/status');
const data = await response.json(); const data = await response.json();
return {
authenticated: data.authenticated, switch (context) {
authRequired: data.authRequired, case 'contributions':
expires: data.expires 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) { } catch (error) {
console.error('Auth check failed:', error); console.error('Auth check failed:', error);
return { return {
@ -130,8 +146,8 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
} }
} }
async function requireClientAuth(callback, returnUrl) { async function requireClientAuth(callback, returnUrl, context = 'general') {
const authStatus = await checkClientAuth(); const authStatus = await checkClientAuth(context);
if (authStatus.authRequired && !authStatus.authenticated) { if (authStatus.authRequired && !authStatus.authenticated) {
const targetUrl = returnUrl || window.location.href; const targetUrl = returnUrl || window.location.href;
@ -145,8 +161,8 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
} }
} }
async function showIfAuthenticated(selector) { async function showIfAuthenticated(selector, context = 'general') {
const authStatus = await checkClientAuth(); const authStatus = await checkClientAuth(context);
const element = document.querySelector(selector); const element = document.querySelector(selector);
if (element) { if (element) {
@ -157,11 +173,9 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
} }
function setupAuthButtons(selector = '[data-contribute-button]') { function setupAuthButtons(selector = '[data-contribute-button]') {
// Use event delegation on document for dynamic content support
document.addEventListener('click', async (e) => { document.addEventListener('click', async (e) => {
if (!e.target) return; if (!e.target) return;
// FIXED: Properly cast EventTarget to Element for closest() method
const button = (e.target as Element).closest(selector); const button = (e.target as Element).closest(selector);
if (!button) return; 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')); console.log('[AUTH] Contribute button clicked:', button.getAttribute('data-contribute-button'));
// ENHANCED: Use contributions context
await requireClientAuth(() => { await requireClientAuth(() => {
console.log('[AUTH] Navigation approved, redirecting to:', (button as HTMLAnchorElement).href); console.log('[AUTH] Navigation approved, redirecting to:', (button as HTMLAnchorElement).href);
window.location.href = (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]'); setupAuthButtons('[data-contribute-button]');
const initAIButton = async () => { const initAIButton = async () => {
await showIfAuthenticated('#ai-view-toggle'); await showIfAuthenticated('#ai-view-toggle', 'ai');
}; };
initAIButton(); 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 }) => { export const POST: APIRoute = async ({ request }) => {
try { try {
// CONSOLIDATED: Replace 20+ lines with single function call (UNCHANGED) // CONSOLIDATED: Replace 20+ lines with single function call (UNCHANGED)
const authResult = await withAPIAuth(request); const authResult = await withAPIAuth(request, 'ai');
if (!authResult.authenticated) { if (!authResult.authenticated) {
return createAuthErrorResponse(); 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 type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js'; import { withAPIAuth } from '../../../utils/auth.js';
import { apiResponse, handleAPIRequest } from '../../../utils/api.js'; import { apiResponse, handleAPIRequest } from '../../../utils/api.js';
@ -7,14 +7,16 @@ export const prerender = false;
export const GET: APIRoute = async ({ request }) => { export const GET: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => { return await handleAPIRequest(async () => {
// CONSOLIDATED: Single function call replaces 35+ lines const contributionAuth = await withAPIAuth(request, 'contributions');
const authResult = await withAPIAuth(request); const aiAuth = await withAPIAuth(request, 'ai');
return apiResponse.success({ return apiResponse.success({
authenticated: authResult.authenticated, authenticated: contributionAuth.authenticated || aiAuth.authenticated,
authRequired: authResult.authRequired, contributionAuthRequired: contributionAuth.authRequired,
expires: authResult.session?.exp ? new Date(authResult.session.exp * 1000).toISOString() : null aiAuthRequired: aiAuth.authRequired,
contributionAuthenticated: contributionAuth.authenticated,
aiAuthenticated: aiAuth.authenticated,
expires: contributionAuth.session?.exp ? new Date(contributionAuth.session.exp * 1000).toISOString() : null
}); });
}, 'Status check failed'); }, 'Status check failed');
}; };

View File

@ -84,12 +84,12 @@ function validateKnowledgebaseData(data: KnowledgebaseContributionData): { valid
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => { return await handleAPIRequest(async () => {
// Check authentication // Check authentication
const authResult = await withAPIAuth(request); const authResult = await withAPIAuth(request, 'contributions');
if (authResult.authRequired && !authResult.authenticated) { if (authResult.authRequired && !authResult.authenticated) {
return apiError.unauthorized(); return apiError.unauthorized();
} }
const userEmail = authResult.session?.email || 'anonymous@example.com'; const userEmail = authResult.session?.email || 'anon@anon.anon';
// Rate limiting // Rate limiting
if (!checkRateLimit(userEmail)) { 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 }) => { export const POST: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => { return await handleAPIRequest(async () => {
// Authentication check // Authentication check
const authResult = await withAPIAuth(request); const authResult = await withAPIAuth(request, 'contributions');
if (authResult.authRequired && !authResult.authenticated) { if (authResult.authRequired && !authResult.authenticated) {
return apiError.unauthorized(); return apiError.unauthorized();
} }
const userId = authResult.session?.userId || 'anonymous'; const userId = authResult.session?.userId || 'anonymous';
const userEmail = authResult.session?.email || 'anonymous@example.com'; const userEmail = authResult.session?.email || 'anon@anon.anon';
// Rate limiting // Rate limiting
if (!checkRateLimit(userId)) { 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 }) => { export const POST: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => { return await handleAPIRequest(async () => {
// Authentication check const authResult = await withAPIAuth(request, 'contributions');
const authResult = await withAPIAuth(request);
if (authResult.authRequired && !authResult.authenticated) { if (authResult.authRequired && !authResult.authenticated) {
return apiError.unauthorized(); return apiError.unauthorized();
} }
const userEmail = authResult.session?.email || 'anonymous@example.com'; const userEmail = authResult.session?.email || 'anon@anon.anon';
// Rate limiting
if (!checkUploadRateLimit(userEmail)) { if (!checkUploadRateLimit(userEmail)) {
return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.'); return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.');
} }
// Parse multipart form data
let formData; let formData;
try { try {
formData = await request.formData(); formData = await request.formData();

View File

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

View File

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

View File

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

View File

@ -121,15 +121,13 @@ const tools = data.tools;
return; return;
} }
// AI Query Button Handler using consolidated auth system
if (aiQueryBtn) { if (aiQueryBtn) {
aiQueryBtn.addEventListener('click', async () => { aiQueryBtn.addEventListener('click', async () => {
// Use the global auth system consistently
if (typeof window.requireClientAuth === 'function') { 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 { } else {
// Better fallback logging console.warn('[AUTH] requireClientAuth not available');
console.warn('[AUTH] requireClientAuth not available - client-auth.js may not be loaded properly');
switchToView('ai'); switchToView('ai');
} }
}); });

View File

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