gated content
This commit is contained in:
@@ -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>
|
||||
</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>
|
||||
Reference in New Issue
Block a user