Skip to main content
Version: 1.0.0

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

  1. Role System Overview
  2. Role Types & Hierarchy
  3. Role Mapping Logic
  4. Role Permissions Matrix
  5. Server Roles
  6. Display Roles
  7. Team Roles
  8. Main Profile Logic
  9. Role-Based Access Control (RBAC)
  10. Menu Visibility by Role
  11. Data Access Filtering
  12. Frame Validation Permissions
  13. Notification Routing by Role
  14. Team Member Visibility Rules
  15. 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 RoleMain ProfileDisplay RoleContext
"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/UnknownN/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?

  • isMainProfile is a boolean flag determining if a Cliente role user is the primary administrator
  • Retrieved from Client document's mainProfileList field
  • 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

PermissionSuper AdminAdminMemberModellerModeller SupervisorGuest
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 in fetchTeamMembers()
  • DashboardLayout.js:424 - Menu generation for Admin role
  • Home.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 check
  • profileApi.js:193-235 - Admin (mainProfile) logic
  • profileApi.js:236-279 - Member (non-mainProfile) logic
  • DashboardLayout.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 definition
  • DashboardLayout.js:410-418 - Menu items for Modeller
  • thumbnailsService.js - canDeleteThumbnails() returns false for Modellista
  • 3DAssetsManager.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 definition
  • DashboardLayout.js:420-423 - Menu items
  • NotificationService.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 RoleServer Role(s)Context
Super AdminAdminSystem administrator
AdminCliente + mainProfile=trueClient company admin
MemberCliente + mainProfile=falseRegular client user
ModellerModellista3D asset creator
Modeller SupervisorModellerSupervisorModeller team lead
GuestGuestExternal 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 RoleCan Edit MembersCan Assign TasksCan Delete ContentNotes
Team AdminFull team control
MemberBasic 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: true for all

Admin (Cliente + mainProfile):

  • Can see all team members in their client
  • Can edit all team members in their team
  • canEdit: true for all members

Member (Cliente + !mainProfile):

  • Can see all team members in their client

Code Location:

  • profileApi.js:150-279 - fetchTeamMembers() implementation
  • profileApi.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:

  1. Conditional component rendering based on role
  2. Permission helper functions that check role + context
  3. API-level filtering that restricts data access
  4. 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]);

Complete Menu Matrix

Menu ItemSuper AdminAdminMemberModellerModeller SupervisorGuest
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.

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: /analytics with sub-menu
  • Access: Admin, Cliente, Modeller Supervisor
  • Sub-items:
    • VTO Analytics: Admin, Cliente
    • Gateways Analytics: Admin, Cliente
  • Component: AnalyticsPage.js
  • Condition: analyticsGatewaysVisible computed 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:

RoleSeesScope
Super AdminAll subscriptionsGlobal
AdminClient subscriptionsTheir client only
MemberClient subscriptionsTheir client only
ModellerNoneN/A
Modeller SupervisorNoneN/A
GuestNoneN/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:

RoleSeesScope
Super AdminAll ordersGlobal
AdminClient ordersTheir client only
MemberClient ordersTheir client only
ModellerNo orders pageN/A
Modeller SupervisorNo orders pageN/A
GuestNo orders pageN/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

FunctionPurposeExample Usage
hasPermission(role, permission)Generic permission checkhasPermission('Admin', 'canModifyCoefficients')
canModifyCoefficients(role)Can edit variant coefficientsFrame variant editor
canView3DAssets(role)Can view 3D model preview3D asset viewer
canModifyTagsBadges(role)Can edit tags and badgesVariant actions column
canModifyPriority(role, status)Can set priority for statusPriority selector
isVariantClickable(role, status)Can interact with variantVariant row clickability
canViewNotes(role, status)Can view notes for statusNotes visibility
canAssignTo(role)Can assign variantsAssign 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):

ItalianEnglishDuration
IncompletoIncompleteInitial state
Rev. ModellistaModelist Rev.Modeller review
Rev. SpaarklySpaarkly Rev.Spaarkly review
Rev. ClienteClient Rev.Client review
In PubblicazioneIn PublicationPublication process
PubblicatoPublishedFinal 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:

StateAdminModellistaModeller SupervisorCliente
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

StateNotification RecipientsReason
→ IncompleteNoneInitial state
→ Modelist Rev.AdminAdmin approval needed
→ Spaarkly Rev.Modeller Supervisor, AdminSpaarkly review required
→ Client Rev.ClienteClient review required
→ In PublicationClientePublication in progress
→ PublishedAdmin, ClientePublished - 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

  1. Variant state changes in frame validation
  2. getTargetRolesForStatus() called to determine recipients
  3. getProfilesWithRoles() finds users with those roles
  4. Brand/Client filtering applied (for Clientes)
  5. Assigned user added (for certain states)
  6. Notifications created in Firestore Notifications collection

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 RoleCan View?ScopeCan Manage?Management Scope
Super Admin✅ AllGlobal✅ AllAll members globally
Admin (Cliente + main)✅ AllClient✅ AllAll team members
Member (Cliente + !main)✅ AllClient❌ NoneRead-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:

  1. Add server role value to Profiles.role in Firestore
  2. Update mapRoleForDisplay() in profileApi.js, DashboardLayout.js, GeneralInfoSection.js
  3. Add display role mapping
  4. Update menu generation in DashboardLayout.js
  5. Define permissions in RolePermissions.js
  6. Update RBAC checks throughout codebase
  7. Update notification routing in NotificationService.js
  8. Add integration tests for new role

Changing Role Permissions

Steps:

  1. Update ROLE_PERMISSIONS in RolePermissions.js
  2. Update related helper functions if needed
  3. Verify menu generation still correct
  4. Test all permission-dependent features
  5. Update notification routing if affected
  6. Document breaking changes

Deprecating a Role

Steps:

  1. Add deprecation notice to role definition
  2. Create migration path for existing users
  3. Update role mapping with fallback display name
  4. Gradually transition users to new role
  5. Remove after transition period complete

Testing Role System

Test Coverage Areas

  1. Role Mapping:

    • Test all server → display role conversions
    • Test main profile status effects on display role
    • Test fallback for unknown roles
  2. Menu Visibility:

    • Verify each role sees correct menu items
    • Test menu item visibility combinations
    • Test dynamic visibility (e.g., Analytics submenu)
  3. Data Filtering:

    • Verify role sees correct data scope
    • Test cross-client data isolation
    • Test filtering with multiple clients
  4. Team Member Editing:

    • Test Super Admin can edit all
    • Test Client Admin can edit all team
  5. 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

RoleServer ValueDisplay NameKey PermissionsMenu ItemsAPI Scope
Super AdminAdminSuper AdminFullAllGlobal
AdminCliente + mainAdminFull team management + client scopeAll client featuresClient scope
MemberCliente + !mainMemberClient operations, no team managementAll client featuresClient scope
ModellerModellistaModeller3D asset workDashboard, AR Library, Frame Val.Own assignments
Modeller SupervisorModellerSupervisorModeller SupervisorModeller oversightAll except AdminAssign variants
GuestGuestGuestRead-onlyLimitedRead-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