diff --git a/backend/package.json b/backend/package.json index 57cddbaa..cdd09d47 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,7 +6,7 @@ "scripts": { "predev": "node scripts/free-ports.js 8080", "dev": "nodemon --watch server --watch .env --ignore logs --ignore uploads server", - "build": "node --check server/index.js && node --check server/server.js && node --check server/bootstrap.js && node --check server/config.js && node --check server/db/sequelize.js && node --check server/db/connect.js && node --check server/db/models/inbox.js && node --check server/db/models/chat.js && node --check server/db/models/chatRoomCounter.js && node --check server/db/models/messageReceipt.js && node --check server/db/models/chatDraft.js && node --check server/db/models/messageRequest.js && node --check server/db/models/chatTopic.js && node --check server/db/models/e2eeDeviceKey.js && node --check server/db/models/resumableUpload.js && node --check server/db/models/chatAiConfig.js && node --check server/db/models/adminSocialAuthConfig.js && node --check server/db/models/profile.js && node --check server/db/models/group.js && node --check server/db/models/channel.js && node --check server/db/models/adminStorageConfig.js && node --check server/db/models/adminCallConfig.js && node --check server/db/models/callHistory.js && node --check server/db/models/profilePhoto.js && node --check server/db/models/nativePushDevice.js && node --check server/helpers/storageConfig.js && node --check server/helpers/callConfig.js && node --check server/helpers/callHistory.js && node --check server/helpers/callState.js && node --check server/helpers/avatarDefaults.js && node --check server/helpers/ensureAvatarDefaults.js && node --check server/helpers/ensureProfile.js && node --check server/helpers/privacy.js && node --check server/helpers/profilePhotos.js && node --check server/helpers/livekit.js && node --check server/helpers/nativePushConfig.js && node --check server/helpers/nativePush.js && node --check server/helpers/storage.js && node --check server/helpers/videoPipeline.js && node --check server/helpers/accountArchive.js && node --check server/helpers/accountExport.js && node --check server/helpers/logger.js && node --check server/helpers/chatAbuse.js && node --check server/helpers/chatMentions.js && node --check server/helpers/messageRequests.js && node --check server/helpers/chatReliability.js && node --check server/helpers/chatAiConfig.js && node --check server/helpers/socialAuthConfig.js && node --check server/helpers/e2eeKeyDirectory.js && node --check server/helpers/chatMaintenance.js && node --check server/middleware/chatSendIdempotency.js && node --check server/middleware/roomAccess.js && node --check server/controllers/avatar.js && node --check server/controllers/profile.js && node --check server/controllers/chatUpload.js && node --check server/controllers/chatSecurity.js && node --check server/controllers/chatDeletion.js && node --check server/controllers/chatV2.js && node --check server/controllers/chatAiAdmin.js && node --check server/controllers/chatSuggestions.js && node --check server/controllers/chatResumableUpload.js && node --check server/controllers/socialAuth.js && node --check server/controllers/socialAuthAdmin.js && node --check server/controllers/storageAdmin.js && node --check server/controllers/callingAdmin.js && node --check server/controllers/callingPushAdmin.js && node --check server/controllers/callingConfig.js && node --check server/controllers/nativePush.js && node --check server/controllers/accountStorage.js && node --check server/routes/profile.js && node --check server/routes/inbox.js && node --check server/routes/chat.js && node --check server/routes/chatV2.js && node --check server/routes/chatAiAdmin.js && node --check server/routes/socialAuthAdmin.js && node --check server/routes/storageAdmin.js && node --check server/routes/callingAdmin.js && node --check server/routes/callingPushAdmin.js && node --check server/routes/callingConfig.js && node --check server/routes/setting.js && node --check server/helpers/socketAdapter.js && node --check server/socket/auth.js && node --check server/socket/index.js && node --check server/socket/events/chatV2.js && node --check server/socket/events/calling.js && node --check server/socket/events/callRealtime.js && node --check server/socket/events/backgroundCalling.js && node --check server/socket/events/groupModeration.js && node --check server/socket/events/room.js && node --check server/routes/cron.js && node --check api/index.js", + "build": "node --check server/index.js && node --check server/server.js && node --check server/bootstrap.js && node --check server/config.js && node --check server/db/sequelize.js && node --check server/db/connect.js && node --check server/db/models/inbox.js && node --check server/db/models/chat.js && node --check server/db/models/chatRoomCounter.js && node --check server/db/models/messageReceipt.js && node --check server/db/models/chatDraft.js && node --check server/db/models/messageRequest.js && node --check server/db/models/chatTopic.js && node --check server/db/models/e2eeDeviceKey.js && node --check server/db/models/resumableUpload.js && node --check server/db/models/chatAiConfig.js && node --check server/db/models/adminSocialAuthConfig.js && node --check server/db/models/profile.js && node --check server/db/models/group.js && node --check server/db/models/channel.js && node --check server/db/models/adminStorageConfig.js && node --check server/db/models/adminCallConfig.js && node --check server/db/models/callHistory.js && node --check server/db/models/profilePhoto.js && node --check server/db/models/nativePushDevice.js && node --check server/helpers/storageConfig.js && node --check server/helpers/callConfig.js && node --check server/helpers/callHistory.js && node --check server/helpers/callState.js && node --check server/helpers/avatarDefaults.js && node --check server/helpers/ensureAvatarDefaults.js && node --check server/helpers/ensureProfile.js && node --check server/helpers/privacy.js && node --check server/helpers/profilePhotos.js && node --check server/helpers/livekit.js && node --check server/helpers/nativePushConfig.js && node --check server/helpers/nativePush.js && node --check server/helpers/storage.js && node --check server/helpers/videoPipeline.js && node --check server/helpers/accountArchive.js && node --check server/helpers/accountExport.js && node --check server/helpers/logger.js && node --check server/helpers/chatAbuse.js && node --check server/helpers/chatMentions.js && node --check server/helpers/messageRequests.js && node --check server/helpers/chatReliability.js && node --check server/helpers/chatAiConfig.js && node --check server/helpers/socialAuthConfig.js && node --check server/helpers/e2eeKeyDirectory.js && node --check server/helpers/chatMaintenance.js && node --check server/middleware/chatSendIdempotency.js && node --check server/middleware/roomAccess.js && node --check server/controllers/avatar.js && node --check server/controllers/profile.js && node --check server/controllers/chatUpload.js && node --check server/controllers/chatSecurity.js && node --check server/controllers/chatDeletion.js && node --check server/controllers/chatV2.js && node --check server/controllers/chatAiAdmin.js && node --check server/controllers/chatSuggestions.js && node --check server/controllers/chatResumableUpload.js && node --check server/controllers/socialAuth.js && node --check server/controllers/socialAuthAdmin.js && node --check server/controllers/storagePublic.js && node --check server/controllers/storageAdmin.js && node --check server/controllers/callingAdmin.js && node --check server/controllers/callingPushAdmin.js && node --check server/controllers/callingConfig.js && node --check server/controllers/nativePush.js && node --check server/controllers/accountStorage.js && node --check server/routes/profile.js && node --check server/routes/inbox.js && node --check server/routes/chat.js && node --check server/routes/chatV2.js && node --check server/routes/chatAiAdmin.js && node --check server/routes/socialAuthAdmin.js && node --check server/routes/storagePublic.js && node --check server/routes/storageAdmin.js && node --check server/routes/callingAdmin.js && node --check server/routes/callingPushAdmin.js && node --check server/routes/callingConfig.js && node --check server/routes/setting.js && node --check server/helpers/socketAdapter.js && node --check server/socket/auth.js && node --check server/socket/index.js && node --check server/socket/events/chatV2.js && node --check server/socket/events/calling.js && node --check server/socket/events/callRealtime.js && node --check server/socket/events/backgroundCalling.js && node --check server/socket/events/groupModeration.js && node --check server/socket/events/room.js && node --check server/routes/cron.js && node --check api/index.js", "start": "node server" }, "dependencies": { diff --git a/backend/server/controllers/storagePublic.js b/backend/server/controllers/storagePublic.js new file mode 100644 index 00000000..2ec3b070 --- /dev/null +++ b/backend/server/controllers/storagePublic.js @@ -0,0 +1,111 @@ +const path = require('path'); +const { getStorageConfig } = require('../helpers/storageConfig'); +const { readStorageFileToBuffer } = require('../helpers/storage'); + +const IMAGE_TYPES = new Map([ + ['.png', 'image/png'], + ['.jpg', 'image/jpeg'], + ['.jpeg', 'image/jpeg'], + ['.webp', 'image/webp'], + ['.gif', 'image/gif'], + ['.svg', 'image/svg+xml'], + ['.ico', 'image/x-icon'], + ['.avif', 'image/avif'], +]); + +const safePathname = (value = '') => { + const raw = String(value || '').trim(); + if (!raw) return ''; + try { + return decodeURIComponent(new URL(raw, 'https://syncchat.invalid').pathname); + } catch (error0) { + return ''; + } +}; + +const resolveConfiguredStorageUrl = async (rawUrl = '') => { + const config = await getStorageConfig(); + if (!config.enabled || !config.publicBaseUrl) { + const error = new Error('Image storage is not configured'); + error.statusCode = 503; + throw error; + } + + const raw = String(rawUrl || '').trim(); + if (!raw) { + const error = new Error('Image URL is required'); + error.statusCode = 400; + throw error; + } + + const base = config.publicBaseUrl.replace(/\/+$/, ''); + if (raw === base || raw.startsWith(`${base}/`)) return raw; + + const pathname = safePathname(raw); + if (!pathname || pathname.includes('..')) { + const error = new Error('Invalid image URL'); + error.statusCode = 400; + throw error; + } + + let basePathname = ''; + try { + basePathname = new URL(base).pathname.replace(/\/+$/, ''); + } catch (error0) { + basePathname = ''; + } + + let relative = ''; + if (basePathname && pathname.startsWith(`${basePathname}/`)) { + relative = pathname.slice(basePathname.length + 1); + } else { + const marker = '/uploads/'; + const index = pathname.toLowerCase().indexOf(marker); + if (index >= 0) relative = pathname.slice(index + marker.length); + } + + relative = String(relative || '').replace(/^\/+/, ''); + if (!relative || relative.includes('..')) { + const error = new Error('Image URL does not belong to configured storage'); + error.statusCode = 400; + throw error; + } + + return `${base}/${relative}`; +}; + +exports.image = async (req, res) => { + try { + const storageUrl = await resolveConfiguredStorageUrl(req.query?.url); + const extension = path.extname(safePathname(storageUrl)).toLowerCase(); + const contentType = IMAGE_TYPES.get(extension); + if (!contentType) { + res.status(415).json({ success: false, message: 'Unsupported image type' }); + return; + } + + const buffer = await readStorageFileToBuffer(storageUrl); + if (!buffer?.length) { + res.status(404).json({ success: false, message: 'Image not found' }); + return; + } + + const maxBytes = 20 * 1024 * 1024; + if (buffer.length > maxBytes) { + res.status(413).json({ success: false, message: 'Image is too large to proxy' }); + return; + } + + res.setHeader('Content-Type', contentType); + res.setHeader('Content-Length', String(buffer.length)); + res.setHeader('Cache-Control', 'public, max-age=3600, stale-while-revalidate=86400'); + res.status(200).send(buffer); + } catch (error0) { + res.status(error0.statusCode || 404).json({ + success: false, + message: error0.message || 'Image could not be loaded', + }); + } +}; + +module.exports.resolveConfiguredStorageUrl = resolveConfiguredStorageUrl; diff --git a/backend/server/routes/index.js b/backend/server/routes/index.js index 9ebb932f..c49a1008 100644 --- a/backend/server/routes/index.js +++ b/backend/server/routes/index.js @@ -33,6 +33,7 @@ const report = require('./report'); const contentControls = require('./contentControls'); const appConfig = require('./appConfig'); const callingConfig = require('./callingConfig'); +const storagePublic = require('./storagePublic'); const storageAdmin = require('./storageAdmin'); const callingAdmin = require('./callingAdmin'); const callingPushAdmin = require('./callingPushAdmin'); @@ -58,6 +59,7 @@ router.use(report); router.use(contentControls); router.use(appConfig); router.use(callingConfig); +router.use(storagePublic); router.use(storageAdmin); router.use(callingAdmin); router.use(callingPushAdmin); diff --git a/backend/server/routes/storagePublic.js b/backend/server/routes/storagePublic.js new file mode 100644 index 00000000..98853a47 --- /dev/null +++ b/backend/server/routes/storagePublic.js @@ -0,0 +1,6 @@ +const router = require('express').Router(); +const controller = require('../controllers/storagePublic'); + +router.get('/storage/image', controller.image); + +module.exports = router; diff --git a/frontend/admin/index.html b/frontend/admin/index.html index 7d01aa70..98eba4a0 100644 --- a/frontend/admin/index.html +++ b/frontend/admin/index.html @@ -5,7 +5,7 @@ - +