# Deployment

## Prequisites

* Docker or Podman (to run the Toolkit Docker image)
* SQL Server (to host the database)
* Keycloak (for authentication and authorization)
* Optional: Microsoft Exchange (for email integration)
* Optional: Azure Blob Storage account (if using Azure for blob storage)
* Optional: Nginx or Apache (if using a reverse proxy)
* Optional: Cron (for scheduled tasks, e.g. email polling)
* Optional: PowerBI account (for PowerBI integration)

## Docker/Podman

Toolkit is a Docker container with a set of apps for UDiTH App such as Tickets/Punchlist, Clashes, Lockout/Tagout, and Redlining. You can pull the Toolkit using Docker:

    docker pull quay.io/caxperts/toolkit:latest

or (for the latest stable release)

    docker pull quay.io/caxperts/toolkit:stable

## Database

The application requires a running SQL Server for its database. Migration scripts are provided that are used to populate the database schema.

## Local or Azure blob storage

Admin can specify an environment variables that will change how the application saves blobs. Those variables are:

- STORAGE_TYPE - accepts only 2 values, "local" or "azure".
- STORAGE_PATH - when "local" type used, defines an absolute path to storage on the hardware the server is run, e.g. "/home/user/localStorage".
- AZURE_STORAGE_CONNECTION_STRING - connection string given by Azure Blob Storage.
- AZURE_STORAGE_CONNECTION_DOMAIN - the domain defined by Azure Blob Storage.

## Reverse Proxy Setup

When configuring a reverse proxy like Nginx or Apache, you must increase the size of headers that can be passed to accommodate larger requests. Below is an example configuration snippet for Nginx to achieve this:

    server {
    ...
    proxy_buffers 16 16k; # Sets 16 buffers of 16KB each

    proxy_buffer_size 32k; # Increases the initial buffer size to 32KB

    proxy_busy_buffers_size 64k; # Sets the busy buffer size to 64KB
    ...
    }

## Keycloak Clients and Scopes Setup

To integrate with Keycloak, you need to configure a custom client and associated scopes. Below are the step-by-step instructions for setting up the client and its scopes.

### Client scope configuration
Create a client scope named "groups" with the following settings:

#### Scope Settings

- **Name:** groups
- **Description:** "description"
- **Type:** None
- **Protocol:** OpenID Connect
- **Display on Consent Screen:** On
- **Consent screen text:** if required
- **Include in Token Scope:** On_
- **Display Order:** Leave empty

#### Mappers

1. Add a predefined mapper.
2. Select the "groups" mapper to include group information in the token.

#### Scope Details

- Leave empty by default unless specific mappings are required.

### Client Configuration

Create a custom client in Keycloak with the following settings to suit your application’s needs.

#### General Settings

- **Client Type:**
  - Leave as default or select "OpenID Connect" if prompted.

#### Capability Configuration

- **Client Authentication:**

  - Off (If confidential access required mark it True)

- **Authorization:**

  - Enable or disable based on the client’s specific requirements.

- **Authentication Flow:**
  - **Standard Flow:** Enabled
  - **Direct Access Grants:** Enabled
  - Enable additional flows as needed based on your use case.

#### Login Settings

- **Root URL:**
  - Leave empty.
- **Home URL:**
  - Leave empty.
- **Valid Redirect URIs:**
  - https://your.domain.com/* (Adjust to match your domain and paths).
- **Valid Post-Logout Redirect URIs:**
  - - (Use defaults or specify custom URIs as required).
- **Web Origins:**
  - - (Use defaults or specify allowed origins explicitly).### Client Scopes Configuration

### User groups
As a minimal setup, a user group named "Admin" exactly in that spelling is required.
A typical user groups setup would look like this: 
![Example of user groups configuration in Keycloak](media/deployment_guide_typical_user_groups.png)



## Email Configuration

The application uses two separate email accounts for different purposes:

### Support Email (Interactions)

The **SUPPORT_EMAIL** account is used for the ticket/interaction system. It serves as the single mailbox for:

- **Inbox scanning**: A cron job polls this mailbox every minute for new incoming emails and automatically creates tickets from them.
- **Sending replies**: When a user replies to a ticket from the Toolkit UI, the reply is sent from this email address.
- **Conversation threading**: The Toolkit UI fetches email conversation chains from this mailbox.

The relevant environment variables are: `AZURE_AD_TENANT_ID`, `SUPPORT_EMAIL`, `SUPPORT_CLIENT_ID`, and `SUPPORT_CLIENT_SECRET`.

To enable inbox scanning and automatic ticket creation, set `ENABLE_CRON_TICKET_CREATION=true`. When set to `false` or left unset, the cron job for ticket creation will not run, and incoming emails will not be processed into tickets.

### Workflow Email

The **WORKFLOW_EMAIL** account is used exclusively for outbound workflow notification emails. When a workflow step is accepted and advances to the next step, notification emails are sent to the responsible users from this address. This mailbox is never scanned for incoming emails and is not used for ticket creation.

The relevant environment variables are: `AZURE_AD_TENANT_ID`, `WORKFLOW_EMAIL`, `WORKFLOW_CLIENT_ID`, and `WORKFLOW_CLIENT_SECRET`.

### Azure AD App Registration

Both email accounts require an Azure AD app registration with Microsoft Graph `Mail.Send` and `Mail.Read` permissions (application-level) for their respective mailboxes. The `AZURE_AD_TENANT_ID` is shared between both accounts.

For detailed support in setting up the Azure AD app registrations, please contact us directly. If the toolkit should also be able to send notifications via the Keycloak service client, a Keycloak client is required (i.e. SERVICE_CLIENT_ID and SERVICE_CLIENT_SECRET as described in the following section).


## Setup for Keycloak role sync for with Service Client
To synchronize roles from Keycloak with Toolkit (and also to send notifcations by email), a service client is required. This service client is triggered with the Keycloak group sync cron job. The schedule of this cron job is defined in KC_GROUP_SYNC_CRON.

### Creating the service client in Keycloak

1. Navigate to tab 'Clients'

2. Create a new client:

3. General Settings:

   - Client Type: OpenID Connect
   - Client ID: provide client id name that you want to use
   - Name: Optional
   - Description: Optional

4. Capability config:
   - Client authentication: ON
   - Authorization: OFF
   - Only Service accounts roles is checked

### Adding Groups scope to service client

1. Navigate to tab 'Clients'

2. Choose your newly created client
Here named "serviceclient": 
![Keycloak Panel for service client](media/deployment-keycloak-service-client.png)

3. Navigate to tab 'Client Scopes'

4. Add client scope

5. Find in scopes the following:

   - Name: 'groups'
   - Protocol: 'OpenID Connect'

6. Add groups scope as 'default'

### Assigning account roles to the Keycloak service client and groups to corresponding service client user

1. Navigate to the newly created client id.

2. Navigate to 'Service accounts roles' tab.

3. Assign the service account roles as specified in the picture below: 

![Keycloak service client privileges](media/keycloak_hidden_user.png)

4. Make sure that user account corresponding to the service client (in this example called 'service-account-serviceclient') is member of the Admin group via the link hightlighted in the picture above. 

### Keycloak service client in environment variables
SERVICE_CLIENT_ID is your service client name, SERVICE_CLIENT_SECRET can taken from the "Credential" tab, labelled Client secret. 

## Comprehensive Environment Variables List

(Variables marked as **bold** are the minimum variables required to start Toolkit)

**DATABASE_URL** = <sql_provider>:<port>;database=<database>;user=<user>;password=<password>trustServerCertificate=true

**KEYCLOAK_BASE_URL** = <url> e.g. https://cax.authudith.com

**KEYCLOAK_ID** = <id> e.g. connector

**KEYCLOAK_ISSUER** = <issuer> e.g. https://cax.authudith.com/realms/apps-demo

**KEYCLOAK_REALM** = <realm> e.g. apps-demo

**KEYCLOAK_REDIRECT_URL** = <appcontrol_url> e.g. https://udithappcontrol.com

**SERVICE_CLIENT_ID**="<id>" // Keycloak client id created specifically for cron, required for group synchronization

**SERVICE_CLIENT_SECRET**="<secret>" // Existing keycloak client secret.

**NEXTAUTH_URL** = <appcontrol_url> e.g. https://udithappcontrol.com

**NEXTAUTH_SECRET** = "any_string_that_represents_secret" // required by nextauth to secure authentication via application

REDIRECT_WEBSERVICES_URL="<upv_web_based_redirect_url>", required for usage in browser-based viewing

AZURE_AD_TENANT_ID=""000000-0000-0000-0000-000000000"

SUPPORT_EMAIL="<email>@<domain>" // Email used for interaction inbox scanning, ticket creation, and sending replies, required for inbound emails

SUPPORT_CLIENT_ID="000000-00-0000-0000-000000000" // Azure AD app client ID for the support email, required for inbound emails

SUPPORT_CLIENT_SECRET="your_secret" // Azure AD app client secret for the support email, required for inbound emails

WORKFLOW_EMAIL="<email>@<domain>" // Email used for outbound workflow notification emails only (never scanned), required for outbound emails

WORKFLOW_CLIENT_ID="000000-00-0000-0000-000000000" // Azure AD app client ID for the workflow email, required for outbound emails

WORKFLOW_CLIENT_SECRET="your_secret" // Azure AD app client secret for the workflow email, required for outbound emails

KC_GROUP_SYNC_CRON=0 3 \* \* \* // this variable is used to run the user roles sync with keyccloak, default is set to every day at 3 am  default.

ENABLE_CRON=<boolean> // 'true' if you want cron job to be running (default), otherwise leave it empty or 'false'

ENABLE_CRON_TICKET_CREATION=<boolean> // 'true' to enable automatic ticket creation from incoming emails, otherwise 'false'

DEVICE_ID = <device_id> e.g. AppControls-udithappcontrol.com, Displayed to UDiTH when authenticating

LOGIN_HEADER = <header_displayed_on_login> e.g. CAXperts

SIGNATURE_REPORT_THRESHOLD = <number> e.g. 1536, represents image size threshold in bytes to filter low quality signature like "dot"

STORAGE_PATH= <absolute_path_to_storage> e.g."/app/storage". If using docker volumes make sure that this path matches the definition there, e.g. use "/app/storage" if it says "toolkit_volume:/app/storage" in the docker stack definition.

STORAGE_TYPE= <storage_type> takes value of "local" || "azure"

AZURE_STORAGE_CONNECTION_STRING = <string> // connection string given by Azure Blob Storage.

AZURE_STORAGE_CONNECTION_DOMAIN = <domain> e.g. caxperts.blob.core.windows.net // the domain defined by Azure Blob Storage.

NEXT_PUBLIC_TICKET_PREFIX="PI-"

PORT=3000

HOST=0.0.0.0

POWERBI_LOGO = <URL_to_logo> it will be displayed on powerbi page

POWERBI_TENANT_ID=<uuid>

POWERBI_CLIENT_ID=<uuid>

POWERBI_SECRET=<secret>


## Browser Authentication in UDiTH Mode

By default, when the Toolkit runs inside the UDiTH application, it uses UDiTH's authentication context for single sign-on. However, this can be incompatible with reverse proxies like Azure EasyAuth that handle authentication at the HTTP level.

### Environment Variable

To enable browser-based authentication even when running inside UPV, set:

```bash
USE_BROWSER_AUTH_IN_UPV=true
```

### Behavior

- **Default (USE_BROWSER_AUTH_IN_UPV=false or unset)**: UDiTH App uses in-app authentication context
- **Enabled (USE_BROWSER_AUTH_IN_UPV=true)**: UDiTH App uses browser authentication flow (same as web browser)

### Use Cases

This flag is particularly useful for:

- **Azure EasyAuth Integration**: When placing Azure App Service EasyAuth in front of the application
- **Corporate Proxy Authentication**: When authentication is handled by enterprise reverse proxies
- **Centralized Identity Management**: When all authentication should flow through a single identity provider endpoint

### Configuration Example

For Azure App Service with EasyAuth:

```bash
# Enable browser auth in UDiTH App
USE_BROWSER_AUTH_IN_UPV=true

# Standard Keycloak configuration (same as browser deployment)
KEYCLOAK_ID=your-client-id
KEYCLOAK_SECRET=your-client-secret
KEYCLOAK_ISSUER=https://your-keycloak-server/realms/your-realm
```

### Testing

1. Deploy with `USE_BROWSER_AUTH_IN_UPV=true`
2. Open the application in UDiTH App
3. Verify that clicking "Sign In" redirects to browser authentication instead of opening UDiTH App authentication dialog
4. Confirm that authentication works through your proxy/EasyAuth setup

## Breaking Change

1. When using STORAGE_TYPE=local with mapped container volume:
   - Please make sure that the mapped volume is owned by user 1000. Otherwise, on blob saving 'permission denied' will be raised.


## Multi-tenancy (Path-Based Multi-tenancy) [Experimental]

### Overview

This feature enables deployment of multiple Toolkit instances on a single domain or subdomain, each accessible via a unique subpath (e.g., `/instance1`, `/instance2`).

**Example URLs:**
- https://mydomain.com/instance1
- https://mydomain.com/instance2
- https://sub.mydomain.com/instance1
- https://sub.mydomain.com/instance2

Each instance can have its own Microsoft SQL Server database and Keycloak Identity Provider. It is strongly recommended to use a separate database for each instance to fully benefit from multi-tenancy.

### Use Cases

**1. Segregated Access Control**

Toolkit does not natively support multi-tenancy. Deploying multiple instances allows you to restrict access to data (e.g., punchlist items) by user group. For example, users in group A only see items relevant to category A, while group B sees category B.

**2. Simplified Deployment**

With multi-tenancy, IT admins only need to acquire a single domain and SSH key. Previously, each new instance required a separate domain setup, which was time-consuming and error-prone. Now, one domain can host multiple instances, streamlining the process.

### Requirements

- Recent Toolkit Docker image (e.g., `toolkit:2.90`).
- Custom Nginx reverse proxy Docker image (e.g., `toolkit:nginx-2.9`).
- Separate SQL Server databases for each instance (recommended).
- At least one Keycloak Identity Provider (shared sessions possible).
- Modified docker-compose file for multi-instance deployment.
- Adjusted environment variables for each instance in the docker-compose file.

### Example docker-compose.yml

Below is an example docker-compose file deploying one Nginx reverse proxy and two Toolkit instances. The Nginx container auto-generates routing based on the `INSTANCES` environment variable. SQL Server and Keycloak are assumed to be running externally.

```yaml
version: '3.8'

services:
  toolkit_entrypoint:
    image: quay.io/caxperts/toolkit:nginx-2.90
    #ports: <- only set if no reverse proxy e.g. nginx proxy manager is used
    #  - 80:80 
    environment:
      INSTANCES: "instance1,instance2"
    depends_on:
      - instance1
      - instance2
    restart: unless-stopped
    # volumes:
    #   - ./index.html:/usr/share/nginx/html/index.html:ro
    networks:
      - toolkit_network
      - external

  instance1:
    image: quay.io/caxperts/toolkit:2.90
    container_name: toolkit-instance1
    environment:
      BASE_PATH: "/instance1"
      INSTANCE_NAME: "instance1"
      NEXT_PUBLIC_BASE_PATH: "/instance1"
      DATABASE_URL: sqlserver://sqlServerEngine:1433;database=toolkit_db1;user=toolkit_user_db1;password=changeme;trustServerCertificate=true;MultipleActiveResultSets=true
      KEYCLOAK_BASE_URL: https://keycloakdomain.com
      KEYCLOAK_ID: connector
      KEYCLOAK_ISSUER: https://keycloakdomain.com/realms/toolkit_realm
      KEYCLOAK_REALM: toolkit_realm
      NEXTAUTH_URL: https://toolkitBaseUrl.com/instance1/api/auth
      NEXTAUTH_SECRET: demoSecret1
      STORAGE_PATH: storage
      STORAGE_TYPE: local
      NODE_ENV: production
      BODY_SIZE_LIMIT: 200mb
      SERVICE_CLIENT_ID: serviceclient
      SERVICE_CLIENT_SECRET: demoSecret2
      SERVICE_ACCOUNT_SECRET: demoSecret2
      DEVICE_ID: Toolkit_instance1
      KEYCLOAK_REDIRECT_URL: https://toolkitBaseUrl.com/instance1
      SUPPORT_CLIENT_ID: 00000000-0000-0000-0000-000000000000
      SUPPORT_CLIENT_SECRET: demoSecret3
      SUPPORT_EMAIL: support@company-name.com
      AZURE_AD_TENANT_ID: 00000000-0000-0000-0000-000000000001
      KC_GROUP_SYNC_CRON: "*/10 * * * *"
      # Email notification
      WORKFLOW_EMAIL: noreply@company-name.com
      WORKFLOW_CLIENT_ID: 00000000-0000-0000-0000-00000000002
      WORKFLOW_CLIENT_SECRET: demoSecret4
    volumes:
      - instance1-storage:/app/storage
    restart: unless-stopped
    networks:
      - toolkit_network
      - external

  instance2:
    image: quay.io/caxperts/toolkit:2.90
    container_name: toolkit-instance2
    environment:
      BASE_PATH: "/instance2"
      INSTANCE_NAME: "instance2"
      NEXT_PUBLIC_BASE_PATH: "/instance2"
      DATABASE_URL: sqlserver://sqlServerEngine:1433;database=toolkit_db2;user=toolkit_user_db2;password=changeme;trustServerCertificate=true;MultipleActiveResultSets=true
      KEYCLOAK_BASE_URL: https://keycloakdomain.com
      KEYCLOAK_ID: connector
      KEYCLOAK_ISSUER: https://keycloakdomain.com/realms/toolkit_realm
      KEYCLOAK_REALM: toolkit_realm
      NEXTAUTH_URL: https://toolkitBaseUrl.com/instance2/api/auth
      NEXTAUTH_SECRET: demoSecret1
      STORAGE_PATH: storage
      STORAGE_TYPE: local
      NODE_ENV: production
      BODY_SIZE_LIMIT: 200mb
      SERVICE_CLIENT_ID: serviceclient
      SERVICE_CLIENT_SECRET: demoSecret2
      SERVICE_ACCOUNT_SECRET: demoSecret2
      DEVICE_ID: Toolkit_instance2
      KEYCLOAK_REDIRECT_URL: https://toolkitBaseUrl.com/instance2
      SUPPORT_CLIENT_ID: 00000000-0000-0000-0000-000000000000
      SUPPORT_CLIENT_SECRET: demoSecret3
      SUPPORT_EMAIL: support@company-name.com
      AZURE_AD_TENANT_ID: 00000000-0000-0000-0000-000000000001
      KC_GROUP_SYNC_CRON: "*/10 * * * *"
      # Email notification
      WORKFLOW_EMAIL: noreply@company-name.com
      WORKFLOW_CLIENT_ID: 00000000-0000-0000-0000-00000000002
      WORKFLOW_CLIENT_SECRET: demoSecret4
    volumes:
      - instance2-storage:/app/storage
    restart: unless-stopped
    networks:
      - toolkit_network

networks: # should be created seperately 
  toolkit_network:
    external: true # set to false if the network should be auto-created with docker-compose up (not recommended)
    name: "toolkit_network"
  external:
    external: true # set to false if the network should be auto-created with docker-compose up (not recommended)
    name: "external"

volumes:
  instance1-storage:
  instance2-storage:
```

### Important Notes

1. Toolkit instance service names (for example instance2 as shown in above docker-compose file) must be unique for the specified docker-network (here; toolkit_network and external) and **MUST** correlate to the values specified in the **INSTANCES** variable in the nginx service. So if you set INSTANCES=app1,app2,app3,... then the service names of the toolkit instances in the docker-compose file must also be app1:,app2:,app3:,... Apart from that rules you can set any name like foo or bar for the individual instances.
2. The Nginx container, Toolkit instances, and SQL Server container must be on the same Docker network (e.g., `toolkit_network`) for proper domain resolution.
3. If using a reverse proxy (e.g., Nginx Proxy Manager), ensure the Nginx container is on the same network for accessibility.
4. All `KEYCLOAK_REDIRECT_URL` values must be set as "Valid redirect URIs" and "Valid post logout redirect URIs" in your Keycloak realm for the client specified in `KEYCLOAK_ID`.
5. Each Toolkit instance requires a valid license and consumes one session per user. A user logged into two instances uses two sessions.


### Configuring Nginx Proxy Manager

If you use Nginx Proxy Manager (recommended) instead of directly exposing the nginx service e.g. via setting the PORT section to Port: -80:80 then make sure the following settings are set and match the values specified in the docker-compose file:

*Note:* This section assumes you are already using Nginx Proxy Manager to host a single-instance deployment of the toolkit and therefore only covers the necessary changes to migrate to a multi-tenant deployment.

The following needs to changed in the Proxy Host:

**Forward Hostname / IP**: toolkit_entrypoint <- must be the same value as the service name of the nginx service in the docker-compose file

**Forward Port**: 80 <- this is the port the nginx service listens on

The rest is the same as for the single instance deployment e.g. the 
**Custom Nginx Configuration** is also set to:
```
proxy_buffers 16 16k;
proxy_buffer_size 32k;
proxy_busy_buffers_size 64k;
```

### Custom Nginx landing page

Per default a simple landing page containing links to all available instances is created that looks like this:

<img src="media/deployment_guide_nginxLandingPage_default.png" width="500">

In order to set your own index.html file that for example contains your corporate identity or an automatic redirect to a specific instance do the following.

Put your custom index.html file in the same folder as the docker-compose.yml file. Than uncomment the volume mapping in the docker-compose.yml file: 

before -> leads to generation of default landing page
```
# volumes:
    #   - ./index.html:/usr/share/nginx/html/index.html:ro
```
after -> your specified index.html file is used instead of the auto-generated one:

```
volumes:
      - ./index.html:/usr/share/nginx/html/index.html:ro
```

Here is an example how this can look like:

<img src="media/deployment_guide_nginxLandingPage_custom.png" width="500">





