gated content
This commit is contained in:
		
							parent
							
								
									2f17370938
								
							
						
					
					
						commit
						d49b031eb9
					
				
							
								
								
									
										6
									
								
								.astro/content.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.astro/content.d.ts
									
									
									
									
										vendored
									
									
								
							@ -164,9 +164,11 @@ declare module 'astro:content' {
 | 
			
		||||
	type DataEntryMap = {
 | 
			
		||||
		"knowledgebase": Record<string, {
 | 
			
		||||
  id: string;
 | 
			
		||||
  body?: string;
 | 
			
		||||
  render(): Render[".md"];
 | 
			
		||||
  slug: string;
 | 
			
		||||
  body: string;
 | 
			
		||||
  collection: "knowledgebase";
 | 
			
		||||
  data: any;
 | 
			
		||||
  data: InferEntrySchema<"knowledgebase">;
 | 
			
		||||
  rendered?: RenderedContent;
 | 
			
		||||
  filePath?: string;
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,7 @@ NODE_ENV=development
 | 
			
		||||
# Set to true to require authentication (RECOMMENDED for production)
 | 
			
		||||
AUTHENTICATION_NECESSARY_CONTRIBUTIONS=false
 | 
			
		||||
AUTHENTICATION_NECESSARY_AI=false
 | 
			
		||||
AUTHENTICATION_NECESSARY_GATEDCONTENT=true
 | 
			
		||||
 | 
			
		||||
# OIDC Provider Configuration
 | 
			
		||||
OIDC_ENDPOINT=https://your-nextcloud.com/index.php/apps/oidc
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,11 @@ const knowledgebaseCollection = defineCollection({
 | 
			
		||||
    tags: z.array(z.string()).default([]),
 | 
			
		||||
    
 | 
			
		||||
    published: z.boolean().default(true),
 | 
			
		||||
    gated_content: z.boolean().default(false), // NEW: Gated content flag
 | 
			
		||||
    
 | 
			
		||||
  })
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const collections = {
 | 
			
		||||
  knowledgebase: knowledgebaseCollection
 | 
			
		||||
};
 | 
			
		||||
@ -6,6 +6,7 @@ last_updated: 2025-07-20
 | 
			
		||||
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
 | 
			
		||||
difficulty: "advanced"
 | 
			
		||||
categories: ["incident-response", "malware-analysis", "network-forensics"]
 | 
			
		||||
gated_content: true
 | 
			
		||||
tags: ["web-based", "endpoint-monitoring", "artifact-extraction", "scripting", "live-forensics", "hunting"]
 | 
			
		||||
sections:
 | 
			
		||||
  overview: true
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										6
									
								
								src/env.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								src/env.d.ts
									
									
									
									
										vendored
									
									
								
							@ -25,9 +25,9 @@ declare global {
 | 
			
		||||
    findToolByIdentifier: (tools: any[], identifier: string) => any | undefined;
 | 
			
		||||
    isToolHosted: (tool: any) => boolean;
 | 
			
		||||
 | 
			
		||||
    checkClientAuth: (context?: string) => Promise<{authenticated: boolean; authRequired: boolean; expires?: string}>;
 | 
			
		||||
    requireClientAuth: (callback?: () => void, returnUrl?: string, context?: string) => Promise<boolean>;
 | 
			
		||||
    showIfAuthenticated: (selector: string, context?: string) => Promise<void>;
 | 
			
		||||
    checkClientAuth: (context?: 'contributions' | 'ai' | 'general' | 'gatedcontent') => Promise<{authenticated: boolean; authRequired: boolean; expires?: string}>;
 | 
			
		||||
    requireClientAuth: (callback?: () => void, returnUrl?: string, context?: 'contributions' | 'ai' | 'general' | 'gatedcontent') => Promise<boolean>;
 | 
			
		||||
    showIfAuthenticated: (selector: string, context?: 'contributions' | 'ai' | 'general' | 'gatedcontent') => Promise<void>;
 | 
			
		||||
    setupAuthButtons: (selector?: string) => void;
 | 
			
		||||
 | 
			
		||||
    scrollToElement: (element: Element | null, options?: ScrollIntoViewOptions) => void;
 | 
			
		||||
 | 
			
		||||
@ -9,13 +9,16 @@ export const GET: APIRoute = async ({ request }) => {
 | 
			
		||||
  return await handleAPIRequest(async () => {
 | 
			
		||||
    const contributionAuth = await withAPIAuth(request, 'contributions');
 | 
			
		||||
    const aiAuth = await withAPIAuth(request, 'ai');
 | 
			
		||||
    const gatedContentAuth = await withAPIAuth(request, 'gatedcontent'); // ADDED
 | 
			
		||||
    
 | 
			
		||||
    return apiResponse.success({
 | 
			
		||||
      authenticated: contributionAuth.authenticated || aiAuth.authenticated,
 | 
			
		||||
      authenticated: contributionAuth.authenticated || aiAuth.authenticated || gatedContentAuth.authenticated,
 | 
			
		||||
      contributionAuthRequired: contributionAuth.authRequired,
 | 
			
		||||
      aiAuthRequired: aiAuth.authRequired,
 | 
			
		||||
      gatedContentAuthRequired: gatedContentAuth.authRequired, // ADDED
 | 
			
		||||
      contributionAuthenticated: contributionAuth.authenticated,
 | 
			
		||||
      aiAuthenticated: aiAuth.authenticated,
 | 
			
		||||
      gatedContentAuthenticated: gatedContentAuth.authenticated, // ADDED
 | 
			
		||||
      expires: contributionAuth.session?.exp ? new Date(contributionAuth.session.exp * 1000).toISOString() : null
 | 
			
		||||
    });
 | 
			
		||||
  }, 'Status check failed');
 | 
			
		||||
 | 
			
		||||
@ -166,6 +166,14 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div class="form-group">
 | 
			
		||||
            <label class="checkbox-wrapper">
 | 
			
		||||
              <input type="checkbox" id="gated-content" name="gatedContent" />
 | 
			
		||||
              <span>🔒 Als geschützten Inhalt markieren (Authentifizierung erforderlich)</span>
 | 
			
		||||
            </label>
 | 
			
		||||
            <small class="form-help">Nur für interne oder vertrauliche Inhalte</small>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div class="form-group">
 | 
			
		||||
            <label for="reason" class="form-label">Grund für den Beitrag (Optional)</label>
 | 
			
		||||
            <textarea 
 | 
			
		||||
 | 
			
		||||
@ -3,12 +3,16 @@ import BaseLayout from '../layouts/BaseLayout.astro';
 | 
			
		||||
import { getCollection } from 'astro:content';
 | 
			
		||||
import { getToolsData } from '../utils/dataService.js';
 | 
			
		||||
import ContributionButton from '../components/ContributionButton.astro';
 | 
			
		||||
import { isGatedContentAuthRequired } from '../utils/auth.js';
 | 
			
		||||
 | 
			
		||||
const data = await getToolsData();
 | 
			
		||||
const allKnowledgebaseEntries = await getCollection('knowledgebase', (entry) => {
 | 
			
		||||
  return entry.data.published !== false;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Check if gated content authentication is enabled globally
 | 
			
		||||
const gatedContentAuthEnabled = isGatedContentAuthRequired();
 | 
			
		||||
 | 
			
		||||
const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
 | 
			
		||||
  const associatedTool = entry.data.tool_name 
 | 
			
		||||
    ? data.tools.find((tool: any) => tool.name === entry.data.tool_name)
 | 
			
		||||
@ -23,6 +27,7 @@ const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
 | 
			
		||||
    difficulty: entry.data.difficulty,
 | 
			
		||||
    categories: entry.data.categories || [],
 | 
			
		||||
    tags: entry.data.tags || [],
 | 
			
		||||
    gated_content: entry.data.gated_content || false, // NEW: Include gated content flag
 | 
			
		||||
    
 | 
			
		||||
    tool_name: entry.data.tool_name,
 | 
			
		||||
    related_tools: entry.data.related_tools || [],
 | 
			
		||||
@ -39,6 +44,10 @@ const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
 | 
			
		||||
// Count gated vs public articles for statistics
 | 
			
		||||
const gatedCount = knowledgebaseEntries.filter(entry => entry.gated_content).length;
 | 
			
		||||
const publicCount = knowledgebaseEntries.length - gatedCount;
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title="Knowledgebase" description="Extended documentation and insights for DFIR tools">
 | 
			
		||||
@ -52,6 +61,24 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
        Praktische Erfahrungen, Konfigurationshinweise und Lektionen aus der Praxis
 | 
			
		||||
      </p>
 | 
			
		||||
      
 | 
			
		||||
      {gatedContentAuthEnabled && gatedCount > 0 && (
 | 
			
		||||
        <div class="gated-content-info mb-4 p-3 rounded" style="background-color: var(--color-bg-secondary); border: 1px solid var(--color-border);">
 | 
			
		||||
          <div class="flex items-center justify-center gap-2 text-sm text-secondary">
 | 
			
		||||
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
              <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
 | 
			
		||||
              <circle cx="12" cy="16" r="1"/>
 | 
			
		||||
              <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
 | 
			
		||||
            </svg>
 | 
			
		||||
            <span>
 | 
			
		||||
              {gatedCount} geschützte Artikel • {publicCount} öffentliche Artikel
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <p class="text-xs text-secondary mt-1">
 | 
			
		||||
            🔒 Geschützte Artikel erfordern Authentifizierung
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      
 | 
			
		||||
      <div class="flex gap-4 justify-center flex-wrap">
 | 
			
		||||
        <ContributionButton type="write" variant="primary" text="Artikel schreiben" style="padding: 0.75rem 1.5rem;" />
 | 
			
		||||
        <button onclick="window.scrollToElementById('kb-entries')" class="btn btn-secondary" style="padding: 0.75rem 1.5rem;">
 | 
			
		||||
@ -60,7 +87,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
            <line x1="21" y1="21" x2="16.65" y2="16.65"/>
 | 
			
		||||
          </svg>
 | 
			
		||||
          Artikel durchsuchen
 | 
			
		||||
        </a>
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -107,6 +134,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
            const isMethod = hasAssociatedTool && entry.associatedTool.type === 'method';
 | 
			
		||||
            const isConcept = hasAssociatedTool && entry.associatedTool.type === 'concept';
 | 
			
		||||
            const isStandalone = !hasAssociatedTool;
 | 
			
		||||
            const isGated = entry.gated_content === true;
 | 
			
		||||
            
 | 
			
		||||
            const articleUrl = `/knowledgebase/${entry.slug}`;
 | 
			
		||||
            
 | 
			
		||||
@ -116,6 +144,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
                id={`kb-${entry.slug}`} 
 | 
			
		||||
                data-tool-name={entry.title.toLowerCase()}
 | 
			
		||||
                data-article-type={isStandalone ? 'standalone' : 'tool-associated'}
 | 
			
		||||
                data-gated={isGated}
 | 
			
		||||
                onclick={`window.location.href='${articleUrl}'`}
 | 
			
		||||
              >
 | 
			
		||||
                <!-- Card Header -->
 | 
			
		||||
@ -125,6 +154,11 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
                    <div class="min-w-0 flex-1">
 | 
			
		||||
                      <h3 class="text-lg font-semibold text-primary mb-1 leading-tight">
 | 
			
		||||
                        {entry.title}
 | 
			
		||||
                        {isGated && gatedContentAuthEnabled && (
 | 
			
		||||
                          <span class="gated-indicator ml-2" title="Geschützter Inhalt - Authentifizierung erforderlich">
 | 
			
		||||
                            🔒
 | 
			
		||||
                          </span>
 | 
			
		||||
                        )}
 | 
			
		||||
                      </h3>
 | 
			
		||||
                      <div class="flex gap-2 flex-wrap mb-2">
 | 
			
		||||
                        {isStandalone && <span class="badge" style="background-color: var(--color-accent); color: white;">Artikel</span>}
 | 
			
		||||
@ -134,6 +168,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
                        {hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
 | 
			
		||||
                        {hasAssociatedTool && entry.associatedTool.license !== 'Proprietary' && !isMethod && !isConcept && <span class="badge badge-success">Open Source</span>}
 | 
			
		||||
                        <span class="badge badge-error">📖</span>
 | 
			
		||||
                        {isGated && gatedContentAuthEnabled && <span class="badge badge-warning">🔒</span>}
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -145,7 +180,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
                        <polyline points="15 3 21 3 21 9"/>
 | 
			
		||||
                        <line x1="10" y1="14" x2="21" y2="3"/>
 | 
			
		||||
                      </svg>
 | 
			
		||||
                      Öffnen
 | 
			
		||||
                      {isGated && gatedContentAuthEnabled ? 'Anmelden' : 'Öffnen'}
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <button 
 | 
			
		||||
                      class="btn btn-secondary btn-sm"
 | 
			
		||||
@ -168,6 +203,16 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
                <!-- Description -->
 | 
			
		||||
                <p class="text-secondary mb-4 leading-relaxed">
 | 
			
		||||
                  {entry.description}
 | 
			
		||||
                  {isGated && gatedContentAuthEnabled && (
 | 
			
		||||
                    <span class="gated-content-hint ml-2 text-xs">
 | 
			
		||||
                      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: inline; margin-right: 0.25rem;">
 | 
			
		||||
                        <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
 | 
			
		||||
                        <circle cx="12" cy="16" r="1"/>
 | 
			
		||||
                        <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
 | 
			
		||||
                      </svg>
 | 
			
		||||
                      Authentifizierung erforderlich
 | 
			
		||||
                    </span>
 | 
			
		||||
                  )}
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
                <!-- Metadata Footer -->
 | 
			
		||||
@ -299,7 +344,26 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
        lastScrollY = window.scrollY;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
  .gated-indicator {
 | 
			
		||||
    font-size: 0.875rem;
 | 
			
		||||
    opacity: 0.8;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .gated-content-hint {
 | 
			
		||||
    color: var(--color-warning);
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .kb-entry[data-gated="true"] {
 | 
			
		||||
    border-left: 3px solid var(--color-warning);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .gated-content-info {
 | 
			
		||||
    border-left: 4px solid var(--color-warning) !important;
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
</BaseLayout>
 | 
			
		||||
@ -2,6 +2,7 @@
 | 
			
		||||
import { getCollection } from 'astro:content';
 | 
			
		||||
import BaseLayout from '../../layouts/BaseLayout.astro';
 | 
			
		||||
import { getToolsData } from '../../utils/dataService.js';
 | 
			
		||||
import { isGatedContentAuthRequired } from '../../utils/auth.js';
 | 
			
		||||
 | 
			
		||||
export const prerender = true;
 | 
			
		||||
 | 
			
		||||
@ -20,6 +21,13 @@ export async function getStaticPaths() {
 | 
			
		||||
 | 
			
		||||
const { entry }: { entry: any } = Astro.props;
 | 
			
		||||
 | 
			
		||||
// Check if this article is gated and if gated content auth is required globally
 | 
			
		||||
const isGatedContent = entry.data.gated_content === true;
 | 
			
		||||
const gatedContentAuthRequired = isGatedContentAuthRequired();
 | 
			
		||||
const requiresAuth = isGatedContent && gatedContentAuthRequired;
 | 
			
		||||
 | 
			
		||||
console.log(`[GATED CONTENT] Article: ${entry.data.title}, Gated: ${isGatedContent}, Auth Required: ${requiresAuth}`);
 | 
			
		||||
 | 
			
		||||
const { Content } = await entry.render();
 | 
			
		||||
 | 
			
		||||
const data = await getToolsData();
 | 
			
		||||
@ -52,6 +60,68 @@ const currentUrl = Astro.url.href;
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title={entry.data.title} description={entry.data.description}>
 | 
			
		||||
  {requiresAuth && (
 | 
			
		||||
    <script define:vars={{ requiresAuth, articleTitle: entry.data.title }}>
 | 
			
		||||
      // Client-side authentication check for gated content
 | 
			
		||||
      document.addEventListener('DOMContentLoaded', async () => {
 | 
			
		||||
        if (!requiresAuth) return;
 | 
			
		||||
        
 | 
			
		||||
        console.log('[GATED CONTENT] Checking client-side auth for: ' + articleTitle);
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
          const response = await fetch('/api/auth/status');
 | 
			
		||||
          const authStatus = await response.json();
 | 
			
		||||
          
 | 
			
		||||
          const isAuthenticated = authStatus.gatedContentAuthenticated || false;
 | 
			
		||||
          const authRequired = authStatus.gatedContentAuthRequired || false;
 | 
			
		||||
          
 | 
			
		||||
          console.log('[GATED CONTENT] Auth status - Required: ' + authRequired + ', Authenticated: ' + isAuthenticated);
 | 
			
		||||
          
 | 
			
		||||
          if (authRequired && !isAuthenticated) {
 | 
			
		||||
            console.log('[GATED CONTENT] Redirecting for authentication: ' + articleTitle);
 | 
			
		||||
            
 | 
			
		||||
            // Show loading message briefly
 | 
			
		||||
            const contentArea = document.querySelector('.article-content');
 | 
			
		||||
            if (contentArea) {
 | 
			
		||||
              contentArea.innerHTML = [
 | 
			
		||||
                '<div style="text-align: center; padding: 3rem;">',
 | 
			
		||||
                  '<div style="font-size: 3rem; margin-bottom: 1rem;">🔒</div>',
 | 
			
		||||
                  '<h3 style="margin-bottom: 1rem;">Authentifizierung erforderlich</h3>',
 | 
			
		||||
                  '<p style="margin-bottom: 2rem;">Sie werden zur Anmeldung weitergeleitet...</p>',
 | 
			
		||||
                '</div>'
 | 
			
		||||
              ].join('');
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Redirect to login after brief delay
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              const currentUrl = encodeURIComponent(window.location.href);
 | 
			
		||||
              window.location.href = '/api/auth/login?returnTo=' + currentUrl;
 | 
			
		||||
            }, 1000);
 | 
			
		||||
          } else {
 | 
			
		||||
            console.log('[GATED CONTENT] Access granted for: ' + articleTitle);
 | 
			
		||||
          }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          console.error('[GATED CONTENT] Auth check failed:', error);
 | 
			
		||||
          // On error, show auth required message
 | 
			
		||||
          if (requiresAuth) {
 | 
			
		||||
            const contentArea = document.querySelector('.article-content');
 | 
			
		||||
            if (contentArea) {
 | 
			
		||||
              const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href);
 | 
			
		||||
              contentArea.innerHTML = [
 | 
			
		||||
                '<div style="text-align: center; padding: 3rem;">',
 | 
			
		||||
                  '<div style="font-size: 3rem; margin-bottom: 1rem;">⚠️</div>',
 | 
			
		||||
                  '<h3 style="margin-bottom: 1rem;">Authentifizierungsfehler</h3>',
 | 
			
		||||
                  '<p style="margin-bottom: 2rem;">Bitte versuchen Sie es später erneut oder melden Sie sich an.</p>',
 | 
			
		||||
                  '<a href="' + loginUrl + '" class="btn btn-primary">Anmelden</a>',
 | 
			
		||||
                '</div>'
 | 
			
		||||
              ].join('');
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    </script>
 | 
			
		||||
  )}
 | 
			
		||||
 | 
			
		||||
  <div class="article-layout">
 | 
			
		||||
    <!-- Article Header -->
 | 
			
		||||
    <header class="article-header">
 | 
			
		||||
@ -77,8 +147,24 @@ const currentUrl = Astro.url.href;
 | 
			
		||||
          <h1 class="article-title">
 | 
			
		||||
            {displayTool?.icon && <span class="article-icon">{displayTool.icon}</span>}
 | 
			
		||||
            {entry.data.title}
 | 
			
		||||
            {isGatedContent && (
 | 
			
		||||
              <span class="gated-indicator" title="Geschützter Inhalt - Authentifizierung erforderlich">
 | 
			
		||||
                🔒
 | 
			
		||||
              </span>
 | 
			
		||||
            )}
 | 
			
		||||
          </h1>
 | 
			
		||||
          <p class="article-description">{entry.data.description}</p>
 | 
			
		||||
          
 | 
			
		||||
          {isGatedContent && gatedContentAuthRequired && (
 | 
			
		||||
            <div class="gated-content-notice">
 | 
			
		||||
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
 | 
			
		||||
                <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
 | 
			
		||||
                <circle cx="12" cy="16" r="1"/>
 | 
			
		||||
                <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
 | 
			
		||||
              </svg>
 | 
			
		||||
              <span>Dieser Artikel enthält geschützte Inhalte</span>
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div class="article-metadata-grid">
 | 
			
		||||
@ -96,6 +182,7 @@ const currentUrl = Astro.url.href;
 | 
			
		||||
                </>
 | 
			
		||||
              )}
 | 
			
		||||
              <span class="badge badge-error">📖</span>
 | 
			
		||||
              {isGatedContent && <span class="badge badge-warning">🔒</span>}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          
 | 
			
		||||
@ -477,5 +564,23 @@ const currentUrl = Astro.url.href;
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
  .gated-indicator {
 | 
			
		||||
    font-size: 0.875rem;
 | 
			
		||||
    opacity: 0.8;
 | 
			
		||||
    margin-left: 0.5rem;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .gated-content-notice {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    padding: 0.75rem 1rem;
 | 
			
		||||
    background-color: var(--color-warning-bg, rgba(255, 193, 7, 0.1));
 | 
			
		||||
    border: 1px solid var(--color-warning, #ffc107);
 | 
			
		||||
    border-radius: 0.375rem;
 | 
			
		||||
    color: var(--color-warning-text, #856404);
 | 
			
		||||
    font-size: 0.875rem;
 | 
			
		||||
    margin-top: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
</BaseLayout>
 | 
			
		||||
@ -112,27 +112,6 @@ class AuditService {
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getDebugInfo(): {
 | 
			
		||||
    config: AuditConfig;
 | 
			
		||||
    environment: Record<string, any>;
 | 
			
		||||
    context: string;
 | 
			
		||||
  } {
 | 
			
		||||
    const context = typeof process !== 'undefined' ? 'server' : 'client';
 | 
			
		||||
    
 | 
			
		||||
    return {
 | 
			
		||||
      config: this.config,
 | 
			
		||||
      environment: {
 | 
			
		||||
        FORENSIC_AUDIT_ENABLED: env('FORENSIC_AUDIT_ENABLED'),
 | 
			
		||||
        FORENSIC_AUDIT_DETAIL_LEVEL: env('FORENSIC_AUDIT_DETAIL_LEVEL'),
 | 
			
		||||
        FORENSIC_AUDIT_RETENTION_HOURS: env('FORENSIC_AUDIT_RETENTION_HOURS'),
 | 
			
		||||
        FORENSIC_AUDIT_MAX_ENTRIES: env('FORENSIC_AUDIT_MAX_ENTRIES'),
 | 
			
		||||
        processEnvKeys: typeof process !== 'undefined' ? Object.keys(process.env).filter(k => k.includes('AUDIT')) : [],
 | 
			
		||||
        importMetaEnvAvailable: typeof import.meta !== 'undefined' && !!(import.meta as any).env
 | 
			
		||||
      },
 | 
			
		||||
      context
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addEntry(
 | 
			
		||||
    phase: string,
 | 
			
		||||
    action: string,
 | 
			
		||||
@ -397,15 +376,3 @@ class AuditService {
 | 
			
		||||
 | 
			
		||||
export const auditService = new AuditService();
 | 
			
		||||
export type { ProcessedAuditTrail, CompressedAuditEntry };
 | 
			
		||||
 | 
			
		||||
export const debugAuditService = {
 | 
			
		||||
  getDebugInfo() {
 | 
			
		||||
    return auditService.getDebugInfo();
 | 
			
		||||
  },
 | 
			
		||||
  isEnabled() {
 | 
			
		||||
    return auditService.isEnabled();
 | 
			
		||||
  },
 | 
			
		||||
  getConfig() {
 | 
			
		||||
    return auditService.getConfig();
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
// src/utils/auth.js (FIXED - Cleaned up and enhanced debugging)
 | 
			
		||||
// src/utils/auth.js (ENHANCED - Added gated content support)
 | 
			
		||||
import type { AstroGlobal } from 'astro';
 | 
			
		||||
import crypto from 'crypto';
 | 
			
		||||
import { config } from 'dotenv';
 | 
			
		||||
@ -27,7 +27,7 @@ export interface AuthContext {
 | 
			
		||||
  userId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AuthContextType = 'contributions' | 'ai' | 'general';
 | 
			
		||||
export type AuthContextType = 'contributions' | 'ai' | 'general' | 'gatedcontent';
 | 
			
		||||
 | 
			
		||||
export interface UserInfo {
 | 
			
		||||
  sub?: string;
 | 
			
		||||
@ -260,6 +260,8 @@ function getAuthRequirement(context: AuthContextType): boolean {
 | 
			
		||||
      return process.env.AUTHENTICATION_NECESSARY_CONTRIBUTIONS !== 'false';
 | 
			
		||||
    case 'ai':
 | 
			
		||||
      return process.env.AUTHENTICATION_NECESSARY_AI !== 'false';
 | 
			
		||||
    case 'gatedcontent':
 | 
			
		||||
      return process.env.AUTHENTICATION_NECESSARY_GATEDCONTENT !== 'false';
 | 
			
		||||
    default:
 | 
			
		||||
      return true;
 | 
			
		||||
  }
 | 
			
		||||
@ -387,3 +389,13 @@ export async function withAPIAuth(request: Request, context: AuthContextType = '
 | 
			
		||||
export function getAuthRequirementForContext(context: AuthContextType): boolean {
 | 
			
		||||
  return getAuthRequirement(context);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NEW: Helper function to check if gated content requires authentication
 | 
			
		||||
export function isGatedContentAuthRequired(): boolean {
 | 
			
		||||
  return getAuthRequirement('gatedcontent');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NEW: Check if specific content should be gated
 | 
			
		||||
export function shouldGateContent(isGatedContent: boolean): boolean {
 | 
			
		||||
  return isGatedContent && isGatedContentAuthRequired();
 | 
			
		||||
}
 | 
			
		||||
@ -48,12 +48,6 @@ class EmbeddingsService {
 | 
			
		||||
 | 
			
		||||
  private async checkEnabledStatus(): Promise<void> {
 | 
			
		||||
    try {   
 | 
			
		||||
      console.log('[EMBEDDINGS] Debug env check:', {
 | 
			
		||||
        AI_EMBEDDINGS_ENABLED: process.env.AI_EMBEDDINGS_ENABLED,
 | 
			
		||||
        envKeys: Object.keys(process.env).filter(k => k.includes('EMBEDDINGS')).length,
 | 
			
		||||
        allEnvKeys: Object.keys(process.env).length
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      const envEnabled = process.env.AI_EMBEDDINGS_ENABLED;
 | 
			
		||||
      
 | 
			
		||||
      if (envEnabled === 'true') {
 | 
			
		||||
@ -466,12 +460,3 @@ class EmbeddingsService {
 | 
			
		||||
const embeddingsService = new EmbeddingsService();
 | 
			
		||||
 | 
			
		||||
export { embeddingsService, type EmbeddingData, type SimilarityResult };
 | 
			
		||||
 | 
			
		||||
export const debugEmbeddings = {
 | 
			
		||||
  async recheckEnvironment() {
 | 
			
		||||
    return embeddingsService.forceRecheckEnvironment();
 | 
			
		||||
  },
 | 
			
		||||
  getStatus() {
 | 
			
		||||
    return embeddingsService.getStats();
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user