Compare commits
	
		
			33 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					f82b589d03 | ||
| 
						 | 
					cddac4d0fb | ||
| 
						 | 
					ff41bbd0e5 | ||
| 
						 | 
					4bdb6ae84e | ||
| 
						 | 
					58fe14bd31 | ||
| 
						 | 
					97f362ed1e | ||
| 
						 | 
					b63e87ecb6 | ||
| 
						 | 
					ac3550dfd7 | ||
| 
						 | 
					8278a4cfd9 | ||
| 
						 | 
					f161a2bbc8 | ||
| 
						 | 
					6a94489df0 | ||
| 
						 | 
					c3a0b9192f | ||
| 
						 | 
					69ff70a9ce | ||
| 
						 | 
					5284eb0af8 | ||
| 
						 | 
					58384ae136 | ||
| 
						 | 
					054cc78e65 | ||
| 
						 | 
					8c283281d6 | ||
| 
						 | 
					241fe41756 | ||
| 
						 | 
					e50e0626fa | ||
| 
						 | 
					c9135f1573 | ||
| 
						 | 
					ec2663a152 | ||
| 
						 | 
					7567042c8a | ||
| 
						 | 
					c99ceb155f | ||
| 
						 | 
					f44c92f0d3 | ||
| 
						 | 
					492701ec62 | ||
| 
						 | 
					a6d0acaa4d | ||
| 
						 | 
					f84b4e7274 | ||
| 
						 | 
					b7ef5b82d8 | ||
| 
						 | 
					a854d2c38c | ||
| 
						 | 
					5140499bbd | ||
| 
						 | 
					7183e9ee85 | ||
| 
						 | 
					11885e0aca | ||
| 
						 | 
					2bda4e822c | 
							
								
								
									
										66
									
								
								.github/workflows/ci-tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								.github/workflows/ci-tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
name: Tests CI
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - "*"
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches:
 | 
			
		||||
      - "*"
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  test:
 | 
			
		||||
    runs-on: self-hosted
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
 | 
			
		||||
      - name: Setup virtual env and install requirements
 | 
			
		||||
        run: |
 | 
			
		||||
          sudo -u postgres psql -c 'DROP DATABASE IF EXISTS pipeline'
 | 
			
		||||
          sudo -u postgres psql -c 'DROP DATABASE IF EXISTS test_pipeline'
 | 
			
		||||
          sudo -u postgres psql -c 'CREATE DATABASE pipeline'
 | 
			
		||||
          sudo -u postgres psql -c "SET client_encoding = 'UTF8'" pipeline
 | 
			
		||||
          pwd
 | 
			
		||||
          rm -rf /actions-runner/_work/trmm-actions/trmm-actions/api/env
 | 
			
		||||
          cd api
 | 
			
		||||
          python3.10 -m venv env
 | 
			
		||||
          source env/bin/activate
 | 
			
		||||
          cd tacticalrmm
 | 
			
		||||
          python --version
 | 
			
		||||
          SETTINGS_FILE="tacticalrmm/settings.py"
 | 
			
		||||
          SETUPTOOLS_VER=$(grep "^SETUPTOOLS_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
 | 
			
		||||
          WHEEL_VER=$(grep "^WHEEL_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
 | 
			
		||||
          pip install --upgrade pip
 | 
			
		||||
          pip install setuptools==${SETUPTOOLS_VER} wheel==${WHEEL_VER}
 | 
			
		||||
          pip install -r requirements.txt -r requirements-test.txt
 | 
			
		||||
 | 
			
		||||
      - name: Run django tests
 | 
			
		||||
        env:
 | 
			
		||||
          GHACTIONS: "yes"
 | 
			
		||||
        run: |
 | 
			
		||||
          cd api/tacticalrmm
 | 
			
		||||
          source ../env/bin/activate
 | 
			
		||||
          rm -f .coverage coverage.lcov
 | 
			
		||||
          coverage run --concurrency=multiprocessing manage.py test -v 2 --parallel
 | 
			
		||||
          coverage combine
 | 
			
		||||
          coverage lcov
 | 
			
		||||
          if [ $? -ne 0 ]; then
 | 
			
		||||
              exit 1
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Codestyle black
 | 
			
		||||
        run: |
 | 
			
		||||
          cd api
 | 
			
		||||
          source env/bin/activate
 | 
			
		||||
          black --exclude migrations/ --check tacticalrmm
 | 
			
		||||
          if [ $? -ne 0 ]; then
 | 
			
		||||
              exit 1
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Coveralls
 | 
			
		||||
        uses: coverallsapp/github-action@master
 | 
			
		||||
        with:
 | 
			
		||||
          github-token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          path-to-lcov: ./api/tacticalrmm/coverage.lcov
 | 
			
		||||
          base-path: ./api/tacticalrmm
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -51,3 +51,4 @@ reset_db.sh
 | 
			
		||||
run_go_cmd.py
 | 
			
		||||
nats-api.conf
 | 
			
		||||
ignore/
 | 
			
		||||
coverage.lcov
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								README.md
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
# Tactical RMM
 | 
			
		||||
 | 
			
		||||
[](https://dev.azure.com/dcparsi/Tactical%20RMM/_build/latest?definitionId=4&branchName=develop)
 | 
			
		||||
[](https://coveralls.io/github/wh1te909/tacticalrmm?branch=develop)
 | 
			
		||||

 | 
			
		||||
[](https://coveralls.io/github/amidaware/tacticalrmm?branch=develop)
 | 
			
		||||
[](https://github.com/python/black)
 | 
			
		||||
 | 
			
		||||
Tactical RMM is a remote monitoring & management tool, built with Django and Vue.\
 | 
			
		||||
@@ -28,9 +28,12 @@ Demo database resets every hour. A lot of features are disabled for obvious reas
 | 
			
		||||
- Remote software installation via chocolatey
 | 
			
		||||
- Software and hardware inventory
 | 
			
		||||
 | 
			
		||||
## Windows versions supported
 | 
			
		||||
## Windows agent versions supported
 | 
			
		||||
 | 
			
		||||
- Windows 7, 8.1, 10, Server 2008R2, 2012R2, 2016, 2019
 | 
			
		||||
- Windows 7, 8.1, 10, 11, Server 2008R2, 2012R2, 2016, 2019, 2022
 | 
			
		||||
 | 
			
		||||
## Linux agent versions supported
 | 
			
		||||
- Any distro with systemd
 | 
			
		||||
 | 
			
		||||
## Installation / Backup / Restore / Usage
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,8 @@
 | 
			
		||||
 | 
			
		||||
| Version | Supported          |
 | 
			
		||||
| ------- | ------------------ |
 | 
			
		||||
| 0.12.0   | :white_check_mark: |
 | 
			
		||||
| < 0.12.0 | :x:                |
 | 
			
		||||
| 0.12.2   | :white_check_mark: |
 | 
			
		||||
| < 0.12.2 | :x:                |
 | 
			
		||||
 | 
			
		||||
## Reporting a Vulnerability
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,5 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import base64
 | 
			
		||||
import re
 | 
			
		||||
import time
 | 
			
		||||
from collections import Counter
 | 
			
		||||
from distutils.version import LooseVersion
 | 
			
		||||
from typing import Any
 | 
			
		||||
@@ -11,10 +9,6 @@ import nats
 | 
			
		||||
import validators
 | 
			
		||||
from asgiref.sync import sync_to_async
 | 
			
		||||
from core.models import TZ_CHOICES, CoreSettings
 | 
			
		||||
from Crypto.Cipher import AES
 | 
			
		||||
from Crypto.Hash import SHA3_384
 | 
			
		||||
from Crypto.Random import get_random_bytes
 | 
			
		||||
from Crypto.Util.Padding import pad
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
@@ -219,7 +213,8 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            try:
 | 
			
		||||
                if not self.wmi_detail["gpus"]:
 | 
			
		||||
                    return "No graphics cards"
 | 
			
		||||
                return self.wmi_detail["gpus"]
 | 
			
		||||
 | 
			
		||||
                return ", ".join(self.wmi_detail["gpus"])
 | 
			
		||||
            except:
 | 
			
		||||
                return "Error getting graphics cards"
 | 
			
		||||
 | 
			
		||||
@@ -613,30 +608,6 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        # Generate tasks based on policies
 | 
			
		||||
        Policy.generate_policy_tasks(self)
 | 
			
		||||
 | 
			
		||||
    # https://github.com/Ylianst/MeshCentral/issues/59#issuecomment-521965347
 | 
			
		||||
    def get_login_token(self, key, user, action=3):
 | 
			
		||||
        try:
 | 
			
		||||
            key = bytes.fromhex(key)
 | 
			
		||||
            key1 = key[0:48]
 | 
			
		||||
            key2 = key[48:]
 | 
			
		||||
            msg = '{{"a":{}, "u":"{}","time":{}}}'.format(
 | 
			
		||||
                action, user.lower(), int(time.time())
 | 
			
		||||
            )
 | 
			
		||||
            iv = get_random_bytes(16)
 | 
			
		||||
 | 
			
		||||
            # sha
 | 
			
		||||
            h = SHA3_384.new()
 | 
			
		||||
            h.update(key1)
 | 
			
		||||
            hashed_msg = h.digest() + msg.encode()
 | 
			
		||||
 | 
			
		||||
            # aes
 | 
			
		||||
            cipher = AES.new(key2, AES.MODE_CBC, iv)
 | 
			
		||||
            msg = cipher.encrypt(pad(hashed_msg, 16))
 | 
			
		||||
 | 
			
		||||
            return base64.b64encode(iv + msg, altchars=b"@$").decode("utf-8")
 | 
			
		||||
        except Exception:
 | 
			
		||||
            return "err"
 | 
			
		||||
 | 
			
		||||
    def _do_nats_debug(self, agent, message):
 | 
			
		||||
        DebugLog.error(agent=agent, log_type="agent_issues", message=message)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -519,7 +519,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.get_login_token")
 | 
			
		||||
    @patch("meshctrl.utils.get_auth_token")
 | 
			
		||||
    def test_meshcentral_tabs(self, mock_token):
 | 
			
		||||
        url = f"{base_url}/{self.agent.agent_id}/meshcentral/"
 | 
			
		||||
        mock_token.return_value = "askjh1k238uasdhk487234jadhsajksdhasd"
 | 
			
		||||
@@ -547,10 +547,6 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        mock_token.return_value = "err"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import os
 | 
			
		||||
import random
 | 
			
		||||
import string
 | 
			
		||||
import time
 | 
			
		||||
from meshctrl.utils import get_auth_token
 | 
			
		||||
 | 
			
		||||
from core.models import CodeSignToken, CoreSettings
 | 
			
		||||
from core.utils import get_mesh_ws_url, remove_mesh_agent, send_command_with_mesh
 | 
			
		||||
@@ -208,13 +209,7 @@ class AgentMeshCentral(APIView):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
        core = CoreSettings.objects.first()
 | 
			
		||||
 | 
			
		||||
        token = agent.get_login_token(
 | 
			
		||||
            key=core.mesh_token,
 | 
			
		||||
            user=f"user//{core.mesh_username.lower()}",  # type:ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if token == "err":
 | 
			
		||||
            return notify_error("Invalid mesh token")
 | 
			
		||||
        token = get_auth_token(user=core.mesh_username, key=core.mesh_token)
 | 
			
		||||
 | 
			
		||||
        control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31"  # type:ignore
 | 
			
		||||
        terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31"  # type:ignore
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
from meshctrl.utils import get_auth_token
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from core.utils import get_auth_token, get_mesh_device_id, get_mesh_ws_url
 | 
			
		||||
from core.utils import get_mesh_device_id, get_mesh_ws_url
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,13 @@ class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
        self.stdout.write("Creating configuration for nats-api...")
 | 
			
		||||
        db = settings.DATABASES["default"]
 | 
			
		||||
        if hasattr(settings, "DB_SSL"):
 | 
			
		||||
            ssl = settings.DB_SSL
 | 
			
		||||
        elif "DB_SSL" in os.environ:
 | 
			
		||||
            ssl = os.getenv("DB_SSL")
 | 
			
		||||
        else:
 | 
			
		||||
            ssl = "disable"
 | 
			
		||||
 | 
			
		||||
        config = {
 | 
			
		||||
            "key": settings.SECRET_KEY,
 | 
			
		||||
            "natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
 | 
			
		||||
@@ -20,6 +27,7 @@ class Command(BaseCommand):
 | 
			
		||||
            "host": db["HOST"],
 | 
			
		||||
            "port": int(db["PORT"]),
 | 
			
		||||
            "dbname": db["NAME"],
 | 
			
		||||
            "sslmode": ssl,
 | 
			
		||||
        }
 | 
			
		||||
        conf = os.path.join(settings.BASE_DIR, "nats-api.conf")
 | 
			
		||||
        with open(conf, "w") as f:
 | 
			
		||||
 
 | 
			
		||||
@@ -45,9 +45,9 @@ class Command(BaseCommand):
 | 
			
		||||
            # Check for Mesh Username
 | 
			
		||||
            if (
 | 
			
		||||
                not mesh_settings.mesh_username
 | 
			
		||||
                or settings.MESH_USERNAME != mesh_settings.mesh_username
 | 
			
		||||
                or settings.MESH_USERNAME.lower() != mesh_settings.mesh_username
 | 
			
		||||
            ):
 | 
			
		||||
                mesh_settings.mesh_username = settings.MESH_USERNAME
 | 
			
		||||
                mesh_settings.mesh_username = settings.MESH_USERNAME.lower()
 | 
			
		||||
 | 
			
		||||
            # Check for Mesh Site
 | 
			
		||||
            if (
 | 
			
		||||
 
 | 
			
		||||
@@ -97,7 +97,7 @@ class CoreSettings(BaseAuditModel):
 | 
			
		||||
        if not self.pk:
 | 
			
		||||
            try:
 | 
			
		||||
                self.mesh_site = settings.MESH_SITE
 | 
			
		||||
                self.mesh_username = settings.MESH_USERNAME
 | 
			
		||||
                self.mesh_username = settings.MESH_USERNAME.lower()
 | 
			
		||||
                self.mesh_token = settings.MESH_TOKEN_KEY
 | 
			
		||||
            except:
 | 
			
		||||
                pass
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +1,14 @@
 | 
			
		||||
import json
 | 
			
		||||
import tempfile
 | 
			
		||||
import time
 | 
			
		||||
from base64 import b64encode
 | 
			
		||||
from meshctrl.utils import get_auth_token
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
import websockets
 | 
			
		||||
from Crypto.Cipher import AES
 | 
			
		||||
from Crypto.Random import get_random_bytes
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.http import FileResponse
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_auth_token(user, key):
 | 
			
		||||
    key = bytes.fromhex(key)
 | 
			
		||||
    key1 = key[0:32]
 | 
			
		||||
    msg = '{{"userid":"{}", "domainid":"{}", "time":{}}}'.format(
 | 
			
		||||
        f"user//{user}", "", int(time.time())
 | 
			
		||||
    )
 | 
			
		||||
    iv = get_random_bytes(12)
 | 
			
		||||
 | 
			
		||||
    a = AES.new(key1, AES.MODE_GCM, iv)
 | 
			
		||||
    msg, tag = a.encrypt_and_digest(bytes(msg, "utf-8"))  # type: ignore
 | 
			
		||||
 | 
			
		||||
    return b64encode(iv + tag + msg, altchars=b"@$").decode("utf-8")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_mesh_ws_url() -> str:
 | 
			
		||||
    from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,3 +3,4 @@ Werkzeug
 | 
			
		||||
django-extensions
 | 
			
		||||
isort
 | 
			
		||||
types-pytz
 | 
			
		||||
django-silk
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
coverage==6.3.2
 | 
			
		||||
coveralls==3.3.1
 | 
			
		||||
coverage
 | 
			
		||||
coveralls
 | 
			
		||||
model_bakery
 | 
			
		||||
black
 | 
			
		||||
tblib
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
asgiref==3.5.0
 | 
			
		||||
celery==5.2.3
 | 
			
		||||
celery==5.2.6
 | 
			
		||||
certifi==2021.10.8
 | 
			
		||||
cffi==1.15.0
 | 
			
		||||
channels==3.0.4
 | 
			
		||||
@@ -14,8 +14,7 @@ django-rest-knox==4.2.0
 | 
			
		||||
djangorestframework==3.13.1
 | 
			
		||||
future==0.18.2
 | 
			
		||||
msgpack==1.0.3
 | 
			
		||||
nats-py==2.0.0
 | 
			
		||||
packaging==21.3
 | 
			
		||||
nats-py==2.1.0
 | 
			
		||||
psycopg2-binary==2.9.3
 | 
			
		||||
pycparser==2.21
 | 
			
		||||
pycryptodome==3.14.1
 | 
			
		||||
@@ -23,15 +22,16 @@ pyotp==2.6.0
 | 
			
		||||
pyparsing==3.0.7
 | 
			
		||||
pytz==2022.1
 | 
			
		||||
qrcode==7.3.1
 | 
			
		||||
redis==4.1.4
 | 
			
		||||
redis==4.2.2
 | 
			
		||||
requests==2.27.1
 | 
			
		||||
six==1.16.0
 | 
			
		||||
sqlparse==0.4.2
 | 
			
		||||
twilio==7.8.0
 | 
			
		||||
twilio==7.8.1
 | 
			
		||||
urllib3==1.26.9
 | 
			
		||||
uWSGI==2.0.20
 | 
			
		||||
validators==0.18.2
 | 
			
		||||
vine==5.0.0
 | 
			
		||||
websockets==10.2
 | 
			
		||||
zipp==3.7.0
 | 
			
		||||
zipp==3.8.0
 | 
			
		||||
drf_spectacular==0.21.2
 | 
			
		||||
meshctrl==0.1.13
 | 
			
		||||
@@ -17,22 +17,22 @@ LINUX_AGENT_SCRIPT = BASE_DIR / "core" / "agent_linux.sh"
 | 
			
		||||
AUTH_USER_MODEL = "accounts.User"
 | 
			
		||||
 | 
			
		||||
# latest release
 | 
			
		||||
TRMM_VERSION = "0.12.1"
 | 
			
		||||
TRMM_VERSION = "0.12.3"
 | 
			
		||||
 | 
			
		||||
# bump this version everytime vue code is changed
 | 
			
		||||
# to alert user they need to manually refresh their browser
 | 
			
		||||
APP_VER = "0.0.159"
 | 
			
		||||
APP_VER = "0.0.160"
 | 
			
		||||
 | 
			
		||||
# https://github.com/amidaware/rmmagent
 | 
			
		||||
LATEST_AGENT_VER = "2.0.1"
 | 
			
		||||
LATEST_AGENT_VER = "2.0.2"
 | 
			
		||||
 | 
			
		||||
MESH_VER = "0.9.98"
 | 
			
		||||
MESH_VER = "1.0.2"
 | 
			
		||||
 | 
			
		||||
NATS_SERVER_VER = "2.7.4"
 | 
			
		||||
 | 
			
		||||
# for the update script, bump when need to recreate venv or npm install
 | 
			
		||||
PIP_VER = "27"
 | 
			
		||||
NPM_VER = "30"
 | 
			
		||||
PIP_VER = "28"
 | 
			
		||||
NPM_VER = "31"
 | 
			
		||||
 | 
			
		||||
SETUPTOOLS_VER = "59.6.0"
 | 
			
		||||
WHEEL_VER = "0.37.1"
 | 
			
		||||
@@ -52,6 +52,29 @@ REST_KNOX = {
 | 
			
		||||
    "MIN_REFRESH_INTERVAL": 600,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if "GHACTIONS" in os.environ:
 | 
			
		||||
    print("-----------------------PIPELINE----------------------------")
 | 
			
		||||
    DATABASES = {
 | 
			
		||||
        "default": {
 | 
			
		||||
            "ENGINE": "django.db.backends.postgresql",
 | 
			
		||||
            "NAME": "pipeline",
 | 
			
		||||
            "USER": "pipeline",
 | 
			
		||||
            "PASSWORD": "pipeline123456",
 | 
			
		||||
            "HOST": "127.0.0.1",
 | 
			
		||||
            "PORT": "",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    SECRET_KEY = "abcdefghijklmnoptravis123456789"
 | 
			
		||||
    DEBUG = False
 | 
			
		||||
    ALLOWED_HOSTS = ["api.example.com"]
 | 
			
		||||
    ADMIN_URL = "abc123456/"
 | 
			
		||||
    CORS_ORIGIN_WHITELIST = ["https://rmm.example.com"]
 | 
			
		||||
    MESH_USERNAME = "pipeline"
 | 
			
		||||
    MESH_SITE = "https://example.com"
 | 
			
		||||
    MESH_TOKEN_KEY = "bd65e957a1e70c622d32523f61508400d6cd0937001a7ac12042227eba0b9ed625233851a316d4f489f02994145f74537a331415d00047dbbf13d940f556806dffe7a8ce1de216dc49edbad0c1a7399c"
 | 
			
		||||
    REDIS_HOST = "localhost"
 | 
			
		||||
    ADMIN_ENABLED = False
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    from .local_settings import *
 | 
			
		||||
except ImportError:
 | 
			
		||||
@@ -74,12 +97,35 @@ SPECTACULAR_SETTINGS = {
 | 
			
		||||
    "AUTHENTICATION_WHITELIST": ["tacticalrmm.auth.APIAuthentication"],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if not "AZPIPELINE" in os.environ:
 | 
			
		||||
 | 
			
		||||
if not DEBUG:  # type: ignore
 | 
			
		||||
    REST_FRAMEWORK.update(
 | 
			
		||||
        {"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",)}
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
MIDDLEWARE = [
 | 
			
		||||
    "django.middleware.security.SecurityMiddleware",
 | 
			
		||||
    "django.contrib.sessions.middleware.SessionMiddleware",
 | 
			
		||||
    "corsheaders.middleware.CorsMiddleware",  ##
 | 
			
		||||
    "tacticalrmm.middleware.LogIPMiddleware",
 | 
			
		||||
    "django.middleware.common.CommonMiddleware",
 | 
			
		||||
    "django.middleware.csrf.CsrfViewMiddleware",
 | 
			
		||||
    "django.contrib.auth.middleware.AuthenticationMiddleware",
 | 
			
		||||
    "tacticalrmm.middleware.AuditMiddleware",
 | 
			
		||||
    "tacticalrmm.middleware.LinuxMiddleware",
 | 
			
		||||
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if ADMIN_ENABLED:  # type: ignore
 | 
			
		||||
    MIDDLEWARE += ("django.contrib.messages.middleware.MessageMiddleware",)
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    if DEMO:  # type: ignore
 | 
			
		||||
        MIDDLEWARE += ("tacticalrmm.middleware.DemoMiddleware",)
 | 
			
		||||
except:
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
INSTALLED_APPS = [
 | 
			
		||||
    "django.contrib.auth",
 | 
			
		||||
    "django.contrib.contenttypes",
 | 
			
		||||
@@ -107,9 +153,14 @@ INSTALLED_APPS = [
 | 
			
		||||
    "drf_spectacular",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
if not "AZPIPELINE" in os.environ:
 | 
			
		||||
 | 
			
		||||
if DEBUG:  # type: ignore
 | 
			
		||||
        INSTALLED_APPS += ("django_extensions",)
 | 
			
		||||
    INSTALLED_APPS += (
 | 
			
		||||
        "django_extensions",
 | 
			
		||||
        "silk",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    MIDDLEWARE.insert(0, "silk.middleware.SilkyMiddleware")
 | 
			
		||||
 | 
			
		||||
CHANNEL_LAYERS = {
 | 
			
		||||
    "default": {
 | 
			
		||||
@@ -120,9 +171,6 @@ if not "AZPIPELINE" in os.environ:
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if "AZPIPELINE" in os.environ:
 | 
			
		||||
    ADMIN_ENABLED = False
 | 
			
		||||
 | 
			
		||||
if ADMIN_ENABLED:  # type: ignore
 | 
			
		||||
    INSTALLED_APPS += (
 | 
			
		||||
        "django.contrib.admin",
 | 
			
		||||
@@ -130,28 +178,6 @@ if ADMIN_ENABLED:  # type: ignore
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
MIDDLEWARE = [
 | 
			
		||||
    "django.middleware.security.SecurityMiddleware",
 | 
			
		||||
    "django.contrib.sessions.middleware.SessionMiddleware",
 | 
			
		||||
    "corsheaders.middleware.CorsMiddleware",  ##
 | 
			
		||||
    "tacticalrmm.middleware.LogIPMiddleware",
 | 
			
		||||
    "django.middleware.common.CommonMiddleware",
 | 
			
		||||
    "django.middleware.csrf.CsrfViewMiddleware",
 | 
			
		||||
    "django.contrib.auth.middleware.AuthenticationMiddleware",
 | 
			
		||||
    "tacticalrmm.middleware.AuditMiddleware",
 | 
			
		||||
    "tacticalrmm.middleware.LinuxMiddleware",
 | 
			
		||||
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
if ADMIN_ENABLED:  # type: ignore
 | 
			
		||||
    MIDDLEWARE += ("django.contrib.messages.middleware.MessageMiddleware",)
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    if DEMO:  # type: ignore
 | 
			
		||||
        MIDDLEWARE += ("tacticalrmm.middleware.DemoMiddleware",)
 | 
			
		||||
except:
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
ROOT_URLCONF = "tacticalrmm.urls"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -226,38 +252,3 @@ LOGGING = {
 | 
			
		||||
        "django.request": {"handlers": ["file"], "level": "ERROR", "propagate": True}
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if "AZPIPELINE" in os.environ:
 | 
			
		||||
    print("PIPELINE")
 | 
			
		||||
    DATABASES = {
 | 
			
		||||
        "default": {
 | 
			
		||||
            "ENGINE": "django.db.backends.postgresql",
 | 
			
		||||
            "NAME": "pipeline",
 | 
			
		||||
            "USER": "pipeline",
 | 
			
		||||
            "PASSWORD": "pipeline123456",
 | 
			
		||||
            "HOST": "127.0.0.1",
 | 
			
		||||
            "PORT": "",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    REST_FRAMEWORK = {
 | 
			
		||||
        "DATETIME_FORMAT": "%b-%d-%Y - %H:%M",
 | 
			
		||||
        "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
 | 
			
		||||
        "DEFAULT_AUTHENTICATION_CLASSES": (
 | 
			
		||||
            "knox.auth.TokenAuthentication",
 | 
			
		||||
            "tacticalrmm.auth.APIAuthentication",
 | 
			
		||||
        ),
 | 
			
		||||
        "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ALLOWED_HOSTS = ["api.example.com"]
 | 
			
		||||
    DEBUG = True
 | 
			
		||||
    SECRET_KEY = "abcdefghijklmnoptravis123456789"
 | 
			
		||||
 | 
			
		||||
    ADMIN_URL = "abc123456/"
 | 
			
		||||
 | 
			
		||||
    SCRIPTS_DIR = os.path.join(Path(BASE_DIR).parents[1], "scripts")
 | 
			
		||||
    MESH_USERNAME = "pipeline"
 | 
			
		||||
    MESH_SITE = "https://example.com"
 | 
			
		||||
    MESH_TOKEN_KEY = "bd65e957a1e70c622d32523f61508400d6cd0937001a7ac12042227eba0b9ed625233851a316d4f489f02994145f74537a331415d00047dbbf13d940f556806dffe7a8ce1de216dc49edbad0c1a7399c"
 | 
			
		||||
    REDIS_HOST = "localhost"
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,9 @@ if getattr(settings, "ADMIN_ENABLED", False):
 | 
			
		||||
 | 
			
		||||
    urlpatterns += (path(settings.ADMIN_URL, admin.site.urls),)
 | 
			
		||||
 | 
			
		||||
if getattr(settings, "DEBUG", False):
 | 
			
		||||
    urlpatterns += [path("silk/", include("silk.urls", namespace="silk"))]
 | 
			
		||||
 | 
			
		||||
if getattr(settings, "SWAGGER_ENABLED", False):
 | 
			
		||||
    from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -161,14 +161,28 @@ def convert_to_iso_duration(string: str) -> str:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def reload_nats():
 | 
			
		||||
    users = [{"user": "tacticalrmm", "password": settings.SECRET_KEY}]
 | 
			
		||||
    users = [
 | 
			
		||||
        {
 | 
			
		||||
            "user": "tacticalrmm",
 | 
			
		||||
            "password": settings.SECRET_KEY,
 | 
			
		||||
            "permissions": {"publish": ">", "subscribe": ">"},
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
    agents = Agent.objects.prefetch_related("user").only(
 | 
			
		||||
        "pk", "agent_id"
 | 
			
		||||
    )  # type:ignore
 | 
			
		||||
    for agent in agents:
 | 
			
		||||
        try:
 | 
			
		||||
            users.append(
 | 
			
		||||
                {"user": agent.agent_id, "password": agent.user.auth_token.key}
 | 
			
		||||
                {
 | 
			
		||||
                    "user": agent.agent_id,
 | 
			
		||||
                    "password": agent.user.auth_token.key,
 | 
			
		||||
                    "permissions": {
 | 
			
		||||
                        "publish": {"allow": agent.agent_id},
 | 
			
		||||
                        "subscribe": {"allow": agent.agent_id},
 | 
			
		||||
                        "allow_responses": True,
 | 
			
		||||
                    },
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        except:
 | 
			
		||||
            DebugLog.critical(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,65 +0,0 @@
 | 
			
		||||
trigger:
 | 
			
		||||
  - master
 | 
			
		||||
  - develop
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  - job: setup_env
 | 
			
		||||
    displayName: "Setup"
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        Debian10:
 | 
			
		||||
          AGENT_NAME: "az-pipeline-fran"
 | 
			
		||||
 | 
			
		||||
    pool:
 | 
			
		||||
      name: linux-vms
 | 
			
		||||
      demands:
 | 
			
		||||
        - agent.name -equals $(AGENT_NAME)
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - script: |
 | 
			
		||||
          sudo -u postgres psql -c 'DROP DATABASE IF EXISTS pipeline'
 | 
			
		||||
          sudo -u postgres psql -c 'DROP DATABASE IF EXISTS test_pipeline'
 | 
			
		||||
          sudo -u postgres psql -c 'CREATE DATABASE pipeline'
 | 
			
		||||
          sudo -u postgres psql -c "SET client_encoding = 'UTF8'" pipeline
 | 
			
		||||
          SETTINGS_FILE="/myagent/_work/1/s/api/tacticalrmm/tacticalrmm/settings.py"
 | 
			
		||||
          rm -rf /myagent/_work/1/s/api/env
 | 
			
		||||
          cd /myagent/_work/1/s/api
 | 
			
		||||
          python3.10 -m venv env
 | 
			
		||||
          source env/bin/activate
 | 
			
		||||
          cd /myagent/_work/1/s/api/tacticalrmm
 | 
			
		||||
          pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --upgrade pip
 | 
			
		||||
          SETUPTOOLS_VER=$(grep "^SETUPTOOLS_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
 | 
			
		||||
          WHEEL_VER=$(grep "^WHEEL_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
 | 
			
		||||
          pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org setuptools==${SETUPTOOLS_VER} wheel==${WHEEL_VER}
 | 
			
		||||
          pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org -r requirements.txt -r requirements-test.txt -r requirements-dev.txt
 | 
			
		||||
        displayName: "Install Python Dependencies"
 | 
			
		||||
 | 
			
		||||
      - script: |
 | 
			
		||||
          cd /myagent/_work/1/s/api
 | 
			
		||||
          source env/bin/activate
 | 
			
		||||
          cd /myagent/_work/1/s/api/tacticalrmm
 | 
			
		||||
          coverage run manage.py test -v 2
 | 
			
		||||
          if [ $? -ne 0 ]; then
 | 
			
		||||
              exit 1
 | 
			
		||||
          fi
 | 
			
		||||
        displayName: "Run django tests"
 | 
			
		||||
 | 
			
		||||
      - script: |
 | 
			
		||||
          cd /myagent/_work/1/s/api
 | 
			
		||||
          source env/bin/activate
 | 
			
		||||
          black --exclude migrations/ --check tacticalrmm
 | 
			
		||||
          if [ $? -ne 0 ]; then
 | 
			
		||||
              exit 1
 | 
			
		||||
          fi
 | 
			
		||||
        displayName: "Codestyle black"
 | 
			
		||||
 | 
			
		||||
      - script: |
 | 
			
		||||
          cd /myagent/_work/1/s/api
 | 
			
		||||
          source env/bin/activate
 | 
			
		||||
          cd /myagent/_work/1/s/api/tacticalrmm
 | 
			
		||||
          export CIRCLE_BRANCH=$BUILD_SOURCEBRANCH
 | 
			
		||||
          coveralls
 | 
			
		||||
        displayName: "coveralls"
 | 
			
		||||
        env:
 | 
			
		||||
          CIRCLECI: 1
 | 
			
		||||
          CIRCLE_BUILD_NUM: $(Build.BuildNumber)
 | 
			
		||||
@@ -129,11 +129,13 @@ processes = ${uwsgiprocs}
 | 
			
		||||
threads = ${uwsgiprocs}
 | 
			
		||||
enable-threads = true
 | 
			
		||||
socket = 0.0.0.0:8080
 | 
			
		||||
harakiri = 300
 | 
			
		||||
chmod-socket = 660
 | 
			
		||||
buffer-size = 65535
 | 
			
		||||
vacuum = true
 | 
			
		||||
die-on-term = true
 | 
			
		||||
max-requests = 2000
 | 
			
		||||
max-requests = 500
 | 
			
		||||
disable-logging = true
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
SCRIPT_VERSION="59"
 | 
			
		||||
SCRIPT_VERSION="61"
 | 
			
		||||
SCRIPT_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/install.sh'
 | 
			
		||||
 | 
			
		||||
sudo apt install -y curl wget dirmngr gnupg lsb-release
 | 
			
		||||
@@ -406,6 +406,7 @@ buffer-size = 65535
 | 
			
		||||
vacuum = true
 | 
			
		||||
die-on-term = true
 | 
			
		||||
max-requests = 500
 | 
			
		||||
disable-logging = true
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
echo "${uwsgini}" > /rmm/api/tacticalrmm/app.ini
 | 
			
		||||
@@ -657,7 +658,7 @@ CELERY_APP="tacticalrmm"
 | 
			
		||||
 | 
			
		||||
CELERYD_MULTI="multi"
 | 
			
		||||
 | 
			
		||||
CELERYD_OPTS="--time-limit=86400 --autoscale=50,3"
 | 
			
		||||
CELERYD_OPTS="--time-limit=86400 --autoscale=20,2"
 | 
			
		||||
 | 
			
		||||
CELERYD_PID_FILE="/rmm/api/tacticalrmm/%n.pid"
 | 
			
		||||
CELERYD_LOG_FILE="/var/log/celery/%n%I.log"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								main.go
									
									
									
									
									
								
							@@ -11,7 +11,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	version = "3.0.1"
 | 
			
		||||
	version = "3.0.2"
 | 
			
		||||
	log     = logrus.New()
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							@@ -13,4 +13,5 @@ type DjangoConfig struct {
 | 
			
		||||
	Host    string `json:"host"`
 | 
			
		||||
	Port    int    `json:"port"`
 | 
			
		||||
	DBName  string `json:"dbname"`
 | 
			
		||||
	SSLMode string `json:"sslmode"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -41,8 +41,8 @@ func GetConfig(cfg string) (db *sqlx.DB, r DjangoConfig, err error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	psqlInfo := fmt.Sprintf("host=%s port=%d user=%s "+
 | 
			
		||||
		"password=%s dbname=%s sslmode=disable",
 | 
			
		||||
		r.Host, r.Port, r.User, r.Pass, r.DBName)
 | 
			
		||||
		"password=%s dbname=%s sslmode=%s",
 | 
			
		||||
		r.Host, r.Port, r.User, r.Pass, r.DBName, r.SSLMode)
 | 
			
		||||
 | 
			
		||||
	db, err = sqlx.Connect("postgres", psqlInfo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
SCRIPT_VERSION="34"
 | 
			
		||||
SCRIPT_VERSION="35"
 | 
			
		||||
SCRIPT_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/restore.sh'
 | 
			
		||||
 | 
			
		||||
sudo apt update
 | 
			
		||||
@@ -276,6 +276,7 @@ buffer-size = 65535
 | 
			
		||||
vacuum = true
 | 
			
		||||
die-on-term = true
 | 
			
		||||
max-requests = 500
 | 
			
		||||
disable-logging = true
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
echo "${uwsgini}" > /rmm/api/tacticalrmm/app.ini
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
SCRIPT_VERSION="131"
 | 
			
		||||
SCRIPT_VERSION="133"
 | 
			
		||||
SCRIPT_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/update.sh'
 | 
			
		||||
LATEST_SETTINGS_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py'
 | 
			
		||||
YELLOW='\033[1;33m'
 | 
			
		||||
@@ -144,6 +144,7 @@ buffer-size = 65535
 | 
			
		||||
vacuum = true
 | 
			
		||||
die-on-term = true
 | 
			
		||||
max-requests = 500
 | 
			
		||||
disable-logging = true
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
echo "${uwsgini}" > /rmm/api/tacticalrmm/app.ini
 | 
			
		||||
@@ -257,6 +258,11 @@ sudo chown ${USER}:${USER} -R /etc/conf.d/
 | 
			
		||||
sudo chown ${USER}:${USER} -R /etc/letsencrypt
 | 
			
		||||
sudo chmod 775 -R /etc/letsencrypt
 | 
			
		||||
 | 
			
		||||
CHECK_CELERY_CONFIG=$(grep "autoscale=20,2" /etc/conf.d/celery.conf)
 | 
			
		||||
if ! [[ $CHECK_CELERY_CONFIG ]]; then
 | 
			
		||||
  sed -i 's/CELERYD_OPTS=.*/CELERYD_OPTS="--time-limit=86400 --autoscale=20,2"/g' /etc/conf.d/celery.conf
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
CHECK_ADMIN_ENABLED=$(grep ADMIN_ENABLED /rmm/api/tacticalrmm/tacticalrmm/local_settings.py)
 | 
			
		||||
if ! [[ $CHECK_ADMIN_ENABLED ]]; then
 | 
			
		||||
adminenabled="$(cat << EOF
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										985
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										985
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -10,12 +10,12 @@
 | 
			
		||||
    "test:e2e:ci": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress run\""
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@quasar/extras": "^1.13.3",
 | 
			
		||||
    "@quasar/extras": "^1.13.5",
 | 
			
		||||
    "apexcharts": "^3.33.2",
 | 
			
		||||
    "axios": "^0.26.1",
 | 
			
		||||
    "dotenv": "^16.0.0",
 | 
			
		||||
    "qrcode.vue": "^3.3.3",
 | 
			
		||||
    "quasar": "^2.6.1",
 | 
			
		||||
    "quasar": "^2.6.6",
 | 
			
		||||
    "vue": "^3.2.31",
 | 
			
		||||
    "vue3-ace-editor": "^2.2.2",
 | 
			
		||||
    "vue3-apexcharts": "^1.4.1",
 | 
			
		||||
@@ -23,7 +23,7 @@
 | 
			
		||||
    "vuex": "^4.0.2"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@quasar/app-webpack": "^3.4.5",
 | 
			
		||||
    "@quasar/app-webpack": "^3.5.0",
 | 
			
		||||
    "@quasar/cli": "^1.3.2"
 | 
			
		||||
  },
 | 
			
		||||
  "browserslist": [
 | 
			
		||||
 
 | 
			
		||||
@@ -304,7 +304,15 @@
 | 
			
		||||
                <q-card-section class="row">
 | 
			
		||||
                  <div class="col-4">Username:</div>
 | 
			
		||||
                  <div class="col-2"></div>
 | 
			
		||||
                  <q-input dense outlined v-model="settings.mesh_username" class="col-6" />
 | 
			
		||||
                  <q-input
 | 
			
		||||
                    dense
 | 
			
		||||
                    outlined
 | 
			
		||||
                    v-model="settings.mesh_username"
 | 
			
		||||
                    class="col-6"
 | 
			
		||||
                    :rules="[
 | 
			
		||||
                      val => (val == val.toLowerCase() && val != val.toUpperCase()) || 'Username must be all lowercase',
 | 
			
		||||
                    ]"
 | 
			
		||||
                  />
 | 
			
		||||
                </q-card-section>
 | 
			
		||||
                <q-card-section class="row">
 | 
			
		||||
                  <div class="col-4">Mesh Site:</div>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user