385 lines
13 KiB
Plaintext
385 lines
13 KiB
Plaintext
---
|
|
import Navigation from '../components/Navigation.astro';
|
|
import Footer from '../components/Footer.astro';
|
|
import '../styles/global.css';
|
|
import '../styles/auditTrail.css';
|
|
import '../styles/knowledgebase.css';
|
|
import '../styles/palette.css';
|
|
import '../styles/autocomplete.css';
|
|
|
|
export interface Props {
|
|
title: string;
|
|
description?: string;
|
|
}
|
|
|
|
const { title, description = 'ForensicPathways - A comprehensive directory of digital forensics and incident response tools' } = Astro.props;
|
|
---
|
|
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="description" content={description}>
|
|
<title>{title} - ForensicPathways</title>
|
|
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
|
|
|
<script>
|
|
async function loadUtilityFunctions() {
|
|
try {
|
|
const { createToolSlug, findToolByIdentifier, isToolHosted } = await import('../utils/clientUtils.js');
|
|
|
|
(window as any).createToolSlug = createToolSlug;
|
|
(window as any).findToolByIdentifier = findToolByIdentifier;
|
|
(window as any).isToolHosted = isToolHosted;
|
|
|
|
console.log('[UTILS] Utility functions loaded successfully');
|
|
} catch (error) {
|
|
console.error('Failed to load utility functions:', error);
|
|
|
|
(window as any).createToolSlug = (toolName: string) => {
|
|
if (!toolName || typeof toolName !== 'string') return '';
|
|
return toolName.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
};
|
|
|
|
(window as any).findToolByIdentifier = (tools: any[], identifier: string) => {
|
|
if (!identifier || !Array.isArray(tools)) return undefined;
|
|
return tools.find((tool: any) =>
|
|
tool.name === identifier ||
|
|
(window as any).createToolSlug(tool.name) === identifier.toLowerCase()
|
|
);
|
|
};
|
|
|
|
(window as any).isToolHosted = (tool: any) => {
|
|
return tool.projectUrl !== undefined &&
|
|
tool.projectUrl !== null &&
|
|
tool.projectUrl !== "" &&
|
|
tool.projectUrl.trim() !== "";
|
|
};
|
|
|
|
console.log('[UTILS] Fallback utility functions registered');
|
|
}
|
|
}
|
|
|
|
function scrollToElement(element: Element | null, options = {}) {
|
|
if (!element) return;
|
|
|
|
setTimeout(() => {
|
|
const headerHeight = document.querySelector('nav')?.offsetHeight || 80;
|
|
const elementRect = element.getBoundingClientRect();
|
|
const absoluteElementTop = elementRect.top + window.pageYOffset;
|
|
const targetPosition = absoluteElementTop - headerHeight - 20;
|
|
|
|
window.scrollTo({
|
|
top: targetPosition,
|
|
behavior: 'smooth'
|
|
});
|
|
}, 100);
|
|
}
|
|
|
|
function scrollToElementById(elementId: string, options = {}) {
|
|
const element = document.getElementById(elementId);
|
|
if (element) {
|
|
scrollToElement(element, options);
|
|
}
|
|
}
|
|
|
|
function scrollToElementBySelector(selector: string, options = {}) {
|
|
const element = document.querySelector(selector);
|
|
if (element) {
|
|
scrollToElement(element, options);
|
|
}
|
|
}
|
|
|
|
function prioritizeSearchResults(tools: any[], searchTerm: string) {
|
|
if (!searchTerm || !searchTerm.trim()) {
|
|
return tools;
|
|
}
|
|
|
|
const lowerSearchTerm = searchTerm.toLowerCase().trim();
|
|
|
|
return tools.sort((a, b) => {
|
|
const aTagsLower = (a.tags || []).map((tag: string) => tag.toLowerCase());
|
|
const bTagsLower = (b.tags || []).map((tag: string) => tag.toLowerCase());
|
|
|
|
const aExactTag = aTagsLower.includes(lowerSearchTerm);
|
|
const bExactTag = bTagsLower.includes(lowerSearchTerm);
|
|
|
|
if (aExactTag && !bExactTag) return -1;
|
|
if (!aExactTag && bExactTag) return 1;
|
|
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
}
|
|
|
|
(window as any).scrollToElement = scrollToElement;
|
|
(window as any).scrollToElementById = scrollToElementById;
|
|
(window as any).scrollToElementBySelector = scrollToElementBySelector;
|
|
(window as any).prioritizeSearchResults = prioritizeSearchResults;
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
await loadUtilityFunctions();
|
|
|
|
const THEME_KEY = 'dfir-theme';
|
|
|
|
function getSystemTheme() {
|
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
}
|
|
|
|
function getStoredTheme() {
|
|
return localStorage.getItem(THEME_KEY) || 'auto';
|
|
}
|
|
|
|
function applyTheme(theme: string) {
|
|
const effectiveTheme = theme === 'auto' ? getSystemTheme() : theme;
|
|
document.documentElement.setAttribute('data-theme', effectiveTheme);
|
|
}
|
|
|
|
function updateThemeToggle(theme: string) {
|
|
document.querySelectorAll('[data-theme-toggle]').forEach(button => {
|
|
button.setAttribute('data-current-theme', theme);
|
|
});
|
|
}
|
|
|
|
function initTheme() {
|
|
const storedTheme = getStoredTheme();
|
|
applyTheme(storedTheme);
|
|
updateThemeToggle(storedTheme);
|
|
}
|
|
|
|
function toggleTheme() {
|
|
const current = getStoredTheme();
|
|
const themes = ['light', 'dark', 'auto'];
|
|
const currentIndex = themes.indexOf(current);
|
|
const nextIndex = (currentIndex + 1) % themes.length;
|
|
const nextTheme = themes[nextIndex];
|
|
|
|
localStorage.setItem(THEME_KEY, nextTheme);
|
|
applyTheme(nextTheme);
|
|
updateThemeToggle(nextTheme);
|
|
}
|
|
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
|
if (getStoredTheme() === 'auto') {
|
|
applyTheme('auto');
|
|
}
|
|
});
|
|
|
|
(window as any).themeUtils = {
|
|
initTheme,
|
|
toggleTheme,
|
|
getStoredTheme
|
|
};
|
|
|
|
(window as any).showToolDetails = function(toolName: string, modalType: string = 'primary') {
|
|
let attempts = 0;
|
|
const maxAttempts = 50;
|
|
|
|
const tryDelegate = () => {
|
|
const matrixShowToolDetails = (window as any).matrixShowToolDetails;
|
|
|
|
if (matrixShowToolDetails && typeof matrixShowToolDetails === 'function') {
|
|
return matrixShowToolDetails(toolName, modalType);
|
|
}
|
|
|
|
const directShowToolDetails = (window as any).directShowToolDetails;
|
|
if (directShowToolDetails && typeof directShowToolDetails === 'function') {
|
|
return directShowToolDetails(toolName, modalType);
|
|
}
|
|
|
|
attempts++;
|
|
if (attempts < maxAttempts) {
|
|
setTimeout(tryDelegate, 100);
|
|
} else {
|
|
}
|
|
};
|
|
|
|
tryDelegate();
|
|
};
|
|
|
|
(window as any).hideToolDetails = function(modalType: string = 'both') {
|
|
const matrixHideToolDetails = (window as any).matrixHideToolDetails;
|
|
if (matrixHideToolDetails && typeof matrixHideToolDetails === 'function') {
|
|
return matrixHideToolDetails(modalType);
|
|
}
|
|
};
|
|
|
|
(window as any).hideAllToolDetails = function() {
|
|
(window as any).hideToolDetails('both');
|
|
};
|
|
|
|
async function checkClientAuth(context = 'general') {
|
|
try {
|
|
const response = await fetch('/api/auth/status');
|
|
const data = await response.json();
|
|
|
|
switch (context) {
|
|
case 'contributions':
|
|
return {
|
|
authenticated: data.contributionAuthenticated,
|
|
authRequired: data.contributionAuthRequired,
|
|
expires: data.expires
|
|
};
|
|
case 'ai':
|
|
return {
|
|
authenticated: data.aiAuthenticated,
|
|
authRequired: data.aiAuthRequired,
|
|
expires: data.expires
|
|
};
|
|
case 'gatedcontent':
|
|
return {
|
|
authenticated: data.gatedContentAuthenticated,
|
|
authRequired: data.gatedContentAuthRequired,
|
|
expires: data.expires
|
|
};
|
|
default:
|
|
return {
|
|
authenticated: data.authenticated,
|
|
authRequired: data.contributionAuthRequired || data.aiAuthRequired,
|
|
expires: data.expires
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.error('Auth check failed:', error);
|
|
return {
|
|
authenticated: false,
|
|
authRequired: true
|
|
};
|
|
}
|
|
}
|
|
|
|
async function requireClientAuth(callback: () => void, returnUrl: string, context = 'general') {
|
|
const authStatus = await checkClientAuth(context);
|
|
|
|
if (authStatus.authRequired && !authStatus.authenticated) {
|
|
const targetUrl = returnUrl || window.location.href;
|
|
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(targetUrl)}`;
|
|
return false;
|
|
} else {
|
|
if (typeof callback === 'function') {
|
|
callback();
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
async function showIfAuthenticated(selector: string, context = 'general') {
|
|
const authStatus = await checkClientAuth(context);
|
|
const element = document.querySelector(selector);
|
|
|
|
if (element) {
|
|
(element as HTMLElement).style.display = (!authStatus.authRequired || authStatus.authenticated)
|
|
? 'inline-flex'
|
|
: 'none';
|
|
}
|
|
}
|
|
|
|
function setupAuthButtons(selector = '[data-contribute-button]') {
|
|
document.addEventListener('click', async (e) => {
|
|
if (!e.target) return;
|
|
|
|
const button = (e.target as Element).closest(selector);
|
|
if (!button) return;
|
|
|
|
e.preventDefault();
|
|
|
|
console.log('[AUTH] Contribute button clicked:', button.getAttribute('data-contribute-button'));
|
|
|
|
await requireClientAuth(() => {
|
|
console.log('[AUTH] Navigation approved, redirecting to:', (button as HTMLAnchorElement).href);
|
|
window.location.href = (button as HTMLAnchorElement).href;
|
|
}, (button as HTMLAnchorElement).href, 'contributions');
|
|
});
|
|
}
|
|
|
|
(window as any).checkClientAuth = checkClientAuth;
|
|
(window as any).requireClientAuth = requireClientAuth;
|
|
(window as any).showIfAuthenticated = showIfAuthenticated;
|
|
(window as any).setupAuthButtons = setupAuthButtons;
|
|
|
|
async function copyUrlToClipboard(url: string, button: HTMLElement) {
|
|
try {
|
|
await navigator.clipboard.writeText(url);
|
|
|
|
const originalHTML = button.innerHTML;
|
|
button.innerHTML = `
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
|
|
<polyline points="20,6 9,17 4,12"/>
|
|
</svg>
|
|
Kopiert!
|
|
`;
|
|
button.style.color = 'var(--color-accent)';
|
|
|
|
setTimeout(() => {
|
|
button.innerHTML = originalHTML;
|
|
button.style.color = '';
|
|
}, 2000);
|
|
} catch (err) {
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = url;
|
|
document.body.appendChild(textArea);
|
|
textArea.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(textArea);
|
|
|
|
const originalHTML = button.innerHTML;
|
|
button.innerHTML = 'Kopiert!';
|
|
setTimeout(() => {
|
|
button.innerHTML = originalHTML;
|
|
}, 2000);
|
|
}
|
|
}
|
|
|
|
async function shareArticle(button: HTMLElement, url: string, title: string) {
|
|
const fullUrl = window.location.origin + url;
|
|
await copyUrlToClipboard(fullUrl, button);
|
|
}
|
|
|
|
async function shareCurrentArticle(button: HTMLElement) {
|
|
await copyUrlToClipboard(window.location.href, button);
|
|
}
|
|
|
|
(window as any).shareArticle = shareArticle;
|
|
(window as any).shareCurrentArticle = shareCurrentArticle;
|
|
|
|
initTheme();
|
|
setupAuthButtons('[data-contribute-button]');
|
|
|
|
const initAIButton = async () => {
|
|
await showIfAuthenticated('#ai-view-toggle', 'ai');
|
|
};
|
|
initAIButton();
|
|
});
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox') ||
|
|
navigator.userAgent.toLowerCase().includes('librewolf');
|
|
|
|
if (isFirefox) {
|
|
console.log('[VIDEO] Firefox detected - setting up error recovery');
|
|
|
|
document.querySelectorAll('video').forEach(video => {
|
|
let errorCount = 0;
|
|
|
|
video.addEventListener('error', () => {
|
|
errorCount++;
|
|
console.log(`[VIDEO] Error ${errorCount} in Firefox for: ${video.getAttribute('data-video-title')}`);
|
|
|
|
});
|
|
|
|
video.addEventListener('loadedmetadata', () => {
|
|
const title = video.getAttribute('data-video-title') || 'Video';
|
|
console.log(`[VIDEO] Successfully loaded: ${title}`);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<Navigation />
|
|
<main class="container flex-1 py-8 px-4">
|
|
<slot />
|
|
</main>
|
|
<Footer />
|
|
</body>
|
|
</html> |