fakebook-aigen/src/backend/backend.js

679 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* =====================================================================
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 dont 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();
}