Role System Documentation
Author: Carmine Antonio Bonavoglia Creation Date: 29/10/2025
Last Reviewer: Carmine Antonio Bonavoglia Last Updated: 30/10/2025
Table of Contents
- Role System Overview
- Role Types & Hierarchy
- Role Mapping Logic
- Role Permissions Matrix
- Server Roles
- Display Roles
- Team Roles
- Main Profile Logic
- Role-Based Access Control (RBAC)
- Menu Visibility by Role
- Data Access Filtering
- Frame Validation Permissions
- Notification Routing by Role
- Team Member Visibility Rules
- Code Patterns & Implementation
Role System Overview
The ARShades Studio application implements a multi-layered role-based access control system that governs:
- Menu visibility in the dashboard sidebar
- Button/field editability across components
- Data access (subscriptions, orders, team members, catalogues)
- Notifications routing and visibility
- Frame validation workflow permissions
The role system operates at three distinct levels:
Level 1: Server Roles (Database)
Stored in Firebase Firestore Profiles collection field: role
- Represents the authoritative role assigned by administrators
- Used for backend permissions and API access control
Level 2: Display Roles (UI)
Calculated from server roles with context-aware mapping
- Transforms server roles into user-friendly display names
- Considers "main profile" status for client users
- Used in headers, UI elements, and user-facing descriptions
Level 3: Team-Level Roles (Optional)
Used in team/collaborative contexts
- Different from global roles - specific to team membership
- Affects permissions within team operations
- Stored alongside team information
Role Types & Hierarchy
Overall Hierarchy
Super Admin (Admin server role)
└─ Highest permissions across entire system
└─ Can view/edit all clients, all variants, all team members
└─ Can manage all subscriptions and licenses
└─ Can view all analytics
├─ Admin (Cliente server role + mainProfile=true)
│ └─ Client administrator
│ └─ Full access within their client's scope
│ └─ Can manage team members and reset passwords
│ └─ Can manage catalogues, orders, and subscriptions
├─ Member (Cliente server role + mainProfile=false)
│ └─ Regular client user
│ └─ Can manage catalogues and orders within client scope
│ └─ Can validate 3D assets and view analytics
│ └─ Cannot manage team members or reset passwords
├─ Modeller Supervisor (ModellerSupervisor server role)
│ └─ High-level 3D asset management
│ └─ Can assign variants to modellers
│ └─ Full 3D asset access
│ └─ Can view analytics
├─ Modeller (Modellista server role)
│ └─ 3D asset worker
│ └─ Works on assigned variants
│ └─ Limited workflow states access
│ └─ Cannot assign or view analytics
└─ Guest (Guest server role)
└─ Lowest permissions
└─ Read-only access to specific resources
Role Mapping Logic
mapRoleForDisplay() Function
Located in:
/src/services/api/profileApi.js(lines 57-75)/src/components/DashboardLayout.js(lines 152-170)/src/components/Home/GeneralInfoSection.js(lines 113-131)
Function Signature:
const mapRoleForDisplay = (serverRole, isMainProfile = false) => {
// Returns display role string based on server role and main profile status
};
Mapping Logic:
| Server Role | Main Profile | Display Role | Context |
|---|---|---|---|
"Admin" | N/A | "Super Admin" | Spaarkly system administrator |
"Cliente" | true | "Admin" | Client company administrator |
"Cliente" | false | "Member" | Regular client user |
"Modellista" | N/A | "Modeller" | 3D asset creator |
"ModellerSupervisor" | N/A | "Modeller Supervisor" | Modeller team lead |
"Guest" | N/A | "Guest" | Guest access |
| Other/Unknown | N/A | "Member" | Fallback role |
Implementation Example:
const mapRoleForDisplay = (serverRole, isMainProfile = false) => {
switch (serverRole) {
case "Admin":
return "Super Admin";
case "Cliente":
return isMainProfile ? "Admin" : "Member";
case "Modellista":
return "Modeller";
case "ModellerSupervisor":
return "Modeller Supervisor";
case "Guest":
return "Guest";
default:
return "Member";
}
};
Main Profile Determination
What is mainProfile?
isMainProfileis a boolean flag determining if aClienterole user is the primary administrator- Retrieved from
Clientdocument'smainProfileListfield - Checked via email matching:
mainProfileList.includes(email.toLowerCase())
Why Does It Matter?
- Distinguishes between Admin (main) and Member (non-main) client users
- Affects permissions for team member management
- Controls whether user can edit other team members' roles
How It's Determined:
// In profileApi.js and DashboardLayout.js
if (serverRole === "Cliente") {
const clientDoc = await getDoc(doc(db, "Client", clientRef));
const mainProfileList = clientDoc.data().mainProfileList || [];
isMainProfile = mainProfileList.includes(email.toLowerCase());
}
Role Permissions Matrix
RBAC Matrix - Core Permissions
| Permission | Super Admin | Admin | Member | Modeller | Modeller Supervisor | Guest |
|---|---|---|---|---|---|---|
| Create / edit catalogues | ✅ | ✅ | ✅* | ❌ | ❌ | ❌ |
| Publish / unpublish catalogues | ✅ | ✅ | ✅* | ❌ | ❌ | ❌ |
| View draft catalogues | ✅ | ✅ | ✅* | ❌ | ❌ | ❌ |
| Add products to catalogue | ✅ | ✅ | ✅* | ❌ | ❌ | ❌ |
| Order new 3D assets | ✅ | ✅ | ✅* | ❌ | ❌ | ❌ |
| Validate 3D assets | ✅ | ✅ | ✅* | ❌ | ❌ | ❌ |
| Access subscriptions | ✅ | ✅ | ✅* | ❌ | ❌ | ❌ |
| View data consumption | ✅ | ✅ | ✅* | ❌ | ❌ | ❌ |
| View analytics | ✅ | ✅ | ✅* | ❌ | ✅ | ❌ |
| Notifications | ✅ | ✅ | ✅* | ✅ | ✅ | ❌ |
| Manage team members | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Reset passwords | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Edit own profile | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Notes:
✅*= Member can perform these actions but with limited scope within their client context- Admin and Member roles differ mainly in team management capabilities
- Super Admin has global access across all clients
Server Roles
Admin
Database Value: "Admin"
Description: System-wide administrator with complete access across all clients and features.
Permissions:
- View all clients and their profiles
- View all subscriptions globally
- Access all orders
- View all variants in frame validation workflow
- Full analytics access
- Can assign variants globally
- Can modify team members across all clients
Associated UI Elements:
- Display Role:
"Super Admin" - Header: "You are Super Admin for Spaarkly"
- Company Scope: Always "Spaarkly"
- Special Badge: Displayed differently in team lists
Locations in Code:
profileApi.js:150-192- Full role logic for Super Admin infetchTeamMembers()DashboardLayout.js:424- Menu generation for Admin roleHome.js:87- Subscriptions/KPI visibility for Admin
Cliente
Database Value: "Cliente"
Description:
Client company user. Behavior differs based on mainProfile status.
When mainProfile = true:
- Acts as client administrator (display role: "Admin")
- Full permissions within client scope
- Can manage all team members
- Can manage subscriptions and reset passwords
- Can view and manage all orders and catalogues
When mainProfile = false:
- Acts as regular client user (display role: "Member")
- Can manage catalogues and orders within client scope
- Can validate 3D assets
- Can view analytics and data consumption
- Cannot manage team members or reset passwords
Database Structure:
// Stored in Firestore Profiles collection
{
_email: "admin@company.com",
role: "Cliente",
clientRef: "client_123",
firstName: "John",
lastName: "Doe"
}
// Main profile status comes from Client collection
// Client document, mainProfileList field
{
mainProfileList: ["admin@company.com", "admin2@company.com"]
}
Locations in Code:
profileApi.js:57-75-mapRoleForDisplay()with main profile checkprofileApi.js:193-235- Admin (mainProfile) logicprofileApi.js:236-279- Member (non-mainProfile) logicDashboardLayout.js:428- Menu generation for "cliente" or "admin"
Modellista
Database Value: "Modellista"
Description: 3D asset creator/modeller working on assigned variants.
Permissions:
- View and modify own 3D assets
- Access assigned variants only
- Modify coefficients on assigned variants
- Modify tags/badges
- Limited workflow states: "Incomplete", "Modelist Rev."
- Cannot assign variants
- Cannot view analytics
- Cannot delete thumbnails (only ModellerSupervisor and Admin can)
Menu Items:
- Dashboard
- ARShades Library
- My Catalogues
- Frame Validation
Frame Validation States Accessible:
- "Incomplete" (Incompleto)
- "Modelist Rev." (Rev. Modellista)
Locations in Code:
frameValidation/services/Utils/RolePermissions.js:47-52- Permission definitionDashboardLayout.js:410-418- Menu items for ModellerthumbnailsService.js-canDeleteThumbnails()returns false for Modellista3DAssetsManager.js-canView3DAssets()returns true for Modellista
ModellerSupervisor
Database Value: "ModellerSupervisor"
Description: Senior 3D asset manager overseeing modeller team and variants across full workflow.
Permissions:
- View all 3D assets
- Modify coefficients on all variants
- Modify tags/badges on all variants
- Assign variants to other modellers
- Access all workflow states: "Incomplete" through "Published"
- Can delete thumbnails
- Can view analytics
- Receive notifications for supervisor-level changes
Menu Items:
- Dashboard
- ARShades Library
- My Catalogues
- Frame Validation
- Analytics (with VTO/Gateways)
- Subscriptions
- Orders
Frame Validation States Accessible:
- "Incomplete" (Incompleto)
- "Modelist Rev." (Rev. Modellista)
- "Spaarkly Rev." (Rev. Spaarkly)
- "Client Rev." (Rev. Cliente)
- "In Publication" (In Pubblicazione)
- "Published" (Pubblicato)
Locations in Code:
frameValidation/services/Utils/RolePermissions.js:54-60- Permission definitionDashboardLayout.js:420-423- Menu itemsNotificationService.js:77- "Rev. Spaarkly" notifications target ModellerSupervisor
Guest
Database Value: "Guest"
Description: Lowest permission level for external or temporary access.
Permissions:
- Read-only access
- View non-sensitive data
- Can modify tags/badges (for documentation purposes)
- Access all workflow states for viewing
- Cannot modify coefficients
- Cannot view 3D assets
- Cannot delete
Notes:
- Rare role, used for special cases
- Default fallback if role is unknown/undefined
Locations in Code:
frameValidation/services/Utils/RolePermissions.js:68-75- Permission definition
Display Roles
Display roles are the user-facing versions of server roles, optimized for UI presentation.
Display Role Map
| Display Role | Server Role(s) | Context |
|---|---|---|
| Super Admin | Admin | System administrator |
| Admin | Cliente + mainProfile=true | Client company admin |
| Member | Cliente + mainProfile=false | Regular client user |
| Modeller | Modellista | 3D asset creator |
| Modeller Supervisor | ModellerSupervisor | Modeller team lead |
| Guest | Guest | External access |
Display Role Usage Locations
Header Display (DashboardLayout.js:736-755):
// Shows: "You are [mappedRole] for [companyName]"
// Example: "You are Super Admin for Spaarkly"
// "You are Admin for ACME Corp"
// "You are Member for ABC Ltd"
const displayText = `You are ${mappedRole} for ${companyName}`;
Home Page Role Info (GeneralInfoSection.js:62-78):
const roleTexts = {
SuperAdmin: {
role_info: "You are logged in as Super Admin for Spaarkly",
description: "You have full access to all features...",
},
Admin: {
role_info: "You are logged in as Admin for [company_name]",
description: "You can manage catalogues, orders...",
},
Member: {
role_info: "You are logged in as Member for [company_name]",
description: "You can manage catalogues and orders...",
},
Modeller: {
role_info: "You are logged in as Modeller for Spaarkly",
description: "You can work on assigned 3D assets...",
},
"Modeller Supervisor": {
role_info: "You are logged in as Modeller Supervisor for Spaarkly",
description: "You can oversee 3D asset assignments...",
},
};
Team Roles
Team roles are distinct from global roles and apply only within team/collaborative contexts.
Team Role Types
| Team Role | Can Edit Members | Can Assign Tasks | Can Delete Content | Notes |
|---|---|---|---|---|
| Team Admin | ✅ | ✅ | ✅ | Full team control |
| Member | ❌ | ❌ | ❌ | Basic team access |
Team Roles vs Global Roles
Team roles are independent of global roles:
- A Super Admin has full team permissions everywhere
- A regular Member might have "Team Admin" role in a specific team
- Team roles provide contextual permissions within projects/teams
Team Member Visibility & Edit Rules
In profileApi.js fetchTeamMembers() function:
Super Admin (Global Admin):
- Can see all team members across all clients
- Can edit all team members globally
canEdit: truefor all
Admin (Cliente + mainProfile):
- Can see all team members in their client
- Can edit all team members in their team
canEdit: truefor all members
Member (Cliente + !mainProfile):
- Can see all team members in their client
Code Location:
profileApi.js:150-279-fetchTeamMembers()implementationprofileApi.js:280-330-fetchTeamMembersBasic()helper function
Main Profile Logic
What is the Main Profile?
The main profile is the primary administrative account for a client company.
Location in Database:
Firestore Collection: Client
Document: [clientId]
Field: mainProfileList (array)
Example:
{
clientId: "acme_corp",
companyName: "ACME Corporation",
mainProfileList: ["admin@acme.com", "ceo@acme.com"],
allProfileList: ["admin@acme.com", "user1@acme.com", ...]
}
Main Profile Status Determination
// Fetched in DashboardLayout.js (lines 226-255)
const fetchUserProfile = async () => {
try {
const profileDoc = await getDoc(doc(db, "Profiles", email));
const profileData = profileDoc.data();
const serverRole = profileData.role || secureGetItem("role");
if (serverRole === "Cliente" && profileData.clientRef) {
// Get the Client document
const clientDoc = await getDoc(
doc(db, "Client", profileData.clientRef)
);
const clientData = clientDoc.data();
// Check if current email is in mainProfileList
if (clientData.mainProfileList?.includes(email.toLowerCase())) {
isMainProfile = true; // → Display as "Admin"
} else {
isMainProfile = false; // → Display as "Member"
}
}
} catch (error) {
console.error("Error fetching user profile:", error);
}
};
Impact on Permissions
Main Profile (isMainProfile = true):
- Server role:
"Cliente" - Display role:
"Admin" - Can manage all team members and reset passwords
- Can manage subscriptions and orders
- Full catalogue and data management
- Company scope: Their specific company
Non-Main Profile (isMainProfile = false):
- Server role:
"Cliente" - Display role:
"Member" - Can manage catalogues and orders within client scope
- Can validate 3D assets and view analytics
- Cannot manage team members or reset passwords
- Company scope: Their specific company
Real-World Example
// Scenario: ACME Corp with multiple users
Client Document:
{
clientId: "acme_corp",
companyName: "ACME Corporation",
mainProfileList: ["admin@acme.com"],
allProfileList: ["admin@acme.com", "john@acme.com"]
}
// admin@acme.com Profile
{
_email: "admin@acme.com",
role: "Cliente",
clientRef: "acme_corp",
firstName: "Admin",
lastName: "User"
// isMainProfile calculated as: true ✅
// Display role: "Admin"
// Can edit: All team members
}
// john@acme.com Profile
{
_email: "john@acme.com",
role: "Cliente",
clientRef: "acme_corp",
firstName: "John",
lastName: "Doe"
// isMainProfile calculated as: false ❌
// Display role: "Member"
// Can't edit, can add only Members
}
Role-Based Access Control (RBAC)
Overview
RBAC in ARShades Studio is implemented through:
- Conditional component rendering based on role
- Permission helper functions that check role + context
- API-level filtering that restricts data access
- Frame Validation state machine with role-based restrictions
Common RBAC Patterns
Pattern 1: Simple Role Check
const userRole = secureGetItem("role");
if (userRole === "Admin" || userRole === "ModellerSupervisor") {
// Show full analytics
} else {
// Show restricted analytics
}
Pattern 2: Restricted Roles Exclusion
const canPerformActions = (role) => {
const restrictedRoles = ["Modeller", "ModellerSupervisor"];
return !restrictedRoles.includes(role);
};
// Usage in MyCatalogues.js, ARSCatalog.js, Home.js
Pattern 3: Role-Based Helper Functions
// From 3DAssetsManager.js
export const canView3DAssets = (role) => {
return ["Admin", "Modellista", "ModellerSupervisor"].includes(role);
};
// From thumbnailsService.js
export const canDeleteThumbnails = (role, userEmail, variant) => {
if (role === "Admin" || role === "ModellerSupervisor") {
return true;
}
if (role === "Modellista") {
return variant.assignedTo === userEmail;
}
return false;
};
Pattern 4: Permission Object Check
// From RolePermissions.js
export const hasPermission = (role, permission) => {
if (!ROLE_PERMISSIONS[role]) {
return false;
}
return ROLE_PERMISSIONS[role][permission] || false;
};
// Usage
if (hasPermission(userRole, "canModifyCoefficients")) {
// Allow coefficient modification
}
Component-Level RBAC Implementation
DashboardLayout.js - Menu Visibility (lines 368-484):
const menuItems = useMemo(() => {
const items = [];
// Dashboard - Always visible
items.push({
key: "home",
icon: <HomeIcon />,
label: "Dashboard",
});
// ARShades Library - Modeller, ModellerSupervisor only
if (
role?.toLowerCase() === "modellista" ||
role?.toLowerCase() === "modellersupervisor"
) {
items.push({
key: "arshadeslibrary",
icon: <LibraryIcon />,
label: "ARShades Library",
});
}
// Catalogues - Not available for Modeller/ModellerSupervisor
if (
role?.toLowerCase() !== "modellista" &&
role?.toLowerCase() !== "modellersupervisor"
) {
items.push({
key: "mycatalogues",
icon: <CatalogIcon />,
label: "My catalogues",
});
}
// Analytics - Admin, Cliente
if (role?.toLowerCase() === "admin" || role?.toLowerCase() === "cliente") {
items.push({
key: "analytics",
icon: <AnalyticsIcon />,
label: "Analytics",
});
}
// Subscriptions - Admin and Cliente only
if (role?.toLowerCase() === "admin" || role?.toLowerCase() === "cliente") {
items.push({
key: "subscriptions",
icon: <SubscriptionIcon />,
label: "Subscriptions",
});
}
// Orders - Admin and Cliente only
if (role?.toLowerCase() === "admin" || role?.toLowerCase() === "cliente") {
items.push({
key: "orders",
icon: <OrderIcon />,
label: "Orders",
});
}
// Frame Validation - All roles except Guest (inherited from Frame Validation system)
items.push({
key: "framevalidation",
icon: <ValidationIcon />,
label: "Frame Validation",
});
return items;
}, [role, analyticsGatewaysVisible]);
Menu Visibility by Role
Complete Menu Matrix
| Menu Item | Super Admin | Admin | Member | Modeller | Modeller Supervisor | Guest |
|---|---|---|---|---|---|---|
| Dashboard | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| ARShades Library | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ |
| My Catalogues | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
| Frame Validation | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| Analytics | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ |
| Subscriptions | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Orders | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Team Members** | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Admin Panel | ✅ only | ❌ | ❌ | ❌ | ❌ | ❌ |
Team Members is a nested section within Home/Dashboard (GeneralInfoSection component)
Note: Admin and Member have access to the same menu items, but with different permission levels within each section.
Menu Item Details
Dashboard
- Location:
/home - Access: All roles
- Content: User greeting, notifications, quick stats, role info
- Component:
GeneralInfoSection.js
ARShades Library
- Location:
/arshadeslibrary - Access: Modeller, Modeller Supervisor
- Content: Shared 3D asset library, templates
- Component:
ARShadesLibrary.js - Notes: Modeller-specific workspace
My Catalogues
- Location:
/mycatalogues - Access: Admin, Cliente, Guest (NOT Modeller/ModellerSupervisor)
- Content: Catalogue management, ARShades catalog browsing
- Component:
MyCatalogues.js,ARSCatalog.js - Notes: "Add Catalogue" button hidden for restrictedRoles
Frame Validation
- Location:
/framevalidation - Access: All roles
- Content: Variant workflow, frame validation
- Component:
FrameValidationDashboard.js - Notes: State access varies by role (see Frame Validation section)
Analytics
- Location:
/analyticswith sub-menu - Access: Admin, Cliente, Modeller Supervisor
- Sub-items:
- VTO Analytics: Admin, Cliente
- Gateways Analytics: Admin, Cliente
- Component:
AnalyticsPage.js - Condition:
analyticsGatewaysVisiblecomputed based on availability
Subscriptions
- Location:
/subscriptions - Access: Admin, Cliente only
- Content: Licence tracking, subscription management
- Component:
DataConsumption.js - Notes: Shows role-based filtered data
Orders
- Location:
/orders - Access: Admin, Cliente only
- Content: Catalogue orders, order history
- Component:
OrderPage.js - Notes: Shows role-based filtered orders
Team Members
- Location: Home page,
GeneralInfoSection.js - Access: Admin, Cliente (view)
- Content: Team list, role management
- Component:
TeamsSection.js - Notes: Edit permissions vary by user role and member role
Data Access Filtering
Subscription Visibility
Location: dataConsumptionApi.js, useDataConsumption.js
Logic:
const getSubscriptions = async (clientRef, userRole) => {
if (userRole === "Admin") {
// Super Admin sees ALL active subscriptions globally
return query(
collection(db, "ARShadesSubscriptions"),
where("status", "==", "Active")
);
} else {
// All other roles see only their client's subscriptions
return query(
collection(db, "ARShadesSubscriptions"),
where("clientRef", "==", clientRef),
where("status", "==", "Active")
);
}
};
Access Matrix:
| Role | Sees | Scope |
|---|---|---|
| Super Admin | All subscriptions | Global |
| Admin | Client subscriptions | Their client only |
| Member | Client subscriptions | Their client only |
| Modeller | None | N/A |
| Modeller Supervisor | None | N/A |
| Guest | None | N/A |
Order Visibility
Location: catalogOrdersApi.js::getCatalogOrdersByRole()
Logic:
if (userRole === "Admin") {
// Super Admin sees ALL orders
const ordersQuery = query(
collection(db, COLLECTION_NAME),
orderBy("timestamp", "desc")
);
} else if (userRole === "Cliente") {
// Cliente sees only their orders (via clientRef)
const ordersQuery = query(
collection(db, COLLECTION_NAME),
where("clientRef", "==", clientRef),
orderBy("timestamp", "desc")
);
}
Access Matrix:
| Role | Sees | Scope |
|---|---|---|
| Super Admin | All orders | Global |
| Admin | Client orders | Their client only |
| Member | Client orders | Their client only |
| Modeller | No orders page | N/A |
| Modeller Supervisor | No orders page | N/A |
| Guest | No orders page | N/A |
Team Member Visibility
Location: profileApi.js::fetchTeamMembers()
Detailed visibility rules:
Super Admin (Admin role):
- Sees all profiles across all clients
- Can view detailed information for each member
- Retrieves from
fetchAllProfilesGlobal() - Full edit permissions on everyone
Client Admin (Cliente + mainProfile):
- Sees all team members in their client
- Retrieves from
fetchTeamMembersBasic(clientRef) - Full edit permissions on all members
- Can manage team members and reset passwords
Client Member (Cliente + !mainProfile):
- Sees all team members in their client
- Retrieves from
fetchTeamMembersBasic(clientRef) - Cannot manage team members or change roles
- Read-only access to team information
Frame Validation Permissions
Centralized Permission System
Location: /src/frameValidation/services/Utils/RolePermissions.js
This file contains the single source of truth for all frame validation role permissions.
ROLE_PERMISSIONS Object Structure
const ROLE_PERMISSIONS = {
Admin: {
canModifyCoefficients: true,
canView3DAssets: true,
canModifyTagsBadges: true,
priorityAccessStates: [...],
variantAccessStates: [...],
canViewNotes: [...],
canAssignTo: true
},
// Similar structure for each role...
};
Permission Functions
| Function | Purpose | Example Usage |
|---|---|---|
hasPermission(role, permission) | Generic permission check | hasPermission('Admin', 'canModifyCoefficients') |
canModifyCoefficients(role) | Can edit variant coefficients | Frame variant editor |
canView3DAssets(role) | Can view 3D model preview | 3D asset viewer |
canModifyTagsBadges(role) | Can edit tags and badges | Variant actions column |
canModifyPriority(role, status) | Can set priority for status | Priority selector |
isVariantClickable(role, status) | Can interact with variant | Variant row clickability |
canViewNotes(role, status) | Can view notes for status | Notes visibility |
canAssignTo(role) | Can assign variants | Assign modal |
Permissions by Role
Admin
{
canModifyCoefficients: true,
canView3DAssets: true,
canModifyTagsBadges: true,
canAssignTo: true,
priorityAccessStates: ["All states"],
variantAccessStates: ["All states"],
canViewNotes: ["All states"]
}
Cliente (from FrameValidation context, not dashboard)
{
canModifyCoefficients: false,
canView3DAssets: false,
canModifyTagsBadges: false,
canAssignTo: false,
priorityAccessStates: ["Client Rev.", "In Publication", "Published"],
variantAccessStates: ["Client Rev.", "In Publication", "Published"],
canViewNotes: ["Client Rev.", "In Publication", "Published", "Spaarkly Rev."]
}
Modellista
{
canModifyCoefficients: true,
canView3DAssets: true,
canModifyTagsBadges: true,
canAssignTo: false,
priorityAccessStates: ["Incomplete", "Modelist Rev."],
variantAccessStates: ["Incomplete", "Modelist Rev."],
canViewNotes: ["Incomplete", "Modelist Rev."]
}
ModellerSupervisor
{
canModifyCoefficients: true,
canView3DAssets: true,
canModifyTagsBadges: true,
canAssignTo: true,
priorityAccessStates: ["All states"],
variantAccessStates: ["All states"],
canViewNotes: ["All states"]
}
Workflow States
States represent the progression of a variant through the validation workflow.
States (Italian → English):
| Italian | English | Duration |
|---|---|---|
| Incompleto | Incomplete | Initial state |
| Rev. Modellista | Modelist Rev. | Modeller review |
| Rev. Spaarkly | Spaarkly Rev. | Spaarkly review |
| Rev. Cliente | Client Rev. | Client review |
| In Pubblicazione | In Publication | Publication process |
| Pubblicato | Published | Final state |
State Progression:
Incomplete
↓ (Modeller completes work)
Modelist Rev.
↓ (Modeller Supervisor approves)
Spaarkly Rev.
↓ (Spaarkly approves)
Client Rev.
↓ (Client approves)
In Publication
↓ (Publication finalized)
Published
State-Specific Access
Who can access each state:
| State | Admin | Modellista | Modeller Supervisor | Cliente |
|---|---|---|---|---|
| Incomplete | ✅ | ✅ | ✅ | ❌ |
| Modelist Rev. | ✅ | ✅ | ✅ | ❌ |
| Spaarkly Rev. | ✅ | ❌ | ✅ | ❌ |
| Client Rev. | ✅ | ❌ | ✅ | ✅ |
| In Publication | ✅ | ❌ | ✅ | ✅ |
| Published | ✅ | ❌ | ✅ | ✅ |
Notification Routing by Role
Overview
Notifications are routed to specific roles based on variant state changes.
Location: /src/frameValidation/services/Utils/NotificationService.js
Notification Routing Logic
Function: getTargetRolesForStatus(newStatus)
Returns array of roles that should receive notification for a given state change.
static getTargetRolesForStatus(newStatus) {
switch (newStatus) {
case "Modelist Rev.":
return ["Admin"];
case "Spaarkly Rev.":
return ["ModellerSupervisor", "Admin"];
case "Client Rev.":
return ["Cliente"];
case "In Publication":
return ["Cliente"];
case "Published":
return ["Admin", "Cliente"];
default:
return [];
}
}
Notification Routing Matrix
| State | Notification Recipients | Reason |
|---|---|---|
| → Incomplete | None | Initial state |
| → Modelist Rev. | Admin | Admin approval needed |
| → Spaarkly Rev. | Modeller Supervisor, Admin | Spaarkly review required |
| → Client Rev. | Cliente | Client review required |
| → In Publication | Cliente | Publication in progress |
| → Published | Admin, Cliente | Published - for records |
Special Rules
Assigned User Inclusion:
// In "Rev. Modellista" (Modelist Rev.) state:
// The assigned user is ALWAYS notified in addition to role-based recipients
if (newStatus === "Rev. Modellista" && assignedTo) {
recipients.push(assignedTo); // Add assigned user
}
Brand-Based Filtering:
// Cliente notifications are filtered by brandId
// Only Clientes with this brand in list_main_brands receive notification
for (const clientEmail of clientEmails) {
const profile = await getDoc(doc(db, "Profiles", clientEmail));
if (profile.data().list_main_brands?.includes(brandId)) {
recipients.push(clientEmail);
}
}
Notification Creation Flow
- Variant state changes in frame validation
getTargetRolesForStatus()called to determine recipientsgetProfilesWithRoles()finds users with those roles- Brand/Client filtering applied (for Clientes)
- Assigned user added (for certain states)
- Notifications created in Firestore
Notificationscollection
Notification Collection Structure
{
brandId: "spaarkly_brand_001",
date: Timestamp.now(),
sentTo: "admin@company.com",
sentBy: "modeller@spaarkly.com",
type: "Variant assigned",
variantId: "variant_12345",
read: false,
skuModel: "SKU_001",
nameVariant: "Model A",
productId: "product_123"
}
Team Member Visibility Rules
Complete Team Member Visibility Matrix
Viewing Team Members:
| Current User Role | Can View? | Scope | Can Manage? | Management Scope |
|---|---|---|---|---|
| Super Admin | ✅ All | Global | ✅ All | All members globally |
| Admin (Cliente + main) | ✅ All | Client | ✅ All | All team members |
| Member (Cliente + !main) | ✅ All | Client | ❌ None | Read-only |
| Modeller | ❌ - | Team only | ❌ - | Cannot access |
| Modeller Supervisor | ❌ - | Team only | ❌ - | Cannot access |
Team Composition (allProfileList vs mainProfileList)
allProfileList:
- Contains ALL users in client (Admins, Members)
- Used for team member visibility
- Includes mainProfileList users
mainProfileList:
- Contains ONLY primary administrators
- Subset of allProfileList
- Used to determine "Admin" vs "Member" display role
- Usually 1-2 users
// Example Client document
{
clientId: "client_123",
companyName: "ACME Corp",
allProfileList: [
"admin@acme.com",
"john@acme.com"
],
mainProfileList: [
"admin@acme.com" // Only the main admin
]
}
Edit Permission Details
Super Admin:
member.canEdit = true; // Can edit all members globally
- Can change roles
- Can remove from team
- Can change contact info
- Can reset passwords
Admin (Cliente+main):
member.canEdit = true; // Can edit all team members
- Can manage all team members
- Can reset passwords
- Can change roles within client
Member (Cliente + !main):
member.canEdit = false; // Read-only access
- Cannot manage team members
- Cannot change roles
- Cannot reset passwords
- Can view team information only
enrichMemberWithDetails() Function
Located in profileApi.js, enriches basic member info with additional details:
const enrichMemberWithDetails = async (member, clientRef) => {
// Adds to each member:
// - Full profile information
// - Team role (if applicable)
// - Additional metadata
// - Contact information
};
Enriched Member Object:
{
email: "member@acme.com",
firstName: "John",
lastName: "Doe",
role: "Cliente",
isAdmin: false,
canEdit: false,
// Additional enriched fields:
phone: "555-1234",
department: "Sales",
joinDate: "2024-01-15",
...
}
Code Patterns & Implementation
Pattern 1: Role Checking in Components
Simple Role Check:
const userRole = secureGetItem("role");
if (userRole === "Admin") {
// Super Admin specific code
} else if (userRole === "Cliente") {
// Client user code
}
Array Inclusion Check:
const adminRoles = ["Admin", "ModellerSupervisor"];
if (adminRoles.includes(userRole)) {
// For admin-level users
}
Negation Check (Restricted Roles):
const canPerformAction = (role) => {
const restrictedRoles = ["Modeller", "ModellerSupervisor"];
return !restrictedRoles.includes(role);
};
if (canPerformAction(userRole)) {
// Allowed action
}
Pattern 2: Conditional Rendering
Role-Based Menu Items:
const menuItems = useMemo(() => {
const items = [
{ key: "dashboard", label: "Dashboard" }, // Always shown
];
if (role?.toLowerCase() === "modellista") {
items.push({ key: "library", label: "AR Library" });
}
if (["admin", "cliente"].includes(role?.toLowerCase())) {
items.push({ key: "orders", label: "Orders" });
}
return items;
}, [role]);
Conditional Component Render:
{
isAdmin && <AdminOnlySection />;
}
{
role === "Cliente" && <ValidationPanel />;
}
Pattern 3: Permission Helper Functions
Central Permission Utility:
// RolePermissions.js
export const hasPermission = (role, permission) => {
if (!ROLE_PERMISSIONS[role]) {
return false;
}
return ROLE_PERMISSIONS[role][permission] || false;
};
// Usage
if (hasPermission(userRole, "canModifyCoefficients")) {
// Show edit buttons
}
Role-Specific Helpers:
export const canDeleteThumbnails = (role, userEmail, variant) => {
// Super Admin or Modeller Supervisor can delete anything
if (["Admin", "ModellerSupervisor"].includes(role)) {
return true;
}
// Modeller can only delete their own
if (role === "Modellista") {
return variant.assignedTo === userEmail;
}
return false;
};
export const canView3DAssets = (role) => {
return ["Admin", "Modellista", "ModellerSupervisor"].includes(role);
};
Pattern 4: API-Level Filtering
Role-Based Data Fetching:
export const getCatalogOrdersByRole = async (userEmail, userRole, clientId) => {
if (userRole === "Admin") {
// Fetch ALL orders globally
return getDocs(query(collection(db, "CatalogOrders")));
} else if (userRole === "Cliente") {
// Fetch only this client's orders
return getDocs(
query(
collection(db, "CatalogOrders"),
where("clientRef", "==", clientId)
)
);
} else {
// Other roles: no access
return [];
}
};
Pattern 5: State-Based Access
Status and Role Combined Check:
const canAccessVariant = (role, variantStatus) => {
if (!ROLE_PERMISSIONS[role]) {
return false;
}
const allowedStates = ROLE_PERMISSIONS[role].variantAccessStates;
return allowedStates.includes(variantStatus);
};
// Usage
if (canAccessVariant(userRole, "Modelist Rev.")) {
// Show variant details
}
Pattern 6: Main Profile Determination
In Component (e.g., DashboardLayout.js):
useEffect(() => {
const fetchUserProfile = async () => {
const profileDoc = await getDoc(doc(db, "Profiles", email));
const profileData = profileDoc.data();
const serverRole = profileData.role;
if (serverRole === "Cliente" && profileData.clientRef) {
const clientDoc = await getDoc(
doc(db, "Client", profileData.clientRef)
);
const isMainProfile = clientDoc
.data()
.mainProfileList?.includes(email.toLowerCase());
const displayRole = mapRoleForDisplay(serverRole, isMainProfile);
setMappedRole(displayRole);
}
};
fetchUserProfile();
}, [email]);
Pattern 7: Role in Notification Context
Notification Sender Tracking:
const notificationData = {
type: "Variant assigned",
sentTo: recipientEmail,
sentBy: secureGetItem("email"), // Current user
sentByRole: secureGetItem("role"), // Logged for audit
date: Timestamp.now(),
// ...
};
Notification Display Role Mapping:
// When displaying who sent a notification
const displaySenderRole = mapRoleForDisplay(
notification.sentByRole,
isMainProfile
);
Pattern 8: Team Member Edit Permission Check
In Team Member Editor:
const canEditMember = (currentUserRole, currentIsMain, targetMember) => {
// Super Admin
if (currentUserRole === "Admin") {
return true;
}
// Client Admin (mainProfile)
if (currentUserRole === "Cliente" && currentIsMain) {
return true;
}
// All others - read-only
return false;
};
Security Considerations
Role Trust Boundary
Client-Side Storage:
// Role is stored in localStorage/secureStorage
const userRole = secureGetItem("role");
// ⚠️ This is READABLE but could theoretically be modified
// Therefore, ALL critical checks should also happen server-side
Backend Validation:
- All role-based API decisions should verify role server-side
- Firebase Security Rules should enforce role-based access
- Never trust client-side role alone for sensitive operations
Permission Escalation Prevention
Example: Editing Team Members
// ✅ Correct: Check both user role AND target member role
const canEditMember = (userRole, isMainProfile, targetRole) => {
if (userRole === "Admin") return true; // Super Admin
if (userRole === "Cliente" && isMainProfile) return true; // Client Admin
return false;
};
### Information Disclosure
**Data Filtering:**
```javascript
// ✅ Correct: Filter sensitive data based on user role
if (userRole === "Admin") {
return allSubscriptions; // Show everything
} else {
return clientSubscriptions; // Show only their client's data
}
// ❌ Wrong: Show all data to all users
return allSubscriptions;
Integration Points
Authentication & Authorization
- Login Flow (
/src/pages/LoginPage.js): Role retrieved after authentication - Profile Loading (
/src/services/api/profileApi.js): Role fetched from Firestore - Redux State (
/src/redux/): Role-based permissions stored
Dashboard Routing
- DashboardLayout.js: Menu generation based on role
- Navigation: Available routes determined by role
- Header Display: Role-specific greeting and info
Frame Validation Workflow
- RolePermissions.js: Centralized permission definitions
- Variant Actions: All actions checked against role permissions
- State Machine: Transitions restricted by role + state
Data APIs
- profileApi.js: Role-based team member visibility
- catalogOrdersApi.js: Role-based order filtering
- dataConsumptionApi.js: Role-based subscription access
Notifications
- NotificationService.js: Role-based notification routing
- Notification Collection: Stores role info for audit trail
Migration & Future Changes
Adding a New Role
Steps:
- Add server role value to
Profiles.rolein Firestore - Update
mapRoleForDisplay()inprofileApi.js,DashboardLayout.js,GeneralInfoSection.js - Add display role mapping
- Update menu generation in
DashboardLayout.js - Define permissions in
RolePermissions.js - Update RBAC checks throughout codebase
- Update notification routing in
NotificationService.js - Add integration tests for new role
Changing Role Permissions
Steps:
- Update
ROLE_PERMISSIONSinRolePermissions.js - Update related helper functions if needed
- Verify menu generation still correct
- Test all permission-dependent features
- Update notification routing if affected
- Document breaking changes
Deprecating a Role
Steps:
- Add deprecation notice to role definition
- Create migration path for existing users
- Update role mapping with fallback display name
- Gradually transition users to new role
- Remove after transition period complete
Testing Role System
Test Coverage Areas
-
Role Mapping:
- Test all server → display role conversions
- Test main profile status effects on display role
- Test fallback for unknown roles
-
Menu Visibility:
- Verify each role sees correct menu items
- Test menu item visibility combinations
- Test dynamic visibility (e.g., Analytics submenu)
-
Data Filtering:
- Verify role sees correct data scope
- Test cross-client data isolation
- Test filtering with multiple clients
-
Team Member Editing:
- Test Super Admin can edit all
- Test Client Admin can edit all team
-
Frame Validation:
- Test each role can access allowed states
- Test role-specific permissions work
- Test notification routing by role
Summary Table
Quick Reference - All Roles
| Role | Server Value | Display Name | Key Permissions | Menu Items | API Scope |
|---|---|---|---|---|---|
| Super Admin | Admin | Super Admin | Full | All | Global |
| Admin | Cliente + main | Admin | Full team management + client scope | All client features | Client scope |
| Member | Cliente + !main | Member | Client operations, no team management | All client features | Client scope |
| Modeller | Modellista | Modeller | 3D asset work | Dashboard, AR Library, Frame Val. | Own assignments |
| Modeller Supervisor | ModellerSupervisor | Modeller Supervisor | Modeller oversight | All except Admin | Assign variants |
| Guest | Guest | Guest | Read-only | Limited | Read-only |
References
Key Files
Role Mapping:
/src/services/api/profileApi.js- Core role and team member logic/src/components/DashboardLayout.js- Menu generation by role/src/components/Home/GeneralInfoSection.js- Role info display
Permissions:
/src/frameValidation/services/Utils/RolePermissions.js- Frame validation permissions/src/services/utils/3DAssetsManager.js- 3D asset permission checks/src/services/api/thumbnailsService.js- Thumbnail deletion checks
Notifications:
/src/frameValidation/services/Utils/NotificationService.js- Role-based notification routing
Data Access:
/src/services/api/catalogOrdersApi.js- Order role filtering/src/services/hooks/useDataConsumption.js- Subscription role filtering/src/services/api/dataConsumptionApi.js- Data consumption filtering
Components:
/src/pages/MyCatalogues.js- Catalogue access by role/src/pages/ARSCatalog.js- Catalog order access/src/pages/Home.js- Dashboard features by role/src/components/ProductDetail/UnifiedProductDetail.js- 3D asset visibility
Glossary
- RBAC: Role-Based Access Control
- Server Role: Role value stored in Firestore (source of truth)
- Display Role: User-friendly version of server role shown in UI
- Main Profile: Primary administrative account for a client
- Team Member: User in a client's allProfileList
- Frame Validation: Variant workflow and QA process
- State: Variant progression stage in frame validation workflow
- Permission: Specific capability (e.g., canModifyCoefficients)
- clientRef: Reference/ID linking Profile to Client document
- brandId: Reference to a brand/product line