Compare commits
	
		
			117 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					b4b63826dc | ||
| 
						 | 
					45e2690a81 | ||
| 
						 | 
					2ff504db09 | ||
| 
						 | 
					0c89e58d8c | ||
| 
						 | 
					e1dc75e2d8 | ||
| 
						 | 
					6dd027c994 | ||
| 
						 | 
					263c1f1d75 | ||
| 
						 | 
					52ee688259 | ||
| 
						 | 
					ce4c3a74b5 | ||
| 
						 | 
					72e493bef0 | ||
| 
						 | 
					14903c888c | ||
| 
						 | 
					f2bdf0e9f1 | ||
| 
						 | 
					02871fdd66 | ||
| 
						 | 
					fd9f1bca8e | ||
| 
						 | 
					739181a7ec | ||
| 
						 | 
					a0c406251f | ||
| 
						 | 
					c420e063bd | ||
| 
						 | 
					24ba9fb598 | ||
| 
						 | 
					c872764541 | ||
| 
						 | 
					3e52924859 | ||
| 
						 | 
					51cc895d12 | ||
| 
						 | 
					0a1f33fede | ||
| 
						 | 
					b539df007b | ||
| 
						 | 
					1a6cb090fe | ||
| 
						 | 
					5e2602c3dc | ||
| 
						 | 
					8a388db603 | ||
| 
						 | 
					e0018871b1 | ||
| 
						 | 
					be508d9c9d | ||
| 
						 | 
					e670e67ef5 | ||
| 
						 | 
					32dae6e181 | ||
| 
						 | 
					0f0a7ed119 | ||
| 
						 | 
					e407a8c59e | ||
| 
						 | 
					6c4d95ebfd | ||
| 
						 | 
					7e54f2456e | ||
| 
						 | 
					2419179877 | ||
| 
						 | 
					58a120e5c8 | ||
| 
						 | 
					19315b6174 | ||
| 
						 | 
					b014f9afd9 | ||
| 
						 | 
					794e128504 | ||
| 
						 | 
					de25074861 | ||
| 
						 | 
					4da70dd23a | ||
| 
						 | 
					b840ee542a | ||
| 
						 | 
					a51939df32 | ||
| 
						 | 
					c3098f023a | ||
| 
						 | 
					857a744c74 | ||
| 
						 | 
					62fd3a207c | ||
| 
						 | 
					ae3acfbc98 | ||
| 
						 | 
					2bf32aeab5 | ||
| 
						 | 
					3642407de8 | ||
| 
						 | 
					f9333c5ffd | ||
| 
						 | 
					b2fb45fe16 | ||
| 
						 | 
					1864a4ea77 | ||
| 
						 | 
					c6e34dd900 | ||
| 
						 | 
					589b36d074 | ||
| 
						 | 
					575ef6fec7 | ||
| 
						 | 
					dd5c009d89 | ||
| 
						 | 
					3fa26a6b25 | ||
| 
						 | 
					1d14f5a8b6 | ||
| 
						 | 
					7f5d5db0ef | ||
| 
						 | 
					592909d890 | ||
| 
						 | 
					5113f42781 | ||
| 
						 | 
					60ddf07be9 | ||
| 
						 | 
					c8f1b1b247 | ||
| 
						 | 
					31f2807295 | ||
| 
						 | 
					08edca4fbf | ||
| 
						 | 
					cb2a740beb | ||
| 
						 | 
					e0f6f4f563 | ||
| 
						 | 
					34652110ca | ||
| 
						 | 
					d4d4bda519 | ||
| 
						 | 
					e83463a3cc | ||
| 
						 | 
					33216fd197 | ||
| 
						 | 
					b332332f79 | ||
| 
						 | 
					ff81f7a9d0 | ||
| 
						 | 
					b8379c4508 | ||
| 
						 | 
					0fbd3a59bd | ||
| 
						 | 
					b03d7b370f | ||
| 
						 | 
					8f1c694071 | ||
| 
						 | 
					789a8b0cf0 | ||
| 
						 | 
					c9dd02ace3 | ||
| 
						 | 
					ad5906c7b6 | ||
| 
						 | 
					e837c494cb | ||
| 
						 | 
					afc40fcbe3 | ||
| 
						 | 
					185f50787b | ||
| 
						 | 
					6c33676f73 | ||
| 
						 | 
					0290002444 | ||
| 
						 | 
					fc5195e817 | ||
| 
						 | 
					efd5c3dca1 | ||
| 
						 | 
					2f438feec2 | ||
| 
						 | 
					07ae9dfddf | ||
| 
						 | 
					64575c5f7d | ||
| 
						 | 
					e0fa339644 | ||
| 
						 | 
					b72a86e514 | ||
| 
						 | 
					62f0414afa | ||
| 
						 | 
					200a02b87b | ||
| 
						 | 
					da5dbeaf0f | ||
| 
						 | 
					4b6d099f72 | ||
| 
						 | 
					842661ada6 | ||
| 
						 | 
					f5148c87c8 | ||
| 
						 | 
					16164c0bbc | ||
| 
						 | 
					f38ddb840b | ||
| 
						 | 
					f86fe26ffe | ||
| 
						 | 
					162360bf45 | ||
| 
						 | 
					612aaa7880 | ||
| 
						 | 
					e91f3fe53d | ||
| 
						 | 
					f0fe4d64bc | ||
| 
						 | 
					07cc6aca6a | ||
| 
						 | 
					23bf81efbb | ||
| 
						 | 
					a55105e5ee | ||
| 
						 | 
					5832a426bc | ||
| 
						 | 
					38dc709108 | ||
| 
						 | 
					5696d3359b | ||
| 
						 | 
					1b4fa84753 | ||
| 
						 | 
					13f0f117da | ||
| 
						 | 
					2db4eeec05 | ||
| 
						 | 
					fe5e8aa5fe | ||
| 
						 | 
					13e35d24a2 | ||
| 
						 | 
					fe8d88497f | 
@@ -3,10 +3,9 @@ version: '3.7'
 | 
			
		||||
services:
 | 
			
		||||
  app-dev:
 | 
			
		||||
    container_name: trmm-app-dev
 | 
			
		||||
    image: node:18-alpine
 | 
			
		||||
    image: node:20-alpine
 | 
			
		||||
    restart: always
 | 
			
		||||
    command: /bin/sh -c "npm install --cache ~/.npm && npm run serve"
 | 
			
		||||
    user: 1000:1000
 | 
			
		||||
    command: /bin/sh -c "npm install --cache ~/.npm && npm i -g @quasar/cli && npm run serve"
 | 
			
		||||
    working_dir: /workspace/web
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ..:/workspace:cached
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								.github/workflows/build-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/build-release.yml
									
									
									
									
										vendored
									
									
								
							@@ -11,11 +11,11 @@ jobs:
 | 
			
		||||
    name: Build web
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      - uses: actions/setup-node@v3
 | 
			
		||||
      - uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 18
 | 
			
		||||
          node-version: "20.11.1"
 | 
			
		||||
 | 
			
		||||
      - run: touch env-config.js
 | 
			
		||||
 | 
			
		||||
@@ -29,6 +29,6 @@ jobs:
 | 
			
		||||
        run: tar -czvf trmm-web-${{github.ref_name}}.tar.gz dist/
 | 
			
		||||
 | 
			
		||||
      - name: Release
 | 
			
		||||
        uses: softprops/action-gh-release@v1
 | 
			
		||||
        uses: softprops/action-gh-release@v2
 | 
			
		||||
        with:
 | 
			
		||||
          files: trmm-web-${{github.ref_name}}.tar.gz
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3102
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3102
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										52
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								package.json
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "web",
 | 
			
		||||
  "version": "0.101.39",
 | 
			
		||||
  "version": "0.101.47",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "productName": "Tactical RMM",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
@@ -10,34 +10,38 @@
 | 
			
		||||
    "format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@quasar/extras": "1.16.9",
 | 
			
		||||
    "apexcharts": "3.45.2",
 | 
			
		||||
    "axios": "1.6.7",
 | 
			
		||||
    "dotenv": "16.4.1",
 | 
			
		||||
    "qrcode.vue": "3.4.1",
 | 
			
		||||
    "quasar": "2.14.3",
 | 
			
		||||
    "vue": "3.4.15",
 | 
			
		||||
    "vue3-apexcharts": "1.4.4",
 | 
			
		||||
    "@quasar/extras": "1.16.12",
 | 
			
		||||
    "@vueuse/core": "10.11.0",
 | 
			
		||||
    "@vueuse/integrations": "10.11.0",
 | 
			
		||||
    "@vueuse/shared": "10.11.0",
 | 
			
		||||
    "apexcharts": "3.49.2",
 | 
			
		||||
    "axios": "1.7.2",
 | 
			
		||||
    "dotenv": "16.4.5",
 | 
			
		||||
    "monaco-editor": "0.50.0",
 | 
			
		||||
    "pinia": "2.1.7",
 | 
			
		||||
    "qrcode": "1.5.3",
 | 
			
		||||
    "quasar": "2.16.6",
 | 
			
		||||
    "vue": "3.4.31",
 | 
			
		||||
    "vue-router": "4.4.0",
 | 
			
		||||
    "vue3-apexcharts": "1.5.3",
 | 
			
		||||
    "vuedraggable": "4.1.0",
 | 
			
		||||
    "vue-router": "4.2.5",
 | 
			
		||||
    "@vueuse/core": "10.7.2",
 | 
			
		||||
    "@vueuse/shared": "10.7.2",
 | 
			
		||||
    "monaco-editor": "0.45.0",
 | 
			
		||||
    "vuex": "4.1.0",
 | 
			
		||||
    "yaml": "2.3.4"
 | 
			
		||||
    "@xterm/xterm": "5.5.0",
 | 
			
		||||
    "@xterm/addon-fit": "0.10.0",
 | 
			
		||||
    "yaml": "2.4.5"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@quasar/cli": "2.3.0",
 | 
			
		||||
    "@intlify/unplugin-vue-i18n": "2.0.0",
 | 
			
		||||
    "@quasar/app-vite": "1.7.3",
 | 
			
		||||
    "@types/node": "20.11.6",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "6.19.1",
 | 
			
		||||
    "@typescript-eslint/parser": "6.19.1",
 | 
			
		||||
    "autoprefixer": "10.4.17",
 | 
			
		||||
    "eslint": "8.56.0",
 | 
			
		||||
    "@intlify/unplugin-vue-i18n": "4.0.0",
 | 
			
		||||
    "@quasar/app-vite": "1.9.3",
 | 
			
		||||
    "@quasar/cli": "2.4.1",
 | 
			
		||||
    "@types/node": "20.14.10",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "7.16.0",
 | 
			
		||||
    "@typescript-eslint/parser": "7.16.0",
 | 
			
		||||
    "autoprefixer": "10.4.19",
 | 
			
		||||
    "eslint": "8.57.0",
 | 
			
		||||
    "eslint-config-prettier": "9.1.0",
 | 
			
		||||
    "eslint-plugin-vue": "8.7.1",
 | 
			
		||||
    "prettier": "3.2.4",
 | 
			
		||||
    "typescript": "5.3.3"
 | 
			
		||||
    "prettier": "3.2.5",
 | 
			
		||||
    "typescript": "5.5.3"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -29,15 +29,15 @@ module.exports = configure(function (/* ctx */) {
 | 
			
		||||
    // app boot file (/src/boot)
 | 
			
		||||
    // --> boot files are part of "main.js"
 | 
			
		||||
    // https://v2.quasar.dev/quasar-cli-vite/boot-files
 | 
			
		||||
    boot: ["axios", "monaco", "integrations"],
 | 
			
		||||
    boot: ["pinia", "axios", "monaco", "integrations"],
 | 
			
		||||
 | 
			
		||||
    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
 | 
			
		||||
    css: ["app.sass"],
 | 
			
		||||
 | 
			
		||||
    // https://github.com/quasarframework/quasar/tree/dev/extras
 | 
			
		||||
    extras: [
 | 
			
		||||
      // 'ionicons-v4',
 | 
			
		||||
      "mdi-v5",
 | 
			
		||||
      "ionicons-v4",
 | 
			
		||||
      "mdi-v7",
 | 
			
		||||
      "fontawesome-v6",
 | 
			
		||||
      // 'eva-icons',
 | 
			
		||||
      // 'themify',
 | 
			
		||||
@@ -51,8 +51,8 @@ module.exports = configure(function (/* ctx */) {
 | 
			
		||||
    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
 | 
			
		||||
    build: {
 | 
			
		||||
      target: {
 | 
			
		||||
        browser: ["es2021"],
 | 
			
		||||
        node: "node16",
 | 
			
		||||
        browser: ["es2022"],
 | 
			
		||||
        node: "node20",
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      vueRouterMode: "history", // available values: 'hash', 'history'
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ export function openAgentWindow(agent_id) {
 | 
			
		||||
 | 
			
		||||
export function runRemoteBackground(agent_id, agentPlatform) {
 | 
			
		||||
  const url = router.resolve(
 | 
			
		||||
    `/remotebackground/${agent_id}?agentPlatform=${agentPlatform}`
 | 
			
		||||
    `/remotebackground/${agent_id}?agentPlatform=${agentPlatform}`,
 | 
			
		||||
  ).href;
 | 
			
		||||
  openURL(url, null, {
 | 
			
		||||
    popup: true,
 | 
			
		||||
@@ -129,7 +129,7 @@ export async function refreshAgentWMI(agent_id) {
 | 
			
		||||
export async function runScript(agent_id, payload) {
 | 
			
		||||
  const { data } = await axios.post(
 | 
			
		||||
    `${baseUrl}/${agent_id}/runscript/`,
 | 
			
		||||
    payload
 | 
			
		||||
    payload,
 | 
			
		||||
  );
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
@@ -153,7 +153,7 @@ export async function fetchAgentProcesses(agent_id, params = {}) {
 | 
			
		||||
export async function killAgentProcess(agent_id, pid, params = {}) {
 | 
			
		||||
  const { data } = await axios.delete(
 | 
			
		||||
    `${baseUrl}/${agent_id}/processes/${pid}/`,
 | 
			
		||||
    { params: params }
 | 
			
		||||
    { params: params },
 | 
			
		||||
  );
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
@@ -162,7 +162,7 @@ export async function fetchAgentEventLog(agent_id, logType, days, params = {}) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios.get(
 | 
			
		||||
      `${baseUrl}/${agent_id}/eventlog/${logType}/${days}/`,
 | 
			
		||||
      { params: params }
 | 
			
		||||
      { params: params },
 | 
			
		||||
    );
 | 
			
		||||
    return data;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
@@ -191,10 +191,15 @@ export async function agentRebootNow(agent_id) {
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function agentShutdown(agent_id) {
 | 
			
		||||
  const { data } = await axios.post(`${baseUrl}/${agent_id}/shutdown/`);
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function sendAgentRecoverMesh(agent_id, params = {}) {
 | 
			
		||||
  const { data } = await axios.post(
 | 
			
		||||
    `${baseUrl}/${agent_id}/meshcentral/recover/`,
 | 
			
		||||
    { params: params }
 | 
			
		||||
    { params: params },
 | 
			
		||||
  );
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								src/api/alerts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/api/alerts.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
 | 
			
		||||
import type { AlertTemplate } from "@/types/alerts";
 | 
			
		||||
 | 
			
		||||
export async function saveAlertTemplate(id: number, payload: AlertTemplate) {
 | 
			
		||||
  const { data } = await axios.put(`alerts/templates/${id}/`, payload);
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function addAlertTemplate(payload: AlertTemplate) {
 | 
			
		||||
  const { data } = await axios.post("alerts/templates/", payload);
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,45 +0,0 @@
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
import { openURL } from "quasar";
 | 
			
		||||
 | 
			
		||||
const baseUrl = "/core";
 | 
			
		||||
 | 
			
		||||
export async function fetchCustomFields(params = {}) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios.get(`${baseUrl}/customfields/`, {
 | 
			
		||||
      params: params,
 | 
			
		||||
    });
 | 
			
		||||
    return data;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error(e);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function fetchDashboardInfo(params = {}) {
 | 
			
		||||
  const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params });
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function fetchURLActions(params = {}) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios.get(`${baseUrl}/urlaction/`, {
 | 
			
		||||
      params: params,
 | 
			
		||||
    });
 | 
			
		||||
    return data;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error(e);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function runURLAction(payload) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios.patch(`${baseUrl}/urlaction/run/`, payload);
 | 
			
		||||
    openURL(data);
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error(e);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function generateScript(payload) {
 | 
			
		||||
  const { data } = await axios.post(`${baseUrl}/openai/generate/`, payload);
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										97
									
								
								src/api/core.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								src/api/core.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
import { openURL } from "quasar";
 | 
			
		||||
import { router } from "@/router";
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  URLAction,
 | 
			
		||||
  TestRunURLActionRequest,
 | 
			
		||||
  TestRunURLActionResponse,
 | 
			
		||||
} from "@/types/core/urlactions";
 | 
			
		||||
 | 
			
		||||
const baseUrl = "/core";
 | 
			
		||||
 | 
			
		||||
export async function fetchDashboardInfo(params = {}) {
 | 
			
		||||
  const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params });
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function fetchCustomFields(params = {}) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios.get(`${baseUrl}/customfields/`, {
 | 
			
		||||
      params: params,
 | 
			
		||||
    });
 | 
			
		||||
    return data;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error(e);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function fetchURLActions(params = {}): Promise<URLAction[]> {
 | 
			
		||||
  const { data } = await axios.get(`${baseUrl}/urlaction/`, {
 | 
			
		||||
    params: params,
 | 
			
		||||
  });
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function saveURLAction(action: URLAction) {
 | 
			
		||||
  const { data } = await axios.post(`${baseUrl}/urlaction/`, action);
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function editURLAction(id: number, action: URLAction) {
 | 
			
		||||
  const { data } = await axios.put(`${baseUrl}/urlaction/${id}/`, action);
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function removeURLAction(id: number) {
 | 
			
		||||
  const { data } = await axios.delete(`${baseUrl}/urlaction/${id}/`);
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface RunURLActionRequest {
 | 
			
		||||
  agent_id?: string;
 | 
			
		||||
  client?: number;
 | 
			
		||||
  site?: number;
 | 
			
		||||
  action: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function runURLAction(payload: RunURLActionRequest) {
 | 
			
		||||
  const { data } = await axios.patch(`${baseUrl}/urlaction/run/`, payload);
 | 
			
		||||
  openURL(data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function runTestURLAction(
 | 
			
		||||
  payload: TestRunURLActionRequest,
 | 
			
		||||
): Promise<TestRunURLActionResponse> {
 | 
			
		||||
  const { data } = await axios.post(`${baseUrl}/urlaction/run/test/`, payload);
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function checkWebTermPerms(): Promise<{
 | 
			
		||||
  message: string;
 | 
			
		||||
  status: number;
 | 
			
		||||
}> {
 | 
			
		||||
  const ret = await axios.post(`${baseUrl}/webtermperms/`);
 | 
			
		||||
  return { message: ret.data, status: ret.status };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function openWebTerminal(): void {
 | 
			
		||||
  const url: string = router.resolve("/webterm").href;
 | 
			
		||||
  openURL(url, undefined, {
 | 
			
		||||
    popup: true,
 | 
			
		||||
    scrollbars: false,
 | 
			
		||||
    location: false,
 | 
			
		||||
    status: false,
 | 
			
		||||
    toolbar: false,
 | 
			
		||||
    menubar: false,
 | 
			
		||||
    width: 1280,
 | 
			
		||||
    height: 720,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: Build out type for openai payload
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
export async function generateScript(payload: any) {
 | 
			
		||||
  const { data } = await axios.post(`${baseUrl}/openai/generate/`, payload);
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
@@ -13,6 +13,11 @@ export async function testScript(agent_id, payload) {
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function testScriptOnServer(payload) {
 | 
			
		||||
  const { data } = await axios.post("core/serverscript/test/", payload);
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function saveScript(payload) {
 | 
			
		||||
  const { data } = await axios.post(`${baseUrl}/`, payload);
 | 
			
		||||
  return data;
 | 
			
		||||
@@ -56,7 +61,7 @@ export async function fetchScriptSnippet(id, params = {}) {
 | 
			
		||||
export async function editScriptSnippet(payload) {
 | 
			
		||||
  const { data } = await axios.put(
 | 
			
		||||
    `${baseUrl}/snippets/${payload.id}/`,
 | 
			
		||||
    payload
 | 
			
		||||
    payload,
 | 
			
		||||
  );
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
import { useAuthStore } from "@/stores/auth";
 | 
			
		||||
import { Notify } from "quasar";
 | 
			
		||||
 | 
			
		||||
export const getBaseUrl = () => {
 | 
			
		||||
@@ -18,27 +19,22 @@ export function setErrorMessage(data, message) {
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function ({ app, router, store }) {
 | 
			
		||||
export default function ({ app, router }) {
 | 
			
		||||
  app.config.globalProperties.$axios = axios;
 | 
			
		||||
 | 
			
		||||
  axios.interceptors.request.use(
 | 
			
		||||
    function (config) {
 | 
			
		||||
      const auth = useAuthStore();
 | 
			
		||||
      config.baseURL = getBaseUrl();
 | 
			
		||||
      const token = store.state.token;
 | 
			
		||||
      const token = auth.token;
 | 
			
		||||
      if (token != null) {
 | 
			
		||||
        config.headers.Authorization = `Token ${token}`;
 | 
			
		||||
      }
 | 
			
		||||
      // config.transformResponse = [
 | 
			
		||||
      //   function (data) {
 | 
			
		||||
      //     console.log(data);
 | 
			
		||||
      //     return data;
 | 
			
		||||
      //   },
 | 
			
		||||
      // ];
 | 
			
		||||
      return config;
 | 
			
		||||
    },
 | 
			
		||||
    function (err) {
 | 
			
		||||
      return Promise.reject(err);
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  axios.interceptors.response.use(
 | 
			
		||||
@@ -101,6 +97,6 @@ export default function ({ app, router, store }) {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return Promise.reject({ ...error });
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								src/boot/pinia.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/boot/pinia.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
import { boot } from "quasar/wrappers";
 | 
			
		||||
import { createPinia } from "pinia";
 | 
			
		||||
 | 
			
		||||
export default boot(({ app }) => {
 | 
			
		||||
  const pinia = createPinia();
 | 
			
		||||
 | 
			
		||||
  app.use(pinia);
 | 
			
		||||
 | 
			
		||||
  // You can add Pinia plugins here
 | 
			
		||||
  // pinia.use(SomePiniaPlugin)
 | 
			
		||||
});
 | 
			
		||||
@@ -170,7 +170,7 @@
 | 
			
		||||
                overdueAlert(
 | 
			
		||||
                  'dashboard',
 | 
			
		||||
                  props.row,
 | 
			
		||||
                  props.row.overdue_dashboard_alert
 | 
			
		||||
                  props.row.overdue_dashboard_alert,
 | 
			
		||||
                )
 | 
			
		||||
              "
 | 
			
		||||
              v-model="props.row.overdue_dashboard_alert"
 | 
			
		||||
@@ -431,8 +431,8 @@ export default {
 | 
			
		||||
            return false;
 | 
			
		||||
          else if (availability === "expired") {
 | 
			
		||||
            let now = new Date();
 | 
			
		||||
            let lastSeen = date.extractDate(row.last_seen, "MM DD YYYY HH:mm");
 | 
			
		||||
            let diff = date.getDateDiff(now, lastSeen, "days");
 | 
			
		||||
            let last_seen = new Date(row.last_seen);
 | 
			
		||||
            let diff = date.getDateDiff(now, last_seen, "days");
 | 
			
		||||
            if (diff < 30) return false;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -278,7 +278,7 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: "resolved_action_name",
 | 
			
		||||
          label: "Resolve Action",
 | 
			
		||||
          label: "Resolved Action",
 | 
			
		||||
          field: "resolved_action_name",
 | 
			
		||||
          align: "left",
 | 
			
		||||
        },
 | 
			
		||||
@@ -326,7 +326,7 @@ export default {
 | 
			
		||||
              this.refresh();
 | 
			
		||||
              this.$q.loading.hide();
 | 
			
		||||
              this.notifySuccess(
 | 
			
		||||
                `Alert template ${template.name} was deleted!`
 | 
			
		||||
                `Alert template ${template.name} was deleted!`,
 | 
			
		||||
              );
 | 
			
		||||
            })
 | 
			
		||||
            .catch(() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -107,7 +107,7 @@
 | 
			
		||||
              />
 | 
			
		||||
              <q-checkbox
 | 
			
		||||
                v-model="localRole.can_reboot_agents"
 | 
			
		||||
                label="Reboot Agents"
 | 
			
		||||
                label="Shutdown / Reboot Agents"
 | 
			
		||||
              />
 | 
			
		||||
              <q-checkbox
 | 
			
		||||
                v-model="localRole.can_send_wol"
 | 
			
		||||
@@ -179,6 +179,11 @@
 | 
			
		||||
                v-model="localRole.can_manage_customfields"
 | 
			
		||||
                label="Edit Custom Fields"
 | 
			
		||||
              />
 | 
			
		||||
              <q-checkbox
 | 
			
		||||
                v-if="!hosted"
 | 
			
		||||
                v-model="localRole.can_use_webterm"
 | 
			
		||||
                label="Use TRMM Server Web Terminal"
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </q-card-section>
 | 
			
		||||
 | 
			
		||||
@@ -328,6 +333,11 @@
 | 
			
		||||
                v-model="localRole.can_manage_scripts"
 | 
			
		||||
                label="Manage Scripts"
 | 
			
		||||
              />
 | 
			
		||||
              <q-checkbox
 | 
			
		||||
                v-if="!hosted"
 | 
			
		||||
                v-model="localRole.can_run_server_scripts"
 | 
			
		||||
                label="Run Scripts on TRMM Server"
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </q-card-section>
 | 
			
		||||
 | 
			
		||||
@@ -409,7 +419,8 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
// composition imports
 | 
			
		||||
import { ref, watch } from "vue";
 | 
			
		||||
import { computed, ref, watch } from "vue";
 | 
			
		||||
import { useStore } from "vuex";
 | 
			
		||||
import { useDialogPluginComponent } from "quasar";
 | 
			
		||||
import { saveRole, editRole } from "@/api/accounts";
 | 
			
		||||
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
 | 
			
		||||
@@ -427,6 +438,10 @@ export default {
 | 
			
		||||
    // quasar setup
 | 
			
		||||
    const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
			
		||||
 | 
			
		||||
    // store
 | 
			
		||||
    const store = useStore();
 | 
			
		||||
    const hosted = computed(() => store.state.hosted);
 | 
			
		||||
 | 
			
		||||
    // dropdown setup
 | 
			
		||||
    const { clientOptions } = useClientDropdown(true);
 | 
			
		||||
    const { siteOptions } = useSiteDropdown(true);
 | 
			
		||||
@@ -511,6 +526,9 @@ export default {
 | 
			
		||||
          can_manage_roles: false,
 | 
			
		||||
          can_view_clients: [],
 | 
			
		||||
          can_view_sites: [],
 | 
			
		||||
          // server scripts and web terminal
 | 
			
		||||
          can_run_server_scripts: false,
 | 
			
		||||
          can_use_webterm: false,
 | 
			
		||||
          // reporting perms
 | 
			
		||||
          can_view_reports: false,
 | 
			
		||||
          can_manage_reports: false,
 | 
			
		||||
@@ -550,6 +568,7 @@ export default {
 | 
			
		||||
      loading,
 | 
			
		||||
      clientOptions,
 | 
			
		||||
      siteOptions,
 | 
			
		||||
      hosted,
 | 
			
		||||
 | 
			
		||||
      onSubmit,
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -176,6 +176,13 @@
 | 
			
		||||
      </q-menu>
 | 
			
		||||
    </q-item>
 | 
			
		||||
 | 
			
		||||
    <q-item clickable v-close-popup @click="shutdown(agent)">
 | 
			
		||||
      <q-item-section side>
 | 
			
		||||
        <q-icon size="xs" name="power" />
 | 
			
		||||
      </q-item-section>
 | 
			
		||||
      <q-item-section>Shutdown</q-item-section>
 | 
			
		||||
    </q-item>
 | 
			
		||||
 | 
			
		||||
    <q-item clickable v-close-popup @click="showPolicyAdd(agent)">
 | 
			
		||||
      <q-item-section side>
 | 
			
		||||
        <q-icon size="xs" name="policy" />
 | 
			
		||||
@@ -192,9 +199,9 @@
 | 
			
		||||
      "
 | 
			
		||||
    >
 | 
			
		||||
      <q-item-section side>
 | 
			
		||||
        <q-icon size="xs" name="integration_instructions" />
 | 
			
		||||
        <q-icon size="xs" name="analytics" />
 | 
			
		||||
      </q-item-section>
 | 
			
		||||
      <q-item-section>Integrations</q-item-section>
 | 
			
		||||
      <q-item-section>Reporting</q-item-section>
 | 
			
		||||
      <q-item-section side>
 | 
			
		||||
        <q-icon name="keyboard_arrow_right" />
 | 
			
		||||
      </q-item-section>
 | 
			
		||||
@@ -231,6 +238,7 @@ import { fetchURLActions, runURLAction } from "@/api/core";
 | 
			
		||||
import {
 | 
			
		||||
  editAgent,
 | 
			
		||||
  agentRebootNow,
 | 
			
		||||
  agentShutdown,
 | 
			
		||||
  sendAgentPing,
 | 
			
		||||
  removeAgent,
 | 
			
		||||
  runRemoteBackground,
 | 
			
		||||
@@ -294,11 +302,13 @@ export default {
 | 
			
		||||
    async function getURLActions() {
 | 
			
		||||
      menuLoading.value = true;
 | 
			
		||||
      try {
 | 
			
		||||
        urlActions.value = await fetchURLActions();
 | 
			
		||||
        urlActions.value = (await fetchURLActions()).filter(
 | 
			
		||||
          (action) => action.action_type === "web",
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (urlActions.value.length === 0) {
 | 
			
		||||
          notifyWarning(
 | 
			
		||||
            "No URL Actions configured. Go to Settings > Global Settings > URL Actions"
 | 
			
		||||
            "No URL Actions configured. Go to Settings > Global Settings > URL Actions",
 | 
			
		||||
          );
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -364,7 +374,7 @@ export default {
 | 
			
		||||
        notifySuccess(
 | 
			
		||||
          `Maintenance mode was ${
 | 
			
		||||
            agent.maintenance_mode ? "disabled" : "enabled"
 | 
			
		||||
          } on ${agent.hostname}`
 | 
			
		||||
          } on ${agent.hostname}`,
 | 
			
		||||
        );
 | 
			
		||||
        store.commit("setRefreshSummaryTab", true);
 | 
			
		||||
        refreshDashboard();
 | 
			
		||||
@@ -437,6 +447,32 @@ export default {
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function shutdown(agent) {
 | 
			
		||||
      $q.dialog({
 | 
			
		||||
        title:
 | 
			
		||||
          'Please type <code style="color:red">yes</code> in the box below to confirm shutdown.',
 | 
			
		||||
        prompt: {
 | 
			
		||||
          model: "",
 | 
			
		||||
          type: "text",
 | 
			
		||||
          isValid: (val) => val === "yes",
 | 
			
		||||
        },
 | 
			
		||||
        cancel: true,
 | 
			
		||||
        ok: { label: "Shutdown", color: "negative" },
 | 
			
		||||
        persistent: true,
 | 
			
		||||
        html: true,
 | 
			
		||||
      }).onOk(async () => {
 | 
			
		||||
        $q.loading.show();
 | 
			
		||||
        try {
 | 
			
		||||
          await agentShutdown(agent.agent_id);
 | 
			
		||||
          notifySuccess(`${agent.hostname} will now be shutdown`);
 | 
			
		||||
          $q.loading.hide();
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          $q.loading.hide();
 | 
			
		||||
          console.error(e);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showPolicyAdd(agent) {
 | 
			
		||||
      $q.dialog({
 | 
			
		||||
        component: PolicyAdd,
 | 
			
		||||
@@ -505,7 +541,7 @@ export default {
 | 
			
		||||
          notifySuccess(data);
 | 
			
		||||
          refreshDashboard(
 | 
			
		||||
            false /* clearTreeSelected */,
 | 
			
		||||
            true /* clearSubTable */
 | 
			
		||||
            true /* clearSubTable */,
 | 
			
		||||
          );
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          console.error(e);
 | 
			
		||||
@@ -534,6 +570,7 @@ export default {
 | 
			
		||||
      runChecks,
 | 
			
		||||
      showRebootLaterModal,
 | 
			
		||||
      rebootNow,
 | 
			
		||||
      shutdown,
 | 
			
		||||
      showPolicyAdd,
 | 
			
		||||
      showAgentRecovery,
 | 
			
		||||
      pingAgent,
 | 
			
		||||
 
 | 
			
		||||
@@ -441,7 +441,7 @@ export default {
 | 
			
		||||
      try {
 | 
			
		||||
        const result = await fetchAgentTasks(selectedAgent.value);
 | 
			
		||||
        tasks.value = result.filter(
 | 
			
		||||
          (task) => task.sync_status !== "pendingdeletion"
 | 
			
		||||
          (task) => task.sync_status !== "pendingdeletion",
 | 
			
		||||
        );
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        console.error(e);
 | 
			
		||||
@@ -495,7 +495,7 @@ export default {
 | 
			
		||||
      try {
 | 
			
		||||
        const result = await runTask(
 | 
			
		||||
          task.id,
 | 
			
		||||
          task.policy ? { agent_id: selectedAgent.value } : {}
 | 
			
		||||
          task.policy ? { agent_id: selectedAgent.value } : {},
 | 
			
		||||
        );
 | 
			
		||||
        notifySuccess(result);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
 
 | 
			
		||||
@@ -666,6 +666,7 @@ export default {
 | 
			
		||||
        componentProps: {
 | 
			
		||||
          check: check,
 | 
			
		||||
          parent: !check ? { agent: selectedAgent.value } : undefined,
 | 
			
		||||
          plat: type === "script" ? agentPlatform.value : undefined,
 | 
			
		||||
        },
 | 
			
		||||
      }).onOk(getChecks);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
			
		||||
        </q-btn>
 | 
			
		||||
      </q-bar>
 | 
			
		||||
      <q-card-section v-if="scriptOptions.length === 0">
 | 
			
		||||
      <q-card-section v-if="filterByPlatformOptions.length === 0">
 | 
			
		||||
        <p>You need to upload a script first</p>
 | 
			
		||||
        <p>Settings -> Script Manager</p>
 | 
			
		||||
      </q-card-section>
 | 
			
		||||
@@ -19,7 +19,7 @@
 | 
			
		||||
            :rules="[(val) => !!val || '*Required']"
 | 
			
		||||
            outlined
 | 
			
		||||
            v-model="state.script"
 | 
			
		||||
            :options="scriptOptions"
 | 
			
		||||
            :options="filterByPlatformOptions"
 | 
			
		||||
            label="Select script"
 | 
			
		||||
            mapOptions
 | 
			
		||||
            :disable="!!check"
 | 
			
		||||
@@ -140,6 +140,7 @@ export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    check: Object,
 | 
			
		||||
    parent: Object, // {agent: agent.agent_id} or {policy: policy.id}
 | 
			
		||||
    plat: String,
 | 
			
		||||
  },
 | 
			
		||||
  setup(props) {
 | 
			
		||||
    // setup quasar dialog
 | 
			
		||||
@@ -148,11 +149,13 @@ export default {
 | 
			
		||||
    // setup script dropdown
 | 
			
		||||
    const {
 | 
			
		||||
      script,
 | 
			
		||||
      scriptOptions,
 | 
			
		||||
      filterByPlatformOptions,
 | 
			
		||||
      defaultTimeout,
 | 
			
		||||
      defaultArgs,
 | 
			
		||||
      defaultEnvVars,
 | 
			
		||||
    } = useScriptDropdown(props.check ? props.check.script : undefined, {
 | 
			
		||||
    } = useScriptDropdown({
 | 
			
		||||
      script: props.check ? props.check.script : undefined,
 | 
			
		||||
      plat: props.plat,
 | 
			
		||||
      onMount: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -181,7 +184,7 @@ export default {
 | 
			
		||||
 | 
			
		||||
      // non-reactive data
 | 
			
		||||
      failOptions,
 | 
			
		||||
      scriptOptions,
 | 
			
		||||
      filterByPlatformOptions,
 | 
			
		||||
      severityOptions,
 | 
			
		||||
      envVarsLabel,
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -20,12 +20,18 @@
 | 
			
		||||
        </div>
 | 
			
		||||
        <br />
 | 
			
		||||
        <div v-if="scriptInfo.stdout">
 | 
			
		||||
          Standard Output
 | 
			
		||||
          <script-output-copy-clip
 | 
			
		||||
            label="Standard Output"
 | 
			
		||||
            :data="scriptInfo.stdout"
 | 
			
		||||
          />
 | 
			
		||||
          <q-separator />
 | 
			
		||||
          <pre>{{ scriptInfo.stdout }}</pre>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="scriptInfo.stderr">
 | 
			
		||||
          Standard Error
 | 
			
		||||
          <script-output-copy-clip
 | 
			
		||||
            label="Standard Error"
 | 
			
		||||
            :data="scriptInfo.stderr"
 | 
			
		||||
          />
 | 
			
		||||
          <q-separator />
 | 
			
		||||
          <pre>{{ scriptInfo.stderr }}</pre>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -43,8 +49,13 @@ import { computed } from "vue";
 | 
			
		||||
import { useStore } from "vuex";
 | 
			
		||||
import { useDialogPluginComponent } from "quasar";
 | 
			
		||||
 | 
			
		||||
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "ScriptOutput",
 | 
			
		||||
  components: {
 | 
			
		||||
    ScriptOutputCopyClip,
 | 
			
		||||
  },
 | 
			
		||||
  emits: [...useDialogPluginComponent.emits],
 | 
			
		||||
  props: { scriptInfo: !Object },
 | 
			
		||||
  setup() {
 | 
			
		||||
 
 | 
			
		||||
@@ -83,7 +83,7 @@
 | 
			
		||||
          <tactical-dropdown
 | 
			
		||||
            :rules="[(val) => !!val || '*Required']"
 | 
			
		||||
            v-model="state.script"
 | 
			
		||||
            :options="filteredScriptOptions"
 | 
			
		||||
            :options="filterByPlatformOptions"
 | 
			
		||||
            label="Select Script"
 | 
			
		||||
            outlined
 | 
			
		||||
            mapOptions
 | 
			
		||||
@@ -210,8 +210,14 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
// composition imports
 | 
			
		||||
import { ref, computed, watch, onMounted } from "vue";
 | 
			
		||||
import { useStore } from "vuex";
 | 
			
		||||
import {
 | 
			
		||||
  ref,
 | 
			
		||||
  reactive,
 | 
			
		||||
  computed,
 | 
			
		||||
  watch,
 | 
			
		||||
  onMounted,
 | 
			
		||||
  defineComponent,
 | 
			
		||||
} from "vue";
 | 
			
		||||
import { useDialogPluginComponent } from "quasar";
 | 
			
		||||
import { useScriptDropdown } from "@/composables/scripts";
 | 
			
		||||
import { useAgentDropdown } from "@/composables/agents";
 | 
			
		||||
@@ -219,7 +225,6 @@ import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
 | 
			
		||||
import { runBulkAction } from "@/api/agents";
 | 
			
		||||
import { notifySuccess } from "@/utils/notify";
 | 
			
		||||
import { cmdPlaceholder } from "@/composables/agents";
 | 
			
		||||
import { removeExtraOptionCategories } from "@/utils/format";
 | 
			
		||||
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
 | 
			
		||||
 | 
			
		||||
// ui imports
 | 
			
		||||
@@ -251,7 +256,7 @@ const patchModeOptions = [
 | 
			
		||||
  { label: "Install", value: "install" },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: "BulkAction",
 | 
			
		||||
  components: { TacticalDropdown },
 | 
			
		||||
  emits: [...useDialogPluginComponent.emits],
 | 
			
		||||
@@ -259,14 +264,8 @@ export default {
 | 
			
		||||
    mode: !String,
 | 
			
		||||
  },
 | 
			
		||||
  setup(props) {
 | 
			
		||||
    // setup vuex store
 | 
			
		||||
    const store = useStore();
 | 
			
		||||
    const showCommunityScripts = computed(
 | 
			
		||||
      () => store.state.showCommunityScripts
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const shellOptions = computed(() => {
 | 
			
		||||
      if (state.value.osType === "windows") {
 | 
			
		||||
      if (state.osType === "windows") {
 | 
			
		||||
        return [
 | 
			
		||||
          { label: "CMD", value: "cmd" },
 | 
			
		||||
          { label: "Powershell", value: "powershell" },
 | 
			
		||||
@@ -293,7 +292,8 @@ export default {
 | 
			
		||||
    // dropdown setup
 | 
			
		||||
    const {
 | 
			
		||||
      script,
 | 
			
		||||
      scriptOptions,
 | 
			
		||||
      plat,
 | 
			
		||||
      filterByPlatformOptions,
 | 
			
		||||
      defaultTimeout,
 | 
			
		||||
      defaultArgs,
 | 
			
		||||
      defaultEnvVars,
 | 
			
		||||
@@ -304,7 +304,7 @@ export default {
 | 
			
		||||
    const { client, clientOptions, getClientOptions } = useClientDropdown();
 | 
			
		||||
 | 
			
		||||
    // bulk action logic
 | 
			
		||||
    const state = ref({
 | 
			
		||||
    const state = reactive({
 | 
			
		||||
      mode: props.mode,
 | 
			
		||||
      target: "client",
 | 
			
		||||
      monType: "all",
 | 
			
		||||
@@ -326,33 +326,39 @@ export default {
 | 
			
		||||
    const loading = ref(false);
 | 
			
		||||
 | 
			
		||||
    watch(
 | 
			
		||||
      () => state.value.target,
 | 
			
		||||
      () => state.target,
 | 
			
		||||
      () => {
 | 
			
		||||
        client.value = null;
 | 
			
		||||
        site.value = null;
 | 
			
		||||
        agents.value = [];
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    plat.value = state.osType;
 | 
			
		||||
 | 
			
		||||
    watch(
 | 
			
		||||
      () => state.value.osType,
 | 
			
		||||
      () => state.osType,
 | 
			
		||||
      (newValue) => {
 | 
			
		||||
        state.value.custom_shell = null;
 | 
			
		||||
        state.value.run_as_user = false;
 | 
			
		||||
        state.custom_shell = null;
 | 
			
		||||
        state.run_as_user = false;
 | 
			
		||||
 | 
			
		||||
        if (newValue === "windows") {
 | 
			
		||||
          state.value.shell = "cmd";
 | 
			
		||||
          state.shell = "cmd";
 | 
			
		||||
        } else {
 | 
			
		||||
          state.value.shell = "/bin/bash";
 | 
			
		||||
          state.shell = "/bin/bash";
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        // set plat to filter script options
 | 
			
		||||
        if (newValue === "all") plat.value = undefined;
 | 
			
		||||
        else plat.value = newValue;
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    async function submit() {
 | 
			
		||||
      loading.value = true;
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const data = await runBulkAction(state.value);
 | 
			
		||||
        const data = await runBulkAction(state);
 | 
			
		||||
        notifySuccess(data);
 | 
			
		||||
        onDialogHide();
 | 
			
		||||
      } catch (e) {}
 | 
			
		||||
@@ -362,9 +368,7 @@ export default {
 | 
			
		||||
 | 
			
		||||
    const supportsRunAsUser = () => {
 | 
			
		||||
      const modes = ["script", "command"];
 | 
			
		||||
      return (
 | 
			
		||||
        state.value.osType === "windows" && modes.includes(state.value.mode)
 | 
			
		||||
      );
 | 
			
		||||
      return state.osType === "windows" && modes.includes(state.mode);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // set modal title and caption
 | 
			
		||||
@@ -372,25 +376,10 @@ export default {
 | 
			
		||||
      return props.mode === "command"
 | 
			
		||||
        ? "Run Bulk Command"
 | 
			
		||||
        : props.mode === "script"
 | 
			
		||||
        ? "Run Bulk Script"
 | 
			
		||||
        : props.mode === "patch"
 | 
			
		||||
        ? "Bulk Patch Management"
 | 
			
		||||
        : "";
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const filteredScriptOptions = computed(() => {
 | 
			
		||||
      if (props.mode !== "script") return [];
 | 
			
		||||
      if (state.value.osType === "all") return scriptOptions.value;
 | 
			
		||||
 | 
			
		||||
      return removeExtraOptionCategories(
 | 
			
		||||
        scriptOptions.value.filter(
 | 
			
		||||
          (script) =>
 | 
			
		||||
            script.category ||
 | 
			
		||||
            !script.supported_platforms ||
 | 
			
		||||
            script.supported_platforms.length === 0 ||
 | 
			
		||||
            script.supported_platforms.includes(state.value.osType)
 | 
			
		||||
        )
 | 
			
		||||
      );
 | 
			
		||||
          ? "Run Bulk Script"
 | 
			
		||||
          : props.mode === "patch"
 | 
			
		||||
            ? "Bulk Patch Management"
 | 
			
		||||
            : "";
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // component lifecycle hooks
 | 
			
		||||
@@ -398,7 +387,7 @@ export default {
 | 
			
		||||
      getAgentOptions();
 | 
			
		||||
      getSiteOptions();
 | 
			
		||||
      getClientOptions();
 | 
			
		||||
      if (props.mode === "script") getScriptOptions(showCommunityScripts.value);
 | 
			
		||||
      if (props.mode === "script") getScriptOptions();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
@@ -407,7 +396,7 @@ export default {
 | 
			
		||||
      agentOptions,
 | 
			
		||||
      clientOptions,
 | 
			
		||||
      siteOptions,
 | 
			
		||||
      filteredScriptOptions,
 | 
			
		||||
      filterByPlatformOptions,
 | 
			
		||||
      loading,
 | 
			
		||||
      shellOptions,
 | 
			
		||||
      filteredOsTypeOptions,
 | 
			
		||||
@@ -433,5 +422,5 @@ export default {
 | 
			
		||||
      onDialogHide,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -39,9 +39,9 @@
 | 
			
		||||
      <q-form @submit.prevent="sendScript">
 | 
			
		||||
        <q-card-section>
 | 
			
		||||
          <tactical-dropdown
 | 
			
		||||
            :rules="[(val) => !!val || '*Required']"
 | 
			
		||||
            :rules="[(val: number) => !!val || '*Required']"
 | 
			
		||||
            v-model="state.script"
 | 
			
		||||
            :options="filteredScriptOptions"
 | 
			
		||||
            :options="filterByPlatformOptions"
 | 
			
		||||
            label="Select script"
 | 
			
		||||
            outlined
 | 
			
		||||
            mapOptions
 | 
			
		||||
@@ -130,7 +130,7 @@
 | 
			
		||||
        </q-card-section>
 | 
			
		||||
        <q-card-section v-if="state.output === 'collector'">
 | 
			
		||||
          <tactical-dropdown
 | 
			
		||||
            :rules="[(val) => !!val || '*Required']"
 | 
			
		||||
            :rules="[(val: number) => !!val || '*Required']"
 | 
			
		||||
            outlined
 | 
			
		||||
            v-model="state.custom_field"
 | 
			
		||||
            :options="customFieldOptions"
 | 
			
		||||
@@ -175,6 +175,8 @@
 | 
			
		||||
          class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
 | 
			
		||||
          style="max-height: 50vh"
 | 
			
		||||
        >
 | 
			
		||||
          <script-output-copy-clip label="Output" :data="ret" />
 | 
			
		||||
          <q-separator />
 | 
			
		||||
          <pre>{{ ret }}</pre>
 | 
			
		||||
        </q-card-section>
 | 
			
		||||
      </q-form>
 | 
			
		||||
@@ -182,22 +184,23 @@
 | 
			
		||||
  </q-dialog>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
// composition imports
 | 
			
		||||
import { ref, watch, computed } from "vue";
 | 
			
		||||
import { ref, watch } from "vue";
 | 
			
		||||
import { useDialogPluginComponent, openURL } from "quasar";
 | 
			
		||||
import { useScriptDropdown } from "@/composables/scripts";
 | 
			
		||||
import { useCustomFieldDropdown } from "@/composables/core";
 | 
			
		||||
import { runScript } from "@/api/agents";
 | 
			
		||||
import { notifySuccess } from "@/utils/notify";
 | 
			
		||||
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
 | 
			
		||||
import {
 | 
			
		||||
  formatScriptSyntax,
 | 
			
		||||
  removeExtraOptionCategories,
 | 
			
		||||
} from "@/utils/format";
 | 
			
		||||
import { formatScriptSyntax } from "@/utils/format";
 | 
			
		||||
 | 
			
		||||
//ui imports
 | 
			
		||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
			
		||||
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
 | 
			
		||||
 | 
			
		||||
// types
 | 
			
		||||
import type { Agent } from "@/types/agents";
 | 
			
		||||
 | 
			
		||||
// static data
 | 
			
		||||
const outputOptions = [
 | 
			
		||||
@@ -208,110 +211,71 @@ const outputOptions = [
 | 
			
		||||
  { label: "Save results to Agent Notes", value: "note" },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "RunScript",
 | 
			
		||||
  emits: [...useDialogPluginComponent.emits],
 | 
			
		||||
  components: { TacticalDropdown },
 | 
			
		||||
  props: {
 | 
			
		||||
    agent: !Object,
 | 
			
		||||
    script: Number,
 | 
			
		||||
  },
 | 
			
		||||
  setup(props) {
 | 
			
		||||
    // setup quasar dialog plugin
 | 
			
		||||
    const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
			
		||||
// emits
 | 
			
		||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
			
		||||
 | 
			
		||||
    // setup dropdowns
 | 
			
		||||
    const {
 | 
			
		||||
      script,
 | 
			
		||||
      scriptOptions,
 | 
			
		||||
      defaultTimeout,
 | 
			
		||||
      defaultArgs,
 | 
			
		||||
      defaultEnvVars,
 | 
			
		||||
      syntax,
 | 
			
		||||
      link,
 | 
			
		||||
    } = useScriptDropdown(props.script, {
 | 
			
		||||
      onMount: true,
 | 
			
		||||
      filterByPlatform: props.agent.plat,
 | 
			
		||||
    });
 | 
			
		||||
    const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
 | 
			
		||||
// props
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  agent: Agent;
 | 
			
		||||
  script?: number;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
    // main run script functionaity
 | 
			
		||||
    const state = ref({
 | 
			
		||||
      output: "wait",
 | 
			
		||||
      emails: [],
 | 
			
		||||
      emailMode: "default",
 | 
			
		||||
      custom_field: null,
 | 
			
		||||
      save_all_output: false,
 | 
			
		||||
      script,
 | 
			
		||||
      args: defaultArgs,
 | 
			
		||||
      env_vars: defaultEnvVars,
 | 
			
		||||
      timeout: defaultTimeout,
 | 
			
		||||
      run_as_user: false,
 | 
			
		||||
    });
 | 
			
		||||
// setup quasar dialog plugin
 | 
			
		||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
			
		||||
 | 
			
		||||
    const ret = ref(null);
 | 
			
		||||
    const loading = ref(false);
 | 
			
		||||
    const maximized = ref(false);
 | 
			
		||||
// setup dropdowns
 | 
			
		||||
const {
 | 
			
		||||
  script,
 | 
			
		||||
  filterByPlatformOptions,
 | 
			
		||||
  defaultTimeout,
 | 
			
		||||
  defaultArgs,
 | 
			
		||||
  defaultEnvVars,
 | 
			
		||||
  syntax,
 | 
			
		||||
  link,
 | 
			
		||||
} = useScriptDropdown({
 | 
			
		||||
  script: props.script,
 | 
			
		||||
  plat: props.agent.plat,
 | 
			
		||||
  onMount: true,
 | 
			
		||||
});
 | 
			
		||||
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
 | 
			
		||||
 | 
			
		||||
    async function sendScript() {
 | 
			
		||||
      ret.value = null;
 | 
			
		||||
      loading.value = true;
 | 
			
		||||
// main run script functionaity
 | 
			
		||||
const state = ref({
 | 
			
		||||
  output: "wait",
 | 
			
		||||
  emails: [],
 | 
			
		||||
  emailMode: "default",
 | 
			
		||||
  custom_field: null,
 | 
			
		||||
  save_all_output: false,
 | 
			
		||||
  script,
 | 
			
		||||
  args: defaultArgs,
 | 
			
		||||
  env_vars: defaultEnvVars,
 | 
			
		||||
  timeout: defaultTimeout,
 | 
			
		||||
  run_as_user: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
      ret.value = await runScript(props.agent.agent_id, state.value);
 | 
			
		||||
      loading.value = false;
 | 
			
		||||
      if (state.value.output === "forget") {
 | 
			
		||||
        onDialogHide();
 | 
			
		||||
        notifySuccess(ret.value);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
const ret = ref(null);
 | 
			
		||||
const loading = ref(false);
 | 
			
		||||
const maximized = ref(false);
 | 
			
		||||
 | 
			
		||||
    function openScriptURL() {
 | 
			
		||||
      link.value ? openURL(link.value) : null;
 | 
			
		||||
    }
 | 
			
		||||
async function sendScript() {
 | 
			
		||||
  ret.value = null;
 | 
			
		||||
  loading.value = true;
 | 
			
		||||
 | 
			
		||||
    const filteredScriptOptions = computed(() => {
 | 
			
		||||
      return removeExtraOptionCategories(
 | 
			
		||||
        scriptOptions.value.filter(
 | 
			
		||||
          (script) =>
 | 
			
		||||
            script.category ||
 | 
			
		||||
            !script.supported_platforms ||
 | 
			
		||||
            script.supported_platforms.length === 0 ||
 | 
			
		||||
            script.supported_platforms.includes(props.agent.plat)
 | 
			
		||||
        )
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  ret.value = await runScript(props.agent.agent_id, state.value);
 | 
			
		||||
  loading.value = false;
 | 
			
		||||
  if (state.value.output === "forget") {
 | 
			
		||||
    onDialogHide();
 | 
			
		||||
    if (ret.value) notifySuccess(ret.value);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    // watchers
 | 
			
		||||
    watch(
 | 
			
		||||
      [() => state.value.output, () => state.value.emailMode],
 | 
			
		||||
      () => (state.value.emails = [])
 | 
			
		||||
    );
 | 
			
		||||
function openScriptURL() {
 | 
			
		||||
  link.value ? openURL(link.value) : null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      // reactive data
 | 
			
		||||
      state,
 | 
			
		||||
      loading,
 | 
			
		||||
      filteredScriptOptions,
 | 
			
		||||
      link,
 | 
			
		||||
      syntax,
 | 
			
		||||
      ret,
 | 
			
		||||
      maximized,
 | 
			
		||||
      customFieldOptions,
 | 
			
		||||
 | 
			
		||||
      // non-reactive data
 | 
			
		||||
      outputOptions,
 | 
			
		||||
      runAsUserToolTip,
 | 
			
		||||
      envVarsLabel,
 | 
			
		||||
 | 
			
		||||
      //methods
 | 
			
		||||
      formatScriptSyntax,
 | 
			
		||||
      sendScript,
 | 
			
		||||
      openScriptURL,
 | 
			
		||||
 | 
			
		||||
      // quasar dialog plugin
 | 
			
		||||
      dialogRef,
 | 
			
		||||
      onDialogHide,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
// watchers
 | 
			
		||||
watch(
 | 
			
		||||
  [() => state.value.output, () => state.value.emailMode],
 | 
			
		||||
  () => (state.value.emails = []),
 | 
			
		||||
);
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -104,6 +104,9 @@
 | 
			
		||||
            type="submit"
 | 
			
		||||
          />
 | 
			
		||||
        </q-card-actions>
 | 
			
		||||
        <q-card-section v-if="ret !== null"
 | 
			
		||||
          ><script-output-copy-clip label="Output" :data="ret" /> <q-separator
 | 
			
		||||
        /></q-card-section>
 | 
			
		||||
        <q-card-section
 | 
			
		||||
          v-if="ret !== null"
 | 
			
		||||
          class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
 | 
			
		||||
@@ -124,8 +127,13 @@ import { sendAgentCommand } from "@/api/agents";
 | 
			
		||||
import { cmdPlaceholder } from "@/composables/agents";
 | 
			
		||||
import { runAsUserToolTip } from "@/constants/constants";
 | 
			
		||||
 | 
			
		||||
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "SendCommand",
 | 
			
		||||
  components: {
 | 
			
		||||
    ScriptOutputCopyClip,
 | 
			
		||||
  },
 | 
			
		||||
  emits: [...useDialogPluginComponent.emits],
 | 
			
		||||
  props: {
 | 
			
		||||
    agent: !Object,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <q-dialog ref="dialog" @hide="onHide">
 | 
			
		||||
  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
			
		||||
    <q-card style="width: 90vw; max-width: 90vw">
 | 
			
		||||
      <q-bar>
 | 
			
		||||
        {{ title }}
 | 
			
		||||
        {{ alertTemplate ? "Edit Alert Template" : "Add Alert Template" }}
 | 
			
		||||
        <q-space />
 | 
			
		||||
        <q-btn dense flat icon="close" v-close-popup>
 | 
			
		||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
			
		||||
@@ -150,50 +150,62 @@
 | 
			
		||||
              <span style="text-decoration: underline; cursor: help"
 | 
			
		||||
                >Alert Failure Settings
 | 
			
		||||
                <q-tooltip>
 | 
			
		||||
                  The selected script will run when an alert is triggered. This
 | 
			
		||||
                  script will run on any online agent.
 | 
			
		||||
                  The selected action will run when an alert is triggered.
 | 
			
		||||
                </q-tooltip>
 | 
			
		||||
              </span>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <q-card-section>
 | 
			
		||||
              <q-select
 | 
			
		||||
                class="q-mb-sm"
 | 
			
		||||
                label="Failure action"
 | 
			
		||||
              <q-option-group
 | 
			
		||||
                v-model="template.action_type"
 | 
			
		||||
                class="q-pb-sm"
 | 
			
		||||
                :options="actionTypeOptions"
 | 
			
		||||
                dense
 | 
			
		||||
                options-dense
 | 
			
		||||
                inline
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              <tactical-dropdown
 | 
			
		||||
                v-if="template.action_type == 'script'"
 | 
			
		||||
                class="q-mb-sm"
 | 
			
		||||
                label="Failure script"
 | 
			
		||||
                outlined
 | 
			
		||||
                clearable
 | 
			
		||||
                v-model="template.action"
 | 
			
		||||
                :options="scriptOptions"
 | 
			
		||||
                map-options
 | 
			
		||||
                emit-value
 | 
			
		||||
                @update:model-value="setScriptDefaults('failure')"
 | 
			
		||||
              >
 | 
			
		||||
                <template v-slot:option="scope">
 | 
			
		||||
                  <q-item
 | 
			
		||||
                    v-if="!scope.opt.category"
 | 
			
		||||
                    v-bind="scope.itemProps"
 | 
			
		||||
                    class="q-pl-lg"
 | 
			
		||||
                  >
 | 
			
		||||
                    <q-item-section>
 | 
			
		||||
                      <q-item-label v-html="scope.opt.label"></q-item-label>
 | 
			
		||||
                    </q-item-section>
 | 
			
		||||
                  </q-item>
 | 
			
		||||
                  <q-item-label
 | 
			
		||||
                    v-if="scope.opt.category"
 | 
			
		||||
                    v-bind="scope.itemProps"
 | 
			
		||||
                    header
 | 
			
		||||
                    class="q-pa-sm"
 | 
			
		||||
                    >{{ scope.opt.category }}</q-item-label
 | 
			
		||||
                  >
 | 
			
		||||
                </template>
 | 
			
		||||
              </q-select>
 | 
			
		||||
                mapOptions
 | 
			
		||||
                filterable
 | 
			
		||||
                :rules="[(val) => !!val || '*Required']"
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              <tactical-dropdown
 | 
			
		||||
                v-else-if="template.action_type == 'server'"
 | 
			
		||||
                class="q-mb-sm"
 | 
			
		||||
                label="Failure script"
 | 
			
		||||
                outlined
 | 
			
		||||
                clearable
 | 
			
		||||
                v-model="template.action"
 | 
			
		||||
                :options="serverScriptOptions"
 | 
			
		||||
                mapOptions
 | 
			
		||||
                filterable
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              <tactical-dropdown
 | 
			
		||||
                v-else
 | 
			
		||||
                class="q-mb-sm"
 | 
			
		||||
                label="Failure Web Hook"
 | 
			
		||||
                outlined
 | 
			
		||||
                clearable
 | 
			
		||||
                v-model="template.action_rest"
 | 
			
		||||
                :options="restActionOptions"
 | 
			
		||||
                mapOptions
 | 
			
		||||
                filterable
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              <q-select
 | 
			
		||||
                v-if="template.action_type !== 'rest'"
 | 
			
		||||
                class="q-mb-sm"
 | 
			
		||||
                dense
 | 
			
		||||
                label="Failure action arguments (press Enter after typing each argument)"
 | 
			
		||||
                label="Failure script arguments (press Enter after typing each argument)"
 | 
			
		||||
                filled
 | 
			
		||||
                v-model="template.action_args"
 | 
			
		||||
                use-input
 | 
			
		||||
@@ -205,9 +217,10 @@
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              <q-select
 | 
			
		||||
                v-if="template.action_type !== 'rest'"
 | 
			
		||||
                class="q-mb-sm"
 | 
			
		||||
                dense
 | 
			
		||||
                label="Failure action environment vars (press Enter after typing each key=value pair)"
 | 
			
		||||
                label="Failure script environment vars (press Enter after typing each key=value pair)"
 | 
			
		||||
                filled
 | 
			
		||||
                v-model="template.action_env_vars"
 | 
			
		||||
                use-input
 | 
			
		||||
@@ -219,16 +232,15 @@
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              <q-input
 | 
			
		||||
                v-if="template.action_type !== 'rest'"
 | 
			
		||||
                class="q-mb-sm"
 | 
			
		||||
                label="Failure action timeout (seconds)"
 | 
			
		||||
                label="Failure script timeout (seconds)"
 | 
			
		||||
                outlined
 | 
			
		||||
                type="number"
 | 
			
		||||
                v-model.number="template.action_timeout"
 | 
			
		||||
                dense
 | 
			
		||||
                :rules="[
 | 
			
		||||
                  (val) => !!val || 'Failure action timeout is required',
 | 
			
		||||
                  (val) => val > 0 || 'Timeout must be greater than 0',
 | 
			
		||||
                  (val) => val <= 60 || 'Timeout must be 60 or less',
 | 
			
		||||
                  (val) => !!val || 'Failure script timeout is required',
 | 
			
		||||
                ]"
 | 
			
		||||
              />
 | 
			
		||||
            </q-card-section>
 | 
			
		||||
@@ -237,50 +249,61 @@
 | 
			
		||||
              <span style="text-decoration: underline; cursor: help"
 | 
			
		||||
                >Alert Resolved Settings
 | 
			
		||||
                <q-tooltip>
 | 
			
		||||
                  The selected script will run when an alert is resolved. This
 | 
			
		||||
                  script will run on any online agent.
 | 
			
		||||
                  The selected action will run when an alert is resolved.
 | 
			
		||||
                </q-tooltip>
 | 
			
		||||
              </span>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <q-card-section>
 | 
			
		||||
              <q-select
 | 
			
		||||
                class="q-mb-sm"
 | 
			
		||||
                label="Resolved Action"
 | 
			
		||||
              <q-option-group
 | 
			
		||||
                v-model="template.resolved_action_type"
 | 
			
		||||
                class="q-pb-sm"
 | 
			
		||||
                :options="actionTypeOptions"
 | 
			
		||||
                dense
 | 
			
		||||
                options-dense
 | 
			
		||||
                inline
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              <tactical-dropdown
 | 
			
		||||
                v-if="template.resolved_action_type === 'script'"
 | 
			
		||||
                class="q-mb-sm"
 | 
			
		||||
                label="Resolved Script"
 | 
			
		||||
                outlined
 | 
			
		||||
                clearable
 | 
			
		||||
                v-model="template.resolved_action"
 | 
			
		||||
                :options="scriptOptions"
 | 
			
		||||
                map-options
 | 
			
		||||
                emit-value
 | 
			
		||||
                @update:model-value="setScriptDefaults('resolved')"
 | 
			
		||||
              >
 | 
			
		||||
                <template v-slot:option="scope">
 | 
			
		||||
                  <q-item
 | 
			
		||||
                    v-if="!scope.opt.category"
 | 
			
		||||
                    v-bind="scope.itemProps"
 | 
			
		||||
                    class="q-pl-lg"
 | 
			
		||||
                  >
 | 
			
		||||
                    <q-item-section>
 | 
			
		||||
                      <q-item-label v-html="scope.opt.label"></q-item-label>
 | 
			
		||||
                    </q-item-section>
 | 
			
		||||
                  </q-item>
 | 
			
		||||
                  <q-item-label
 | 
			
		||||
                    v-if="scope.opt.category"
 | 
			
		||||
                    v-bind="scope.itemProps"
 | 
			
		||||
                    header
 | 
			
		||||
                    class="q-pa-sm"
 | 
			
		||||
                    >{{ scope.opt.category }}</q-item-label
 | 
			
		||||
                  >
 | 
			
		||||
                </template>
 | 
			
		||||
              </q-select>
 | 
			
		||||
                mapOptions
 | 
			
		||||
                filterable
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              <tactical-dropdown
 | 
			
		||||
                v-else-if="template.resolved_action_type === 'server'"
 | 
			
		||||
                class="q-mb-sm"
 | 
			
		||||
                label="Resolved Script"
 | 
			
		||||
                outlined
 | 
			
		||||
                clearable
 | 
			
		||||
                v-model="template.resolved_action"
 | 
			
		||||
                :options="serverScriptOptions"
 | 
			
		||||
                mapOptions
 | 
			
		||||
                filterable
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              <tactical-dropdown
 | 
			
		||||
                v-else
 | 
			
		||||
                class="q-mb-sm"
 | 
			
		||||
                label="Resolved Web Hook"
 | 
			
		||||
                outlined
 | 
			
		||||
                clearable
 | 
			
		||||
                v-model="template.resolved_action_rest"
 | 
			
		||||
                :options="restActionOptions"
 | 
			
		||||
                mapOptions
 | 
			
		||||
                filterable
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              <q-select
 | 
			
		||||
                v-if="template.resolved_action_type !== 'rest'"
 | 
			
		||||
                class="q-mb-sm"
 | 
			
		||||
                dense
 | 
			
		||||
                label="Resolved action arguments (press Enter after typing each argument)"
 | 
			
		||||
                label="Resolved script arguments (press Enter after typing each argument)"
 | 
			
		||||
                filled
 | 
			
		||||
                v-model="template.resolved_action_args"
 | 
			
		||||
                use-input
 | 
			
		||||
@@ -292,6 +315,7 @@
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              <q-select
 | 
			
		||||
                v-if="template.resolved_action_type !== 'rest'"
 | 
			
		||||
                class="q-mb-sm"
 | 
			
		||||
                dense
 | 
			
		||||
                label="Resolved action environment vars (press Enter after typing each key=value pair)"
 | 
			
		||||
@@ -306,16 +330,15 @@
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              <q-input
 | 
			
		||||
                v-if="template.resolved_action_type !== 'rest'"
 | 
			
		||||
                class="q-mb-sm"
 | 
			
		||||
                label="Resolved action timeout (seconds)"
 | 
			
		||||
                label="Resolved script timeout (seconds)"
 | 
			
		||||
                outlined
 | 
			
		||||
                type="number"
 | 
			
		||||
                v-model.number="template.resolved_action_timeout"
 | 
			
		||||
                dense
 | 
			
		||||
                :rules="[
 | 
			
		||||
                  (val) => !!val || 'Resolved action timeout is required',
 | 
			
		||||
                  (val) => val > 0 || 'Timeout must be greater than 0',
 | 
			
		||||
                  (val) => val <= 60 || 'Timeout must be 60 or less',
 | 
			
		||||
                  (val) => !!val || 'Resolved script timeout is required',
 | 
			
		||||
                ]"
 | 
			
		||||
              />
 | 
			
		||||
            </q-card-section>
 | 
			
		||||
@@ -324,7 +347,7 @@
 | 
			
		||||
              <span style="text-decoration: underline; cursor: help"
 | 
			
		||||
                >Run actions only on
 | 
			
		||||
                <q-tooltip>
 | 
			
		||||
                  The selected script will only run on the following types of
 | 
			
		||||
                  The selected action will only run on the following types of
 | 
			
		||||
                  alerts
 | 
			
		||||
                </q-tooltip>
 | 
			
		||||
              </span>
 | 
			
		||||
@@ -674,7 +697,7 @@
 | 
			
		||||
                left-label
 | 
			
		||||
              />
 | 
			
		||||
              <q-toggle
 | 
			
		||||
                v-model="template.check_text_on_resolved"
 | 
			
		||||
                v-model="template.task_text_on_resolved"
 | 
			
		||||
                label="Text"
 | 
			
		||||
                color="green"
 | 
			
		||||
                left-label
 | 
			
		||||
@@ -688,18 +711,23 @@
 | 
			
		||||
              v-if="step > 1"
 | 
			
		||||
              flat
 | 
			
		||||
              color="primary"
 | 
			
		||||
              @click="$refs.stepper.previous()"
 | 
			
		||||
              @click="stepper?.previous()"
 | 
			
		||||
              label="Back"
 | 
			
		||||
              class="q-mr-xs"
 | 
			
		||||
            />
 | 
			
		||||
            <q-btn
 | 
			
		||||
              v-if="step < 5"
 | 
			
		||||
              @click="$refs.stepper.next()"
 | 
			
		||||
              @click="stepper?.next()"
 | 
			
		||||
              color="primary"
 | 
			
		||||
              label="Next"
 | 
			
		||||
            />
 | 
			
		||||
            <q-space />
 | 
			
		||||
            <q-btn @click="onSubmit" color="primary" label="Submit" />
 | 
			
		||||
            <q-btn
 | 
			
		||||
              @click="onSubmit"
 | 
			
		||||
              color="primary"
 | 
			
		||||
              label="Submit"
 | 
			
		||||
              :loading="loading"
 | 
			
		||||
            />
 | 
			
		||||
          </q-stepper-navigation>
 | 
			
		||||
        </template>
 | 
			
		||||
      </q-stepper>
 | 
			
		||||
@@ -707,195 +735,279 @@
 | 
			
		||||
  </q-dialog>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import mixins from "@/mixins/mixins";
 | 
			
		||||
import { mapGetters } from "vuex";
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, ref, reactive, watch, nextTick } from "vue";
 | 
			
		||||
import { useStore } from "vuex";
 | 
			
		||||
import { useQuasar, useDialogPluginComponent, type QStepper } from "quasar";
 | 
			
		||||
import { useScriptDropdown } from "@/composables/scripts";
 | 
			
		||||
import { useURLActionDropdown } from "@/composables/core";
 | 
			
		||||
import { notifyError, notifySuccess } from "@/utils/notify";
 | 
			
		||||
import { addAlertTemplate, saveAlertTemplate } from "@/api/alerts";
 | 
			
		||||
import { isValidEmail } from "@/utils/validation";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "AlertTemplateForm",
 | 
			
		||||
  emits: ["hide", "ok", "cancel"],
 | 
			
		||||
  mixins: [mixins],
 | 
			
		||||
  props: { alertTemplate: Object },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      step: 1,
 | 
			
		||||
      template: {
 | 
			
		||||
        name: "",
 | 
			
		||||
        is_active: true,
 | 
			
		||||
        action: null,
 | 
			
		||||
        action_args: [],
 | 
			
		||||
        action_env_vars: [],
 | 
			
		||||
        action_timeout: 15,
 | 
			
		||||
        resolved_action: null,
 | 
			
		||||
        resolved_action_args: [],
 | 
			
		||||
        resolved_action_env_vars: [],
 | 
			
		||||
        resolved_action_timeout: 15,
 | 
			
		||||
        email_recipients: [],
 | 
			
		||||
        email_from: "",
 | 
			
		||||
        text_recipients: [],
 | 
			
		||||
        agent_email_on_resolved: false,
 | 
			
		||||
        agent_text_on_resolved: false,
 | 
			
		||||
        agent_always_email: null,
 | 
			
		||||
        agent_always_text: null,
 | 
			
		||||
        agent_always_alert: null,
 | 
			
		||||
        agent_periodic_alert_days: 0,
 | 
			
		||||
        agent_script_actions: true,
 | 
			
		||||
        check_email_alert_severity: [],
 | 
			
		||||
        check_text_alert_severity: [],
 | 
			
		||||
        check_dashboard_alert_severity: [],
 | 
			
		||||
        check_email_on_resolved: false,
 | 
			
		||||
        check_text_on_resolved: false,
 | 
			
		||||
        check_always_email: null,
 | 
			
		||||
        check_always_text: null,
 | 
			
		||||
        check_always_alert: null,
 | 
			
		||||
        check_periodic_alert_days: 0,
 | 
			
		||||
        check_script_actions: true,
 | 
			
		||||
        task_email_alert_severity: [],
 | 
			
		||||
        task_text_alert_severity: [],
 | 
			
		||||
        task_dashboard_alert_severity: [],
 | 
			
		||||
        task_email_on_resolved: false,
 | 
			
		||||
        task_text_on_resolved: false,
 | 
			
		||||
        task_always_email: null,
 | 
			
		||||
        task_always_text: null,
 | 
			
		||||
        task_always_alert: null,
 | 
			
		||||
        task_periodic_alert_days: 0,
 | 
			
		||||
        task_script_actions: true,
 | 
			
		||||
      },
 | 
			
		||||
      scriptOptions: [],
 | 
			
		||||
      severityOptions: [
 | 
			
		||||
        { label: "Error", value: "error" },
 | 
			
		||||
        { label: "Warning", value: "warning" },
 | 
			
		||||
        { label: "Informational", value: "info" },
 | 
			
		||||
      ],
 | 
			
		||||
      thumbStyle: {
 | 
			
		||||
        right: "2px",
 | 
			
		||||
        borderRadius: "5px",
 | 
			
		||||
        backgroundColor: "#027be3",
 | 
			
		||||
        width: "5px",
 | 
			
		||||
        opacity: 0.75,
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
// components
 | 
			
		||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
			
		||||
 | 
			
		||||
// types
 | 
			
		||||
import type { AlertTemplate, AlertSeverity } from "@/types/alerts";
 | 
			
		||||
 | 
			
		||||
// store
 | 
			
		||||
const store = useStore();
 | 
			
		||||
const hosted = computed(() => store.state.hosted);
 | 
			
		||||
const server_scripts_enabled = computed(
 | 
			
		||||
  () => store.state.server_scripts_enabled,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// props
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  alertTemplate?: AlertTemplate;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
// emits
 | 
			
		||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
			
		||||
 | 
			
		||||
// setup quasar plugins
 | 
			
		||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
			
		||||
const $q = useQuasar();
 | 
			
		||||
 | 
			
		||||
const step = ref(1);
 | 
			
		||||
 | 
			
		||||
// setup script dropdowns
 | 
			
		||||
const {
 | 
			
		||||
  script: failureAction,
 | 
			
		||||
  defaultArgs: failureArgs,
 | 
			
		||||
  defaultEnvVars: failureEnvVars,
 | 
			
		||||
  defaultTimeout: failureTimeout,
 | 
			
		||||
  serverScriptOptions,
 | 
			
		||||
  scriptOptions,
 | 
			
		||||
} = useScriptDropdown({ script: props.alertTemplate?.action, onMount: true });
 | 
			
		||||
 | 
			
		||||
const {
 | 
			
		||||
  script: resolvedAction,
 | 
			
		||||
  defaultArgs: resolvedArgs,
 | 
			
		||||
  defaultEnvVars: resolvedEnvVars,
 | 
			
		||||
  defaultTimeout: resolvedTimeout,
 | 
			
		||||
} = useScriptDropdown({
 | 
			
		||||
  script: props.alertTemplate?.resolved_action,
 | 
			
		||||
  onMount: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// setup custom field dropdown
 | 
			
		||||
const { restActionOptions } = useURLActionDropdown({ onMount: true });
 | 
			
		||||
 | 
			
		||||
// alert template form logic
 | 
			
		||||
const template: AlertTemplate = props.alertTemplate
 | 
			
		||||
  ? reactive(Object.assign({}, { ...props.alertTemplate }))
 | 
			
		||||
  : reactive({
 | 
			
		||||
      id: 0,
 | 
			
		||||
      name: "",
 | 
			
		||||
      is_active: true,
 | 
			
		||||
      action_type: "script",
 | 
			
		||||
      action: failureAction,
 | 
			
		||||
      action_rest: undefined,
 | 
			
		||||
      action_args: failureArgs,
 | 
			
		||||
      action_env_vars: failureEnvVars,
 | 
			
		||||
      action_timeout: failureTimeout,
 | 
			
		||||
      resolved_action_type: "script",
 | 
			
		||||
      resolved_action: resolvedAction,
 | 
			
		||||
      resolved_action_rest: undefined,
 | 
			
		||||
      resolved_action_args: resolvedArgs,
 | 
			
		||||
      resolved_action_env_vars: resolvedEnvVars,
 | 
			
		||||
      resolved_action_timeout: resolvedTimeout,
 | 
			
		||||
      email_recipients: [] as string[],
 | 
			
		||||
      email_from: "",
 | 
			
		||||
      text_recipients: [] as string[],
 | 
			
		||||
      agent_email_on_resolved: false,
 | 
			
		||||
      agent_text_on_resolved: false,
 | 
			
		||||
      agent_always_email: null,
 | 
			
		||||
      agent_always_text: null,
 | 
			
		||||
      agent_always_alert: null,
 | 
			
		||||
      agent_periodic_alert_days: 0,
 | 
			
		||||
      agent_script_actions: true,
 | 
			
		||||
      check_email_alert_severity: [] as AlertSeverity[],
 | 
			
		||||
      check_text_alert_severity: [] as AlertSeverity[],
 | 
			
		||||
      check_dashboard_alert_severity: [] as AlertSeverity[],
 | 
			
		||||
      check_email_on_resolved: false,
 | 
			
		||||
      check_text_on_resolved: false,
 | 
			
		||||
      check_always_email: null,
 | 
			
		||||
      check_always_text: null,
 | 
			
		||||
      check_always_alert: null,
 | 
			
		||||
      check_periodic_alert_days: 0,
 | 
			
		||||
      check_script_actions: true,
 | 
			
		||||
      task_email_alert_severity: [] as AlertSeverity[],
 | 
			
		||||
      task_text_alert_severity: [] as AlertSeverity[],
 | 
			
		||||
      task_dashboard_alert_severity: [] as AlertSeverity[],
 | 
			
		||||
      task_email_on_resolved: false,
 | 
			
		||||
      task_text_on_resolved: false,
 | 
			
		||||
      task_always_email: null,
 | 
			
		||||
      task_always_text: null,
 | 
			
		||||
      task_always_alert: null,
 | 
			
		||||
      task_periodic_alert_days: 0,
 | 
			
		||||
      task_script_actions: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
// reset selected script if action type is changed
 | 
			
		||||
watch(
 | 
			
		||||
  () => template.action_type,
 | 
			
		||||
  () => {
 | 
			
		||||
    template.action_rest = undefined;
 | 
			
		||||
    template.action = undefined;
 | 
			
		||||
    template.action_args = [];
 | 
			
		||||
    template.action_env_vars = [];
 | 
			
		||||
    template.action_timeout = 30;
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters(["showCommunityScripts"]),
 | 
			
		||||
    title() {
 | 
			
		||||
      return this.editing ? "Edit Alert Template" : "Add Alert Template";
 | 
			
		||||
    },
 | 
			
		||||
    editing() {
 | 
			
		||||
      return !!this.alertTemplate;
 | 
			
		||||
    },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => template.resolved_action_type,
 | 
			
		||||
  () => {
 | 
			
		||||
    template.resolved_action_rest = undefined;
 | 
			
		||||
    template.resolved_action = undefined;
 | 
			
		||||
    template.resolved_action_args = [];
 | 
			
		||||
    template.resolved_action_env_vars = [];
 | 
			
		||||
    template.resolved_action_timeout = 30;
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    setScriptDefaults(type) {
 | 
			
		||||
      if (type === "failure") {
 | 
			
		||||
        const script = this.scriptOptions.find(
 | 
			
		||||
          (i) => i.value === this.template.action
 | 
			
		||||
        );
 | 
			
		||||
        this.template.action_args = script.args;
 | 
			
		||||
        this.template.action_env_vars = script.env_vars;
 | 
			
		||||
      } else if (type === "resolved") {
 | 
			
		||||
        const script = this.scriptOptions.find(
 | 
			
		||||
          (i) => i.value === this.template.resolved_action
 | 
			
		||||
        );
 | 
			
		||||
        this.template.resolved_action_args = script.args;
 | 
			
		||||
        this.template.resolved_action_env_vars = script.env_vars;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    toggleAddEmail() {
 | 
			
		||||
      this.$q
 | 
			
		||||
        .dialog({
 | 
			
		||||
          title: "Add email",
 | 
			
		||||
          prompt: {
 | 
			
		||||
            model: "",
 | 
			
		||||
            isValid: (val) => this.isValidEmail(val),
 | 
			
		||||
            type: "email",
 | 
			
		||||
          },
 | 
			
		||||
          cancel: true,
 | 
			
		||||
          ok: { label: "Add", color: "primary" },
 | 
			
		||||
          persistent: false,
 | 
			
		||||
        })
 | 
			
		||||
        .onOk((data) => {
 | 
			
		||||
          this.template.email_recipients.push(data);
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// sync selected script to scriptdropdown
 | 
			
		||||
// only add watchers if editting template
 | 
			
		||||
if (props.alertTemplate) {
 | 
			
		||||
  watch(
 | 
			
		||||
    () => template.action,
 | 
			
		||||
    (newValue) => {
 | 
			
		||||
      if (newValue) {
 | 
			
		||||
        failureAction.value = newValue;
 | 
			
		||||
 | 
			
		||||
        // wait for the script change to happen
 | 
			
		||||
        nextTick(() => {
 | 
			
		||||
          template.action_args = failureArgs.value;
 | 
			
		||||
          template.action_env_vars = failureEnvVars.value;
 | 
			
		||||
          template.action_timeout = failureTimeout.value;
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    toggleAddSMSNumber() {
 | 
			
		||||
      this.$q
 | 
			
		||||
        .dialog({
 | 
			
		||||
          title: "Add number",
 | 
			
		||||
          message:
 | 
			
		||||
            "Use E.164 format: must have the <b>+</b> symbol and <span class='text-red'>country code</span>, followed by the <span class='text-green'>phone number</span> e.g. <b>+<span class='text-red'>1</span><span class='text-green'>2131231234</span></b>",
 | 
			
		||||
          prompt: {
 | 
			
		||||
            model: "",
 | 
			
		||||
          },
 | 
			
		||||
          html: true,
 | 
			
		||||
          cancel: true,
 | 
			
		||||
          ok: { label: "Add", color: "primary" },
 | 
			
		||||
          persistent: false,
 | 
			
		||||
        })
 | 
			
		||||
        .onOk((data) => {
 | 
			
		||||
          this.template.text_recipients.push(data);
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  watch(
 | 
			
		||||
    () => template.resolved_action,
 | 
			
		||||
    (newValue) => {
 | 
			
		||||
      if (newValue) {
 | 
			
		||||
        resolvedAction.value = newValue;
 | 
			
		||||
 | 
			
		||||
        // wait for the script change to happen
 | 
			
		||||
        nextTick(() => {
 | 
			
		||||
          template.resolved_action_args = resolvedArgs.value;
 | 
			
		||||
          template.resolved_action_env_vars = resolvedEnvVars.value;
 | 
			
		||||
          template.resolved_action_timeout = resolvedTimeout.value;
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    removeEmail(email) {
 | 
			
		||||
      const removed = this.template.email_recipients.filter((k) => k !== email);
 | 
			
		||||
      this.template.email_recipients = removed;
 | 
			
		||||
    },
 | 
			
		||||
    removeSMSNumber(num) {
 | 
			
		||||
      const removed = this.template.text_recipients.filter((k) => k !== num);
 | 
			
		||||
      this.template.text_recipients = removed;
 | 
			
		||||
    },
 | 
			
		||||
    onSubmit() {
 | 
			
		||||
      if (!this.template.name) {
 | 
			
		||||
        this.notifyError("Name needs to be set");
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.$q.loading.show();
 | 
			
		||||
 | 
			
		||||
      if (this.editing) {
 | 
			
		||||
        this.$axios
 | 
			
		||||
          .put(`alerts/templates/${this.template.id}/`, this.template)
 | 
			
		||||
          .then(() => {
 | 
			
		||||
            this.$q.loading.hide();
 | 
			
		||||
            this.onOk();
 | 
			
		||||
            this.notifySuccess("Alert Template edited!");
 | 
			
		||||
          })
 | 
			
		||||
          .catch(() => {
 | 
			
		||||
            this.$q.loading.hide();
 | 
			
		||||
          });
 | 
			
		||||
      } else {
 | 
			
		||||
        this.$axios
 | 
			
		||||
          .post("alerts/templates/", this.template)
 | 
			
		||||
          .then(() => {
 | 
			
		||||
            this.$q.loading.hide();
 | 
			
		||||
            this.onOk();
 | 
			
		||||
            this.notifySuccess("Alert Template was added!");
 | 
			
		||||
          })
 | 
			
		||||
          .catch(() => {
 | 
			
		||||
            this.$q.loading.hide();
 | 
			
		||||
          });
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    show() {
 | 
			
		||||
      this.$refs.dialog.show();
 | 
			
		||||
    },
 | 
			
		||||
    hide() {
 | 
			
		||||
      this.$refs.dialog.hide();
 | 
			
		||||
    },
 | 
			
		||||
    onHide() {
 | 
			
		||||
      this.$emit("hide");
 | 
			
		||||
    },
 | 
			
		||||
    onOk() {
 | 
			
		||||
      this.$emit("ok");
 | 
			
		||||
      this.hide();
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.getScriptOptions(this.showCommunityScripts).then(
 | 
			
		||||
      (options) => (this.scriptOptions = Object.freeze(options))
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const severityOptions = [
 | 
			
		||||
  { label: "Error", value: "error" },
 | 
			
		||||
  { label: "Warning", value: "warning" },
 | 
			
		||||
  { label: "Informational", value: "info" },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const staticActionTypeOptions = [
 | 
			
		||||
  { label: "Send a Web Hook", value: "rest" },
 | 
			
		||||
  { label: "Run script on Agent", value: "script" },
 | 
			
		||||
  { label: "Run script on TRMM Server", value: "server" },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const actionTypeOptions = computed(() => {
 | 
			
		||||
  // don't show for hosted at all
 | 
			
		||||
  if (hosted.value) {
 | 
			
		||||
    return staticActionTypeOptions.filter(
 | 
			
		||||
      (option) => option.value !== "server",
 | 
			
		||||
    );
 | 
			
		||||
    // Copy alertTemplate prop locally
 | 
			
		||||
    if (this.editing) Object.assign(this.template, this.alertTemplate);
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
  }
 | 
			
		||||
  // disable the server script radio button if feature is disabled globally
 | 
			
		||||
  const modifiedOptions = staticActionTypeOptions.map((option) => {
 | 
			
		||||
    if (!server_scripts_enabled.value && option.value === "server") {
 | 
			
		||||
      return { ...option, disable: true };
 | 
			
		||||
    }
 | 
			
		||||
    return option;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return modifiedOptions;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const stepper = ref<QStepper | null>(null);
 | 
			
		||||
function toggleAddEmail() {
 | 
			
		||||
  $q.dialog({
 | 
			
		||||
    title: "Add email",
 | 
			
		||||
    prompt: {
 | 
			
		||||
      model: "",
 | 
			
		||||
      isValid: (val) => isValidEmail(val),
 | 
			
		||||
      type: "email",
 | 
			
		||||
    },
 | 
			
		||||
    cancel: true,
 | 
			
		||||
    ok: { label: "Add", color: "primary" },
 | 
			
		||||
    persistent: false,
 | 
			
		||||
  }).onOk((data) => {
 | 
			
		||||
    template.email_recipients.push(data);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleAddSMSNumber() {
 | 
			
		||||
  $q.dialog({
 | 
			
		||||
    title: "Add number",
 | 
			
		||||
    message:
 | 
			
		||||
      "Use E.164 format: must have the <b>+</b> symbol and <span class='text-red'>country code</span>, followed by the <span class='text-green'>phone number</span> e.g. <b>+<span class='text-red'>1</span><span class='text-green'>2131231234</span></b>",
 | 
			
		||||
    prompt: {
 | 
			
		||||
      model: "",
 | 
			
		||||
    },
 | 
			
		||||
    html: true,
 | 
			
		||||
    cancel: true,
 | 
			
		||||
    ok: { label: "Add", color: "primary" },
 | 
			
		||||
    persistent: false,
 | 
			
		||||
  }).onOk((data: string) => {
 | 
			
		||||
    template.text_recipients.push(data);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeEmail(email: string) {
 | 
			
		||||
  const removed = template.email_recipients.filter((k) => k !== email);
 | 
			
		||||
  template.email_recipients = removed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeSMSNumber(num: string) {
 | 
			
		||||
  const removed = template.text_recipients.filter((k) => k !== num);
 | 
			
		||||
  template.text_recipients = removed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const loading = ref(false);
 | 
			
		||||
 | 
			
		||||
async function onSubmit() {
 | 
			
		||||
  // TODO rework this ghetto form validation
 | 
			
		||||
  if (!template.name) {
 | 
			
		||||
    notifyError("Name needs to be set");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loading.value = true;
 | 
			
		||||
 | 
			
		||||
  if (props.alertTemplate) {
 | 
			
		||||
    try {
 | 
			
		||||
      await saveAlertTemplate(template.id, template);
 | 
			
		||||
      notifySuccess("Alert Template edited!");
 | 
			
		||||
      onDialogOK();
 | 
			
		||||
    } catch {
 | 
			
		||||
    } finally {
 | 
			
		||||
      loading.value = false;
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    try {
 | 
			
		||||
      await addAlertTemplate(template);
 | 
			
		||||
      notifySuccess("Alert Template edited!");
 | 
			
		||||
      onDialogOK();
 | 
			
		||||
    } catch {
 | 
			
		||||
    } finally {
 | 
			
		||||
      loading.value = false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -191,24 +191,6 @@
 | 
			
		||||
              }}</q-badge>
 | 
			
		||||
            </q-td>
 | 
			
		||||
          </template>
 | 
			
		||||
 | 
			
		||||
          <template v-slot:body-cell-alert_time="props">
 | 
			
		||||
            <q-td :props="props">
 | 
			
		||||
              {{ formatDate(props.value) }}
 | 
			
		||||
            </q-td>
 | 
			
		||||
          </template>
 | 
			
		||||
 | 
			
		||||
          <template v-slot:body-cell-resolve_on="props">
 | 
			
		||||
            <q-td :props="props">
 | 
			
		||||
              {{ formatDate(props.value) }}
 | 
			
		||||
            </q-td>
 | 
			
		||||
          </template>
 | 
			
		||||
 | 
			
		||||
          <template v-slot:body-cell-snoozed_until="props">
 | 
			
		||||
            <q-td :props="props">
 | 
			
		||||
              {{ formatDate(props.value) }}
 | 
			
		||||
            </q-td>
 | 
			
		||||
          </template>
 | 
			
		||||
        </q-table>
 | 
			
		||||
      </q-card-section>
 | 
			
		||||
    </q-card>
 | 
			
		||||
@@ -265,6 +247,7 @@ export default {
 | 
			
		||||
          field: "alert_time",
 | 
			
		||||
          align: "left",
 | 
			
		||||
          sortable: true,
 | 
			
		||||
          format: (a) => this.formatDate(a),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: "hostname",
 | 
			
		||||
@@ -296,11 +279,12 @@ export default {
 | 
			
		||||
          sortable: true,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: "resolve_on",
 | 
			
		||||
          name: "resolved_on",
 | 
			
		||||
          label: "Resolved On",
 | 
			
		||||
          field: "resolve_on",
 | 
			
		||||
          field: "resolved_on",
 | 
			
		||||
          align: "left",
 | 
			
		||||
          sortable: true,
 | 
			
		||||
          format: (a) => this.formatDate(a),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: "snoozed_until",
 | 
			
		||||
@@ -308,6 +292,7 @@ export default {
 | 
			
		||||
          field: "snoozed_until",
 | 
			
		||||
          align: "left",
 | 
			
		||||
          sortable: true,
 | 
			
		||||
          format: (a) => this.formatDate(a),
 | 
			
		||||
        },
 | 
			
		||||
        { name: "actions", label: "Actions", align: "left" },
 | 
			
		||||
      ],
 | 
			
		||||
@@ -328,7 +313,7 @@ export default {
 | 
			
		||||
      return this.columns.map((column) => {
 | 
			
		||||
        if (column.name === "snoozed_until") {
 | 
			
		||||
          if (this.includeSnoozed) return column.name;
 | 
			
		||||
        } else if (column.name === "resolve_on") {
 | 
			
		||||
        } else if (column.name === "resolved_on") {
 | 
			
		||||
          if (this.includeResolved) return column.name;
 | 
			
		||||
        } else {
 | 
			
		||||
          return column.name;
 | 
			
		||||
@@ -340,7 +325,7 @@ export default {
 | 
			
		||||
    getClients() {
 | 
			
		||||
      this.$axios.get("/clients/").then((r) => {
 | 
			
		||||
        this.clientsOptions = Object.freeze(
 | 
			
		||||
          r.data.map((client) => ({ label: client.name, value: client.id }))
 | 
			
		||||
          r.data.map((client) => ({ label: client.name, value: client.id })),
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@
 | 
			
		||||
          <q-tab name="customfields" label="Custom Fields" />
 | 
			
		||||
          <q-tab name="keystore" label="Key Store" />
 | 
			
		||||
          <q-tab name="urlactions" label="URL Actions" />
 | 
			
		||||
          <q-tab name="webhooks" label="Web Hooks" />
 | 
			
		||||
          <q-tab name="retention" label="Retention" />
 | 
			
		||||
          <q-tab name="apikeys" label="API Keys" />
 | 
			
		||||
          <!-- <q-tab name="openai" label="Open AI" /> -->
 | 
			
		||||
@@ -41,6 +42,51 @@
 | 
			
		||||
                    <q-tooltip> Runs at 35mins past every hour </q-tooltip>
 | 
			
		||||
                  </q-checkbox>
 | 
			
		||||
                </q-card-section>
 | 
			
		||||
                <q-card-section v-if="!hosted" class="row">
 | 
			
		||||
                  <q-checkbox
 | 
			
		||||
                    v-model="settings.enable_server_scripts"
 | 
			
		||||
                    label="Enable server side scripts"
 | 
			
		||||
                  >
 | 
			
		||||
                    <q-tooltip
 | 
			
		||||
                      >Allow running scripts on TRMM server for alert
 | 
			
		||||
                      failure/resolve actions</q-tooltip
 | 
			
		||||
                    >
 | 
			
		||||
                  </q-checkbox>
 | 
			
		||||
                  <q-btn
 | 
			
		||||
                    size="sm"
 | 
			
		||||
                    round
 | 
			
		||||
                    dense
 | 
			
		||||
                    flat
 | 
			
		||||
                    icon="warning"
 | 
			
		||||
                    @click="
 | 
			
		||||
                      openURL(
 | 
			
		||||
                        'https://docs.tacticalrmm.com/functions/permissions/#permissions-with-extra-security-implications',
 | 
			
		||||
                      )
 | 
			
		||||
                    "
 | 
			
		||||
                  >
 | 
			
		||||
                  </q-btn>
 | 
			
		||||
                </q-card-section>
 | 
			
		||||
                <q-card-section v-if="!hosted" class="row">
 | 
			
		||||
                  <q-checkbox
 | 
			
		||||
                    v-model="settings.enable_server_webterminal"
 | 
			
		||||
                    label="Enable web terminal"
 | 
			
		||||
                  >
 | 
			
		||||
                    <q-tooltip>Enable the web terminal</q-tooltip>
 | 
			
		||||
                  </q-checkbox>
 | 
			
		||||
                  <q-btn
 | 
			
		||||
                    size="sm"
 | 
			
		||||
                    roundenable_server_webterminal
 | 
			
		||||
                    dense
 | 
			
		||||
                    flat
 | 
			
		||||
                    icon="warning"
 | 
			
		||||
                    @click="
 | 
			
		||||
                      openURL(
 | 
			
		||||
                        'https://docs.tacticalrmm.com/functions/permissions/#permissions-with-extra-security-implications',
 | 
			
		||||
                      )
 | 
			
		||||
                    "
 | 
			
		||||
                  >
 | 
			
		||||
                  </q-btn>
 | 
			
		||||
                </q-card-section>
 | 
			
		||||
                <q-card-section class="row">
 | 
			
		||||
                  <div class="col-4">Default agent timezone:</div>
 | 
			
		||||
                  <div class="col-2"></div>
 | 
			
		||||
@@ -125,6 +171,24 @@
 | 
			
		||||
                    class="col-6"
 | 
			
		||||
                  />
 | 
			
		||||
                </q-card-section>
 | 
			
		||||
                <q-card-section class="row">
 | 
			
		||||
                  <div class="col-4 flex items-center">
 | 
			
		||||
                    Receive notifications on:
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-2"></div>
 | 
			
		||||
                  <q-checkbox
 | 
			
		||||
                    dense
 | 
			
		||||
                    v-model="settings.notify_on_info_alerts"
 | 
			
		||||
                    class="col-3"
 | 
			
		||||
                    label="Informational Alerts"
 | 
			
		||||
                  />
 | 
			
		||||
                  <q-checkbox
 | 
			
		||||
                    dense
 | 
			
		||||
                    v-model="settings.notify_on_warning_alerts"
 | 
			
		||||
                    class="col-3"
 | 
			
		||||
                    label="Warning Alerts"
 | 
			
		||||
                  />
 | 
			
		||||
                </q-card-section>
 | 
			
		||||
                <q-card-section class="row">
 | 
			
		||||
                  <div class="col-4">Agent Debug Level:</div>
 | 
			
		||||
                  <div class="col-2"></div>
 | 
			
		||||
@@ -389,7 +453,7 @@
 | 
			
		||||
              <q-tab-panel name="meshcentral">
 | 
			
		||||
                <div class="text-subtitle2">MeshCentral Settings</div>
 | 
			
		||||
                <q-separator />
 | 
			
		||||
                <q-card-section class="row">
 | 
			
		||||
                <q-card-section class="row" v-if="!hosted">
 | 
			
		||||
                  <div class="col-4">Username:</div>
 | 
			
		||||
                  <div class="col-2"></div>
 | 
			
		||||
                  <q-input
 | 
			
		||||
@@ -405,7 +469,7 @@
 | 
			
		||||
                    ]"
 | 
			
		||||
                  />
 | 
			
		||||
                </q-card-section>
 | 
			
		||||
                <q-card-section class="row">
 | 
			
		||||
                <q-card-section class="row" v-if="!hosted">
 | 
			
		||||
                  <div class="col-4">Mesh Site:</div>
 | 
			
		||||
                  <div class="col-2"></div>
 | 
			
		||||
                  <q-input
 | 
			
		||||
@@ -415,7 +479,7 @@
 | 
			
		||||
                    class="col-6"
 | 
			
		||||
                  />
 | 
			
		||||
                </q-card-section>
 | 
			
		||||
                <q-card-section class="row">
 | 
			
		||||
                <q-card-section class="row" v-if="!hosted">
 | 
			
		||||
                  <div class="col-4">Mesh Token:</div>
 | 
			
		||||
                  <div class="col-2"></div>
 | 
			
		||||
                  <q-input
 | 
			
		||||
@@ -425,7 +489,7 @@
 | 
			
		||||
                    class="col-6"
 | 
			
		||||
                  />
 | 
			
		||||
                </q-card-section>
 | 
			
		||||
                <q-card-section class="row">
 | 
			
		||||
                <q-card-section class="row" v-if="!hosted">
 | 
			
		||||
                  <div class="col-4">Mesh Device Group Name:</div>
 | 
			
		||||
                  <div class="col-2"></div>
 | 
			
		||||
                  <q-input
 | 
			
		||||
@@ -435,29 +499,81 @@
 | 
			
		||||
                    class="col-6"
 | 
			
		||||
                  />
 | 
			
		||||
                </q-card-section>
 | 
			
		||||
                <q-card-section class="row">
 | 
			
		||||
                  <div class="col-4">
 | 
			
		||||
                    Disable Auto Login for Remote Control and Remote background:
 | 
			
		||||
                <q-card-section class="row" v-if="!hosted">
 | 
			
		||||
                  <div class="col-4 flex items-center">
 | 
			
		||||
                    Sync Mesh Perms with TRMM:
 | 
			
		||||
                    <q-icon
 | 
			
		||||
                      right
 | 
			
		||||
                      name="ion-information-circle-outline"
 | 
			
		||||
                      size="sm"
 | 
			
		||||
                      class="cursor-pointer"
 | 
			
		||||
                    >
 | 
			
		||||
                      <q-tooltip class="text-caption">
 | 
			
		||||
                        It is recommended to keep this option enabled;
 | 
			
		||||
                        otherwise, all TRMM users will have full permissions in
 | 
			
		||||
                        MeshCentral regardless of their permissions in TRMM.
 | 
			
		||||
                      </q-tooltip>
 | 
			
		||||
                    </q-icon>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-2"></div>
 | 
			
		||||
                  <q-checkbox
 | 
			
		||||
                    dense
 | 
			
		||||
                    v-model="settings.mesh_disable_auto_login"
 | 
			
		||||
                    :model-value="settings.sync_mesh_with_trmm"
 | 
			
		||||
                    @update:model-value="confirmSyncChange"
 | 
			
		||||
                    class="col-6"
 | 
			
		||||
                  />
 | 
			
		||||
                </q-card-section>
 | 
			
		||||
 | 
			
		||||
                <q-card-section class="row items-center">
 | 
			
		||||
                  <div class="col-4 flex items-center">
 | 
			
		||||
                    Company Name:
 | 
			
		||||
                    <q-icon
 | 
			
		||||
                      name="ion-information-circle-outline"
 | 
			
		||||
                      size="sm"
 | 
			
		||||
                      class="q-ml-sm cursor-pointer"
 | 
			
		||||
                    >
 | 
			
		||||
                      <q-tooltip class="text-caption">
 | 
			
		||||
                        Adding your company name here will append it to the
 | 
			
		||||
                        user's full name that appears when doing a remote
 | 
			
		||||
                        control session, for example: 'John Doe - Amidaware
 | 
			
		||||
                        Inc.'
 | 
			
		||||
                      </q-tooltip>
 | 
			
		||||
                    </q-icon>
 | 
			
		||||
                  </div>
 | 
			
		||||
 | 
			
		||||
                  <div class="col-2"></div>
 | 
			
		||||
 | 
			
		||||
                  <q-input
 | 
			
		||||
                    dense
 | 
			
		||||
                    outlined
 | 
			
		||||
                    v-model="settings.mesh_company_name"
 | 
			
		||||
                    class="col-6"
 | 
			
		||||
                  >
 | 
			
		||||
                  </q-input>
 | 
			
		||||
                </q-card-section>
 | 
			
		||||
              </q-tab-panel>
 | 
			
		||||
 | 
			
		||||
              <!-- custom fields -->
 | 
			
		||||
              <q-tab-panel name="customfields">
 | 
			
		||||
                <CustomFields />
 | 
			
		||||
              </q-tab-panel>
 | 
			
		||||
 | 
			
		||||
              <!-- key store -->
 | 
			
		||||
              <q-tab-panel name="keystore">
 | 
			
		||||
                <KeyStoreTable />
 | 
			
		||||
              </q-tab-panel>
 | 
			
		||||
 | 
			
		||||
              <!-- url actions -->
 | 
			
		||||
              <q-tab-panel name="urlactions">
 | 
			
		||||
                <URLActionsTable />
 | 
			
		||||
                <URLActionsTable type="web" />
 | 
			
		||||
              </q-tab-panel>
 | 
			
		||||
 | 
			
		||||
              <!-- web hooks -->
 | 
			
		||||
              <q-tab-panel name="webhooks">
 | 
			
		||||
                <URLActionsTable type="rest" />
 | 
			
		||||
              </q-tab-panel>
 | 
			
		||||
 | 
			
		||||
              <!-- retention -->
 | 
			
		||||
              <q-tab-panel name="retention">
 | 
			
		||||
                <q-card-section class="row">
 | 
			
		||||
                  <div class="col-4">Check History (days):</div>
 | 
			
		||||
@@ -615,6 +731,7 @@ export default {
 | 
			
		||||
    KeyStoreTable,
 | 
			
		||||
    URLActionsTable,
 | 
			
		||||
    APIKeysTable,
 | 
			
		||||
    // ServerTasksTable,
 | 
			
		||||
  },
 | 
			
		||||
  mixins: [mixins],
 | 
			
		||||
  data() {
 | 
			
		||||
@@ -645,6 +762,11 @@ export default {
 | 
			
		||||
      ],
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    hosted() {
 | 
			
		||||
      return this.$store.state.hosted;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    openURL(url) {
 | 
			
		||||
      openURL(url);
 | 
			
		||||
@@ -679,6 +801,19 @@ export default {
 | 
			
		||||
        }));
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    confirmSyncChange(newValue) {
 | 
			
		||||
      this.$q
 | 
			
		||||
        .dialog({
 | 
			
		||||
          title: "Are you sure?",
 | 
			
		||||
          message:
 | 
			
		||||
            "This operation may take several minutes to complete in the background and can be very CPU/disk intensive, depending on your hardware and number of agents. Please allow time for the sync to fully complete.",
 | 
			
		||||
          ok: { label: "Yes", color: "primary" },
 | 
			
		||||
          cancel: { label: "No", color: "negative" },
 | 
			
		||||
        })
 | 
			
		||||
        .onOk(() => {
 | 
			
		||||
          this.settings.sync_mesh_with_trmm = newValue;
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    showResetPatchPolicy() {
 | 
			
		||||
      this.$q.dialog({
 | 
			
		||||
        component: ResetPatchPolicy,
 | 
			
		||||
@@ -768,6 +903,7 @@ export default {
 | 
			
		||||
              });
 | 
			
		||||
          } else {
 | 
			
		||||
            this.$emit("close");
 | 
			
		||||
            this.$store.dispatch("getDashInfo", false);
 | 
			
		||||
            this.notifySuccess("Settings were edited!");
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										160
									
								
								src/components/modals/coresettings/TestURLAction.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								src/components/modals/coresettings/TestURLAction.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,160 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
			
		||||
    <q-card class="q-dialog-plugin" style="width: 80vw">
 | 
			
		||||
      <q-bar>
 | 
			
		||||
        Testing {{ urlAction.name }}
 | 
			
		||||
        <q-space />
 | 
			
		||||
        <q-btn dense flat icon="close" v-close-popup>
 | 
			
		||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
			
		||||
        </q-btn>
 | 
			
		||||
      </q-bar>
 | 
			
		||||
 | 
			
		||||
      <q-card-section>
 | 
			
		||||
        <q-option-group
 | 
			
		||||
          v-model="runAgainst"
 | 
			
		||||
          :options="runAgainstOptions"
 | 
			
		||||
          inline
 | 
			
		||||
          dense
 | 
			
		||||
        />
 | 
			
		||||
      </q-card-section>
 | 
			
		||||
 | 
			
		||||
      <q-card-section v-if="runAgainst === 'agent'">
 | 
			
		||||
        <tactical-dropdown
 | 
			
		||||
          v-model="agent"
 | 
			
		||||
          :options="agentOptions"
 | 
			
		||||
          label="Agents"
 | 
			
		||||
          mapOptions
 | 
			
		||||
          filterable
 | 
			
		||||
          dense
 | 
			
		||||
          filled
 | 
			
		||||
        />
 | 
			
		||||
      </q-card-section>
 | 
			
		||||
 | 
			
		||||
      <q-card-section v-else-if="runAgainst === 'site'">
 | 
			
		||||
        <tactical-dropdown
 | 
			
		||||
          v-model="site"
 | 
			
		||||
          :options="siteOptions"
 | 
			
		||||
          label="Sites"
 | 
			
		||||
          mapOptions
 | 
			
		||||
          filterable
 | 
			
		||||
          dense
 | 
			
		||||
          filled
 | 
			
		||||
        />
 | 
			
		||||
      </q-card-section>
 | 
			
		||||
 | 
			
		||||
      <q-card-section v-else-if="runAgainst === 'client'">
 | 
			
		||||
        <tactical-dropdown
 | 
			
		||||
          v-model="client"
 | 
			
		||||
          :options="clientOptions"
 | 
			
		||||
          label="Client"
 | 
			
		||||
          mapOptions
 | 
			
		||||
          filterable
 | 
			
		||||
          dense
 | 
			
		||||
          filled
 | 
			
		||||
        />
 | 
			
		||||
      </q-card-section>
 | 
			
		||||
 | 
			
		||||
      <q-card-section style="height: 60vh" class="scroll">
 | 
			
		||||
        <div>
 | 
			
		||||
          URL:
 | 
			
		||||
          <code>{{ return_url }}</code>
 | 
			
		||||
        </div>
 | 
			
		||||
        <br />
 | 
			
		||||
        <div>
 | 
			
		||||
          Body
 | 
			
		||||
          <q-separator />
 | 
			
		||||
          <code>{{ return_request }}</code>
 | 
			
		||||
        </div>
 | 
			
		||||
        <br />
 | 
			
		||||
        <div>
 | 
			
		||||
          Response
 | 
			
		||||
          <q-separator />
 | 
			
		||||
          <code>{{ return_result }}</code>
 | 
			
		||||
        </div>
 | 
			
		||||
      </q-card-section>
 | 
			
		||||
 | 
			
		||||
      <q-card-actions align="right">
 | 
			
		||||
        <q-btn flat label="Close" v-close-popup />
 | 
			
		||||
        <q-btn
 | 
			
		||||
          :loading="loading"
 | 
			
		||||
          flat
 | 
			
		||||
          label="Run"
 | 
			
		||||
          color="primary"
 | 
			
		||||
          @click="submit"
 | 
			
		||||
        />
 | 
			
		||||
      </q-card-actions>
 | 
			
		||||
    </q-card>
 | 
			
		||||
  </q-dialog>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
// composition imports
 | 
			
		||||
import { ref, reactive, computed } from "vue";
 | 
			
		||||
import { useDialogPluginComponent } from "quasar";
 | 
			
		||||
import { useAgentDropdown } from "@/composables/agents";
 | 
			
		||||
import { useSiteDropdown, useClientDropdown } from "@/composables/clients";
 | 
			
		||||
import { runTestURLAction } from "@/api/core";
 | 
			
		||||
import { URLAction } from "@/types/core/urlactions";
 | 
			
		||||
 | 
			
		||||
// ui imports
 | 
			
		||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
			
		||||
 | 
			
		||||
// define emits
 | 
			
		||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
			
		||||
 | 
			
		||||
// define props
 | 
			
		||||
const props = defineProps<{ urlAction: URLAction }>();
 | 
			
		||||
 | 
			
		||||
// setup quasar
 | 
			
		||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
			
		||||
 | 
			
		||||
// setup dropdowns
 | 
			
		||||
const { agent, agentOptions } = useAgentDropdown({ onMount: true });
 | 
			
		||||
const { client, clientOptions } = useClientDropdown(true);
 | 
			
		||||
const { site, siteOptions } = useSiteDropdown(true);
 | 
			
		||||
 | 
			
		||||
const runAgainst = ref<"agent" | "site" | "client" | "none">("none");
 | 
			
		||||
 | 
			
		||||
const runAgainstOptions = [
 | 
			
		||||
  { label: "Agent", value: "agent" },
 | 
			
		||||
  { label: "Site", value: "site" },
 | 
			
		||||
  { label: "Client", value: "client" },
 | 
			
		||||
  { label: "None", value: "none" },
 | 
			
		||||
];
 | 
			
		||||
const loading = ref(false);
 | 
			
		||||
 | 
			
		||||
const runAgainstID = computed(() => {
 | 
			
		||||
  if (runAgainst.value === "agent") return agent.value;
 | 
			
		||||
  else if (runAgainst.value === "site") return site.value;
 | 
			
		||||
  else if (runAgainst.value === "client") return client.value;
 | 
			
		||||
  else return 0;
 | 
			
		||||
});
 | 
			
		||||
const state = reactive({
 | 
			
		||||
  pattern: props.urlAction.pattern,
 | 
			
		||||
  rest_body: props.urlAction.rest_body,
 | 
			
		||||
  rest_headers: props.urlAction.rest_headers,
 | 
			
		||||
  rest_method: props.urlAction.rest_method,
 | 
			
		||||
  run_instance_type: runAgainst,
 | 
			
		||||
  run_instance_id: runAgainstID,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const return_url = ref("");
 | 
			
		||||
const return_result = ref("");
 | 
			
		||||
const return_request = ref("");
 | 
			
		||||
 | 
			
		||||
async function submit() {
 | 
			
		||||
  loading.value = true;
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const { url, result, body } = await runTestURLAction(state);
 | 
			
		||||
 | 
			
		||||
    return_result.value = result;
 | 
			
		||||
    return_url.value = url;
 | 
			
		||||
    return_request.value = body;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error(e);
 | 
			
		||||
  } finally {
 | 
			
		||||
    loading.value = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,14 +1,31 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <q-dialog ref="dialog" @hide="onHide">
 | 
			
		||||
    <q-card class="q-dialog-plugin" style="width: 60vw">
 | 
			
		||||
  <q-dialog
 | 
			
		||||
    ref="dialogRef"
 | 
			
		||||
    @hide="onDialogHide"
 | 
			
		||||
    @show="loadEditor"
 | 
			
		||||
    @before-hide="cleanupEditors"
 | 
			
		||||
  >
 | 
			
		||||
    <q-card
 | 
			
		||||
      class="q-dialog-plugin"
 | 
			
		||||
      :style="`width: ${props.type === 'web' ? 50 : 60}vw; max-width: ${props.type === 'web' ? 60 : 70}vw`"
 | 
			
		||||
    >
 | 
			
		||||
      <q-bar>
 | 
			
		||||
        {{ title }}
 | 
			
		||||
        {{
 | 
			
		||||
          props.action
 | 
			
		||||
            ? props.type === "web"
 | 
			
		||||
              ? "Edit URL Action"
 | 
			
		||||
              : "Edit Web Hook"
 | 
			
		||||
            : props.type === "web"
 | 
			
		||||
              ? "Add URL Action"
 | 
			
		||||
              : "Add Web Hook"
 | 
			
		||||
        }}
 | 
			
		||||
        <q-space />
 | 
			
		||||
        <q-btn dense flat icon="close" v-close-popup>
 | 
			
		||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
			
		||||
        </q-btn>
 | 
			
		||||
      </q-bar>
 | 
			
		||||
      <q-form @submit="submit">
 | 
			
		||||
 | 
			
		||||
      <div style="max-height: 80vh" class="scroll">
 | 
			
		||||
        <!-- name -->
 | 
			
		||||
        <q-card-section>
 | 
			
		||||
          <q-input
 | 
			
		||||
@@ -26,6 +43,8 @@
 | 
			
		||||
            label="Description"
 | 
			
		||||
            outlined
 | 
			
		||||
            dense
 | 
			
		||||
            type="textarea"
 | 
			
		||||
            rows="2"
 | 
			
		||||
            v-model="localAction.desc"
 | 
			
		||||
          />
 | 
			
		||||
        </q-card-section>
 | 
			
		||||
@@ -41,89 +60,186 @@
 | 
			
		||||
          />
 | 
			
		||||
        </q-card-section>
 | 
			
		||||
 | 
			
		||||
        <q-card-actions align="right">
 | 
			
		||||
          <q-btn flat label="Cancel" v-close-popup />
 | 
			
		||||
          <q-btn flat label="Submit" color="primary" type="submit" />
 | 
			
		||||
        </q-card-actions>
 | 
			
		||||
      </q-form>
 | 
			
		||||
        <q-card-section v-if="type === 'rest'">
 | 
			
		||||
          <q-select
 | 
			
		||||
            v-model="localAction.rest_method"
 | 
			
		||||
            label="Method"
 | 
			
		||||
            :options="URLActionMethods"
 | 
			
		||||
            outlined
 | 
			
		||||
            dense
 | 
			
		||||
            map-options
 | 
			
		||||
            emit-value
 | 
			
		||||
          />
 | 
			
		||||
        </q-card-section>
 | 
			
		||||
 | 
			
		||||
        <q-card-section v-show="type === 'rest'">
 | 
			
		||||
          <q-toolbar>
 | 
			
		||||
            <q-tabs v-model="tab" dense shrink>
 | 
			
		||||
              <q-tab
 | 
			
		||||
                name="body"
 | 
			
		||||
                label="Request Body"
 | 
			
		||||
                :ripple="false"
 | 
			
		||||
                :disable="disableBodyTab"
 | 
			
		||||
              />
 | 
			
		||||
              <q-tab name="headers" label="Request Headers" :ripple="false" />
 | 
			
		||||
            </q-tabs>
 | 
			
		||||
          </q-toolbar>
 | 
			
		||||
          <div ref="editorDiv" :style="{ height: '30vh' }"></div>
 | 
			
		||||
        </q-card-section>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <q-card-actions align="right">
 | 
			
		||||
        <q-btn
 | 
			
		||||
          v-if="type === 'rest'"
 | 
			
		||||
          flat
 | 
			
		||||
          label="Test"
 | 
			
		||||
          color="primary"
 | 
			
		||||
          @click="testWebHook"
 | 
			
		||||
        />
 | 
			
		||||
        <q-btn flat label="Cancel" v-close-popup />
 | 
			
		||||
        <q-btn flat label="Submit" color="primary" @click="submit" />
 | 
			
		||||
      </q-card-actions>
 | 
			
		||||
    </q-card>
 | 
			
		||||
  </q-dialog>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import mixins from "@/mixins/mixins";
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
// composition imports
 | 
			
		||||
import { ref, computed, reactive, watch } from "vue";
 | 
			
		||||
import { useDialogPluginComponent, useQuasar, extend } from "quasar";
 | 
			
		||||
import { editURLAction, saveURLAction } from "@/api/core";
 | 
			
		||||
import { notifySuccess } from "@/utils/notify";
 | 
			
		||||
import { URLAction, URLActionType } from "@/types/core/urlactions";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "URLActionsForm",
 | 
			
		||||
  emits: ["hide", "ok", "cancel"],
 | 
			
		||||
  mixins: [mixins],
 | 
			
		||||
  props: { action: Object },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      localAction: {
 | 
			
		||||
        name: "",
 | 
			
		||||
        desc: "",
 | 
			
		||||
        pattern: "",
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    title() {
 | 
			
		||||
      return this.editing ? "Edit URL Action" : "Add URL Action";
 | 
			
		||||
    },
 | 
			
		||||
    editing() {
 | 
			
		||||
      return !!this.action;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    submit() {
 | 
			
		||||
      this.$q.loading.show();
 | 
			
		||||
// ui imports
 | 
			
		||||
import TestURLAction from "@/components/modals/coresettings/TestURLAction.vue";
 | 
			
		||||
 | 
			
		||||
      let data = {
 | 
			
		||||
        ...this.localAction,
 | 
			
		||||
      };
 | 
			
		||||
import * as monaco from "monaco-editor";
 | 
			
		||||
 | 
			
		||||
      if (this.editing) {
 | 
			
		||||
        this.$axios
 | 
			
		||||
          .put(`/core/urlaction/${data.id}/`, data)
 | 
			
		||||
          .then(() => {
 | 
			
		||||
            this.$q.loading.hide();
 | 
			
		||||
            this.onOk();
 | 
			
		||||
            this.notifySuccess("Url Action was edited!");
 | 
			
		||||
          })
 | 
			
		||||
          .catch(() => {
 | 
			
		||||
            this.$q.loading.hide();
 | 
			
		||||
          });
 | 
			
		||||
      } else {
 | 
			
		||||
        this.$axios
 | 
			
		||||
          .post("/core/urlaction/", data)
 | 
			
		||||
          .then(() => {
 | 
			
		||||
            this.$q.loading.hide();
 | 
			
		||||
            this.onOk();
 | 
			
		||||
            this.notifySuccess("URL Action was added!");
 | 
			
		||||
          })
 | 
			
		||||
          .catch(() => {
 | 
			
		||||
            this.$q.loading.hide();
 | 
			
		||||
          });
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    show() {
 | 
			
		||||
      this.$refs.dialog.show();
 | 
			
		||||
    },
 | 
			
		||||
    hide() {
 | 
			
		||||
      this.$refs.dialog.hide();
 | 
			
		||||
    },
 | 
			
		||||
    onHide() {
 | 
			
		||||
      this.$emit("hide");
 | 
			
		||||
    },
 | 
			
		||||
    onOk() {
 | 
			
		||||
      this.$emit("ok");
 | 
			
		||||
      this.hide();
 | 
			
		||||
    },
 | 
			
		||||
// define emits
 | 
			
		||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
			
		||||
 | 
			
		||||
// define props
 | 
			
		||||
const props = defineProps<{ type: URLActionType; action?: URLAction }>();
 | 
			
		||||
 | 
			
		||||
// setup quasar
 | 
			
		||||
const $q = useQuasar();
 | 
			
		||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
			
		||||
 | 
			
		||||
// static data
 | 
			
		||||
const URLActionMethods = [
 | 
			
		||||
  { value: "get", label: "GET" },
 | 
			
		||||
  { value: "post", label: "POST" },
 | 
			
		||||
  { value: "put", label: "PUT" },
 | 
			
		||||
  { value: "delete", label: "DELETE" },
 | 
			
		||||
  { value: "patch", label: "PATCH" },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const localAction: URLAction = props.action
 | 
			
		||||
  ? reactive(extend({}, props.action))
 | 
			
		||||
  : reactive({
 | 
			
		||||
      name: "",
 | 
			
		||||
      desc: "",
 | 
			
		||||
      pattern: "",
 | 
			
		||||
      action_type: props.type,
 | 
			
		||||
      rest_body: "{\n    \n}",
 | 
			
		||||
      rest_method: "post",
 | 
			
		||||
      rest_headers: `{\n  "Content-Type": "application/json"\n}`, // eslint-disable-line
 | 
			
		||||
    } as URLAction);
 | 
			
		||||
 | 
			
		||||
const disableBodyTab = computed(() =>
 | 
			
		||||
  ["get", "delete"].includes(localAction.rest_method),
 | 
			
		||||
);
 | 
			
		||||
const tab = ref(disableBodyTab.value ? "headers" : "body");
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => localAction.rest_method,
 | 
			
		||||
  () => {
 | 
			
		||||
    disableBodyTab.value ? (tab.value = "headers") : undefined;
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    // If pk prop is set that means we are editing
 | 
			
		||||
    if (this.action) Object.assign(this.localAction, this.action);
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
async function submit() {
 | 
			
		||||
  $q.loading.show();
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    props.action
 | 
			
		||||
      ? await editURLAction(localAction.id, localAction)
 | 
			
		||||
      : await saveURLAction(localAction);
 | 
			
		||||
    onDialogOK();
 | 
			
		||||
    notifySuccess("Url Action was edited!");
 | 
			
		||||
  } catch (e) {}
 | 
			
		||||
 | 
			
		||||
  $q.loading.hide();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const editorDiv = ref<HTMLElement | null>(null);
 | 
			
		||||
let editor: monaco.editor.IStandaloneCodeEditor;
 | 
			
		||||
var modelBodyUri = monaco.Uri.parse("model://body"); // a made up unique URI for our model
 | 
			
		||||
var modelHeadersUri = monaco.Uri.parse("model://headers"); // a made up unique URI for our model
 | 
			
		||||
var modelBody = monaco.editor.createModel(
 | 
			
		||||
  localAction.rest_body,
 | 
			
		||||
  "json",
 | 
			
		||||
  modelBodyUri,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
var modelHeaders = monaco.editor.createModel(
 | 
			
		||||
  localAction.rest_headers,
 | 
			
		||||
  "json",
 | 
			
		||||
  modelHeadersUri,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
function testWebHook() {
 | 
			
		||||
  $q.dialog({
 | 
			
		||||
    component: TestURLAction,
 | 
			
		||||
    componentProps: {
 | 
			
		||||
      urlAction: localAction,
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// watch tab change and change model
 | 
			
		||||
watch(tab, (newValue, oldValue) => {
 | 
			
		||||
  if (oldValue === "body") {
 | 
			
		||||
    localAction.rest_body = editor.getValue();
 | 
			
		||||
  } else if (oldValue === "headers") {
 | 
			
		||||
    localAction.rest_headers = editor.getValue();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (newValue === "body") {
 | 
			
		||||
    editor.setModel(modelBody);
 | 
			
		||||
    editor.setValue(localAction.rest_body);
 | 
			
		||||
  } else if (newValue === "headers") {
 | 
			
		||||
    editor.setModel(modelHeaders);
 | 
			
		||||
    editor.setValue(localAction.rest_headers);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function loadEditor() {
 | 
			
		||||
  const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
			
		||||
 | 
			
		||||
  if (!editorDiv.value) return;
 | 
			
		||||
 | 
			
		||||
  editor = monaco.editor.create(editorDiv.value, {
 | 
			
		||||
    model: tab.value === "body" ? modelBody : modelHeaders,
 | 
			
		||||
    theme: theme,
 | 
			
		||||
    automaticLayout: true,
 | 
			
		||||
    minimap: { enabled: false },
 | 
			
		||||
    quickSuggestions: false,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  editor.onDidChangeModelContent(() => {
 | 
			
		||||
    if (tab.value === "body") {
 | 
			
		||||
      localAction.rest_body = editor.getValue();
 | 
			
		||||
    } else if (tab.value === "headers") {
 | 
			
		||||
      localAction.rest_headers = editor.getValue();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function cleanupEditors() {
 | 
			
		||||
  modelBody.dispose();
 | 
			
		||||
  modelHeaders.dispose();
 | 
			
		||||
  editor.dispose();
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,21 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <div class="row">
 | 
			
		||||
      <div class="text-subtitle2">URL Actions</div>
 | 
			
		||||
      <div class="text-subtitle2">
 | 
			
		||||
        {{
 | 
			
		||||
          props.type === "web"
 | 
			
		||||
            ? "URL Actions"
 | 
			
		||||
            : "Web Hooks for Alert Failure/Resolved Actions"
 | 
			
		||||
        }}
 | 
			
		||||
      </div>
 | 
			
		||||
      <q-space />
 | 
			
		||||
      <q-btn
 | 
			
		||||
        size="sm"
 | 
			
		||||
        color="grey-5"
 | 
			
		||||
        icon="fas fa-plus"
 | 
			
		||||
        text-color="black"
 | 
			
		||||
        label="Add URL Action"
 | 
			
		||||
        @click="addAction"
 | 
			
		||||
        :label="`Add ${props.type === 'web' ? 'URL Action' : 'Web Hook'}`"
 | 
			
		||||
        @click="addURLAction"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
    <q-separator />
 | 
			
		||||
@@ -17,31 +23,36 @@
 | 
			
		||||
      dense
 | 
			
		||||
      :rows="actions"
 | 
			
		||||
      :columns="columns"
 | 
			
		||||
      v-model:pagination="pagination"
 | 
			
		||||
      :pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
 | 
			
		||||
      row-key="id"
 | 
			
		||||
      binary-state-sort
 | 
			
		||||
      hide-pagination
 | 
			
		||||
      virtual-scroll
 | 
			
		||||
      :rows-per-page-options="[0]"
 | 
			
		||||
      no-data-label="No URL Actions added yet"
 | 
			
		||||
      :no-data-label="`No ${props.type === 'web' ? 'URL Actions' : 'Web Hooks'} added yet`"
 | 
			
		||||
      :loading="loading"
 | 
			
		||||
    >
 | 
			
		||||
      <!-- body slots -->
 | 
			
		||||
      <template v-slot:body="props">
 | 
			
		||||
        <q-tr
 | 
			
		||||
          :props="props"
 | 
			
		||||
          class="cursor-pointer"
 | 
			
		||||
          @dblclick="editAction(props.row)"
 | 
			
		||||
          @dblclick="editURLAction(props.row)"
 | 
			
		||||
        >
 | 
			
		||||
          <!-- context menu -->
 | 
			
		||||
          <q-menu context-menu>
 | 
			
		||||
            <q-list dense style="min-width: 200px">
 | 
			
		||||
              <q-item clickable v-close-popup @click="editAction(props.row)">
 | 
			
		||||
              <q-item clickable v-close-popup @click="editURLAction(props.row)">
 | 
			
		||||
                <q-item-section side>
 | 
			
		||||
                  <q-icon name="edit" />
 | 
			
		||||
                </q-item-section>
 | 
			
		||||
                <q-item-section>Edit</q-item-section>
 | 
			
		||||
              </q-item>
 | 
			
		||||
              <q-item clickable v-close-popup @click="deleteAction(props.row)">
 | 
			
		||||
              <q-item
 | 
			
		||||
                clickable
 | 
			
		||||
                v-close-popup
 | 
			
		||||
                @click="deleteURLAction(props.row)"
 | 
			
		||||
              >
 | 
			
		||||
                <q-item-section side>
 | 
			
		||||
                  <q-icon name="delete" />
 | 
			
		||||
                </q-item-section>
 | 
			
		||||
@@ -57,15 +68,15 @@
 | 
			
		||||
          </q-menu>
 | 
			
		||||
          <!-- name -->
 | 
			
		||||
          <q-td>
 | 
			
		||||
            {{ props.row.name }}
 | 
			
		||||
            {{ truncateText(props.row.name, 30) }}
 | 
			
		||||
          </q-td>
 | 
			
		||||
          <!-- desc -->
 | 
			
		||||
          <q-td>
 | 
			
		||||
            {{ props.row.desc }}
 | 
			
		||||
            {{ truncateText(props.row.desc, 20) }}
 | 
			
		||||
          </q-td>
 | 
			
		||||
          <!-- pattern -->
 | 
			
		||||
          <q-td>
 | 
			
		||||
            {{ props.row.pattern }}
 | 
			
		||||
            {{ truncateText(props.row.pattern, 20) }}
 | 
			
		||||
          </q-td>
 | 
			
		||||
        </q-tr>
 | 
			
		||||
      </template>
 | 
			
		||||
@@ -73,105 +84,103 @@
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
// composition imports
 | 
			
		||||
import { ref, onMounted } from "vue";
 | 
			
		||||
import { QTableColumn, useQuasar } from "quasar";
 | 
			
		||||
import { fetchURLActions, removeURLAction } from "@/api/core";
 | 
			
		||||
import { notifySuccess } from "@/utils/notify";
 | 
			
		||||
import { truncateText } from "@/utils/format";
 | 
			
		||||
 | 
			
		||||
// ui imports
 | 
			
		||||
import URLActionsForm from "@/components/modals/coresettings/URLActionsForm.vue";
 | 
			
		||||
import mixins from "@/mixins/mixins";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "URLActionTable",
 | 
			
		||||
  mixins: [mixins],
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      actions: [],
 | 
			
		||||
      pagination: {
 | 
			
		||||
        rowsPerPage: 0,
 | 
			
		||||
        sortBy: "name",
 | 
			
		||||
        descending: true,
 | 
			
		||||
      },
 | 
			
		||||
      columns: [
 | 
			
		||||
        {
 | 
			
		||||
          name: "name",
 | 
			
		||||
          label: "Name",
 | 
			
		||||
          field: "name",
 | 
			
		||||
          align: "left",
 | 
			
		||||
          sortable: true,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: "desc",
 | 
			
		||||
          label: "Description",
 | 
			
		||||
          field: "desc",
 | 
			
		||||
          align: "left",
 | 
			
		||||
          sortable: true,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: "pattern",
 | 
			
		||||
          label: "Pattern",
 | 
			
		||||
          field: "pattern",
 | 
			
		||||
          align: "left",
 | 
			
		||||
          sortable: true,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    getURLActions() {
 | 
			
		||||
      this.$q.loading.show();
 | 
			
		||||
// types
 | 
			
		||||
import { type URLActionType, type URLAction } from "@/types/core/urlactions";
 | 
			
		||||
 | 
			
		||||
      this.$axios
 | 
			
		||||
        .get("/core/urlaction/")
 | 
			
		||||
        .then((r) => {
 | 
			
		||||
          this.$q.loading.hide();
 | 
			
		||||
          this.actions = r.data;
 | 
			
		||||
        })
 | 
			
		||||
        .catch(() => {
 | 
			
		||||
          this.$q.loading.hide();
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    addAction() {
 | 
			
		||||
      this.$q
 | 
			
		||||
        .dialog({
 | 
			
		||||
          component: URLActionsForm,
 | 
			
		||||
        })
 | 
			
		||||
        .onOk(() => {
 | 
			
		||||
          this.getURLActions();
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    editAction(action) {
 | 
			
		||||
      this.$q
 | 
			
		||||
        .dialog({
 | 
			
		||||
          component: URLActionsForm,
 | 
			
		||||
          componentProps: {
 | 
			
		||||
            action: action,
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
        .onOk(() => {
 | 
			
		||||
          this.getURLActions();
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    deleteAction(action) {
 | 
			
		||||
      this.$q
 | 
			
		||||
        .dialog({
 | 
			
		||||
          title: `Delete URL Action: ${action.name}?`,
 | 
			
		||||
          cancel: true,
 | 
			
		||||
          ok: { label: "Delete", color: "negative" },
 | 
			
		||||
        })
 | 
			
		||||
        .onOk(() => {
 | 
			
		||||
          this.$q.loading.show();
 | 
			
		||||
          this.$axios
 | 
			
		||||
            .delete(`/core/urlaction/${action.id}/`)
 | 
			
		||||
            .then(() => {
 | 
			
		||||
              this.getURLActions();
 | 
			
		||||
              this.$q.loading.hide();
 | 
			
		||||
              this.notifySuccess(`URL Action: ${action.name} was deleted!`);
 | 
			
		||||
            })
 | 
			
		||||
            .catch(() => {
 | 
			
		||||
              this.$q.loading.hide();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
// define props
 | 
			
		||||
const props = defineProps<{ type: URLActionType }>();
 | 
			
		||||
 | 
			
		||||
// setup quasar
 | 
			
		||||
const $q = useQuasar();
 | 
			
		||||
 | 
			
		||||
const loading = ref(false);
 | 
			
		||||
 | 
			
		||||
const actions = ref([] as URLAction[]);
 | 
			
		||||
 | 
			
		||||
const columns: QTableColumn[] = [
 | 
			
		||||
  {
 | 
			
		||||
    name: "name",
 | 
			
		||||
    label: "Name",
 | 
			
		||||
    field: "name",
 | 
			
		||||
    align: "left",
 | 
			
		||||
    sortable: true,
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.getURLActions();
 | 
			
		||||
  {
 | 
			
		||||
    name: "desc",
 | 
			
		||||
    label: "Description",
 | 
			
		||||
    field: "desc",
 | 
			
		||||
    align: "left",
 | 
			
		||||
    sortable: true,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
  {
 | 
			
		||||
    name: "pattern",
 | 
			
		||||
    label: "URL Pattern",
 | 
			
		||||
    field: "pattern",
 | 
			
		||||
    align: "left",
 | 
			
		||||
    sortable: true,
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
async function getURLActions() {
 | 
			
		||||
  $q.loading.show();
 | 
			
		||||
  try {
 | 
			
		||||
    const result = await fetchURLActions();
 | 
			
		||||
    actions.value = result.filter(
 | 
			
		||||
      (action) => action.action_type === props.type,
 | 
			
		||||
    );
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error(e);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $q.loading.hide();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addURLAction() {
 | 
			
		||||
  $q.dialog({
 | 
			
		||||
    component: URLActionsForm,
 | 
			
		||||
    componentProps: {
 | 
			
		||||
      type: props.type,
 | 
			
		||||
    },
 | 
			
		||||
  }).onOk(getURLActions);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function editURLAction(action: URLAction) {
 | 
			
		||||
  $q.dialog({
 | 
			
		||||
    component: URLActionsForm,
 | 
			
		||||
    componentProps: {
 | 
			
		||||
      type: props.type,
 | 
			
		||||
      action: action,
 | 
			
		||||
    },
 | 
			
		||||
  }).onOk(getURLActions);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function deleteURLAction(action: URLAction) {
 | 
			
		||||
  $q.dialog({
 | 
			
		||||
    title: `Delete URL Action: ${action.name}?`,
 | 
			
		||||
    cancel: true,
 | 
			
		||||
    ok: { label: "Delete", color: "negative" },
 | 
			
		||||
  }).onOk(async () => {
 | 
			
		||||
    loading.value = true;
 | 
			
		||||
    try {
 | 
			
		||||
      await removeURLAction(action.id);
 | 
			
		||||
      await getURLActions();
 | 
			
		||||
      notifySuccess(`URL Action: ${action.name} was deleted!`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.error(e);
 | 
			
		||||
    }
 | 
			
		||||
    loading.value = false;
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
onMounted(getURLActions);
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -319,10 +319,12 @@ export default {
 | 
			
		||||
          );
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        this.urlActions = r.data.map((action) => ({
 | 
			
		||||
          label: action.name,
 | 
			
		||||
          value: action.id,
 | 
			
		||||
        }));
 | 
			
		||||
        this.urlActions = r.data
 | 
			
		||||
          .filter((action) => action.action_type === "web")
 | 
			
		||||
          .map((action) => ({
 | 
			
		||||
            label: action.name,
 | 
			
		||||
            value: action.id,
 | 
			
		||||
          }));
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    getUserPrefs() {
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,8 @@
 | 
			
		||||
              :readonly="readonly"
 | 
			
		||||
              v-model="script.description"
 | 
			
		||||
              label="Description"
 | 
			
		||||
              type="textarea"
 | 
			
		||||
              rows="2"
 | 
			
		||||
            />
 | 
			
		||||
            <q-select
 | 
			
		||||
              :readonly="readonly"
 | 
			
		||||
@@ -167,7 +169,7 @@
 | 
			
		||||
      </div>
 | 
			
		||||
      <q-card-actions>
 | 
			
		||||
        <tactical-dropdown
 | 
			
		||||
          style="width: 350px"
 | 
			
		||||
          style="width: 450px"
 | 
			
		||||
          dense
 | 
			
		||||
          :loading="agentLoading"
 | 
			
		||||
          filled
 | 
			
		||||
@@ -187,7 +189,21 @@
 | 
			
		||||
              :disable="
 | 
			
		||||
                !agent || !script.script_body || !script.default_timeout
 | 
			
		||||
              "
 | 
			
		||||
              @click="openTestScriptModal"
 | 
			
		||||
              @click="openTestScriptModal('agent')"
 | 
			
		||||
            />
 | 
			
		||||
            <q-btn
 | 
			
		||||
              v-if="!hosted"
 | 
			
		||||
              size="md"
 | 
			
		||||
              color="secondary"
 | 
			
		||||
              dense
 | 
			
		||||
              flat
 | 
			
		||||
              label="Test on Server"
 | 
			
		||||
              :disable="
 | 
			
		||||
                !script.script_body ||
 | 
			
		||||
                !script.default_timeout ||
 | 
			
		||||
                !server_scripts_enabled
 | 
			
		||||
              "
 | 
			
		||||
              @click="openTestScriptModal('server')"
 | 
			
		||||
            />
 | 
			
		||||
          </template>
 | 
			
		||||
        </tactical-dropdown>
 | 
			
		||||
@@ -215,13 +231,42 @@ import { useQuasar, useDialogPluginComponent } from "quasar";
 | 
			
		||||
import { saveScript, editScript, downloadScript } from "@/api/scripts";
 | 
			
		||||
import { useAgentDropdown, agentPlatformOptions } from "@/composables/agents";
 | 
			
		||||
import { generateScript } from "@/api/core";
 | 
			
		||||
import { notifySuccess } from "@/utils/notify";
 | 
			
		||||
import { notifyError, notifySuccess } from "@/utils/notify";
 | 
			
		||||
 | 
			
		||||
// ui imports
 | 
			
		||||
import TestScriptModal from "@/components/scripts/TestScriptModal.vue";
 | 
			
		||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
			
		||||
import * as monaco from "monaco-editor";
 | 
			
		||||
 | 
			
		||||
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
 | 
			
		||||
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
 | 
			
		||||
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
 | 
			
		||||
import jsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
 | 
			
		||||
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
 | 
			
		||||
 | 
			
		||||
// https://github.com/microsoft/monaco-editor/issues/4045#issuecomment-1723787448
 | 
			
		||||
self.MonacoEnvironment = {
 | 
			
		||||
  getWorker: function (workerId, label) {
 | 
			
		||||
    switch (label) {
 | 
			
		||||
      case "json":
 | 
			
		||||
        return new jsonWorker();
 | 
			
		||||
      case "css":
 | 
			
		||||
      case "scss":
 | 
			
		||||
      case "less":
 | 
			
		||||
        return new cssWorker();
 | 
			
		||||
      case "html":
 | 
			
		||||
      case "handlebars":
 | 
			
		||||
      case "razor":
 | 
			
		||||
        return new htmlWorker();
 | 
			
		||||
      case "typescript":
 | 
			
		||||
      case "javascript":
 | 
			
		||||
        return new jsWorker();
 | 
			
		||||
      default:
 | 
			
		||||
        return new editorWorker();
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// types
 | 
			
		||||
import type { Script } from "@/types/scripts";
 | 
			
		||||
 | 
			
		||||
@@ -256,6 +301,10 @@ const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
 | 
			
		||||
 | 
			
		||||
// setup agent dropdown
 | 
			
		||||
const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
 | 
			
		||||
const hosted = computed(() => store.state.hosted);
 | 
			
		||||
const server_scripts_enabled = computed(
 | 
			
		||||
  () => store.state.server_scripts_enabled,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// script form logic
 | 
			
		||||
const script: Script = props.script
 | 
			
		||||
@@ -276,7 +325,7 @@ const agentLoading = ref(false);
 | 
			
		||||
 | 
			
		||||
const missingShebang = computed(() => {
 | 
			
		||||
  if (script.shell === "shell" || script.shell === "python") {
 | 
			
		||||
    return !script.script_body.includes("#!");
 | 
			
		||||
    return !script.script_body.startsWith("#!");
 | 
			
		||||
  } else {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -287,8 +336,8 @@ const title = computed(() => {
 | 
			
		||||
    return props.readonly
 | 
			
		||||
      ? `Viewing ${script.name}`
 | 
			
		||||
      : props.clone
 | 
			
		||||
      ? `Copying ${script.name}`
 | 
			
		||||
      : `Editing ${script.name}`;
 | 
			
		||||
        ? `Copying ${script.name}`
 | 
			
		||||
        : `Editing ${script.name}`;
 | 
			
		||||
  } else {
 | 
			
		||||
    return "Adding new script";
 | 
			
		||||
  }
 | 
			
		||||
@@ -296,11 +345,21 @@ const title = computed(() => {
 | 
			
		||||
 | 
			
		||||
// convert highlighter language to match what ace expects
 | 
			
		||||
const lang = computed(() => {
 | 
			
		||||
  if (script.shell === "cmd") return "bat";
 | 
			
		||||
  else if (script.shell === "powershell") return "powershell";
 | 
			
		||||
  else if (script.shell === "python") return "python";
 | 
			
		||||
  else if (script.shell === "shell") return "shell";
 | 
			
		||||
  else return "";
 | 
			
		||||
  switch (script.shell) {
 | 
			
		||||
    case "cmd":
 | 
			
		||||
      return "bat";
 | 
			
		||||
    case "powershell":
 | 
			
		||||
      return "powershell";
 | 
			
		||||
    case "python":
 | 
			
		||||
      return "python";
 | 
			
		||||
    case "shell":
 | 
			
		||||
    case "nushell":
 | 
			
		||||
      return "shell";
 | 
			
		||||
    case "deno":
 | 
			
		||||
      return "typescript";
 | 
			
		||||
    default:
 | 
			
		||||
      return "";
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function submit() {
 | 
			
		||||
@@ -325,12 +384,20 @@ async function submit() {
 | 
			
		||||
  loading.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function openTestScriptModal() {
 | 
			
		||||
function openTestScriptModal(ctx: string) {
 | 
			
		||||
  if (ctx === "server" && !script.script_body.startsWith("#!")) {
 | 
			
		||||
    notifyError(
 | 
			
		||||
      "A shebang is required at the top of the script to specify the interpreter's path. Please ensure your script begins with a shebang line.",
 | 
			
		||||
      7000,
 | 
			
		||||
    );
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  $q.dialog({
 | 
			
		||||
    component: TestScriptModal,
 | 
			
		||||
    componentProps: {
 | 
			
		||||
      script: { ...script },
 | 
			
		||||
      agent: agent.value,
 | 
			
		||||
      ctx: ctx,
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -339,12 +406,7 @@ const scriptEditor = ref<HTMLElement | null>(null);
 | 
			
		||||
let editor: monaco.editor.IStandaloneCodeEditor;
 | 
			
		||||
 | 
			
		||||
function loadEditor() {
 | 
			
		||||
  var modelUri = monaco.Uri.parse("model://new"); // a made up unique URI for our model
 | 
			
		||||
  var model = monaco.editor.createModel(
 | 
			
		||||
    script.script_body,
 | 
			
		||||
    lang.value,
 | 
			
		||||
    modelUri,
 | 
			
		||||
  );
 | 
			
		||||
  var model = monaco.editor.createModel(script.script_body, lang.value);
 | 
			
		||||
 | 
			
		||||
  const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -175,6 +175,20 @@
 | 
			
		||||
              >
 | 
			
		||||
                <q-tooltip> Shell </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
              <q-icon
 | 
			
		||||
                v-else-if="props.node.shell === 'nushell'"
 | 
			
		||||
                name="mdi-code-greater-than"
 | 
			
		||||
                color="primary"
 | 
			
		||||
              >
 | 
			
		||||
                <q-tooltip> Nushell </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
              <q-icon
 | 
			
		||||
                v-else-if="props.node.shell === 'deno'"
 | 
			
		||||
                name="mdi-language-typescript"
 | 
			
		||||
                color="primary"
 | 
			
		||||
              >
 | 
			
		||||
                <q-tooltip> Deno </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
 | 
			
		||||
              <!-- is community script icon -->
 | 
			
		||||
              <img
 | 
			
		||||
@@ -471,6 +485,22 @@
 | 
			
		||||
              >
 | 
			
		||||
                <q-tooltip> Shell </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
              <q-icon
 | 
			
		||||
                v-else-if="props.row.shell === 'nushell'"
 | 
			
		||||
                size="sm"
 | 
			
		||||
                name="mdi-code-greater-than"
 | 
			
		||||
                color="primary"
 | 
			
		||||
              >
 | 
			
		||||
                <q-tooltip> Nushell </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
              <q-icon
 | 
			
		||||
                v-else-if="props.row.shell === 'deno'"
 | 
			
		||||
                size="sm"
 | 
			
		||||
                name="mdi-language-typescript"
 | 
			
		||||
                color="primary"
 | 
			
		||||
              >
 | 
			
		||||
                <q-tooltip> Deno </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
            </q-td>
 | 
			
		||||
            <!-- supported platforms -->
 | 
			
		||||
            <q-td key="supported_platforms" :props="props">
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								src/components/scripts/ScriptOutputCopyClip.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/components/scripts/ScriptOutputCopyClip.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="row q-gutter-sm items-center">
 | 
			
		||||
    <div class="col-auto">{{ label }}</div>
 | 
			
		||||
    <div class="col-auto">
 | 
			
		||||
      <q-btn dense flat size="md" icon="content_copy" @click="copyText">
 | 
			
		||||
        <q-tooltip>Copy to Clipboard</q-tooltip>
 | 
			
		||||
      </q-btn>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { copyOutput } from "@/utils/helpers";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  label: String,
 | 
			
		||||
  data: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const copyText = () => {
 | 
			
		||||
  copyOutput(props.data);
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
@@ -86,6 +86,35 @@ import { notifySuccess } from "@/utils/notify";
 | 
			
		||||
// ui imports
 | 
			
		||||
import * as monaco from "monaco-editor";
 | 
			
		||||
 | 
			
		||||
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
 | 
			
		||||
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
 | 
			
		||||
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
 | 
			
		||||
import jsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
 | 
			
		||||
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
 | 
			
		||||
 | 
			
		||||
// https://github.com/microsoft/monaco-editor/issues/4045#issuecomment-1723787448
 | 
			
		||||
self.MonacoEnvironment = {
 | 
			
		||||
  getWorker: function (workerId, label) {
 | 
			
		||||
    switch (label) {
 | 
			
		||||
      case "json":
 | 
			
		||||
        return new jsonWorker();
 | 
			
		||||
      case "css":
 | 
			
		||||
      case "scss":
 | 
			
		||||
      case "less":
 | 
			
		||||
        return new cssWorker();
 | 
			
		||||
      case "html":
 | 
			
		||||
      case "handlebars":
 | 
			
		||||
      case "razor":
 | 
			
		||||
        return new htmlWorker();
 | 
			
		||||
      case "typescript":
 | 
			
		||||
      case "javascript":
 | 
			
		||||
        return new jsWorker();
 | 
			
		||||
      default:
 | 
			
		||||
        return new editorWorker();
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// types
 | 
			
		||||
import type { ScriptSnippet } from "@/types/scripts";
 | 
			
		||||
 | 
			
		||||
@@ -124,11 +153,21 @@ const title = computed(() => {
 | 
			
		||||
 | 
			
		||||
// convert highlighter language to match what ace expects
 | 
			
		||||
const lang = computed(() => {
 | 
			
		||||
  if (snippet.shell === "cmd") return "bat";
 | 
			
		||||
  else if (snippet.shell === "powershell") return "powershell";
 | 
			
		||||
  else if (snippet.shell === "python") return "python";
 | 
			
		||||
  else if (snippet.shell === "shell") return "shell";
 | 
			
		||||
  else return "";
 | 
			
		||||
  switch (snippet.shell) {
 | 
			
		||||
    case "cmd":
 | 
			
		||||
      return "bat";
 | 
			
		||||
    case "powershell":
 | 
			
		||||
      return "powershell";
 | 
			
		||||
    case "python":
 | 
			
		||||
      return "python";
 | 
			
		||||
    case "shell":
 | 
			
		||||
    case "nushell":
 | 
			
		||||
      return "shell";
 | 
			
		||||
    case "deno":
 | 
			
		||||
      return "typescript";
 | 
			
		||||
    default:
 | 
			
		||||
      return "";
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function submit() {
 | 
			
		||||
@@ -150,8 +189,7 @@ const snippetEditor = ref<HTMLElement | null>(null);
 | 
			
		||||
let editor: monaco.editor.IStandaloneCodeEditor;
 | 
			
		||||
 | 
			
		||||
function loadEditor() {
 | 
			
		||||
  var modelUri = monaco.Uri.parse("model://snippet"); // a made up unique URI for our model
 | 
			
		||||
  var model = monaco.editor.createModel(snippet.code, lang.value, modelUri);
 | 
			
		||||
  var model = monaco.editor.createModel(snippet.code, lang.value);
 | 
			
		||||
 | 
			
		||||
  const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -124,6 +124,22 @@
 | 
			
		||||
              >
 | 
			
		||||
                <q-tooltip> Shell </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
              <q-icon
 | 
			
		||||
                v-else-if="props.row.shell === 'nushell'"
 | 
			
		||||
                name="mdi-nushell"
 | 
			
		||||
                color="primary"
 | 
			
		||||
                size="sm"
 | 
			
		||||
              >
 | 
			
		||||
                <q-tooltip> Nushell </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
              <q-icon
 | 
			
		||||
                v-else-if="props.row.shell === 'deno'"
 | 
			
		||||
                name="mdi-typescript"
 | 
			
		||||
                color="primary"
 | 
			
		||||
                size="sm"
 | 
			
		||||
              >
 | 
			
		||||
                <q-tooltip> Deno </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
            </q-td>
 | 
			
		||||
            <!-- name -->
 | 
			
		||||
            <q-td>{{ props.row.name }}</q-td>
 | 
			
		||||
 
 | 
			
		||||
@@ -18,12 +18,12 @@
 | 
			
		||||
        </div>
 | 
			
		||||
        <br />
 | 
			
		||||
        <div v-if="ret.stdout">
 | 
			
		||||
          Standard Output
 | 
			
		||||
          <script-output-copy-clip label="Standard Output" :data="ret.stdout" />
 | 
			
		||||
          <q-separator />
 | 
			
		||||
          <pre>{{ ret.stdout }}</pre>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="ret.stderr">
 | 
			
		||||
          Standard Error
 | 
			
		||||
          <script-output-copy-clip label="Standard Error" :data="ret.stderr" />
 | 
			
		||||
          <q-separator />
 | 
			
		||||
          <pre>{{ ret.stderr }}</pre>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -36,15 +36,20 @@
 | 
			
		||||
<script>
 | 
			
		||||
// composition imports
 | 
			
		||||
import { ref, onMounted } from "vue";
 | 
			
		||||
import { testScript } from "@/api/scripts";
 | 
			
		||||
import { testScript, testScriptOnServer } from "@/api/scripts";
 | 
			
		||||
import { useDialogPluginComponent } from "quasar";
 | 
			
		||||
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "TestScriptModal",
 | 
			
		||||
  components: {
 | 
			
		||||
    ScriptOutputCopyClip,
 | 
			
		||||
  },
 | 
			
		||||
  emits: [...useDialogPluginComponent.emits],
 | 
			
		||||
  props: {
 | 
			
		||||
    script: !Object,
 | 
			
		||||
    agent: !String,
 | 
			
		||||
    ctx: !String,
 | 
			
		||||
  },
 | 
			
		||||
  setup(props) {
 | 
			
		||||
    // setup quasar dialog plugin
 | 
			
		||||
@@ -70,7 +75,11 @@ export default {
 | 
			
		||||
        env_vars: props.script.env_vars,
 | 
			
		||||
      };
 | 
			
		||||
      try {
 | 
			
		||||
        ret.value = await testScript(props.agent, data);
 | 
			
		||||
        if (props.ctx === "server") {
 | 
			
		||||
          ret.value = await testScriptOnServer(data);
 | 
			
		||||
        } else {
 | 
			
		||||
          ret.value = await testScript(props.agent, data);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        console.error(e);
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -755,7 +755,7 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
// composition imports
 | 
			
		||||
import { ref, watch, onMounted } from "vue";
 | 
			
		||||
import { ref, watch, onMounted, defineComponent } from "vue";
 | 
			
		||||
import { useDialogPluginComponent } from "quasar";
 | 
			
		||||
import draggable from "vuedraggable";
 | 
			
		||||
import { saveTask, updateTask } from "@/api/tasks";
 | 
			
		||||
@@ -843,7 +843,7 @@ const taskInstancePolicyOptions = [
 | 
			
		||||
  { label: "Stop Existing", value: 3 },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  components: { TacticalDropdown, draggable },
 | 
			
		||||
  name: "AddAutomatedTask",
 | 
			
		||||
  emits: [...useDialogPluginComponent.emits],
 | 
			
		||||
@@ -858,18 +858,19 @@ export default {
 | 
			
		||||
    // setup dropdowns
 | 
			
		||||
    const {
 | 
			
		||||
      script,
 | 
			
		||||
      scriptName,
 | 
			
		||||
      scriptOptions,
 | 
			
		||||
      defaultTimeout,
 | 
			
		||||
      defaultArgs,
 | 
			
		||||
      defaultEnvVars,
 | 
			
		||||
    } = useScriptDropdown(undefined, {
 | 
			
		||||
    } = useScriptDropdown({
 | 
			
		||||
      onMount: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // set defaultTimeout to 30
 | 
			
		||||
    defaultTimeout.value = 30;
 | 
			
		||||
 | 
			
		||||
    const { checkOptions, getCheckOptions } = useCheckDropdown();
 | 
			
		||||
    const { checkOptions, getCheckOptions } = useCheckDropdown(props.parent);
 | 
			
		||||
    const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
 | 
			
		||||
 | 
			
		||||
    // add task logic
 | 
			
		||||
@@ -952,9 +953,7 @@ export default {
 | 
			
		||||
      if (actionType.value === "script") {
 | 
			
		||||
        task.value.actions.push({
 | 
			
		||||
          type: "script",
 | 
			
		||||
          name: scriptOptions.value.find(
 | 
			
		||||
            (option) => option.value === script.value,
 | 
			
		||||
          ).label,
 | 
			
		||||
          name: scriptName.value,
 | 
			
		||||
          script: script.value,
 | 
			
		||||
          timeout: defaultTimeout.value,
 | 
			
		||||
          script_args: defaultArgs.value,
 | 
			
		||||
@@ -1179,7 +1178,7 @@ export default {
 | 
			
		||||
      onDialogHide,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
import { computed, ref } from "vue";
 | 
			
		||||
import { ref, computed, onMounted } from "vue";
 | 
			
		||||
import { useStore } from "vuex";
 | 
			
		||||
import { fetchAgents } from "@/api/agents";
 | 
			
		||||
import { formatAgentOptions } from "@/utils/format";
 | 
			
		||||
 | 
			
		||||
// agent dropdown
 | 
			
		||||
export function useAgentDropdown() {
 | 
			
		||||
export function useAgentDropdown(opts = {}) {
 | 
			
		||||
  const agent = ref(null);
 | 
			
		||||
  const agents = ref([]);
 | 
			
		||||
  const agentOptions = ref([]);
 | 
			
		||||
@@ -13,10 +13,14 @@ export function useAgentDropdown() {
 | 
			
		||||
  async function getAgentOptions(flat = false) {
 | 
			
		||||
    agentOptions.value = formatAgentOptions(
 | 
			
		||||
      await fetchAgents({ detail: false }),
 | 
			
		||||
      flat
 | 
			
		||||
      flat,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (opts.onMount) {
 | 
			
		||||
    onMounted(getAgentOptions);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    //data
 | 
			
		||||
    agent,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
import { ref, onMounted } from "vue";
 | 
			
		||||
import { fetchCustomFields } from "@/api/core";
 | 
			
		||||
import { formatCustomFieldOptions } from "@/utils/format";
 | 
			
		||||
 | 
			
		||||
export function useCustomFieldDropdown({ onMount = false }) {
 | 
			
		||||
  const customFieldOptions = ref([]);
 | 
			
		||||
 | 
			
		||||
  // type can be "client", "site", or "agent"
 | 
			
		||||
  async function getCustomFieldOptions(model = null, flat = false) {
 | 
			
		||||
    const params = {};
 | 
			
		||||
 | 
			
		||||
    if (model) params[model] = model;
 | 
			
		||||
    customFieldOptions.value = formatCustomFieldOptions(
 | 
			
		||||
      await fetchCustomFields(params),
 | 
			
		||||
      flat
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (onMount) onMounted(getCustomFieldOptions);
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    //data
 | 
			
		||||
    customFieldOptions,
 | 
			
		||||
 | 
			
		||||
    //methods
 | 
			
		||||
    getCustomFieldOptions,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										88
									
								
								src/composables/core.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/composables/core.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
import { ref, computed, onMounted } from "vue";
 | 
			
		||||
import { fetchCustomFields, fetchURLActions } from "@/api/core";
 | 
			
		||||
import {
 | 
			
		||||
  formatCustomFieldOptions,
 | 
			
		||||
  formatURLActionOptions,
 | 
			
		||||
} from "@/utils/format";
 | 
			
		||||
import type { CustomField } from "@/types/core/customfields";
 | 
			
		||||
import type { URLAction } from "@/types/core/urlactions";
 | 
			
		||||
 | 
			
		||||
export interface URLActionOption extends URLAction {
 | 
			
		||||
  value: number;
 | 
			
		||||
  label: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CustomFieldOption extends CustomField {
 | 
			
		||||
  value: number;
 | 
			
		||||
  label: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UseCustomFieldDropdownParams {
 | 
			
		||||
  onMount?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useCustomFieldDropdown(opts: UseCustomFieldDropdownParams) {
 | 
			
		||||
  const customFieldOptions = ref([] as CustomFieldOption[]);
 | 
			
		||||
 | 
			
		||||
  // type can be "client", "site", or "agent"
 | 
			
		||||
  async function getCustomFieldOptions(model = null, flat = false) {
 | 
			
		||||
    const params = {};
 | 
			
		||||
 | 
			
		||||
    if (model) params[model] = model;
 | 
			
		||||
    customFieldOptions.value = formatCustomFieldOptions(
 | 
			
		||||
      await fetchCustomFields(params),
 | 
			
		||||
      flat,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const restActionOptions = computed(() =>
 | 
			
		||||
    customFieldOptions.value.filter((option) => option.type === "rest"),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (opts.onMount) onMounted(getCustomFieldOptions);
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    customFieldOptions,
 | 
			
		||||
    restActionOptions,
 | 
			
		||||
 | 
			
		||||
    //methods
 | 
			
		||||
    getCustomFieldOptions,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UseURLActionDropdownParams {
 | 
			
		||||
  onMount?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useURLActionDropdown(opts: UseURLActionDropdownParams) {
 | 
			
		||||
  const urlActionOptions = ref([] as URLActionOption[]);
 | 
			
		||||
 | 
			
		||||
  // type can be "client", "site", or "agent"
 | 
			
		||||
  async function getURLActionOptions(flat = false) {
 | 
			
		||||
    const params = {};
 | 
			
		||||
 | 
			
		||||
    urlActionOptions.value = formatURLActionOptions(
 | 
			
		||||
      await fetchURLActions(params),
 | 
			
		||||
      flat,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const webActionOptions = computed(() =>
 | 
			
		||||
    urlActionOptions.value.filter((action) => action.action_type === "web"),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const restActionOptions = computed(() =>
 | 
			
		||||
    urlActionOptions.value.filter((action) => action.action_type === "rest"),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (opts?.onMount) onMounted(getURLActionOptions);
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    urlActionOptions,
 | 
			
		||||
    restActionOptions,
 | 
			
		||||
    webActionOptions,
 | 
			
		||||
 | 
			
		||||
    //methods
 | 
			
		||||
    getURLActionOptions,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -1,68 +0,0 @@
 | 
			
		||||
import { ref, watch, computed, onMounted } from "vue";
 | 
			
		||||
import { useStore } from "vuex";
 | 
			
		||||
import { fetchScripts } from "@/api/scripts";
 | 
			
		||||
import { formatScriptOptions } from "@/utils/format";
 | 
			
		||||
 | 
			
		||||
// script dropdown
 | 
			
		||||
export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
 | 
			
		||||
  const scriptOptions = ref([]);
 | 
			
		||||
  const defaultTimeout = ref(30);
 | 
			
		||||
  const defaultArgs = ref([]);
 | 
			
		||||
  const defaultEnvVars = ref([]);
 | 
			
		||||
  const script = ref(setScript);
 | 
			
		||||
  const syntax = ref("");
 | 
			
		||||
  const link = ref("");
 | 
			
		||||
  const baseUrl =
 | 
			
		||||
    "https://github.com/amidaware/community-scripts/blob/main/scripts/";
 | 
			
		||||
 | 
			
		||||
  // specify parameters to filter out community scripts
 | 
			
		||||
  async function getScriptOptions(showCommunityScripts = false) {
 | 
			
		||||
    scriptOptions.value = Object.freeze(
 | 
			
		||||
      formatScriptOptions(await fetchScripts({ showCommunityScripts }))
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // watch scriptPk for changes and update the default timeout and args
 | 
			
		||||
  watch([script, scriptOptions], () => {
 | 
			
		||||
    if (script.value && scriptOptions.value.length > 0) {
 | 
			
		||||
      const tmpScript = scriptOptions.value.find(
 | 
			
		||||
        (i) => i.value === script.value
 | 
			
		||||
      );
 | 
			
		||||
      defaultTimeout.value = tmpScript.timeout;
 | 
			
		||||
      defaultArgs.value = tmpScript.args;
 | 
			
		||||
      defaultEnvVars.value = tmpScript.env_vars;
 | 
			
		||||
      syntax.value = tmpScript.syntax;
 | 
			
		||||
      link.value =
 | 
			
		||||
        tmpScript.script_type === "builtin"
 | 
			
		||||
          ? `${baseUrl}${tmpScript.filename}`
 | 
			
		||||
          : null;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // vuex show community scripts
 | 
			
		||||
  const store = useStore();
 | 
			
		||||
  const showCommunityScripts = computed(() => store.state.showCommunityScripts);
 | 
			
		||||
 | 
			
		||||
  if (onMount) onMounted(() => getScriptOptions(showCommunityScripts.value));
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    //data
 | 
			
		||||
    script,
 | 
			
		||||
    scriptOptions,
 | 
			
		||||
    defaultTimeout,
 | 
			
		||||
    defaultArgs,
 | 
			
		||||
    defaultEnvVars,
 | 
			
		||||
    syntax,
 | 
			
		||||
    link,
 | 
			
		||||
 | 
			
		||||
    //methods
 | 
			
		||||
    getScriptOptions,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const shellOptions = [
 | 
			
		||||
  { label: "Powershell", value: "powershell" },
 | 
			
		||||
  { label: "Batch", value: "cmd" },
 | 
			
		||||
  { label: "Python", value: "python" },
 | 
			
		||||
  { label: "Shell", value: "shell" },
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										141
									
								
								src/composables/scripts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/composables/scripts.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,141 @@
 | 
			
		||||
import { ref, watch, computed, onMounted } from "vue";
 | 
			
		||||
import { useStore } from "vuex";
 | 
			
		||||
import { fetchScripts } from "@/api/scripts";
 | 
			
		||||
import {
 | 
			
		||||
  formatScriptOptions,
 | 
			
		||||
  removeExtraOptionCategories,
 | 
			
		||||
} from "@/utils/format";
 | 
			
		||||
import type { Script } from "@/types/scripts";
 | 
			
		||||
import { AgentPlatformType } from "@/types/agents";
 | 
			
		||||
 | 
			
		||||
export interface ScriptOption extends Script {
 | 
			
		||||
  label: string;
 | 
			
		||||
  value: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface useScriptDropdownParams {
 | 
			
		||||
  script?: number; // set a selected script on init
 | 
			
		||||
  plat?: AgentPlatformType; // set a platform for filterByPlatform
 | 
			
		||||
  onMount?: boolean; // loads script options on mount
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// script dropdown
 | 
			
		||||
export function useScriptDropdown(opts?: useScriptDropdownParams) {
 | 
			
		||||
  const scriptOptions = ref([] as ScriptOption[]);
 | 
			
		||||
  const defaultTimeout = ref(30);
 | 
			
		||||
  const defaultArgs = ref([] as string[]);
 | 
			
		||||
  const defaultEnvVars = ref([] as string[]);
 | 
			
		||||
  const script = ref(opts?.script);
 | 
			
		||||
  const scriptName = ref("");
 | 
			
		||||
  const syntax = ref<string | undefined>("");
 | 
			
		||||
  const link = ref<string | undefined>("");
 | 
			
		||||
  const plat = ref<AgentPlatformType | undefined>(opts?.plat);
 | 
			
		||||
  const baseUrl =
 | 
			
		||||
    "https://github.com/amidaware/community-scripts/blob/main/scripts/";
 | 
			
		||||
 | 
			
		||||
  // specify parameters to filter out community scripts
 | 
			
		||||
  async function getScriptOptions() {
 | 
			
		||||
    scriptOptions.value = Object.freeze(
 | 
			
		||||
      formatScriptOptions(
 | 
			
		||||
        await fetchScripts({
 | 
			
		||||
          showCommunityScripts: showCommunityScripts.value,
 | 
			
		||||
        }),
 | 
			
		||||
      ),
 | 
			
		||||
    ) as ScriptOption[];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // watch scriptPk for changes and update the default timeout and args
 | 
			
		||||
  watch([script, scriptOptions], () => {
 | 
			
		||||
    if (script.value && scriptOptions.value.length > 0) {
 | 
			
		||||
      const tmpScript = scriptOptions.value.find(
 | 
			
		||||
        (i) => i.value === script.value,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (tmpScript) {
 | 
			
		||||
        defaultTimeout.value = tmpScript.default_timeout;
 | 
			
		||||
        defaultArgs.value = tmpScript.args;
 | 
			
		||||
        defaultEnvVars.value = tmpScript.env_vars;
 | 
			
		||||
        syntax.value = tmpScript.syntax;
 | 
			
		||||
        scriptName.value = tmpScript.label;
 | 
			
		||||
        link.value =
 | 
			
		||||
          tmpScript.script_type === "builtin"
 | 
			
		||||
            ? `${baseUrl}${tmpScript.filename}`
 | 
			
		||||
            : undefined;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // vuex show community scripts
 | 
			
		||||
  const store = useStore();
 | 
			
		||||
  const showCommunityScripts = computed(() => store.state.showCommunityScripts);
 | 
			
		||||
 | 
			
		||||
  // filter for only getting server tasks
 | 
			
		||||
  const serverScriptOptions = computed(
 | 
			
		||||
    () =>
 | 
			
		||||
      removeExtraOptionCategories(
 | 
			
		||||
        scriptOptions.value.filter(
 | 
			
		||||
          (script) =>
 | 
			
		||||
            script.category ||
 | 
			
		||||
            !script.supported_platforms ||
 | 
			
		||||
            script.supported_platforms.length === 0 ||
 | 
			
		||||
            script.supported_platforms.includes("linux"),
 | 
			
		||||
        ),
 | 
			
		||||
      ) as ScriptOption[],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const filterByPlatformOptions = computed(() => {
 | 
			
		||||
    if (!plat.value) {
 | 
			
		||||
      return scriptOptions.value;
 | 
			
		||||
    } else {
 | 
			
		||||
      return removeExtraOptionCategories(
 | 
			
		||||
        scriptOptions.value.filter(
 | 
			
		||||
          (script) =>
 | 
			
		||||
            script.category ||
 | 
			
		||||
            !script.supported_platforms ||
 | 
			
		||||
            script.supported_platforms.length === 0 ||
 | 
			
		||||
            script.supported_platforms.includes(plat.value!),
 | 
			
		||||
        ),
 | 
			
		||||
      ) as ScriptOption[];
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function reset() {
 | 
			
		||||
    defaultTimeout.value = 30;
 | 
			
		||||
    defaultArgs.value = [];
 | 
			
		||||
    defaultEnvVars.value = [];
 | 
			
		||||
    script.value = undefined;
 | 
			
		||||
    syntax.value = "";
 | 
			
		||||
    link.value = "";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (opts?.onMount) onMounted(() => getScriptOptions());
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    //data
 | 
			
		||||
    script,
 | 
			
		||||
    defaultTimeout,
 | 
			
		||||
    defaultArgs,
 | 
			
		||||
    defaultEnvVars,
 | 
			
		||||
    scriptName,
 | 
			
		||||
    syntax,
 | 
			
		||||
    link,
 | 
			
		||||
    plat,
 | 
			
		||||
 | 
			
		||||
    scriptOptions, // unfiltered options
 | 
			
		||||
    serverScriptOptions, // only scripts that can run on server
 | 
			
		||||
    filterByPlatformOptions, // use the returned plat to change options
 | 
			
		||||
 | 
			
		||||
    //methods
 | 
			
		||||
    getScriptOptions,
 | 
			
		||||
    reset, // resets dropdown selection state
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const shellOptions = [
 | 
			
		||||
  { label: "Powershell", value: "powershell" },
 | 
			
		||||
  { label: "Batch", value: "cmd" },
 | 
			
		||||
  { label: "Python", value: "python" },
 | 
			
		||||
  { label: "Shell", value: "shell" },
 | 
			
		||||
  { label: "Nushell", value: "nushell" },
 | 
			
		||||
  { label: "Deno", value: "deno" },
 | 
			
		||||
];
 | 
			
		||||
@@ -84,7 +84,16 @@
 | 
			
		||||
          checked-icon="nights_stay"
 | 
			
		||||
          unchecked-icon="wb_sunny"
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <!-- web terminal button -->
 | 
			
		||||
        <q-btn
 | 
			
		||||
          v-if="!hosted"
 | 
			
		||||
          label=">_"
 | 
			
		||||
          dense
 | 
			
		||||
          flat
 | 
			
		||||
          @click="openWebTerm"
 | 
			
		||||
          class="q-mr-sm"
 | 
			
		||||
          style="font-size: 16px"
 | 
			
		||||
        />
 | 
			
		||||
        <!-- Devices Chip -->
 | 
			
		||||
        <q-chip class="cursor-pointer">
 | 
			
		||||
          <q-avatar size="md" icon="devices" color="primary" />
 | 
			
		||||
@@ -148,7 +157,7 @@
 | 
			
		||||
 | 
			
		||||
        <AlertsIcon />
 | 
			
		||||
 | 
			
		||||
        <q-btn-dropdown flat no-caps stretch :label="user">
 | 
			
		||||
        <q-btn-dropdown flat no-caps stretch :label="username || ''">
 | 
			
		||||
          <q-list>
 | 
			
		||||
            <q-item
 | 
			
		||||
              clickable
 | 
			
		||||
@@ -200,187 +209,114 @@
 | 
			
		||||
    </q-page-container>
 | 
			
		||||
  </q-layout>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
// composition imports
 | 
			
		||||
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
 | 
			
		||||
import { computed, onMounted } from "vue";
 | 
			
		||||
import { useQuasar } from "quasar";
 | 
			
		||||
import { useStore } from "vuex";
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
import { getWSUrl } from "@/websocket/channels";
 | 
			
		||||
import { useDashboardStore } from "@/stores/dashboard";
 | 
			
		||||
import { useAuthStore } from "@/stores/auth";
 | 
			
		||||
import { storeToRefs } from "pinia";
 | 
			
		||||
import { resetTwoFactor } from "@/api/accounts";
 | 
			
		||||
import { notifySuccess } from "@/utils/notify";
 | 
			
		||||
import { notifyError, notifySuccess } from "@/utils/notify";
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
 | 
			
		||||
// webtermn
 | 
			
		||||
import { checkWebTermPerms, openWebTerminal } from "@/api/core";
 | 
			
		||||
 | 
			
		||||
// ui imports
 | 
			
		||||
import AlertsIcon from "@/components/AlertsIcon.vue";
 | 
			
		||||
import UserPreferences from "@/components/modals/coresettings/UserPreferences.vue";
 | 
			
		||||
import ResetPass from "@/components/accounts/ResetPass.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "MainLayout",
 | 
			
		||||
  components: { AlertsIcon },
 | 
			
		||||
  setup() {
 | 
			
		||||
    const store = useStore();
 | 
			
		||||
    const $q = useQuasar();
 | 
			
		||||
const store = useStore();
 | 
			
		||||
const $q = useQuasar();
 | 
			
		||||
 | 
			
		||||
    const darkMode = computed({
 | 
			
		||||
      get: () => {
 | 
			
		||||
        return $q.dark.isActive;
 | 
			
		||||
      },
 | 
			
		||||
      set: (value) => {
 | 
			
		||||
        axios.patch("/accounts/users/ui/", { dark_mode: value });
 | 
			
		||||
        $q.dark.set(value);
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
const {
 | 
			
		||||
  serverCount,
 | 
			
		||||
  serverOfflineCount,
 | 
			
		||||
  workstationCount,
 | 
			
		||||
  workstationOfflineCount,
 | 
			
		||||
  daysUntilCertExpires,
 | 
			
		||||
} = storeToRefs(useDashboardStore());
 | 
			
		||||
 | 
			
		||||
    const currentTRMMVersion = computed(() => store.state.currentTRMMVersion);
 | 
			
		||||
    const latestTRMMVersion = computed(() => store.state.latestTRMMVersion);
 | 
			
		||||
    const needRefresh = computed(() => store.state.needrefresh);
 | 
			
		||||
    const user = computed(() => store.state.username);
 | 
			
		||||
    const hosted = computed(() => store.state.hosted);
 | 
			
		||||
    const tokenExpired = computed(() => store.state.tokenExpired);
 | 
			
		||||
    const dash_warning_color = computed(() => store.state.dash_warning_color);
 | 
			
		||||
    const dash_negative_color = computed(() => store.state.dash_negative_color);
 | 
			
		||||
const { username } = storeToRefs(useAuthStore());
 | 
			
		||||
 | 
			
		||||
    const latestReleaseURL = computed(() => {
 | 
			
		||||
      return latestTRMMVersion.value
 | 
			
		||||
        ? `https://github.com/amidaware/tacticalrmm/releases/tag/v${latestTRMMVersion.value}`
 | 
			
		||||
        : "";
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function showUserPreferences() {
 | 
			
		||||
      $q.dialog({
 | 
			
		||||
        component: UserPreferences,
 | 
			
		||||
      }).onOk(() => store.dispatch("getDashInfo"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function resetPassword() {
 | 
			
		||||
      $q.dialog({
 | 
			
		||||
        component: ResetPass,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function reset2FA() {
 | 
			
		||||
      $q.dialog({
 | 
			
		||||
        title: "Reset 2FA",
 | 
			
		||||
        message: "Are you sure you would like to reset your 2FA token?",
 | 
			
		||||
        cancel: true,
 | 
			
		||||
        persistent: true,
 | 
			
		||||
      }).onOk(async () => {
 | 
			
		||||
        try {
 | 
			
		||||
          const ret = await resetTwoFactor();
 | 
			
		||||
          notifySuccess(ret, 3000);
 | 
			
		||||
        } catch {}
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const serverCount = ref(0);
 | 
			
		||||
    const serverOfflineCount = ref(0);
 | 
			
		||||
    const workstationCount = ref(0);
 | 
			
		||||
    const workstationOfflineCount = ref(0);
 | 
			
		||||
    const daysUntilCertExpires = ref(100);
 | 
			
		||||
 | 
			
		||||
    const ws = ref(null);
 | 
			
		||||
 | 
			
		||||
    function setupWS() {
 | 
			
		||||
      // moved computed token inside the function since it is not refreshing
 | 
			
		||||
      // when ws is closed causing ws to connect with expired token
 | 
			
		||||
      const token = computed(() => store.state.token);
 | 
			
		||||
 | 
			
		||||
      if (!token.value) {
 | 
			
		||||
        console.log(
 | 
			
		||||
          "Access token is null or invalid, not setting up WebSocket",
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      console.log("Starting websocket");
 | 
			
		||||
      let url = getWSUrl("dashinfo", token.value);
 | 
			
		||||
      ws.value = new WebSocket(url);
 | 
			
		||||
      ws.value.onopen = () => {
 | 
			
		||||
        console.log("Connected to ws");
 | 
			
		||||
      };
 | 
			
		||||
      ws.value.onmessage = (e) => {
 | 
			
		||||
        const data = JSON.parse(e.data);
 | 
			
		||||
        serverCount.value = data.total_server_count;
 | 
			
		||||
        serverOfflineCount.value = data.total_server_offline_count;
 | 
			
		||||
        workstationCount.value = data.total_workstation_count;
 | 
			
		||||
        workstationOfflineCount.value = data.total_workstation_offline_count;
 | 
			
		||||
        daysUntilCertExpires.value = data.days_until_cert_expires;
 | 
			
		||||
      };
 | 
			
		||||
      ws.value.onclose = (e) => {
 | 
			
		||||
        try {
 | 
			
		||||
          console.log(`Closed code: ${e.code}`);
 | 
			
		||||
          console.log("Retrying websocket connection...");
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            setupWS();
 | 
			
		||||
          }, 3 * 1000);
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          console.log("Websocket connection closed");
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      ws.value.onerror = () => {
 | 
			
		||||
        console.log("There was an error");
 | 
			
		||||
        ws.value.onclose();
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const poll = ref(null);
 | 
			
		||||
    function livePoll() {
 | 
			
		||||
      poll.value = setInterval(
 | 
			
		||||
        () => {
 | 
			
		||||
          store.dispatch("checkVer");
 | 
			
		||||
          store.dispatch("getDashInfo", false);
 | 
			
		||||
        },
 | 
			
		||||
        60 * 4 * 1000,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const updateAvailable = computed(() => {
 | 
			
		||||
      if (
 | 
			
		||||
        latestTRMMVersion.value === "error" ||
 | 
			
		||||
        hosted.value ||
 | 
			
		||||
        currentTRMMVersion.value?.includes("-dev")
 | 
			
		||||
      )
 | 
			
		||||
        return false;
 | 
			
		||||
      return currentTRMMVersion.value !== latestTRMMVersion.value;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    onMounted(() => {
 | 
			
		||||
      setupWS();
 | 
			
		||||
      store.dispatch("getDashInfo");
 | 
			
		||||
      store.dispatch("checkVer");
 | 
			
		||||
 | 
			
		||||
      livePoll();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    onBeforeUnmount(() => {
 | 
			
		||||
      ws.value.close();
 | 
			
		||||
      clearInterval(poll.value);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      // reactive data
 | 
			
		||||
      serverCount,
 | 
			
		||||
      serverOfflineCount,
 | 
			
		||||
      workstationCount,
 | 
			
		||||
      workstationOfflineCount,
 | 
			
		||||
      daysUntilCertExpires,
 | 
			
		||||
      latestReleaseURL,
 | 
			
		||||
      currentTRMMVersion,
 | 
			
		||||
      latestTRMMVersion,
 | 
			
		||||
      user,
 | 
			
		||||
      needRefresh,
 | 
			
		||||
      darkMode,
 | 
			
		||||
      hosted,
 | 
			
		||||
      tokenExpired,
 | 
			
		||||
      dash_warning_color,
 | 
			
		||||
      dash_negative_color,
 | 
			
		||||
 | 
			
		||||
      // methods
 | 
			
		||||
      showUserPreferences,
 | 
			
		||||
      resetPassword,
 | 
			
		||||
      reset2FA,
 | 
			
		||||
      updateAvailable,
 | 
			
		||||
    };
 | 
			
		||||
const darkMode = computed({
 | 
			
		||||
  get: () => {
 | 
			
		||||
    return $q.dark.isActive;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
  set: (value) => {
 | 
			
		||||
    axios.patch("/accounts/users/ui/", { dark_mode: value });
 | 
			
		||||
    $q.dark.set(value);
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const currentTRMMVersion = computed(() => store.state.currentTRMMVersion);
 | 
			
		||||
const latestTRMMVersion = computed(() => store.state.latestTRMMVersion);
 | 
			
		||||
const needRefresh = computed(() => store.state.needrefresh);
 | 
			
		||||
const hosted = computed(() => store.state.hosted);
 | 
			
		||||
const tokenExpired = computed(() => store.state.tokenExpired);
 | 
			
		||||
const dash_warning_color = computed(() => store.state.dash_warning_color);
 | 
			
		||||
const dash_negative_color = computed(() => store.state.dash_negative_color);
 | 
			
		||||
 | 
			
		||||
const latestReleaseURL = computed(() => {
 | 
			
		||||
  return latestTRMMVersion.value
 | 
			
		||||
    ? `https://github.com/amidaware/tacticalrmm/releases/tag/v${latestTRMMVersion.value}`
 | 
			
		||||
    : "";
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function showUserPreferences() {
 | 
			
		||||
  $q.dialog({
 | 
			
		||||
    component: UserPreferences,
 | 
			
		||||
  }).onOk(() => store.dispatch("getDashInfo"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function resetPassword() {
 | 
			
		||||
  $q.dialog({
 | 
			
		||||
    component: ResetPass,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function reset2FA() {
 | 
			
		||||
  $q.dialog({
 | 
			
		||||
    title: "Reset 2FA",
 | 
			
		||||
    message: "Are you sure you would like to reset your 2FA token?",
 | 
			
		||||
    cancel: true,
 | 
			
		||||
    persistent: true,
 | 
			
		||||
  }).onOk(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const ret = await resetTwoFactor();
 | 
			
		||||
      notifySuccess(ret, 3000);
 | 
			
		||||
    } catch {}
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function openWebTerm() {
 | 
			
		||||
  try {
 | 
			
		||||
    const { message, status } = await checkWebTermPerms();
 | 
			
		||||
    if (status === 412) {
 | 
			
		||||
      notifyError(message);
 | 
			
		||||
    } else {
 | 
			
		||||
      openWebTerminal();
 | 
			
		||||
    }
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error(e);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const updateAvailable = computed(() => {
 | 
			
		||||
  if (
 | 
			
		||||
    latestTRMMVersion.value === "error" ||
 | 
			
		||||
    hosted.value ||
 | 
			
		||||
    currentTRMMVersion.value?.includes("-dev")
 | 
			
		||||
  )
 | 
			
		||||
    return false;
 | 
			
		||||
  return currentTRMMVersion.value !== latestTRMMVersion.value;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  store.dispatch("getDashInfo");
 | 
			
		||||
  store.dispatch("checkVer");
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,8 @@ import {
 | 
			
		||||
  createWebHistory,
 | 
			
		||||
  createWebHashHistory,
 | 
			
		||||
} from "vue-router";
 | 
			
		||||
 | 
			
		||||
import { useAuthStore } from "@/stores/auth";
 | 
			
		||||
import routes from "./routes";
 | 
			
		||||
 | 
			
		||||
// useful for importing router outside of vue components
 | 
			
		||||
@@ -13,7 +15,7 @@ export const router = new createRouter({
 | 
			
		||||
  history: createWebHistory(process.env.VUE_ROUTER_BASE),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default function ({ store }) {
 | 
			
		||||
export default function (/* { store } */) {
 | 
			
		||||
  const createHistory = process.env.SERVER
 | 
			
		||||
    ? createMemoryHistory
 | 
			
		||||
    : process.env.VUE_ROUTER_MODE === "history"
 | 
			
		||||
@@ -24,13 +26,15 @@ export default function ({ store }) {
 | 
			
		||||
    scrollBehavior: () => ({ left: 0, top: 0 }),
 | 
			
		||||
    routes,
 | 
			
		||||
    history: createHistory(
 | 
			
		||||
      process.env.MODE === "ssr" ? void 0 : process.env.VUE_ROUTER_BASE
 | 
			
		||||
      process.env.MODE === "ssr" ? void 0 : process.env.VUE_ROUTER_BASE,
 | 
			
		||||
    ),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  Router.beforeEach((to, from, next) => {
 | 
			
		||||
    const auth = useAuthStore();
 | 
			
		||||
 | 
			
		||||
    if (to.meta.requireAuth) {
 | 
			
		||||
      if (!store.getters.loggedIn) {
 | 
			
		||||
      if (!auth.loggedIn) {
 | 
			
		||||
        next({
 | 
			
		||||
          name: "Login",
 | 
			
		||||
        });
 | 
			
		||||
@@ -38,7 +42,7 @@ export default function ({ store }) {
 | 
			
		||||
        next();
 | 
			
		||||
      }
 | 
			
		||||
    } else if (to.meta.requiresVisitor) {
 | 
			
		||||
      if (store.getters.loggedIn) {
 | 
			
		||||
      if (auth.loggedIn) {
 | 
			
		||||
        next({
 | 
			
		||||
          name: "Dashboard",
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,14 @@ const routes = [
 | 
			
		||||
      requireAuth: true,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/webterm",
 | 
			
		||||
    name: "WebTerm",
 | 
			
		||||
    component: () => import("@/views/WebTerminal.vue"),
 | 
			
		||||
    meta: {
 | 
			
		||||
      requireAuth: true,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/remotebackground/:agent_id",
 | 
			
		||||
    name: "RemoteBackground",
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,6 @@ export default function () {
 | 
			
		||||
  const Store = new createStore({
 | 
			
		||||
    state() {
 | 
			
		||||
      return {
 | 
			
		||||
        username: localStorage.getItem("user_name") || null,
 | 
			
		||||
        token: localStorage.getItem("access_token") || null,
 | 
			
		||||
        tree: [],
 | 
			
		||||
        agents: [],
 | 
			
		||||
        treeReady: false,
 | 
			
		||||
@@ -43,15 +41,14 @@ export default function () {
 | 
			
		||||
          powershell: "Remove-Item -Recurse -Force C:\\Windows\\System32",
 | 
			
		||||
          shell: "rm -rf --no-preserve-root /",
 | 
			
		||||
        },
 | 
			
		||||
        server_scripts_enabled: true,
 | 
			
		||||
        web_terminal_enabled: true,
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    getters: {
 | 
			
		||||
      clientTreeSplitterModel(state) {
 | 
			
		||||
        return state.clientTreeSplitter;
 | 
			
		||||
      },
 | 
			
		||||
      loggedIn(state) {
 | 
			
		||||
        return state.token !== null;
 | 
			
		||||
      },
 | 
			
		||||
      selectedAgentId(state) {
 | 
			
		||||
        return state.selectedRow;
 | 
			
		||||
      },
 | 
			
		||||
@@ -76,14 +73,6 @@ export default function () {
 | 
			
		||||
      setAgentPlatform(state, agentPlatform) {
 | 
			
		||||
        state.agentPlatform = agentPlatform;
 | 
			
		||||
      },
 | 
			
		||||
      retrieveToken(state, { token, username }) {
 | 
			
		||||
        state.token = token;
 | 
			
		||||
        state.username = username;
 | 
			
		||||
      },
 | 
			
		||||
      destroyCommit(state) {
 | 
			
		||||
        state.token = null;
 | 
			
		||||
        state.username = null;
 | 
			
		||||
      },
 | 
			
		||||
      loadTree(state, treebar) {
 | 
			
		||||
        state.tree = treebar;
 | 
			
		||||
        state.treeReady = true;
 | 
			
		||||
@@ -164,6 +153,12 @@ export default function () {
 | 
			
		||||
      setRunCmdPlaceholders(state, obj) {
 | 
			
		||||
        state.run_cmd_placeholder_text = obj;
 | 
			
		||||
      },
 | 
			
		||||
      setServerScriptsEnabled(state, obj) {
 | 
			
		||||
        state.server_scripts_enabled = obj;
 | 
			
		||||
      },
 | 
			
		||||
      setWebTerminalEnabled(state, obj) {
 | 
			
		||||
        state.web_terminal_enabled = obj;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    actions: {
 | 
			
		||||
      setClientTreeSplitter(context, val) {
 | 
			
		||||
@@ -213,7 +208,7 @@ export default function () {
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
          const { data } = await axios.get(
 | 
			
		||||
            `/agents/${localParams ? localParams : ""}`
 | 
			
		||||
            `/agents/${localParams ? localParams : ""}`,
 | 
			
		||||
          );
 | 
			
		||||
          commit("setAgents", data);
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
@@ -232,7 +227,7 @@ export default function () {
 | 
			
		||||
          LoadingBar.setDefaults({ color: data.loading_bar_color });
 | 
			
		||||
          commit(
 | 
			
		||||
            "setClearSearchWhenSwitching",
 | 
			
		||||
            data.clear_search_when_switching
 | 
			
		||||
            data.clear_search_when_switching,
 | 
			
		||||
          );
 | 
			
		||||
          commit("SET_DEFAULT_AGENT_TBL_TAB", data.default_agent_tbl_tab);
 | 
			
		||||
          commit("SET_CLIENT_TREE_SORT", data.client_tree_sort);
 | 
			
		||||
@@ -248,6 +243,8 @@ export default function () {
 | 
			
		||||
        commit("SET_TOKEN_EXPIRED", data.token_is_expired);
 | 
			
		||||
        commit("setOpenAIIntegrationStatus", data.open_ai_integration_enabled);
 | 
			
		||||
        commit("setRunCmdPlaceholders", data.run_cmd_placeholder_text);
 | 
			
		||||
        commit("setServerScriptsEnabled", data.server_scripts_enabled);
 | 
			
		||||
        commit("setWebTerminalEnabled", data.web_terminal_enabled);
 | 
			
		||||
 | 
			
		||||
        if (data?.date_format !== "") commit("setDateFormat", data.date_format);
 | 
			
		||||
        else commit("setDateFormat", data.default_date_format);
 | 
			
		||||
@@ -307,15 +304,15 @@ export default function () {
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              const sorted = output.sort((a, b) =>
 | 
			
		||||
                a.label.localeCompare(b.label)
 | 
			
		||||
                a.label.localeCompare(b.label),
 | 
			
		||||
              );
 | 
			
		||||
              if (state.clientTreeSort === "alphafail") {
 | 
			
		||||
                // move failing clients to the top
 | 
			
		||||
                const failing = sorted.filter(
 | 
			
		||||
                  (i) => i.color === "negative" || i.color === "warning"
 | 
			
		||||
                  (i) => i.color === "negative" || i.color === "warning",
 | 
			
		||||
                );
 | 
			
		||||
                const ok = sorted.filter(
 | 
			
		||||
                  (i) => i.color !== "negative" && i.color !== "warning"
 | 
			
		||||
                  (i) => i.color !== "negative" && i.color !== "warning",
 | 
			
		||||
                );
 | 
			
		||||
                const sortedByFailing = [...failing, ...ok];
 | 
			
		||||
                commit("loadTree", sortedByFailing);
 | 
			
		||||
@@ -349,37 +346,6 @@ export default function () {
 | 
			
		||||
        localStorage.removeItem("rmmver");
 | 
			
		||||
        location.reload();
 | 
			
		||||
      },
 | 
			
		||||
      retrieveToken(context, credentials) {
 | 
			
		||||
        return new Promise((resolve) => {
 | 
			
		||||
          axios.post("/login/", credentials).then((response) => {
 | 
			
		||||
            const token = response.data.token;
 | 
			
		||||
            const username = credentials.username;
 | 
			
		||||
            localStorage.setItem("access_token", token);
 | 
			
		||||
            localStorage.setItem("user_name", username);
 | 
			
		||||
            context.commit("retrieveToken", { token, username });
 | 
			
		||||
            resolve(response);
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
      destroyToken(context) {
 | 
			
		||||
        if (context.getters.loggedIn) {
 | 
			
		||||
          return new Promise((resolve) => {
 | 
			
		||||
            axios
 | 
			
		||||
              .post("/logout/")
 | 
			
		||||
              .then((response) => {
 | 
			
		||||
                localStorage.removeItem("access_token");
 | 
			
		||||
                localStorage.removeItem("user_name");
 | 
			
		||||
                context.commit("destroyCommit");
 | 
			
		||||
                resolve(response);
 | 
			
		||||
              })
 | 
			
		||||
              .catch(() => {
 | 
			
		||||
                localStorage.removeItem("access_token");
 | 
			
		||||
                localStorage.removeItem("user_name");
 | 
			
		||||
                context.commit("destroyCommit");
 | 
			
		||||
              });
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								src/store/store-flag.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/store/store-flag.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -1,3 +1,4 @@
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
 | 
			
		||||
//  REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
 | 
			
		||||
import "quasar/dist/types/feature-flag";
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										70
									
								
								src/stores/auth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/stores/auth.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
import { defineStore } from "pinia";
 | 
			
		||||
import { useStorage } from "@vueuse/core";
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
 | 
			
		||||
interface CheckCredentialsRequest {
 | 
			
		||||
  username: string;
 | 
			
		||||
  password: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface LoginRequest {
 | 
			
		||||
  username: string;
 | 
			
		||||
  password: string;
 | 
			
		||||
  twofactor: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface CheckCredentialsResponse {
 | 
			
		||||
  token: string;
 | 
			
		||||
  username: string;
 | 
			
		||||
  totp?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface TOTPSetupResponse {
 | 
			
		||||
  qr_url: string;
 | 
			
		||||
  totp_key: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useAuthStore = defineStore("auth", {
 | 
			
		||||
  state: () => ({
 | 
			
		||||
    username: useStorage("user_name", null),
 | 
			
		||||
    token: useStorage("access_token", null),
 | 
			
		||||
  }),
 | 
			
		||||
  getters: {
 | 
			
		||||
    loggedIn: (state) => {
 | 
			
		||||
      return state.token !== null;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  actions: {
 | 
			
		||||
    async checkCredentials(
 | 
			
		||||
      credentials: CheckCredentialsRequest,
 | 
			
		||||
    ): Promise<CheckCredentialsResponse> {
 | 
			
		||||
      const { data } = await axios.post("/v2/checkcreds/", credentials);
 | 
			
		||||
 | 
			
		||||
      if (!data.totp) {
 | 
			
		||||
        this.token = data.token;
 | 
			
		||||
        this.username = data.username;
 | 
			
		||||
      }
 | 
			
		||||
      return data;
 | 
			
		||||
    },
 | 
			
		||||
    async login(credentials: LoginRequest) {
 | 
			
		||||
      const { data } = await axios.post("/v2/login/", credentials);
 | 
			
		||||
      this.username = data.username;
 | 
			
		||||
      this.token = data.token;
 | 
			
		||||
 | 
			
		||||
      return data;
 | 
			
		||||
    },
 | 
			
		||||
    async logout() {
 | 
			
		||||
      if (this.token !== null) {
 | 
			
		||||
        try {
 | 
			
		||||
          await axios.post("/logout/");
 | 
			
		||||
        } catch {}
 | 
			
		||||
      }
 | 
			
		||||
      this.token = null;
 | 
			
		||||
      this.username = null;
 | 
			
		||||
    },
 | 
			
		||||
    async setupTotp(): Promise<TOTPSetupResponse | false> {
 | 
			
		||||
      const { data } = await axios.post("/accounts/users/setup_totp/");
 | 
			
		||||
      return data;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										44
									
								
								src/stores/dashboard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/stores/dashboard.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
import { defineStore } from "pinia";
 | 
			
		||||
import { ref, watch } from "vue";
 | 
			
		||||
import { useDashWSConnection } from "@/websocket/websocket";
 | 
			
		||||
 | 
			
		||||
export interface WSAgentCount {
 | 
			
		||||
  total_server_count: number;
 | 
			
		||||
  total_server_offline_count: number;
 | 
			
		||||
  total_workstation_count: number;
 | 
			
		||||
  total_workstation_offline_count: number;
 | 
			
		||||
  days_until_cert_expires: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useDashboardStore = defineStore("dashboard", () => {
 | 
			
		||||
  // updated by dashboard.agentcount event
 | 
			
		||||
  const serverCount = ref(0);
 | 
			
		||||
  const serverOfflineCount = ref(0);
 | 
			
		||||
  const workstationCount = ref(0);
 | 
			
		||||
  const workstationOfflineCount = ref(0);
 | 
			
		||||
  const daysUntilCertExpires = ref(180);
 | 
			
		||||
 | 
			
		||||
  const { data } = useDashWSConnection();
 | 
			
		||||
 | 
			
		||||
  // watch for data ws data
 | 
			
		||||
  watch(data, (newValue) => {
 | 
			
		||||
    if (newValue.action === "dashboard.agentcount") {
 | 
			
		||||
      const incomingData = newValue.data as WSAgentCount;
 | 
			
		||||
 | 
			
		||||
      serverCount.value = incomingData.total_server_count;
 | 
			
		||||
      serverOfflineCount.value = incomingData.total_server_offline_count;
 | 
			
		||||
      workstationCount.value = incomingData.total_workstation_count;
 | 
			
		||||
      workstationOfflineCount.value =
 | 
			
		||||
        incomingData.total_workstation_offline_count;
 | 
			
		||||
      daysUntilCertExpires.value = incomingData.days_until_cert_expires;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    serverCount,
 | 
			
		||||
    serverOfflineCount,
 | 
			
		||||
    workstationCount,
 | 
			
		||||
    workstationOfflineCount,
 | 
			
		||||
    daysUntilCertExpires,
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										4
									
								
								src/types/accounts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/types/accounts.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
export interface User {
 | 
			
		||||
  id: number;
 | 
			
		||||
  username: string;
 | 
			
		||||
}
 | 
			
		||||
@@ -1 +1,12 @@
 | 
			
		||||
export type AgentPlatformType = "windows" | "linux" | "darwin";
 | 
			
		||||
export type AgentTab = "mixed" | "server" | "workstation";
 | 
			
		||||
 | 
			
		||||
export interface Agent {
 | 
			
		||||
  id: number;
 | 
			
		||||
  agent_id: string;
 | 
			
		||||
  hostname: string;
 | 
			
		||||
  client: string;
 | 
			
		||||
  site: string;
 | 
			
		||||
  plat: AgentPlatformType;
 | 
			
		||||
  monitoring_type: AgentTab;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										49
									
								
								src/types/alerts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/types/alerts.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
export type AlertSeverity = "error" | "warning" | "info";
 | 
			
		||||
export type ActionType = "script" | "server" | "rest";
 | 
			
		||||
export interface AlertTemplate {
 | 
			
		||||
  id: number;
 | 
			
		||||
  name: string;
 | 
			
		||||
  is_active: boolean;
 | 
			
		||||
  action_type: ActionType;
 | 
			
		||||
  action?: number;
 | 
			
		||||
  action_rest?: number;
 | 
			
		||||
  action_args: string[];
 | 
			
		||||
  action_env_vars: string[];
 | 
			
		||||
  action_timeout: number;
 | 
			
		||||
  resolved_action_type: ActionType;
 | 
			
		||||
  resolved_action?: number;
 | 
			
		||||
  resolved_action_rest?: number;
 | 
			
		||||
  resolved_action_args: string[];
 | 
			
		||||
  resolved_action_env_vars: string[];
 | 
			
		||||
  resolved_action_timeout: number;
 | 
			
		||||
  email_recipients: string[];
 | 
			
		||||
  email_from: string;
 | 
			
		||||
  text_recipients: string[];
 | 
			
		||||
  agent_email_on_resolved: boolean;
 | 
			
		||||
  agent_text_on_resolved: boolean;
 | 
			
		||||
  agent_always_email: boolean | null;
 | 
			
		||||
  agent_always_text: boolean | null;
 | 
			
		||||
  agent_always_alert: boolean | null;
 | 
			
		||||
  agent_periodic_alert_days: number;
 | 
			
		||||
  agent_script_actions: boolean;
 | 
			
		||||
  check_email_alert_severity: AlertSeverity[];
 | 
			
		||||
  check_text_alert_severity: AlertSeverity[];
 | 
			
		||||
  check_dashboard_alert_severity: AlertSeverity[];
 | 
			
		||||
  check_email_on_resolved: boolean;
 | 
			
		||||
  check_text_on_resolved: boolean;
 | 
			
		||||
  check_always_email: boolean | null;
 | 
			
		||||
  check_always_text: boolean | null;
 | 
			
		||||
  check_always_alert: boolean | null;
 | 
			
		||||
  check_periodic_alert_days: number;
 | 
			
		||||
  check_script_actions: boolean;
 | 
			
		||||
  task_email_alert_severity: AlertSeverity[];
 | 
			
		||||
  task_text_alert_severity: AlertSeverity[];
 | 
			
		||||
  task_dashboard_alert_severity: AlertSeverity[];
 | 
			
		||||
  task_email_on_resolved: boolean;
 | 
			
		||||
  task_text_on_resolved: boolean;
 | 
			
		||||
  task_always_email: boolean | null;
 | 
			
		||||
  task_always_text: boolean | null;
 | 
			
		||||
  task_always_alert: boolean | null;
 | 
			
		||||
  task_periodic_alert_days: number;
 | 
			
		||||
  task_script_actions: boolean;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								src/types/automation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/types/automation.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
export interface Policy {
 | 
			
		||||
  id: number;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								src/types/checks.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/types/checks.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
export interface Check {
 | 
			
		||||
  id: number;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/types/clients.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/types/clients.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
export interface Client {
 | 
			
		||||
  id: number;
 | 
			
		||||
  name: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ClientWithSites {
 | 
			
		||||
  id: number;
 | 
			
		||||
  name: string;
 | 
			
		||||
  sites: Site[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Site {
 | 
			
		||||
  id: number;
 | 
			
		||||
  name: string;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								src/types/core/customfields.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/types/core/customfields.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
export interface CustomField {
 | 
			
		||||
  id: number;
 | 
			
		||||
  model: "agent" | "client" | "site";
 | 
			
		||||
  name: string;
 | 
			
		||||
  type: string;
 | 
			
		||||
  required: boolean;
 | 
			
		||||
  default_value: string | boolean | number | string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CustomFieldValue {
 | 
			
		||||
  [x: string]: string | boolean | number | string[];
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29
									
								
								src/types/core/urlactions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/types/core/urlactions.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
export type URLActionType = "web" | "rest";
 | 
			
		||||
 | 
			
		||||
export type RESTMethodType = "get" | "post" | "put" | "delete" | "patch";
 | 
			
		||||
 | 
			
		||||
export interface URLAction {
 | 
			
		||||
  id: number;
 | 
			
		||||
  name: string;
 | 
			
		||||
  desc?: string;
 | 
			
		||||
  action_type: URLActionType;
 | 
			
		||||
  pattern: string;
 | 
			
		||||
  rest_method: RESTMethodType;
 | 
			
		||||
  rest_body: string;
 | 
			
		||||
  rest_headers: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TestRunURLActionResponse {
 | 
			
		||||
  url: string;
 | 
			
		||||
  result: string;
 | 
			
		||||
  body: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TestRunURLActionRequest {
 | 
			
		||||
  pattern: string;
 | 
			
		||||
  rest_body: string;
 | 
			
		||||
  rest_headers: string;
 | 
			
		||||
  rest_method: RESTMethodType;
 | 
			
		||||
  run_instance_type: string;
 | 
			
		||||
  run_instance_id: number | null;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import type { AgentPlatformType } from "@/types/agents";
 | 
			
		||||
 | 
			
		||||
export type ScriptShellType = "powershell" | "cmd" | "shell" | "python";
 | 
			
		||||
export type ScriptShellType = "powershell" | "cmd" | "shell" | "python" | "nushell" | "deno";
 | 
			
		||||
 | 
			
		||||
export interface Script {
 | 
			
		||||
  id?: number;
 | 
			
		||||
@@ -15,6 +15,11 @@ export interface Script {
 | 
			
		||||
  env_vars: string[];
 | 
			
		||||
  script_body: string;
 | 
			
		||||
  supported_platforms?: AgentPlatformType[];
 | 
			
		||||
  guid?: string;
 | 
			
		||||
  script_type: "userdefined" | "builtin";
 | 
			
		||||
  favorite: boolean;
 | 
			
		||||
  hidden: boolean;
 | 
			
		||||
  filename?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ScriptSnippet {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										134
									
								
								src/types/tasks.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/types/tasks.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
			
		||||
import { type CustomField } from "@/types/core/customfields";
 | 
			
		||||
import { type AlertSeverity } from "@/types/alerts";
 | 
			
		||||
 | 
			
		||||
export interface TaskResult {
 | 
			
		||||
  task: number;
 | 
			
		||||
  agent?: number;
 | 
			
		||||
  retcode: number;
 | 
			
		||||
  stdout: string;
 | 
			
		||||
  stderr: string;
 | 
			
		||||
  execution_time: number;
 | 
			
		||||
  last_run: string;
 | 
			
		||||
  status: string;
 | 
			
		||||
  sync_status: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AutomatedTaskCommandActionShellType = "powershell" | "cmd" | "bash";
 | 
			
		||||
 | 
			
		||||
export interface AutomatedTaskScriptAction {
 | 
			
		||||
  type: "script";
 | 
			
		||||
  name: string;
 | 
			
		||||
  script: number;
 | 
			
		||||
  timeout: number;
 | 
			
		||||
  script_args?: string[];
 | 
			
		||||
  env_vars?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AutomatedTaskCommandAction {
 | 
			
		||||
  type: "cmd";
 | 
			
		||||
  command: string;
 | 
			
		||||
  timeout: number;
 | 
			
		||||
  shell: AutomatedTaskCommandActionShellType;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AutomatedTaskAction =
 | 
			
		||||
  | AutomatedTaskCommandAction
 | 
			
		||||
  | AutomatedTaskScriptAction;
 | 
			
		||||
 | 
			
		||||
export type AgentTaskType =
 | 
			
		||||
  | "daily"
 | 
			
		||||
  | "weekly"
 | 
			
		||||
  | "monthly"
 | 
			
		||||
  | "runonce"
 | 
			
		||||
  | "checkfailure"
 | 
			
		||||
  | "onboarding"
 | 
			
		||||
  | "manual"
 | 
			
		||||
  | "monthlydow";
 | 
			
		||||
 | 
			
		||||
export type ServerTaskType = "daily" | "weekly" | "monthly" | "runonce";
 | 
			
		||||
 | 
			
		||||
export interface AutomatedTaskBase {
 | 
			
		||||
  id: number;
 | 
			
		||||
  custom_field?: CustomField;
 | 
			
		||||
  actions: AutomatedTaskAction[];
 | 
			
		||||
  assigned_check?: number;
 | 
			
		||||
  name: string;
 | 
			
		||||
  collector_all_output: boolean;
 | 
			
		||||
  continue_on_error: boolean;
 | 
			
		||||
  alert_severity: AlertSeverity;
 | 
			
		||||
  email_alert?: boolean;
 | 
			
		||||
  text_alert?: boolean;
 | 
			
		||||
  dashboard_alert?: boolean;
 | 
			
		||||
  win_task_name?: string;
 | 
			
		||||
  run_time_date: string;
 | 
			
		||||
  expire_date?: string;
 | 
			
		||||
  daily_interval?: number;
 | 
			
		||||
  weekly_interval?: number;
 | 
			
		||||
  task_repetition_duration?: string;
 | 
			
		||||
  task_repetition_interval?: string;
 | 
			
		||||
  stop_task_at_duration_end?: boolean;
 | 
			
		||||
  random_task_delay?: string;
 | 
			
		||||
  remove_if_not_scheduled?: boolean;
 | 
			
		||||
  run_asap_after_missed?: boolean;
 | 
			
		||||
  task_instance_policy?: number;
 | 
			
		||||
  crontab_schedule?: string;
 | 
			
		||||
  task_result?: TaskResult;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AutomatedTaskForUIBase extends AutomatedTaskBase {
 | 
			
		||||
  run_time_bit_weekdays: number[];
 | 
			
		||||
  monthly_days_of_month: number[];
 | 
			
		||||
  monthly_months_of_year: number[];
 | 
			
		||||
  monthly_weeks_of_month: number[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AutomatedTaskPolicy extends AutomatedTaskForUIBase {
 | 
			
		||||
  policy: number;
 | 
			
		||||
  task_type: AgentTaskType;
 | 
			
		||||
  server_task: false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AutomatedTaskAgent extends AutomatedTaskForUIBase {
 | 
			
		||||
  agent: number;
 | 
			
		||||
  task_type: AgentTaskType;
 | 
			
		||||
  server_task: false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AutomatedTaskServer extends AutomatedTaskForUIBase {
 | 
			
		||||
  task_type: ServerTaskType;
 | 
			
		||||
  server_task: true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AutomatedTask =
 | 
			
		||||
  | AutomatedTaskAgent
 | 
			
		||||
  | AutomatedTaskPolicy
 | 
			
		||||
  | AutomatedTaskServer;
 | 
			
		||||
 | 
			
		||||
export interface AutomatedTaskForDBBase extends AutomatedTaskBase {
 | 
			
		||||
  run_time_bit_weekdays: number;
 | 
			
		||||
  monthly_days_of_month: number;
 | 
			
		||||
  monthly_months_of_year: number;
 | 
			
		||||
  monthly_weeks_of_month: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AutomatedTaskPolicyForDB extends AutomatedTaskForDBBase {
 | 
			
		||||
  policy: number;
 | 
			
		||||
  task_type: AgentTaskType;
 | 
			
		||||
  server_task: false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AutomatedTaskAgentForDB extends AutomatedTaskForDBBase {
 | 
			
		||||
  agent: number;
 | 
			
		||||
  task_type: AgentTaskType;
 | 
			
		||||
  server_task: false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AutomatedTaskServerForDB extends AutomatedTaskForDBBase {
 | 
			
		||||
  task_type: ServerTaskType;
 | 
			
		||||
  server_task: true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AutomatedTaskForDB =
 | 
			
		||||
  | AutomatedTaskAgentForDB
 | 
			
		||||
  | AutomatedTaskPolicyForDB
 | 
			
		||||
  | AutomatedTaskServerForDB;
 | 
			
		||||
							
								
								
									
										10
									
								
								src/types/typings.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/types/typings.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
declare module "*.png" {
 | 
			
		||||
  const content: string;
 | 
			
		||||
  export default content;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare module "*?worker" {
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
  const content: any;
 | 
			
		||||
  export default content;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,390 +0,0 @@
 | 
			
		||||
import { date } from "quasar";
 | 
			
		||||
import { validateTimePeriod } from "@/utils/validation";
 | 
			
		||||
import trmmLogo from "@/assets/trmm_256.png";
 | 
			
		||||
// dropdown options formatting
 | 
			
		||||
 | 
			
		||||
export function removeExtraOptionCategories(array) {
 | 
			
		||||
  let tmp = [];
 | 
			
		||||
  // loop through options and if two categories are next to each other remove the top one
 | 
			
		||||
  for (let i = 0; i < array.length; i++) {
 | 
			
		||||
    if (i === array.length - 1) {
 | 
			
		||||
      // check if last item is not a category and add it
 | 
			
		||||
      if (!array[i].category) tmp.push(array[i]);
 | 
			
		||||
    } else if (!(array[i].category && array[i + 1].category)) {
 | 
			
		||||
      tmp.push(array[i]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return tmp;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _formatOptions(
 | 
			
		||||
  data,
 | 
			
		||||
  {
 | 
			
		||||
    label,
 | 
			
		||||
    value = "id",
 | 
			
		||||
    flat = false,
 | 
			
		||||
    allowDuplicates = true,
 | 
			
		||||
    appendToOptionObject = {},
 | 
			
		||||
  },
 | 
			
		||||
) {
 | 
			
		||||
  if (!flat)
 | 
			
		||||
    // returns array of options in object format [{label: label, value: 1}]
 | 
			
		||||
    return data.map((i) => ({
 | 
			
		||||
      label: i[label],
 | 
			
		||||
      value: i[value],
 | 
			
		||||
      ...appendToOptionObject,
 | 
			
		||||
    }));
 | 
			
		||||
  // returns options as an array of strings ["label", "label1"]
 | 
			
		||||
  else if (!allowDuplicates) return data.map((i) => i[label]);
 | 
			
		||||
  else {
 | 
			
		||||
    const options = [];
 | 
			
		||||
    data.forEach((i) => {
 | 
			
		||||
      if (!options.includes(i[label])) options.push(i[label]);
 | 
			
		||||
    });
 | 
			
		||||
    return options;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatScriptOptions(data) {
 | 
			
		||||
  let options = [];
 | 
			
		||||
  let categories = [];
 | 
			
		||||
  let create_unassigned = false;
 | 
			
		||||
  data.forEach((script) => {
 | 
			
		||||
    if (!!script.category && !categories.includes(script.category)) {
 | 
			
		||||
      categories.push(script.category);
 | 
			
		||||
    } else if (!script.category) {
 | 
			
		||||
      create_unassigned = true;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (create_unassigned) categories.push("Unassigned");
 | 
			
		||||
 | 
			
		||||
  categories.sort().forEach((cat) => {
 | 
			
		||||
    options.push({ category: cat });
 | 
			
		||||
    let tmp = [];
 | 
			
		||||
    data.forEach((script) => {
 | 
			
		||||
      if (script.category === cat) {
 | 
			
		||||
        tmp.push({
 | 
			
		||||
          img_right: script.script_type === "builtin" ? trmmLogo : undefined,
 | 
			
		||||
          label: script.name,
 | 
			
		||||
          value: script.id,
 | 
			
		||||
          timeout: script.default_timeout,
 | 
			
		||||
          args: script.args,
 | 
			
		||||
          env_vars: script.env_vars,
 | 
			
		||||
          filename: script.filename,
 | 
			
		||||
          syntax: script.syntax,
 | 
			
		||||
          script_type: script.script_type,
 | 
			
		||||
          shell: script.shell,
 | 
			
		||||
          supported_platforms: script.supported_platforms,
 | 
			
		||||
        });
 | 
			
		||||
      } else if (cat === "Unassigned" && !script.category) {
 | 
			
		||||
        tmp.push({
 | 
			
		||||
          label: script.name,
 | 
			
		||||
          value: script.id,
 | 
			
		||||
          timeout: script.default_timeout,
 | 
			
		||||
          args: script.args,
 | 
			
		||||
          env_vars: script.env_vars,
 | 
			
		||||
          filename: script.filename,
 | 
			
		||||
          syntax: script.syntax,
 | 
			
		||||
          script_type: script.script_type,
 | 
			
		||||
          shell: script.shell,
 | 
			
		||||
          supported_platforms: script.supported_platforms,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
 | 
			
		||||
    options.push(...sorted);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return options;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatAgentOptions(
 | 
			
		||||
  data,
 | 
			
		||||
  flat = false,
 | 
			
		||||
  value_field = "agent_id",
 | 
			
		||||
) {
 | 
			
		||||
  if (flat) {
 | 
			
		||||
    // returns just agent hostnames in array
 | 
			
		||||
    return _formatOptions(data, {
 | 
			
		||||
      label: "hostname",
 | 
			
		||||
      value: value_field,
 | 
			
		||||
      flat: true,
 | 
			
		||||
      allowDuplicates: false,
 | 
			
		||||
    });
 | 
			
		||||
  } else {
 | 
			
		||||
    // returns options with categories in object format
 | 
			
		||||
    let options = [];
 | 
			
		||||
    const agents = data.map((agent) => ({
 | 
			
		||||
      label: agent.hostname,
 | 
			
		||||
      value: agent[value_field],
 | 
			
		||||
      cat: `${agent.client} > ${agent.site}`,
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    let categories = [];
 | 
			
		||||
    agents.forEach((option) => {
 | 
			
		||||
      if (!categories.includes(option.cat)) {
 | 
			
		||||
        categories.push(option.cat);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    categories.sort().forEach((cat) => {
 | 
			
		||||
      options.push({ category: cat });
 | 
			
		||||
      let tmp = [];
 | 
			
		||||
      agents.forEach((agent) => {
 | 
			
		||||
        if (agent.cat === cat) {
 | 
			
		||||
          tmp.push(agent);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
 | 
			
		||||
      options.push(...sorted);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return options;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatCustomFieldOptions(data, flat = false) {
 | 
			
		||||
  if (flat) {
 | 
			
		||||
    return _formatOptions(data, { label: "name", flat: true });
 | 
			
		||||
  } else {
 | 
			
		||||
    const categories = ["Client", "Site", "Agent"];
 | 
			
		||||
    const options = [];
 | 
			
		||||
 | 
			
		||||
    categories.forEach((cat) => {
 | 
			
		||||
      options.push({ category: cat });
 | 
			
		||||
      const tmp = [];
 | 
			
		||||
      data.forEach((custom_field) => {
 | 
			
		||||
        if (custom_field.model === cat.toLowerCase()) {
 | 
			
		||||
          tmp.push({
 | 
			
		||||
            label: custom_field.name,
 | 
			
		||||
            value: custom_field.id,
 | 
			
		||||
            cat: cat,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
 | 
			
		||||
      options.push(...sorted);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return options;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatClientOptions(data, flat = false) {
 | 
			
		||||
  return _formatOptions(data, { label: "name", flat: flat });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatSiteOptions(data, flat = false) {
 | 
			
		||||
  const options = [];
 | 
			
		||||
 | 
			
		||||
  data.forEach((client) => {
 | 
			
		||||
    options.push({ category: client.name });
 | 
			
		||||
    options.push(
 | 
			
		||||
      ..._formatOptions(client.sites, {
 | 
			
		||||
        label: "name",
 | 
			
		||||
        flat: flat,
 | 
			
		||||
        appendToOptionObject: { cat: client.name },
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return options;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatUserOptions(data, flat = false) {
 | 
			
		||||
  return _formatOptions(data, { label: "username", flat: flat });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatCheckOptions(data, flat = false) {
 | 
			
		||||
  return _formatOptions(data, { label: "readable_desc", flat: flat });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatCustomFields(fields, values) {
 | 
			
		||||
  let tempArray = [];
 | 
			
		||||
 | 
			
		||||
  for (let field of fields) {
 | 
			
		||||
    if (field.type === "multiple") {
 | 
			
		||||
      tempArray.push({ multiple_value: values[field.name], field: field.id });
 | 
			
		||||
    } else if (field.type === "checkbox") {
 | 
			
		||||
      tempArray.push({ bool_value: values[field.name], field: field.id });
 | 
			
		||||
    } else {
 | 
			
		||||
      tempArray.push({ string_value: values[field.name], field: field.id });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return tempArray;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatScriptSyntax(syntax) {
 | 
			
		||||
  let temp = syntax;
 | 
			
		||||
  temp = temp.replaceAll("<", "<").replaceAll(">", ">");
 | 
			
		||||
  temp = temp
 | 
			
		||||
    .replaceAll("<", '<span style="color:#d4d4d4"><</span>')
 | 
			
		||||
    .replaceAll(">", '<span style="color:#d4d4d4">></span>');
 | 
			
		||||
  temp = temp
 | 
			
		||||
    .replaceAll("[", '<span style="color:#ffd70a">[</span>')
 | 
			
		||||
    .replaceAll("]", '<span style="color:#ffd70a">]</span>');
 | 
			
		||||
  temp = temp
 | 
			
		||||
    .replaceAll("(", '<span style="color:#87cefa">(</span>')
 | 
			
		||||
    .replaceAll(")", '<span style="color:#87cefa">)</span>');
 | 
			
		||||
  temp = temp
 | 
			
		||||
    .replaceAll("{", '<span style="color:#c586b6">{</span>')
 | 
			
		||||
    .replaceAll("}", '<span style="color:#c586b6">}</span>');
 | 
			
		||||
  temp = temp.replaceAll("\n", "<br />");
 | 
			
		||||
  return temp;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// date formatting
 | 
			
		||||
 | 
			
		||||
export function getTimeLapse(unixtime) {
 | 
			
		||||
  if (date.inferDateFormat(unixtime) === "string") {
 | 
			
		||||
    unixtime = date.formatDate(unixtime, "X");
 | 
			
		||||
  }
 | 
			
		||||
  var previous = unixtime * 1000;
 | 
			
		||||
  var current = new Date();
 | 
			
		||||
  var msPerMinute = 60 * 1000;
 | 
			
		||||
  var msPerHour = msPerMinute * 60;
 | 
			
		||||
  var msPerDay = msPerHour * 24;
 | 
			
		||||
  var msPerMonth = msPerDay * 30;
 | 
			
		||||
  var msPerYear = msPerDay * 365;
 | 
			
		||||
  var elapsed = current - previous;
 | 
			
		||||
  if (elapsed < msPerMinute) {
 | 
			
		||||
    return Math.round(elapsed / 1000) + " seconds ago";
 | 
			
		||||
  } else if (elapsed < msPerHour) {
 | 
			
		||||
    return Math.round(elapsed / msPerMinute) + " minutes ago";
 | 
			
		||||
  } else if (elapsed < msPerDay) {
 | 
			
		||||
    return Math.round(elapsed / msPerHour) + " hours ago";
 | 
			
		||||
  } else if (elapsed < msPerMonth) {
 | 
			
		||||
    return Math.round(elapsed / msPerDay) + " days ago";
 | 
			
		||||
  } else if (elapsed < msPerYear) {
 | 
			
		||||
    return Math.round(elapsed / msPerMonth) + " months ago";
 | 
			
		||||
  } else {
 | 
			
		||||
    return Math.round(elapsed / msPerYear) + " years ago";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatDate(dateString, format = "MMM-DD-YYYY HH:mm") {
 | 
			
		||||
  if (!dateString) return "";
 | 
			
		||||
  return date.formatDate(dateString, format);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getNextAgentUpdateTime() {
 | 
			
		||||
  const d = new Date();
 | 
			
		||||
  let ret;
 | 
			
		||||
  if (d.getMinutes() <= 35) {
 | 
			
		||||
    ret = d.setMinutes(35);
 | 
			
		||||
  } else {
 | 
			
		||||
    ret = date.addToDate(d, { hours: 1 });
 | 
			
		||||
    ret.setMinutes(35);
 | 
			
		||||
  }
 | 
			
		||||
  const a = date.formatDate(ret, "MMM D, YYYY");
 | 
			
		||||
  const b = date.formatDate(ret, "h:mm A");
 | 
			
		||||
  return `${a} at ${b}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// converts a date with timezone to local for html native datetime fields -> YYYY-MM-DD HH:mm:ss
 | 
			
		||||
export function formatDateInputField(isoDateString, noTimezone = false) {
 | 
			
		||||
  if (noTimezone) {
 | 
			
		||||
    isoDateString = isoDateString.replace("Z", "");
 | 
			
		||||
  }
 | 
			
		||||
  return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// converts a local date string "YYYY-MM-DDTHH:mm:ss" to an iso date string with the local timezone
 | 
			
		||||
export function formatDateStringwithTimezone(localDateString) {
 | 
			
		||||
  return date.formatDate(localDateString, "YYYY-MM-DDTHH:mm:ssZ");
 | 
			
		||||
}
 | 
			
		||||
// string formatting
 | 
			
		||||
 | 
			
		||||
export function capitalize(string) {
 | 
			
		||||
  return string[0].toUpperCase() + string.substring(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatTableColumnText(text) {
 | 
			
		||||
  let string = "";
 | 
			
		||||
  // split at underscore if exists
 | 
			
		||||
  const words = text.split("_");
 | 
			
		||||
  words.forEach((word) => (string = string + " " + capitalize(word)));
 | 
			
		||||
 | 
			
		||||
  return string.trim();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function truncateText(txt, chars) {
 | 
			
		||||
  if (!txt) return;
 | 
			
		||||
 | 
			
		||||
  return txt.length >= chars ? txt.substring(0, chars) + "..." : txt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function bytes2Human(bytes) {
 | 
			
		||||
  if (bytes == 0) return "0B";
 | 
			
		||||
  const k = 1024;
 | 
			
		||||
  const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
 | 
			
		||||
  const i = Math.floor(Math.log(bytes) / Math.log(k));
 | 
			
		||||
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function convertMemoryToPercent(percent, memory) {
 | 
			
		||||
  const mb = memory * 1024;
 | 
			
		||||
  return Math.ceil((percent * mb) / 100).toLocaleString();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// convert time period(str) to seconds(int) (3h -> 10800) used for comparing time intervals
 | 
			
		||||
export function convertPeriodToSeconds(period) {
 | 
			
		||||
  if (!validateTimePeriod(period)) {
 | 
			
		||||
    console.error("Time Period is invalid");
 | 
			
		||||
    return NaN;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (period.toUpperCase().includes("S"))
 | 
			
		||||
    // remove last letter from string and return since already in seconds
 | 
			
		||||
    return parseInt(period.slice(0, -1));
 | 
			
		||||
  else if (period.toUpperCase().includes("M"))
 | 
			
		||||
    // remove last letter from string and multiple by 60 to get seconds
 | 
			
		||||
    return parseInt(period.slice(0, -1)) * 60;
 | 
			
		||||
  else if (period.toUpperCase().includes("H"))
 | 
			
		||||
    // remove last letter from string and multiple by 60 twice to get seconds
 | 
			
		||||
    return parseInt(period.slice(0, -1)) * 60 * 60;
 | 
			
		||||
  else if (period.toUpperCase().includes("D"))
 | 
			
		||||
    // remove last letter from string and multiply by 24 and 60 twice to get seconds
 | 
			
		||||
    return parseInt(period.slice(0, -1)) * 24 * 60 * 60;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// takes an integer and converts it to an array in binary format. i.e: 13 -> [8, 4, 1]
 | 
			
		||||
// Needed to work with multi-select fields in tasks form
 | 
			
		||||
export function convertToBitArray(number) {
 | 
			
		||||
  let bitArray = [];
 | 
			
		||||
  let binary = number.toString(2);
 | 
			
		||||
  for (let i = 0; i < binary.length; ++i) {
 | 
			
		||||
    if (binary[i] !== "0") {
 | 
			
		||||
      // last binary digit
 | 
			
		||||
      if (binary.slice(i).length === 1) {
 | 
			
		||||
        bitArray.push(1);
 | 
			
		||||
      } else {
 | 
			
		||||
        bitArray.push(
 | 
			
		||||
          parseInt(binary.slice(i), 2) - parseInt(binary.slice(i + 1), 2),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return bitArray;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// takes an array of integers and adds them together
 | 
			
		||||
export function convertFromBitArray(array) {
 | 
			
		||||
  let result = 0;
 | 
			
		||||
  for (let i = 0; i < array.length; i++) {
 | 
			
		||||
    result += array[i];
 | 
			
		||||
  }
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function convertCamelCase(str) {
 | 
			
		||||
  return str
 | 
			
		||||
    .replace(/[^a-zA-Z0-9]+/g, " ")
 | 
			
		||||
    .replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
 | 
			
		||||
      return index == 0 ? word.toLowerCase() : word.toUpperCase();
 | 
			
		||||
    })
 | 
			
		||||
    .replace(/\s+/g, "");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										472
									
								
								src/utils/format.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										472
									
								
								src/utils/format.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,472 @@
 | 
			
		||||
import { date } from "quasar";
 | 
			
		||||
import { validateTimePeriod } from "@/utils/validation";
 | 
			
		||||
import trmmLogo from "@/assets/trmm_256.png";
 | 
			
		||||
 | 
			
		||||
import type { Script } from "@/types/scripts";
 | 
			
		||||
import type { Agent } from "@/types/agents";
 | 
			
		||||
import type { Client, ClientWithSites } from "@/types/clients";
 | 
			
		||||
import type { User } from "@/types/accounts";
 | 
			
		||||
import type { Check } from "@/types/checks";
 | 
			
		||||
import { CustomField, CustomFieldValue } from "@/types/core/customfields";
 | 
			
		||||
import { URLAction } from "@/types/core/urlactions";
 | 
			
		||||
 | 
			
		||||
// dropdown options formatting
 | 
			
		||||
export interface SelectOptionCategory {
 | 
			
		||||
  category: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface OptionWithoutCategory {
 | 
			
		||||
  label: string;
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
  value: any;
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
  [x: string]: any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Option = SelectOptionCategory | OptionWithoutCategory | string;
 | 
			
		||||
 | 
			
		||||
export function removeExtraOptionCategories(array: Option[]) {
 | 
			
		||||
  const tmp: Option[] = [];
 | 
			
		||||
  for (let i = 0; i < array.length; i++) {
 | 
			
		||||
    const currentOption = array[i];
 | 
			
		||||
    const nextOption = array[i + 1];
 | 
			
		||||
 | 
			
		||||
    // Determine if current and next options are categories
 | 
			
		||||
    const isCurrentCategory =
 | 
			
		||||
      typeof currentOption === "object" && "category" in currentOption;
 | 
			
		||||
    const isNextCategory =
 | 
			
		||||
      typeof nextOption === "object" && "category" in nextOption;
 | 
			
		||||
 | 
			
		||||
    if (i === array.length - 1) {
 | 
			
		||||
      // Always add the last item if it's not a category
 | 
			
		||||
      if (!isCurrentCategory) {
 | 
			
		||||
        tmp.push(currentOption);
 | 
			
		||||
      }
 | 
			
		||||
    } else if (!(isCurrentCategory && isNextCategory)) {
 | 
			
		||||
      // Add the current option if it's not followed by a category option
 | 
			
		||||
      tmp.push(currentOption);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return tmp;
 | 
			
		||||
}
 | 
			
		||||
interface FormatOptionsParams {
 | 
			
		||||
  label: string; // Key to use for the label
 | 
			
		||||
  value?: string; // Key to use for the value, defaults to "id"
 | 
			
		||||
  flat?: boolean; // Whether to return a flat array of strings
 | 
			
		||||
  allowDuplicates?: boolean; // Whether to allow duplicate labels
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
  appendToOptionObject?: { [key: string]: any }; // Additional properties to append to each option object
 | 
			
		||||
  copyPropertiesList?: string[]; // List of properties to copy from the original objects
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
function _formatOptions<T extends { [key: string]: any }>(
 | 
			
		||||
  data: T[],
 | 
			
		||||
  {
 | 
			
		||||
    label,
 | 
			
		||||
    value = "id",
 | 
			
		||||
    flat = false,
 | 
			
		||||
    allowDuplicates = true,
 | 
			
		||||
    appendToOptionObject = {},
 | 
			
		||||
    copyPropertiesList = [],
 | 
			
		||||
  }: FormatOptionsParams,
 | 
			
		||||
): Option[] | string[] {
 | 
			
		||||
  if (!flat) {
 | 
			
		||||
    return data.map((item) => {
 | 
			
		||||
      const option: Partial<Option> = {
 | 
			
		||||
        label: item[label],
 | 
			
		||||
        value: item[value],
 | 
			
		||||
        ...appendToOptionObject,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      copyPropertiesList.forEach((prop) => {
 | 
			
		||||
        if (Object.hasOwn(item, prop)) {
 | 
			
		||||
          option[prop] = item[prop];
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return option as Option;
 | 
			
		||||
    });
 | 
			
		||||
  } else {
 | 
			
		||||
    const labels = data.map((item) => item[label]);
 | 
			
		||||
    return allowDuplicates ? labels : [...new Set(labels)];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatScriptOptions(data: Script[]): Option[] {
 | 
			
		||||
  const categoryMap = new Map<string, Script[]>();
 | 
			
		||||
  let hasUnassigned = false;
 | 
			
		||||
 | 
			
		||||
  data.forEach((script) => {
 | 
			
		||||
    const category = script.category || "Unassigned";
 | 
			
		||||
    if (!script.category) hasUnassigned = true;
 | 
			
		||||
 | 
			
		||||
    if (!categoryMap.has(category)) {
 | 
			
		||||
      categoryMap.set(category, []);
 | 
			
		||||
    }
 | 
			
		||||
    categoryMap.get(category)!.push(script);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const categories = Array.from(categoryMap.keys());
 | 
			
		||||
  if (hasUnassigned) {
 | 
			
		||||
    // Ensure "Unassigned" is the last category
 | 
			
		||||
    const index = categories.indexOf("Unassigned");
 | 
			
		||||
    categories.splice(index, 1);
 | 
			
		||||
    categories.push("Unassigned");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  categories.sort();
 | 
			
		||||
 | 
			
		||||
  const options: Option[] = [];
 | 
			
		||||
  categories.forEach((cat) => {
 | 
			
		||||
    options.push({ category: cat });
 | 
			
		||||
 | 
			
		||||
    const scripts = categoryMap
 | 
			
		||||
      .get(cat)!
 | 
			
		||||
      .sort((a, b) => a.name.localeCompare(b.name));
 | 
			
		||||
    scripts.forEach((script) => {
 | 
			
		||||
      const option: Option = {
 | 
			
		||||
        img_right: script.script_type === "builtin" ? trmmLogo : undefined,
 | 
			
		||||
        label: script.name,
 | 
			
		||||
        value: script.id,
 | 
			
		||||
        default_timeout: script.default_timeout,
 | 
			
		||||
        args: script.args,
 | 
			
		||||
        env_vars: script.env_vars,
 | 
			
		||||
        filename: script.filename,
 | 
			
		||||
        syntax: script.syntax,
 | 
			
		||||
        script_type: script.script_type,
 | 
			
		||||
        shell: script.shell,
 | 
			
		||||
        supported_platforms: script.supported_platforms,
 | 
			
		||||
      };
 | 
			
		||||
      options.push(option);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return options;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatAgentOptions(
 | 
			
		||||
  data: Agent[],
 | 
			
		||||
  flat = false,
 | 
			
		||||
  value_field: keyof Agent = "agent_id",
 | 
			
		||||
): Option[] | string[] {
 | 
			
		||||
  if (flat) {
 | 
			
		||||
    // Returns just agent hostnames in an array
 | 
			
		||||
    return _formatOptions(data, {
 | 
			
		||||
      label: "hostname",
 | 
			
		||||
      value: value_field as string,
 | 
			
		||||
      flat: true,
 | 
			
		||||
      allowDuplicates: false,
 | 
			
		||||
    });
 | 
			
		||||
  } else {
 | 
			
		||||
    // Returns options with categories in object format
 | 
			
		||||
    const options: Option[] = [];
 | 
			
		||||
    const agents = data.map((agent) => ({
 | 
			
		||||
      label: agent.hostname,
 | 
			
		||||
      value: agent[value_field] as string,
 | 
			
		||||
      cat: `${agent.client} > ${agent.site}`,
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    const categories = [...new Set(agents.map((agent) => agent.cat))].sort();
 | 
			
		||||
 | 
			
		||||
    categories.forEach((cat) => {
 | 
			
		||||
      options.push({ category: cat });
 | 
			
		||||
      const agentsInCategory = agents.filter((agent) => agent.cat === cat);
 | 
			
		||||
      const sortedAgents = agentsInCategory.sort((a, b) =>
 | 
			
		||||
        a.label.localeCompare(b.label),
 | 
			
		||||
      );
 | 
			
		||||
      options.push(
 | 
			
		||||
        ...sortedAgents.map(({ label, value }) => ({ label, value })),
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return options;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatCustomFieldOptions(
 | 
			
		||||
  data: CustomField[],
 | 
			
		||||
  flat = false,
 | 
			
		||||
): Option[] {
 | 
			
		||||
  if (flat) {
 | 
			
		||||
    // For a flat list, simply format the options based on the "name" property
 | 
			
		||||
    return _formatOptions(data, { label: "name", flat: true });
 | 
			
		||||
  } else {
 | 
			
		||||
    // Predefined categories for organizing the custom fields
 | 
			
		||||
    const categories = ["Client", "Site", "Agent"];
 | 
			
		||||
    const options: Option[] = [];
 | 
			
		||||
 | 
			
		||||
    categories.forEach((cat) => {
 | 
			
		||||
      // Add a category header as an option
 | 
			
		||||
      options.push({ category: cat, label: cat, value: cat });
 | 
			
		||||
 | 
			
		||||
      // Filter and map the custom fields that match the current category
 | 
			
		||||
      const matchingFields = data
 | 
			
		||||
        .filter((custom_field) => custom_field.model === cat.toLowerCase())
 | 
			
		||||
        .map((custom_field) => ({
 | 
			
		||||
          label: custom_field.name,
 | 
			
		||||
          value: custom_field.id,
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
      // Sort the filtered custom fields by their labels and add them to the options
 | 
			
		||||
      const sortedFields = matchingFields.sort((a, b) =>
 | 
			
		||||
        a.label.localeCompare(b.label),
 | 
			
		||||
      );
 | 
			
		||||
      options.push(...sortedFields);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return options;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatClientOptions(data: Client[], flat = false) {
 | 
			
		||||
  return _formatOptions(data, { label: "name", flat: flat });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatSiteOptions(data: ClientWithSites[], flat = false) {
 | 
			
		||||
  const options = [] as Option[];
 | 
			
		||||
  data.forEach((client) => {
 | 
			
		||||
    options.push({ category: client.name });
 | 
			
		||||
    options.push(
 | 
			
		||||
      ..._formatOptions(client.sites, {
 | 
			
		||||
        label: "name",
 | 
			
		||||
        flat: flat,
 | 
			
		||||
        appendToOptionObject: { cat: client.name },
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return options;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatUserOptions(data: User[], flat = false) {
 | 
			
		||||
  return _formatOptions(data, { label: "username", flat: flat });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatCheckOptions(data: Check[], flat = false) {
 | 
			
		||||
  return _formatOptions(data, { label: "readable_desc", flat: flat });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatURLActionOptions(data: URLAction[], flat = false) {
 | 
			
		||||
  return _formatOptions(data, {
 | 
			
		||||
    label: "name",
 | 
			
		||||
    flat: flat,
 | 
			
		||||
    copyPropertiesList: ["action_type"],
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatCustomFields(
 | 
			
		||||
  fields: CustomField[],
 | 
			
		||||
  values: CustomFieldValue,
 | 
			
		||||
) {
 | 
			
		||||
  const tempArray = [];
 | 
			
		||||
 | 
			
		||||
  for (const field of fields) {
 | 
			
		||||
    if (field.type === "multiple") {
 | 
			
		||||
      tempArray.push({ multiple_value: values[field.name], field: field.id });
 | 
			
		||||
    } else if (field.type === "checkbox") {
 | 
			
		||||
      tempArray.push({ bool_value: values[field.name], field: field.id });
 | 
			
		||||
    } else {
 | 
			
		||||
      tempArray.push({ string_value: values[field.name], field: field.id });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return tempArray;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatScriptSyntax(syntax: string) {
 | 
			
		||||
  let temp = syntax;
 | 
			
		||||
  temp = temp.replaceAll("<", "<").replaceAll(">", ">");
 | 
			
		||||
  temp = temp
 | 
			
		||||
    .replaceAll("<", '<span style="color:#d4d4d4"><</span>')
 | 
			
		||||
    .replaceAll(">", '<span style="color:#d4d4d4">></span>');
 | 
			
		||||
  temp = temp
 | 
			
		||||
    .replaceAll("[", '<span style="color:#ffd70a">[</span>')
 | 
			
		||||
    .replaceAll("]", '<span style="color:#ffd70a">]</span>');
 | 
			
		||||
  temp = temp
 | 
			
		||||
    .replaceAll("(", '<span style="color:#87cefa">(</span>')
 | 
			
		||||
    .replaceAll(")", '<span style="color:#87cefa">)</span>');
 | 
			
		||||
  temp = temp
 | 
			
		||||
    .replaceAll("{", '<span style="color:#c586b6">{</span>')
 | 
			
		||||
    .replaceAll("}", '<span style="color:#c586b6">}</span>');
 | 
			
		||||
  temp = temp.replaceAll("\n", "<br />");
 | 
			
		||||
  return temp;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// date formatting
 | 
			
		||||
 | 
			
		||||
export function getTimeLapse(unixtime: number) {
 | 
			
		||||
  if (date.inferDateFormat(unixtime) === "string") {
 | 
			
		||||
    unixtime = parseInt(date.formatDate(unixtime, "X"));
 | 
			
		||||
  }
 | 
			
		||||
  const previous = unixtime * 1000;
 | 
			
		||||
  const current = Date.now();
 | 
			
		||||
  const msPerMinute = 60 * 1000;
 | 
			
		||||
  const msPerHour = msPerMinute * 60;
 | 
			
		||||
  const msPerDay = msPerHour * 24;
 | 
			
		||||
  const msPerMonth = msPerDay * 30;
 | 
			
		||||
  const msPerYear = msPerDay * 365;
 | 
			
		||||
  const elapsed = current - previous;
 | 
			
		||||
  if (elapsed < msPerMinute) {
 | 
			
		||||
    return Math.round(elapsed / 1000) + " seconds ago";
 | 
			
		||||
  } else if (elapsed < msPerHour) {
 | 
			
		||||
    return Math.round(elapsed / msPerMinute) + " minutes ago";
 | 
			
		||||
  } else if (elapsed < msPerDay) {
 | 
			
		||||
    return Math.round(elapsed / msPerHour) + " hours ago";
 | 
			
		||||
  } else if (elapsed < msPerMonth) {
 | 
			
		||||
    return Math.round(elapsed / msPerDay) + " days ago";
 | 
			
		||||
  } else if (elapsed < msPerYear) {
 | 
			
		||||
    return Math.round(elapsed / msPerMonth) + " months ago";
 | 
			
		||||
  } else {
 | 
			
		||||
    return Math.round(elapsed / msPerYear) + " years ago";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatDate(
 | 
			
		||||
  dateString: string | number | Date,
 | 
			
		||||
  format = "MMM-DD-YYYY HH:mm",
 | 
			
		||||
) {
 | 
			
		||||
  if (!dateString) return "";
 | 
			
		||||
  return date.formatDate(dateString, format);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getNextAgentUpdateTime() {
 | 
			
		||||
  const d = new Date();
 | 
			
		||||
  let ret;
 | 
			
		||||
  if (d.getMinutes() <= 35) {
 | 
			
		||||
    ret = d.setMinutes(35);
 | 
			
		||||
  } else {
 | 
			
		||||
    ret = date.addToDate(d, { hours: 1 });
 | 
			
		||||
    ret.setMinutes(35);
 | 
			
		||||
  }
 | 
			
		||||
  const a = date.formatDate(ret, "MMM D, YYYY");
 | 
			
		||||
  const b = date.formatDate(ret, "h:mm A");
 | 
			
		||||
  return `${a} at ${b}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// converts a date with timezone to local for html native datetime fields -> YYYY-MM-DD HH:mm:ss
 | 
			
		||||
export function formatDateInputField(
 | 
			
		||||
  isoDateString: string | number,
 | 
			
		||||
  noTimezone = false,
 | 
			
		||||
) {
 | 
			
		||||
  if (noTimezone && typeof isoDateString === "string") {
 | 
			
		||||
    isoDateString = isoDateString.replace("Z", "");
 | 
			
		||||
  }
 | 
			
		||||
  return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// converts a local date string "YYYY-MM-DDTHH:mm:ss" to an iso date string with the local timezone
 | 
			
		||||
export function formatDateStringwithTimezone(localDateString: string) {
 | 
			
		||||
  return date.formatDate(localDateString, "YYYY-MM-DDTHH:mm:ssZ");
 | 
			
		||||
}
 | 
			
		||||
// string formatting
 | 
			
		||||
 | 
			
		||||
export function capitalize(string: string) {
 | 
			
		||||
  return string[0].toUpperCase() + string.substring(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatTableColumnText(text: string) {
 | 
			
		||||
  let string = "";
 | 
			
		||||
  // split at underscore if exists
 | 
			
		||||
  const words = text.split("_");
 | 
			
		||||
  words.forEach((word) => (string = string + " " + capitalize(word)));
 | 
			
		||||
 | 
			
		||||
  return string.trim();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function truncateText(txt: string, chars: number) {
 | 
			
		||||
  if (!txt) return;
 | 
			
		||||
 | 
			
		||||
  return txt.length >= chars ? txt.substring(0, chars) + "..." : txt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function bytes2Human(bytes: number) {
 | 
			
		||||
  if (bytes == 0) return "0B";
 | 
			
		||||
  const k = 1024;
 | 
			
		||||
  const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
 | 
			
		||||
  const i = Math.floor(Math.log(bytes) / Math.log(k));
 | 
			
		||||
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function convertMemoryToPercent(percent: number, memory: number) {
 | 
			
		||||
  const mb = memory * 1024;
 | 
			
		||||
  return Math.ceil((percent * mb) / 100).toLocaleString();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// convert time period(str) to seconds(int) (3h -> 10800) used for comparing time intervals
 | 
			
		||||
export function convertPeriodToSeconds(period: string) {
 | 
			
		||||
  if (!validateTimePeriod(period)) {
 | 
			
		||||
    console.error("Time Period is invalid");
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (period.toUpperCase().includes("S"))
 | 
			
		||||
    // remove last letter from string and return since already in seconds
 | 
			
		||||
    return parseInt(period.slice(0, -1));
 | 
			
		||||
  else if (period.toUpperCase().includes("M"))
 | 
			
		||||
    // remove last letter from string and multiple by 60 to get seconds
 | 
			
		||||
    return parseInt(period.slice(0, -1)) * 60;
 | 
			
		||||
  else if (period.toUpperCase().includes("H"))
 | 
			
		||||
    // remove last letter from string and multiple by 60 twice to get seconds
 | 
			
		||||
    return parseInt(period.slice(0, -1)) * 60 * 60;
 | 
			
		||||
  else if (period.toUpperCase().includes("D"))
 | 
			
		||||
    // remove last letter from string and multiply by 24 and 60 twice to get seconds
 | 
			
		||||
    return parseInt(period.slice(0, -1)) * 24 * 60 * 60;
 | 
			
		||||
 | 
			
		||||
  return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// takes an integer and converts it to an array in binary format. i.e: 13 -> [8, 4, 1]
 | 
			
		||||
// Needed to work with multi-select fields in tasks form
 | 
			
		||||
export function convertToBitArray(number: number) {
 | 
			
		||||
  const bitArray = [];
 | 
			
		||||
  const binary = number.toString(2);
 | 
			
		||||
  for (let i = 0; i < binary.length; ++i) {
 | 
			
		||||
    if (binary[i] !== "0") {
 | 
			
		||||
      // last binary digit
 | 
			
		||||
      if (binary.slice(i).length === 1) {
 | 
			
		||||
        bitArray.push(1);
 | 
			
		||||
      } else {
 | 
			
		||||
        bitArray.push(
 | 
			
		||||
          parseInt(binary.slice(i), 2) - parseInt(binary.slice(i + 1), 2),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return bitArray;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// takes an array of integers and adds them together
 | 
			
		||||
export function convertFromBitArray(array: number[]) {
 | 
			
		||||
  let result = 0;
 | 
			
		||||
  for (let i = 0; i < array.length; i++) {
 | 
			
		||||
    result += array[i];
 | 
			
		||||
  }
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function convertCamelCase(str: string) {
 | 
			
		||||
  return str
 | 
			
		||||
    .replace(/[^a-zA-Z0-9]+/g, " ")
 | 
			
		||||
    .replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
 | 
			
		||||
      return index == 0 ? word.toLowerCase() : word.toUpperCase();
 | 
			
		||||
    })
 | 
			
		||||
    .replace(/\s+/g, "");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This will take an object and make a clone of it without including some of the keys
 | 
			
		||||
export function copyObjectWithoutKeys<
 | 
			
		||||
  T extends Record<string, unknown>,
 | 
			
		||||
  K extends keyof T,
 | 
			
		||||
>(objToCopy: T, keysToExclude: Array<K>): Omit<T, K> {
 | 
			
		||||
  const result: Partial<T> = {};
 | 
			
		||||
 | 
			
		||||
  Object.keys(objToCopy).forEach((key) => {
 | 
			
		||||
    if (!keysToExclude.includes(key as K)) {
 | 
			
		||||
      // Use an intermediate variable with a more permissive type
 | 
			
		||||
      const safeKey: keyof T = key as keyof T;
 | 
			
		||||
      result[safeKey] = objToCopy[safeKey];
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return result as Omit<T, K>;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								src/utils/helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/utils/helpers.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
import { copyToClipboard } from "quasar";
 | 
			
		||||
import { notifySuccess } from "@/utils/notify";
 | 
			
		||||
 | 
			
		||||
export function copyOutput(val: string) {
 | 
			
		||||
  copyToClipboard(val).then(() => {
 | 
			
		||||
    notifySuccess("Copied to clipboard");
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { Notify } from "quasar";
 | 
			
		||||
 | 
			
		||||
export function notifySuccess(msg, timeout = 2000) {
 | 
			
		||||
export function notifySuccess(msg: string, timeout = 2000) {
 | 
			
		||||
  Notify.create({
 | 
			
		||||
    type: "positive",
 | 
			
		||||
    message: msg,
 | 
			
		||||
@@ -8,7 +8,7 @@ export function notifySuccess(msg, timeout = 2000) {
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function notifyError(msg, timeout = 2000) {
 | 
			
		||||
export function notifyError(msg: string, timeout = 2000) {
 | 
			
		||||
  Notify.create({
 | 
			
		||||
    type: "negative",
 | 
			
		||||
    message: msg,
 | 
			
		||||
@@ -16,7 +16,7 @@ export function notifyError(msg, timeout = 2000) {
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function notifyWarning(msg, timeout = 2000) {
 | 
			
		||||
export function notifyWarning(msg: string, timeout = 2000) {
 | 
			
		||||
  Notify.create({
 | 
			
		||||
    type: "warning",
 | 
			
		||||
    message: msg,
 | 
			
		||||
@@ -24,7 +24,7 @@ export function notifyWarning(msg, timeout = 2000) {
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function notifyInfo(msg, timeout = 2000) {
 | 
			
		||||
export function notifyInfo(msg: string, timeout = 2000) {
 | 
			
		||||
  Notify.create({
 | 
			
		||||
    type: "info",
 | 
			
		||||
    message: msg,
 | 
			
		||||
@@ -1,6 +1,10 @@
 | 
			
		||||
import { Notify } from "quasar";
 | 
			
		||||
 | 
			
		||||
export function isValidThreshold(warning, error, diskcheck = false) {
 | 
			
		||||
export function isValidThreshold(
 | 
			
		||||
  warning: number,
 | 
			
		||||
  error: number,
 | 
			
		||||
  diskcheck = false,
 | 
			
		||||
) {
 | 
			
		||||
  if (warning === 0 && error === 0) {
 | 
			
		||||
    Notify.create({
 | 
			
		||||
      type: "negative",
 | 
			
		||||
@@ -31,7 +35,7 @@ export function isValidThreshold(warning, error, diskcheck = false) {
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function validateEventID(val) {
 | 
			
		||||
export function validateEventID(val: number | "*") {
 | 
			
		||||
  if (val === null || val.toString().replace(/\s/g, "") === "") {
 | 
			
		||||
    return false;
 | 
			
		||||
  } else if (val === "*") {
 | 
			
		||||
@@ -44,10 +48,20 @@ export function validateEventID(val) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// validate script return code
 | 
			
		||||
export function validateRetcode(val, done) {
 | 
			
		||||
// function is used for quasar's q-select on-new-value function
 | 
			
		||||
export function validateRetcode(
 | 
			
		||||
  val: string,
 | 
			
		||||
  done: (item?: unknown, mode?: "add" | "add-unique" | "toggle") => void,
 | 
			
		||||
) {
 | 
			
		||||
  /^\d+$/.test(val) ? done(val) : done();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function validateTimePeriod(val) {
 | 
			
		||||
export function validateTimePeriod(val: string) {
 | 
			
		||||
  return /^\d{1,3}(H|h|M|m|S|s|d|D)$/.test(val);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isValidEmail(val: string) {
 | 
			
		||||
  const email =
 | 
			
		||||
    /^(?=[a-zA-Z0-9@._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/;
 | 
			
		||||
  return email.test(val);
 | 
			
		||||
}
 | 
			
		||||
@@ -693,7 +693,7 @@ export default {
 | 
			
		||||
        this.$q
 | 
			
		||||
          .dialog({
 | 
			
		||||
            title: "Are you sure?",
 | 
			
		||||
            message: `Delete site: ${node.label}.`,
 | 
			
		||||
            message: `Delete ${node.children ? "client" : "site"}: ${node.label}.`,
 | 
			
		||||
            cancel: true,
 | 
			
		||||
            ok: { label: "Delete", color: "negative" },
 | 
			
		||||
          })
 | 
			
		||||
@@ -824,7 +824,9 @@ export default {
 | 
			
		||||
          );
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        this.urlActions = r.data;
 | 
			
		||||
        this.urlActions = r.data.filter(
 | 
			
		||||
          (action) => action.action_type === "web",
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    runURLAction(id, action, model) {
 | 
			
		||||
 
 | 
			
		||||
@@ -53,6 +53,26 @@
 | 
			
		||||
                :options="allTimezones"
 | 
			
		||||
              />
 | 
			
		||||
            </q-card-section>
 | 
			
		||||
 | 
			
		||||
            <q-card-section>
 | 
			
		||||
              <div>
 | 
			
		||||
                Company name:
 | 
			
		||||
                <q-icon
 | 
			
		||||
                  name="ion-information-circle-outline"
 | 
			
		||||
                  size="sm"
 | 
			
		||||
                  class="q-ml-sm cursor-pointer"
 | 
			
		||||
                >
 | 
			
		||||
                  <q-tooltip class="text-caption">
 | 
			
		||||
                    Adding your company name here will append it to the user's
 | 
			
		||||
                    full name that appears when doing a remote control session,
 | 
			
		||||
                    for example: 'John Doe - Amidaware Inc.'
 | 
			
		||||
                  </q-tooltip>
 | 
			
		||||
                </q-icon>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <q-input dense outlined v-model="companyname"> </q-input>
 | 
			
		||||
            </q-card-section>
 | 
			
		||||
 | 
			
		||||
            <q-card-actions align="center">
 | 
			
		||||
              <q-btn
 | 
			
		||||
                label="Finish"
 | 
			
		||||
@@ -86,6 +106,7 @@ export default {
 | 
			
		||||
      allTimezones: [],
 | 
			
		||||
      timezone: null,
 | 
			
		||||
      arch: "64",
 | 
			
		||||
      companyname: "",
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
@@ -95,6 +116,7 @@ export default {
 | 
			
		||||
        client: this.client,
 | 
			
		||||
        site: this.site,
 | 
			
		||||
        timezone: this.timezone,
 | 
			
		||||
        companyname: this.companyname,
 | 
			
		||||
        initialsetup: true,
 | 
			
		||||
      };
 | 
			
		||||
      this.$axios
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
          </q-card-section>
 | 
			
		||||
          <q-card-section>
 | 
			
		||||
            <q-form @submit.prevent="checkCreds" class="q-gutter-md">
 | 
			
		||||
            <q-form ref="form" @submit.prevent="checkCreds" class="q-gutter-md">
 | 
			
		||||
              <q-input
 | 
			
		||||
                filled
 | 
			
		||||
                v-model="credentials.username"
 | 
			
		||||
@@ -24,7 +24,7 @@
 | 
			
		||||
              <q-input
 | 
			
		||||
                v-model="credentials.password"
 | 
			
		||||
                filled
 | 
			
		||||
                :type="isPwd ? 'password' : 'text'"
 | 
			
		||||
                :type="showPassword ? 'password' : 'text'"
 | 
			
		||||
                label="Password"
 | 
			
		||||
                lazy-rules
 | 
			
		||||
                :rules="[
 | 
			
		||||
@@ -33,9 +33,9 @@
 | 
			
		||||
              >
 | 
			
		||||
                <template v-slot:append>
 | 
			
		||||
                  <q-icon
 | 
			
		||||
                    :name="isPwd ? 'visibility_off' : 'visibility'"
 | 
			
		||||
                    :name="showPassword ? 'visibility_off' : 'visibility'"
 | 
			
		||||
                    class="cursor-pointer"
 | 
			
		||||
                    @click="isPwd = !isPwd"
 | 
			
		||||
                    @click="showPassword = !showPassword"
 | 
			
		||||
                  />
 | 
			
		||||
                </template>
 | 
			
		||||
              </q-input>
 | 
			
		||||
@@ -53,7 +53,7 @@
 | 
			
		||||
        <!-- 2 factor modal -->
 | 
			
		||||
        <q-dialog persistent v-model="prompt">
 | 
			
		||||
          <q-card style="min-width: 400px">
 | 
			
		||||
            <q-form @submit.prevent="onSubmit">
 | 
			
		||||
            <q-form ref="formToken" @submit.prevent="onSubmit">
 | 
			
		||||
              <q-card-section class="text-center text-h6"
 | 
			
		||||
                >Two-Factor Token</q-card-section
 | 
			
		||||
              >
 | 
			
		||||
@@ -62,7 +62,8 @@
 | 
			
		||||
                <q-input
 | 
			
		||||
                  autofocus
 | 
			
		||||
                  outlined
 | 
			
		||||
                  v-model="credentials.twofactor"
 | 
			
		||||
                  autocomplete="one-time-code"
 | 
			
		||||
                  v-model="twofactor"
 | 
			
		||||
                  :rules="[
 | 
			
		||||
                    (val) =>
 | 
			
		||||
                      (val && val.length > 0) || 'This field is required',
 | 
			
		||||
@@ -82,53 +83,58 @@
 | 
			
		||||
  </q-layout>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import mixins from "@/mixins/mixins";
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, reactive } from "vue";
 | 
			
		||||
import { type QForm, useQuasar } from "quasar";
 | 
			
		||||
import { useAuthStore } from "@/stores/auth";
 | 
			
		||||
import { useRouter } from "vue-router";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "LoginView",
 | 
			
		||||
  mixins: [mixins],
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      credentials: {},
 | 
			
		||||
      prompt: false,
 | 
			
		||||
      isPwd: true,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
// setup quasar
 | 
			
		||||
const $q = useQuasar();
 | 
			
		||||
$q.dark.set(true);
 | 
			
		||||
 | 
			
		||||
  methods: {
 | 
			
		||||
    checkCreds() {
 | 
			
		||||
      this.$axios.post("/checkcreds/", this.credentials).then((r) => {
 | 
			
		||||
        if (r.data.totp === "totp not set") {
 | 
			
		||||
          // sign in to setup two factor temporarily
 | 
			
		||||
          const token = r.data.token;
 | 
			
		||||
          const username = r.data.username;
 | 
			
		||||
          localStorage.setItem("access_token", token);
 | 
			
		||||
          localStorage.setItem("user_name", username);
 | 
			
		||||
          this.$store.commit("retrieveToken", { token, username });
 | 
			
		||||
          this.$router.push({ name: "TOTPSetup" });
 | 
			
		||||
        } else {
 | 
			
		||||
          this.prompt = true;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    onSubmit() {
 | 
			
		||||
      this.$store
 | 
			
		||||
        .dispatch("retrieveToken", this.credentials)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          this.credentials = {};
 | 
			
		||||
          this.$router.push({ name: "Dashboard" });
 | 
			
		||||
        })
 | 
			
		||||
        .catch(() => {
 | 
			
		||||
          this.credentials = {};
 | 
			
		||||
          this.prompt = false;
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.$q.dark.set(true);
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
// setup auth store
 | 
			
		||||
const auth = useAuthStore();
 | 
			
		||||
 | 
			
		||||
// setup router
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
 | 
			
		||||
const form = ref<QForm | null>(null);
 | 
			
		||||
const formToken = ref<QForm | null>(null);
 | 
			
		||||
 | 
			
		||||
// login logic
 | 
			
		||||
const credentials = reactive({ username: "", password: "" });
 | 
			
		||||
const twofactor = ref("");
 | 
			
		||||
const prompt = ref(false);
 | 
			
		||||
const showPassword = ref(true);
 | 
			
		||||
 | 
			
		||||
async function checkCreds() {
 | 
			
		||||
  try {
 | 
			
		||||
    const { totp } = await auth.checkCredentials(credentials);
 | 
			
		||||
 | 
			
		||||
    if (!totp) {
 | 
			
		||||
      router.push({ name: "TOTPSetup" });
 | 
			
		||||
    } else {
 | 
			
		||||
      twofactor.value = "";
 | 
			
		||||
      prompt.value = true;
 | 
			
		||||
    }
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.error(err);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function onSubmit() {
 | 
			
		||||
  try {
 | 
			
		||||
    await auth.login({ ...credentials, twofactor: twofactor.value });
 | 
			
		||||
    router.push({ name: "Dashboard" });
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.error(err);
 | 
			
		||||
  } finally {
 | 
			
		||||
    form.value?.reset();
 | 
			
		||||
    formToken.value?.reset();
 | 
			
		||||
    prompt.value = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,11 +5,19 @@
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: "SessionExpired",
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.$store.dispatch("destroyToken");
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { onMounted } from "vue";
 | 
			
		||||
import { useAuthStore } from "@/stores/auth";
 | 
			
		||||
import { useDashWSConnection } from "@/websocket/websocket";
 | 
			
		||||
 | 
			
		||||
// setup store
 | 
			
		||||
const auth = useAuthStore();
 | 
			
		||||
 | 
			
		||||
// setup websocket
 | 
			
		||||
const { close } = useDashWSConnection();
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  await auth.logout();
 | 
			
		||||
  close();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,20 +7,20 @@
 | 
			
		||||
          <q-card-section class="row items-center">
 | 
			
		||||
            <div class="text-h6">Setup 2-Factor</div>
 | 
			
		||||
          </q-card-section>
 | 
			
		||||
          <q-card-section v-if="qr_url">
 | 
			
		||||
          <q-card-section v-if="qrUrl">
 | 
			
		||||
            <p>
 | 
			
		||||
              Scan the QR Code with your authenticator app and then click Finish
 | 
			
		||||
              to be redirected back to the signin page. If you navigate away
 | 
			
		||||
              from this page you 2FA signin will need to be reset!
 | 
			
		||||
            </p>
 | 
			
		||||
            <qrcode-vue :value="qr_url" :size="200" level="H" />
 | 
			
		||||
            <img :src="qrCode" alt="QR Code" />
 | 
			
		||||
          </q-card-section>
 | 
			
		||||
          <q-card-section v-if="totp_key">
 | 
			
		||||
          <q-card-section v-if="totpKey">
 | 
			
		||||
            <p>
 | 
			
		||||
              You can also use the below code to configure the authenticator
 | 
			
		||||
              manually.
 | 
			
		||||
            </p>
 | 
			
		||||
            <p>{{ totp_key }}</p>
 | 
			
		||||
            <p>{{ totpKey }}</p>
 | 
			
		||||
          </q-card-section>
 | 
			
		||||
          <q-card-actions align="center">
 | 
			
		||||
            <q-btn
 | 
			
		||||
@@ -28,6 +28,7 @@
 | 
			
		||||
              color="primary"
 | 
			
		||||
              class="full-width"
 | 
			
		||||
              @click="logout"
 | 
			
		||||
              :loading="loading"
 | 
			
		||||
            />
 | 
			
		||||
          </q-card-actions>
 | 
			
		||||
        </q-card>
 | 
			
		||||
@@ -37,65 +38,63 @@
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import QrcodeVue from "qrcode.vue";
 | 
			
		||||
import mixins from "@/mixins/mixins";
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, onMounted, onBeforeUnmount } from "vue";
 | 
			
		||||
import { useQuasar } from "quasar";
 | 
			
		||||
import { useAuthStore } from "@/stores/auth";
 | 
			
		||||
import { useRouter } from "vue-router";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "TOTPSetup",
 | 
			
		||||
  mixins: [mixins],
 | 
			
		||||
  components: { QrcodeVue },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      totp_key: null,
 | 
			
		||||
      qr_url: null,
 | 
			
		||||
      cleared_token: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    getQRCodeData() {
 | 
			
		||||
      this.$q.loading.show();
 | 
			
		||||
import { useQRCode } from "@vueuse/integrations/useQRCode";
 | 
			
		||||
 | 
			
		||||
      this.$axios
 | 
			
		||||
        .post("/accounts/users/setup_totp/")
 | 
			
		||||
        .then((r) => {
 | 
			
		||||
          this.$q.loading.hide();
 | 
			
		||||
// setup quasar
 | 
			
		||||
const $q = useQuasar();
 | 
			
		||||
 | 
			
		||||
          if (r.data === "totp token already set") {
 | 
			
		||||
            //don't logout user if totp is already set
 | 
			
		||||
            this.cleared_token = true;
 | 
			
		||||
            this.$router.push({ name: "Login" });
 | 
			
		||||
          } else {
 | 
			
		||||
            this.totp_key = r.data.totp_key;
 | 
			
		||||
            this.qr_url = r.data.qr_url;
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        .catch(() => this.$q.loading.hide());
 | 
			
		||||
    },
 | 
			
		||||
    logout() {
 | 
			
		||||
      this.$q.loading.show();
 | 
			
		||||
      this.$store
 | 
			
		||||
        .dispatch("destroyToken")
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          this.cleared_token = true;
 | 
			
		||||
          this.$q.loading.hide();
 | 
			
		||||
          this.$router.push({ name: "Login" });
 | 
			
		||||
        })
 | 
			
		||||
        .catch(() => {
 | 
			
		||||
          this.cleared_token = true;
 | 
			
		||||
          this.$q.loading.hide();
 | 
			
		||||
          this.$router.push({ name: "Login" });
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.getQRCodeData();
 | 
			
		||||
    this.$q.dark.set(false);
 | 
			
		||||
  },
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    if (!this.cleared_token) {
 | 
			
		||||
      this.logout();
 | 
			
		||||
// setup auth store
 | 
			
		||||
const auth = useAuthStore();
 | 
			
		||||
 | 
			
		||||
// setup router
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
 | 
			
		||||
const totpKey = ref("");
 | 
			
		||||
const qrUrl = ref("");
 | 
			
		||||
const clearToken = ref(true);
 | 
			
		||||
const loading = ref(false);
 | 
			
		||||
 | 
			
		||||
const qrCode = useQRCode(qrUrl);
 | 
			
		||||
 | 
			
		||||
async function getQRCodeData() {
 | 
			
		||||
  loading.value = true;
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const data = await auth.setupTotp();
 | 
			
		||||
 | 
			
		||||
    if (!data) {
 | 
			
		||||
      //don't logout user if totp is already set
 | 
			
		||||
      clearToken.value = false;
 | 
			
		||||
      router.push({ name: "Login" });
 | 
			
		||||
    } else {
 | 
			
		||||
      totpKey.value = data.totp_key;
 | 
			
		||||
      qrUrl.value = data.qr_url;
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
  } finally {
 | 
			
		||||
    loading.value = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function logout() {
 | 
			
		||||
  await auth.logout();
 | 
			
		||||
  clearToken.value = false;
 | 
			
		||||
  router.push({ name: "Login" });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  getQRCodeData();
 | 
			
		||||
  $q.dark.set(false);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onBeforeUnmount(async () => {
 | 
			
		||||
  if (clearToken.value) {
 | 
			
		||||
    await auth.logout();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -90,7 +90,7 @@ export default {
 | 
			
		||||
        control.value = data.control;
 | 
			
		||||
        status.value = data.status;
 | 
			
		||||
        useMeta({
 | 
			
		||||
          title: `${data.hostname} - ${data.client} - ${data.site} | Remote Background`,
 | 
			
		||||
          title: `${data.hostname} - ${data.client} - ${data.site} | Take Control`,
 | 
			
		||||
        });
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        console.error(e);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										88
									
								
								src/views/WebTerminal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/views/WebTerminal.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="full-page-terminal">
 | 
			
		||||
    <div ref="xtermContainer" class="xterm-container"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
.full-page-terminal {
 | 
			
		||||
  height: 100vh;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm-container {
 | 
			
		||||
  flex-grow: 1;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
 | 
			
		||||
import { Terminal } from "@xterm/xterm";
 | 
			
		||||
import { FitAddon } from "@xterm/addon-fit";
 | 
			
		||||
import { useResizeObserver, useDebounceFn } from "@vueuse/core";
 | 
			
		||||
import { useCliWSConnection } from "@/websocket/websocket";
 | 
			
		||||
import "@xterm/xterm/css/xterm.css";
 | 
			
		||||
 | 
			
		||||
const xtermContainer = ref<HTMLElement | null>(null);
 | 
			
		||||
let term: Terminal;
 | 
			
		||||
const fit = new FitAddon();
 | 
			
		||||
 | 
			
		||||
const { data, send, close } = useCliWSConnection();
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  setupXTerm();
 | 
			
		||||
  useResizeObserver(xtermContainer, () => {
 | 
			
		||||
    resizeWindow();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onBeforeUnmount(() => {
 | 
			
		||||
  disconnect();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function setupXTerm() {
 | 
			
		||||
  term = new Terminal({
 | 
			
		||||
    convertEol: true,
 | 
			
		||||
    fontFamily: "Menlo, Monaco, Courier New, monospace",
 | 
			
		||||
    fontSize: 15,
 | 
			
		||||
    fontWeight: 400,
 | 
			
		||||
    cursorBlink: true,
 | 
			
		||||
    theme: {
 | 
			
		||||
      background: "#333",
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  term.loadAddon(fit);
 | 
			
		||||
  term.open(xtermContainer.value!);
 | 
			
		||||
  fit.fit();
 | 
			
		||||
  term.onData((data) => {
 | 
			
		||||
    send(JSON.stringify({ action: "trmmcli.input", data: { input: data } }));
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const resizeWindow = useDebounceFn(() => {
 | 
			
		||||
  fit.fit();
 | 
			
		||||
  const dims = { cols: term.cols, rows: term.rows };
 | 
			
		||||
  send(JSON.stringify({ action: "trmmcli.resize", data: dims }));
 | 
			
		||||
}, 300);
 | 
			
		||||
 | 
			
		||||
function disconnect() {
 | 
			
		||||
  term.dispose();
 | 
			
		||||
  close();
 | 
			
		||||
  send(JSON.stringify({ action: "trmmcli.disconnect" }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface WSTrmmCliOutput {
 | 
			
		||||
  output: string;
 | 
			
		||||
  messageId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(data, (newValue) => {
 | 
			
		||||
  if (newValue.action === "trmmcli.output") {
 | 
			
		||||
    const incomingData = newValue.data as WSTrmmCliOutput;
 | 
			
		||||
    term.write(incomingData.output);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
import { getBaseUrl } from "@/boot/axios";
 | 
			
		||||
 | 
			
		||||
export function getWSUrl(path, token) {
 | 
			
		||||
  const url = getBaseUrl().split("://")[1];
 | 
			
		||||
 | 
			
		||||
  const proto =
 | 
			
		||||
    process.env.NODE_ENV === "production" || process.env.DOCKER_BUILD
 | 
			
		||||
      ? "wss"
 | 
			
		||||
      : "ws";
 | 
			
		||||
  return `${proto}://${url}/ws/${path}/?access_token=${token}`;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										81
									
								
								src/websocket/websocket.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/websocket/websocket.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
import { ref, watch } from "vue";
 | 
			
		||||
import { UseWebSocketReturn, useWebSocket } from "@vueuse/core";
 | 
			
		||||
import { getBaseUrl } from "@/boot/axios";
 | 
			
		||||
import { useAuthStore } from "@/stores/auth";
 | 
			
		||||
 | 
			
		||||
export function getWSUrl(path: string, token: string | null) {
 | 
			
		||||
  const url = getBaseUrl().split("://")[1];
 | 
			
		||||
 | 
			
		||||
  const proto =
 | 
			
		||||
    process.env.NODE_ENV === "production" || process.env.DOCKER_BUILD
 | 
			
		||||
      ? "wss"
 | 
			
		||||
      : "ws";
 | 
			
		||||
  return `${proto}://${url}/ws/${path}/?access_token=${token}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface WSReturn {
 | 
			
		||||
  action: string;
 | 
			
		||||
  data: unknown;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let WSConnection: UseWebSocketReturn<string> | undefined = undefined;
 | 
			
		||||
export function useDashWSConnection() {
 | 
			
		||||
  const auth = useAuthStore();
 | 
			
		||||
 | 
			
		||||
  if (WSConnection === undefined) {
 | 
			
		||||
    const url = getWSUrl("dashinfo", auth.token);
 | 
			
		||||
    WSConnection = useWebSocket(url, {
 | 
			
		||||
      autoReconnect: true,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const { status, data, send, open, close } = WSConnection;
 | 
			
		||||
  const parsedData = ref<WSReturn>({ action: "", data: {} });
 | 
			
		||||
 | 
			
		||||
  watch(data, (newValue) => {
 | 
			
		||||
    if (newValue) parsedData.value = JSON.parse(newValue);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function closeConnection() {
 | 
			
		||||
    WSConnection = undefined;
 | 
			
		||||
    close();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    status,
 | 
			
		||||
    data: parsedData,
 | 
			
		||||
    send,
 | 
			
		||||
    open,
 | 
			
		||||
    close: closeConnection,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let WSCliConnection: UseWebSocketReturn<string> | undefined = undefined;
 | 
			
		||||
export function useCliWSConnection() {
 | 
			
		||||
  const auth = useAuthStore();
 | 
			
		||||
 | 
			
		||||
  if (WSCliConnection === undefined) {
 | 
			
		||||
    const url = getWSUrl("trmmcli", auth.token);
 | 
			
		||||
    WSCliConnection = useWebSocket(url);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const { status, data, send, open, close } = WSCliConnection;
 | 
			
		||||
  const parsedData = ref<WSReturn>({ action: "", data: {} });
 | 
			
		||||
 | 
			
		||||
  watch(data, (newValue) => {
 | 
			
		||||
    if (newValue) parsedData.value = JSON.parse(newValue);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function closeConnection() {
 | 
			
		||||
    WSCliConnection = undefined;
 | 
			
		||||
    close();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    status,
 | 
			
		||||
    data: parsedData,
 | 
			
		||||
    send,
 | 
			
		||||
    open,
 | 
			
		||||
    close: closeConnection,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user