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 = {
|
type DataEntryMap = {
|
||||||
"knowledgebase": Record<string, {
|
"knowledgebase": Record<string, {
|
||||||
id: string;
|
id: string;
|
||||||
body?: string;
|
render(): Render[".md"];
|
||||||
|
slug: string;
|
||||||
|
body: string;
|
||||||
collection: "knowledgebase";
|
collection: "knowledgebase";
|
||||||
data: any;
|
data: InferEntrySchema<"knowledgebase">;
|
||||||
rendered?: RenderedContent;
|
rendered?: RenderedContent;
|
||||||
filePath?: string;
|
filePath?: string;
|
||||||
}>;
|
}>;
|
||||||
|
@ -30,6 +30,7 @@ NODE_ENV=development
|
|||||||
# Set to true to require authentication (RECOMMENDED for production)
|
# Set to true to require authentication (RECOMMENDED for production)
|
||||||
AUTHENTICATION_NECESSARY_CONTRIBUTIONS=false
|
AUTHENTICATION_NECESSARY_CONTRIBUTIONS=false
|
||||||
AUTHENTICATION_NECESSARY_AI=false
|
AUTHENTICATION_NECESSARY_AI=false
|
||||||
|
AUTHENTICATION_NECESSARY_GATEDCONTENT=true
|
||||||
|
|
||||||
# OIDC Provider Configuration
|
# OIDC Provider Configuration
|
||||||
OIDC_ENDPOINT=https://your-nextcloud.com/index.php/apps/oidc
|
OIDC_ENDPOINT=https://your-nextcloud.com/index.php/apps/oidc
|
||||||
|
@ -16,6 +16,11 @@ const knowledgebaseCollection = defineCollection({
|
|||||||
tags: z.array(z.string()).default([]),
|
tags: z.array(z.string()).default([]),
|
||||||
|
|
||||||
published: z.boolean().default(true),
|
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)"
|
author: "Claude 4 Sonnett (Prompt: Mario Stöckl)"
|
||||||
difficulty: "advanced"
|
difficulty: "advanced"
|
||||||
categories: ["incident-response", "malware-analysis", "network-forensics"]
|
categories: ["incident-response", "malware-analysis", "network-forensics"]
|
||||||
|
gated_content: true
|
||||||
tags: ["web-based", "endpoint-monitoring", "artifact-extraction", "scripting", "live-forensics", "hunting"]
|
tags: ["web-based", "endpoint-monitoring", "artifact-extraction", "scripting", "live-forensics", "hunting"]
|
||||||
sections:
|
sections:
|
||||||
overview: true
|
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;
|
findToolByIdentifier: (tools: any[], identifier: string) => any | undefined;
|
||||||
isToolHosted: (tool: any) => boolean;
|
isToolHosted: (tool: any) => boolean;
|
||||||
|
|
||||||
checkClientAuth: (context?: string) => Promise<{authenticated: boolean; authRequired: boolean; expires?: string}>;
|
checkClientAuth: (context?: 'contributions' | 'ai' | 'general' | 'gatedcontent') => Promise<{authenticated: boolean; authRequired: boolean; expires?: string}>;
|
||||||
requireClientAuth: (callback?: () => void, returnUrl?: string, context?: string) => Promise<boolean>;
|
requireClientAuth: (callback?: () => void, returnUrl?: string, context?: 'contributions' | 'ai' | 'general' | 'gatedcontent') => Promise<boolean>;
|
||||||
showIfAuthenticated: (selector: string, context?: string) => Promise<void>;
|
showIfAuthenticated: (selector: string, context?: 'contributions' | 'ai' | 'general' | 'gatedcontent') => Promise<void>;
|
||||||
setupAuthButtons: (selector?: string) => void;
|
setupAuthButtons: (selector?: string) => void;
|
||||||
|
|
||||||
scrollToElement: (element: Element | null, options?: ScrollIntoViewOptions) => void;
|
scrollToElement: (element: Element | null, options?: ScrollIntoViewOptions) => void;
|
||||||
|
@ -9,13 +9,16 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
return await handleAPIRequest(async () => {
|
return await handleAPIRequest(async () => {
|
||||||
const contributionAuth = await withAPIAuth(request, 'contributions');
|
const contributionAuth = await withAPIAuth(request, 'contributions');
|
||||||
const aiAuth = await withAPIAuth(request, 'ai');
|
const aiAuth = await withAPIAuth(request, 'ai');
|
||||||
|
const gatedContentAuth = await withAPIAuth(request, 'gatedcontent'); // ADDED
|
||||||
|
|
||||||
return apiResponse.success({
|
return apiResponse.success({
|
||||||
authenticated: contributionAuth.authenticated || aiAuth.authenticated,
|
authenticated: contributionAuth.authenticated || aiAuth.authenticated || gatedContentAuth.authenticated,
|
||||||
contributionAuthRequired: contributionAuth.authRequired,
|
contributionAuthRequired: contributionAuth.authRequired,
|
||||||
aiAuthRequired: aiAuth.authRequired,
|
aiAuthRequired: aiAuth.authRequired,
|
||||||
|
gatedContentAuthRequired: gatedContentAuth.authRequired, // ADDED
|
||||||
contributionAuthenticated: contributionAuth.authenticated,
|
contributionAuthenticated: contributionAuth.authenticated,
|
||||||
aiAuthenticated: aiAuth.authenticated,
|
aiAuthenticated: aiAuth.authenticated,
|
||||||
|
gatedContentAuthenticated: gatedContentAuth.authenticated, // ADDED
|
||||||
expires: contributionAuth.session?.exp ? new Date(contributionAuth.session.exp * 1000).toISOString() : null
|
expires: contributionAuth.session?.exp ? new Date(contributionAuth.session.exp * 1000).toISOString() : null
|
||||||
});
|
});
|
||||||
}, 'Status check failed');
|
}, 'Status check failed');
|
||||||
|
@ -166,6 +166,14 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="form-group">
|
||||||
<label for="reason" class="form-label">Grund für den Beitrag (Optional)</label>
|
<label for="reason" class="form-label">Grund für den Beitrag (Optional)</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
@ -3,12 +3,16 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import { getToolsData } from '../utils/dataService.js';
|
import { getToolsData } from '../utils/dataService.js';
|
||||||
import ContributionButton from '../components/ContributionButton.astro';
|
import ContributionButton from '../components/ContributionButton.astro';
|
||||||
|
import { isGatedContentAuthRequired } from '../utils/auth.js';
|
||||||
|
|
||||||
const data = await getToolsData();
|
const data = await getToolsData();
|
||||||
const allKnowledgebaseEntries = await getCollection('knowledgebase', (entry) => {
|
const allKnowledgebaseEntries = await getCollection('knowledgebase', (entry) => {
|
||||||
return entry.data.published !== false;
|
return entry.data.published !== false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if gated content authentication is enabled globally
|
||||||
|
const gatedContentAuthEnabled = isGatedContentAuthRequired();
|
||||||
|
|
||||||
const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
|
const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
|
||||||
const associatedTool = entry.data.tool_name
|
const associatedTool = entry.data.tool_name
|
||||||
? data.tools.find((tool: any) => tool.name === 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,
|
difficulty: entry.data.difficulty,
|
||||||
categories: entry.data.categories || [],
|
categories: entry.data.categories || [],
|
||||||
tags: entry.data.tags || [],
|
tags: entry.data.tags || [],
|
||||||
|
gated_content: entry.data.gated_content || false, // NEW: Include gated content flag
|
||||||
|
|
||||||
tool_name: entry.data.tool_name,
|
tool_name: entry.data.tool_name,
|
||||||
related_tools: entry.data.related_tools || [],
|
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));
|
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">
|
<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
|
Praktische Erfahrungen, Konfigurationshinweise und Lektionen aus der Praxis
|
||||||
</p>
|
</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">
|
<div class="flex gap-4 justify-center flex-wrap">
|
||||||
<ContributionButton type="write" variant="primary" text="Artikel schreiben" style="padding: 0.75rem 1.5rem;" />
|
<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;">
|
<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"/>
|
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||||
</svg>
|
</svg>
|
||||||
Artikel durchsuchen
|
Artikel durchsuchen
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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 isMethod = hasAssociatedTool && entry.associatedTool.type === 'method';
|
||||||
const isConcept = hasAssociatedTool && entry.associatedTool.type === 'concept';
|
const isConcept = hasAssociatedTool && entry.associatedTool.type === 'concept';
|
||||||
const isStandalone = !hasAssociatedTool;
|
const isStandalone = !hasAssociatedTool;
|
||||||
|
const isGated = entry.gated_content === true;
|
||||||
|
|
||||||
const articleUrl = `/knowledgebase/${entry.slug}`;
|
const articleUrl = `/knowledgebase/${entry.slug}`;
|
||||||
|
|
||||||
@ -116,6 +144,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
|
|||||||
id={`kb-${entry.slug}`}
|
id={`kb-${entry.slug}`}
|
||||||
data-tool-name={entry.title.toLowerCase()}
|
data-tool-name={entry.title.toLowerCase()}
|
||||||
data-article-type={isStandalone ? 'standalone' : 'tool-associated'}
|
data-article-type={isStandalone ? 'standalone' : 'tool-associated'}
|
||||||
|
data-gated={isGated}
|
||||||
onclick={`window.location.href='${articleUrl}'`}
|
onclick={`window.location.href='${articleUrl}'`}
|
||||||
>
|
>
|
||||||
<!-- Card Header -->
|
<!-- Card Header -->
|
||||||
@ -125,6 +154,11 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
|
|||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<h3 class="text-lg font-semibold text-primary mb-1 leading-tight">
|
<h3 class="text-lg font-semibold text-primary mb-1 leading-tight">
|
||||||
{entry.title}
|
{entry.title}
|
||||||
|
{isGated && gatedContentAuthEnabled && (
|
||||||
|
<span class="gated-indicator ml-2" title="Geschützter Inhalt - Authentifizierung erforderlich">
|
||||||
|
🔒
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex gap-2 flex-wrap mb-2">
|
<div class="flex gap-2 flex-wrap mb-2">
|
||||||
{isStandalone && <span class="badge" style="background-color: var(--color-accent); color: white;">Artikel</span>}
|
{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>}
|
{hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
|
||||||
{hasAssociatedTool && entry.associatedTool.license !== 'Proprietary' && !isMethod && !isConcept && <span class="badge badge-success">Open Source</span>}
|
{hasAssociatedTool && entry.associatedTool.license !== 'Proprietary' && !isMethod && !isConcept && <span class="badge badge-success">Open Source</span>}
|
||||||
<span class="badge badge-error">📖</span>
|
<span class="badge badge-error">📖</span>
|
||||||
|
{isGated && gatedContentAuthEnabled && <span class="badge badge-warning">🔒</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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"/>
|
<polyline points="15 3 21 3 21 9"/>
|
||||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||||
</svg>
|
</svg>
|
||||||
Öffnen
|
{isGated && gatedContentAuthEnabled ? 'Anmelden' : 'Öffnen'}
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
class="btn btn-secondary btn-sm"
|
class="btn btn-secondary btn-sm"
|
||||||
@ -168,6 +203,16 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
|
|||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<p class="text-secondary mb-4 leading-relaxed">
|
<p class="text-secondary mb-4 leading-relaxed">
|
||||||
{entry.description}
|
{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>
|
</p>
|
||||||
|
|
||||||
<!-- Metadata Footer -->
|
<!-- Metadata Footer -->
|
||||||
@ -299,7 +344,26 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
|
|||||||
lastScrollY = window.scrollY;
|
lastScrollY = window.scrollY;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</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 { getCollection } from 'astro:content';
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
import { getToolsData } from '../../utils/dataService.js';
|
import { getToolsData } from '../../utils/dataService.js';
|
||||||
|
import { isGatedContentAuthRequired } from '../../utils/auth.js';
|
||||||
|
|
||||||
export const prerender = true;
|
export const prerender = true;
|
||||||
|
|
||||||
@ -20,6 +21,13 @@ export async function getStaticPaths() {
|
|||||||
|
|
||||||
const { entry }: { entry: any } = Astro.props;
|
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 { Content } = await entry.render();
|
||||||
|
|
||||||
const data = await getToolsData();
|
const data = await getToolsData();
|
||||||
@ -52,6 +60,68 @@ const currentUrl = Astro.url.href;
|
|||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title={entry.data.title} description={entry.data.description}>
|
<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">
|
<div class="article-layout">
|
||||||
<!-- Article Header -->
|
<!-- Article Header -->
|
||||||
<header class="article-header">
|
<header class="article-header">
|
||||||
@ -77,8 +147,24 @@ const currentUrl = Astro.url.href;
|
|||||||
<h1 class="article-title">
|
<h1 class="article-title">
|
||||||
{displayTool?.icon && <span class="article-icon">{displayTool.icon}</span>}
|
{displayTool?.icon && <span class="article-icon">{displayTool.icon}</span>}
|
||||||
{entry.data.title}
|
{entry.data.title}
|
||||||
|
{isGatedContent && (
|
||||||
|
<span class="gated-indicator" title="Geschützter Inhalt - Authentifizierung erforderlich">
|
||||||
|
🔒
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="article-description">{entry.data.description}</p>
|
<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>
|
||||||
|
|
||||||
<div class="article-metadata-grid">
|
<div class="article-metadata-grid">
|
||||||
@ -96,6 +182,7 @@ const currentUrl = Astro.url.href;
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<span class="badge badge-error">📖</span>
|
<span class="badge badge-error">📖</span>
|
||||||
|
{isGatedContent && <span class="badge badge-warning">🔒</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -477,5 +564,23 @@ const currentUrl = Astro.url.href;
|
|||||||
});
|
});
|
||||||
</script>
|
</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>
|
</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(
|
addEntry(
|
||||||
phase: string,
|
phase: string,
|
||||||
action: string,
|
action: string,
|
||||||
@ -396,16 +375,4 @@ class AuditService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const auditService = new AuditService();
|
export const auditService = new AuditService();
|
||||||
export type { ProcessedAuditTrail, CompressedAuditEntry };
|
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 type { AstroGlobal } from 'astro';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { config } from 'dotenv';
|
import { config } from 'dotenv';
|
||||||
@ -27,7 +27,7 @@ export interface AuthContext {
|
|||||||
userId: string;
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthContextType = 'contributions' | 'ai' | 'general';
|
export type AuthContextType = 'contributions' | 'ai' | 'general' | 'gatedcontent';
|
||||||
|
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
sub?: string;
|
sub?: string;
|
||||||
@ -260,6 +260,8 @@ function getAuthRequirement(context: AuthContextType): boolean {
|
|||||||
return process.env.AUTHENTICATION_NECESSARY_CONTRIBUTIONS !== 'false';
|
return process.env.AUTHENTICATION_NECESSARY_CONTRIBUTIONS !== 'false';
|
||||||
case 'ai':
|
case 'ai':
|
||||||
return process.env.AUTHENTICATION_NECESSARY_AI !== 'false';
|
return process.env.AUTHENTICATION_NECESSARY_AI !== 'false';
|
||||||
|
case 'gatedcontent':
|
||||||
|
return process.env.AUTHENTICATION_NECESSARY_GATEDCONTENT !== 'false';
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -386,4 +388,14 @@ export async function withAPIAuth(request: Request, context: AuthContextType = '
|
|||||||
|
|
||||||
export function getAuthRequirementForContext(context: AuthContextType): boolean {
|
export function getAuthRequirementForContext(context: AuthContextType): boolean {
|
||||||
return getAuthRequirement(context);
|
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();
|
||||||
}
|
}
|
@ -47,13 +47,7 @@ class EmbeddingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async checkEnabledStatus(): Promise<void> {
|
private async checkEnabledStatus(): Promise<void> {
|
||||||
try {
|
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;
|
const envEnabled = process.env.AI_EMBEDDINGS_ENABLED;
|
||||||
|
|
||||||
if (envEnabled === 'true') {
|
if (envEnabled === 'true') {
|
||||||
@ -465,13 +459,4 @@ class EmbeddingsService {
|
|||||||
|
|
||||||
const embeddingsService = new EmbeddingsService();
|
const embeddingsService = new EmbeddingsService();
|
||||||
|
|
||||||
export { embeddingsService, type EmbeddingData, type SimilarityResult };
|
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