From 66bca931063d992ca7df8414fa0afe09e03a810c Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Wed, 19 Aug 2026 10:15:40 +0300 Subject: [PATCH 01/10] add DB model for social auth configuration --- .../server/db/models/adminSocialAuthConfig.js | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 backend/server/db/models/adminSocialAuthConfig.js diff --git a/backend/server/db/models/adminSocialAuthConfig.js b/backend/server/db/models/adminSocialAuthConfig.js new file mode 100644 index 00000000..2562b193 --- /dev/null +++ b/backend/server/db/models/adminSocialAuthConfig.js @@ -0,0 +1,46 @@ +const { DataTypes } = require('sequelize'); +const sequelize = require('../sequelize'); + +const AdminSocialAuthConfigModel = sequelize.define( + 'admin_social_auth_configs', + { + _id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + google: { + type: DataTypes.JSON, + allowNull: false, + defaultValue: { + enabled: false, + clientId: '', + clientSecret: '', + }, + }, + facebook: { + type: DataTypes.JSON, + allowNull: false, + defaultValue: { + enabled: false, + appId: '', + appSecret: '', + }, + }, + telegram: { + type: DataTypes.JSON, + allowNull: false, + defaultValue: { + enabled: false, + botUsername: '', + botToken: '', + }, + }, + }, + { + timestamps: true, + version: false, + } +); + +module.exports = AdminSocialAuthConfigModel; From 7579b85c914042b96b962c26e75232acc5a3f149 Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Wed, 19 Aug 2026 10:16:06 +0300 Subject: [PATCH 02/10] add encrypted DB-backed social auth configuration --- backend/server/helpers/socialAuthConfig.js | 262 +++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 backend/server/helpers/socialAuthConfig.js diff --git a/backend/server/helpers/socialAuthConfig.js b/backend/server/helpers/socialAuthConfig.js new file mode 100644 index 00000000..0cf48669 --- /dev/null +++ b/backend/server/helpers/socialAuthConfig.js @@ -0,0 +1,262 @@ +const crypto = require('crypto'); +const AdminSocialAuthConfigModel = require('../db/models/adminSocialAuthConfig'); + +const DEFAULT_SOCIAL_AUTH_CONFIG = Object.freeze({ + google: { + enabled: false, + clientId: '', + clientSecret: '', + }, + facebook: { + enabled: false, + appId: '', + appSecret: '', + }, + telegram: { + enabled: false, + botUsername: '', + botToken: '', + }, +}); + +const CACHE_TTL_MS = 30 * 1000; +let cachedConfig = null; +let cachedAt = 0; + +const secretMaterial = () => + String( + process.env.SOCIAL_AUTH_CONFIG_SECRET || + process.env.CALL_CONFIG_SECRET || + process.env.STORAGE_CONFIG_SECRET || + process.env.JWT_SECRET || + '' + ).trim(); + +const encryptionKey = () => { + const secret = secretMaterial(); + if (!secret) { + throw new Error( + 'SOCIAL_AUTH_CONFIG_SECRET, CALL_CONFIG_SECRET, STORAGE_CONFIG_SECRET, or JWT_SECRET is required to protect social auth credentials' + ); + } + return crypto.createHash('sha256').update(secret).digest(); +}; + +const encryptSecret = (value) => { + const plain = String(value || ''); + if (!plain) return ''; + if (plain.startsWith('enc:v1:')) return plain; + const iv = crypto.randomBytes(12); + const cipher = crypto.createCipheriv('aes-256-gcm', encryptionKey(), iv); + const encrypted = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]); + const tag = cipher.getAuthTag(); + return `enc:v1:${iv.toString('base64url')}:${tag.toString('base64url')}:${encrypted.toString('base64url')}`; +}; + +const decryptSecret = (value) => { + const stored = String(value || ''); + if (!stored) return ''; + if (!stored.startsWith('enc:v1:')) return stored; + const [, version, ivB64, tagB64, dataB64] = stored.split(':'); + if (version !== 'v1' || !ivB64 || !tagB64 || !dataB64) { + throw new Error('Stored social auth credential is invalid'); + } + const decipher = crypto.createDecipheriv( + 'aes-256-gcm', + encryptionKey(), + Buffer.from(ivB64, 'base64url') + ); + decipher.setAuthTag(Buffer.from(tagB64, 'base64url')); + return Buffer.concat([ + decipher.update(Buffer.from(dataB64, 'base64url')), + decipher.final(), + ]).toString('utf8'); +}; + +const cleanBotUsername = (value) => + String(value || '') + .trim() + .replace(/^@+/, ''); + +const normalizeSocialAuthConfig = (raw = {}, { decrypt = true } = {}) => { + const google = raw.google && typeof raw.google === 'object' ? raw.google : {}; + const facebook = raw.facebook && typeof raw.facebook === 'object' ? raw.facebook : {}; + const telegram = raw.telegram && typeof raw.telegram === 'object' ? raw.telegram : {}; + + const googleSecret = String(google.clientSecret || ''); + const facebookSecret = String(facebook.appSecret || ''); + const telegramToken = String(telegram.botToken || ''); + + return { + google: { + enabled: google.enabled === true, + clientId: String(google.clientId || '').trim(), + clientSecret: decrypt ? decryptSecret(googleSecret) : googleSecret, + }, + facebook: { + enabled: facebook.enabled === true, + appId: String(facebook.appId || '').trim(), + appSecret: decrypt ? decryptSecret(facebookSecret) : facebookSecret, + }, + telegram: { + enabled: telegram.enabled === true, + botUsername: cleanBotUsername(telegram.botUsername), + botToken: decrypt ? decryptSecret(telegramToken) : telegramToken, + }, + }; +}; + +const validateSocialAuthConfig = (config) => { + if (config.google.enabled) { + if (!config.google.clientId) { + throw new Error('Google Client ID is required when Google login is enabled'); + } + } + + if (config.facebook.enabled) { + if (!config.facebook.appId) { + throw new Error('Facebook App ID is required when Facebook login is enabled'); + } + if (!config.facebook.appSecret) { + throw new Error('Facebook App Secret is required when Facebook login is enabled'); + } + } + + if (config.telegram.enabled) { + if (!config.telegram.botUsername) { + throw new Error('Telegram bot username is required when Telegram login is enabled'); + } + if (!config.telegram.botToken) { + throw new Error('Telegram bot token is required when Telegram login is enabled'); + } + } +}; + +const loadRow = async () => { + const [row] = await AdminSocialAuthConfigModel.findOrCreate({ + where: {}, + defaults: DEFAULT_SOCIAL_AUTH_CONFIG, + }); + return row; +}; + +const refreshSocialAuthConfigCache = () => { + cachedConfig = null; + cachedAt = 0; +}; + +const getSocialAuthConfig = async () => { + const now = Date.now(); + if (cachedConfig && now - cachedAt < CACHE_TTL_MS) return cachedConfig; + + const row = await loadRow(); + const plain = row?.get ? row.get({ plain: true }) : row; + cachedConfig = normalizeSocialAuthConfig(plain || DEFAULT_SOCIAL_AUTH_CONFIG); + cachedAt = now; + return cachedConfig; +}; + +const getSocialAuthConfigForAdmin = async () => { + const config = await getSocialAuthConfig(); + return { + google: { + enabled: config.google.enabled, + clientId: config.google.clientId, + clientSecretSet: Boolean(config.google.clientSecret), + }, + facebook: { + enabled: config.facebook.enabled, + appId: config.facebook.appId, + appSecretSet: Boolean(config.facebook.appSecret), + }, + telegram: { + enabled: config.telegram.enabled, + botUsername: config.telegram.botUsername, + botTokenSet: Boolean(config.telegram.botToken), + }, + }; +}; + +const getPublicSocialAuthConfig = async () => { + const config = await getSocialAuthConfig(); + return { + googleClientId: config.google.enabled ? config.google.clientId : '', + facebookAppId: config.facebook.enabled ? config.facebook.appId : '', + telegramBotUsername: config.telegram.enabled ? config.telegram.botUsername : '', + }; +}; + +const saveSocialAuthConfig = async (raw = {}) => { + const current = await getSocialAuthConfig(); + const googleInput = raw.google && typeof raw.google === 'object' ? raw.google : {}; + const facebookInput = raw.facebook && typeof raw.facebook === 'object' ? raw.facebook : {}; + const telegramInput = raw.telegram && typeof raw.telegram === 'object' ? raw.telegram : {}; + + const hasGoogleSecret = + Object.prototype.hasOwnProperty.call(googleInput, 'clientSecret') && + String(googleInput.clientSecret || '').trim().length > 0; + const hasFacebookSecret = + Object.prototype.hasOwnProperty.call(facebookInput, 'appSecret') && + String(facebookInput.appSecret || '').trim().length > 0; + const hasTelegramToken = + Object.prototype.hasOwnProperty.call(telegramInput, 'botToken') && + String(telegramInput.botToken || '').trim().length > 0; + + const next = normalizeSocialAuthConfig({ + google: { + ...current.google, + ...googleInput, + clientSecret: hasGoogleSecret + ? String(googleInput.clientSecret) + : current.google.clientSecret, + }, + facebook: { + ...current.facebook, + ...facebookInput, + appSecret: hasFacebookSecret + ? String(facebookInput.appSecret) + : current.facebook.appSecret, + }, + telegram: { + ...current.telegram, + ...telegramInput, + botToken: hasTelegramToken + ? String(telegramInput.botToken) + : current.telegram.botToken, + }, + }); + + validateSocialAuthConfig(next); + + const row = await loadRow(); + await row.update({ + google: { + enabled: next.google.enabled, + clientId: next.google.clientId, + clientSecret: encryptSecret(next.google.clientSecret), + }, + facebook: { + enabled: next.facebook.enabled, + appId: next.facebook.appId, + appSecret: encryptSecret(next.facebook.appSecret), + }, + telegram: { + enabled: next.telegram.enabled, + botUsername: next.telegram.botUsername, + botToken: encryptSecret(next.telegram.botToken), + }, + }); + refreshSocialAuthConfigCache(); + return getSocialAuthConfigForAdmin(); +}; + +module.exports = { + DEFAULT_SOCIAL_AUTH_CONFIG, + normalizeSocialAuthConfig, + validateSocialAuthConfig, + getSocialAuthConfig, + getSocialAuthConfigForAdmin, + getPublicSocialAuthConfig, + saveSocialAuthConfig, + refreshSocialAuthConfigCache, +}; From 9062c24b9d9e56a23506915fb1ce534107053156 Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Wed, 19 Aug 2026 10:16:15 +0300 Subject: [PATCH 03/10] add admin APIs for social auth configuration --- backend/server/controllers/socialAuthAdmin.js | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 backend/server/controllers/socialAuthAdmin.js diff --git a/backend/server/controllers/socialAuthAdmin.js b/backend/server/controllers/socialAuthAdmin.js new file mode 100644 index 00000000..8cac89da --- /dev/null +++ b/backend/server/controllers/socialAuthAdmin.js @@ -0,0 +1,37 @@ +const response = require('../helpers/response'); +const { + getSocialAuthConfigForAdmin, + saveSocialAuthConfig, +} = require('../helpers/socialAuthConfig'); + +exports.getConfig = async (req, res) => { + try { + const payload = await getSocialAuthConfigForAdmin(); + response({ res, payload }); + } catch (error0) { + response({ + res, + statusCode: error0.statusCode || 500, + success: false, + message: error0.message, + }); + } +}; + +exports.updateConfig = async (req, res) => { + try { + const payload = await saveSocialAuthConfig(req.body || {}); + response({ + res, + message: 'Social login configuration saved in MongoDB', + payload, + }); + } catch (error0) { + response({ + res, + statusCode: error0.statusCode || 400, + success: false, + message: error0.message, + }); + } +}; From 15029087cd3bc180c7807b38416a2a235166125c Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Wed, 19 Aug 2026 10:17:04 +0300 Subject: [PATCH 04/10] use DB-backed social login providers at runtime --- backend/server/controllers/socialAuth.js | 431 +++++++++++++++++++++++ 1 file changed, 431 insertions(+) create mode 100644 backend/server/controllers/socialAuth.js diff --git a/backend/server/controllers/socialAuth.js b/backend/server/controllers/socialAuth.js new file mode 100644 index 00000000..b88533ce --- /dev/null +++ b/backend/server/controllers/socialAuth.js @@ -0,0 +1,431 @@ +const crypto = require('crypto'); +const axios = require('axios'); + +const UserModel = require('../db/models/user'); +const ProfileModel = require('../db/models/profile'); +const SettingModel = require('../db/models/setting'); +const { asArray } = require('../db/utils'); +const response = require('../helpers/response'); +const encrypt = require('../helpers/encrypt'); +const { applyDefaultSettings } = require('../helpers/appConfig'); +const { DEFAULT_USER_AVATAR_URL } = require('../helpers/avatarDefaults'); +const { + getSocialAuthConfig, + getPublicSocialAuthConfig, +} = require('../helpers/socialAuthConfig'); +const { + createSession, + notifySuspiciousLogin, + signTwoFactorTempToken, + signUserToken, +} = require('../helpers/userSessions'); + +const SOCIAL_EMAIL_DOMAIN = 'social.syncchat.local'; + +const createError = (statusCode, message) => { + const error = new Error(message); + error.statusCode = statusCode; + return error; +}; + +const normalizeUsernameSeed = (value = '') => + String(value || '') + .trim() + .toLowerCase() + .replace(/[^a-z0-9_]+/g, '_') + .replace(/_{2,}/g, '_') + .replace(/^_+|_+$/g, '') + .slice(0, 24); + +const buildSyntheticEmail = ({ provider, providerUserId }) => + `${provider}_${providerUserId}@${SOCIAL_EMAIL_DOMAIN}`; + +const mergeSocialAccounts = ({ current = [], next }) => { + const list = asArray(current).filter( + (item) => item?.provider && item?.providerId + ); + const exists = list.find( + (item) => + item.provider === next.provider && + String(item.providerId) === String(next.providerId) + ); + + if (exists) { + return list.map((item) => + item.provider === next.provider && + String(item.providerId) === String(next.providerId) + ? { + ...item, + ...next, + linkedAt: item.linkedAt || next.linkedAt, + } + : item + ); + } + + return [...list, next]; +}; + +const generateUniqueUsername = async (seed) => { + const normalized = normalizeUsernameSeed(seed); + const base = normalized.length >= 3 ? normalized : `${normalized}user`.slice(0, 24); + const prefix = base.slice(0, 18); + + for (let i = 0; i < 2000; i += 1) { + const suffix = i === 0 ? '' : String(i); + const candidate = `${prefix}${suffix}`.slice(0, 24); + // eslint-disable-next-line no-await-in-loop + const existing = await UserModel.findOne({ + where: { username: candidate }, + attributes: ['_id'], + }); + if (!existing) return candidate; + } + + return `user_${crypto.randomBytes(4).toString('hex')}`.slice(0, 24); +}; + +const buildLoginPayload = async ({ user, req, authProvider }) => { + if (user?.status === 'blocked') throw createError(403, 'Account is blocked'); + if (user?.status === 'banned') { + throw createError(403, 'You are banned from SyncChat.'); + } + if (user?.status === 'deleted') throw createError(403, 'Account is deleted'); + + const setting = await SettingModel.findOne({ + where: { userId: user._id }, + attributes: ['twoFactorEnabled', 'twoFactorSecret'], + }); + + if (setting?.twoFactorEnabled && setting?.twoFactorSecret) { + return { + requiresTwoFactor: true, + tempToken: signTwoFactorTempToken({ userId: user._id }), + }; + } + + const session = await createSession({ + userId: user._id, + req, + authProvider, + }); + notifySuspiciousLogin(session); + + return { + requiresTwoFactor: false, + token: signUserToken({ userId: user._id, sessionId: session._id }), + }; +}; + +const verifyGooglePayload = async ({ credential }, config) => { + if (!config.google.enabled) throw createError(403, 'Google login is disabled'); + if (!config.google.clientId) { + throw createError(503, 'Google login is not configured'); + } + if (!credential) throw createError(400, 'Google credential is required'); + + const { data } = await axios.get('https://oauth2.googleapis.com/tokeninfo', { + params: { id_token: credential }, + timeout: 12000, + }); + + if (String(data?.aud || '') !== config.google.clientId) { + throw createError(401, 'Google token audience mismatch'); + } + if (!data?.sub) throw createError(401, 'Invalid Google account payload'); + + return { + provider: 'google', + providerUserId: String(data.sub), + email: String(data.email || '').trim().toLowerCase() || null, + fullname: String(data.name || data.given_name || 'Google User').trim(), + usernameHint: String( + data.email || data.name || data.given_name || 'google_user' + ) + .split('@')[0] + .trim(), + avatar: data.picture || null, + }; +}; + +const verifyFacebookPayload = async ({ accessToken }, config) => { + if (!config.facebook.enabled) { + throw createError(403, 'Facebook login is disabled'); + } + if (!config.facebook.appId || !config.facebook.appSecret) { + throw createError(503, 'Facebook login is not fully configured'); + } + if (!accessToken) { + throw createError(400, 'Facebook access token is required'); + } + + const appAccessToken = `${config.facebook.appId}|${config.facebook.appSecret}`; + const debug = await axios.get('https://graph.facebook.com/debug_token', { + params: { + input_token: accessToken, + access_token: appAccessToken, + }, + timeout: 12000, + }); + const debugData = debug?.data?.data || {}; + if ( + debugData.is_valid !== true || + String(debugData.app_id || '') !== String(config.facebook.appId) + ) { + throw createError(401, 'Facebook access token is not valid for this app'); + } + + const { data } = await axios.get('https://graph.facebook.com/me', { + params: { + fields: 'id,name,email,picture.width(256).height(256)', + access_token: accessToken, + }, + timeout: 12000, + }); + + if (!data?.id) throw createError(401, 'Invalid Facebook account payload'); + + return { + provider: 'facebook', + providerUserId: String(data.id), + email: + String(data.email || '').trim().toLowerCase() || + buildSyntheticEmail({ + provider: 'facebook', + providerUserId: String(data.id), + }), + fullname: String(data.name || 'Facebook User').trim(), + usernameHint: String(data.name || `facebook_${data.id}`).trim(), + avatar: data?.picture?.data?.url || null, + }; +}; + +const verifyTelegramPayload = async ({ telegram }, config) => { + if (!config.telegram.enabled) { + throw createError(403, 'Telegram login is disabled'); + } + const botToken = String(config.telegram.botToken || ''); + if (!config.telegram.botUsername || !botToken) { + throw createError(503, 'Telegram login is not fully configured'); + } + if (!telegram || typeof telegram !== 'object') { + throw createError(400, 'Telegram payload is required'); + } + + const hash = String(telegram.hash || ''); + if (!hash) throw createError(401, 'Invalid Telegram payload hash'); + + const allowedKeys = [ + 'id', + 'first_name', + 'last_name', + 'username', + 'photo_url', + 'auth_date', + 'hash', + ]; + const compact = {}; + allowedKeys.forEach((key) => { + if (telegram[key] !== undefined && telegram[key] !== null) { + compact[key] = telegram[key]; + } + }); + + const dataCheckString = Object.keys(compact) + .filter((key) => key !== 'hash') + .sort() + .map((key) => `${key}=${compact[key]}`) + .join('\n'); + + const secretKey = crypto.createHash('sha256').update(botToken).digest(); + const expectedHash = crypto + .createHmac('sha256', secretKey) + .update(dataCheckString) + .digest('hex'); + + const expectedBuffer = Buffer.from(expectedHash, 'hex'); + const actualBuffer = Buffer.from(hash, 'hex'); + if ( + expectedBuffer.length !== actualBuffer.length || + !crypto.timingSafeEqual(expectedBuffer, actualBuffer) + ) { + throw createError(401, 'Telegram verification failed'); + } + + const authDate = Number(compact.auth_date || 0); + const nowSec = Math.floor(Date.now() / 1000); + if (!authDate || Math.abs(nowSec - authDate) > 24 * 60 * 60) { + throw createError(401, 'Telegram auth data expired'); + } + + const providerUserId = String(compact.id || ''); + if (!providerUserId) { + throw createError(401, 'Invalid Telegram account payload'); + } + + const fullname = String( + [compact.first_name || '', compact.last_name || ''].join(' ').trim() || + 'Telegram User' + ); + + return { + provider: 'telegram', + providerUserId, + email: buildSyntheticEmail({ provider: 'telegram', providerUserId }), + fullname, + usernameHint: String( + compact.username || compact.first_name || `telegram_${providerUserId}` + ), + avatar: compact.photo_url || null, + }; +}; + +const verifySocialPayload = async ({ provider, payload, config }) => { + if (provider === 'google') return verifyGooglePayload(payload || {}, config); + if (provider === 'facebook') { + return verifyFacebookPayload(payload || {}, config); + } + if (provider === 'telegram') { + return verifyTelegramPayload(payload || {}, config); + } + throw createError(400, 'Unsupported social provider'); +}; + +const findUserBySocialIdentity = async ({ email, provider, providerUserId }) => { + if (email) { + const user = await UserModel.findOne({ where: { email } }); + if (user) return user; + } + + const profiles = await ProfileModel.findAll({ + attributes: ['userId', 'socialAccounts'], + }); + const profile = profiles + .map((item) => (item?.get ? item.get({ plain: true }) : item)) + .find((item) => + asArray(item?.socialAccounts).some( + (entry) => + entry?.provider === provider && + String(entry?.providerId || '') === String(providerUserId) + ) + ); + + if (!profile?.userId) return null; + return UserModel.findOne({ where: { _id: profile.userId } }); +}; + +const upsertSocialUser = async (socialData) => { + const nowIso = new Date().toISOString(); + const socialAccount = { + provider: socialData.provider, + providerId: socialData.providerUserId, + username: socialData.usernameHint || '', + linkedAt: nowIso, + }; + + let user = await findUserBySocialIdentity({ + email: socialData.email, + provider: socialData.provider, + providerUserId: socialData.providerUserId, + }); + + if (!user) { + const username = await generateUniqueUsername( + socialData.usernameHint || socialData.fullname || socialData.provider + ); + const fullname = + String(socialData.fullname || username).trim().slice(0, 32) || username; + const email = + String(socialData.email || '').trim().toLowerCase() || + buildSyntheticEmail({ + provider: socialData.provider, + providerUserId: socialData.providerUserId, + }); + + user = await UserModel.create({ + username, + fullname, + email, + password: encrypt(crypto.randomBytes(32).toString('hex')), + verified: true, + otp: null, + }); + + await SettingModel.create(await applyDefaultSettings({ userId: user._id })); + await ProfileModel.create({ + userId: user._id, + username, + fullname, + email, + avatar: socialData.avatar || DEFAULT_USER_AVATAR_URL, + socialAccounts: [socialAccount], + }); + + return { user, created: true }; + } + + const profile = await ProfileModel.findOne({ where: { userId: user._id } }); + if (profile) { + await profile.update({ + socialAccounts: mergeSocialAccounts({ + current: profile.socialAccounts, + next: socialAccount, + }), + avatar: profile.avatar || socialData.avatar || DEFAULT_USER_AVATAR_URL, + fullname: profile.fullname || socialData.fullname || user.fullname, + email: profile.email || user.email, + }); + } + + if (!user.verified) await user.update({ verified: true, otp: null }); + return { user, created: false }; +}; + +exports.socialConfig = async (req, res) => { + try { + response({ res, payload: await getPublicSocialAuthConfig() }); + } catch (error0) { + response({ + res, + statusCode: error0.statusCode || 500, + success: false, + message: error0.message, + }); + } +}; + +exports.socialAuth = async (req, res) => { + try { + const provider = String(req.body?.provider || '') + .trim() + .toLowerCase(); + const payload = req.body?.payload || {}; + const config = await getSocialAuthConfig(); + const socialData = await verifySocialPayload({ provider, payload, config }); + const { user, created } = await upsertSocialUser(socialData); + const loginPayload = await buildLoginPayload({ + user, + req, + authProvider: provider || 'social', + }); + + response({ + res, + statusCode: created ? 201 : 200, + message: created + ? `Account created with ${provider}` + : `Signed in with ${provider}`, + payload: loginPayload, + }); + } catch (error0) { + response({ + res, + statusCode: error0.statusCode || error0.response?.status || 500, + success: false, + message: + error0.statusCode || !error0.response + ? error0.message + : 'Social provider verification failed', + }); + } +}; From 1c030408376c2efecc5305d4ba542e3a7d45f681 Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Wed, 19 Aug 2026 10:17:22 +0300 Subject: [PATCH 05/10] route social login through DB-backed controller --- backend/server/routes/user.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/server/routes/user.js b/backend/server/routes/user.js index 12d647f3..ddaaaa34 100644 --- a/backend/server/routes/user.js +++ b/backend/server/routes/user.js @@ -2,14 +2,15 @@ const router = require('express').Router(); const authenticate = require('../middleware/auth'); const ctrl = require('../controllers/user'); +const socialAuth = require('../controllers/socialAuth'); router.post('/users/register', ctrl.register); router.post('/users/login', ctrl.login); router.post('/users/login/2fa-verify', ctrl.verifyLoginTwoFactor); router.post('/users/device-link/info', ctrl.deviceLinkInfo); router.post('/users/device-link/complete', ctrl.completeDeviceLink); -router.get('/users/social-config', ctrl.socialConfig); -router.post('/users/social-auth', ctrl.socialAuth); +router.get('/users/social-config', socialAuth.socialConfig); +router.post('/users/social-auth', socialAuth.socialAuth); router.post('/users/forgot-pass/request', ctrl.requestForgotPass); router.post('/users/forgot-pass/verify', ctrl.verifyForgotPass); router.post('/users/forgot-pass/reset', ctrl.resetForgotPass); From df73f28362514c053c030969fcbd6f68b59cbcec Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Wed, 19 Aug 2026 10:17:31 +0300 Subject: [PATCH 06/10] add admin social auth configuration routes --- backend/server/routes/socialAuthAdmin.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 backend/server/routes/socialAuthAdmin.js diff --git a/backend/server/routes/socialAuthAdmin.js b/backend/server/routes/socialAuthAdmin.js new file mode 100644 index 00000000..07d4606a --- /dev/null +++ b/backend/server/routes/socialAuthAdmin.js @@ -0,0 +1,21 @@ +const router = require('express').Router(); +const adminAuth = require('../middleware/adminAuth'); +const { requirePermission } = require('../middleware/adminPermission'); +const { PERMISSIONS } = require('../helpers/adminPermissions'); +const ctrl = require('../controllers/socialAuthAdmin'); + +router.get( + '/admin/social-auth/config', + adminAuth, + requirePermission(PERMISSIONS.APP_CONFIG_READ), + ctrl.getConfig +); + +router.patch( + '/admin/social-auth/config', + adminAuth, + requirePermission(PERMISSIONS.APP_CONFIG_WRITE), + ctrl.updateConfig +); + +module.exports = router; From 5df8901877a58e7d313b1ca3e5f35c2e64edbd28 Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Wed, 19 Aug 2026 10:18:01 +0300 Subject: [PATCH 07/10] mount social auth admin routes --- backend/server/routes/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/server/routes/index.js b/backend/server/routes/index.js index 02a3736b..9ebb932f 100644 --- a/backend/server/routes/index.js +++ b/backend/server/routes/index.js @@ -36,6 +36,7 @@ const callingConfig = require('./callingConfig'); const storageAdmin = require('./storageAdmin'); const callingAdmin = require('./callingAdmin'); const callingPushAdmin = require('./callingPushAdmin'); +const socialAuthAdmin = require('./socialAuthAdmin'); const chatAiAdmin = require('./chatAiAdmin'); const adminProfileSecurity = require('./adminProfileSecurity'); const admin = require('./admin'); @@ -60,6 +61,7 @@ router.use(callingConfig); router.use(storageAdmin); router.use(callingAdmin); router.use(callingPushAdmin); +router.use(socialAuthAdmin); router.use(chatAiAdmin); router.use(adminProfileSecurity); router.use(admin); From 84597830b28dae109d11cc767d81b38b9c46d3a4 Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Wed, 19 Aug 2026 10:18:45 +0300 Subject: [PATCH 08/10] add DB-backed social login admin page --- frontend/admin/socialAuth.jsx | 399 ++++++++++++++++++++++++++++++++++ 1 file changed, 399 insertions(+) create mode 100644 frontend/admin/socialAuth.jsx diff --git a/frontend/admin/socialAuth.jsx b/frontend/admin/socialAuth.jsx new file mode 100644 index 00000000..ef3e1904 --- /dev/null +++ b/frontend/admin/socialAuth.jsx @@ -0,0 +1,399 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import * as ReactDOM from 'react-dom/client'; +import axios from 'axios'; +import config from './config'; +import './style.css'; + +axios.defaults.baseURL = config.apiBaseUrl; + +const initial = { + google: { + enabled: false, + clientId: '', + clientSecret: '', + clientSecretSet: false, + }, + facebook: { + enabled: false, + appId: '', + appSecret: '', + appSecretSet: false, + }, + telegram: { + enabled: false, + botUsername: '', + botToken: '', + botTokenSet: false, + }, +}; + +const styles = { + page: { + minHeight: '100vh', + background: '#f5f7fb', + padding: '32px 16px', + color: '#172033', + }, + card: { + maxWidth: 1040, + margin: '0 auto', + background: '#fff', + borderRadius: 18, + padding: 28, + boxShadow: '0 18px 50px rgba(24,39,75,.08)', + }, + provider: { + border: '1px solid #e3e8f0', + borderRadius: 16, + padding: 20, + marginTop: 18, + }, + row: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit,minmax(240px,1fr))', + gap: 16, + }, + field: { + display: 'flex', + flexDirection: 'column', + gap: 7, + marginBottom: 14, + }, + input: { + minHeight: 44, + border: '1px solid #d8deea', + borderRadius: 10, + padding: '10px 12px', + fontSize: 14, + background: '#fff', + }, + help: { fontSize: 12, color: '#697386', lineHeight: 1.5 }, + button: { + border: 0, + borderRadius: 10, + padding: '11px 18px', + fontWeight: 700, + cursor: 'pointer', + }, +}; + +function SocialAuthAdmin() { + const [form, setForm] = useState(initial); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [message, setMessage] = useState(''); + const [error, setError] = useState(''); + const token = useMemo(() => localStorage.getItem('admin_token') || '', []); + + useEffect(() => { + if (!token) { + window.location.replace('/admin'); + return; + } + axios.defaults.headers.Authorization = `Bearer ${token}`; + axios + .get('/admin/social-auth/config') + .then(({ data }) => { + const payload = data?.payload || {}; + setForm({ + google: { + ...initial.google, + ...(payload.google || {}), + clientSecret: '', + }, + facebook: { + ...initial.facebook, + ...(payload.facebook || {}), + appSecret: '', + }, + telegram: { + ...initial.telegram, + ...(payload.telegram || {}), + botToken: '', + }, + }); + }) + .catch((err) => setError(err?.response?.data?.message || err.message)) + .finally(() => setLoading(false)); + }, [token]); + + const setProvider = (provider, key, value) => { + setForm((prev) => ({ + ...prev, + [provider]: { + ...prev[provider], + [key]: value, + }, + })); + }; + + const buildPayload = () => { + const payload = { + google: { + enabled: Boolean(form.google.enabled), + clientId: String(form.google.clientId || '').trim(), + }, + facebook: { + enabled: Boolean(form.facebook.enabled), + appId: String(form.facebook.appId || '').trim(), + }, + telegram: { + enabled: Boolean(form.telegram.enabled), + botUsername: String(form.telegram.botUsername || '') + .trim() + .replace(/^@+/, ''), + }, + }; + if (String(form.google.clientSecret || '').trim()) { + payload.google.clientSecret = form.google.clientSecret; + } + if (String(form.facebook.appSecret || '').trim()) { + payload.facebook.appSecret = form.facebook.appSecret; + } + if (String(form.telegram.botToken || '').trim()) { + payload.telegram.botToken = form.telegram.botToken; + } + return payload; + }; + + const save = async () => { + setSaving(true); + setMessage(''); + setError(''); + try { + const { data } = await axios.patch( + '/admin/social-auth/config', + buildPayload() + ); + const payload = data?.payload || {}; + setForm((prev) => ({ + google: { + ...prev.google, + ...(payload.google || {}), + clientSecret: '', + }, + facebook: { + ...prev.facebook, + ...(payload.facebook || {}), + appSecret: '', + }, + telegram: { + ...prev.telegram, + ...(payload.telegram || {}), + botToken: '', + }, + })); + setMessage(data?.message || 'Social login configuration saved'); + } catch (err) { + setError(err?.response?.data?.message || err.message); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+
Loading social login settings…
+
+ ); + } + + return ( +
+
+
+
+

Social Login

+

+ Google, Facebook and Telegram credentials are loaded from MongoDB. + Provider secrets are encrypted at rest and are never returned to the browser. +

+
+ + ← Admin + +
+ + {error && ( +
+ {error} +
+ )} + {message && ( +
+ {message} +
+ )} + +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+
+
+ ); +} + +const root = ReactDOM.createRoot(document.querySelector('#social-auth-root')); +root.render(); From 69350da06334983c07f1ee9884abc817592ee00e Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Wed, 19 Aug 2026 10:18:54 +0300 Subject: [PATCH 09/10] add social login admin HTML shell --- frontend/admin/socialAuth.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 frontend/admin/socialAuth.html diff --git a/frontend/admin/socialAuth.html b/frontend/admin/socialAuth.html new file mode 100644 index 00000000..45cd444d --- /dev/null +++ b/frontend/admin/socialAuth.html @@ -0,0 +1,14 @@ + + + + + + + + + SyncChat Admin — Social Login + + +
+ + From 073667b34f98fb8e2cd25c954dd5f69ec1947b0e Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Wed, 19 Aug 2026 10:19:11 +0300 Subject: [PATCH 10/10] add admin sidebar tool links --- frontend/admin/sidebarTools.js | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 frontend/admin/sidebarTools.js diff --git a/frontend/admin/sidebarTools.js b/frontend/admin/sidebarTools.js new file mode 100644 index 00000000..b79cb28f --- /dev/null +++ b/frontend/admin/sidebarTools.js @@ -0,0 +1,63 @@ +const TOOL_LINKS = [ + { label: 'Native Call Push', href: '/admin/calling-push' }, + { label: 'Calling & WebRTC', href: '/admin/calling' }, + { label: 'FTP Storage', href: '/admin/storage' }, + { label: 'Social Login', href: '/admin/social-auth' }, +]; + +let observer = null; + +const createDivider = () => { + const divider = document.createElement('div'); + divider.dataset.syncchatAdminToolsDivider = '1'; + divider.textContent = 'Infrastructure'; + Object.assign(divider.style, { + marginTop: '8px', + padding: '8px 4px 2px', + color: '#94a3b8', + fontSize: '11px', + fontWeight: '700', + letterSpacing: '0.12em', + textTransform: 'uppercase', + }); + return divider; +}; + +const createToolButton = ({ label, href }) => { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'nav-item'; + button.dataset.syncchatAdminTool = href; + button.textContent = label; + button.addEventListener('click', () => { + window.location.assign(href); + }); + return button; +}; + +const ensureAdminSidebarTools = () => { + const nav = document.querySelector('.admin-sidebar .admin-nav'); + if (!nav) return; + + if (!nav.querySelector('[data-syncchat-admin-tools-divider="1"]')) { + nav.appendChild(createDivider()); + } + + TOOL_LINKS.forEach((item) => { + if (nav.querySelector(`[data-syncchat-admin-tool="${item.href}"]`)) return; + nav.appendChild(createToolButton(item)); + }); +}; + +export const installAdminSidebarTools = () => { + ensureAdminSidebarTools(); + if (observer) return; + + observer = new MutationObserver(() => ensureAdminSidebarTools()); + observer.observe(document.documentElement, { + childList: true, + subtree: true, + }); +}; + +export default installAdminSidebarTools;