gatedcontent

This commit is contained in:
overcuriousity 2025-08-11 22:55:11 +02:00
parent d49b031eb9
commit a52c0781e1
6 changed files with 210 additions and 52 deletions

View File

@ -32,8 +32,8 @@ AUTHENTICATION_NECESSARY_CONTRIBUTIONS=false
AUTHENTICATION_NECESSARY_AI=false AUTHENTICATION_NECESSARY_AI=false
AUTHENTICATION_NECESSARY_GATEDCONTENT=true AUTHENTICATION_NECESSARY_GATEDCONTENT=true
# OIDC Provider Configuration # OIDC Provider Configuration - Server appends endpoint (e.g. auth/callback) automatically
OIDC_ENDPOINT=https://your-nextcloud.com/index.php/apps/oidc OIDC_ENDPOINT=https://cloud.cc24.dev/index.php
OIDC_CLIENT_ID=your-client-id OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-client-secret OIDC_CLIENT_SECRET=your-client-secret

View File

@ -229,6 +229,12 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
authRequired: data.aiAuthRequired, authRequired: data.aiAuthRequired,
expires: data.expires expires: data.expires
}; };
case 'gatedcontent': // ADD THIS CASE
return {
authenticated: data.gatedContentAuthenticated,
authRequired: data.gatedContentAuthRequired,
expires: data.expires
};
default: default:
return { return {
authenticated: data.authenticated, authenticated: data.authenticated,

View File

@ -44,16 +44,12 @@ export const POST: APIRoute = async ({ request }) => {
}); });
const responseHeaders = new Headers(); const responseHeaders = new Headers();
responseHeaders.set('Content-Type', 'application/json'); responseHeaders.set('Location', stateVerification.stateData.returnTo);
responseHeaders.append('Set-Cookie', sessionResult.sessionCookie); responseHeaders.append('Set-Cookie', sessionResult.sessionCookie);
responseHeaders.append('Set-Cookie', sessionResult.clearStateCookie); responseHeaders.append('Set-Cookie', sessionResult.clearStateCookie);
return new Response(JSON.stringify({ return new Response(null, {
success: true, status: 302,
redirectTo: stateVerification.stateData.returnTo
}), {
status: 200,
headers: responseHeaders headers: responseHeaders
}); });

View File

@ -174,7 +174,9 @@ const publicCount = knowledgebaseEntries.length - gatedCount;
</div> </div>
<div class="flex gap-2 flex-shrink-0" onclick="event.stopPropagation();"> <div class="flex gap-2 flex-shrink-0" onclick="event.stopPropagation();">
<a href={articleUrl} class="btn btn-primary btn-sm"> <a href={articleUrl}
class="btn btn-primary btn-sm"
title={isGated && isGatedContentAuthRequired() ? "Geschützter Inhalt - Anmeldung erforderlich" : "Artikel öffnen"}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/> <polyline points="15 3 21 3 21 9"/>

View File

@ -68,6 +68,19 @@ const currentUrl = Astro.url.href;
console.log('[GATED CONTENT] Checking client-side auth for: ' + articleTitle); console.log('[GATED CONTENT] Checking client-side auth for: ' + articleTitle);
// Hide content immediately while checking auth
const contentArea = document.querySelector('.article-content');
const sidebar = document.querySelector('.article-sidebar');
if (contentArea) {
contentArea.style.display = 'none';
}
// DON'T hide the sidebar container - just prevent TOC generation
//if (sidebar) {
//sidebar.innerHTML = ''; // Clear any content instead of hiding
//}
try { try {
const response = await fetch('/api/auth/status'); const response = await fetch('/api/auth/status');
const authStatus = await response.json(); const authStatus = await response.json();
@ -78,44 +91,64 @@ const currentUrl = Astro.url.href;
console.log('[GATED CONTENT] Auth status - Required: ' + authRequired + ', Authenticated: ' + isAuthenticated); console.log('[GATED CONTENT] Auth status - Required: ' + authRequired + ', Authenticated: ' + isAuthenticated);
if (authRequired && !isAuthenticated) { if (authRequired && !isAuthenticated) {
console.log('[GATED CONTENT] Redirecting for authentication: ' + articleTitle); console.log('[GATED CONTENT] Access denied - showing auth required message: ' + articleTitle);
// Show loading message briefly // Show authentication required message (no auto-redirect)
const contentArea = document.querySelector('.article-content');
if (contentArea) { if (contentArea) {
const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href);
contentArea.innerHTML = [ contentArea.innerHTML = [
'<div style="text-align: center; padding: 3rem;">', '<div class="gated-content-block">',
'<div style="font-size: 3rem; margin-bottom: 1rem;">🔒</div>', '<div class="gated-icon">🔒</div>',
'<h3 style="margin-bottom: 1rem;">Authentifizierung erforderlich</h3>', '<h3 class="gated-title">Authentifizierung erforderlich</h3>',
'<p style="margin-bottom: 2rem;">Sie werden zur Anmeldung weitergeleitet...</p>', '<p class="gated-description">Dieser Artikel enthält geschützte Inhalte und ist nur für authentifizierte Benutzer zugänglich.</p>',
'<div class="gated-actions">',
'<a href="' + loginUrl + '" class="btn btn-primary">',
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">',
'<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>',
'<polyline points="10 17 15 12 10 7"/>',
'<line x1="15" y1="12" x2="3" y2="12"/>',
'</svg>',
'Anmelden',
'</a>',
'<a href="/knowledgebase" class="btn btn-secondary">Zurück zur Übersicht</a>',
'</div>',
'<div class="gated-help">',
'<small>Nach der Anmeldung werden Sie automatisch zu diesem Artikel zurückgeleitet.</small>',
'</div>',
'</div>' '</div>'
].join(''); ].join('');
contentArea.style.display = 'block';
} }
// Redirect to login after brief delay
setTimeout(() => {
const currentUrl = encodeURIComponent(window.location.href);
window.location.href = '/api/auth/login?returnTo=' + currentUrl;
}, 1000);
} else { } else {
console.log('[GATED CONTENT] Access granted for: ' + articleTitle); console.log('[GATED CONTENT] Access granted for: ' + articleTitle);
// Show content for authenticated users
if (contentArea) {
contentArea.style.display = 'block';
}
// Let TOC generate normally for authenticated users
setTimeout(() => {
if (typeof generateTOCContent === 'function') {
generateTOCContent();
}
}, 100);
} }
} catch (error) { } catch (error) {
console.error('[GATED CONTENT] Auth check failed:', error); console.error('[GATED CONTENT] Auth check failed:', error);
// On error, show auth required message // On error, show auth required message
if (requiresAuth) { if (requiresAuth && contentArea) {
const contentArea = document.querySelector('.article-content'); const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href);
if (contentArea) { contentArea.innerHTML = [
const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href); '<div class="gated-content-block">',
contentArea.innerHTML = [ '<div class="gated-icon error">⚠️</div>',
'<div style="text-align: center; padding: 3rem;">', '<h3 class="gated-title">Authentifizierungsfehler</h3>',
'<div style="font-size: 3rem; margin-bottom: 1rem;">⚠️</div>', '<p class="gated-description">Es gab ein Problem bei der Überprüfung Ihrer Berechtigung. Bitte versuchen Sie es erneut.</p>',
'<h3 style="margin-bottom: 1rem;">Authentifizierungsfehler</h3>', '<div class="gated-actions">',
'<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>', '<a href="' + loginUrl + '" class="btn btn-primary">Anmelden</a>',
'</div>' '<button onclick="location.reload()" class="btn btn-secondary">Seite neu laden</button>',
].join(''); '</div>',
} '</div>'
].join('');
contentArea.style.display = 'block';
} }
} }
}); });
@ -245,7 +278,7 @@ const currentUrl = Astro.url.href;
<!-- Article Content --> <!-- Article Content -->
<main class="article-main"> <main class="article-main">
<article class="article-content"> <article class="article-content" style={requiresAuth ? "display: none;" : ""}>
<div class="markdown-content"> <div class="markdown-content">
<Content /> <Content />
</div> </div>
@ -337,7 +370,7 @@ const currentUrl = Astro.url.href;
</div> </div>
</div> </div>
<script> <script define:vars={{ requiresAuth }}>
/** @template {Element} T /** @template {Element} T
* @param {string} sel * @param {string} sel
* @param {Document|Element} [root=document] * @param {Document|Element} [root=document]
@ -369,6 +402,33 @@ const currentUrl = Astro.url.href;
} }
function generateSidebarTOC() { function generateSidebarTOC() {
// NEW: Don't generate TOC for gated content that requires auth
if (requiresAuth) {
fetch('/api/auth/status')
.then(response => response.json())
.then(authStatus => {
const isAuthenticated = authStatus.gatedContentAuthenticated || false;
const authRequired = authStatus.gatedContentAuthRequired || false;
// Only generate TOC if user is authenticated for gated content
if (authRequired && !isAuthenticated) {
return; // Don't generate TOC
} else {
generateTOCContent(); // Generate TOC for authenticated users
}
})
.catch(() => {
// On error, don't generate TOC for gated content
return;
});
return;
}
// For non-gated content, generate TOC normally
generateTOCContent();
}
function generateTOCContent() {
/** @type {HTMLElement|null} */ /** @type {HTMLElement|null} */
const article = qs('.markdown-content'); const article = qs('.markdown-content');
/** @type {HTMLElement|null} */ /** @type {HTMLElement|null} */
@ -582,5 +642,70 @@ const currentUrl = Astro.url.href;
font-size: 0.875rem; font-size: 0.875rem;
margin-top: 1rem; margin-top: 1rem;
} }
.gated-content-block {
text-align: center;
padding: 4rem 2rem;
max-width: 600px;
margin: 0 auto;
}
.gated-icon {
font-size: 4rem;
margin-bottom: 1.5rem;
opacity: 0.8;
}
.gated-icon.error {
color: var(--color-error, #dc3545);
}
.gated-title {
font-size: 1.5rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 1rem;
}
.gated-description {
font-size: 1rem;
color: var(--color-text-secondary);
line-height: 1.6;
margin-bottom: 2rem;
}
.gated-actions {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 1.5rem;
}
.gated-help {
color: var(--color-text-secondary);
font-size: 0.875rem;
}
.gated-help small {
opacity: 0.8;
}
/* Responsive adjustments */
@media (max-width: 640px) {
.gated-content-block {
padding: 3rem 1rem;
}
.gated-actions {
flex-direction: column;
align-items: center;
}
.gated-actions .btn {
width: 100%;
max-width: 280px;
}
}
</style> </style>
</BaseLayout> </BaseLayout>

View File

@ -511,14 +511,14 @@
/* Content wrapper with sidebar */ /* Content wrapper with sidebar */
.article-content-wrapper { .article-content-wrapper {
display: grid; display: grid;
grid-template-columns: 280px 1fr; grid-template-columns: var(--main-grid-columns, 280px 1fr);
gap: 3rem; gap: 3rem;
align-items: start; align-items: start;
} }
.article-sidebar { .article-sidebar {
position: sticky; position: sticky;
top: 6rem; top: 0.1rem;
max-height: calc(100vh - 8rem); max-height: 100vh;
overflow-y: auto; overflow-y: auto;
} }
@ -567,19 +567,44 @@
.toc-level-6 { padding-left: 3rem; font-size: 0.8125rem; opacity: 0.8; } .toc-level-6 { padding-left: 3rem; font-size: 0.8125rem; opacity: 0.8; }
/* Main article column */ /* Main article column */
.article-main { min-width: 0; max-width: 95ch; } .article-main {
.article-content { margin-bottom: 3rem; } min-width: 0;
max-width: 95ch;
/* Footer */ background-color: var(--color-bg);
.article-footer { border: 1px solid var(--color-border);
border-top: 2px solid var(--color-border); border-radius: 1rem;
padding-top: 2rem; overflow: hidden;
margin-top: 3rem; box-shadow: var(--shadow-sm);
} }
.article-footer h3 { margin: 0 0 1.5rem 0; color: var(--color-primary); }
.footer-actions-grid { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 2rem; }
.footer-actions-grid .btn { flex: 1; min-width: 200px; }
.article-content {
padding: 2.5rem;
margin-bottom: 0;
background: linear-gradient(135deg, var(--color-bg) 0%, var(--color-bg-secondary) 100%);
}
.article-footer {
background-color: var(--color-bg-secondary);
border-top: 1px solid var(--color-border);
padding: 2rem 2.5rem;
margin-top: 0;
}
.article-footer h3 {
margin: 0 0 1.5rem 0;
color: var(--color-primary);
font-size: 1.25rem;
font-weight: 600;
}
.footer-actions-grid {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 2rem;
}
.footer-actions-grid .btn {
flex: 1;
min-width: 200px;
justify-content: center;
}
/* Tool button variants (keep since template uses them) */ /* Tool button variants (keep since template uses them) */
.btn-concept { background-color: var(--color-concept); color: white; border-color: var(--color-concept); } .btn-concept { background-color: var(--color-concept); color: white; border-color: var(--color-concept); }
.btn-concept:hover { opacity: 0.9; } .btn-concept:hover { opacity: 0.9; }
@ -628,6 +653,10 @@
.article-actions { flex-direction: column; } .article-actions { flex-direction: column; }
.footer-actions-grid { flex-direction: column; } .footer-actions-grid { flex-direction: column; }
.footer-actions-grid .btn { min-width: auto; } .footer-actions-grid .btn { min-width: auto; }
.article-content { padding: 1.5rem; }
.article-footer { padding: 1.5rem; }
.footer-actions-grid { flex-direction: column; }
.footer-actions-grid .btn { min-width: auto; }
:where(.markdown-content) { :where(.markdown-content) {
font-size: 1rem; font-size: 1rem;