gatedcontent
This commit is contained in:
		
							parent
							
								
									d49b031eb9
								
							
						
					
					
						commit
						a52c0781e1
					
				@ -32,8 +32,8 @@ 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
 | 
			
		||||
# OIDC Provider Configuration - Server appends endpoint (e.g. auth/callback) automatically
 | 
			
		||||
OIDC_ENDPOINT=https://cloud.cc24.dev/index.php
 | 
			
		||||
OIDC_CLIENT_ID=your-client-id
 | 
			
		||||
OIDC_CLIENT_SECRET=your-client-secret
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -229,6 +229,12 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
 | 
			
		||||
              authRequired: data.aiAuthRequired,
 | 
			
		||||
              expires: data.expires
 | 
			
		||||
            };
 | 
			
		||||
            case 'gatedcontent':  // ADD THIS CASE
 | 
			
		||||
              return {
 | 
			
		||||
                authenticated: data.gatedContentAuthenticated,
 | 
			
		||||
                authRequired: data.gatedContentAuthRequired,
 | 
			
		||||
                expires: data.expires
 | 
			
		||||
            };
 | 
			
		||||
          default:
 | 
			
		||||
            return {
 | 
			
		||||
              authenticated: data.authenticated,
 | 
			
		||||
 | 
			
		||||
@ -44,16 +44,12 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    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.clearStateCookie);
 | 
			
		||||
    
 | 
			
		||||
    return new Response(JSON.stringify({ 
 | 
			
		||||
      success: true, 
 | 
			
		||||
      redirectTo: stateVerification.stateData.returnTo 
 | 
			
		||||
    }), {
 | 
			
		||||
      status: 200,
 | 
			
		||||
 | 
			
		||||
    return new Response(null, {
 | 
			
		||||
      status: 302,
 | 
			
		||||
      headers: responseHeaders
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
@ -174,7 +174,9 @@ const publicCount = knowledgebaseEntries.length - gatedCount;
 | 
			
		||||
                  </div>
 | 
			
		||||
                  
 | 
			
		||||
                  <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;">
 | 
			
		||||
                        <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"/>
 | 
			
		||||
 | 
			
		||||
@ -68,6 +68,19 @@ const currentUrl = Astro.url.href;
 | 
			
		||||
        
 | 
			
		||||
        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 {
 | 
			
		||||
          const response = await fetch('/api/auth/status');
 | 
			
		||||
          const authStatus = await response.json();
 | 
			
		||||
@ -78,44 +91,64 @@ const currentUrl = Astro.url.href;
 | 
			
		||||
          console.log('[GATED CONTENT] Auth status - Required: ' + authRequired + ', Authenticated: ' + 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
 | 
			
		||||
            const contentArea = document.querySelector('.article-content');
 | 
			
		||||
            // Show authentication required message (no auto-redirect)
 | 
			
		||||
            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;">Authentifizierung erforderlich</h3>',
 | 
			
		||||
                  '<p style="margin-bottom: 2rem;">Sie werden zur Anmeldung weitergeleitet...</p>',
 | 
			
		||||
                '<div class="gated-content-block">',
 | 
			
		||||
                  '<div class="gated-icon">🔒</div>',
 | 
			
		||||
                  '<h3 class="gated-title">Authentifizierung erforderlich</h3>',
 | 
			
		||||
                  '<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>'
 | 
			
		||||
              ].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 {
 | 
			
		||||
            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) {
 | 
			
		||||
          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>',
 | 
			
		||||
          if (requiresAuth && contentArea) {
 | 
			
		||||
            const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href);
 | 
			
		||||
            contentArea.innerHTML = [
 | 
			
		||||
              '<div class="gated-content-block">',
 | 
			
		||||
                '<div class="gated-icon error">⚠️</div>',
 | 
			
		||||
                '<h3 class="gated-title">Authentifizierungsfehler</h3>',
 | 
			
		||||
                '<p class="gated-description">Es gab ein Problem bei der Überprüfung Ihrer Berechtigung. Bitte versuchen Sie es erneut.</p>',
 | 
			
		||||
                '<div class="gated-actions">',
 | 
			
		||||
                  '<a href="' + loginUrl + '" class="btn btn-primary">Anmelden</a>',
 | 
			
		||||
                '</div>'
 | 
			
		||||
              ].join('');
 | 
			
		||||
            }
 | 
			
		||||
                  '<button onclick="location.reload()" class="btn btn-secondary">Seite neu laden</button>',
 | 
			
		||||
                '</div>',
 | 
			
		||||
              '</div>'
 | 
			
		||||
            ].join('');
 | 
			
		||||
            contentArea.style.display = 'block';
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
@ -245,7 +278,7 @@ const currentUrl = Astro.url.href;
 | 
			
		||||
      
 | 
			
		||||
      <!-- Article Content -->
 | 
			
		||||
      <main class="article-main">
 | 
			
		||||
        <article class="article-content">
 | 
			
		||||
        <article class="article-content" style={requiresAuth ? "display: none;" : ""}>
 | 
			
		||||
          <div class="markdown-content">
 | 
			
		||||
            <Content />
 | 
			
		||||
          </div>
 | 
			
		||||
@ -337,7 +370,7 @@ const currentUrl = Astro.url.href;
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
<script define:vars={{ requiresAuth }}>
 | 
			
		||||
  /** @template {Element} T
 | 
			
		||||
   *  @param {string} sel
 | 
			
		||||
   *  @param {Document|Element} [root=document]
 | 
			
		||||
@ -369,6 +402,33 @@ const currentUrl = Astro.url.href;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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} */
 | 
			
		||||
    const article = qs('.markdown-content');
 | 
			
		||||
    /** @type {HTMLElement|null} */
 | 
			
		||||
@ -582,5 +642,70 @@ const currentUrl = Astro.url.href;
 | 
			
		||||
    font-size: 0.875rem;
 | 
			
		||||
    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>
 | 
			
		||||
</BaseLayout>
 | 
			
		||||
@ -511,14 +511,14 @@
 | 
			
		||||
/* Content wrapper with sidebar */
 | 
			
		||||
.article-content-wrapper {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 280px 1fr;
 | 
			
		||||
  grid-template-columns: var(--main-grid-columns, 280px 1fr);
 | 
			
		||||
  gap: 3rem;
 | 
			
		||||
  align-items: start;
 | 
			
		||||
}
 | 
			
		||||
.article-sidebar {
 | 
			
		||||
  position: sticky;
 | 
			
		||||
  top: 6rem;
 | 
			
		||||
  max-height: calc(100vh - 8rem);
 | 
			
		||||
  top: 0.1rem;
 | 
			
		||||
  max-height: 100vh;
 | 
			
		||||
  overflow-y: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -567,19 +567,44 @@
 | 
			
		||||
.toc-level-6 { padding-left: 3rem; font-size: 0.8125rem; opacity: 0.8; }
 | 
			
		||||
 | 
			
		||||
/* Main article column */
 | 
			
		||||
.article-main { min-width: 0; max-width: 95ch; }
 | 
			
		||||
.article-content { margin-bottom: 3rem; }
 | 
			
		||||
 | 
			
		||||
/* Footer */
 | 
			
		||||
.article-footer {
 | 
			
		||||
  border-top: 2px solid var(--color-border);
 | 
			
		||||
  padding-top: 2rem;
 | 
			
		||||
  margin-top: 3rem;
 | 
			
		||||
.article-main { 
 | 
			
		||||
  min-width: 0; 
 | 
			
		||||
  max-width: 95ch;
 | 
			
		||||
  background-color: var(--color-bg);
 | 
			
		||||
  border: 1px solid var(--color-border);
 | 
			
		||||
  border-radius: 1rem;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  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) */
 | 
			
		||||
.btn-concept { background-color: var(--color-concept); color: white; border-color: var(--color-concept); }
 | 
			
		||||
.btn-concept:hover { opacity: 0.9; }
 | 
			
		||||
@ -628,6 +653,10 @@
 | 
			
		||||
  .article-actions { flex-direction: column; }
 | 
			
		||||
  .footer-actions-grid { flex-direction: column; }
 | 
			
		||||
  .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) {
 | 
			
		||||
    font-size: 1rem;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user