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)
Step 1: Invite Link & Token Validation
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)— LogincreateUserWithEmailAndPassword(auth, email, password)— RegistersendPasswordResetEmail(auth, email)— Reset passwordupdatePassword(user, password)— Change passwordreauthenticateWithCredential(user, credential)— Re-verifyfetchSignInMethodsForEmail(auth, email)— Check account existssignOut(auth)— LogoutgetAuth()— Get auth instance
Backend API Functions
acceptTeamInvitation(teamId, email)— Accept invite (update status)notifyTeamAdminsOfAcceptance(data)— Send notificationsfetchAvailableLicenses()— Get user licenses- Custom token endpoint:
${REACT_APP_ACTIVE_BASE_TOKEN}?token=${idToken}
Firestore Functions
doc(db, collection, docId)— Get referencegetDoc(docRef)— Fetch documentgetDocs(query)— Fetch multiplecollection(db, name)— Get collection referencequery(collection, where(...))— Create querywhere(field, operator, value)— Add filterrunTransaction(db, callback)— Atomic transaction
Related Files & Architecture
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)