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 <[email protected]>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user