Add authentication improvements and search functionality

- Implement anonymous access control with ALLOW_ANONYMOUS_ACCESS env var
- Add complete password reset workflow with token-based validation
- Add username recovery functionality for better UX
- Implement full-text search API with relevance scoring and highlighting
- Add Docker compatibility improvements with permission handling and fallback storage
- Add quick stats API for real-time dashboard updates
- Improve security with proper token expiration and input validation
- Add search result pagination and navigation
- Enhance error handling and logging throughout the application

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-11 21:11:03 -05:00
parent 36bb905f99
commit 83dd85ffa3
8 changed files with 960 additions and 12 deletions

View File

@@ -728,8 +728,28 @@ document.addEventListener('DOMContentLoaded', function() {
setupFilterSwitching();
setupInfiniteScroll();
setupAutoRefresh();
loadQuickStats();
});
// Load quick stats data
async function loadQuickStats() {
try {
const response = await fetch('/api/stats');
const data = await response.json();
if (data.posts_today !== undefined) {
// Update posts today stat
const postsTodayElement = document.querySelector('.stat-card .stat-number');
if (postsTodayElement) {
postsTodayElement.textContent = data.posts_today;
}
}
} catch (error) {
console.error('Error loading quick stats:', error);
// Keep default value if API fails
}
}
// Load platform configuration and communities
async function loadPlatformConfig() {
try {
@@ -1032,11 +1052,170 @@ document.querySelector('.search-input').addEventListener('keypress', function(e)
if (e.key === 'Enter') {
const query = this.value.trim();
if (query) {
alert(`Search functionality coming soon! You searched for: "${query}"`);
performSearch(query);
}
}
});
// Search button functionality
document.querySelector('.search-btn').addEventListener('click', function() {
const query = document.querySelector('.search-input').value.trim();
if (query) {
performSearch(query);
}
});
// Search posts function
async function performSearch(query) {
try {
// Show loading state in search bar
const searchInput = document.querySelector('.search-input');
const searchBtn = document.querySelector('.search-btn');
const originalPlaceholder = searchInput.placeholder;
searchInput.placeholder = 'Searching...';
searchBtn.disabled = true;
// Build search parameters
const params = new URLSearchParams();
params.append('q', query);
params.append('page', 1);
params.append('per_page', 20);
const response = await fetch(`/api/search?${params}`);
const data = await response.json();
if (response.ok && data.posts) {
// Hide loading state
searchInput.placeholder = originalPlaceholder;
searchBtn.disabled = false;
// Update UI for search results
displaySearchResults(query, data);
} else {
throw new Error(data.error || 'Search failed');
}
} catch (error) {
console.error('Search error:', error);
// Hide loading state
const searchInput = document.querySelector('.search-input');
const searchBtn = document.querySelector('.search-btn');
searchInput.placeholder = 'Search failed...';
searchBtn.disabled = false;
setTimeout(() => {
searchInput.placeholder = 'Search content...';
}, 2000);
}
}
// Display search results in the main content area
function displaySearchResults(query, searchData) {
// Update page title and header
document.title = `Search Results: "${query}" - BalanceBoard`;
const contentHeader = document.querySelector('.content-header h1');
contentHeader.textContent = `Search Results for "${query}"`;
// Update page info to show search results
const pageInfo = document.querySelector('.page-info');
pageInfo.textContent = `Found ${searchData.pagination.total_posts} results`;
// Render search results using the same post card template
const postsContainer = document.getElementById('posts-container');
postsContainer.innerHTML = '';
if (searchData.posts.length === 0) {
postsContainer.innerHTML = `
<div class="no-posts">
<h3>No results found</h3>
<p>Try different keywords or check your spelling.</p>
</div>
`;
return;
}
// Create post cards for search results
const postsHTML = searchData.posts.map(post => createSearchResultPostCard(post, query)).join('');
postsContainer.innerHTML = postsHTML;
}
// Create search result post card with highlighted matches
function createSearchResultPostCard(post, query) {
const timeAgo = formatTimeAgo(post.timestamp);
const platformClass = `platform-${post.platform}`;
const platformInitial = post.platform.charAt(0).toUpperCase();
const hasExternalLink = post.external_url && !post.external_url.includes(window.location.hostname);
// Highlight matched fields in title and content
const highlightedTitle = highlightText(post.title, query);
const highlightedContent = highlightText(post.content_preview || '', query);
return `
<article class="post-card" onclick="openPost('${post.id}')">
<div class="post-header">
<div class="platform-badge ${platformClass}" onclick="event.stopPropagation(); filterByPlatform('${post.platform}')" title="Filter by ${post.platform}">
${platformInitial}
</div>
<div class="post-meta">
<span class="post-author">${escapeHtml(post.author)}</span>
<span class="post-separator">•</span>
${post.source_display ? `<span class="post-source" onclick="event.stopPropagation(); filterByCommunity('${post.source}', '${post.platform}')" title="Filter by ${post.source_display}">${escapeHtml(post.source_display)}</span><span class="post-separator">•</span>` : ''}
<span class="post-time">${timeAgo}</span>
${hasExternalLink ? '<span class="external-link-indicator">🔗</span>' : ''}
</div>
</div>
<h3 class="post-title">${highlightedTitle}</h3>
${highlightedContent ? `<div class="post-preview">${highlightedContent}</div>` : ''}
${post.tags && post.tags.length > 0 ? `
<div class="post-tags">
${post.tags.map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).join('')}
</div>
` : ''}
<div class="post-footer">
<div class="post-stats">
<div class="post-score">
<span>▲</span>
<span>${post.score}</span>
</div>
<div class="post-comments">
<span>💬</span>
<span>${post.comment_count || 0} comments</span>
</div>
</div>
<div class="post-actions">
${hasExternalLink ? `<button class="post-action" onclick="event.stopPropagation(); window.open('${escapeHtml(post.external_url)}', '_blank')">🔗 Source</button>` : ''}
<button class="post-action" onclick="event.stopPropagation(); sharePost('${post.id}')">Share</button>
<button class="post-action" onclick="event.stopPropagation(); savePost('${post.id}')">Save</button>
</div>
</div>
<!-- Show search match info -->
<div class="search-match-info" style="background: #e0f7fa; padding: 8px 12px; margin-top: 8px; border-radius: 6px; font-size: 0.85rem; color: #0277bd;">
<strong>Matched in:</strong> ${post.matched_fields.join(', ') || 'title, content'}
</div>
</article>
`;
}
// Highlight matching text
function highlightText(text, query) {
if (!text || !query) return text;
const regex = new RegExp(`(${escapeRegex(query)})`, 'gi');
return text.replace(regex, '<mark style="background: #fff3cd; padding: 2px 4px; border-radius: 2px;">$1</mark>');
}
// Escape regex special characters
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Setup infinite scroll functionality
function setupInfiniteScroll() {
if (!userSettings?.experience?.infinite_scroll) {
@@ -1193,6 +1372,55 @@ function loadNextPage() {
}
}
// Search result pagination functions
function loadNextSearchPage(currentQuery, currentPage) {
const params = new URLSearchParams();
params.append('q', currentQuery);
params.append('page', currentPage + 1);
params.append('per_page', 20);
fetch(`/api/search?${params}`)
.then(response => response.json())
.then(data => {
if (data.posts) {
// Append new results to existing ones
const postsContainer = document.getElementById('posts-container');
const newPostsHTML = data.posts.map(post => createSearchResultPostCard(post, currentQuery)).join('');
postsContainer.insertAdjacentHTML('beforeend', newPostsHTML);
// Update pagination info
updateSearchPagination(data.pagination);
}
})
.catch(error => {
console.error('Error loading next search page:', error);
});
}
function loadPreviousSearchPage(currentQuery, currentPage) {
const params = new URLSearchParams();
params.append('q', currentQuery);
params.append('page', currentPage - 1);
params.append('per_page', 20);
fetch(`/api/search?${params}`)
.then(response => response.json())
.then(data => {
if (data.posts) {
// Replace current results with previous page
displaySearchResults(currentQuery, data);
}
})
.catch(error => {
console.error('Error loading previous search page:', error);
});
}
function updateSearchPagination(pagination) {
const pageInfo = document.querySelector('.page-info');
pageInfo.textContent = `Page ${pagination.current_page} of ${pagination.total_pages} (${pagination.total_posts} results)`;
}
function loadPreviousPage() {
if (paginationData.has_prev) {
loadPosts(currentPage - 1, currentCommunity, currentPlatform);