Make user authentication workflow work

This commit is contained in:
Alex Erdei 2025-04-29 19:39:47 +01:00
parent dd8f88d749
commit 03ecf07c6a
1 changed files with 360 additions and 246 deletions

View File

@ -1,20 +1,8 @@
/* backend.js FIRST MIGRATION STEP /* =====================================================================
------------------------------------ Fakebook back-end file (AUTH + mock social features)
0. NO Firebase imports at all. ===================================================================== */
1. Export *exactly* the same function names the UI already uses.
2. Provide no-op or mock implementations that fulfil those contracts.
3. Tiny in-memory cache (+ localStorage) so screens have something
to display and dont blow up on undefined values.
*/
/* Redux store + slices stay exactly as they were */
import store from "../app/store"; import store from "../app/store";
@ -22,8 +10,8 @@ import {
signIn, signIn,
signOut, signOut,
errorOccured, errorOccured,
loadingFinished,
loadingStarted, loadingStarted,
loadingFinished,
} from "../features/user/userSlice"; } from "../features/user/userSlice";
import { currentUserUpdated } from "../features/currentUser/currentUserSlice"; import { currentUserUpdated } from "../features/currentUser/currentUserSlice";
@ -36,134 +24,490 @@ import { incomingMessagesUpdated } from "../features/incomingMessages/incomingMe
import { outgoingMessagesUpdated } from "../features/outgoingMessages/outgoingMessagesSlice"; import { outgoingMessagesUpdated } from "../features/outgoingMessages/outgoingMessagesSlice";
/* ------------------------------------------------------------------ */ /* --------------------------- CONSTANTS -------------------------------- */
/* Small helper utilities */ const API_BASE = "https://alexerdei-team.us.ainiro.io/magic/modules/fakebook";
/* ------------------------------------------------------------------ */ const REGISTER_URL = `${API_BASE}/register`;
const LOGIN_URL = `${API_BASE}/login`;
const USERS_URL = `${API_BASE}/users`;
const LS_TOKEN = "fakebook.jwt";
const LS_USER_ID = "fakebook.user_id";
/* --------------------------- Utilities -------------------------------- */
const genId = () => Math.random().toString(36).slice(2, 11); const genId = () => Math.random().toString(36).slice(2, 11);
const delay = (ms = 250) => new Promise((r) => setTimeout(r, ms)); const delay = (ms = 200) => new Promise((r) => setTimeout(r, ms));
/* Persist minimal mock DB so a reload keeps state (optional) */ async function $fetch(url, opts = {}) {
const res = await fetch(url, {
headers: { "Content-Type": "application/json", ...(opts.headers || {}) },
...opts,
});
const LS_KEY = "__fakebook_mock_db__"; const data = await res.json();
const loadDB = () => JSON.parse(localStorage.getItem(LS_KEY) || "{}"); if (!res.ok) throw new Error(data.message || res.statusText);
const saveDB = (db) => localStorage.setItem(LS_KEY, JSON.stringify(db)); return data;
}
/* ------------------------------------------------------------------ */ /* ---------------------- REST → UI mapper (moved up) ------------------ */
/* “Database” lives in memory, persisted to localStorage */ function mapRestUser(u) {
return {
userID: u.user_id,
/* ------------------------------------------------------------------ */ firstname: u.firstname,
let DB = { lastname: u.lastname,
currentUser: null, // { id, displayName, isEmailVerified }
users: [], // array of “profile” objects profilePictureURL: u.profilePictureURL,
posts: [], // array of posts backgroundPictureURL: u.backgroundPictureURL,
messages: [], // array of { id, sender, recipient, text, isRead, ... } photos: JSON.parse(u.photos || "[]"),
...loadDB(), posts: JSON.parse(u.posts || "[]"),
isOnline: !!u.isOnline,
isEmailVerified: !!u.isEmailVerified,
index: u.index ?? 0,
}; };
}
/* Convenience finders */ /* ===================================================================== *
SECTION A REAL AUTH WORKFLOW (FIXED) *
===================================================================== */
/* ---------------------- 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) {
store.dispatch(signOut());
store.dispatch(loadingFinished());
return;
}
try {
/* The users endpoint does NOT understand ?user_id=.
Workaround: fetch all users (limit=-1) then find ours. */
const users = await $fetch(`${USERS_URL}?limit=-1`, {
headers: { Authorization: `Bearer ${token}` },
});
const u = users.find((u) => u.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,
})
);
} catch (err) {
/* Token invalid / expired / user not found */
console.warn("[Auth] subscribeAuth failed:", err.message);
localStorage.removeItem(LS_TOKEN);
localStorage.removeItem(LS_USER_ID);
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("")); // clear previous error
} catch (err) {
store.dispatch(errorOccured(err.message));
} finally {
store.dispatch(loadingFinished());
}
}
/* ---------------------- signInUser (FIXED) -------------------------- */
export async function signInUser(user) {
store.dispatch(loadingStarted());
try {
/* 1. Login with email + password (as API expects) */
const url = `${LOGIN_URL}?email=${encodeURIComponent(
user.email
)}&password=${encodeURIComponent(user.password)}`;
const { token, user_id } = await $fetch(url);
/* 2. Persist session */
localStorage.setItem(LS_TOKEN, token);
localStorage.setItem(LS_USER_ID, user_id);
/* 3. Fetch FULL user list, find our profile */
const users = await $fetch(`${USERS_URL}?limit=-1`, {
headers: { Authorization: `Bearer ${token}` },
});
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");
}
/* 4. Dispatch sign-in */
store.dispatch(
signIn({
id: user_id,
displayName: `${profile.firstname} ${profile.lastname}`,
isEmailVerified: true,
})
);
store.dispatch(errorOccured(""));
} catch (err) {
store.dispatch(errorOccured(err.message));
localStorage.removeItem(LS_TOKEN);
localStorage.removeItem(LS_USER_ID);
} finally {
store.dispatch(loadingFinished());
}
}
/* ---------------------- signUserOut ---------------------------------- */
export async function signUserOut() {
/* mark offline in DB */
await patchOnline(false);
/* clear session */
localStorage.removeItem(LS_TOKEN);
localStorage.removeItem(LS_USER_ID);
store.dispatch(signOut());
}
/* -------------------- sendPasswordReminder --------------------------- */
export function sendPasswordReminder(email) {
console.info("[TODO] Implement password reminder for", email);
return Promise.resolve();
}
/* ===================================================================== *
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 me = () => DB.users.find((u) => u.userID === DB.currentUser?.id);
const nowISO = () => new Date().toISOString(); const nowISO = () => new Date().toISOString();
/* Anytime we mutate DB we persist */ /* --------------------- generic helpers ------------------------------- */
const commit = () => saveDB(DB);
/* ------------------------------------------------------------------ */
/* PUBLIC API (ALL the names the UI already imports) */
/* ------------------------------------------------------------------ */
/* ---------- Generic helpers ---------- */
export async function getImageURL(path) { export async function getImageURL(path) {
/* Pretend we have a CDN */
return `/assets/${path}`; return `/assets/${path}`;
} }
/* ---------- Auth ---------- */ /* ------------------- current user subscriptions ---------------------- */
export function subscribeAuth() { /* ------------------------------------------------------------------ */
/* Mimic Firebase: emit immediately, return an unsubscribe fn */
setTimeout(() => { /* Current user document */
if (DB.currentUser) {
const { id, displayName, isEmailVerified } = DB.currentUser;
store.dispatch(signIn({ id, displayName, isEmailVerified })); /* ------------------------------------------------------------------ */
} else {
store.dispatch(signOut()); 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(mapRestUser(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",
mode: "cors",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
user_id,
isOnline: isOnline ? 1 : 0, // DB needs 1/0
}),
});
/* -------- optimistic Redux update (merge, not overwrite) -------- */
const cur = store.getState().currentUser;
store.dispatch(currentUserUpdated({ ...cur, isOnline })); // boolean
const usersNew = store
.getState()
.users.map((u) => (u.userID === user_id ? { ...u, isOnline } : u));
store.dispatch(usersUpdated(usersNew));
} catch (err) {
console.warn("[online/offline] PUT failed:", err.message);
}
} }
store.dispatch(loadingFinished()); 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.map(mapRestUser)));
}
} catch (err) {
console.warn("[subscribeUsers] failed:", err.message);
}
})();
/* return unsubscribe fn to keep same contract */
return () => {
cancelled = true;
};
}
/* ------------------------- posts ------------------------------------- */
export function subscribePosts() {
setTimeout(() => {
store.dispatch(
postsUpdated(
DB.posts.map((p) => ({
...p,
timestamp: new Date(p.timestamp).toLocaleString(),
}))
)
);
}, 0); }, 0);
return () => {}; // no-op unsubscribe return () => {};
} }
export async function signUserOut() { export async function upload(post) {
store.dispatch(loadingStarted());
DB.currentUser = null;
commit();
store.dispatch(signOut());
store.dispatch(loadingFinished());
}
/* createUserAccount mimics registration + e-mail verification mail */
export async function createUserAccount(user) {
try {
await delay(); await delay();
const uid = genId(); const doc = { ...post, postID: genId(), timestamp: nowISO() };
DB.currentUser = { DB.posts.unshift(doc);
id: uid,
displayName: `${user.firstname} ${user.lastname}`, if (me()) me().posts.unshift(doc.postID);
isEmailVerified: true, persist();
};
/* -------------------------------------------------- */ return { id: doc.postID };
}
/* Work out the index: */ export function updatePost(post, postID) {
const idx = DB.posts.findIndex((p) => p.postID === postID);
/* 0 for the first user with this first+last name */ if (idx !== -1) {
DB.posts[idx] = { ...DB.posts[idx], ...post };
persist();
}
}
/* N for the N-th user who shares that name */ /* ------------------------- storage ----------------------------------- */
/* -------------------------------------------------- */ export function addFileToStorage(file) {
console.info("[Mock] saved file", file.name);
const duplicates = DB.users.filter( return Promise.resolve({
(u) => u.firstname === user.firstname && u.lastname === user.lastname ref: { fullPath: `${DB.currentUser?.id}/${file.name}` },
).length; });
}
/* ------------------------- 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({ DB.users.push({
userID: uid, userID: genId(),
firstname: user.firstname, firstname: "Demo",
lastname: user.lastname, lastname: "User",
profilePictureURL: "fakebook-avatar.jpeg", profilePictureURL: "fakebook-avatar.jpeg",
@ -173,242 +517,12 @@ export async function createUserAccount(user) {
posts: [], posts: [],
isOnline: false, isOnline: 0,
isEmailVerified: true, isEmailVerified: true,
index: duplicates, // 0 for first, 1 for second, … index: 0,
}); });
commit(); persist();
console.info("[Mock] Registration OK — verification email “sent”.");
} catch (err) {
store.dispatch(errorOccured(err.message));
} }
}
export async function signInUser(user) {
const NO_ERROR = "";
const EMAIL_VERIF_ERROR = "Please verify your email before to continue";
await delay();
/* Any credentials work in the mock */
const existing =
DB.users.find((u) => u.firstname === user.email.split("@")[0]) ||
DB.users[0];
if (!existing) {
store.dispatch(errorOccured("No account found (mock)"));
} else if (existing && !existing.isEmailVerified) {
DB.currentUser = null;
store.dispatch(errorOccured(EMAIL_VERIF_ERROR));
} else {
DB.currentUser = {
id: existing.userID,
displayName: `${existing.firstname} ${existing.lastname}`,
isEmailVerified: true,
};
/* 👇 NEW: tell Redux that were signed in */
store.dispatch(
signIn({
id: DB.currentUser.id,
displayName: DB.currentUser.displayName,
isEmailVerified: true,
})
);
store.dispatch(errorOccured(NO_ERROR));
commit();
}
store.dispatch(loadingFinished());
}
export function sendPasswordReminder(email) {
console.info(`[Mock] password reset email sent to ${email}`);
return Promise.resolve();
}
/* ---------- Current user doc ---------- */
export function subscribeCurrentUser() {
const uid = store.getState().user.id;
/* Emit once */
setTimeout(() => {
const doc = DB.users.find((u) => u.userID === uid);
store.dispatch(currentUserUpdated(doc || {}));
}, 0);
/* Return unsubscribe noop */
return () => {};
}
export async function currentUserOnline() {
if (me()) {
me().isOnline = true;
commit();
}
}
export async function currentUserOffline() {
if (me()) {
me().isOnline = false;
commit();
}
}
/* ---------- Users list ---------- */
export function subscribeUsers() {
/* Emit immediately */
setTimeout(() => {
store.dispatch(usersUpdated(DB.users.slice()));
}, 0);
return () => {};
}
/* ---------- Posts ---------- */
export function subscribePosts() {
setTimeout(() => {
const postsWithStrings = DB.posts.map((p) => ({
...p,
timestamp: new Date(p.timestamp).toLocaleString(),
}));
store.dispatch(postsUpdated(postsWithStrings));
}, 0);
return () => {};
}
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);
}
commit();
return { id: doc.postID }; // mimic Firebases docRef id
}
export function updatePost(post, postID) {
const idx = DB.posts.findIndex((p) => p.postID === postID);
if (idx !== -1) {
DB.posts[idx] = { ...DB.posts[idx], ...post };
commit();
}
}
/* ---------- Storage uploads ---------- */
export function addFileToStorage(file) {
console.info("[Mock] file saved:", file.name);
return Promise.resolve({
ref: { fullPath: `${DB.currentUser?.id}/${file.name}` },
});
}
/* ---------- Profile ---------- */
export function updateProfile(profile) {
if (me()) {
Object.assign(me(), profile);
commit();
}
return Promise.resolve();
}
/* ---------- Messages ---------- */
export function subscribeMessages(kind) {
const uid = DB.currentUser?.id;
const isIncoming = kind === "incoming";
setTimeout(() => {
const msgs = DB.messages
.filter((m) => (isIncoming ? m.recipient === uid : m.sender === uid))
.map((m) => ({ ...m, timestamp: m.timestamp }));
store.dispatch(
(isIncoming ? incomingMessagesUpdated : outgoingMessagesUpdated)(msgs)
);
}, 0);
return () => {};
}
export function uploadMessage(msg) {
DB.messages.push({
...msg,
id: genId(),
timestamp: nowISO(),
isRead: false,
});
commit();
return Promise.resolve();
}
export function updateToBeRead(messageID) {
const m = DB.messages.find((m) => m.id === messageID);
if (m) {
m.isRead = true;
commit();
}
return Promise.resolve();
}
/* ------ internal helper still referenced by UI code ---------------- */
function updateUserPosts() {
/* kept only to satisfy imports; real logic in upload() above */
}
export { updateUserPosts }; // keep named export so imports dont break