Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
111 changes: 111 additions & 0 deletions backend/server/controllers/storagePublic.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions backend/server/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions backend/server/routes/storagePublic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const router = require('express').Router();
const controller = require('../controllers/storagePublic');

router.get('/storage/image', controller.image);

module.exports = router;
2 changes: 1 addition & 1 deletion frontend/admin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0ea5e9" />
<link rel="icon" type="image/png" href="https://syncchat.smshagor.com/uploads/favicon.png" />
<link rel="icon" type="image/png" href="/pwa-192x192.png" />
<link rel="apple-touch-icon" sizes="512x512" href="/pwa-512x512.png" />
<link rel="manifest" href="/manifest.json" />
<title>SyncChat Admin</title>
Expand Down
2 changes: 1 addition & 1 deletion frontend/admin/socialAuth.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0ea5e9" />
<link rel="icon" href="https://syncchat.smshagor.com/uploads/favicon.png" />
<link rel="icon" type="image/png" href="/pwa-192x192.png" />
<title>SyncChat Admin — Social Login</title>
</head>
<body>
Expand Down
29 changes: 24 additions & 5 deletions frontend/client/helpers/imageFallbacks.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { toStorageImageProxyUrl } from './storageImageUrl';

const FALLBACKS = [
{
match: /(?:default-group-avatar\.png|\/assets\/icons\/group-avatar\.svg)(?:[?#].*)?$/i,
Expand All @@ -20,6 +22,7 @@ const getLegacyFallback = (img) => {
};

const getErrorFallback = (img) => {
const original = String(img?.dataset?.syncchatOriginalSrc || '').trim();
const legacyFallback = getLegacyFallback(img);
if (legacyFallback) return legacyFallback;

Expand All @@ -28,15 +31,22 @@ const getErrorFallback = (img) => {
.toLowerCase();
if (alt === 'syncchat') return '/pwa-192x192.png';

if (/(?:^|\/)uploads\/avatars\//i.test(original)) {
return '/assets/icons/user-avatar.svg';
}

return '';
};

const bindImage = (img) => {
if (!(img instanceof HTMLImageElement)) return;

const legacyFallback = getLegacyFallback(img);
const current = String(img.getAttribute('src') || '');
if (current && !img.dataset.syncchatOriginalSrc) {
img.dataset.syncchatOriginalSrc = current;
}

const legacyFallback = getLegacyFallback(img);
if (
legacyFallback &&
current &&
Expand All @@ -50,11 +60,20 @@ const bindImage = (img) => {
img.dataset.syncchatFallbackBound = '1';

img.addEventListener('error', () => {
const fallback = getErrorFallback(img);
if (!fallback) return;
const active = String(img.getAttribute('src') || '').trim();
const original = String(img.dataset.syncchatOriginalSrc || active).trim();

if (img.dataset.syncchatStorageProxyTried !== '1') {
const proxyUrl = toStorageImageProxyUrl(original);
if (proxyUrl && proxyUrl !== active) {
img.dataset.syncchatStorageProxyTried = '1';
img.src = proxyUrl;
return;
}
}

const active = String(img.getAttribute('src') || '');
if (active.endsWith(fallback)) return;
const fallback = getErrorFallback(img);
if (!fallback || active.endsWith(fallback)) return;
img.src = fallback;
});
};
Expand Down
42 changes: 42 additions & 0 deletions frontend/client/helpers/runtimeBranding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import axios from 'axios';
import config from '../config';
import { toStorageImageProxyUrl } from './storageImageUrl';

const setIconHref = (href) => {
const value = String(href || '').trim() || '/pwa-192x192.png';
let icon = document.querySelector('link[rel="icon"]');
if (!icon) {
icon = document.createElement('link');
icon.setAttribute('rel', 'icon');
document.head.appendChild(icon);
}
icon.removeAttribute('type');
icon.setAttribute('href', value);
};

const applyRuntimeBranding = (payload = {}) => {
if (payload.appName) {
config.brandName = String(payload.appName || config.brandName);
document.title = config.brandName;
}

const appLogo = String(payload.appLogo || '').trim();
if (appLogo) {
config.brandLogo = appLogo;
setIconHref(toStorageImageProxyUrl(appLogo) || appLogo);
} else {
setIconHref('/pwa-192x192.png');
}
};

const installRuntimeBranding = () => {
if (typeof window === 'undefined' || typeof document === 'undefined') return;

axios
.get('/app-config', { baseURL: config.apiBaseUrl })
.then(({ data }) => applyRuntimeBranding(data?.payload || {}))
.catch(() => setIconHref('/pwa-192x192.png'));
};

export { applyRuntimeBranding };
export default installRuntimeBranding;
26 changes: 26 additions & 0 deletions frontend/client/helpers/storageImageUrl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import config from '../config';

const normalizeApiBase = () => String(config.apiBaseUrl || '/api').replace(/\/+$/, '');

const isStorageProxyUrl = (value = '') =>
String(value || '').includes('/storage/image?url=');

const isStoredUploadUrl = (value = '') => {
const raw = String(value || '').trim();
if (!raw || raw.startsWith('data:') || raw.startsWith('blob:')) return false;

try {
const parsed = new URL(raw, window.location.origin);
return /(?:^|\/)uploads\//i.test(parsed.pathname);
} catch (error0) {
return /(?:^|\/)uploads\//i.test(raw);
}
};

const toStorageImageProxyUrl = (value = '') => {
const raw = String(value || '').trim();
if (!raw || !isStoredUploadUrl(raw) || isStorageProxyUrl(raw)) return '';
return `${normalizeApiBase()}/storage/image?url=${encodeURIComponent(raw)}`;
};

export { isStoredUploadUrl, isStorageProxyUrl, toStorageImageProxyUrl };
2 changes: 1 addition & 1 deletion frontend/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<meta name="description" content="SyncChat is an instant messaging app that allows you to quickly share text messages, emojis, photos, or files with other SyncChat users." />

<meta name="theme-color" content="#0ea5e9" />
<link rel="icon" type="image/png" href="https://syncchat.smshagor.com/uploads/favicon.png" />
<link rel="icon" type="image/png" href="/pwa-192x192.png" />
<link rel="apple-touch-icon" sizes="512x512" href="/pwa-512x512.png" />
<link rel="manifest" href="/manifest.json" />
<meta name="mobile-web-app-capable" content="yes" />
Expand Down
2 changes: 2 additions & 0 deletions frontend/client/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import installChatHttpReliability from './helpers/chatHttpReliability';
import installMentionAutocompleteV2 from './helpers/mentionAutocompleteV2';
import installChatToolsSettingsBridge from './helpers/chatToolsSettingsBridge';
import installImageFallbacks from './helpers/imageFallbacks';
import installRuntimeBranding from './helpers/runtimeBranding';
import './styles/chatToolsTheme.css';
import './styles/layoutFixes.css';

Expand Down Expand Up @@ -43,6 +44,7 @@ installChatDraftV2();
installMentionAutocompleteV2();
installChatToolsSettingsBridge();
installImageFallbacks();
installRuntimeBranding();
registerServiceWorker();

requestNotificationPermission();
Loading
Loading