videos #17

Merged
mstoeck3 merged 7 commits from videos into main 2025-08-12 20:35:06 +00:00
18 changed files with 20 additions and 141 deletions
Showing only changes of commit d1c297189d - Show all commits

View File

@ -193,7 +193,6 @@ domains.forEach((domain: any) => {
</div> </div>
<script define:vars={{ toolsData: tools, domainAgnosticSoftware, domainAgnosticTools }}> <script define:vars={{ toolsData: tools, domainAgnosticSoftware, domainAgnosticTools }}>
// Ensure isToolHosted is available
if (!window.isToolHosted) { if (!window.isToolHosted) {
window.isToolHosted = function(tool) { window.isToolHosted = function(tool) {
return tool.projectUrl !== undefined && return tool.projectUrl !== undefined &&
@ -765,14 +764,12 @@ domains.forEach((domain: any) => {
hideToolDetails('both'); hideToolDetails('both');
} }
// Register all functions globally
window.showToolDetails = showToolDetails; window.showToolDetails = showToolDetails;
window.hideToolDetails = hideToolDetails; window.hideToolDetails = hideToolDetails;
window.hideAllToolDetails = hideAllToolDetails; window.hideAllToolDetails = hideAllToolDetails;
window.toggleDomainAgnosticSection = toggleDomainAgnosticSection; window.toggleDomainAgnosticSection = toggleDomainAgnosticSection;
window.showShareDialog = showShareDialog; window.showShareDialog = showShareDialog;
// Register matrix-prefixed versions for delegation
window.matrixShowToolDetails = showToolDetails; window.matrixShowToolDetails = showToolDetails;
window.matrixHideToolDetails = hideToolDetails; window.matrixHideToolDetails = hideToolDetails;

View File

@ -37,7 +37,6 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
} catch (error) { } catch (error) {
console.error('Failed to load utility functions:', error); console.error('Failed to load utility functions:', error);
// Provide fallback implementations
(window as any).createToolSlug = (toolName: string) => { (window as any).createToolSlug = (toolName: string) => {
if (!toolName || typeof toolName !== 'string') return ''; if (!toolName || typeof toolName !== 'string') return '';
return toolName.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); return toolName.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
@ -119,7 +118,6 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
(window as any).prioritizeSearchResults = prioritizeSearchResults; (window as any).prioritizeSearchResults = prioritizeSearchResults;
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
// CRITICAL: Load utility functions FIRST before any URL handling
await loadUtilityFunctions(); await loadUtilityFunctions();
const THEME_KEY = 'dfir-theme'; const THEME_KEY = 'dfir-theme';
@ -366,7 +364,6 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
errorCount++; errorCount++;
console.log(`[VIDEO] Error ${errorCount} in Firefox for: ${video.getAttribute('data-video-title')}`); console.log(`[VIDEO] Error ${errorCount} in Firefox for: ${video.getAttribute('data-video-title')}`);
// Only try once to avoid infinite loops
if (errorCount === 1 && video.src.includes('/download')) { if (errorCount === 1 && video.src.includes('/download')) {
console.log('[VIDEO] Trying /preview URL for Firefox compatibility'); console.log('[VIDEO] Trying /preview URL for Firefox compatibility');
video.src = video.src.replace('/download', '/preview'); video.src = video.src.replace('/download', '/preview');

View File

@ -1,4 +1,4 @@
// src/pages/api/auth/login.ts (ENHANCED - Consistent cookie handling) // src/pages/api/auth/login.ts
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { generateAuthUrl, generateState, logAuthEvent } from '../../../utils/auth.js'; import { generateAuthUrl, generateState, logAuthEvent } from '../../../utils/auth.js';
import { serialize } from 'cookie'; import { serialize } from 'cookie';
@ -18,7 +18,6 @@ export const GET: APIRoute = async ({ url, redirect }) => {
const stateData = JSON.stringify({ state, returnTo }); const stateData = JSON.stringify({ state, returnTo });
// Use consistent cookie serialization (same as session cookies)
const publicBaseUrl = process.env.PUBLIC_BASE_URL || ''; const publicBaseUrl = process.env.PUBLIC_BASE_URL || '';
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production';
const isSecure = publicBaseUrl.startsWith('https://') || isProduction; const isSecure = publicBaseUrl.startsWith('https://') || isProduction;

View File

@ -1,4 +1,4 @@
// src/pages/api/auth/process.ts (ENHANCED - Proper auth success indication) // src/pages/api/auth/process.ts
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { import {
verifyAuthState, verifyAuthState,
@ -49,7 +49,6 @@ export const POST: APIRoute = async ({ request }) => {
email: sessionResult.userEmail email: sessionResult.userEmail
}); });
// Add auth success indicator to the return URL
const returnUrl = new URL(stateVerification.stateData.returnTo, request.url); const returnUrl = new URL(stateVerification.stateData.returnTo, request.url);
returnUrl.searchParams.set('auth', 'success'); returnUrl.searchParams.set('auth', 'success');
const redirectUrl = returnUrl.toString(); const redirectUrl = returnUrl.toString();

View File

@ -9,16 +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 const gatedContentAuth = await withAPIAuth(request, 'gatedcontent');
return apiResponse.success({ return apiResponse.success({
authenticated: contributionAuth.authenticated || aiAuth.authenticated || gatedContentAuth.authenticated, authenticated: contributionAuth.authenticated || aiAuth.authenticated || gatedContentAuth.authenticated,
contributionAuthRequired: contributionAuth.authRequired, contributionAuthRequired: contributionAuth.authRequired,
aiAuthRequired: aiAuth.authRequired, aiAuthRequired: aiAuth.authRequired,
gatedContentAuthRequired: gatedContentAuth.authRequired, // ADDED gatedContentAuthRequired: gatedContentAuth.authRequired,
contributionAuthenticated: contributionAuth.authenticated, contributionAuthenticated: contributionAuth.authenticated,
aiAuthenticated: aiAuth.authenticated, aiAuthenticated: aiAuth.authenticated,
gatedContentAuthenticated: gatedContentAuth.authenticated, // ADDED gatedContentAuthenticated: gatedContentAuth.authenticated,
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');

View File

@ -1,4 +1,4 @@
// src/pages/api/contribute/knowledgebase.ts - SIMPLIFIED: Issues only, minimal validation // src/pages/api/contribute/knowledgebase.ts
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js'; import { withAPIAuth } from '../../../utils/auth.js';
import { apiResponse, apiError, apiServerError, handleAPIRequest } from '../../../utils/api.js'; import { apiResponse, apiError, apiServerError, handleAPIRequest } from '../../../utils/api.js';

View File

@ -1,4 +1,4 @@
// src/pages/api/contribute/tool.ts (UPDATED - Using consolidated API responses + related_software) // src/pages/api/contribute/tool.ts
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js'; import { withAPIAuth } from '../../../utils/auth.js';
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js'; import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
@ -82,31 +82,27 @@ function sanitizeInput(obj: any): any {
} }
function preprocessFormData(body: any): any { function preprocessFormData(body: any): any {
// Handle comma-separated strings from autocomplete inputs
if (body.tool) { if (body.tool) {
// Handle tags
if (typeof body.tool.tags === 'string') { if (typeof body.tool.tags === 'string') {
body.tool.tags = body.tool.tags.split(',').map((t: string) => t.trim()).filter(Boolean); body.tool.tags = body.tool.tags.split(',').map((t: string) => t.trim()).filter(Boolean);
} }
// Handle related concepts
if (body.tool.relatedConcepts) { if (body.tool.relatedConcepts) {
if (typeof body.tool.relatedConcepts === 'string') { if (typeof body.tool.relatedConcepts === 'string') {
body.tool.related_concepts = body.tool.relatedConcepts.split(',').map((t: string) => t.trim()).filter(Boolean); body.tool.related_concepts = body.tool.relatedConcepts.split(',').map((t: string) => t.trim()).filter(Boolean);
} else { } else {
body.tool.related_concepts = body.tool.relatedConcepts; body.tool.related_concepts = body.tool.relatedConcepts;
} }
delete body.tool.relatedConcepts; // Remove the original key delete body.tool.relatedConcepts;
} }
// Handle related software
if (body.tool.relatedSoftware) { if (body.tool.relatedSoftware) {
if (typeof body.tool.relatedSoftware === 'string') { if (typeof body.tool.relatedSoftware === 'string') {
body.tool.related_software = body.tool.relatedSoftware.split(',').map((t: string) => t.trim()).filter(Boolean); body.tool.related_software = body.tool.relatedSoftware.split(',').map((t: string) => t.trim()).filter(Boolean);
} else { } else {
body.tool.related_software = body.tool.relatedSoftware; body.tool.related_software = body.tool.relatedSoftware;
} }
delete body.tool.relatedSoftware; // Remove the original key delete body.tool.relatedSoftware;
} }
} }
@ -142,14 +138,11 @@ async function validateToolData(tool: any, action: string): Promise<{ valid: boo
} }
} }
// Validate related items exist (optional validation - could be enhanced)
if (tool.related_concepts && tool.related_concepts.length > 0) { if (tool.related_concepts && tool.related_concepts.length > 0) {
// Could validate that referenced concepts actually exist
console.log('[VALIDATION] Related concepts provided:', tool.related_concepts); console.log('[VALIDATION] Related concepts provided:', tool.related_concepts);
} }
if (tool.related_software && tool.related_software.length > 0) { if (tool.related_software && tool.related_software.length > 0) {
// Could validate that referenced software actually exists
console.log('[VALIDATION] Related software provided:', tool.related_software); console.log('[VALIDATION] Related software provided:', tool.related_software);
} }

View File

@ -35,7 +35,6 @@ export const POST: APIRoute = async ({ request }) => {
); );
} }
/* --- (rest of the handler unchanged) -------------------------- */
const { embeddingsService } = await import('../../../utils/embeddings.js'); const { embeddingsService } = await import('../../../utils/embeddings.js');
if (!embeddingsService.isEnabled()) { if (!embeddingsService.isEnabled()) {

View File

@ -23,7 +23,6 @@ const editToolName = Astro.url.searchParams.get('edit');
const editTool = editToolName ? existingTools.find(tool => tool.name === editToolName) : null; const editTool = editToolName ? existingTools.find(tool => tool.name === editToolName) : null;
const isEdit = !!editTool; const isEdit = !!editTool;
// Extract data for autocomplete
const allTags = [...new Set(existingTools.flatMap(tool => tool.tags || []))].sort(); const allTags = [...new Set(existingTools.flatMap(tool => tool.tags || []))].sort();
const allSoftwareAndMethods = existingTools const allSoftwareAndMethods = existingTools
.filter(tool => tool.type === 'software' || tool.type === 'method') .filter(tool => tool.type === 'software' || tool.type === 'method')
@ -300,7 +299,6 @@ const allConcepts = existingTools
</BaseLayout> </BaseLayout>
<script define:vars={{ isEdit, editTool, domains, phases, domainAgnosticSoftware, allTags, allSoftwareAndMethods, allConcepts }}> <script define:vars={{ isEdit, editTool, domains, phases, domainAgnosticSoftware, allTags, allSoftwareAndMethods, allConcepts }}>
// Consolidated Autocomplete Functionality - inlined to avoid module loading issues
class AutocompleteManager { class AutocompleteManager {
constructor(inputElement, dataSource, options = {}) { constructor(inputElement, dataSource, options = {}) {
this.input = inputElement; this.input = inputElement;
@ -337,7 +335,6 @@ class AutocompleteManager {
this.dropdown = document.createElement('div'); this.dropdown = document.createElement('div');
this.dropdown.className = 'autocomplete-dropdown'; this.dropdown.className = 'autocomplete-dropdown';
// Insert dropdown after input
this.input.parentNode.style.position = 'relative'; this.input.parentNode.style.position = 'relative';
this.input.parentNode.insertBefore(this.dropdown, this.input.nextSibling); this.input.parentNode.insertBefore(this.dropdown, this.input.nextSibling);
} }
@ -358,7 +355,6 @@ class AutocompleteManager {
}); });
this.input.addEventListener('blur', (e) => { this.input.addEventListener('blur', (e) => {
// Delay to allow click events on dropdown items
setTimeout(() => { setTimeout(() => {
if (!this.dropdown.contains(document.activeElement)) { if (!this.dropdown.contains(document.activeElement)) {
this.hideDropdown(); this.hideDropdown();
@ -450,7 +446,6 @@ class AutocompleteManager {
}) })
.join(''); .join('');
// Bind click events
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => { this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
option.addEventListener('click', () => { option.addEventListener('click', () => {
this.selectItem(this.filteredData[index]); this.selectItem(this.filteredData[index]);
@ -484,7 +479,6 @@ class AutocompleteManager {
this.hideDropdown(); this.hideDropdown();
} }
// Trigger change event
this.input.dispatchEvent(new CustomEvent('autocomplete:select', { this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
detail: { item, text, selectedItems: Array.from(this.selectedItems) } detail: { item, text, selectedItems: Array.from(this.selectedItems) }
})); }));
@ -510,7 +504,6 @@ class AutocompleteManager {
`) `)
.join(''); .join('');
// Bind remove events
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => { this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
@ -636,7 +629,6 @@ class ContributionForm {
} }
setupAutocomplete() { setupAutocomplete() {
// Tags autocomplete
if (this.elements.tagsInput && this.elements.tagsHidden) { if (this.elements.tagsInput && this.elements.tagsHidden) {
const tagsManager = new AutocompleteManager(this.elements.tagsInput, allTags, { const tagsManager = new AutocompleteManager(this.elements.tagsInput, allTags, {
allowMultiple: true, allowMultiple: true,
@ -644,7 +636,6 @@ class ContributionForm {
placeholder: 'Beginne zu tippen, um Tags hinzuzufügen...' placeholder: 'Beginne zu tippen, um Tags hinzuzufügen...'
}); });
// Set initial values if editing
if (this.editTool?.tags) { if (this.editTool?.tags) {
tagsManager.setSelectedItems(this.editTool.tags); tagsManager.setSelectedItems(this.editTool.tags);
} }
@ -652,7 +643,6 @@ class ContributionForm {
this.autocompleteManagers.set('tags', tagsManager); this.autocompleteManagers.set('tags', tagsManager);
} }
// Related concepts autocomplete
if (this.elements.relatedConceptsInput && this.elements.relatedConceptsHidden) { if (this.elements.relatedConceptsInput && this.elements.relatedConceptsHidden) {
const conceptsManager = new AutocompleteManager(this.elements.relatedConceptsInput, allConcepts, { const conceptsManager = new AutocompleteManager(this.elements.relatedConceptsInput, allConcepts, {
allowMultiple: true, allowMultiple: true,
@ -660,7 +650,6 @@ class ContributionForm {
placeholder: 'Beginne zu tippen, um Konzepte zu finden...' placeholder: 'Beginne zu tippen, um Konzepte zu finden...'
}); });
// Set initial values if editing
if (this.editTool?.related_concepts) { if (this.editTool?.related_concepts) {
conceptsManager.setSelectedItems(this.editTool.related_concepts); conceptsManager.setSelectedItems(this.editTool.related_concepts);
} }
@ -668,7 +657,6 @@ class ContributionForm {
this.autocompleteManagers.set('relatedConcepts', conceptsManager); this.autocompleteManagers.set('relatedConcepts', conceptsManager);
} }
// Related software autocomplete
if (this.elements.relatedSoftwareInput && this.elements.relatedSoftwareHidden) { if (this.elements.relatedSoftwareInput && this.elements.relatedSoftwareHidden) {
const softwareManager = new AutocompleteManager(this.elements.relatedSoftwareInput, allSoftwareAndMethods, { const softwareManager = new AutocompleteManager(this.elements.relatedSoftwareInput, allSoftwareAndMethods, {
allowMultiple: true, allowMultiple: true,
@ -676,7 +664,6 @@ class ContributionForm {
placeholder: 'Beginne zu tippen, um Software/Methoden zu finden...' placeholder: 'Beginne zu tippen, um Software/Methoden zu finden...'
}); });
// Set initial values if editing
if (this.editTool?.related_software) { if (this.editTool?.related_software) {
softwareManager.setSelectedItems(this.editTool.related_software); softwareManager.setSelectedItems(this.editTool.related_software);
} }
@ -684,7 +671,6 @@ class ContributionForm {
this.autocompleteManagers.set('relatedSoftware', softwareManager); this.autocompleteManagers.set('relatedSoftware', softwareManager);
} }
// Listen for autocomplete changes to update YAML preview
Object.values(this.autocompleteManagers).forEach(manager => { Object.values(this.autocompleteManagers).forEach(manager => {
if (manager.input) { if (manager.input) {
manager.input.addEventListener('autocomplete:select', () => { manager.input.addEventListener('autocomplete:select', () => {
@ -726,14 +712,10 @@ class ContributionForm {
updateFieldVisibility() { updateFieldVisibility() {
const type = this.elements.typeSelect.value; const type = this.elements.typeSelect.value;
// Only hide/show software-specific fields (platforms, license)
// Relations should always be visible since all tool types can have relationships
this.elements.softwareFields.style.display = type === 'software' ? 'block' : 'none'; this.elements.softwareFields.style.display = type === 'software' ? 'block' : 'none';
// Always show relations - all tool types can have relationships
this.elements.relationsFields.style.display = 'block'; this.elements.relationsFields.style.display = 'block';
// Only mark platform/license as required for software
if (this.elements.platformsRequired) { if (this.elements.platformsRequired) {
this.elements.platformsRequired.style.display = type === 'software' ? 'inline' : 'none'; this.elements.platformsRequired.style.display = type === 'software' ? 'inline' : 'none';
} }
@ -741,7 +723,6 @@ class ContributionForm {
this.elements.licenseRequired.style.display = type === 'software' ? 'inline' : 'none'; this.elements.licenseRequired.style.display = type === 'software' ? 'inline' : 'none';
} }
// Always show both relation sections - let users decide what's relevant
const conceptsSection = document.getElementById('related-concepts-section'); const conceptsSection = document.getElementById('related-concepts-section');
const softwareSection = document.getElementById('related-software-section'); const softwareSection = document.getElementById('related-software-section');
if (conceptsSection) conceptsSection.style.display = 'block'; if (conceptsSection) conceptsSection.style.display = 'block';
@ -806,19 +787,16 @@ class ContributionForm {
tool.knowledgebase = true; tool.knowledgebase = true;
} }
// Handle tags from autocomplete
const tagsValue = this.elements.tagsHidden?.value || ''; const tagsValue = this.elements.tagsHidden?.value || '';
if (tagsValue) { if (tagsValue) {
tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean); tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
} }
// Handle related concepts from autocomplete
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || ''; const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
if (relatedConceptsValue) { if (relatedConceptsValue) {
tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean); tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
} }
// Handle related software from autocomplete
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || ''; const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
if (relatedSoftwareValue) { if (relatedSoftwareValue) {
tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean); tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
@ -983,19 +961,16 @@ class ContributionForm {
} }
}; };
// Handle tags from autocomplete
const tagsValue = this.elements.tagsHidden?.value || ''; const tagsValue = this.elements.tagsHidden?.value || '';
if (tagsValue) { if (tagsValue) {
submission.tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean); submission.tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
} }
// Handle related concepts from autocomplete
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || ''; const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
if (relatedConceptsValue) { if (relatedConceptsValue) {
submission.tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean); submission.tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
} }
// Handle related software from autocomplete
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || ''; const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
if (relatedSoftwareValue) { if (relatedSoftwareValue) {
submission.tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean); submission.tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
@ -1072,7 +1047,6 @@ class ContributionForm {
} }
destroy() { destroy() {
// Clean up autocomplete managers
this.autocompleteManagers.forEach(manager => { this.autocompleteManagers.forEach(manager => {
manager.destroy(); manager.destroy();
}); });

View File

@ -686,8 +686,6 @@ if (aiAuthRequired) {
window.switchToAIView = () => switchToView('ai'); window.switchToAIView = () => switchToView('ai');
window.switchToView = switchToView; window.switchToView = switchToView;
// CRITICAL: Handle shared URLs AFTER everything is set up
// Increased timeout to ensure all components and utility functions are loaded
setTimeout(() => { setTimeout(() => {
handleSharedURL(); handleSharedURL();
}, 1000); }, 1000);

View File

@ -10,7 +10,6 @@ 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 gatedContentAuthEnabled = isGatedContentAuthRequired();
const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => { const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
@ -27,8 +26,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 gated_content: entry.data.gated_content || false,
tool_name: entry.data.tool_name, tool_name: entry.data.tool_name,
related_tools: entry.data.related_tools || [], related_tools: entry.data.related_tools || [],
associatedTool, associatedTool,
@ -45,7 +43,6 @@ 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 gatedCount = knowledgebaseEntries.filter(entry => entry.gated_content).length;
const publicCount = knowledgebaseEntries.length - gatedCount; const publicCount = knowledgebaseEntries.length - gatedCount;
--- ---

View File

@ -21,7 +21,6 @@ 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 isGatedContent = entry.data.gated_content === true;
const gatedContentAuthRequired = isGatedContentAuthRequired(); const gatedContentAuthRequired = isGatedContentAuthRequired();
const requiresAuth = isGatedContent && gatedContentAuthRequired; const requiresAuth = isGatedContent && gatedContentAuthRequired;
@ -62,17 +61,14 @@ const currentUrl = Astro.url.href;
<BaseLayout title={entry.data.title} description={entry.data.description}> <BaseLayout title={entry.data.title} description={entry.data.description}>
{requiresAuth && ( {requiresAuth && (
<script define:vars={{ requiresAuth, articleTitle: entry.data.title }}> <script define:vars={{ requiresAuth, articleTitle: entry.data.title }}>
// Enhanced client-side authentication check for gated content with improved error handling
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
if (!requiresAuth) return; if (!requiresAuth) return;
console.log('[GATED CONTENT] Checking client-side auth for: ' + articleTitle); console.log('[GATED CONTENT] Checking client-side auth for: ' + articleTitle);
// Check for auth success indicator in URL (from callback)
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const authSuccess = urlParams.get('auth') === 'success'; const authSuccess = urlParams.get('auth') === 'success';
// Hide content immediately while checking auth
const contentArea = document.querySelector('.article-content'); const contentArea = document.querySelector('.article-content');
const sidebar = document.querySelector('.article-sidebar'); const sidebar = document.querySelector('.article-sidebar');
@ -80,12 +76,10 @@ const currentUrl = Astro.url.href;
contentArea.style.display = 'none'; contentArea.style.display = 'none';
} }
// If this is a redirect from successful auth, wait a bit for session to be available
if (authSuccess) { if (authSuccess) {
console.log('[GATED CONTENT] Auth success detected, waiting for session...'); console.log('[GATED CONTENT] Auth success detected, waiting for session...');
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
// Clean the URL
const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname; const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl); window.history.replaceState({}, document.title, cleanUrl);
} }
@ -102,7 +96,6 @@ const currentUrl = Astro.url.href;
if (authRequired && !isAuthenticated) { if (authRequired && !isAuthenticated) {
console.log('[GATED CONTENT] Access denied - showing auth required message: ' + articleTitle); console.log('[GATED CONTENT] Access denied - showing auth required message: ' + articleTitle);
// Show authentication required message
if (contentArea) { if (contentArea) {
const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href); const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href);
contentArea.innerHTML = [ contentArea.innerHTML = [
@ -130,11 +123,9 @@ const currentUrl = Astro.url.href;
} }
} 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) { if (contentArea) {
contentArea.style.display = 'block'; contentArea.style.display = 'block';
} }
// Generate TOC for authenticated users
setTimeout(() => { setTimeout(() => {
if (typeof generateTOCContent === 'function') { if (typeof generateTOCContent === 'function') {
generateTOCContent(); generateTOCContent();
@ -143,7 +134,6 @@ const currentUrl = Astro.url.href;
} }
} 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 with retry option
if (requiresAuth && contentArea) { if (requiresAuth && contentArea) {
const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href); const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href);
contentArea.innerHTML = [ contentArea.innerHTML = [
@ -411,13 +401,10 @@ const currentUrl = Astro.url.href;
} }
function generateSidebarTOC() { function generateSidebarTOC() {
// Only generate TOC if not gated content OR user is authenticated
if (requiresAuth) { if (requiresAuth) {
// For gated content, TOC will be generated by the auth check script
return; return;
} }
// For non-gated content, generate TOC normally
generateTOCContent(); generateTOCContent();
} }
@ -523,17 +510,14 @@ const currentUrl = Astro.url.href;
pre.dataset.copyEnhanced = 'true'; pre.dataset.copyEnhanced = 'true';
pre.style.position ||= 'relative'; pre.style.position ||= 'relative';
// Try to find an existing copy button we can reuse
let btn = let btn =
pre.querySelector('.copy-btn') || // our class pre.querySelector('.copy-btn') ||
pre.querySelector('.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]'); pre.querySelector('.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]');
// If there is an "old" button that is NOT ours, prefer to reuse it by giving it our class.
if (btn && !btn.classList.contains('copy-btn')) { if (btn && !btn.classList.contains('copy-btn')) {
btn.classList.add('copy-btn'); btn.classList.add('copy-btn');
} }
// If no button at all, create one
if (!btn) { if (!btn) {
btn = document.createElement('button'); btn = document.createElement('button');
btn.type = 'button'; btn.type = 'button';
@ -548,7 +532,6 @@ const currentUrl = Astro.url.href;
pre.appendChild(btn); pre.appendChild(btn);
} }
// If there is a SECOND old button lingering (top-left in your case), hide it
const possibleOldButtons = pre.querySelectorAll( const possibleOldButtons = pre.querySelectorAll(
'.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]' '.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]'
); );
@ -556,7 +539,6 @@ const currentUrl = Astro.url.href;
if (b !== btn) b.style.display = 'none'; if (b !== btn) b.style.display = 'none';
}); });
// Success pill
if (!pre.querySelector('.copied-pill')) { if (!pre.querySelector('.copied-pill')) {
const pill = document.createElement('div'); const pill = document.createElement('div');
pill.className = 'copied-pill'; pill.className = 'copied-pill';
@ -564,7 +546,6 @@ const currentUrl = Astro.url.href;
pre.appendChild(pill); pre.appendChild(pill);
} }
// Screen reader live region
if (!pre.querySelector('.sr-live')) { if (!pre.querySelector('.sr-live')) {
const live = document.createElement('div'); const live = document.createElement('div');
live.className = 'sr-live'; live.className = 'sr-live';

View File

@ -1083,7 +1083,6 @@ class ImprovedMicroTaskAIPipeline {
return; return;
} }
// Step 1: AI selection of tools for completion
const selectionPrompt = AI_PROMPTS.generatePhaseCompletionPrompt(originalQuery, phase, phaseTools, phaseConcepts); const selectionPrompt = AI_PROMPTS.generatePhaseCompletionPrompt(originalQuery, phase, phaseTools, phaseConcepts);
const selectionResult = await this.callMicroTaskAI(selectionPrompt, context, 800); const selectionResult = await this.callMicroTaskAI(selectionPrompt, context, 800);
@ -1108,7 +1107,6 @@ class ImprovedMicroTaskAIPipeline {
return; return;
} }
// Step 2: Generate detailed reasoning for each selected tool
for (const tool of validTools) { for (const tool of validTools) {
console.log('[AI-PIPELINE] Generating reasoning for phase completion tool:', tool.name); console.log('[AI-PIPELINE] Generating reasoning for phase completion tool:', tool.name);

View File

@ -1,4 +1,4 @@
// src/utils/auth.js (ENHANCED - Added gated content support) // src/utils/auth.js
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';
@ -390,12 +390,10 @@ 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 { export function isGatedContentAuthRequired(): boolean {
return getAuthRequirement('gatedcontent'); return getAuthRequirement('gatedcontent');
} }
// NEW: Check if specific content should be gated
export function shouldGateContent(isGatedContent: boolean): boolean { export function shouldGateContent(isGatedContent: boolean): boolean {
return isGatedContent && isGatedContentAuthRequired(); return isGatedContent && isGatedContentAuthRequired();
} }

View File

@ -1,9 +1,5 @@
// src/utils/clientUtils.ts // src/utils/clientUtils.ts
// MINIMAL utilities that don't conflict with BaseLayout.astro or env.d.ts
// ============================================================================
// CORE TOOL UTILITIES (shared between client and server)
// ============================================================================
export function createToolSlug(toolName: string): string { export function createToolSlug(toolName: string): string {
if (!toolName || typeof toolName !== 'string') { if (!toolName || typeof toolName !== 'string') {
@ -12,10 +8,10 @@ export function createToolSlug(toolName: string): string {
} }
return toolName.toLowerCase() return toolName.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters .replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-') // Replace spaces with hyphens .replace(/\s+/g, '-')
.replace(/-+/g, '-') // Remove duplicate hyphens .replace(/-+/g, '-')
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens .replace(/^-|-$/g, '');
} }
export function findToolByIdentifier(tools: any[], identifier: string): any | undefined { export function findToolByIdentifier(tools: any[], identifier: string): any | undefined {
@ -34,10 +30,6 @@ export function isToolHosted(tool: any): boolean {
tool.projectUrl.trim() !== ""; tool.projectUrl.trim() !== "";
} }
// ============================================================================
// AUTOCOMPLETE FUNCTIONALITY (keep this here since it's complex)
// ============================================================================
interface AutocompleteOptions { interface AutocompleteOptions {
minLength?: number; minLength?: number;
maxResults?: number; maxResults?: number;
@ -104,7 +96,6 @@ export class AutocompleteManager {
display: none; display: none;
`; `;
// Insert dropdown after input
const parentElement = this.input.parentNode as HTMLElement; const parentElement = this.input.parentNode as HTMLElement;
parentElement.style.position = 'relative'; parentElement.style.position = 'relative';
parentElement.insertBefore(this.dropdown, this.input.nextSibling); parentElement.insertBefore(this.dropdown, this.input.nextSibling);
@ -126,7 +117,6 @@ export class AutocompleteManager {
}); });
this.input.addEventListener('blur', () => { this.input.addEventListener('blur', () => {
// Delay to allow click events on dropdown items
setTimeout(() => { setTimeout(() => {
const activeElement = document.activeElement; const activeElement = document.activeElement;
if (!activeElement || !this.dropdown.contains(activeElement)) { if (!activeElement || !this.dropdown.contains(activeElement)) {
@ -233,7 +223,6 @@ export class AutocompleteManager {
}) })
.join(''); .join('');
// Bind click events
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => { this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
option.addEventListener('click', () => { option.addEventListener('click', () => {
this.selectItem(this.filteredData[index]); this.selectItem(this.filteredData[index]);
@ -267,7 +256,6 @@ export class AutocompleteManager {
this.hideDropdown(); this.hideDropdown();
} }
// Trigger change event
this.input.dispatchEvent(new CustomEvent('autocomplete:select', { this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
detail: { item, text, selectedItems: Array.from(this.selectedItems) } detail: { item, text, selectedItems: Array.from(this.selectedItems) }
})); }));
@ -314,7 +302,6 @@ export class AutocompleteManager {
`) `)
.join(''); .join('');
// Bind remove events
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => { this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();

View File

@ -1,19 +1,14 @@
// src/utils/remarkVideoPlugin.ts - Simple, working approach // src/utils/remarkVideoPlugin.ts
import { visit } from 'unist-util-visit'; import { visit } from 'unist-util-visit';
import type { Plugin } from 'unified'; import type { Plugin } from 'unified';
import type { Root } from 'hast'; import type { Root } from 'hast';
/**
* Simple video plugin - just makes videos responsive and adds /download to bare Nextcloud URLs
* No CORS complications, no crossorigin attributes
*/
export const remarkVideoPlugin: Plugin<[], Root> = () => { export const remarkVideoPlugin: Plugin<[], Root> = () => {
return (tree: Root) => { return (tree: Root) => {
// Find HTML nodes containing <video> tags
visit(tree, 'html', (node: any, index: number | undefined, parent: any) => { visit(tree, 'html', (node: any, index: number | undefined, parent: any) => {
if (node.value && node.value.includes('<video') && typeof index === 'number') { if (node.value && node.value.includes('<video') && typeof index === 'number') {
// Extract video attributes
const srcMatch = node.value.match(/src=["']([^"']+)["']/); const srcMatch = node.value.match(/src=["']([^"']+)["']/);
const titleMatch = node.value.match(/title=["']([^"']+)["']/); const titleMatch = node.value.match(/title=["']([^"']+)["']/);
@ -21,17 +16,14 @@ export const remarkVideoPlugin: Plugin<[], Root> = () => {
const originalSrc = srcMatch[1]; const originalSrc = srcMatch[1];
const title = titleMatch?.[1] || 'Video'; const title = titleMatch?.[1] || 'Video';
// Smart URL processing - add /download to bare Nextcloud URLs
const finalSrc = processNextcloudUrl(originalSrc); const finalSrc = processNextcloudUrl(originalSrc);
// Check for existing attributes to preserve them
const hasControls = node.value.includes('controls'); const hasControls = node.value.includes('controls');
const hasAutoplay = node.value.includes('autoplay'); const hasAutoplay = node.value.includes('autoplay');
const hasMuted = node.value.includes('muted'); const hasMuted = node.value.includes('muted');
const hasLoop = node.value.includes('loop'); const hasLoop = node.value.includes('loop');
const hasPreload = node.value.match(/preload=["']([^"']+)["']/); const hasPreload = node.value.match(/preload=["']([^"']+)["']/);
// Create simple, working video HTML - NO crossorigin attribute
const enhancedHTML = ` const enhancedHTML = `
<div class="video-container aspect-16-9"> <div class="video-container aspect-16-9">
<video <video
@ -55,7 +47,6 @@ export const remarkVideoPlugin: Plugin<[], Root> = () => {
</div> </div>
`.trim(); `.trim();
// Replace the node
parent.children[index] = { type: 'html', value: enhancedHTML }; parent.children[index] = { type: 'html', value: enhancedHTML };
console.log(`[VIDEO] Processed: ${title}`); console.log(`[VIDEO] Processed: ${title}`);
@ -66,25 +57,17 @@ export const remarkVideoPlugin: Plugin<[], Root> = () => {
}; };
}; };
/**
* Simple URL processing - just add /download to bare Nextcloud URLs if needed
*/
function processNextcloudUrl(originalUrl: string): string { function processNextcloudUrl(originalUrl: string): string {
// If it's a bare Nextcloud share URL, add /download
if (isNextcloudShareUrl(originalUrl) && !originalUrl.includes('/download')) { if (isNextcloudShareUrl(originalUrl) && !originalUrl.includes('/download')) {
const downloadUrl = `${originalUrl}/download`; const downloadUrl = `${originalUrl}/download`;
console.log(`[VIDEO] Auto-added /download: ${originalUrl}${downloadUrl}`); console.log(`[VIDEO] Auto-added /download: ${originalUrl}${downloadUrl}`);
return downloadUrl; return downloadUrl;
} }
// Otherwise, use the URL as-is
return originalUrl; return originalUrl;
} }
/**
* Check if URL is a Nextcloud share URL (bare or with /download)
* Format: https://cloud.cc24.dev/s/TOKEN
*/
function isNextcloudShareUrl(url: string): boolean { function isNextcloudShareUrl(url: string): boolean {
const pattern = /\/s\/[a-zA-Z0-9]+/; const pattern = /\/s\/[a-zA-Z0-9]+/;
return pattern.test(url) && (url.includes('nextcloud') || url.includes('cloud.')); return pattern.test(url) && (url.includes('nextcloud') || url.includes('cloud.'));

View File

@ -1,5 +1,4 @@
// src/utils/toolHelpers.ts - CONSOLIDATED to remove code duplication // src/utils/toolHelpers.ts
// Re-export functions from clientUtils to avoid duplication
export interface Tool { export interface Tool {
name: string; name: string;
@ -16,7 +15,6 @@ export interface Tool {
related_concepts?: string[]; related_concepts?: string[];
} }
// CONSOLIDATED: Import shared utilities instead of duplicating
export { export {
createToolSlug, createToolSlug,
findToolByIdentifier, findToolByIdentifier,

View File

@ -1,19 +1,12 @@
// src/utils/videoUtils.ts - SIMPLIFIED - Basic utilities only // src/utils/videoUtils.ts - SIMPLIFIED - Basic utilities only
import 'dotenv/config'; import 'dotenv/config';
/**
* Simple video utilities for basic video support
* No caching, no complex processing, no authentication
*/
export interface SimpleVideoMetadata { export interface SimpleVideoMetadata {
title?: string; title?: string;
description?: string; description?: string;
} }
/**
* Get video MIME type from file extension
*/
export function getVideoMimeType(url: string): string { export function getVideoMimeType(url: string): string {
let extension: string | undefined; let extension: string | undefined;
try { try {
@ -37,9 +30,6 @@ export function getVideoMimeType(url: string): string {
return (extension && mimeTypes[extension]) || 'video/mp4'; return (extension && mimeTypes[extension]) || 'video/mp4';
} }
/**
* Format duration in MM:SS or HH:MM:SS format
*/
export function formatDuration(seconds: number): string { export function formatDuration(seconds: number): string {
const hours = Math.floor(seconds / 3600); const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60); const minutes = Math.floor((seconds % 3600) / 60);
@ -52,9 +42,6 @@ export function formatDuration(seconds: number): string {
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
} }
/**
* Format file size in human readable format
*/
export function formatFileSize(bytes: number): string { export function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`; if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@ -62,9 +49,6 @@ export function formatFileSize(bytes: number): string {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
} }
/**
* Escape HTML for safe output
*/
export function escapeHtml(unsafe: string): string { export function escapeHtml(unsafe: string): string {
if (typeof unsafe !== 'string') return ''; if (typeof unsafe !== 'string') return '';
@ -76,9 +60,6 @@ export function escapeHtml(unsafe: string): string {
.replace(/'/g, "&#039;"); .replace(/'/g, "&#039;");
} }
/**
* Generate basic responsive video HTML
*/
export function generateVideoHTML( export function generateVideoHTML(
src: string, src: string,
options: { options: {