Skip to content

Commit e15cff0

Browse files
committed
feat: verify mongo connection on config update and expose server IP
1 parent 2cf65c7 commit e15cff0

6 files changed

Lines changed: 139 additions & 18 deletions

File tree

backend/app.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const rateLimit = require('express-rate-limit');
88
const app = express();
99
app.set('trust proxy', 1);
1010
const GC = require('./utils/GC');
11+
const { getPublicIp } = require('./utils/network');
1112

1213
// Middleware
1314
app.use(cors());
@@ -64,8 +65,14 @@ app.use('/api/auth', dashboardLimiter, authRoute); // Developer Auth
6465
app.use('/api/projects', dashboardLimiter, projectRoute); // Project Mgmt
6566
app.use('/api/userAuth', limiter, logger, userAuthRoute);
6667
app.use('/api/data', limiter, cors(adminCorsOptions), logger, dataRoute);
68+
app.use('/api/data', limiter, cors(adminCorsOptions), logger, dataRoute);
6769
app.use('/api/storage', limiter, cors(adminCorsOptions), logger, storageRoute);
6870

71+
app.get('/api/server-ip', async (req, res) => {
72+
const ip = await getPublicIp();
73+
res.json({ ip });
74+
});
75+
6976
// Test Route
7077
app.get('/', (req, res) => {
7178
res.status(200).json({ status: "success", message: "urBackend API is running 🚀" })

backend/controllers/project.controller.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const { getConnection } = require("../utils/connection.manager");
1212
const { getCompiledModel } = require("../utils/injectModel")
1313
const { storageRegistry } = require("../utils/registry");
1414
const { deleteProjectByApiKeyCache, setProjectById, getProjectById, deleteProjectById } = require("../services/redisCaching");
15+
const { getPublicIp } = require("../utils/network");
1516

1617

1718

@@ -143,6 +144,27 @@ module.exports.updateExternalConfig = async (req, res) => {
143144
// Naye model structure ke hisaab se save karein
144145
updateData['resources.db.config'] = encrypt(JSON.stringify({ dbUri }));
145146
updateData['resources.db.isExternal'] = true;
147+
148+
// --- VERIFY CONNECTION ---
149+
console.log("Verifying connection to:", projectId);
150+
try {
151+
const tempConn = mongoose.createConnection(dbUri, { serverSelectionTimeoutMS: 5000 });
152+
await tempConn.asPromise();
153+
await tempConn.close();
154+
} catch (connErr) {
155+
console.error("Verification Connection Failed:", connErr.message);
156+
let errorMsg = "Could not connect to the provided MongoDB URI.";
157+
158+
if (connErr.message.includes("Server selection timed out") || connErr.message.includes("Could not connect")) {
159+
const serverIp = await getPublicIp();
160+
errorMsg = `Access Denied: Please whitelist Server IP [${serverIp}] in MongoDB Atlas.`;
161+
} else {
162+
errorMsg += " " + connErr.message;
163+
}
164+
165+
return res.status(400).json({ error: errorMsg });
166+
}
167+
// -------------------------
146168
}
147169

148170
// 3. Storage Config Encryption

backend/utils/connection.manager.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { registry } = require("./registry");
2+
const { getPublicIp } = require("./network");
23
const Project = require("../models/Project");
34
const { decrypt } = require("./encryption");
45
const mongoose = require("mongoose");
@@ -39,9 +40,20 @@ async function getConnection(projectId) {
3940
console.log(`✅ External DB connected: ${projectId}`);
4041
});
4142

43+
try {
44+
await connection.asPromise();
45+
} catch (err) {
46+
console.error("❌ Initial Connection Failed:", err.message);
47+
if (err.message.includes("Server selection timed out") || err.message.includes("Could not connect")) {
48+
const serverIp = await getPublicIp();
49+
throw new Error(`Access Denied: Please whitelist Server IP [${serverIp}] in MongoDB Atlas.`);
50+
}
51+
throw err;
52+
}
53+
4254
connection.on("error", (err) => {
4355
console.error("❌ Connection error [%s]:", projectId, err);
44-
registry.delete(key);
56+
registry.delete(key);
4557
});
4658

4759
connection.on("close", () => {

backend/utils/network.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Node 18+ has global fetch, so no import needed
2+
3+
let cachedIp = null;
4+
let lastFetch = 0;
5+
6+
async function getPublicIp() {
7+
// Cache IP for 1 hour to avoid spamming the IP service
8+
const now = Date.now();
9+
if (cachedIp && (now - lastFetch < 3600000)) {
10+
return cachedIp;
11+
}
12+
13+
try {
14+
const response = await fetch('https://api.ipify.org?format=json');
15+
if (!response.ok) throw new Error('Failed to fetch IP');
16+
const data = await response.json();
17+
cachedIp = data.ip;
18+
lastFetch = now;
19+
return cachedIp;
20+
} catch (error) {
21+
console.error("Error fetching public IP:", error.message);
22+
return "Unavailable";
23+
}
24+
}
25+
26+
module.exports = { getPublicIp };

frontend/src/pages/CreateCollection.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ function CreateCollection() {
1414

1515
const [name, setName] = useState('');
1616
const [fields, setFields] = useState([
17-
{ key: 'id', type: 'String', required: true } // Default ID field suggestion
17+
{ key: 'username', type: 'String', required: true },
18+
{ key: 'email', type: 'String', required: true },
19+
{ key: 'password', type: 'String', required: true },
1820
]);
1921
const [loading, setLoading] = useState(false);
2022

frontend/src/pages/ProjectSettings.jsx

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useParams, useNavigate } from "react-router-dom";
33
import axios from "axios";
44
import { useAuth } from "../context/AuthContext";
55
import toast from "react-hot-toast";
6-
import { Trash2, AlertTriangle, Save, CheckCircle } from "lucide-react";
6+
import { Trash2, AlertTriangle, Save, CheckCircle, Copy, Server } from "lucide-react";
77
import { API_URL } from "../config";
88
import ConfirmationModal from "./ConfirmationModal";
99

@@ -308,11 +308,31 @@ function DatabaseConfigForm({ project, projectId, token }) {
308308
const isConfigured = project?.resources?.db?.isExternal || false;
309309
const [showForm, setShowForm] = useState(!isConfigured);
310310

311+
const [serverIp, setServerIp] = useState(null);
312+
311313
useEffect(() => {
312314
const configured = project?.resources?.db?.isExternal || false;
313315
setShowForm(!configured);
316+
317+
// Fetch Server IP
318+
const fetchIp = async () => {
319+
try {
320+
const res = await axios.get(`${API_URL}/api/server-ip`);
321+
setServerIp(res.data.ip);
322+
} catch (e) {
323+
console.error("Failed to fetch server IP");
324+
}
325+
};
326+
fetchIp();
314327
}, [project]);
315328

329+
const copyIp = () => {
330+
if (serverIp) {
331+
navigator.clipboard.writeText(serverIp);
332+
toast.success("Server IP copied!");
333+
}
334+
};
335+
316336
const handleUpdate = async () => {
317337
if (!dbUri) return toast.error("Database URI is required");
318338

@@ -329,7 +349,20 @@ function DatabaseConfigForm({ project, projectId, token }) {
329349
// Typically we'd reload project data here, but for now we rely on user refresh or optimistic ui if needed
330350
// Ideally notify parent to refresh project, but basic flow:
331351
} catch (err) {
332-
toast.error(err.response?.data?.error || "Failed to update DB config");
352+
const errorMsg = err.response?.data?.error || "Failed to update DB config";
353+
354+
if (errorMsg.includes("whitelist Server IP")) {
355+
toast.error(
356+
<div>
357+
<b>Access Denied!</b>
358+
<br />
359+
{errorMsg}
360+
</div>,
361+
{ duration: 6000 }
362+
);
363+
} else {
364+
toast.error(errorMsg);
365+
}
333366
} finally {
334367
setLoading(false);
335368
}
@@ -439,26 +472,45 @@ function DatabaseConfigForm({ project, projectId, token }) {
439472
<div
440473
style={{
441474
display: "flex",
442-
justifyContent: "flex-end",
443-
gap: "12px",
475+
justifyContent: "space-between",
476+
alignItems: "center",
444477
marginTop: "1rem",
478+
paddingTop: "1rem",
479+
borderTop: "1px solid var(--color-border)",
445480
}}
446481
>
447-
{isConfigured && (
482+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.85rem', color: 'var(--color-text-muted)' }}>
483+
<Server size={14} />
484+
<span>Server Public IP: <code style={{ background: 'rgba(255,255,255,0.1)', padding: '2px 6px', borderRadius: '4px' }}>{serverIp || "Loading..."}</code></span>
485+
{serverIp && (
486+
<button
487+
onClick={copyIp}
488+
className="btn-icon"
489+
title="Copy IP"
490+
style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: 'var(--color-primary)', padding: 0, marginLeft: '5px' }}
491+
>
492+
<Copy size={14} />
493+
</button>
494+
)}
495+
</div>
496+
497+
<div style={{ display: 'flex', gap: '12px' }}>
498+
{isConfigured && (
499+
<button
500+
className="btn btn-ghost"
501+
onClick={() => setShowForm(false)}
502+
>
503+
Cancel
504+
</button>
505+
)}
448506
<button
449-
className="btn btn-ghost"
450-
onClick={() => setShowForm(false)}
507+
onClick={handleUpdate}
508+
className="btn btn-primary"
509+
disabled={loading}
451510
>
452-
Cancel
511+
{loading ? "Saving..." : "Connect Database"}
453512
</button>
454-
)}
455-
<button
456-
onClick={handleUpdate}
457-
className="btn btn-primary"
458-
disabled={loading}
459-
>
460-
{loading ? "Saving..." : "Connect Database"}
461-
</button>
513+
</div>
462514
</div>
463515
</div>
464516
)}

0 commit comments

Comments
 (0)