diff --git a/apps/dashboard-api/src/controllers/project.controller.js b/apps/dashboard-api/src/controllers/project.controller.js index 2e544aee6..0a976c2b9 100644 --- a/apps/dashboard-api/src/controllers/project.controller.js +++ b/apps/dashboard-api/src/controllers/project.controller.js @@ -723,22 +723,62 @@ module.exports.updateExternalConfig = async (req, res) => { updateData["resources.db.isExternal"] = true; console.log("Verifying connection to:", projectId); + + let dashboardIp = null; + let publicApiIp = null; + try { + dashboardIp = await getPublicIp(); + + // 1. Test local connection (Dashboard API) const tempConn = mongoose.createConnection(dbUri, { serverSelectionTimeoutMS: 5000, }); await tempConn.asPromise(); await tempConn.close(); + + // 2. Test remote connection (Public API) if PUBLIC_API_URL is configured + if (process.env.PUBLIC_API_URL && process.env.INTERNAL_SECRET) { + const publicApiRes = await axios.post(`${process.env.PUBLIC_API_URL}/api/internal/test-db`, + { dbUri }, + { headers: { 'x-internal-secret': process.env.INTERNAL_SECRET }, timeout: 8000 } + ); + // If it didn't throw, it succeeded. + } else { + console.warn("Skipping Public API DB verification because PUBLIC_API_URL or INTERNAL_SECRET is missing."); + } + } catch (connErr) { console.error("Verification Connection Failed:", connErr.message); let errorMsg = "Could not connect to the provided MongoDB URI."; - - if ( + + // If the error came from Axios (Public API verification failed) + if (connErr.response && connErr.response.data && !connErr.response.data.success) { + errorMsg = connErr.response.data.message; + if (connErr.response.data.serverIp) { + publicApiIp = connErr.response.data.serverIp; + } + } else if ( connErr.message.includes("Server selection timed out") || connErr.message.includes("Could not connect") ) { - const serverIp = await getPublicIp(); - errorMsg = `Access Denied: Please whitelist Server IP [${serverIp}] in MongoDB Atlas.`; + // Local failure + errorMsg = `Access Denied: Please whitelist Server IPs in MongoDB Atlas.`; + } + + // Fetch Public API IP if we don't have it yet, to show both in the error message + if (!publicApiIp && process.env.PUBLIC_API_URL) { + try { + const ipRes = await axios.get(`${process.env.PUBLIC_API_URL}/api/server-ip`, { timeout: 3000 }); + if (ipRes.data && ipRes.data.ip) publicApiIp = ipRes.data.ip; + } catch (e) { + console.error("Failed to fetch public API IP for error message", e.message); + } + } + + if (dashboardIp || publicApiIp) { + const ips = [dashboardIp, publicApiIp].filter(Boolean).join(", "); + errorMsg = `Access Denied: Please whitelist Server IPs [${ips}] in MongoDB Atlas.`; } return res.status(400).json({ success: false, data: {}, message: errorMsg }); diff --git a/apps/public-api/src/app.js b/apps/public-api/src/app.js index 8c5ea76d8..951ea7327 100644 --- a/apps/public-api/src/app.js +++ b/apps/public-api/src/app.js @@ -79,6 +79,9 @@ app.use('/api/storage', limiter, logger, storageRoute); app.use('/api/mail', limiter, logger, mailRoute); app.use('/api/health', limiter, logger, healthRoute); +const internalRoute = require('./routes/internal'); +app.use('/api/internal', internalRoute); + app.get('/api/server-ip', async (req, res) => { const ip = await getPublicIp(); res.json({ ip }); diff --git a/apps/public-api/src/routes/internal.js b/apps/public-api/src/routes/internal.js new file mode 100644 index 000000000..dd532f083 --- /dev/null +++ b/apps/public-api/src/routes/internal.js @@ -0,0 +1,53 @@ +const express = require('express'); +const mongoose = require('mongoose'); +const { getPublicIp } = require('@urbackend/common'); +const router = express.Router(); + +// Middleware to protect internal routes +const internalAuth = (req, res, next) => { + const secret = process.env.INTERNAL_SECRET; + if (!secret) { + console.error("INTERNAL_SECRET is not configured on Public API"); + return res.status(500).json({ success: false, message: "Server misconfiguration" }); + } + + const providedSecret = req.headers['x-internal-secret']; + if (!providedSecret || providedSecret !== secret) { + return res.status(403).json({ success: false, message: "Forbidden: Invalid internal secret" }); + } + + next(); +}; + +router.post('/test-db', internalAuth, async (req, res) => { + const { dbUri } = req.body; + if (!dbUri) { + return res.status(400).json({ success: false, message: "dbUri is required" }); + } + + console.log("[Internal] Verifying connection for external DB..."); + try { + const tempConn = mongoose.createConnection(dbUri, { + serverSelectionTimeoutMS: 5000, + }); + await tempConn.asPromise(); + await tempConn.close(); + return res.status(200).json({ success: true, message: "Connection verified" }); + } catch (connErr) { + console.error("[Internal] Verification Connection Failed:", connErr.message); + let errorMsg = "Could not connect to the provided MongoDB URI."; + let serverIp = null; + + if ( + connErr.message.includes("Server selection timed out") || + connErr.message.includes("Could not connect") + ) { + serverIp = await getPublicIp(); + errorMsg = `Access Denied: Please whitelist Server IP [${serverIp}] in MongoDB Atlas.`; + } + + return res.status(400).json({ success: false, message: errorMsg, serverIp }); + } +}); + +module.exports = router; diff --git a/apps/web-dashboard/src/components/Settings/DatabaseConfigForm.jsx b/apps/web-dashboard/src/components/Settings/DatabaseConfigForm.jsx index a47f6e763..435315573 100644 --- a/apps/web-dashboard/src/components/Settings/DatabaseConfigForm.jsx +++ b/apps/web-dashboard/src/components/Settings/DatabaseConfigForm.jsx @@ -20,8 +20,17 @@ export default function DatabaseConfigForm({ project, projectId, onProjectUpdate Promise.resolve().then(() => setShowForm(!(project?.resources?.db?.isExternal || false))); const fetchIp = async () => { try { - const res = await api.get(`/api/server-ip`); - if (isMounted) setServerIp(res.data.ip); + const { PUBLIC_API_URL } = await import('../../config'); + const [res1, res2] = await Promise.allSettled([ + api.get(`/api/server-ip`), + fetch(`${PUBLIC_API_URL}/api/server-ip`).then(r => r.json()) + ]); + + const ips = []; + if (res1.status === 'fulfilled' && res1.value.data.ip) ips.push(res1.value.data.ip); + if (res2.status === 'fulfilled' && res2.value.ip) ips.push(res2.value.ip); + + if (isMounted) setServerIp(ips.join(', ')); } catch (e) { console.error("Failed to fetch server IP", e); } };