Skip to main content
Version: 1.0.0

Authentication System

Author: Carmine Antonio Bonavoglia Creation Date: 29/10/2025
Last Reviewer: Carmine Antonio Bonavoglia Last Updated: 31/10/2025

Overview

The Authentication System provides comprehensive user authentication and account management. It's a sophisticated flow with:

  • Firebase Authentication (email/password, sign-in, sign-up, password reset)
  • JWT token-based invitation system (team invitations with expiration)
  • Custom token generation (backend integration for secure API access)
  • Dual profile structure support (old Profile/Brand vs new Profiles/Catalogues)
  • Permission-based profile loading (Super Admin vs Client users role logic)
  • Login access logging (historical tracking with device info)
  • Session management (Redux auth state, secure storage with encryption)
  • Password management (reset via email, change with reauthentication, remember-me feature)

Key characteristics:

  • Email normalization (all emails stored lowercase for consistency)
  • Non-blocking license fetch (login completes if license fetch fails)
  • Token validation & expiration (JWT decode without verification, exp check)
  • Account existence checking (prevents duplicate registrations)
  • Catalogue filtering by role (Super Admin sees all, Client users filtered by permissions)
  • Custom secure token (backend-generated for REST API calls)
  • Remember me password (Base64 encoded localStorage storage)
  • Multi-device tracking (device info saved on each login)
  • Team invitation workflow (invite → email → accept → activate)

Location:

  • Login: /src/pages/Login.js
  • Registration: /src/pages/CompleteRegistration.js
  • Components: /src/components/Login/

Architecture Overview

Authentication System
├─ Entry Points
│ ├─ /login → Login.js + LoginForm.js
│ ├─ /complete-registration → CompleteRegistration.js + SetPasswordForm.js
│ └─ Profile settings → ChangePassword.js

├─ Firebase Auth Layer
│ ├─ signInWithEmailAndPassword (login)
│ ├─ createUserWithEmailAndPassword (registration)
│ ├─ sendPasswordResetEmail (reset)
│ ├─ updatePassword (change)
│ └─ reauthenticateWithCredential (re-verify)

├─ JWT Token Processing
│ ├─ Invite token generation (backend)
│ ├─ JWT decode (email, teamId, exp check)
│ └─ Account existence verification

├─ Profile Data Structure (Dual Support)
│ ├─ Old structure: Profile + Brand collections
│ │ └─ catalogsList, firstName, lastName, role
│ │
│ └─ New structure: Profiles + Catalogues collections
│ └─ list_catalogues, list_main_brands, firstName, lastName, role

├─ State Management
│ ├─ Redux: authSlice (user, token, version)
│ ├─ Local: userId, email, token
│ └─ Secure storage: role, ref_cliente, csToken, email

├─ Permission Loading (Post-Login)
│ ├─ Step 1: Detect profile structure (old vs new)
│ ├─ Step 2: Retrieve user catalogues
│ ├─ Step 3: Filter by role (Super Admin vs Client users)
│ ├─ Step 4: Load brand list
│ └─ Step 5: Store in Redux + localStorage

└─ Post-Login Workflows
├─ Create custom token (backend API call)
├─ Fetch available licenses
├─ Write login access log
├─ Load catalogue permissions
└─ Redirect to /home

Login Flow (Complete Workflow)

Step 1: User Input & Firebase Authentication

Component: LoginForm.js

const handleLogin = async (values) => {
const { email, password } = values;
const normalizedEmail = email.toLowerCase(); // CRITICAL: Normalize to lowercase

try {
// 1. Authenticate with Firebase
await signInWithEmailAndPassword(auth, normalizedEmail, password);

// 2. Check user permissions (canPlaceOrders)
await checkUserPermissions(normalizedEmail);

// 3. Call parent's onSubmit (Login.js handler)
if (onSubmit) {
await onSubmit({ ...values, email: normalizedEmail });
}

// 4. Optional: Save password for next login (encrypted Base64)
if (rememberPassword) {
savePassword(normalizedEmail, password);
}
} catch (error) {
if (error.code === "auth/multi-factor-auth-required") {
messageApi.error("MFA required. Please complete the verification.");
} else {
messageApi.error(error?.message);
}
}
};

Email Normalization:

// Always normalize to lowercase
const normalizedEmail = email.toLowerCase();

// Reason: Firebase Auth internally stores emails in their original case,
// but Firestore lookups MUST use lowercase for consistency

Step 2: Post-Login Data Retrieval

Component: Login.js → postLogin(email)

const postLogin = async (email) => {
try {
// 1. Clean corrupted localStorage values
cleanCorruptedValues();

// 2. Detect & load appropriate profile structure
const userDocRef = doc(db, "Profiles", email.toLowerCase());
const userDoc = await getDoc(userDocRef);

let userData;
if (userDoc.exists()) {
userData = userDoc.data();
// Detect structure: old vs new
if (userData.list_catalogues) {
// NEW structure (Profiles with list_catalogues)
await handleNewProfileData(userData, email);
} else {
// OLD structure (Profile with catalogsList)
await handleOldProfileData(userData, email);
}
} else {
throw new Error("User document does not exist");
}

// 3. Fetch client info
await fetchAndStoreClientInfo(userData);

// 4. Get fresh ID token
const idToken = await auth.currentUser.getIdToken();

// 5. Dispatch Redux action
dispatch(
loginSuccess({
user: { uid: auth.currentUser.uid, email },
token: idToken,
version: "1.0",
})
);

// 6. Store essential data in secure storage
secureSetItem("role", userData.role || "");
secureSetItem("email", email.toLowerCase());
secureSetItem("ref_cliente", userData.clientRef || "");

// 7. Fetch & store licenses (non-blocking)
try {
const licenses = await fetchAvailableLicenses();
dispatch(setAvailableLicenses(licenses));
localStorage.setItem("availableLicenses", JSON.stringify(licenses));
} catch (licenseError) {
console.error("License fetch error:", licenseError);
// IMPORTANT: Don't fail login if license fetch fails
}

// 8. Log access
await writeLoginAccess();

// 9. Create custom token for API
await createCustomToken(idToken);

// 10. Navigate to home
navigate(`${process.env.PUBLIC_URL}/home`);
} catch (error) {
console.error("Post-login error:", error);
messageApi.error(
"An error occurred during login. Please contact support."
);
}
};

Profile Structure Detection (Dual Support)

Old Structure (Profile + Brand collections)

const handleOldProfileData = async (userData, email) => {
try {
// Store old structure fields
secureSetItem("o_firstName", userData.firstName || "");
secureSetItem("o_lastName", userData.lastName || "");
secureSetItem("o_catalogsList", userData.catalogsList || []);
secureSetItem("o_ref_log_access", userData.ref_log_access || "");

// Fetch Brand collection
const brandsSnapshot = await getDocs(collection(db, "Brand"));
let listaBrands = [];

if (userData.role === "Admin") {
// Admin: all brands
listaBrands = brandsSnapshot.docs.map((doc) => doc.id);
} else {
// Non-Admin: filtered by catalogsList + exclude "REV" brands
const tempCatalogsList = userData.catalogsList || [];
listaBrands = brandsSnapshot.docs
.filter((doc) => {
const nomeBrand = doc.data()?.nome_brand || "";
return (
!nomeBrand.includes("REV") &&
tempCatalogsList.includes(doc.id)
);
})
.map((doc) => doc.id);
}

dispatch(configActions.setListaBrand(listaBrands));

// Also load Catalogues for backward compatibility
const cataloguesSnapshot = await getDocs(collection(db, "Catalogues"));
let listaCatalogues = [];

if (userData.role === "Admin") {
listaCatalogues = cataloguesSnapshot.docs.map((doc) => doc.id);
} else {
const tempCatalogsList = userData.catalogsList || [];
listaCatalogues = cataloguesSnapshot.docs
.filter((doc) => tempCatalogsList.includes(doc.id))
.map((doc) => doc.id);
}

dispatch(setListaCatalogues(listaCatalogues));
} catch (error) {
console.error("Error handling old profile data:", error);
throw error;
}
};

New Structure (Profiles + Catalogues collections)

const handleNewProfileData = async (userData, email) => {
try {
// Store new structure fields
secureSetItem("p_firstName", userData.firstName || "");
secureSetItem("p_lastName", userData.lastName || "");
secureSetItem("p_list_catalogues", userData.list_catalogues || []);
secureSetItem("p_list_main_brands", userData.list_main_brands || []);
secureSetItem("p_refLogAccess", userData.refLogAccess || "");

// Fetch Catalogues collection
const cataloguesSnapshot = await getDocs(collection(db, "Catalogues"));
let listaCatalogues = [];

if (userData.role === "Admin") {
// Admin: all catalogues
listaCatalogues = cataloguesSnapshot.docs.map((doc) => doc.id);
} else {
// Non-Admin: filtered by list_catalogues
const tempListCatalogues = userData.list_catalogues || [];
listaCatalogues = cataloguesSnapshot.docs
.filter((doc) => tempListCatalogues.includes(doc.id))
.map((doc) => doc.id);
}

dispatch(setListaCatalogues(listaCatalogues));

// Load brands
let listaBrands = [];
const brandsSnapshot = await getDocs(collection(db, "Brand"));

if (userData.role === "Admin") {
listaBrands = brandsSnapshot.docs.map((doc) => doc.id);
} else {
// Prefer list_main_brands if available
if (
userData.list_main_brands &&
userData.list_main_brands.length > 0
) {
listaBrands = userData.list_main_brands;
} else {
// Fallback: filter from Catalogues
const tempListCatalogues = userData.list_catalogues || [];
listaBrands = brandsSnapshot.docs
.filter((doc) => {
const nomeBrand = doc.data()?.nome_brand || "";
return (
!nomeBrand.includes("REV") &&
tempListCatalogues.includes(doc.id)
);
})
.map((doc) => doc.id);
}
}

dispatch(configActions.setListaBrand(listaBrands));
} catch (error) {
console.error("Error handling new profile data:", error);
throw error;
}
};

Role-Based Filtering Logic

Understanding Role Types in Authentication

During login, the authentication system applies different data filtering rules based on the user's server role stored in the Profiles collection.

Important Distinction:

  • "Admin" in authentication code refers to the Super Admin role (server role: "Admin")
  • Client Admins (Cliente + mainProfile) and Members (Cliente + !mainProfile) follow the non-Admin filtering logic
  • The display role mapping (Admin/Member) happens later in the UI layer

Filtering Rules

Super Admin (server role: "Admin"):

if (userData.role === "Admin") {
// Load ALL catalogues and brands globally
listaCatalogues = cataloguesSnapshot.docs.map((doc) => doc.id);
listaBrands = brandsSnapshot.docs.map((doc) => doc.id);
}
  • Access: Global (all catalogues, all brands)
  • Scope: Entire system
  • Use case: Spaarkly administrators managing all clients

Client Users (server role: "Cliente"):

else {
// Filter by user's catalogue/brand permissions
listaCatalogues = cataloguesSnapshot.docs
.filter((doc) => userData.list_catalogues.includes(doc.id))
.map((doc) => doc.id);

// Use list_main_brands or filter from catalogues
listaBrands = userData.list_main_brands || /* filtered brands */;
}
  • Access: Client-scoped (own catalogues and brands only)
  • Scope: Single client organization
  • Applies to: Both Admin (mainProfile) and Member (!mainProfile)
  • Note: Admin vs Member distinction is made in the UI layer for team management permissions

Other Roles (Modeller, ModellerSupervisor):

  • Follow client-scoped filtering
  • Typically have restricted catalogue access
  • May have limited brand visibility

Catalogue Filtering Pattern

Old Structure (Profile + Brand):

// Filter by catalogsList field
const tempCatalogsList = userData.catalogsList || [];
listaBrands = brandsSnapshot.docs
.filter((doc) => {
const nomeBrand = doc.data()?.nome_brand || "";
// Exclude REV brands and check against catalogsList
return !nomeBrand.includes("REV") && tempCatalogsList.includes(doc.id);
})
.map((doc) => doc.id);

New Structure (Profiles + Catalogues):

// Filter by list_catalogues field
const tempListCatalogues = userData.list_catalogues || [];
listaCatalogues = cataloguesSnapshot.docs
.filter((doc) => tempListCatalogues.includes(doc.id))
.map((doc) => doc.id);

// Prefer list_main_brands if available
if (userData.list_main_brands && userData.list_main_brands.length > 0) {
listaBrands = userData.list_main_brands;
}

Registration Flow (Team Invitation)

Component: CompleteRegistration.js

const decodeInviteToken = () => {
try {
// 1. Extract token from URL
const urlToken = searchParams.get("token");
if (!urlToken) {
throw new Error("Token not found in URL");
}

// 2. Decode token WITHOUT verification (no secret key available)
const decoded = jwtDecode(urlToken);
if (!decoded) {
throw new Error("Invalid or malformed token");
}

// 3. Check expiration
const now = Math.floor(Date.now() / 1000);
if (decoded.exp && now > decoded.exp) {
return {
success: false,
expired: true,
error: "Token expired",
data: null,
};
}

// 4. Extract data from decoded token
return {
success: true,
expired: false,
data: {
email: decoded.email,
teamId: decoded.teamId,
teamName: decoded.teamName,
invitedAt: decoded.invitedAt,
expiresAt: decoded.exp
? new Date(decoded.exp * 1000).toISOString()
: null,
},
rawToken: urlToken,
};
} catch (error) {
console.error("Token decode error:", error);
return {
success: false,
expired: false,
error: error.message,
data: null,
};
}
};

Step 2: Account Existence Check

Component: CompleteRegistration.js

useEffect(() => {
const checkUserAndToken = async () => {
const result = decodeInviteToken();

if (!result.success) {
setTokenState({
loading: false,
valid: false,
expired: result.expired || false,
error: result.error || null,
data: null,
userExists: false,
});
return;
}

// Token valid - check if user exists in Firebase Auth
try {
const auth = getAuth();
const email = result.data.email.toLowerCase().trim();

// Check if email already has sign-in methods
const signInMethods = await fetchSignInMethodsForEmail(auth, email);
const userExists = signInMethods && signInMethods.length > 0;

setTokenState({
loading: false,
valid: true,
expired: false,
error: null,
data: result.data,
userExists: userExists,
});

// If user exists, redirect to login
if (userExists) {
setTimeout(() => {
navigate(
`${process.env.PUBLIC_URL}/login?accountExists=true`
);
}, 2000);
}
} catch (error) {
console.error("Error checking user existence:", error);
// Proceed with registration anyway on error
setTokenState({
loading: false,
valid: true,
expired: false,
error: null,
data: result.data,
userExists: false,
});
}
};

checkUserAndToken();
}, []);

Step 3: Set Password & Create Account

Component: SetPasswordForm.js

const handleSetPassword = async (values) => {
setLoading(true);
const { password, confirmPassword } = values;

if (password !== confirmPassword) {
messageApi.error("Passwords do not match");
setLoading(false);
return;
}

try {
const normalizedEmail = userEmail.toLowerCase().trim();
const auth = getAuth();

// 1. Create Firebase Auth account
console.log("Creating Firebase Auth account for:", normalizedEmail);
await createUserWithEmailAndPassword(auth, normalizedEmail, password);
console.log("Firebase Auth account created successfully");

// 2. Accept team invitation (update status to "active")
console.log("Accepting team invitation...");
const acceptResult = await acceptTeamInvitation(
teamId,
normalizedEmail
);
console.log("Team invitation accepted:", acceptResult);

// 3. Notify admins of acceptance (non-blocking)
try {
console.log("Notifying admins of member acceptance...");
await notifyTeamAdminsOfAcceptance({
teamId: teamId,
teamName: acceptResult.teamName || "Unknown Team",
adminEmails: [], // Function retrieves admins automatically
memberEmail: normalizedEmail,
});
console.log("✅ All admins notified");
} catch (notifError) {
console.error("⚠️ Failed to notify admins:", notifError);
// Don't fail registration on notification errors
}

setLoading(false);

// Show success modal
Modal.success({
title: "Registration Completed Successfully",
content:
"Your password has been set successfully. You can now log in with your credentials.",
okText: "Go to Login",
centered: true,
onOk: () => {
navigate(`${process.env.PUBLIC_URL}/login`);
},
});
} catch (error) {
setLoading(false);
console.error("Set password error:", error);

// Map Firebase error codes to user-friendly messages
let errorMessage =
"An error occurred whilst completing your registration.";

if (error.code === "auth/email-already-in-use") {
errorMessage =
"An account with this email already exists. Please try logging in instead.";
} else if (error.code === "auth/invalid-email") {
errorMessage =
"The email address is invalid. Please contact the administration.";
} else if (error.code === "auth/weak-password") {
errorMessage =
"The password is too weak. Please use a stronger password.";
}

Modal.error({
title: "Something Went Wrong",
content: errorMessage,
centered: true,
});
}
};

Password Reset Flow

Component: ForgotPasswordForm.js

const handleConfirm = async (values) => {
setLoading(true);
const { email } = values;
const normalizedEmail = email.toLowerCase(); // Normalize email

try {
// Send Firebase password reset email
await sendPasswordResetEmail(auth, normalizedEmail);

messageApi.success(
"Password reset email sent. Please check your inbox."
);
form.resetFields(["email"]);

setLoading(false);
} catch (error) {
messageApi.error(
"Failed to send password reset email. Check your email address."
);
console.error(error);
form.resetFields(["email"]);
setLoading(false);
}
};

User receives email with:

  • Reset link from Firebase
  • Instructions to set new password
  • Link expires after 24 hours (Firebase default)

Password Change Flow

Component: ChangePassword.js

const changePassword = async () => {
if (loading) return;

try {
setLoading(true);

// 1. Validate new passwords match
if (newPassword !== repeatPassword) {
throw new Error("New passwords do not match.");
}

const user = auth.currentUser;

// 2. Reauthenticate with current password
// CRITICAL: User must provide current password for security
const credential = EmailAuthProvider.credential(
user.email,
currentPassword
);
await reauthenticateWithCredential(user, credential);

// 3. Update password
await updatePassword(user, newPassword);

// 4. Clear fields
setCurrentPassword("");
setNewPassword("");
setRepeatPassword("");

message.success("Password changed successfully.");
} catch (error) {
message.error("Error changing password: " + error.message);
} finally {
setLoading(false);
}
};

Security Pattern:

  • Reauthentication required (user proves they know current password)
  • Prevents password theft scenarios where user leaves browser unattended
  • Firebase enforces secure connection (HTTPS only)

Remember Me Feature

Component: LoginForm.js

// Save password (Base64 encoded, NOT secure for production)
const savePassword = (email, password) => {
const encoded = btoa(password); // Simple Base64 encoding
localStorage.setItem(`password_${email}`, encoded);
};

// Retrieve password on email blur
const handleBlur = (e) => {
const email = e.target.value.toLowerCase();
const savedPassword = localStorage.getItem(`password_${email}`);

if (savedPassword) {
setPassword(atob(savedPassword)); // Decode Base64
form.setFieldsValue({ password: atob(savedPassword) });
}
};

⚠️ Security Warning: Base64 encoding is NOT encryption. Use only for convenience, not sensitive environments.


Login Access Logging

Component: Login.js → writeLoginAccess()

const writeLoginAccess = async () => {
if (!email) return;

try {
// 1. Query Coll_LogAccess collection
const logAccessCollection = collection(db, "Coll_LogAccess");
const logAccessQuery = query(
logAccessCollection,
where("account", "==", email)
);
const logDocs = await getDocs(logAccessQuery);

let docRef = null;
logDocs.forEach((doc) => {
docRef = doc.ref;
});

if (docRef) {
// 2. Update with transaction (atomic)
await runTransaction(db, async (transaction) => {
const tDoc = await transaction.get(docRef);
if (!tDoc.exists()) return;

const logAccess = tDoc.data().listaLog || [];
const date = new Date();

// 3. Create log entry with device info
const newLog = {
data: `${date.getFullYear()}-${
date.getMonth() + 1
}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`,
dispositivo: navigator.userAgent, // Browser/device info
};

// 4. Append to history
logAccess.push(newLog);
transaction.update(docRef, { listaLog: logAccess });
});
}
} catch (error) {
console.error("Error writing login access:", error);
// Non-blocking: don't fail login if logging fails
}
};

Data Collected:

  • Timestamp (YYYY-MM-DD HH:mm format)
  • User Agent (browser, OS, device info)
  • Email (from user document)

Custom Token Generation

Component: Login.js → createCustomToken(idToken)

const createCustomToken = async (idToken) => {
try {
// 1. Call backend token service with Firebase ID token
const url = `${process.env.REACT_APP_ACTIVE_BASE_TOKEN}?token=${idToken}`;
const response = await fetch(url);
const result = await response.json();

// 2. Store custom token securely
secureSetItem("csToken", result.csToken);

console.log("Custom token created successfully");
} catch (error) {
console.error("Error creating custom token:", error);
messageApi.error("Dashboard failed to authenticate");
}
};

Purpose:

  • Backend generates custom token for REST API authentication
  • Bridges Firebase Auth with REST API layer
  • Token used in subsequent API calls (Authorization header)

Secure Storage Implementation

Pattern: secureSetItem() and secureGetItem()

// From /src/data/utils.js
const secureSetItem = (key, value) => {
// Implementation-specific: may use AES encryption or simple encoding
localStorage.setItem(`secure_${key}`, JSON.stringify(value));
};

const secureGetItem = (key) => {
const value = localStorage.getItem(`secure_${key}`);
return value ? JSON.parse(value) : null;
};

Stored Items:

secureSetItem("role", userData.role); // "Admin" | "Manager" | "Editor"
secureSetItem("email", email.toLowerCase()); // User email (lowercase)
secureSetItem("ref_cliente", userData.clientRef); // Client reference
secureSetItem("csToken", result.csToken); // Custom API token
secureSetItem("p_firstName", userData.firstName); // First name
secureSetItem("p_list_catalogues", userData.list_catalogues); // Catalogue IDs
secureSetItem("clientId", userData.clientRef); // Client ID

Permission Checking (Post-Login)

Component: LoginForm.js → checkUserPermissions(email)

const checkUserPermissions = async (email) => {
try {
const lowerCaseEmail = email.toLowerCase();
const userDocRef = doc(db, "Profiles", lowerCaseEmail);
const userDoc = await getDoc(userDocRef);

if (userDoc.exists()) {
const data = userDoc.data();
// Check if user can place orders
const canPlaceOrders = data.canPlaceOrders ? "true" : "false";
localStorage.setItem("canPlaceOrders", canPlaceOrders);
} else {
localStorage.setItem("canPlaceOrders", "false");
}
} catch (error) {
console.error("Error fetching user permissions:", error);
localStorage.setItem("canPlaceOrders", "false"); // Default: deny
}
};

Redux Authentication State

File: /src/redux/authentication/authSlice.js

import { createSlice } from "@reduxjs/toolkit";
import { getAuth, onAuthStateChanged, signOut } from "firebase/auth";

export const authSlice = createSlice({
name: "auth",
initialState: {
user: null,
token: null,
version: null,
},
reducers: {
loginSuccess: (state, action) => {
state.user = action.payload.user;
state.token = action.payload.token;
state.version = action.payload.version;
},
logoutSuccess: (state) => {
state.user = null;
state.token = null;
state.version = null;
},
},
});

export const { loginSuccess, logoutSuccess } = authSlice.actions;
export default authSlice.reducer;

Logout Flow

Typically triggered by:

  • User click "Logout" button
  • Token expiration
  • Permission revocation
  • Version enforcement
const handleLogout = async () => {
try {
// 1. Clear Redux state
dispatch(logoutSuccess());

// 2. Clear localStorage
localStorage.clear();
sessionStorage.clear();

// 3. Sign out from Firebase
await signOut(auth);

// 4. Redirect to login
navigate(`${process.env.PUBLIC_URL}/login`);
} catch (error) {
console.error("Logout error:", error);
// Force redirect even on error
navigate(`${process.env.PUBLIC_URL}/login`);
}
};

Multi-Factor Authentication (MFA)

Status: Supported but not fully implemented in current UI

catch (error) {
if (error.code === "auth/multi-factor-auth-required") {
messageApi.error("MFA required. Please complete the verification.");
// UI should show MFA verification screen
}
}

Data Structures

Firebase Auth User

{
uid: string,
email: string,
emailVerified: boolean,
displayName: string | null,
isAnonymous: boolean,
metadata: {
creationTime: timestamp,
lastSignInTime: timestamp
}
}

Profiles Document (New Structure)

{
email: string,
firstName: string,
lastName: string,
role: "Admin" | "Manager" | "Editor" | "Viewer",
list_catalogues: string[], // IDs of accessible catalogues
list_main_brands: string[], // Optional: main brands
clientRef: string, // Client ID reference
refLogAccess: string, // Log access reference
canPlaceOrders: boolean,
// ... other fields
}

Invite Token (JWT)

{
email: string,
teamId: string,
teamName: string,
invitedAt: timestamp,
exp: number, // Expiration in Unix seconds
iat: number, // Issued at time
// ... other claims
}

Login Access Log Entry

{
account: string, // User email
listaLog: [{
data: string, // "2025-10-29 14:30" format
dispositivo: string // User Agent
}, ...]
}

Error Handling & Edge Cases

Edge Case 1: Firebase Multi-Factor Auth Required

if (error.code === "auth/multi-factor-auth-required") {
messageApi.error("MFA required. Please complete the verification.");
// UI should show MFA verification flow
}

Edge Case 2: Weak Password

if (error.code === "auth/weak-password") {
errorMessage = "The password is too weak. Please use a stronger password.";
}

Edge Case 3: Email Already in Use

if (error.code === "auth/email-already-in-use") {
errorMessage =
"An account with this email already exists. Please try logging in instead.";
}

Edge Case 4: Invalid Email

if (error.code === "auth/invalid-email") {
errorMessage =
"The email address is invalid. Please contact the administration.";
}

Edge Case 5: Token Expired

const now = Math.floor(Date.now() / 1000);
if (decoded.exp && now > decoded.exp) {
// Token is expired
return {
success: false,
expired: true,
error: "Token expired",
};
}

Edge Case 6: User Document Not Found

const userDoc = await getDoc(userDocRef);
if (!userDoc.exists()) {
throw new Error("User document does not exist");
}

Edge Case 7: License Fetch Failure (Non-Blocking)

try {
const licenses = await fetchAvailableLicenses();
dispatch(setAvailableLicenses(licenses));
} catch (licenseError) {
console.error("License fetch error:", licenseError);
// CRITICAL: Don't fail login if license fetch fails
// Continue with login process
}

Edge Case 8: Login Access Logging Failure (Non-Blocking)

try {
await writeLoginAccess();
} catch (error) {
console.error("Error writing login access:", error);
// Don't fail login if logging fails
}

Security Patterns

Pattern 1: Email Normalization

const normalizedEmail = email.toLowerCase();
// Ensures: firebase-auth-email-case ≠ firestore-lookup-email

Pattern 2: Reauthentication for Password Change

const credential = EmailAuthProvider.credential(user.email, currentPassword);
await reauthenticateWithCredential(user, credential);
// Ensures: user proves they know current password before changing

Pattern 3: Non-Blocking Error Handling

try {
await fetchAvailableLicenses();
} catch (error) {
console.error(error);
// Continue flow - don't block login on non-critical errors
}

Pattern 4: Account Existence Checking

const signInMethods = await fetchSignInMethodsForEmail(auth, email);
const userExists = signInMethods && signInMethods.length > 0;
// Prevents duplicate account registration

Pattern 5: Role-Based Access Control

if (userData.role === "Admin") {
// Super Admin: all catalogues globally
} else {
// Client users (Admin/Member): filtered by list_catalogues
// Note: "Admin" here refers to Super Admin (server role: "Admin")
// Client Admins (Cliente + mainProfile) follow the else branch
}

Key Points:

  • userData.role === "Admin" checks for Super Admin (server role)
  • Client Admins and Members (both have server role "Cliente") follow the filtered path
  • Display role mapping (Admin/Member) happens in UI components, not during authentication

API Functions Used

Firebase Auth Functions

  • signInWithEmailAndPassword(auth, email, password) — Login
  • createUserWithEmailAndPassword(auth, email, password) — Register
  • sendPasswordResetEmail(auth, email) — Reset password
  • updatePassword(user, password) — Change password
  • reauthenticateWithCredential(user, credential) — Re-verify
  • fetchSignInMethodsForEmail(auth, email) — Check account exists
  • signOut(auth) — Logout
  • getAuth() — Get auth instance

Backend API Functions

  • acceptTeamInvitation(teamId, email) — Accept invite (update status)
  • notifyTeamAdminsOfAcceptance(data) — Send notifications
  • fetchAvailableLicenses() — Get user licenses
  • Custom token endpoint: ${REACT_APP_ACTIVE_BASE_TOKEN}?token=${idToken}

Firestore Functions

  • doc(db, collection, docId) — Get reference
  • getDoc(docRef) — Fetch document
  • getDocs(query) — Fetch multiple
  • collection(db, name) — Get collection reference
  • query(collection, where(...)) — Create query
  • where(field, operator, value) — Add filter
  • runTransaction(db, callback) — Atomic transaction

Pages:

  • /src/pages/Login.js (main login page)
  • /src/pages/CompleteRegistration.js (registration via invite)

Components:

  • /src/components/Login/LoginForm.js (login form)
  • /src/components/Login/ForgotPasswordForm.js (password reset)
  • /src/components/Login/SetPasswordForm.js (set password on register)
  • /src/components/Profile/ChangePassword.js (change password in profile)

Redux:

  • /src/redux/authentication/authSlice.js (auth state management)
  • /src/redux/config/config.js (brand/catalogue config)
  • /src/redux/licenses/actions.js (license management)
  • /src/redux/Catalogues/actions.js (catalogue selection)

Services:

  • /src/services/api/profileApi.js (acceptTeamInvitation)
  • /src/services/api/notificationService.js (notifyTeamAdminsOfAcceptance)
  • /src/services/api/cataloguesApi.js (fetchAvailableLicenses)
  • /src/data/utils.js (secureSetItem, secureGetItem, cleanCorruptedValues)
  • /src/data/base.js (Firebase config: db, auth)

Firestore Collections:

  • Profiles (user profile documents)
  • Brand (old structure: brand collection)
  • Catalogues (new structure: catalogue collection)
  • Coll_LogAccess (login access logs)

Environment Variables:

  • REACT_APP_ACTIVE_BASE_TOKEN (custom token endpoint URL)