file uploaded path correction

This commit is contained in:
sandhiya-hepl
2026-07-23 12:32:42 +05:30
parent b5f7a38c2a
commit 1b40a451b1
10 changed files with 247 additions and 75 deletions
+8 -4
View File
@@ -14,7 +14,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PORT = process.env.PORT || 3001;
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, 'uploads');
// Public URL prefix on Apache (e.g. /citpl_website). Leave empty if site is at domain root.
const SITE_BASE = (process.env.SITE_BASE || '/citpl_website').replace(/\/$/, '');
const SITE_BASE = (process.env.SITE_BASE ?? '').replace(/\/$/, '');
const distDir = path.join(__dirname, '..', 'dist');
const publicAssetsDir = path.join(__dirname, '..', 'public', 'assets');
const serveStatic =
@@ -31,7 +31,11 @@ app.use(cors({
credentials: true,
}));
app.use(express.json({ limit: '10mb' }));
// Only parse JSON for non-multipart requests — multer handles multipart/form-data
app.use((req, res, next) => {
if (req.headers['content-type']?.startsWith('multipart/form-data')) return next();
express.json({ limit: '10mb' })(req, res, next);
});
function mountApp(base) {
const prefix = base || '';
@@ -48,8 +52,8 @@ function mountApp(base) {
if (serveStatic) {
app.use(prefix || '/', express.static(distDir));
// SPA fallback only for the app's base path, not root-level unknown files
app.get(`${prefix}/*`, (req, res, next) => {
// SPA fallback — Express 5 requires named wildcard /*path (not /*)
app.get(`${prefix}/*path`, (req, res, next) => {
const ext = req.path.split('.').pop();
if (ext && ext !== req.path) return next();
res.sendFile(path.join(distDir, 'index.html'));
+17 -1
View File
@@ -33,7 +33,23 @@ const upload = multer({
const router = Router();
router.post('/', requireAuth, upload.single('file'), (req, res) => {
// Wrap multer for Express 5 — multer v2 doesn't handle Express 5 async errors natively
function runUpload(req, res) {
return new Promise((resolve, reject) => {
upload.single('file')(req, res, (err) => {
if (err) reject(err);
else resolve();
});
});
}
router.post('/', requireAuth, async (req, res) => {
try {
await runUpload(req, res);
} catch (err) {
const status = err.code === 'LIMIT_FILE_SIZE' ? 413 : 400;
return res.status(status).json({ error: err.message, code: err.code || 'UPLOAD_ERROR' });
}
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded', code: 'VALIDATION' });
}
+35 -18
View File
@@ -1,4 +1,6 @@
import { API_BASE } from '../lib/apiBase';
import { uploadLocal } from '../lib/localFileStore';
import { notifyContentUpdated } from '../context/ContentContext';
/** Frontend-only fallback when the Node API is not reachable on the host. */
const STATIC_USER = 'admin';
@@ -45,7 +47,9 @@ async function apiFetch(path, options = {}) {
: res.status === 502
? 'API server unreachable. Start Node on the host (port 3001).'
: 'Request failed';
throw new Error(data.error || fallback);
const err = new Error(data.error || fallback);
err.status = res.status;
throw err;
}
return data;
}
@@ -65,7 +69,17 @@ export const adminApi = {
me: async () => {
if (isStaticSession()) {
return { username: STATIC_USER, mode: 'static' };
// If server is now running, clear the stale static token so real login is used
try {
await apiFetch('/api/auth/me');
// Server responded — clear static token so user re-authenticates properly
clearToken();
throw new Error('Session expired, please log in again');
} catch (err) {
if (err.status) throw err; // server is up but rejected — force re-login
// Server unreachable — static session is fine
return { username: STATIC_USER, mode: 'static' };
}
}
return apiFetch('/api/auth/me');
},
@@ -82,14 +96,28 @@ export const adminApi = {
},
updateSection: async (section, data) => {
try {
return await apiFetch(`/api/content/${section}`, { method: 'PUT', body: JSON.stringify(data) });
} catch {
// Skip API entirely for static sessions
if (isStaticSession()) {
const saved = localStorage.getItem('static_content');
const { default: seed } = await import('../../server/seed/default-content.json');
const current = saved ? JSON.parse(saved) : seed;
const updated = { ...current, [section]: data, _updatedAt: new Date().toISOString() };
localStorage.setItem('static_content', JSON.stringify(updated));
notifyContentUpdated();
return { message: `${section} updated (local)`, updatedAt: updated._updatedAt };
}
try {
const result = await apiFetch(`/api/content/${section}`, { method: 'PUT', body: JSON.stringify(data) });
notifyContentUpdated();
return result;
} catch (err) {
if (err.status === 401) clearToken();
const saved = localStorage.getItem('static_content');
const { default: seed } = await import('../../server/seed/default-content.json');
const current = saved ? JSON.parse(saved) : seed;
const updated = { ...current, [section]: data, _updatedAt: new Date().toISOString() };
localStorage.setItem('static_content', JSON.stringify(updated));
notifyContentUpdated();
return { message: `${section} updated (local)`, updatedAt: updated._updatedAt };
}
},
@@ -103,18 +131,7 @@ export const adminApi = {
},
upload: async (file) => {
// Try Node API first; fall back to local data URL when server is unavailable.
try {
const form = new FormData();
form.append('file', file);
return await apiFetch('/api/upload', { method: 'POST', body: form });
} catch {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve({ url: reader.result, filename: file.name });
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsDataURL(file);
});
}
// Store file locally in IndexedDB — no server needed
return uploadLocal(file);
},
};
+1 -1
View File
@@ -14,7 +14,7 @@ export function ImageUpload({ value, onChange, label = 'Image' }) {
const { url } = await adminApi.upload(file);
onChange(url);
} catch (err) {
alert(err.message);
alert('Upload failed: ' + err.message);
} finally {
setUploading(false);
}
+9 -5
View File
@@ -2,9 +2,13 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './admin.css';
import App from './App.jsx';
import { restoreAllBlobUrls } from '../lib/localFileStore';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
// Restore IndexedDB-stored file blob URLs before rendering
restoreAllBlobUrls().then(() => {
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
});
+69 -37
View File
@@ -2,81 +2,113 @@ import { createContext, useContext, useEffect, useState } from 'react';
import defaultContent from '../../server/seed/default-content.json';
import { API_BASE } from '../lib/apiBase';
import { prefixAssets } from '../lib/assetUrl';
import { restoreAllBlobUrls } from '../lib/localFileStore';
const ContentContext = createContext(null);
// BroadcastChannel works across tabs on the same origin
const BC = typeof BroadcastChannel !== 'undefined' ? new BroadcastChannel('citpl_content') : null;
// Called by admin api.js after every save to push update to all tabs
export function notifyContentUpdated() {
BC?.postMessage({ type: 'updated' });
window.dispatchEvent(new CustomEvent('citpl:content-updated'));
}
function allowedPreviewOrigins() {
const origins = new Set(['https://demo.cavinkare.in/', 'https://demo.cavinkare.in']);
if (typeof window !== 'undefined') origins.add(window.location.origin);
return origins;
}
function loadFromLocalStorage() {
const saved = localStorage.getItem('static_content');
if (!saved) return null;
try {
const { _updatedAt, ...rest } = JSON.parse(saved);
return { content: prefixAssets(rest), updatedAt: _updatedAt ?? null };
} catch { return null; }
}
export function ContentProvider({ children, previewMode = false }) {
const [content, setContent] = useState(prefixAssets(defaultContent));
const [loading, setLoading] = useState(!previewMode);
const [error, setError] = useState(null);
const [updatedAt, setUpdatedAt] = useState(null);
function applyContent(data) {
const { _updatedAt, ...rest } = data;
setContent(prefixAssets(rest));
setUpdatedAt(_updatedAt ?? null);
setError(null);
}
function refresh() {
restoreAllBlobUrls().then(() => {
fetch(`${API_BASE}/api/content`)
.then((res) => res.ok ? res.json() : Promise.reject())
.then((data) => applyContent(data))
.catch(() => {
const local = loadFromLocalStorage();
if (local) { setContent(local.content); setUpdatedAt(local.updatedAt); }
});
});
}
useEffect(() => {
if (previewMode) return;
fetch(`${API_BASE}/api/content`)
.then((res) => {
if (!res.ok) throw new Error('Failed to load content');
return res.json();
})
.then((data) => {
const { _updatedAt, ...rest } = data;
setContent(prefixAssets(rest));
setUpdatedAt(_updatedAt);
setError(null);
})
.catch((err) => {
console.warn('Using fallback content:', err.message);
const saved = localStorage.getItem('static_content');
if (saved) {
try {
const { _updatedAt, ...rest } = JSON.parse(saved);
setContent(prefixAssets(rest));
setUpdatedAt(_updatedAt ?? null);
return;
} catch { /* ignore parse error */ }
}
setError(err.message);
})
.finally(() => setLoading(false));
// Initial load
restoreAllBlobUrls().then(() => {
fetch(`${API_BASE}/api/content`)
.then((res) => {
if (!res.ok) throw new Error('Failed to load content');
return res.json();
})
.then((data) => applyContent(data))
.catch((err) => {
console.warn('Using fallback content:', err.message);
const local = loadFromLocalStorage();
if (local) { setContent(local.content); setUpdatedAt(local.updatedAt); }
else setError(err.message);
})
.finally(() => setLoading(false));
});
// Cross-tab: BroadcastChannel (admin tab → main site tab)
if (BC) BC.onmessage = (e) => { if (e.data?.type === 'updated') refresh(); };
// Same-tab: custom event
window.addEventListener('citpl:content-updated', refresh);
// Cross-tab fallback: storage event
const onStorage = (e) => { if (e.key === 'static_content') refresh(); };
window.addEventListener('storage', onStorage);
return () => {
window.removeEventListener('citpl:content-updated', refresh);
window.removeEventListener('storage', onStorage);
if (BC) BC.onmessage = null;
};
}, [previewMode]);
useEffect(() => {
if (!previewMode) return;
setLoading(false);
const origins = allowedPreviewOrigins();
const onMessage = (event) => {
if (!origins.has(event.origin)) return;
if (event.data?.type !== 'ADMIN_PREVIEW' || !event.data.fullContent) return;
setContent(prefixAssets(event.data.fullContent));
setLoading(false);
const anchor = event.data.anchor;
if (!anchor) return;
window.setTimeout(() => {
document.getElementById(anchor)?.scrollIntoView({ behavior: 'auto', block: 'start' });
}, 180);
};
window.addEventListener('message', onMessage);
for (const origin of origins) {
try {
window.parent?.postMessage({ type: 'PREVIEW_READY' }, origin);
} catch {
/* ignore */
}
try { window.parent?.postMessage({ type: 'PREVIEW_READY' }, origin); } catch { /* ignore */ }
}
return () => window.removeEventListener('message', onMessage);
}, [previewMode]);
+5
View File
@@ -1,3 +1,5 @@
import { blobCache } from './localFileStore';
/**
* Vite base, e.g. "/citpl_website/" in production and "/" in local dev.
* Runtime fallback: if an old/dev build baked base "/" but the page is
@@ -26,6 +28,9 @@ export function assetUrl(path) {
if (/^(data:|blob:)/i.test(path)) return path;
// Resolve locally uploaded files from IndexedDB blob cache
if (blobCache.has(path)) return blobCache.get(path);
const assetBase = resolveAssetBase();
const prefix = assetBase.replace(/\/$/, '');
+83
View File
@@ -0,0 +1,83 @@
/**
* Local file store using IndexedDB.
* Stores uploaded files as binary blobs keyed by a stable path like /assets/filename.
* On load, restores blob URLs so images/videos display correctly.
*/
const DB_NAME = 'citpl_uploads';
const STORE = 'files';
const DB_VERSION = 1;
function openDb() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VERSION);
req.onupgradeneeded = (e) => e.target.result.createObjectStore(STORE);
req.onsuccess = (e) => resolve(e.target.result);
req.onerror = () => reject(req.error);
});
}
export async function saveFile(key, file) {
const db = await openDb();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE, 'readwrite');
tx.objectStore(STORE).put({ blob: file, name: file.name, type: file.type }, key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
export async function getFileUrl(key) {
const db = await openDb();
return new Promise((resolve) => {
const req = db.transaction(STORE).objectStore(STORE).get(key);
req.onsuccess = () => {
if (req.result) resolve(URL.createObjectURL(req.result.blob));
else resolve(null);
};
req.onerror = () => resolve(null);
});
}
export async function getAllKeys() {
const db = await openDb();
return new Promise((resolve) => {
const req = db.transaction(STORE).objectStore(STORE).getAllKeys();
req.onsuccess = () => resolve(req.result);
req.onerror = () => resolve([]);
});
}
/**
* Save file locally and return a stable key path like /assets/user_upload_filename.ext
*/
export async function uploadLocal(file) {
const ext = file.name.split('.').pop().toLowerCase();
const base = file.name.replace(/[^a-zA-Z0-9-_]/g, '_').replace(/\.[^.]+$/, '').slice(0, 40);
const key = `/assets/user_upload_${base}_${Date.now()}.${ext}`;
await saveFile(key, file);
const url = URL.createObjectURL(file);
// Cache the blob URL in memory for this session
blobCache.set(key, url);
return { url: key, blobUrl: url };
}
// In-memory cache: key → blob URL (valid for current session)
export const blobCache = new Map();
/**
* Call once on app init to restore blob URLs for all stored files.
* Returns a Map of key → blobUrl.
*/
export async function restoreAllBlobUrls() {
const keys = await getAllKeys();
await Promise.all(
keys.map(async (key) => {
if (!blobCache.has(key)) {
const url = await getFileUrl(key);
if (url) blobCache.set(key, url);
}
})
);
return blobCache;
}
+10 -7
View File
@@ -3,11 +3,14 @@ import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import { ContentProvider } from './context/ContentContext.jsx'
import { restoreAllBlobUrls } from './lib/localFileStore'
createRoot(document.getElementById('root')).render(
<StrictMode>
<ContentProvider>
<App />
</ContentProvider>
</StrictMode>,
)
restoreAllBlobUrls().then(() => {
createRoot(document.getElementById('root')).render(
<StrictMode>
<ContentProvider>
<App />
</ContentProvider>
</StrictMode>,
)
})
+10 -2
View File
@@ -12,8 +12,16 @@ export default defineConfig((configEnv) => {
plugins: [react()],
server: {
proxy: {
'/api': 'http://localhost:3001',
'/uploads': 'http://localhost:3001',
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
proxyTimeout: 120000,
timeout: 120000,
},
'/uploads': {
target: 'http://localhost:3001',
changeOrigin: true,
},
},
},
build: {