Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions api/tacticalrmm/core/migrations/0052_coresettings_branding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.26 on 2025-12-04 00:47

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("core", "0051_alter_coresettings_default_time_zone"),
]

operations = [
migrations.AddField(
model_name="coresettings",
name="branding",
field=models.JSONField(default=dict),
),
]
1 change: 1 addition & 0 deletions api/tacticalrmm/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ class CoreSettings(BaseAuditModel):

block_local_user_logon = models.BooleanField(default=False)
sso_enabled = models.BooleanField(default=False)
branding = models.JSONField(default=dict)

def save(self, *args, **kwargs) -> None:
from alerts.tasks import cache_agents_alert_template
Expand Down
3 changes: 2 additions & 1 deletion api/tacticalrmm/core/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.urls import path
from django.urls import include, path
from django.conf import settings

from . import views
Expand All @@ -24,6 +24,7 @@
path("clearcache/", views.clear_cache),
path("openai/generate/", views.OpenAICodeCompletion.as_view()),
path("webtermperms/", views.webterm_perms),
path("", include("ee.whitelabel.urls")),
]

if not getattr(settings, "DEMO", False):
Expand Down
24 changes: 3 additions & 21 deletions api/tacticalrmm/ee/reporting/management/commands/get_webtar_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,14 @@
For details, see: https://license.tacticalrmm.com/ee
"""

import urllib.parse
from typing import Any, Optional
from typing import Any

from core.models import CodeSignToken
from django.conf import settings
from django.core.management.base import BaseCommand
from tacticalrmm.utils import get_webtar_url


class Command(BaseCommand):
help = "Get webtar url"

def handle(self, *args: tuple[Any, Any], **kwargs: dict[str, Any]) -> None:
webtar = f"trmm-web-v{settings.WEB_VERSION}.tar.gz"
url = f"https://github.com/amidaware/tacticalrmm-web/releases/download/v{settings.WEB_VERSION}/{webtar}"

t: "Optional[CodeSignToken]" = CodeSignToken.objects.first()
if not t or not t.token:
self.stdout.write(url)
return

if t.is_valid:
params = {
"token": t.token,
"webver": settings.WEB_VERSION,
"api": settings.ALLOWED_HOSTS[0],
}
url = settings.WEBTAR_DL_URL + urllib.parse.urlencode(params)

self.stdout.write(url)
self.stdout.write(get_webtar_url())
5 changes: 5 additions & 0 deletions api/tacticalrmm/ee/whitelabel/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
Copyright (c) 2025-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
"""
12 changes: 12 additions & 0 deletions api/tacticalrmm/ee/whitelabel/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""
Copyright (c) 2025-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
"""

from django.apps import AppConfig


class WhiteLabelConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "ee.whitelabel"
11 changes: 11 additions & 0 deletions api/tacticalrmm/ee/whitelabel/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
Copyright (c) 2025-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
"""

from django.urls import path

from . import views

urlpatterns = [path("branding/", views.AddBranding.as_view())]
78 changes: 78 additions & 0 deletions api/tacticalrmm/ee/whitelabel/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""
Copyright (c) 2025-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
"""

from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import serializers
from django.conf import settings

from tacticalrmm.helpers import notify_error
from core.permissions import CoreSettingsPerms
from core.utils import get_core_settings
from tacticalrmm.utils import download_and_extract_webtar


class AddBranding(APIView):
permission_classes = [IsAuthenticated, CoreSettingsPerms]

class InputRequest(serializers.Serializer):
company_name = serializers.CharField(
max_length=255, required=False, allow_blank=True
)
primary_color = serializers.CharField(
max_length=25, required=False, allow_blank=True
)
secondary_color = serializers.CharField(
max_length=25, required=False, allow_blank=True
)
accent_color = serializers.CharField(
max_length=25, required=False, allow_blank=True
)
dark_color = serializers.CharField(
max_length=25, required=False, allow_blank=True
)
dark_page_color = serializers.CharField(
max_length=25, required=False, allow_blank=True
)
positive_color = serializers.CharField(
max_length=25, required=False, allow_blank=True
)
negative_color = serializers.CharField(
max_length=25, required=False, allow_blank=True
)
info_color = serializers.CharField(
max_length=25, required=False, allow_blank=True
)
warning_color = serializers.CharField(
max_length=25, required=False, allow_blank=True
)
favicon = serializers.CharField(required=False, allow_blank=True)
toolbar_color: serializers.CharField(max_length=25, required=False, allow_blank=True)
toolbar_text_color: serializers.CharField(max_length=25, required=False, allow_blank=True)
light_page_color: serializers.CharField(max_length=25, required=False, allow_blank=True)
light_color: serializers.CharField(max_length=25, required=False, allow_blank=True)

def get(self, request):
settings = get_core_settings()
return Response(settings.branding)

def post(self, request):
core = get_core_settings()

self.InputRequest(data=request.data).is_valid(raise_exception=True)

core.branding = request.data
core.save(update_fields=["branding"])

if settings.DOCKER_BUILD:
return Response()
else:
result = download_and_extract_webtar()
if not result:
return notify_error("Failed to download and extract webtar")
else:
return Response()
2 changes: 1 addition & 1 deletion api/tacticalrmm/tacticalrmm/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@
]

if not DEMO:
INSTALLED_APPS += ("ee.reporting",)
INSTALLED_APPS += ("ee.reporting", "ee.whitelabel")

CHANNEL_LAYERS = {
"default": {
Expand Down
75 changes: 75 additions & 0 deletions api/tacticalrmm/tacticalrmm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import re
import socket
import subprocess
import tarfile
import tempfile
import time
from contextlib import contextmanager
Expand All @@ -20,9 +21,11 @@
from rest_framework.response import Response

from agents.models import Agent
from core.models import CodeSignToken
from core.utils import get_core_settings, token_is_valid
from logs.models import DebugLog
from tacticalrmm.celery import app as celery_app
from tacticalrmm.logger import logger
from tacticalrmm.constants import (
MONTH_DAYS,
MONTHS,
Expand Down Expand Up @@ -488,3 +491,75 @@ def localhost_port_is_open(port):
return True
except (socket.timeout, ConnectionRefusedError):
return False


def get_webtar_url():
webtar = f"trmm-web-v{settings.WEB_VERSION}.tar.gz"
url = f"https://github.com/amidaware/tacticalrmm-web/releases/download/v{settings.WEB_VERSION}/{webtar}"

t: "Optional[CodeSignToken]" = CodeSignToken.objects.first()
if not t or not t.token:
return url

if t.is_valid:
return settings.WEBTAR_DL_URL
else:
return url


def download_and_extract_webtar() -> bool:
try:
url = get_webtar_url()

if not url:
logger.warning("get_webtar_url returned empty URL")
return False

if url.startswith("https://github.com"):
return False

core = get_core_settings()
payload = {
"token": core.code_sign_token.token,
"webver": settings.WEB_VERSION,
"api": settings.ALLOWED_HOSTS[0],
**core.branding.get("custom_branding", {}),
}

response = requests.post(url, stream=True, json=payload)

if response.status_code != 200:
return False

server_checksum = response.headers.get("X-File-Checksum")
if not server_checksum:
return False

sha256_hasher = hashlib.sha256()
downloaded_bytes = 0

with open(OUTPUT_FILENAME, "wb") as f:
for chunk in response.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
sha256_hasher.update(chunk)
downloaded_bytes += len(chunk)

local_checksum = sha256_hasher.hexdigest()

if server_checksum != local_checksum:
return False

try:
with tarfile.open(tmp_file_path, "r:gz") as tar:
tar.extractall(extract_base)
finally:
try:
os.unlink(tmp_file_path)
except OSError:
pass

return True

except Exception as e:
return False
Loading