Table Of Contents
- Introduction
- System Overview
- Technology Stack
- Database
- Authentication
- Authorization
- Back-end Flows Structure
- Entry points : uibuilder integration
- Action Dispatching With Switch Nodes
- Internal Feature Workflows (Tabs)
- Role-Based Authorization Flows
- Database Operations Layer
- Response Routing Back to UI
- Workflows
- Main
- Sign-up
- Login
- Forget Password
- User Dashboard Data
- How Users Get their Dashboard Data ?
- Applicant Dashboard Data
- Applicant & Auditor Dashboard Data
- Applicant & Approver Dashboard Data
- Get User’s Dashboard Data using API
- Dashboard Requests
- How Dashboard Requests Are Handled ?
- Create Draft
- Update Draft
- Get Draft
- Delete Draft
- Create request
- Edit Request
- Archive Request
- Audit Request
- Approve Request
- Set Role
- Edit Profile
- Update Avatar
- Logout
- Database
- Collections
- Documents
- TTL Index
- Index
Introduction
Trusted Cloud is a role‑based onboarding system that helps users register, submit onboarding requests, and go through auditing and approval steps. Each user gets access to a dashboard and features that match their role, so the system stays both secure and easy to use.
Users can have one or more of these roles:
- Applicant
- Applicant & Auditor
- Applicant & Approver
- Admin
Based on their role, They have a dashboard which displays the onboarding requests in their panel.
Each role controls what the user can see and do:
- Applicants can create requests, save drafts, and view only their own data.
- Auditors can see every request in the system and perform audits.
- Approvers can view audited requests and make final decisions.
- Admins have full access, including role management and complete visibility over users and requests.
Every onboarding request goes through a simple lifecycle:
- Pending
- Audited
- Approved
- Rejected
Applicants submit requests, Auditors review them, and Approvers finalize the outcome.
Alongside the request workflow, Trusted Cloud includes essential account features:
- Email‑verified sign‑up
- Login with token‑based sessions
- Reset password by email code
- Profile and avatar editing
- Draft saving and editing for multi‑step forms
- Logout and token cleanup
System Overview
Technology Stack
Trusted Cloud is built using a combination of Node‑RED’s built‑in capabilities and carefully selected third‑party nodes. JavaScript serves as the core language for both backend logic and custom function nodes. The following technologies form the foundation of the platform:
-
Node‑RED
Used extensively for authentication, authorization, request handling, routing, and internal operations. Built‑in nodes, along with custom Function nodes, define the majority of the application’s logic.
-
MongoDB
Acts as the primary database for persisting user profiles, requests, drafts, and system data. The platform uses the node‑red‑contrib‑mongodb4 module, which provides a shared MongoDB client for reliable and secure connections via configured credentials.
-
Bcrypt
The node‑red‑contrib‑bcrypt module is used for hashing and verifying user passwords. It ensures secure password storage and follows industry‑standard cryptographic practices.
-
UIBUILDER
The main frontend integration layer. All incoming client requests pass through the node‑red‑contrib‑uibuilder nodes, and all backend responses are routed back through the same channel. It acts as the bridge between UI actions and backend workflows.
-
Email
Trusted Cloud uses the node‑red‑node‑email module to send transactional emails, including account verification and password‑reset notifications. It ensures reliable communication with users during key account lifecycle events.
Database
Trusted Cloud communicates directly with MongoDB, using it as the primary storage layer for all user data, onboarding requests, and system‑generated records. Backend flows perform essential CRUD operations through Node‑RED’s MongoDB nodes, and most frontend actions require direct interaction with these database operations.
Trusted Cloud uses the following five collections to organize and persist data:
-
Users
Stores user accounts, authentication tokens, roles, and profile information. This collection holds all core identity and authorization data for the platform.
-
Requests
Contains all onboarding requests created by Applicants. It also stores review information from Auditors and Approvers, including decisions, comments, and status updates throughout the request lifecycle.
-
Drafts
Maintains partially completed or unsubmitted onboarding requests. This allows Applicants to save progress and continue their request at a later time.
-
ResetPasswordCodes
Temporarily stores verification codes sent to users who initiate the password reset process. These codes are short‑lived and cleared after use or expiration.
-
EmailVerificationCodes
Temporarily stores email verification codes used during the account creation (sign‑up) process. These entries ensure that only verified email addresses can be used to create an account.
Authentication
Trusted Cloud uses a token‑based authentication mechanism to manage both user identity and access control. When a user provides valid login credentials, the system generates a fresh access token and returns it to the frontend. The frontend stores this token in a cookie.
For every subsequent dashboard request, the frontend automatically sends this token back to the backend. Backend flows validate the token on each request to ensure that:
- the token exists
- the token belongs to a valid user
- the token has not expired
Only after these checks are passed does the backend allow access to protected routes such as dashboard actions, request creation, auditing, and approval operations. If the token is missing, invalid, or expired, the user is considered unauthenticated and is redirected to the login process.
Authorization
All backend routes in Trusted Cloud are protected and require a valid access token before they can be executed. Every request coming from the frontend passes through a uibuilder entry point. As soon as the request reaches the backend, the authorization process begins.
The system performs two initial checks:
-
Token Validity
Ensures the provided token exists and belongs to a registered user.
-
Token Expiry
Verifies that the token has not expired and is still within its valid timeframe.
If any of these checks fail, the user is immediately treated as unauthorized and is redirected to the login process.
If the token is valid and active, the request continues to the appropriate flow responsible for the specific action—whether it is accessing the dashboard, creating a request, auditing, approving, or managing drafts.
This ensures that only authenticated and authorized users can interact with protected backend workflows.
Back-End Flows Structure
Trusted Cloud’s backend is organized into a large set of interconnected flows. Each flow represents a specific backend capability—such as authentication, request management, dashboard data retrieval, or profile updates. The goal of the architecture is to keep every feature isolated, easy to follow, and consistent with a unified message-routing pattern.
The flows are grouped into multiple tabs that together form the entire Trusted Cloud backend. While each tab focuses on a particular domain, the overall structure can be understood through three layers: entry points, internal workflows, and response routing.
1. Entry Points: UIBUILDER Nodes
Every backend operation begins when the frontend sends a message through a uibuilder node. Trusted Cloud uses two major uibuilder nodes:
- login uibuilder (for sign-up, login, and password-reset actions)
- dashboard uibuilder (for authenticated dashboards, role-based panels, request actions, profile updates)
These uibuilder nodes receive the message from the frontend and immediately pass it into the Node‑RED flow system. Each incoming message includes:
- the request type (msg.type)
- the user’s access token (if authenticated)
- the request body data
The system uses these values to determine which backend workflow must be executed next.
2. Identifying Action (Request type) With Switch Nodes
Directly after entering through uibuilder and authorization step, each inbound message flows into a central router— A switch node that checks the msg.type value.
The TC-Main tab contains 2 switch nodes with multiple outputs, routing actions with a specific request type (msg.type).
Each output of the Switch node connects to a dedicated link out node.
Each of these link-out nodes then forwards the message into the dedicated workflow for that feature using corresponding link in nodes on other tabs.
This structure ensures that:
- a single uibuilder entry point can serve many features
- each feature remains isolated and clean
- new actions can be added easily without changing existing flows
3. Internal Feature Workflows (Tabs)
Trusted Cloud back-end structure uses multiple tabs, each implementing one complete backend feature and dividing flows into logical sections.
Examples include:
- TC-Main : Entry point of the app. Receives front-end requests and routes the incoming msg.
- TC-Signup (multi-step email verification + user creation)
- TC-Login (token creation and verifying user credentials)
- TC-ForgetPassword (send code, verify code, set new password)
- TC-GetDashboard (generate dashboard data based on user’s role)
- TC-RequestHandling (handles incoming request from dashboard like: request submission, audition , approval and …)
- TC-EditProfile ( handles updating user profile and avatar)
- TC-API ( provides 2 endpoints for authentication and getting user’s dashboard data)
Each of these tabs includes a fully contained workflow that begins with a link in node and ends with a response back to the correct uibuilder using a link out node.
4. Role-Based Authorization Flows
During authentication flow, The user’s role is detected and once the user is authenticated, all getting dashboard data requests are routed through a role-checking step. The backend extracts the user’s role from the token and determines whether the user is an:
- Applicant
- Applicant & Auditor
- Applicant & Approver
- Admin
Based on this, the request is forwarded to the correct dashboard data flow for handling.
This role-routing is implemented through “Find Roles” function nodes and multiple Switch nodes wired to the respective flows.
5. Database Operations Layer
Almost every workflow interacts with MongoDB using the node-red-contrib-mongodb4 nodes. The typical operations include:
- finding user profiles
- checking/resetting verification codes
- creating requests
- updating request status
- saving drafts
- updating profile/avatars
- modifying tokens on login/logout
Most operations follow a consistent pattern:
- Build query in a Function node
- Send to MongoDB node
- Process result
- Route success or failure back to frontend
6. Response Routing Back to UI
Every workflow ends with a link out node such as:
Each of these connects back to a matching link in node attached to the correct uibuilder node.
This ensures that the response returns to the exact uibuilder node that triggered the flow.
WorkFlows
a. Main Workflow
Main Workflow is the entry point of the application. TC-Main tab contains 2 uibuilder nodes with following URLs:
- login
- dashboard
Most of the requests(msg) coming from these 2 uibuilder contain:
- msg.type(dashboard requests) or msg.auth.type(login request): for routing and checking request type
- msg.auth : for authentication and authorization
- msg.payload : the data coming from front-end. it comes with different name such as : msg.userId , msg.companyDetails and etc.
a.1. Login uibuilder
This node contains the front-end source code for login and sign-up page. Each incoming request related to user sign-up, login and forget password comes from this node. The URL of this node is “login”.
a.2. Dashboard Uibuilder
This node contains the dashboard source code . All the requests coming from dashboard are sent from this node.
Note : Before each uibuilder node, there is a specific link-in node which receive the response from other flows and after each of them , there is debug node which displays the incoming request from uibuilder.
a.3. Auth Flow for login
Right after each uibuilder node , there is a Auth flow which does authentication and authorization. It checks msg.auth.userToken to verify user’s token.
Example Request :
{
"auth": {
"userToken": "mipu8povvawyrs6t",
"clientId": "n0FLDra1km8sKx7hMCw9j"
},
"_socketId": "C97hCGuZSGTqqRFAAA_W",
"_msgid": "1bae240438dd8954"
}
Checking msg.auth.userToken is done by a flow which sits between uibuilder node and switch node the unction nodes of this flow. Table below represents the flow for validating user token :
- Initial token check ⇒ check if userToken has a value or not :
// Normalize so msg.auth always exists
msg.auth = msg.auth || msg.payload?.auth || {};
if (msg.auth.userToken) {
msg.payload = "hasToken";
} else {
msg.payload = "hasNotToken";
}
return msg;
- Then switch on payload to check if userToken has a value
- Check token match :
msg.collection = "Users"
msg.operation = "findOne"
msg.auth = msg.auth || msg.payload?.auth || {};
msg.payload = {
token: msg.auth.userToken
}
return msg;
- switch to payload and check if it it null or not
- if null :
{
"state": "notLoggedIn"
}
- if not null ( token exists ) :
const now = Date.now(); // milliseconds
const expiryMs = new Date(msg.payload.expiresAt).getTime(); // convert DB date to ms
msg.userData = msg.payload;
if (now <= expiryMs) {
msg.payload.isNotExpired = true; // still valid
} else {
msg.payload.isNotExpired = false; // expired
}
return msg;
- if token expired :
{
"state": "notLoggedIn"
}
- if token isn’t expired the request will be routed to check request type switch
So when the request enters this workflow:
- The flow checks if msg.auth.userToken has a value , exists in Users collection, not expired.
- Saves user data.
- ** routes request to the check request type switch node if token has no value.
- returns a no token response if token is empty, invalid or expired
- returns a redirect response to uibuilder node if token is valid.
- No Token Response
{
"state": "notLoggedIn"
}
- Valid Token Response
{
"payload" = {
"action": "login",
"status": "redirect",
"message": "User Already Logged In"
}
}
a.4. Auth Flow for dashboard
Authentication and Authorization for dashboard is pretty same as login flow and it has all same nodes but with a minor difference:
when the request enters this workflow:
- The flow checks if msg.auth.userToken has a value , exists in Users collection, not expired.
- Saves user data.
- ** routes request to the check request type switch node if token is valid **
- returns a no token response if token is empty, invalid or expired
a.5. Check request type
In TC-Main tab There 2 swith nodes which check request type using msg.auth.type field which comes with almost every request. After auth flow, the incoming request is routed to this node to check which type of operation must be executed.
a.5.1 Check request for Login page
The switch node for login uibuilder request check the below types:
- signup-validateInputs
- signup-verifyEmail&createUser
- signup-resendEmailVerifyCode
- signup-cancelEmailVerification
- login
- resetPassword
- resendResetPassword
- sendResetCode
- setNewPassword
- cancelResetPassword
The output of each type is connected to a link-out node which routes the message to it’s specific flow.
a.5.2 Check request for Dashboard
The switch node for login uibuilder request check the below types:
- getDashboard
- createRequest
- archiveRequest
- auditRequest
- approveRequest
- editCompanyDetails
- editProfile
- updateAvatar
- createDraft
- updateDraft
- getDrafts
- setRole
- deleteDraft
- logOut
The getDashboard output is connected to a function which find the role of the user and after that routes the message to correct dashboard data flow.
b. Sign-up Workflow
Sign-up Workflow is responsible for creating new user accounts. It is located inside TC-Signup tab which has 3 flows to handle sign-up logic. Below here are the request types which are routed to this tab :
- signup-validateInputs
- signup-verifyEmail&createUser
- signup-resendEmailVerifyCode
- signup-cancelEmailVerification
so if msg.type matches one these types inside check request type switch node , it is routed to this workflow.
Below here is the step by step sign-up logic of 3 the flows, each flow explaining its own functionality :
a.1. First Flow
This flow is responsible for checking email uniqueness, inputs and verification code generation. This flow is activated when users clicks on sign-up button. The flow receives the following request and implements the step by step logic listed below:
{
"auth":{
"clientId":"n0FLDra1km8sKx7hMCw9j",
"fullName":"Hossein Rafieekhah",
"email":"hosseinrafieekhah@gmail.com",
"password":"111111",
"confirmPassword":"111111",
"type":"signup-validateInputs"
},
"_socketId":"M5v_T-zD9ctkUW-1ABIR",
"_msgid":"d5cd744bbc0f1187"
}
Then the flow :
- Checks if user doesn’t already exists in Users collection using his email
- Send failed response if user already exists :
{
"payload":{
"action":"register",
"status":"failed",
"message":"User With This Email Already Exists"
}
}
- Check if user inputs are not empty
- Send failed response if empty :
{
"action": "register",
"status": "failed",
"message": "Invalid Inputs"
}
- Delete user’s old verification codes from EmailVerificationCodes collection if there is any
- Generate a 6-digit verification code with 2 minute expiry and prepare email topic and payload
- Send email with email node
- insert email into EmailVerificationCodes collection :
msg.collection = "EmailVerificationCodes";
msg.operation = "insertOne"
const expiryUTC = new Date(Date.now() + 2 * 60 * 1000); // 2 minutes from now (UTC)
msg.payload = {
email: msg.auth.email,
verifyCode: msg.emailVerifyCode,
expiry: expiryUTC
}
return msg;
- send a success response to notify front-end email has been sent :
{
"payload":{
"action":"signup",
"status":"success",
"state":"validInputs"
},
}
Note : The resend code option with (msg.auth.type = signup-resendEmailVerifyCode) in linked to the delete users verification codes node not at the beginning of the flow.
a.2. Second Flow
This flow handles code verification for sign-up user account creation. The flow receives the following request and implements the step by step logic listed below:
{
"auth":{
"clientId":"n0FLDra1km8sKx7hMCw9j",
"type":"signup-verifyEmail&createUser",
"email":"alirezabsh.apps@gmail.com",
"fullName":"alireza",
"password":"111111",
"confirmPassword":"111111",
"code":"111111"
},
"_socketId":"M5v_T-zD9ctkUW-1ABIR",
"topic":"Verify Your Email Address – TrustedCloud ",
"_msgid":"ef5cc28575fbbfa3"
}
Then the flow :
- Check if the user’s code exists in DB
- Check code expiry and save it
- Send response if code is expired
{
"payload":{
"action":"verifyEmail",
"status":"expiredCode",
"message":"The verification Code Is Expired. Please choose resend code option to get a new code."
}
}
- Send response if code is invalid
{
"payload":{
"action":"verifyEmail",
"status":"invalidCode",
"message":"The verification Code Is Invalid."
},
}
- Find last user with highest userId
- Add 1 to it and save it
- Hash users password
- Create user (insert user document )
- Send success response
// ### Response - ValidEmail ###
{
"payload":{
"action":"register",
"status":"success",
"state":"verifiedEmail",
"message":"user created successfully"
},
}
- Delete the verificatoon code from DB
a.3. Third Flow
This flow handles the cancelation of code verification . When user clicks on ❌ icon in code verification pop-up. A request with (msg.auth.type=signup-cancelEmailVerification) is sent to this flow. It uses the email to delete sent verification code.
Example Request
{
"auth":{
"email":"alirezabsh.apps@gmail.com",
"type":"signup-cancelEmailVerification"
},
"_socketId":"M5v_T-zD9ctkUW-1ABIR",
"topic":"Verify Your Email Address – TrustedCloud ",
"_msgid":"3d50d44f359b2af0"
}
c. Login Workflow
Login flows is located inside TC-Login tab. A single flow is responsible for handling login logic.
It checks user credentials, generate a new fresh random access token and redirect user to dashboard. The msg.auth.type with value of “login” is routed to this flow. This flow receives the following request :
{
"auth":{
"clientId":"n0FLDra1km8sKx7hMCw9j",
"email":"alirezabsh.apps@gmail.com",
"password":"111111",
"rememberMe":false,
"type":"login"
},
"_socketId":"qpav191Nuyx1GVY8ABKi",
"_msgid":"bad7f6719cb0b974"
}
Then the flow :
- Checks if incoming email exists in DB (Users collection)
- Sends Invalid Credentials error if email not found :
{
"action": "login",
"status": "Invalid Credentials",
"message": "Invalid email or password"
}
- Compares passwords
- Sends Invalid Credentials error if password do not match :
{
"action": "login",
"status": "Invalid Credentials",
"message": "Invalid email or password"
}
- If email and password are valid , a random 16-character token is generated with its 7-day expiry and the clientId is updated :
// generate a radnom 16-char access token
const token = Date.now().toString(36) + Math.random().toString(36).substring(2, 10);
// set 7-day expiry for token
const expiresAt = new Date(Date.now() + 604800000);
msg.auth.userToken = token;
msg.auth.expiresAt = expiresAt;
msg.collection = "Users"
msg.operation = "updateOne"
msg.payload = [
{ email: msg.auth.email },
{
$set: {
token: token,
expiresAt: expiresAt,
clientId: msg.auth.clientId
}
},
{ upsert: false }
];
return msg;
- A success login response is sent to browser along with token, and token is set inside user’s cookie ( front-end code handles setting cookie) :
{
"payload":{
"action":"login",
"status":"success",
"message":"logged in successfully"
},
"auth" : {
"userToken": "16-char-token",
"expiresAt" : "expiryDate"
}
Note : Each time user logs in, a new access token is generated so the old will be replaced by the new one.
d. Forget Password Workflow
Forget Password handles resetting user’s password by providing a 6-digit code which is sent user’s email after verifying their email. After checking the code, users are able to reset their password. This workflow has 4 flow to handle this functionality which are located inside TC-ForgetPassword tab.
Below here are the request types which are routed to this workflow :
- resetPassword
- resendResetPassword
- sendResetCode
- setNewPassword
- cancelResetPassword
Here the logic behind each flow is explained by order which makes it easy to understand the step by step implementation of resetting user’s password:
d.1. First flow
The first flow verifies user’s email , delete old 6-digit verification codes in ResetPasswordCodes collection if there is any , prepare email body and code , send email and save code with its 2 minutes expiry along users email. This flow receive the request below :
{
"auth":{
"type":"resetPassword",
"email":"alirezabsh.app@gmail.com"
},
"_socketId":"fLbns1iB2ABwrfu_ABIs",
"_msgid":"5756a599021d711e"
}
Then :
- Checks if incoming email exists in DB.
- Send response if email exists :
{
"payload":{
"action":"resetPassword",
"status":"validEmail",
"message":"valid email"
}
}
- Send response if email doesn’t exist :
{
"payload":{
"action":"resetPassword",
"status":"invalidEmail",
"message":"Invalid email"
}
}
- Delete all old 6-digit generated codes from ResetPasswordCodes collection
- prepare email topic , body and generate a 6 digit code
- Send email using email node
- save 6-digit code with its 2 minute expiry and user’s email inside ResetPasswordCodes collection :
msg.collection = "ResetPasswordCodes";
msg.operation = "insertOne";
const codeExpiry = new Date(Date.now() + 2 * 60 * 1000); // 2 minutes from now (UTC)
msg.payload = {
email: msg.auth.email,
code: msg.resetCode,
expiry: codeExpiry,
}
return msg;
Note : resend option is routed directly to the beginning of step 4; because user’s email has already been checked.
d.2. Second flow
The second flow verifies input code, check expiry send response and deletes the code from DB. This flow receives the request below :
{
"auth":{
"type":"sendResetCode",
"email":"alirezabsh.apps@gmail.com",
"code":"111111"
},
"_socketId":"fLbns1iB2ABwrfu_ABIs",
"topic":"uibuilder",
"_msgid":"bcc225611f1f48c8"
}
Then the flow :
- Checks if user’s email exists in ResetPasswordCodes collection; because each code is saved with user’s email
- If exists, it means the code is not expired. If doesn’t exist. it means the code is expired. ( we use TTL index in this collection on expiry field which automatically deletes documents after 2 minute based expiry field
-
Send expired code response if email could not found :
{ "action": "resetPassword", "status": "expired", "message": "The reset Code Is Expired. Please request a new password reset to continue." } -
Send invalid reset code response if codes do not match
{ "action": "resetPassword", "status": "invalidCode", "message": "The Reset Password Code Is Invalid." } -
Send valid reset code response if codes match
{ "status": "validCode", "action": "resetPassword" } Finally , delete the code from DB ( not needed after validation)
d.3. Third flow
This flow simply receives users new password, hashes it and save it into user’s document in Users collection. It receives the request below :
{
"auth":{
"type":"setNewPassword",
"email":"alirezabsh.apps@gmail.com",
"password":"212121"
},
"_socketId":"fLbns1iB2ABwrfu_ABIs",
"topic":"uibuilder",
"_msgid":"38d780d25ed51112"
}
Then :
- Hash user’s password with bcrypt node
- Save new password into his document
- Sends success response :
{
"status": "success",
"action": "setNewPassword"
}
d.4. Fourth flow
This flow handles reset password cancelation. When user clicks on ❌ icon on pop-up, he requests to cancel password reset . The flow receives the request below :
{
"auth":{
"email":"alirezabsh.apps@gmail.com",
"type":"cancelResetPassword"
},
"_socketId":"fLbns1iB2ABwrfu_ABIs",
"topic":"uibuilder",
"_msgid":"aefc019258a8b760"
}
Then :
- Use user’s email to delete user’s 6-digit code from DB
e. Dashboard Data
e.1. How Users Get Their dashboard data ?
TrustedCloud system is designed to handle 4 roles, each capable of specific actions :
-
Applicant :
- The default role assigned to all users
- Can only see his own requests
- Has only one dashboard panel for applicant role
- Can create onboarding request and fill out the form, import a JSON to fill out the form or export the filled out fields
- Can create draft requests, complete and submit later
- Can edit their request if it has Pending or Rejected status
- Can see the decision of multiple auditors and approvers
- A decision includes : date, comment and status
-
Applicant & Auditor :
- As an applicant, has all the applicant’s capabilities
- Has 2 dashboard panels, one for applicant role and one for auditor role.
- As an auditor :
- Can see all the requests in system with different status
- Can’t see drafts made by applicant!
- Can audit ( approve or reject ) Applicant’s request with status Pending, Provide date, comment and status
- Can audit his own request
- Can reaudit the request if approver hasn’t finalized yet
- Can reaudit the request which is audited by other auditors
-
Applicant & Approver
- As an applicant, has all the applicant’s capabilities
- Has 2 dashboard panels, one for applicant role and one for auditor role.
- Can see only the audited requests (approved by an auditor) , approved and rejected requests (by an approve
- Can see his own approved requests
- Can approve or reject the requests he sees; Also provide date, comment and status
- Can reapprove or reject all the requests he sees again
-
Admin
- Monitors requests and users in system
- Can see all requests and users
- Can change user role
Based on these four roles, the system generate 4 dashboard data when the user enters the dashboard.
Users get their dashboard data when the request below is sent :
{
"type": "getDashboard",
"auth": {
"userToken": "miu845yw02qo42ur",
"clientId": "iom60cMlTJrYya6kEa2zD"
},
"_socketId": "YW57AYq2V1EScTkPAFzN",
"_msgid": "1ac478c7d0baf42f"
}
This request is sent each time the user switches between panels, refreshes the page or redirect into dashboard from login page.
As we said, In dashboard auth flow the msg.auth.userToken is always validated. when the token from browser is being checked, we save user’s document fields inside msg.userData to have access to the user who is requesting to get his dashboard data :
const now = Date.now();
const expiryMs = new Date(msg.payload.expiresAt).getTime();
msg.userData = msg.payload; // Here we save the result of findOne query => used msg.auth.userToken for the query
if (now <= expiryMs) {
msg.payload.isNotExpired = true;
} else {
msg.payload.isNotExpired = false;
}
return msg;
Therefore we have roles field inside msg.userData which helps to route the request to one of the four get dashboard data flows.
When the requests arrives at check request type switch node, The msg.type matches “getDashboard” output which then routes the request (msg) to a function that extracts user role or roles :
// Get array of roles from userData
const roles = msg.userData.roles || [];
// Flags for each role
const isApplicant = roles.includes("Applicant");
const isAuditor = roles.includes("Auditor");
const isApprover = roles.includes("Approver");
const isAdmin = roles.includes("Admin");
// Decide outcome
if (isApplicant && !isAuditor && !isApprover) {
msg.payload = "isApplicant";
}
else if (isApplicant && isAuditor && !isApprover) {
msg.payload = "isApplicantAndAuditor";
}
else if (isApplicant && isApprover && !isAuditor) {
msg.payload = "isApplicantAndApprover";
} else if (!isApplicant && !isApprover && !isAuditor && isAdmin) {
msg.payload = "isAdmin"
}
return msg;
After this node , there is switch which routes the request to one of the 4 flows inside TC-GetDashboard tab; each responsible for handing dashboard data of one of the roles.
Note : for dashboard data flows , the msg.userData is used all the time becuase it has access to user’s email , profile and etc.
e.2. Applicant Dashboard Data
In previous section , We the role of request sender is determined . Now in this section we explain the flow for getting applicant dashboard data ( role ). It is located inside TC-GetDashboard tab. Here is the step by step logic to handle this request :
- A user can have multiple draft requests which are not submitted yet. The flow gets all draft requests of the user in Drafts Collection:
msg.collection = "Drafts";
msg.operation = "find";
msg.payload = {
email: msg.userData.email
};
return msg;
- Saves draft requests
-
Gets user’s onboarding requests from request collection :
msg.collection = "Requests" msg.operation = "find" msg.payload = { "createdBy.userId": msg.userData.userId } return msg; Format the requests and save them :
// Helper function to capitalize first letter safely
function capitalize(str) {
if (!str || typeof str !== 'string') return '';
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
msg.payload = msg.payload.map(item => {
return {
id: item.requestId,
timeEstimation: item.timeEstimation,
status: capitalize(item.status), // Capitalized
auditInfo: item.auditInfo,
approveInfo: item.approveInfo,
decisionHistory: item.decisionHistory,
companyDetails: item.companyDetails
};
});
msg.applicantRequests = msg.payload; // all requests for applicant dashboard panel
return msg;
- Prepare applicant dashboard data (response) :
msg.payload = {
type: "getDashboard", // the type must be sent back to the front-end
applicantRequests: msg.applicantRequests,
draftRequests: msg.drafts,
profile : {
userId: msg.userData.userId,
email: msg.userData.email,
roles: msg.userData.roles,
avatarUrl: msg.userData.profile.avatarUrl,
firstName: msg.userData.profile.firstName,
lastName: msg.userData.profile.lastName,
contactNumber: msg.userData.profile.contactNumber,
address: msg.userData.profile.address,
country: msg.userData.profile.country,
stateOrProvince: msg.userData.profile.stateOrProvince,
postalCode: msg.userData.profile.postalCode
}
}
// delete useless properties
delete msg.collection;
delete msg.operation;
delete msg.userData;
delete msg.applicantRequests;
return msg;
e.3. Applicant & Auditor Dashboard Data
Preparing dashboard data for this role is same as the previous one except for auditor role :
- Get user draft request (same as applicant role )
- Save draft requests ( same as applicant role )
- Get user onboarding request ( same as applicant role)
- Format the applicant requests and save them ( same as applicant role )
- Get user auditor dashboard data :
msg.collection = "Requests"
msg.operation = "find"
msg.payload = {
status: { $in: ["Audited", "Approved","Pending", "Rejected"] },
};
return msg;
- Format the auditor dashboard requests and save them :
// Helper function to capitalize first letter safely
function capitalize(str) {
if (!str || typeof str !== 'string') return '';
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
msg.payload = msg.payload.map(item => {
return {
id: item.requestId,
user: {
name: item.createdBy.fullName,
email: item.createdBy.email,
avatar: item.createdBy.avatarUrl
},
registrationDate: item.companyDetails.registrationDate,
priority: "low",
status: capitalize(item.status),
archive: item.archive || false,
auditInfo: item.auditInfo,
approveInfo: item.approveInfo,
decisionHistory: item.decisionHistory,
companyDetails: item.companyDetails
};
});
msg.auditorRequests = msg.payload; // all request for auditor dashboard panel
return msg;
- Send Applicant & Auditor Dashboard Response :
msg.payload = {
type: "getDashboard",
applicantRequests: msg.applicantRequests,
auditorRequests: msg.auditorRequests,
draftRequests: msg.drafts,
profile : {
userId: msg.userData.userId,
email: msg.userData.email,
roles: msg.userData.roles,
avatarUrl: msg.userData.profile.avatarUrl,
firstName: msg.userData.profile.firstName,
lastName: msg.userData.profile.lastName,
contactNumber: msg.userData.profile.contactNumber,
address: msg.userData.profile.address,
country: msg.userData.profile.country,
stateOrProvince: msg.userData.profile.stateOrProvince,
postalCode: msg.userData.profile.postalCode
}
}
delete msg.collection;
delete msg.operation;
delete msg.userData;
delete msg.applicantRequests;
delete msg.auditorRequests;
return msg;
e.4. Applicant & Approver Dashboard Data
Preparing dashboard data for this role is same as the previous one except for approver role :
- Get user draft request (same as applicant role )
- Save draft requests ( same as applicant role )
- Get user onboarding request ( same as applicant role)
- Format the applicant requests and save them ( same as applicant role )
- Get user approver dashboard data :
msg.collection = "Requests";
msg.operation = "find";
let query = {
status: { $in: ["Audited", "Approved", "Rejected"] },
"auditInfo.decision": { $ne: "Rejected" }, // exclue request which auditor rejected
};
msg.payload = query
return msg;
- Format the approver dashboard requests and save them :
// Helper function to capitalize first letter safely
function capitalize(str) {
if (!str || typeof str !== 'string') return '';
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
msg.payload = msg.payload.map(item => {
return {
id: item.requestId,
user: {
name: item.createdBy.fullName,
email: item.createdBy.email,
avatar: item.createdBy.avatarUrl
},
registrationDate: item.companyDetails.registrationDate,
priority: "low",
status: capitalize(item.status), // Capitalized
archive: item.archive || false,
auditInfo: item.auditInfo,
approveInfo: item.approveInfo,
decisionHistory: item.decisionHistory,
companyDetails: item.companyDetails
};
});
msg.approverRequests = msg.payload;
return msg;
- Send Applicant & Approver Dashboard Response :
msg.payload = {
type: "getDashboard",
applicantRequests: msg.applicantRequests,
approverRequests: msg.approverRequests,
draftRequests: msg.drafts,
profile : {
userId: msg.userData.userId,
email: msg.userData.email,
roles: msg.userData.roles,
avatarUrl: msg.userData.profile.avatarUrl,
firstName: msg.userData.profile.firstName,
lastName: msg.userData.profile.lastName,
contactNumber: msg.userData.profile.contactNumber,
address: msg.userData.profile.address,
country: msg.userData.profile.country,
stateOrProvince: msg.userData.profile.stateOrProvince,
postalCode: msg.userData.profile.postalCode
}
}
delete msg.collection;
delete msg.operation;
delete msg.userData;
delete msg.applicantRequests;
delete msg.auditorRequests;
return msg;
e.5. Applicant & Approver Dashboard Data
The admin can see all the requests and user in system. Here is the flow which handles this logic :
- Get all users from Users collection except admin, exclude some fields :
msg.collection = "Users";
msg.operation = "find";
// find query argument
const query = {
roles: { $ne: "Admin" }
};
// find option argument
const options = {
projection: { _id: 0, clientId: 0, password: 0, expiresAt: 0, token: 0 }
};
msg.payload = [query, options];
return msg;
- Save them
- Get all requests from request collection
- Send admin dashboard response :
msg.payload = {
type: "getDashboard",
requests: msg.payload, // all requests in requests collection
usersList: msg.listOfUsers, // all users
profile: { // admin profile
userId: msg.userData.userId,
email: msg.userData.email,
roles: msg.userData.roles,
avatarUrl: msg.userData.profile.avatarUrl,
firstName: msg.userData.profile.firstName,
lastName: msg.userData.profile.lastName,
contactNumber: msg.userData.profile.contactNumber,
address: msg.userData.profile.address,
country: msg.userData.profile.country,
stateOrProvince: msg.userData.profile.stateOrProvince,
postalCode: msg.userData.profile.postalCode
}
}
delete msg.collection;
delete msg.operation;
delete msg.userData;
delete msg.listOfUsers;
return msg;
e.6. Get User’s Dashboard Data Using API
A user can also send an https request to get dashboard data in json. By providing his credentials ( email and password), He gets a access token which he can use to get his dashboard data . The TC-API tab includes two separate flows responsible for user authentication and getting user’s dashboard data. Below here is the step by step approach to get user’s dashboard data :
-
Authentication :
- Endpoint : https://tc.facis.cloud/service/api/login
- Method : POST
- Body : JSON
- Format :
{ "email": "admin@eco.de", "password": "trustedcloud" } -
After the request is sent, the flow :
- Validates Credentials :
msg.collection = "Users" msg.operation = "findOne" msg.payload = { email : msg.req.body.email } return msg;- Invalid Credentials Response ( wrong email )
{ "action": "Login", "status": "Invalid Credentials", "message": "Invalid email or password" }- Compares passwords if email is correct
- Invalid Credentials Response ( wrong password )
{ "action": "Login", "status": "Invalid Credentials", "message": "Invalid email or password" }- After validating credentials the API access token is created with its expiry :
const token = Date.now().toString(36) + Math.random().toString(36).substring(2, 10); const expiresAt = new Date(Date.now() + 604800000); msg.token = token; msg.collection = "Users" msg.operation = "updateOne" msg.payload = [ { email: msg.req.body.email }, { $set: { apiToken: token } }, { upsert: false } ]; return msg;- The response (token) is sent at the end :
msg.payload = { action: "Login", status: "Valid Credentials", token: msg.token } return msg; -
Getting Dashboard Data :
- The user send an http request to https://tc.facis.cloud/service/api/dashboard endpoint
- User provides the token received from login api in header ⇒ token = “received token”
- The flow validates token :
msg.collection = "Users" msg.operation = "findOne" msg.payload = { apiToken: msg.req.headers.token } return msg;- the query result which is user data is saved to extract user’s role :
msg.userData = msg.payload; return msg;- Then a linked node is used at the end to route msg (request) to TC-Main , find roles function which then routes request to one of the 4 data dashboard flows inside TC-GetDashboard
f. Dashboard Requests
The user interaction with his dashboard panel and profile tab involves multiple operations which include handling drafts, creating and reviewing onboarding requests and updating user profile .
Below is the list of the requests coming with their type from dashboard :
- Create a draft : msg.type = “createDraft”
- Update a draft : msg.type = “updateDraft”
- Get drafts : msg.type = “getDrafts”
- Delete a draft : msg.type = “deleteDraft”
- Create a request : msg.type = “createRequest”
- Archive a request : msg.type = “archiveRequest”
- Edit a request : msg.type = “editCompanyDetails”
- Audit a request : msg.type = “auditRequest”
- Approve a request : msg.type = “approveRequest”
- Set user role : msg.type = “setRole”
- Update user avatar : msg.type = “updateAvatar”
- Update user profile : msg.tyoe = “editProfile”
- Logout : msg.type = “logOut”
f.1. How Dashboard Requests Are Handled ?
Each request, after validating access token , is routed to the TC-RequestHandling tab which contains the flows that handles requests above. The each output of check request type switch is wired to a link-out node that is connected a link-in node inside TC-RequestHandling tab . There for the incoming request is routed the correct flow .
f.2. Create Draft
Each request has the initials state of draft until submitted. When user clicks on create onboarding request button, a draft of the request with request id and user’s email is inserted inside Drafts collection. It helps to save all the changes and allow user to fill out the form later by keeping filled data.
Below is flow which handles this request :
- Request :
{
"type":"createDraft",
"auth":{
"userToken":"mh9068y3sn0ehnao",
"clientId":"nfTazIf_wXWRudFTF0_m2"
},
"companyDetails":{all the fields in wizard},
"_socketId":"b1bbN-z4FWFcploIAH3z",
"topic":"uibuilder",
"_msgid":"3977bf9dcc6bb534"
}
- Creates request draft :
const randomId = Math.floor(100000 + Math.random() * 900000); // generate a random request id
msg.collection = "Drafts";
msg.operation = "insertOne";
msg.payload = {
requestId: randomId,
email: msg.userData.email,
companyDetails: msg.companyDetails // the wizard fields
}
msg.response = msg.payload;
return msg;
- Sends request id (draft id) as response to the dashboard uibuilder :
// send requestId as response
msg.payload = {
id : msg.response.requestId,
};
return msg;
f.3. Update Draft
To save real-time changes in wizard , every 3 second a request with new company details is sent with id . we use the id to update the draft inside Drafts collection :
- Request :
{
"type":"updateDraft",
"auth":{
"userToken":"mh9068y3sn0ehnao",
"clientId":"nfTazIf_wXWRudFTF0_m2"
},
"companyDetails":{ all the fields in wizard
},
"requestID": 12233
"_socketId":"b1bbN-z4FWFcploIAH3z",
"topic":"uibuilder",
"_msgid":"3977bf9dcc6bb534"
}
- Update company details
msg.collection = "Drafts";
msg.operation = "updateOne";
msg.payload = [
{requestId: msg.requestID},
{$set: {companyDetails: msg.companyDetails}}
]
return msg;
- No Response
f.4. Get Drafts
When applicant clicks on ❌ icon to close the wizard , a request is sent to get all user drafts :
- Request :
{
"type":"getDrafts",
"auth":{
"userToken":"mh9068y3sn0ehnao",
"clientId":"nfTazIf_wXWRudFTF0_m2"
},
"_socketId":"b1bbN-z4FWFcploIAH3z",
"topic":"uibuilder",
"_msgid":"3977bf9dcc6bb534"
}
- Get applicant’s drafts :
msg.collection = "Drafts";
msg.operation = "find";
msg.payload = {
email: msg.userData.email
};
return msg;
- Send applicant drafts as response :
{
"type": "getDrafts",
"payload": [user's draft requests]
}
f.5. Delete Draft
Applicant can delete his drafts :
- Request :
{
"type":"deleteDraft",
"auth":{
"userToken":"miu845yw02qo42ur",
"clientId":"iom60cMlTJrYya6kEa2zD"
},
"draftId":628078,
"_socketId":"nLTy7CV2-7-JWCblAF3e",
"topic":"uibuilder",
"_msgid":"e37e96a56b7c2323"
}
- Delete draft :
msg.collection = "Drafts";
msg.operation = "deleteOne";
msg.payload = {
requestId: msg.draftId // delete draft using id
};
return msg;
- Send response :
// send deleted draft request id as response
msg.payload = {
draftId: msg.draftId
};
return msg;
f.6. Create Request
Users can fill out the forms in the wizard and submit the request . Here is the step by step logic behind creating a new request .
- When user clicks on submit button ⇒ Request :
{
"type":"createRequest",
"auth":{
"userToken":"mibf46czatedxkia",
"clientId":"vC-whXYcY5anJIHrt_WOa"
},
"companyDetails": {},
"draftID":183808,
"_socketId":"ixLzeRDQJhz947JkAEcM",
"topic":"uibuilder",
"_msgid":"402e83deb25604e2"
}
- Because the request has a draft instance initially, the flow deletes it first :
msg.collection = "Drafts";
msg.operation = "deleteOne";
msg.payload = {
requestId: msg.draftID
}
return msg;
- Then get all user’s draft requests and save it:
msg.collection = "Drafts";
msg.operation = "find";
msg.payload = {
email: msg.userData.email
};
return msg;
- then get last user’s userId :
// Find query
const query = {}; // empty = all docs
// Find options (sort userId descending)
const options = {
sort: { requestId: -1 },
limit: 1
};
msg.collection = "Requests";
msg.operation = "find";
msg.payload = [query, options];
return msg;
- Then create the request (insert into DB):
// add 1 to last user userId
const lastRequest = (Array.isArray(msg.payload) && msg.payload.length > 0) ? msg.payload[0] : null;
let nextId;
if (!lastRequest) {
nextId = 1;
} else {
nextId = (lastRequest.requestId || 0) + 1;
}
// insert request into DB
msg.collection = "Requests"
msg.operation = "insertOne"
msg.payload = {
requestId: nextId,
createdBy: {
userId: msg.userData.userId,
fullName: msg.userData.fullName,
email: msg.userData.email,
avatarUrl: msg.userData.profile.avatarUrl
},
companyDetails: msg.companyDetails,
priority: "low",
status: "Pending",
archive: false,
timeEstimation: "10 business day",
auditInfo: [],
approveInfo: [],
createdAt: new Date(),
decisionHistory: []
}
msg.request = msg.payload;
return msg;
- Send Response (payload and response fields are both needed) :
// send request document fields, companyDetails and user draft requests
msg.payload = {
archive: msg.request.archive,
auditDate: msg.request.auditInfo.date,
auditorComment: msg.request.auditInfo.comment,
companyDetails: msg.request.companyDetails,
id: msg.request.requestId,
status: msg.request.status,
timeEstimation: msg.request.timeEstimation,
title: msg.request.companyDetails.companyName,
draftRequests: msg.draftRequests,
createdAt: msg.request.createdAt
}
msg.response = {
status: "success",
action: "createRequest",
message: "Request Created Successfully"
}
// remove useless fields
delete msg.auth;
delete msg.companyDetails;
delete msg.collection;
delete msg.operation;
delete msg.userData;
delete msg.request;
return msg;
f.7. Edit Request
Applicants can edit their request when the status is Pending or Rejected :
- Request :
{
"type":"editCompanyDetails",
"auth":{
"userToken":"mhunlv5sr22g99ed",
"clientId":"z8QDDMrSqr84FjT4wYXF8"
},
"requestID":32,
"companyDetails":{ all the fields in wizard
},
"_socketId":"O7ELWx_woqymqJ_-ABTC",
"topic":"uibuilder",
"_msgid":"2666a59d3f19718c"
}
- The flow checks msg.status
- If Pending, only updates new company details:
msg.collection = "Requests";
msg.operation = "updateOne";
msg.payload = [
{ requestId: msg.requestID },
{ $set: { companyDetails: msg.companyDetails} }
];
return msg;
- If Rejected, updates company details along status :
msg.collection = "Requests";
msg.operation = "updateOne";
msg.payload = [
{ requestId: msg.requestID },
{ $set: { companyDetails: msg.companyDetails, status: "Pending"} }
];
return msg;
- Response :
msg.response = {
status: "success",
action: "editCompanyDetails",
message: "Company details edited successfully"
};
delete msg.auth;
delete msg.collection;
delete msg.operation;
delete msg.userData;
return msg;
f.8. Archive Request
Applicant , auditor and approver can archive a request which puts request into archive tab :
- Request :
{
"type":"archiveRequest",
"auth":{
"userToken":"mhulornneiazg6qq",
"clientId":"z8QDDMrSqr84FjT4wYXF8"
},
"requestID":28,
"_socketId":"sNOGZS6_IaOeQNITABOR",
"topic":"uibuilder",
"_msgid":"29ac7ba9a5639b8d"
}
- Find the request :
msg.collection = "Requests"
msg.operation = "findOne"
msg.payload = {
requestId: msg.requestID
}
return msg;
- Archive it :
let currentArchive = msg.payload.archive === true; // make sure it's boolean
let newArchive = !currentArchive; // toggle
msg.collection = "Requests";
msg.operation = "updateOne";
msg.payload = [
{ requestId: msg.requestID },
{ $set: { archive: newArchive } }
];
return msg;
- Response :
msg.payload = {
status: "success",
action: "archiveRequest",
message: "Request Archived Successfully"
}
delete msg.auth;
delete msg.collection;
delete msg.operation;
delete msg.userData;
delete msg.response;
return msg;
f.9. Audit Request
The auditor audits the request (approves or rejects) by providing his decision :
- Request :
{
"type":"auditRequest",
"auth":{
"userToken":"miu845yw02qo42ur",
"clientId":"iom60cMlTJrYya6kEa2zD"
},
"userId":3,
"requestId":4,
"auditDecision":{
"status":"Audited",
"date":"2025-12-09",
"comment":"dfg",
"profile":{
"userId":3,
"email":"alireza@gmail.com",
"roles":[
"Applicant",
"Auditor"
],
"avatarUrl":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxITEhUSExMVFhUXFxcVGBcVGBUVFxgXGBcXGBcXFRUYHSggGBolGxUVITEhJSkrLi4uFx8zODMsNygtLisBCgoKDg0OGxAQGy0lHyUtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLf/AABEIAPsAyQMBEQACEQEDEQH/xAAbAAADAQEBAQEAAAAAAAAAAAADBAUCBgEAB//EAEEQAAEDAgMFBgQDBwMCBwAAAAEAAgMRIQQSMQVBUWFxBhMigZGhMrHB8EJS0RQjYnKC4fEkM6IVQzRTY5Kz0uL/xAAbAQACAwEBAQAAAAAAAAAAAAACBAEDBQAGB//EADARAAICAQQBAwMDAgcBAAAAAAABAgMRBBIhMQUTIkEyUWEGFHFCgSMkM5GhsdHB/9oADAMBAAIRAxEAPwCfEF2D0+RuNcTkMCuYXB7mQNgpngcqmwuAlUOQ0aaociG0NRhAVobiC5dksMArYlbYWMq4CXQwwKRaTN5VBU2byoCqTBzIX0LyEZEDKmfRlVsAZjKFHBw5FkLJ6ChDQWNiJIIYYxWrglm8pXHYMvUgsUmkXECveqNwPB+ewyJo9G2ORvXA7wweobJ3mHvQM5Pk+Y5UyYaYeNBkLIwxQwWNMKkLAy0rkcEqrUC0grCjTAkhqJ29EKzQXVRkXZvchKpAZ0LKJCD9UDK2ZY5VMrCskUYODNcpJQxEiSLEORNRIIZY1GEe0XHC8x1XEMmYp6hgiPeKsg4CN6cybzHoCpyQxpoUEYMvChlkVg8jKXsRakNxFVhYGmhcgMBQiRYFa6ilEGzIjR2UEZIiQEhqJ6sXItKLYCbbcEbsrpADw1RODXZdDxt01uSKLcUwgHMOV1W4sSnpbc42mJcQziu2Mp/ZWv4FZFXJYFLKpQ+pAXKp9FLR4...",
"firstName":"alireza",
"lastName":"behnam shivaa",
"contactNumber":"",
"address":"Tehran",
"country":"",
"stateOrProvince":"",
"postalCode":""
},
"role":"Auditor"
},
"_socketId":"l8pE-iDIAHbVcyr3AF3w",
"topic":"uibuilder",
"_msgid":"f8998cd108dafb2b"
}
- Audit (approve or reject) the request :
msg.collection = "Requests"
msg.operation = "updateOne"
msg.payload = [
{ requestId: msg.requestId },
{
$set: {
status: msg.auditDecision.status
},
$push: {
auditInfo: msg.auditDecision,
decisionHistory: msg.auditDecision
}
}
]
return msg;
- Response :
msg.response = {
status: "success",
action: "auditRequest",
message: "Request Audited Successfully"
}
delete msg.auth;
delete msg.collection;
delete msg.operation;
delete msg.userData;
return msg;
f.10. Approve Request
Approve approves or rejects the audited requests that are approved by the auditor :
- Request :
{
"type":"approveRequest",
"auth":{
"userToken":"miu845yw02qo42ur",
"clientId":"iom60cMlTJrYya6kEa2zD"
},
"userId":3,
"requestId":4,
"approverDecision":{
"status":"Approved",
"date":"2025-12-15",
"comment":"good",
"profile":{
"userId":3,
"email":"alireza@gmail.com",
"roles":[
"Applicant",
"Approver"
],
"avatarUrl":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxITEhUSExMVFhUXFxcVGBcVGBUVFxgXGBcXGBcXFRUYHSggGBolGxUVITEhJSkrLi4uFx8zODMsNygtLisBCgoKDg0OGxAQGy0lHyUtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLf/AABEIAPsAyQMBEQACEQEDEQH/xAAbAAADAQEBAQEAAAAAAAAAAAADBAUCBgEAB//EAEEQAAEDAgMFBgQDBwMCBwAAAAEAAgMRIQQSMQVBUWFxBhMigZGhMrHB8EJS0RQjYnKC4fEkM6IVQzRTY5Kz0uL/xAAbAQACAwEBAQAAAAAAAAAAAAACBAEDBQAGB//EADARAAICAQQBAwMDAgcBAAAAAAABAgMRBBIhMQUTIkEyUWEGFHFCgSMkM5GhsdHB/9oADAMBAAIRAxEAPwCfEF2D0+RuNcTkMCuYXB7mQNgpngcqmwuAlUOQ0aaociG0NRhAVobiC5dksMArYlbYWMq4CXQwwKRaTN5VBU2byoCqTBzIX0LyEZEDKmfRlVsAZjKFHBw5FkLJ6ChDQWNiJIIYYxWrglm8pXHYMvUgsUmkXECveqNwPB+ewyJo9G2ORvXA7wweobJ3mHvQM5Pk+Y5UyYaYeNBkLIwxQwWNMKkLAy0rkcEqrUC0grCjTAkhqJ29EKzQXVRkXZvchKpAZ0LKJCD9UDK2ZY5VMrCskUYODNcpJQxEiSLEORNRIIZY1GEe0XHC8x1XEMmYp6hgiPeKsg4CN6cybzHoCpyQxpoUEYMvChlkVg8jKXsRakNxFVhYGmhcgMBQiRYFa6ilEGzIjR2UEZIiQEhqJ6sXItKLYCbbcEbsrpADw1RODXZdDxt01uSKLcUwgHMOV1W4sSnpbc42mJcQziu2Mp/ZWv4FZFXJYFLKpQ+pAXKp9FLR4...",
"firstName":"alireza",
"lastName":"behnam shivaa",
"contactNumber":"",
"address":"Tehran",
"country":"",
"stateOrProvince":"",
"postalCode":""
},
"role":"Approver"
},
"_socketId":"0-8VfLQiiw3VKklbAF36",
"topic":"uibuilder",
"_msgid":"c26956511cae17f2"
}
- Approve or reject the request :
msg.collection = "Requests"
msg.operation = "updateOne"
msg.payload = [
{ requestId: msg.requestId },
{
$set: {
status: msg.approverDecision.status
},
$push: {
approveInfo: msg.approverDecision,
decisionHistory: msg.approverDecision
}
}
]
return msg;
- Response :
msg.response = {
status: "success",
action: "approveRequest",
message: "Request Approved Successfully"
}
delete msg.auth;
delete msg.collection;
delete msg.operation;
delete msg.userData;
return msg;
f.11. Set Role
Admin can set or change user roles in his dashboard :
- Request :
{
"type":"setRole",
"auth":{
"userToken":"mibemj786j5uo2us",
"clientId":"vC-whXYcY5anJIHrt_WOa"
},
"editAdmin":{
"userId":28,
"isAuditor":true
},
"_socketId":"Mu7fSfuQQIsWovEjAEaz",
"topic":"uibuilder",
"_msgid":"5025c1435b706710"
}
- Update user’s role :
const edit = msg.editAdmin;
const { userId } = edit;
const flagToRole = { isApplicant: "Applicant", isApprover: "Approver", isAuditor: "Auditor" };
let op = {};
let set = { updatedAt: new Date() };
for (const flag in flagToRole) {
if (flag in edit) {
const role = flagToRole[flag];
const val = edit[flag];
const isTrue = val === true || String(val).toLowerCase() === "true";
const isFalse = val === false || String(val).toLowerCase() === "false";
if (isTrue) {
if (!op.$addToSet) op.$addToSet = { roles: { $each: [] } };
op.$addToSet.roles.$each.push(role);
} else if (isFalse) {
if (!op.$pull) op.$pull = { roles: { $in: [] } };
op.$pull.roles.$in.push(role);
}
}
}
op.$set = set;
msg.collection = "Users";
msg.operation = "updateOne";
msg.payload = [
{ userId: userId },
op,
{ upsert: false }
];
return msg;
- Response :
// Send User Roles and UserId As Response
msg.collection = "Users";
msg.operation = "findOne";
msg.payload = [
{userId: msg.editAdmin.userId},
{projection: { roles: 1, userId: 1, _id: 0}}
];
return msg;
f.12. Edit Profile
Inside dashboard , user can update his profile :
- Request :
{
"type":"editProfile",
"auth":{
"userToken":"miu845yw02qo42ur",
"clientId":"iom60cMlTJrYya6kEa2zD"
},
"profile":{
"userId":3,
"email":"alireza@gmail.com",
"roles":[
"Applicant",
"Approver"
],
"avatarUrl":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxITEhUSExMVFhUXFxcVGBcVGBUVFxgXGBcXGBcXFRUYHSggGBolGxUVITEhJSkrLi4uFx8zODMsNygtLisBCgoKDg0OGxAQGy0lHyUtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLf/AABEIAPsAyQMBEQACEQEDEQH/xAAbAAADAQEBAQEAAAAAAAAAAAADBAUCBgEAB//EAEEQAAEDAgMFBgQDBwMCBwAAAAEAAgMRIQQSMQVBUWFxBhMigZGhMrHB8EJS0RQjYnKC4fEkM6IVQzRTY5Kz0uL/xAAbAQACAwEBAQAAAAAAAAAAAAACBAEDBQAGB//EADARAAICAQQBAwMDAgcBAAAAAAABAgMRBBIhMQUTIkEyUWEGFHFCgSMkM5GhsdHB/9oADAMBAAIRAxEAPwCfEF2D0+RuNcTkMCuYXB7mQNgpngcqmwuAlUOQ0aaociG0NRhAVobiC5dksMArYlbYWMq4CXQwwKRaTN5VBU2byoCqTBzIX0LyEZEDKmfRlVsAZjKFHBw5FkLJ6ChDQWNiJIIYYxWrglm8pXHYMvUgsUmkXECveqNwPB+ewyJo9G2ORvXA7wweobJ3mHvQM5Pk+Y5UyYaYeNBkLIwxQwWNMKkLAy0rkcEqrUC0grCjTAkhqJ29EKzQXVRkXZvchKpAZ0LKJCD9UDK2ZY5VMrCskUYODNcpJQxEiSLEORNRIIZY1GEe0XHC8x1XEMmYp6hgiPeKsg4CN6cybzHoCpyQxpoUEYMvChlkVg8jKXsRakNxFVhYGmhcgMBQiRYFa6ilEGzIjR2UEZIiQEhqJ6sXItKLYCbbcEbsrpADw1RODXZdDxt01uSKLcUwgHMOV1W4sSnpbc42mJcQziu2Mp/ZWv4FZFXJYFLKpQ+pAXKp9FLR4...",
"firstName":"alireza",
"lastName":"behnam shivaa",
"contactNumber":"093612121212",
"address":"Tehran",
"country":"",
"stateOrProvince":"",
"postalCode":""
},
"userId":3,
"_socketId":"0-8VfLQiiw3VKklbAF36",
"topic":"uibuilder",
"_msgid":"e68ebc025e72bac2"
}
- Update user’s profile :
// Extract full profile object from incoming payload
let incomingProfile = { ...msg.profile };
// Extract email from the profile
let incomingEmail = incomingProfile.email;
// Remove email from the profile object
delete incomingProfile.email;
delete incomingProfile.userId,
delete incomingProfile.roles,
msg.collection = "Users";
msg.operation = "updateOne";
msg.payload = [
{
userId: msg.userId
},
{
$set: {
fullName: incomingProfile.firstName + " " + incomingProfile.lastName,
profile: incomingProfile,
email: incomingEmail
}
},
];
return msg;
- Response :
msg.response = {
status: "success",
action: "editProfile",
message: "Profile Edited Successfully"
}
return msg;
f.13. Update Avatar
User can choose an avatar for his profile .
- Request :
{
"type":"updateAvatar",
"auth":{
"userToken":"mhupsks27wni63rj",
"clientId":"z8QDDMrSqr84FjT4wYXF8"
},
"userId":3,
"avatarBase64":"base64",
"_socketId":"ktiYPapIW7VSqRg_ABUl",
"topic":"uibuilder",
"_msgid":"ef18831dc149fa03"
}
- Update user’s avatar :
msg.collection = "Users";
msg.operation = "updateOne";
msg.payload = [
{ userId: msg.userId },
{ $set: { "profile.avatarUrl": msg.avatarBase64 } }
];
return msg;
- Update requests avatar ⇒ each request hold the creator avatar :
msg.collection = "Requests";
msg.operation = "updateMany";
msg.payload = [
{ "createdBy.userId": msg.userId },
{ $set: { "createdBy.avatarUrl": msg.avatarBase64 } },
{ upsert: false }
];
return msg;
- Response :
{
"status": "success",
"action": "uploadAvatar",
"message": "Avatar Uploaded Successfully"
}
f.14. Logout
Users can click on log out button and move to login page :
- Request :
{
"type":"logOut",
"auth":{
"userToken":"mhupsks27wni63rj",
"clientId":"z8QDDMrSqr84FjT4wYXF8"
},
"_socketId":"ktiYPapIW7VSqRg_ABUl",
"topic":"uibuilder",
"_msgid":"94699ce606f7b1e8"
}
- logout user : the user’s token is removed when he logs out
msg.collection = "Users"
msg.operation = "updateOne"
msg.payload = [
{ token: msg.auth.userToken },
{ $unset: { token: "", expiresAt: "" } },
{ upsert: false }
];
return msg;
Database
Mongodb is the primary DB to store and fetch data . node-red-contrib-mongodb4 node is used to connect the flows to remote mongodb cluster .
Collections
For trusted cloud database we use 5 collections :
-
Users
Stores user accounts, authentication tokens, roles, and profile information. This collection holds all core identity and authorization data for the platform.
-
Requests
Contains all onboarding requests created by Applicants. It also stores review information from Auditors and Approvers, including decisions, comments, and status updates throughout the request lifecycle.
-
Drafts
Maintains partially completed or unsubmitted onboarding requests. This allows Applicants to save progress and continue their request at a later time.
-
ResetPasswordCodes
Temporarily stores verification codes sent to users who initiate the password reset process. These codes are short‑lived and cleared after use or expiration.
-
EmailVerificationCodes
Temporarily stores email verification codes used during the account creation (sign‑up) process. These entries ensure that only verified email addresses can be used to create an account.
Documents :
There is not any strict schema for our 3 entities :
- User
- Request
- Draft
But the list below contains a document sample for each entity :
- User Document :
```
{
"clientId": "Dk8lxo_zsPDSWQXPaHIPb",
"fullName": "Alireza",
"email": "alirezabsh.apps@gmail.com",
"password": "$2a$10$cT597OKUwbSPwRUgxxiUZOm9B4NUbj5iOcwMd83dBrh1HdtN4bGgm",
"roles": [
"Admin"
],
"userId": 28,
"profile": {
"avatarUrl": "./images/user-avatar.png",
"firstName": "Alireza",
"lastName": "",
"contactNumber": "",
"address": "",
"country": "",
"stateOrProvince": "",
"postalCode": ""
},
"createdAt": {
"$date": "2025-11-27T09:14:02.872Z"
},
"apiToken": "min4qva8th8v57se",
"expiresAt": {
"$date": "2025-12-14T06:07:36.465Z"
},
"token": "mivbm87ltlqrgokc"
}
```
- Request Document :
```
{
"_id": objectId('692ee990bd184710aa45faa5'),
"requestId": 4,
"createdBy": obj,
"companyDetails": obj,
"priority": "low",
"status": "Approved",
"archive": true,
"timeEstimation": "10 business day",
"auditInfo": Array,
"approveInfo": Array,
"createdAt": Date(),
"decisionHistory" : Array
}
```
- Draft Document :
```
{
"_id": objectId('692ee990bd184710aa45faa5'),
"requestId": 254634,
"email" : "applicant@gmail.com",
companyDetails : obj
}
```
### TTL Index :
We use A TTL Index on expiry field inside EmailVerificationCodes and ResetPasswordCodes collections which deletes the document automatically after 2 minutes.
### Index :
A index is used for the fields which are most used by queries
- Users Collection ⇒ index on ⇒ token - userId - email fields
- Requests Collection ⇒ index on ⇒ createdBy.userId - requestId fields
- Drafts Collection ⇒ index on ⇒ email , requestId fields
- EmailVerificationCodes & ResetPasswordCodes collections ⇒ index on ⇒ email field
Top comments (0)