diff --git a/frontend/src/components/sidebar/Navbar2.jsx b/frontend/src/components/sidebar/Navbar2.jsx
index c0bbb40..f71d254 100644
--- a/frontend/src/components/sidebar/Navbar2.jsx
+++ b/frontend/src/components/sidebar/Navbar2.jsx
@@ -31,7 +31,7 @@ function Navbar2({ friends, onNavigate }) {
return (
- {server_id == "@me" || server_id == undefined ? (
+ {server_id == "@me" || server_id == undefined || server_id === "explore" ? (
0) {
+ memoryTimers.set(
+ key,
+ setTimeout(() => {
+ memoryCache.delete(key);
+ memoryTimers.delete(key);
+ }, ttl * 1000),
+ );
+ }
+ return true;
+}
+
+function memGet(key) {
+ return memoryCache.get(key) ?? null;
+}
+
+function memDel(key) {
+ if (memoryTimers.has(key)) {
+ clearTimeout(memoryTimers.get(key));
+ memoryTimers.delete(key);
+ }
+ memoryCache.delete(key);
+ return true;
+}
+
+if (!redisUrl) {
+ logger.info("[cache] No Redis URL configured – using in-memory cache (dev only)");
+}
+/* ─── end fallback ─── */
+
async function getRedis() {
if (!redisUrl) return null;
@@ -34,7 +75,16 @@ async function getRedis() {
async function getJson(key) {
const client = await getRedis();
- if (!client) return null;
+ if (!client) {
+ // Fallback to memory
+ const raw = memGet(key);
+ if (!raw) return null;
+ try {
+ return JSON.parse(raw);
+ } catch {
+ return null;
+ }
+ }
try {
const raw = await client.get(key);
@@ -47,7 +97,10 @@ async function getJson(key) {
async function setJson(key, value, ttlSeconds = config.REDIS_CACHE_TTL_SECONDS) {
const client = await getRedis();
- if (!client) return false;
+ if (!client) {
+ // Fallback to memory
+ return memSet(key, JSON.stringify(value), ttlSeconds);
+ }
try {
const ttl = Number(ttlSeconds);
@@ -65,7 +118,10 @@ async function setJson(key, value, ttlSeconds = config.REDIS_CACHE_TTL_SECONDS)
async function del(key) {
const client = await getRedis();
- if (!client) return false;
+ if (!client) {
+ // Fallback to memory
+ return memDel(key);
+ }
try {
await client.del(key);
@@ -88,3 +144,4 @@ async function closeRedis() {
}
export { getJson, setJson, del, closeRedis };
+
diff --git a/server/src/routes/servers.js b/server/src/routes/servers.js
index 7938afa..b293631 100644
--- a/server/src/routes/servers.js
+++ b/server/src/routes/servers.js
@@ -25,6 +25,7 @@ import {
addNewChannelValidator,
createServerValidator,
deleteServerValidator,
+ joinServerValidator,
leaveServerValidator,
serverInfoValidator,
} from "../validators/servers.js";
@@ -312,4 +313,139 @@ router.post("/leave_server", leaveServerValidator, validate, async (req, res) =>
}
});
+router.get("/explore", async (req, res) => {
+ let user_id;
+ try {
+ user_id = jwt.verify(
+ req.headers["x-auth-token"],
+ config.ACCESS_TOKEN
+ );
+ } catch (e) {
+ return res.status(401).json({ message: "Unauthorized", status: 401 });
+ }
+
+ try {
+ // Get server IDs the user already belongs to
+ const user = await User.findById(user_id.id).lean();
+ const joinedServerIds = (user?.servers || []).map((s) => s.server_id);
+
+ // Build query: active servers, not yet joined
+ const query = { active: { $ne: false } };
+ if (joinedServerIds.length > 0) {
+ query._id = {
+ $nin: joinedServerIds
+ .filter((id) => mongoose.isValidObjectId(id))
+ .map((id) => new mongoose.Types.ObjectId(id)),
+ };
+ }
+
+ // Optional name search
+ const search = req.query.search;
+ if (search && typeof search === "string" && search.trim()) {
+ query.server_name = { $regex: search.trim(), $options: "i" };
+ }
+
+ const servers = await Server.find(query)
+ .select("server_name server_pic users categories")
+ .lean();
+
+ // Return lightweight payload (counts instead of full arrays)
+ const result = servers.map((s) => ({
+ _id: s._id,
+ server_name: s.server_name,
+ server_pic: s.server_pic || "",
+ member_count: Array.isArray(s.users) ? s.users.length : 0,
+ channel_count: Array.isArray(s.categories)
+ ? s.categories.reduce(
+ (sum, cat) =>
+ sum + (Array.isArray(cat.channels) ? cat.channels.length : 0),
+ 0
+ )
+ : 0,
+ }));
+
+ return res.json({ status: 200, servers: result });
+ } catch (err) {
+ return res.status(500).json({ status: 500, message: "Server error" });
+ }
+});
+
+router.post("/join_server", joinServerValidator, validate, async (req, res) => {
+ const { server_id } = req.body;
+ let user_id;
+ try {
+ user_id = jwt.verify(
+ req.headers["x-auth-token"],
+ config.ACCESS_TOKEN
+ );
+ } catch (e) {
+ return res.status(401).json({ message: "Unauthorized", status: 401 });
+ }
+
+ if (!server_id || !mongoose.isValidObjectId(server_id)) {
+ return res.status(400).json({ status: 400, message: "Invalid server ID" });
+ }
+
+ try {
+ // Check server exists and is active
+ const server = await Server.findById(server_id).lean();
+ if (!server || server.active === false) {
+ return res.status(404).json({ status: 404, message: "Server not found" });
+ }
+
+ // Check user not already in this server
+ const alreadyJoined = await checkServerInUser(user_id.id, server_id);
+ if (
+ alreadyJoined[0] &&
+ alreadyJoined[0].servers &&
+ alreadyJoined[0].servers.length > 0
+ ) {
+ return res.json({ status: 403, message: "Already a member" });
+ }
+
+ // Get user info
+ const user = await User.findById(user_id.id).lean();
+ if (!user) {
+ return res.status(404).json({ status: 404, message: "User not found" });
+ }
+
+ const userDetails = {
+ id: user_id.id,
+ username: user.username,
+ tag: user.tag,
+ profile_pic: user.profile_pic || "",
+ };
+
+ // Add user to server
+ const added = await addUserToServer(userDetails, server_id);
+ if (!added) {
+ return res.status(500).json({ status: 500, message: "Failed to join" });
+ }
+
+ // Add server to user
+ await addServerToUser(user_id.id, {
+ server_name: server.server_name,
+ server_pic: server.server_pic || "",
+ server_id: String(server._id),
+ }, "member");
+
+ // Emit real-time events
+ const io = getIO();
+ if (io) {
+ io.to(String(user_id.id)).emit("user_servers_updated", {
+ user_id: String(user_id.id),
+ });
+ io.to(`server:${String(server_id)}`).emit("server_updated", {
+ server_id: String(server_id),
+ reason: "member_joined",
+ user_id: String(user_id.id),
+ });
+ }
+
+ return res.json({ status: 200, message: "Joined server" });
+ } catch (err) {
+ return res.status(500).json({ status: 500, message: "Server error" });
+ }
+});
+
export default router;
diff --git a/server/src/validators/servers.js b/server/src/validators/servers.js
index 657fab1..8c98171 100644
--- a/server/src/validators/servers.js
+++ b/server/src/validators/servers.js
@@ -59,4 +59,8 @@ export const deleteServerValidator = [
export const leaveServerValidator = [
serverIdValidator(),
+];
+
+export const joinServerValidator = [
+ serverIdValidator(),
];
\ No newline at end of file