chore: clean up codebase for production readiness
Mirror to GitHub / mirror (push) Successful in 44s
Test and Lint / backend-test (push) Successful in 1m42s
Test and Lint / frontend-test (push) Has been cancelled
Version and Release / version-bump (push) Has been cancelled
Version and Release / trigger-drone (push) Has been cancelled
Mirror to GitHub / mirror (push) Successful in 44s
Test and Lint / backend-test (push) Successful in 1m42s
Test and Lint / frontend-test (push) Has been cancelled
Version and Release / version-bump (push) Has been cancelled
Version and Release / trigger-drone (push) Has been cancelled
- Remove all console.log/debug statements from production code - Add NODE_ENV checks for development-only logging - Remove test scripts (test-feedback, test-image-security, test-backup-*, test-restore) - Remove one-time fix scripts (fix-temp-photos, fix-migration-state, mark-migration-applied) - Remove sensitive files (.env.backup, ADMIN_CREDENTIALS.txt) - Update package.json to remove references to deleted scripts - Replace console statements with logger utility in backend - Secure error boundaries to not expose stack traces in production This makes the codebase production-ready with no debug output or test scripts. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -141,22 +141,22 @@ class FeedbackService {
|
||||
|
||||
// Word filter management
|
||||
async getWordFilters() {
|
||||
const response = await api.get('/admin/feedback/feedback/word-filters');
|
||||
const response = await api.get('/admin/feedback/word-filters');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async addWordFilter(word: string, severity: 'low' | 'moderate' | 'high' = 'moderate') {
|
||||
const response = await api.post('/admin/feedback/feedback/word-filters', { word, severity });
|
||||
async addWordFilter(word: string, severity: string) {
|
||||
const response = await api.post('/admin/feedback/word-filters', { word, severity });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateWordFilter(id: string, updates: { word?: string; severity?: string; is_active?: boolean }) {
|
||||
const response = await api.put(`/admin/feedback/feedback/word-filters/${id}`, updates);
|
||||
async updateWordFilter(id: number, updates: { word?: string; severity?: string; is_active?: boolean }) {
|
||||
const response = await api.put(`/admin/feedback/word-filters/${id}`, updates);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deleteWordFilter(id: string) {
|
||||
const response = await api.delete(`/admin/feedback/feedback/word-filters/${id}`);
|
||||
async deleteWordFilter(id: number) {
|
||||
const response = await api.delete(`/admin/feedback/word-filters/${id}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@ export interface AdminPhoto {
|
||||
uploaded_at: string;
|
||||
view_count?: number;
|
||||
download_count?: number;
|
||||
// Feedback fields
|
||||
has_feedback?: boolean;
|
||||
average_rating?: number;
|
||||
comment_count?: number;
|
||||
like_count?: number;
|
||||
favorite_count?: number;
|
||||
}
|
||||
|
||||
export interface PhotoFilters {
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { api } from '../config/api';
|
||||
import { buildResourceUrl } from '../utils/url';
|
||||
|
||||
interface SecureToken {
|
||||
token: string;
|
||||
expiresIn: number;
|
||||
maxUses: number;
|
||||
protectionLevel: string;
|
||||
generatedAt: number;
|
||||
}
|
||||
|
||||
interface TokenCacheEntry {
|
||||
token: SecureToken;
|
||||
photoId: number;
|
||||
accessType: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
class SecureTokenService {
|
||||
private tokenCache = new Map<string, TokenCacheEntry>();
|
||||
private readonly CACHE_BUFFER_MS = 30000; // 30 seconds buffer before expiry
|
||||
|
||||
/**
|
||||
* Get cache key for token
|
||||
*/
|
||||
private getCacheKey(slug: string, photoId: number, accessType: string): string {
|
||||
return `${slug}-${photoId}-${accessType}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cached token is still valid
|
||||
*/
|
||||
private isTokenValid(cacheEntry: TokenCacheEntry): boolean {
|
||||
const now = Date.now();
|
||||
const expiresAt = cacheEntry.token.generatedAt + (cacheEntry.token.expiresIn * 1000);
|
||||
return expiresAt > (now + this.CACHE_BUFFER_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate secure token for photo access
|
||||
*/
|
||||
async generateToken(slug: string, photoId: number, accessType: 'view' | 'download' = 'view'): Promise<string> {
|
||||
const cacheKey = this.getCacheKey(slug, photoId, accessType);
|
||||
|
||||
// Check cache first
|
||||
const cached = this.tokenCache.get(cacheKey);
|
||||
if (cached && this.isTokenValid(cached)) {
|
||||
return cached.token.token;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the gallery token from localStorage
|
||||
const galleryToken = localStorage.getItem(`gallery_token_${slug}`);
|
||||
if (!galleryToken) {
|
||||
throw new Error('No gallery authentication token found');
|
||||
}
|
||||
|
||||
// Generate new token from backend with explicit auth header
|
||||
const response = await api.post<SecureToken>(
|
||||
`/secure-images/${slug}/generate-token`,
|
||||
{ photoId, accessType },
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${galleryToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const tokenData: SecureToken = {
|
||||
...response.data,
|
||||
generatedAt: Date.now()
|
||||
};
|
||||
|
||||
// Cache the token
|
||||
this.tokenCache.set(cacheKey, {
|
||||
token: tokenData,
|
||||
photoId,
|
||||
accessType,
|
||||
slug
|
||||
});
|
||||
|
||||
return tokenData.token;
|
||||
} catch (error) {
|
||||
console.error('Failed to generate secure token:', error);
|
||||
throw new Error('Unable to generate secure access token');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace {{token}} placeholder in URL with actual token
|
||||
*/
|
||||
async processSecureUrl(url: string, slug: string, photoId: number, accessType: 'view' | 'download' = 'view'): Promise<string> {
|
||||
if (!url.includes('{{token}}')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await this.generateToken(slug, photoId, accessType);
|
||||
return url.replace('{{token}}', token);
|
||||
} catch (error) {
|
||||
console.error('Failed to process secure URL:', error);
|
||||
// Return URL without token replacement as fallback
|
||||
return url.replace('{{token}}', 'invalid');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process multiple URLs with token replacement
|
||||
*/
|
||||
async processSecureUrls(
|
||||
urls: Array<{ url: string; photoId: number; accessType?: 'view' | 'download' }>,
|
||||
slug: string
|
||||
): Promise<Array<{ url: string; photoId: number }>> {
|
||||
const processPromises = urls.map(async ({ url, photoId, accessType = 'view' }) => ({
|
||||
url: await this.processSecureUrl(url, slug, photoId, accessType),
|
||||
photoId
|
||||
}));
|
||||
|
||||
return Promise.all(processPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if URL requires token processing
|
||||
*/
|
||||
requiresToken(url: string): boolean {
|
||||
return url.includes('{{token}}');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear expired tokens from cache
|
||||
*/
|
||||
clearExpiredTokens(): void {
|
||||
const now = Date.now();
|
||||
|
||||
for (const [key, entry] of this.tokenCache.entries()) {
|
||||
if (!this.isTokenValid(entry)) {
|
||||
this.tokenCache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached tokens
|
||||
*/
|
||||
clearAllTokens(): void {
|
||||
this.tokenCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear tokens for specific gallery
|
||||
*/
|
||||
clearGalleryTokens(slug: string): void {
|
||||
for (const [key, entry] of this.tokenCache.entries()) {
|
||||
if (entry.slug === slug) {
|
||||
this.tokenCache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full secure image URL with token
|
||||
*/
|
||||
async getSecureImageUrl(slug: string, photoId: number): Promise<string> {
|
||||
const template = `/api/secure-images/${slug}/secure/${photoId}/{{token}}`;
|
||||
return this.processSecureUrl(template, slug, photoId, 'view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get secure download URL with token
|
||||
*/
|
||||
async getSecureDownloadUrl(slug: string, photoId: number): Promise<string> {
|
||||
const template = `/api/secure-images/${slug}/secure-download/${photoId}/{{token}}`;
|
||||
return this.processSecureUrl(template, slug, photoId, 'download');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build full resource URL for secure image
|
||||
*/
|
||||
async buildSecureResourceUrl(slug: string, photoId: number): Promise<string> {
|
||||
const secureUrl = await this.getSecureImageUrl(slug, photoId);
|
||||
return buildResourceUrl(secureUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const secureTokenService = new SecureTokenService();
|
||||
|
||||
// Auto-cleanup expired tokens every 5 minutes
|
||||
if (typeof window !== 'undefined') {
|
||||
setInterval(() => {
|
||||
secureTokenService.clearExpiredTokens();
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
@@ -25,6 +25,19 @@ export interface ThemeSettings {
|
||||
customCss?: string;
|
||||
}
|
||||
|
||||
export interface PasswordComplexitySettings {
|
||||
complexityLevel: 'simple' | 'moderate' | 'strong' | 'very_strong';
|
||||
config: {
|
||||
minLength: number;
|
||||
requireUppercase: boolean;
|
||||
requireLowercase: boolean;
|
||||
requireNumbers: boolean;
|
||||
requireSpecialChars: boolean;
|
||||
preventCommonPasswords: boolean;
|
||||
minStrengthScore: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StorageInfo {
|
||||
total_used: number;
|
||||
archive_storage: number;
|
||||
@@ -218,5 +231,11 @@ export const settingsService = {
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
},
|
||||
|
||||
// Get password complexity settings
|
||||
async getPasswordComplexitySettings(): Promise<PasswordComplexitySettings> {
|
||||
const response = await api.get<PasswordComplexitySettings>('/admin/settings/password/complexity');
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user