/* ===================================================================== Fakebook — back-end file (AUTH + mock social features) ===================================================================== */ import * as signalR from "@microsoft/signalr"; import store from "../app/store"; import { signIn, signOut, errorOccured, loadingStarted, loadingFinished, } from "../features/user/userSlice"; import { currentUserUpdated } from "../features/currentUser/currentUserSlice"; import { usersUpdated } from "../features/users/usersSlice"; import { postsLoaded, postsUpdated } from "../features/posts/postsSlice"; import { incomingMessagesUpdated } from "../features/incomingMessages/incomingMessagesSlice"; import { outgoingMessagesUpdated } from "../features/outgoingMessages/outgoingMessagesSlice"; /* --------------------------- CONSTANTS -------------------------------- */ const API_BASE = "https://alexerdei-team.us.ainiro.io/magic/modules/fakebook"; const SOCKETS_URL = "wss://alexerdei-team.us.ainiro.io/sockets"; const REGISTER_URL = `${API_BASE}/register`; const LOGIN_URL = `${API_BASE}/login`; const USERS_URL = `${API_BASE}/users`; const POSTS_URL = `${API_BASE}/posts`; const LS_TOKEN = "fakebook.jwt"; const LS_USER_ID = "fakebook.user_id"; /* --------------------------- SESSION (NEW) ----------------------------- */ /* In-memory copy – null while logged-out */ let authToken = null; let authUser = null; /* Header helper – import this anywhere you need the bearer token */ const authHeader = () => authToken ? { Authorization: `Bearer ${authToken}` } : {}; /* Initialise / clear session */ function setAuth(token, user_id) { authToken = token; authUser = user_id; localStorage.setItem(LS_TOKEN, token); localStorage.setItem(LS_USER_ID, user_id); openSocket(); } /* --------------------------- Utilities -------------------------------- */ const genId = () => Math.random().toString(36).slice(2, 11); const delay = (ms = 200) => new Promise((r) => setTimeout(r, ms)); async function $fetch(url, opts = {}) { /* inject bearer automatically */ const res = await fetch(url, { headers: { "Content-Type": "application/json", ...authHeader(), ...(opts.headers || {}), }, ...opts, }); const data = await res.json(); if (!res.ok) throw new Error(data.message || res.statusText); return data; } /* ===================================================================== * SECTION A — REAL AUTH WORKFLOW ===================================================================== */ /* ---------------------- subscribeAuth -------------------------------- */ export function subscribeAuth() { store.dispatch(loadingStarted()); (async () => { const token = localStorage.getItem(LS_TOKEN); const user_id = localStorage.getItem(LS_USER_ID); if (!token || !user_id) { clearAuth(); /* make sure RAM copy is empty */ store.dispatch(signOut()); store.dispatch(loadingFinished()); return; } /* restore session into RAM */ setAuth(token, user_id); try { /* users endpoint lacks ?user_id, so fetch all */ const users = await $fetch(`${USERS_URL}?limit=-1`); const u = users.find((x) => x.user_id === user_id); if (!u) throw new Error("User not found"); store.dispatch( signIn({ id: user_id, displayName: `${u.firstname} ${u.lastname}`, isEmailVerified: !!u.isEmailVerified, }) ); // ------------------------------------------------------------------ // cold-start: get the full feed once // ------------------------------------------------------------------ subscribePosts(); // <—— fetches /posts and dispatches postsLoaded currentUserOnline(); // mark myself online immediately } catch (err) { console.warn("[Auth] subscribeAuth failed:", err.message); clearAuth(); store.dispatch(signOut()); } finally { store.dispatch(loadingFinished()); } })(); return () => {}; } /* ---------------------- createUserAccount ---------------------------- */ export async function createUserAccount(user) { store.dispatch(loadingStarted()); try { await $fetch(REGISTER_URL, { method: "POST", body: JSON.stringify({ firstname: user.firstname, lastname: user.lastname, email: user.email, password: user.password, }), }); store.dispatch(errorOccured("")); } catch (err) { store.dispatch(errorOccured(err.message)); } finally { store.dispatch(loadingFinished()); } } /* ---------------------- signInUser ---------------------------------- */ export async function signInUser(user) { store.dispatch(loadingStarted()); try { const url = `${LOGIN_URL}?email=${encodeURIComponent( user.email )}&password=${encodeURIComponent(user.password)}`; const { token, user_id } = await $fetch(url); /* persist + put into RAM */ setAuth(token, user_id); /* get full profile */ const users = await $fetch(`${USERS_URL}?limit=-1`); const profile = users.find((u) => u.user_id === user_id); if (!profile) throw new Error("User not found"); if (!profile.isEmailVerified) throw new Error("Please verify your email before to continue"); store.dispatch( signIn({ id: user_id, displayName: `${profile.firstname} ${profile.lastname}`, isEmailVerified: true, }) ); store.dispatch(errorOccured("")); currentUserOnline(); // mark myself online immediately } catch (err) { store.dispatch(errorOccured(err.message)); clearAuth(); } finally { store.dispatch(loadingFinished()); } } /* ---------------------- signUserOut ---------------------------------- */ export async function signUserOut() { await patchOnline(false); /* mark offline */ clearAuth(); store.dispatch(signOut()); } /* -------------------- sendPasswordReminder --------------------------- */ export function sendPasswordReminder(email) { console.info("[TODO] Implement password reminder for", email); return Promise.resolve(); } /* ------------------------------------------------------------------ */ /* SignalR hub – one connection shared across the app */ /* ------------------------------------------------------------------ */ let hub = null; function openSocket() { if (hub) return; // already connected / connecting const token = localStorage.getItem(LS_TOKEN); if (!token) return; // not logged-in → no live updates hub = new signalR.HubConnectionBuilder() .withUrl(SOCKETS_URL, { accessTokenFactory: () => token, skipNegotiation: true, // no CORS pre-flight transport: signalR.HttpTransportType.WebSockets, // WebSocket only }) .withAutomaticReconnect() .configureLogging(signalR.LogLevel.Warning) .build(); /* ---------------------------------------------------- */ /* helper – SignalR sends a JSON-string → return POJO */ /* ---------------------------------------------------- */ const parseMsg = (raw) => (typeof raw === "string" ? JSON.parse(raw) : raw); /* 1️⃣ Start first … */ hub .start() .then(() => { console.info("[SignalR] connected, id:", hub.connectionId); /* 2️⃣ … then register listeners ----------------------------- */ /* inside openSocket() – unchanged except the helper */ hub.on("fakebook.users.post", (raw) => { store.dispatch(usersUpdated([parseMsg(raw)])); }); hub.on("fakebook.users.put", (raw) => { store.dispatch(usersUpdated([parseMsg(raw)])); }); hub.on("fakebook.posts.post", (raw) => { store.dispatch(postsUpdated([JSON.parse(raw)])); }); hub.on("fakebook.posts.put", (raw) => { store.dispatch(postsUpdated([JSON.parse(raw)])); }); }) .catch((err) => { console.warn("[SignalR] start failed:", err); hub = null; // let auto-reconnect retry }); /* extra diagnostics ---------------------------------------------- */ hub.onreconnecting((err) => console.warn("[SignalR] reconnecting:", err)); hub.onreconnected((id) => console.info("[SignalR] reconnected, id:", id)); hub.onclose((err) => console.warn("[SignalR] closed:", err)); } /* ------------------------------------------------------------------ */ /* Close SignalR + forget credentials */ /* ------------------------------------------------------------------ */ function clearAuth() { /* forget in-memory copies */ authToken = null; authUser = null; /* forget persisted copies */ localStorage.removeItem(LS_TOKEN); // "fakebook.jwt" localStorage.removeItem(LS_USER_ID); // "fakebook.user_id" /* close the hub if it exists */ if (hub) { hub.stop(); // graceful shutdown → returns a promise we don’t await hub = null; } } /* ===================================================================== * SECTION B — IN-MEMORY MOCK (UNCHANGED) * ===================================================================== */ /* ------------- tiny DB (persists to localStorage for demo) ----------- */ const LS_DB = "__fakebook_mock_db__"; const DB = Object.assign( { currentUser: null, users: [], posts: [], messages: [], }, JSON.parse(localStorage.getItem(LS_DB) || "{}") ); const persist = () => localStorage.setItem(LS_DB, JSON.stringify(DB)); const me = () => DB.users.find((u) => u.userID === DB.currentUser?.id); const nowISO = () => new Date().toISOString(); /* --------------------- generic helpers ------------------------------- */ export async function getImageURL(path) { return `/assets/${path}`; } /* ------------------- current user subscriptions ---------------------- */ /* ------------------------------------------------------------------ */ /* Current user document */ /* ------------------------------------------------------------------ */ export function subscribeCurrentUser() { let cancelled = false; (async () => { try { const token = localStorage.getItem(LS_TOKEN); const user_id = localStorage.getItem(LS_USER_ID); if (!token || !user_id) return; const users = await $fetch(`${USERS_URL}?limit=-1`, { headers: { Authorization: `Bearer ${token}` }, }); const meRow = users.find((u) => u.user_id === user_id); if (meRow && !cancelled) { store.dispatch(currentUserUpdated(meRow)); } } catch (err) { console.warn("[subscribeCurrentUser] failed:", err.message); } })(); return () => { cancelled = true; }; } /* ------------------------------------------------------------------ */ /* Online / offline flag */ /* ------------------------------------------------------------------ */ async function patchOnline(isOnline) { const token = localStorage.getItem(LS_TOKEN); const user_id = localStorage.getItem(LS_USER_ID); if (!token || !user_id) return; try { await $fetch(USERS_URL, { method: "PUT", body: JSON.stringify({ user_id, isOnline: isOnline ? 1 : 0, // DB needs 1/0 }), }); } catch (err) { console.warn("[online/offline] PUT failed:", err.message); } } export const currentUserOnline = () => patchOnline(true); export const currentUserOffline = () => patchOnline(false); /* ------------------------- users list -------------------------------- */ export function subscribeUsers() { let cancelled = false; (async () => { try { const token = localStorage.getItem(LS_TOKEN); const users = await $fetch(`${USERS_URL}?limit=-1`, { headers: token ? { Authorization: `Bearer ${token}` } : {}, }); if (!cancelled) { store.dispatch(usersUpdated(users)); } } catch (err) { console.warn("[subscribeUsers] failed:", err.message); } })(); /* return unsubscribe fn to keep same contract */ return () => { cancelled = true; }; } /* ------------------------------------------------------------------ */ /* posts subscription: replaces old in-memory mock version */ /* ------------------------------------------------------------------ */ export function subscribePosts() { let cancelled = false; (async () => { try { const token = localStorage.getItem(LS_TOKEN); const arr = await $fetch(`${POSTS_URL}?limit=-1`, { headers: token ? { Authorization: `Bearer ${token}` } : {}, }); if (!cancelled) { store.dispatch(postsLoaded(arr)); // full list once } } catch (err) { console.warn("[subscribePosts] failed:", err.message); } })(); /* return unsubscribe fn (keeps contract identical) */ return () => { cancelled = true; }; } export async function upload(post) { await delay(); const doc = { ...post, postID: genId(), timestamp: nowISO() }; DB.posts.unshift(doc); if (me()) me().posts.unshift(doc.postID); persist(); return { id: doc.postID }; } /* -------------------------------------------------------------- Update a post (likes, comments, text, etc.) post → partial object in Redux/UI format postID → numeric/string id in the REST DB -------------------------------------------------------------- */ export async function updatePost(post, postID) { const token = localStorage.getItem(LS_TOKEN); if (!token) throw new Error("Not authenticated"); /* 1. Map Redux-shape → Magic API shape ----------------------- */ const body = { post_id: postID }; // mandatory key if (post.comments !== undefined) body.comments = JSON.stringify(post.comments); if (post.likes !== undefined) body.likes = JSON.stringify(post.likes); /* 2. Fire PUT /posts ---------------------------------------- */ await $fetch(POSTS_URL, { method: "PUT", body: JSON.stringify(body), }); /* 3. Refresh Redux state (merge) ---------------------------- */ //store.dispatch(postsUpdated([updatedRow])); } /* ------------------------- storage ----------------------------------- */ export function addFileToStorage(file) { console.info("[Mock] saved file", file.name); return Promise.resolve({ ref: { fullPath: `${DB.currentUser?.id}/${file.name}` }, }); } /* ------------------------- profile ----------------------------------- */ export function updateProfile(profile) { if (me()) Object.assign(me(), profile); persist(); return Promise.resolve(); } /* ------------------------- messages ---------------------------------- */ export function subscribeMessages(kind) { const uid = DB.currentUser?.id; const inc = kind === "incoming"; setTimeout(() => { const msgs = DB.messages.filter((m) => inc ? m.recipient === uid : m.sender === uid ); store.dispatch( (inc ? incomingMessagesUpdated : outgoingMessagesUpdated)([...msgs]) ); }, 0); return () => {}; } export function uploadMessage(msg) { DB.messages.push({ ...msg, id: genId(), timestamp: nowISO(), isRead: false }); persist(); return Promise.resolve(); } export function updateToBeRead(id) { const m = DB.messages.find((m) => m.id === id); if (m) { m.isRead = true; persist(); } return Promise.resolve(); } /* Ensure demo DB has at least one user */ if (!DB.users.length) { DB.users.push({ userID: genId(), firstname: "Demo", lastname: "User", profilePictureURL: "fakebook-avatar.jpeg", backgroundPictureURL: "background-server.jpg", photos: [], posts: [], isOnline: 0, isEmailVerified: true, index: 0, }); persist(); }