From a75bba4378896ffc11dd4aefedff41d46fbdb7b1 Mon Sep 17 00:00:00 2001 From: Alex Erdei Date: Wed, 7 May 2025 19:21:55 +0100 Subject: [PATCH] Clean up the backend.js file for consistency --- src/backend/backend.js | 1044 ++++++++++++---------------------------- 1 file changed, 317 insertions(+), 727 deletions(-) diff --git a/src/backend/backend.js b/src/backend/backend.js index 7a5270c..2500ff7 100644 --- a/src/backend/backend.js +++ b/src/backend/backend.js @@ -1,10 +1,4 @@ -/* ===================================================================== - - - Fakebook — back-end file (AUTH + mock social features) - - - ===================================================================== */ +// backend.js ------------------------------------------------------------- import * as signalR from "@microsoft/signalr"; @@ -30,69 +24,59 @@ import { outgoingMessagesUpdated } from "../features/outgoingMessages/outgoingMe import { mapRestMessage } from "../utils/mapRestMessage"; -/* --------------------------- CONSTANTS -------------------------------- */ +// urls / constants ------------------------------------------------------- -const API_BASE = "https://alexerdei-team.us.ainiro.io/magic/modules/fakebook"; +const API = "https://alexerdei-team.us.ainiro.io/magic/modules/fakebook"; -const SOCKETS_URL = "wss://alexerdei-team.us.ainiro.io/sockets"; +const SOCKETS = "wss://alexerdei-team.us.ainiro.io/sockets"; -const REGISTER_URL = `${API_BASE}/register`; +const USERS_URL = `${API}/users`; -const LOGIN_URL = `${API_BASE}/login`; +const POSTS_URL = `${API}/posts`; -const USERS_URL = `${API_BASE}/users`; +const MSG_URL = `${API}/message`; -const POSTS_URL = `${API_BASE}/posts`; +const IMAGE_URL = `${API}/image`; const LS_TOKEN = "fakebook.jwt"; -const LS_USER_ID = "fakebook.user_id"; +const LS_UID = "fakebook.user_id"; -/* --------------------------- SESSION (NEW) ----------------------------- */ - -/* In-memory copy – null while logged-out */ +// auth state ------------------------------------------------------------- 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 */ +// localStorage helpers --------------------------------------------------- -function setAuth(token, user_id) { - authToken = token; +const loadAuthFromStorage = () => { + authToken = localStorage.getItem(LS_TOKEN); + authUser = localStorage.getItem(LS_UID); +}; - authUser = user_id; +const saveAuthToStorage = (t, u) => { + localStorage.setItem(LS_TOKEN, t); + localStorage.setItem(LS_UID, u); +}; - localStorage.setItem(LS_TOKEN, token); +const clearAuthStorage = () => { + localStorage.removeItem(LS_TOKEN); + localStorage.removeItem(LS_UID); +}; - 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 */ +// fetch helper ----------------------------------------------------------- +const $fetch = async (url, opts = {}) => { const res = await fetch(url, { headers: { "Content-Type": "application/json", - ...authHeader(), - ...(opts.headers || {}), }, - ...opts, }); @@ -101,206 +85,25 @@ async function $fetch(url, opts = {}) { if (!res.ok) throw new Error(data.message || res.statusText); return data; -} +}; -/* ===================================================================== * - - - SECTION A — REAL AUTH WORKFLOW - - - ===================================================================== */ +// misc helpers ----------------------------------------------------------- -/* ========================================================================= +const genId = () => Math.random().toString(36).slice(2, 11); - BOOTSTRAP SESSION – used by subscribeAuth() *and* signInUser() - - ====================================================================== */ - -/** - - * Fetches all users, finds the logged-in user, updates Redux, - - * cold-starts the post feed, and marks the user online. - - * - - * Throws if the user row cannot be found. - - */ - -async function bootstrapSession(user_id) { - // 1. fetch all users (Magic API has no `/users/:id`) - - const users = await $fetch(`${USERS_URL}?limit=-1`); - - const meRow = users.find((u) => u.user_id === user_id); - - if (!meRow) throw new Error("User not found"); - - // 2. update the auth slice (for navbar, etc.) - - store.dispatch( - signIn({ - id: user_id, - - displayName: `${meRow.firstname} ${meRow.lastname}`, - - isEmailVerified: !!meRow.isEmailVerified, - }) - ); - - // 3. populate currentUser slice (fixes “missing photos / posts”) - - store.dispatch(currentUserUpdated(meRow)); - - // 4. cold-start posts feed and mark myself online - - subscribePosts(); - - currentUserOnline(); - - /* inside bootstrapSession(), after openSocket() + currentUserOnline() */ - - subscribeMessages("incoming"); - - subscribeMessages("outgoing"); -} - -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(); - - store.dispatch(signOut()); - - store.dispatch(loadingFinished()); - - return; - } - - setAuth(token, user_id); - - try { - await bootstrapSession(user_id); // <── single shared call - } 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()); - } -} - -export async function signInUser(user) { - store.dispatch(loadingStarted()); - - try { - // 1. login - - const url = `${LOGIN_URL}?email=${encodeURIComponent( - user.email - )}&password=${encodeURIComponent(user.password)}`; - - const { token, user_id } = await $fetch(url); - - // 2. persist token + open socket - - setAuth(token, user_id); - - // 3. reuse the exact same bootstrap logic - - await bootstrapSession(user_id); - - store.dispatch(errorOccured("")); // clear possible old errors - } 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 */ - -/* ------------------------------------------------------------------ */ +// socket ----------------------------------------------------------------- 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 +const openSocket = () => { + if (hub || !authToken) return; hub = new signalR.HubConnectionBuilder() - .withUrl(SOCKETS_URL, { - accessTokenFactory: () => token, - - skipNegotiation: true, // no CORS pre-flight - - transport: signalR.HttpTransportType.WebSockets, // WebSocket only + .withUrl(SOCKETS, { + accessTokenFactory: () => authToken, + skipNegotiation: true, + transport: signalR.HttpTransportType.WebSockets, }) .withAutomaticReconnect() @@ -309,461 +112,398 @@ function openSocket() { .build(); - /* ---------------------------------------------------- */ - - /* helper – SignalR sends a JSON-string → return POJO */ - - /* ---------------------------------------------------- */ - - /* helper – does the row belong to me? */ - - const isMe = (row) => row && row.user_id === localStorage.getItem(LS_USER_ID); - - /* 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([JSON.parse(raw)])); - }); + hub.on("fakebook.users.post", (raw) => + store.dispatch(usersUpdated([JSON.parse(raw)])) + ); hub.on("fakebook.users.put", (raw) => { const row = JSON.parse(raw); store.dispatch(usersUpdated([row])); - if (isMe(row)) store.dispatch(currentUserUpdated(row)); + if (row.user_id === authUser) store.dispatch(currentUserUpdated(row)); }); - hub.on("fakebook.posts.post", (raw) => { - store.dispatch(postsUpdated([JSON.parse(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)])); - }); - - /* helper – my uid so we filter only my messages */ - - const myUID = () => localStorage.getItem(LS_USER_ID); - - /* ---------- new message created -------------------------------- */ + hub.on("fakebook.posts.put", (raw) => + store.dispatch(postsUpdated([JSON.parse(raw)])) + ); hub.on("fakebook.message.post", (raw) => { - const msg = mapRestMessage(JSON.parse(raw)); // ← reuse the mapper + const msg = mapRestMessage(JSON.parse(raw)); - if (msg.recipient === myUID()) + if (msg.recipient === authUser) store.dispatch(incomingMessagesUpdated([msg])); - if (msg.sender === myUID()) + if (msg.sender === authUser) store.dispatch(outgoingMessagesUpdated([msg])); }); }) - .catch((err) => { console.warn("[SignalR] start failed:", err); - - hub = null; // let auto-reconnect retry + hub = null; }); +}; - /* extra diagnostics ---------------------------------------------- */ +// auth helpers ----------------------------------------------------------- - 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 */ +const setAuth = (t, u) => { + authToken = t; + authUser = u; + saveAuthToStorage(t, u); + openSocket(); +}; +const clearAuth = () => { 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 */ - + clearAuthStorage(); if (hub) { - hub.stop(); // graceful shutdown → returns a promise we don’t await - + hub.stop(); hub = null; } -} +}; -/* ===================================================================== * - - SECTION B — IN-MEMORY MOCK (UNCHANGED) * - - ===================================================================== */ +// presence --------------------------------------------------------------- -/* ------------- 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; +const patchOnline = async (on) => { + if (!authUser) return; try { await $fetch(USERS_URL, { method: "PUT", - - body: JSON.stringify({ - user_id, - - isOnline: isOnline ? 1 : 0, // DB needs 1/0 - }), + body: JSON.stringify({ user_id: authUser, isOnline: on ? 1 : 0 }), }); - } catch (err) { - console.warn("[online/offline] PUT failed:", err.message); + } catch (e) { + console.warn("[online] PUT failed:", e.message); } -} +}; export const currentUserOnline = () => patchOnline(true); export const currentUserOffline = () => patchOnline(false); -/* ------------------------- users list -------------------------------- */ +// bootstrap after login/restore ----------------------------------------- -export function subscribeUsers() { +const bootstrapSession = async (uid) => { + const users = await $fetch(`${USERS_URL}?limit=-1`); + + const me = users.find((u) => u.user_id === uid); + + if (!me) throw new Error("User not found"); + + store.dispatch( + signIn({ + id: uid, + displayName: `${me.firstname} ${me.lastname}`, + isEmailVerified: !!me.isEmailVerified, + }) + ); + + store.dispatch(currentUserUpdated(me)); + + subscribePosts(); + + currentUserOnline(); + + subscribeMessages("incoming"); + + subscribeMessages("outgoing"); +}; + +// public auth api -------------------------------------------------------- + +export const subscribeAuth = () => { + store.dispatch(loadingStarted()); + + loadAuthFromStorage(); + + if (!authToken || !authUser) { + clearAuth(); + store.dispatch(signOut()); + store.dispatch(loadingFinished()); + return () => {}; + } + + setAuth(authToken, authUser); + + bootstrapSession(authUser) + .catch((err) => { + console.warn("[Auth] bootstrap failed:", err.message); + clearAuth(); + store.dispatch(signOut()); + }) + + .finally(() => store.dispatch(loadingFinished())); + + return () => {}; +}; + +export const createUserAccount = async (u) => { + store.dispatch(loadingStarted()); + + try { + await $fetch(`${API}/register`, { + method: "POST", + body: JSON.stringify(u), + }); + store.dispatch(errorOccured("")); + } catch (e) { + store.dispatch(errorOccured(e.message)); + } + + store.dispatch(loadingFinished()); +}; + +export const signInUser = async (u) => { + store.dispatch(loadingStarted()); + + try { + const { token, user_id } = await $fetch( + `${API}/login?email=${encodeURIComponent( + u.email + )}&password=${encodeURIComponent(u.password)}` + ); + + setAuth(token, user_id); + await bootstrapSession(user_id); + store.dispatch(errorOccured("")); + } catch (e) { + store.dispatch(errorOccured(e.message)); + clearAuth(); + } + + store.dispatch(loadingFinished()); +}; + +export const signUserOut = async () => { + await patchOnline(false); + clearAuth(); + store.dispatch(signOut()); +}; + +// password reminder ------------------------------------------------------ + +export const sendPasswordReminder = async (email) => { + // no real endpoint yet – stub keeps call-sites happy + + console.info("[TODO] send password reminder for", email); + + return Promise.resolve(); +}; + +// users subscription ----------------------------------------------------- + +export const 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); + const u = await $fetch(`${USERS_URL}?limit=-1`); + if (!cancelled) store.dispatch(usersUpdated(u)); + } catch (e) { + console.warn("[subscribeUsers] failed:", e.message); } })(); - /* return unsubscribe fn to keep same contract */ - return () => { cancelled = true; }; -} +}; -/* ------------------------------------------------------------------ */ +// posts ------------------------------------------------------------------ -/* posts subscription: replaces old in-memory mock version */ - -/* ------------------------------------------------------------------ */ - -export function subscribePosts() { +export const 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); + const p = await $fetch(`${POSTS_URL}?limit=-1`); + if (!cancelled) store.dispatch(postsLoaded(p)); + } catch (e) { + console.warn("[subscribePosts] failed:", e.message); } })(); - /* return unsubscribe fn (keeps contract identical) */ - return () => { cancelled = true; }; -} -/* ------------------------------------------------------------------ */ +}; -/* CREATE A NEW POST – uses in-memory auth + sends post_id */ +export const upload = async (p) => { + if (!authUser) throw new Error("Not authenticated"); -/* ------------------------------------------------------------------ */ - -export async function upload(post) { - /* fast path: in-memory → fall back to localStorage once */ - - const token = authToken || localStorage.getItem(LS_TOKEN); - - const user_id = authUser || localStorage.getItem(LS_USER_ID); - - if (!token || !user_id) throw new Error("Not authenticated"); - - /* Magic table requires the primary key up front */ - - const post_id = genId(); // already in backend.js - - const body = { - post_id, // ← fixes NOT NULL error - - user_id, - - text: post.text ?? "", - - photoURL: post.photoURL ?? "", - - youtubeURL: post.youtubeURL ?? "", - - isPhoto: post.isPhoto ? 1 : 0, // Magic expects 1/0 - - isYoutube: post.isYoutube ? 1 : 0, - - likes: JSON.stringify(post.likes ?? []), - - comments: JSON.stringify(post.comments ?? []), - - timestamp: new Date().toISOString(), // optional but useful - }; - - /* POST /posts – let SignalR broadcast the newly created row */ + const post_id = genId(); await $fetch(POSTS_URL, { method: "POST", - - body: JSON.stringify(body), - }); // $fetch adds Authorization - - /* -------------------------------------------------------------- - - 2️⃣ Update my users.posts array (DB + Redux) - - -------------------------------------------------------------- */ + body: JSON.stringify({ + post_id, + user_id: authUser, + text: p.text ?? "", + photoURL: p.photoURL ?? "", + youtubeURL: p.youtubeURL ?? "", + isPhoto: p.isPhoto ? 1 : 0, + isYoutube: p.isYoutube ? 1 : 0, + likes: JSON.stringify(p.likes ?? []), + comments: JSON.stringify(p.comments ?? []), + timestamp: new Date().toISOString(), + }), + }); try { - const state = store.getState(); - - const me = state.currentUser; // already normalised - - const currentPosts = me?.posts ?? []; - - const updatedPosts = [...currentPosts, post_id]; - - /* 2a. Persist to the server ---------------------------------- */ + const curPosts = JSON.parse(store.getState().currentUser?.posts ?? "[]"); await $fetch(USERS_URL, { method: "PUT", - body: JSON.stringify({ - user_id, - - posts: JSON.stringify(updatedPosts), + user_id: authUser, + posts: JSON.stringify([...curPosts, post_id]), }), }); - } catch (err) { - console.warn("[upload] failed to patch users.posts:", err.message); + } catch (e) { + console.warn("[uploadPost] users.posts PUT failed:", e.message); } - /* keep old contract: caller expects { id } */ return { id: post_id }; -} +}; -/* -------------------------------------------------------------- +export const updatePost = async (patch, id) => { + if (!authToken) throw new Error("Not authenticated"); - Update a post (likes, comments, text, etc.) + const body = { post_id: id }; - post → partial object in Redux/UI format + if (patch.comments !== undefined) + body.comments = JSON.stringify(patch.comments); - postID → numeric/string id in the REST DB + if (patch.likes !== undefined) body.likes = JSON.stringify(patch.likes); - -------------------------------------------------------------- */ + await $fetch(POSTS_URL, { method: "PUT", body: JSON.stringify(body) }); +}; -export async function updatePost(post, postID) { - const token = localStorage.getItem(LS_TOKEN); +// messages --------------------------------------------------------------- - if (!token) throw new Error("Not authenticated"); +export const subscribeMessages = (kind) => { + let cancelled = false; - /* 1. Map Redux-shape → Magic API shape ----------------------- */ + (async () => { + try { + if (!authUser) return; - const body = { post_id: postID }; // mandatory key + const filter = + kind === "incoming" + ? `message.recipient.eq=${authUser}` + : `message.sender.eq=${authUser}`; - if (post.comments !== undefined) - body.comments = JSON.stringify(post.comments); + const rows = await $fetch(`${MSG_URL}?limit=-1&${filter}`); - if (post.likes !== undefined) body.likes = JSON.stringify(post.likes); + const mapped = rows.map(mapRestMessage); - /* 2. Fire PUT /posts ---------------------------------------- */ + if (!cancelled) + kind === "incoming" + ? store.dispatch(incomingMessagesUpdated(mapped)) + : store.dispatch(outgoingMessagesUpdated(mapped)); + } catch (e) { + console.warn("[subscribeMessages] failed:", e.message); + } + })(); - await $fetch(POSTS_URL, { - method: "PUT", + return () => { + cancelled = true; + }; +}; - body: JSON.stringify(body), +export const uploadMessage = async (m) => { + if (!authUser) throw new Error("Not authenticated"); + + const message_id = genId(); + + await $fetch(MSG_URL, { + method: "POST", + body: JSON.stringify({ + message_id, + sender: authUser, + recipient: m.recipient, + text: m.text ?? "", + photoURL: m.photoURL ?? "", + isPhoto: m.photoURL ? 1 : 0, + isRead: 0, + }), }); -} -/* ===================================================================== + return { id: message_id }; +}; - PHOTO / PROFILE HELPERS +export const updateToBeRead = async (id) => { + const patch = [{ message_id: id, isRead: 1 }]; - ===================================================================== */ + store.dispatch(incomingMessagesUpdated(patch)); -const IMAGE_UPLOAD_URL = `${API_BASE}/image`; // POST image + store.dispatch(outgoingMessagesUpdated(patch)); -/* -------------------------------------------------------------- - - addFileToStorage - - -------------------------------------------------------------- */ + try { + await $fetch(MSG_URL, { + method: "PUT", + body: JSON.stringify({ message_id: id, isRead: 1 }), + }); + } catch (e) { + console.warn("[updateToBeRead] PUT failed:", e.message); + } +}; -/* file → native File object coming from */ +// currentUser refresh ---------------------------------------------------- -/* RETURNS: { url, path, ... } exactly what the Magic endpoint sends */ +export const subscribeCurrentUser = () => { + let cancelled = false; -export async function addFileToStorage(file) { - if (!file) throw new Error("No file given"); + (async () => { + try { + if (!authUser) return; + + const users = await $fetch(`${USERS_URL}?limit=-1`); + + const me = users.find((u) => u.user_id === authUser); + + if (me && !cancelled) store.dispatch(currentUserUpdated(me)); + } catch (e) { + console.warn("[subscribeCurrentUser] failed:", e.message); + } + })(); + + return () => { + cancelled = true; + }; +}; + +// files & profile -------------------------------------------------------- + +export const addFileToStorage = async (file) => { + if (!file) throw new Error("No file"); const fd = new FormData(); - fd.append("file", file, file.name); - const res = await fetch(IMAGE_UPLOAD_URL, { + const res = await fetch(IMAGE_URL, { method: "POST", - - headers: { - ...authHeader(), // adds Authorization if we’re logged in - - // DO NOT set Content-Type – the browser will add multipart boundary - }, - + headers: { ...authHeader() }, body: fd, }); - if (!res.ok) { - const msg = await res.text(); + if (!res.ok) throw new Error(await res.text()); - throw new Error(`Image upload failed: ${msg || res.statusText}`); - } + return res.json(); +}; - /* Magic returns JSON with at least { url } (and sometimes path, size…) */ +export const updateProfile = async (patch) => { + if (!authUser) throw new Error("Not authenticated"); - const data = await res.json(); - - return data; // UploadPhoto.jsx ignores, updateDatabase uses -} - -/* -------------------------------------------------------------- - - updateProfile - - -------------------------------------------------------------- */ - -/* patch → only the fields that changed, e.g. */ - -/* { profilePictureURL: "...jpg" } */ - -/* { photos: ["p1.jpg", "p2.jpg"] } */ - -/* Updates DB and refreshes Redux */ - -export async function updateProfile(patch) { - const token = localStorage.getItem(LS_TOKEN); - - const user_id = localStorage.getItem(LS_USER_ID); - - if (!token || !user_id) throw new Error("Not authenticated"); - - /* Shape request body exactly like the Magic API expects -------- */ - - const body = { user_id }; + const body = { user_id: authUser }; if (patch.profilePictureURL !== undefined) body.profilePictureURL = patch.profilePictureURL; @@ -771,168 +511,18 @@ export async function updateProfile(patch) { if (patch.backgroundPictureURL !== undefined) body.backgroundPictureURL = patch.backgroundPictureURL; - if (patch.photos !== undefined) - // array → JSON string + if (patch.photos !== undefined) body.photos = JSON.stringify(patch.photos); - body.photos = JSON.stringify(patch.photos); - - /* Fire PUT /users --------------------------------------------- */ - - const updatedRow = await $fetch(USERS_URL, { + const updated = await $fetch(USERS_URL, { method: "PUT", - - body: JSON.stringify(body), - }); // $fetch auto-adds headers - - /* Update Redux immediately for smooth UX ---------------------- */ - - store.dispatch(currentUserUpdated(updatedRow)); - - store.dispatch(usersUpdated([updatedRow])); - - /* When the hub later sends fakebook.users.put the same row will - - merge in again; that’s harmless. */ -} -/* ===================================================================== - - - MESSAGES – read-only bootstrap (Magic back-end) - - - ===================================================================== */ - -const MESSAGE_URL = `${API_BASE}/message`; // ← singular table name - -/* -------------------------------------------------------------- - - subscribeMessages(kind) - - - kind = "incoming" | "outgoing" - - → issues ONE network call with server-side filter - - -------------------------------------------------------------- */ - -export function subscribeMessages(kind = "incoming") { - let cancelled = false; - - (async () => { - try { - const uid = localStorage.getItem(LS_USER_ID); - - if (!uid) return; - - /* Magic filter param: message.sender.eq= etc. */ - - const filter = - kind === "incoming" - ? `message.recipient.eq=${uid}` - : `message.sender.eq=${uid}`; - - const url = `${MESSAGE_URL}?limit=-1&${filter}`; - - const rows = await $fetch(url); // $fetch auto adds auth - - if (!cancelled) { - const mapped = rows.map(mapRestMessage); - - store.dispatch( - (kind === "incoming" - ? incomingMessagesUpdated - : outgoingMessagesUpdated)(mapped) - ); - } - } catch (err) { - console.warn("[subscribeMessages] failed:", err.message); - } - })(); - - return () => { - cancelled = true; - }; -} - -/* -------------------------------------------------------------- - - uploadMessage(msg) - - - msg = { recipient, text, photoURL? } - - RETURNS { id } // new message_id - - -------------------------------------------------------------- */ - -export async function uploadMessage(msg) { - const sender = localStorage.getItem(LS_USER_ID); - - if (!sender) throw new Error("Not authenticated"); - - const message_id = genId(); // Magic table PK - - const body = { - message_id, - - sender, // who sends - - recipient: msg.recipient, // who receives - - text: msg.text ?? "", - - photoURL: msg.photoURL ?? "", - - isPhoto: msg.photoURL ? 1 : 0, - - isRead: 0, // unread on insert - - /* timestamp is generated by the server */ - }; - - await $fetch(`${API_BASE}/message`, { - method: "POST", - body: JSON.stringify(body), }); - /* No local dispatch necessary – the hub’s `message.post` event - - will arrive in a few ms and update Redux for both parties. */ + store.dispatch(currentUserUpdated(updated)); - return { id: message_id }; -} + store.dispatch(usersUpdated([updated])); +}; -/* ------------------------------------------------------------------ +// misc helper ------------------------------------------------------------ - updateToBeRead(message_id) - - – flips the flag in Redux immediately (optimistic) - - – then notifies the server; no need to wait for a hub echo - ------------------------------------------------------------------- */ - -export async function updateToBeRead(message_id) { - /* 1️⃣ optimistic local patch (affects whichever slice holds the row) */ - - const patch = [{ message_id, isRead: 1 }]; - - store.dispatch(incomingMessagesUpdated(patch)); - - store.dispatch(outgoingMessagesUpdated(patch)); - - /* 2️⃣ tell the server – ignore the hub echo, we no longer listen */ - - try { - await $fetch(`${API_BASE}/message`, { - method: "PUT", - - body: JSON.stringify({ message_id, isRead: 1 }), - }); - } catch (err) { - console.warn("[updateToBeRead] PUT failed:", err.message); - - // Optional: revert optimistic change on error - } -} +export const getImageURL = async (path) => `/assets/${path}`;