DEV Community

Alireza Behnam Shiva
Alireza Behnam Shiva

Posted on • Edited on

TrustedCloud Documentation

Table Of Contents

  1. Introduction
  2. System Overview
    1. Technology Stack
    2. Database
    3. Authentication
    4. Authorization
    5. 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
  3. Workflows
    1. Main
    2. Sign-up
    3. Login
    4. Forget Password
    5. 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
    6. 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
  4. Database
    1. Collections
    2. Documents
    3. TTL Index
    4. 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:

  1. Applicants can create requests, save drafts, and view only their own data.
  2. Auditors can see every request in the system and perform audits.
  3. Approvers can view audited requests and make final decisions.
  4. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

  1. Users

    Stores user accounts, authentication tokens, roles, and profile information. This collection holds all core identity and authorization data for the platform.

  2. 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.

  3. Drafts

    Maintains partially completed or unsubmitted onboarding requests. This allows Applicants to save progress and continue their request at a later time.

  4. 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.

  5. 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:

  1. Token Validity

    Ensures the provided token exists and belongs to a registered user.

  2. 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:

  1. Build query in a Function node
  2. Send to MongoDB node
  3. Process result
  4. 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:

  1. login
  2. 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"
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • switch to payload and check if it it null or not
  • if null :
{
    "state": "notLoggedIn"
}
Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode
  • if token expired :
{
    "state": "notLoggedIn"
}
Enter fullscreen mode Exit fullscreen mode
  • if token isn’t expired the request will be routed to check request type switch

So when the request enters this workflow:

  1. The flow checks if msg.auth.userToken has a value , exists in Users collection, not expired.
  2. Saves user data.
  3. ** routes request to the check request type switch node if token has no value.
  4. returns a no token response if token is empty, invalid or expired
  5. returns a redirect response to uibuilder node if token is valid.
  • No Token Response
{
    "state": "notLoggedIn"
} 
Enter fullscreen mode Exit fullscreen mode
  • Valid Token Response
{
  "payload" = {
    "action": "login",
    "status": "redirect",
    "message": "User Already Logged In"
  } 
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. The flow checks if msg.auth.userToken has a value , exists in Users collection, not expired.
  2. Saves user data.
  3. ** routes request to the check request type switch node if token is valid **
  4. 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"
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Check if user inputs are not empty
  • Send failed response if empty :
{
    "action": "register",
    "status": "failed",
    "message": "Invalid Inputs"
}
Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode
  • send a success response to notify front-end email has been sent :
{
  "payload":{
    "action":"signup",
    "status":"success",
    "state":"validInputs"
  },
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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."
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Send response if code is invalid
{
  "payload":{
    "action":"verifyEmail",
    "status":"invalidCode",
    "message":"The verification Code Is Invalid."
  },
}
Enter fullscreen mode Exit fullscreen mode
  • 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"
  },
}
Enter fullscreen mode Exit fullscreen mode
  • 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"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
  • Compares passwords
  • Sends Invalid Credentials error if password do not match :
{
    "action": "login",
    "status": "Invalid Credentials",
    "message": "Invalid email or password"
}
Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode
  • 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"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Then :

  1. Checks if incoming email exists in DB.
  2. Send response if email exists :
{
  "payload":{
    "action":"resetPassword",
    "status":"validEmail",
    "message":"valid email"
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Send response if email doesn’t exist :
{
  "payload":{
    "action":"resetPassword",
    "status":"invalidEmail",
    "message":"Invalid email"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Then the flow :

  1. Checks if user’s email exists in ResetPasswordCodes collection; because each code is saved with user’s email
  2. 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
  3. 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."
    }
    
  4. Send invalid reset code response if codes do not match

    {
        "action": "resetPassword",
        "status": "invalidCode",
        "message": "The Reset Password Code Is Invalid."
    }
    
  5. Send valid reset code response if codes match

    {
        "status": "validCode",
        "action": "resetPassword"
    }
    
  6. 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"
}
Enter fullscreen mode Exit fullscreen mode

Then :

  1. Hash user’s password with bcrypt node
  2. Save new password into his document
  3. Sends success response :
{
"status": "success",
"action": "setNewPassword"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Then :

  1. 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"
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode

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 :

    {
        "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 :

    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"
}
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • Sends request id (draft id) as response to the dashboard uibuilder :
// send requestId as response
msg.payload = {
    id : msg.response.requestId,
};
return msg;
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
  • Update company details
msg.collection = "Drafts";
msg.operation = "updateOne";

msg.payload = [
    {requestId: msg.requestID},
    {$set: {companyDetails: msg.companyDetails}}
]

return msg;
Enter fullscreen mode Exit fullscreen mode
  • 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"
}
Enter fullscreen mode Exit fullscreen mode
  • Get applicant’s drafts :
msg.collection = "Drafts";
msg.operation = "find";

msg.payload = {
    email: msg.userData.email
};

return msg;
Enter fullscreen mode Exit fullscreen mode
  • Send applicant drafts as response :
{
  "type": "getDrafts",
  "payload": [user's draft requests]
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
  • Delete draft :
msg.collection = "Drafts";
msg.operation = "deleteOne";

msg.payload = {
    requestId: msg.draftId // delete draft using id
};

return msg;
Enter fullscreen mode Exit fullscreen mode
  • Send response :
// send deleted draft request id as response
msg.payload = {
    draftId: msg.draftId
};

return msg;
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • Then get all user’s draft requests and save it:
msg.collection = "Drafts";
msg.operation = "find";

msg.payload = {
    email: msg.userData.email
};

return msg;
Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
  • Find the request :
msg.collection = "Requests"
msg.operation = "findOne"

msg.payload = {
    requestId: msg.requestID
}

return msg;
Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode

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"
}

Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode
  • Response :
msg.response = {  
    status: "success",
    action: "editProfile",
    message: "Profile Edited Successfully"

}
return msg;
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
  • Update user’s avatar :
msg.collection = "Users";
msg.operation = "updateOne";
msg.payload = [
    { userId: msg.userId },
    { $set: { "profile.avatarUrl": msg.avatarBase64 } }
];

return msg;
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • Response :
{
    "status": "success",
    "action": "uploadAvatar",
    "message": "Avatar Uploaded Successfully"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode

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 :

  1. Users

    Stores user accounts, authentication tokens, roles, and profile information. This collection holds all core identity and authorization data for the platform.

  2. 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.

  3. Drafts

    Maintains partially completed or unsubmitted onboarding requests. This allows Applicants to save progress and continue their request at a later time.

  4. 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.

  5. 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 :
Enter fullscreen mode Exit fullscreen mode
```
{
  "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"
}

```
Enter fullscreen mode Exit fullscreen mode
- Request Document :
Enter fullscreen mode Exit fullscreen mode
```
{
  "_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
}

```
Enter fullscreen mode Exit fullscreen mode
- Draft Document :
Enter fullscreen mode Exit fullscreen mode
```
{
  "_id": objectId('692ee990bd184710aa45faa5'),
  "requestId": 254634,
  "email" : "applicant@gmail.com",
  companyDetails : obj
}

```
Enter fullscreen mode Exit fullscreen mode
### 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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)