feat(db-hardening): Add database persistence, backups, and MinIO assets storage
Database Hardening: - Add docker-compose.db.yml with persistent PostgreSQL volume - Add automatic DB backups every 12h (7 days, 4 weeks, 6 months retention) - Add MinIO S3-compatible storage for assets Assets Migration: - Add MinIO client (lib/assets_client.py) for upload/delete - Update upload endpoint to use MinIO (with local fallback) - Add migration 043_asset_urls_to_text.sql for full HTTPS URLs - Simplify normalizeAssetUrl for S3 URLs Recovery: - Add seed_full_city_reset.py for emergency city recovery - Add DB_RESTORE.md with backup restore instructions - Add SEED_RECOVERY.md with recovery procedures - Add INFRA_ASSETS_MINIO.md with MinIO setup guide Task: TASK_PHASE_DATABASE_HARDENING_AND_ASSETS_MIGRATION_v1
This commit is contained in:
@@ -1,13 +1,8 @@
|
||||
/**
|
||||
* Normalize asset URL for display.
|
||||
* This is the SINGLE source of truth for building static asset URLs.
|
||||
*
|
||||
* Handles various URL formats from the backend:
|
||||
* - /api/static/uploads/... - already correct, return as-is
|
||||
* - /assets/... - static assets in public folder, return as-is
|
||||
* - /static/uploads/... - needs /api prefix
|
||||
* - https://... or http://... - external URL, return as-is
|
||||
* - uploads/... or static/uploads/... - relative path, needs /api/static prefix
|
||||
* After migration to MinIO/S3, most assets will be full HTTPS URLs.
|
||||
* This function handles both new S3 URLs and legacy local file paths.
|
||||
*
|
||||
* IMPORTANT: Do NOT manually concatenate /api/static anywhere in the codebase.
|
||||
* Always use this function instead.
|
||||
@@ -15,45 +10,33 @@
|
||||
export function normalizeAssetUrl(url: string | null | undefined): string | null {
|
||||
if (!url) return null;
|
||||
|
||||
// External URLs - return as-is
|
||||
// Full HTTPS/HTTP URLs (from MinIO/S3) - return as-is
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Already correct format with /api/static
|
||||
// Legacy: Already correct format with /api/static
|
||||
if (url.startsWith('/api/static')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Static assets in public folder (/assets/...)
|
||||
// Legacy: Static assets in public folder (/assets/...)
|
||||
if (url.startsWith('/assets/')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Old format with /static/ prefix - add /api
|
||||
// Legacy: Old format with /static/ prefix - add /api
|
||||
if (url.startsWith('/static/')) {
|
||||
return `/api${url}`;
|
||||
}
|
||||
|
||||
// Relative path without leading slash (uploads/..., static/...)
|
||||
// Remove any duplicate prefixes and normalize
|
||||
let cleaned = url;
|
||||
|
||||
// Remove leading /api/static if somehow duplicated
|
||||
cleaned = cleaned.replace(/^\/api\/static\//, '');
|
||||
// Remove leading /static/
|
||||
cleaned = cleaned.replace(/^\/static\//, '');
|
||||
// Remove leading static/ (no slash)
|
||||
cleaned = cleaned.replace(/^static\//, '');
|
||||
// Remove leading slash
|
||||
cleaned = cleaned.replace(/^\/+/, '');
|
||||
|
||||
// If it looks like a relative upload path, prefix with /api/static/
|
||||
if (cleaned.startsWith('uploads/') || cleaned.includes('/')) {
|
||||
// Legacy: Relative paths - normalize to /api/static/
|
||||
let cleaned = url.replace(/^\/+/, ''); // Remove leading slashes
|
||||
if (cleaned && !cleaned.startsWith('http')) {
|
||||
return `/api/static/${cleaned}`;
|
||||
}
|
||||
|
||||
// Unknown format - return as-is (might be a simple filename)
|
||||
// Unknown format - return as-is
|
||||
return url;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user