Compare commits
	
		
			71 Commits
		
	
	
		
			v0.100.0-d
			...
			v0.101.14-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | a63a9ccd76 | ||
|  | ff1eb791db | ||
|  | 13bd88b979 | ||
|  | 5b0c244920 | ||
|  | 0318a17cac | ||
|  | 75296ed8ee | ||
|  | 09bee45b2f | ||
|  | 3573c48872 | ||
|  | 784841c221 | ||
|  | ed788a1861 | ||
|  | bd6b08505a | ||
|  | acd64f25f2 | ||
|  | 087be2c232 | ||
|  | 91a3272843 | ||
|  | 6e64f0a11b | ||
|  | 8f34f76a1d | ||
|  | d87861c212 | ||
|  | 5f56e7017b | ||
|  | 9c033c1c90 | ||
|  | ba14ed348e | ||
|  | 7e25db6622 | ||
|  | 78636c436f | ||
|  | d37122386f | ||
|  | 17d960fca9 | ||
|  | d2e0b8ad9b | ||
|  | 776c27ec26 | ||
|  | 41c61ce152 | ||
|  | 8e9de8b6b6 | ||
|  | 4cf5f7a3cb | ||
|  | 9729492d1c | ||
|  | d6da8b4a96 | ||
|  | 9264cf4044 | ||
|  | 3a45c2a309 | ||
|  | 59de35c698 | ||
|  | 5b8ac2c809 | ||
|  | 83d0ff1c0a | ||
|  | 8a6ec6ceab | ||
|  | 93dbc74e33 | ||
|  | 5f2add48a9 | ||
|  | b7369875af | ||
|  | 2eb6580fed | ||
|  | 9f85fbb330 | ||
|  | ee9715a4cf | ||
|  | 76f330fb9c | ||
|  | e67c1ff331 | ||
|  | 137a5648ce | ||
|  | a944bc50d1 | ||
|  | 0a4b00298d | ||
|  | 1eaed284a3 | ||
|  | b278e0bed4 | ||
|  | 6ee3df7e4e | ||
|  | 7ee87da3b6 | ||
|  | 7bce958633 | ||
|  | 57963f6d1a | ||
|  | c9d76bdddc | ||
|  | c279a44679 | ||
|  | 974ba53926 | ||
|  | 021fbbe14f | ||
|  | bbd74c34b7 | ||
|  | dfef0a5b4b | ||
|  | ee687bf559 | ||
|  | 627d0e91f1 | ||
|  | bffaba1f60 | ||
|  | fdf28539cb | ||
|  | ac1246c81c | ||
|  | 4feed0c65c | ||
|  | 197f2f237b | ||
|  | 0dc0d010bd | ||
|  | b17aff8c6f | ||
|  | 63147ce116 | ||
|  | ba9f93962a | 
							
								
								
									
										7
									
								
								.devcontainer/.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.devcontainer/.env.example
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| COMPOSE_PROJECT_NAME=trmm | ||||
| IMAGE_REPO=tacticalrmm/ | ||||
| VERSION=latest | ||||
|  | ||||
| # DEV SETTINGS | ||||
| APP_PORT=443 | ||||
| DOCKER_NETWORK=172.21.0.0/24 | ||||
							
								
								
									
										26
									
								
								.devcontainer/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								.devcontainer/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| version: '3.4' | ||||
|  | ||||
| services: | ||||
|   app-dev: | ||||
|     container_name: trmm-app-dev | ||||
|     image: node:16-alpine | ||||
|     restart: always | ||||
|     command: /bin/sh -c "npm install --cache ~/.npm && npm run serve" | ||||
|     user: 1000:1000 | ||||
|     working_dir: /workspace/web | ||||
|     volumes: | ||||
|       - ..:/workspace:cached | ||||
|     ports: | ||||
|       - "8080:443" | ||||
|     networks: | ||||
|       dev: | ||||
|         aliases: | ||||
|           - tactical-frontend | ||||
|  | ||||
| networks: | ||||
|   dev: | ||||
|     driver: bridge | ||||
|     ipam: | ||||
|       driver: default | ||||
|       config: | ||||
|         - subnet: ${DOCKER_NETWORK} | ||||
| @@ -2,4 +2,5 @@ PROD_URL = "https://api.example.com" | ||||
| DEV_URL = "https://api.example.com" | ||||
| APP_URL = "https://app.example.com" | ||||
| DEV_HOST = 0.0.0.0 | ||||
| DEV_PORT = 80 | ||||
| DEV_PORT = 80 | ||||
| USE_HTTPS = false | ||||
|   | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -33,3 +33,4 @@ yarn-error.log* | ||||
| *.sln | ||||
|  | ||||
| .env | ||||
| /public/env-config.js | ||||
|   | ||||
							
								
								
									
										2
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							| @@ -5,7 +5,7 @@ | ||||
|     "esbenp.prettier-vscode", | ||||
|     "editorconfig.editorconfig", | ||||
|     "vue.volar", | ||||
|     "wayou.vscode-todo-highlight", | ||||
|     "wayou.vscode-todo-highlight" | ||||
|   ], | ||||
|   "unwantedRecommendations": [ | ||||
|     "octref.vetur", | ||||
|   | ||||
							
								
								
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|   "editor.formatOnSave": true, | ||||
|   "[vue][javascript][typescript][javascriptreact]": { | ||||
|     "editor.defaultFormatter": "esbenp.prettier-vscode", | ||||
|     "editor.codeActionsOnSave": ["source.fixAll.eslint"], | ||||
|     "editor.codeActionsOnSave": ["source.fixAll.eslint"] | ||||
|   }, | ||||
|   "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"], | ||||
|   "typescript.tsdk": "node_modules/typescript/lib", | ||||
| @@ -15,7 +15,7 @@ | ||||
|       "**/node_modules/": true, | ||||
|       "/node_modules/**": true, | ||||
|       "**/env/": true, | ||||
|       "/env/**": true, | ||||
|       "/env/**": true | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										36
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								index.html
									
									
									
									
									
								
							| @@ -1,24 +1,22 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
|   <head> | ||||
|     <title><%= productName %></title> | ||||
|  | ||||
| <head> | ||||
|   <title> | ||||
|     <%= productName %> | ||||
|   </title> | ||||
|  | ||||
|   <meta charset="utf-8" /> | ||||
|   <meta name="robots" content="noindex" /> | ||||
|   <meta name="description" content="<%= productDescription %>" /> | ||||
|   <meta name="format-detection" content="telephone=no" /> | ||||
|   <meta name="msapplication-tap-highlight" content="no" /> | ||||
|   <meta name="viewport" | ||||
|     content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>" /> | ||||
|   <link rel="icon" type="image/ico" href="favicon.ico" /> | ||||
|   <script src="/env-config.js"></script> | ||||
| </head> | ||||
|  | ||||
| <body> | ||||
|   <!-- quasar:entry-point --> | ||||
| </body> | ||||
|     <meta charset="utf-8" /> | ||||
|     <meta name="robots" content="noindex" /> | ||||
|     <meta name="description" content="<%= productDescription %>" /> | ||||
|     <meta name="format-detection" content="telephone=no" /> | ||||
|     <meta name="msapplication-tap-highlight" content="no" /> | ||||
|     <meta | ||||
|       name="viewport" | ||||
|       content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>" | ||||
|     /> | ||||
|     <link rel="icon" type="image/ico" href="favicon.ico" /> | ||||
|     <script src="/env-config.js"></script> | ||||
|   </head> | ||||
|  | ||||
|   <body> | ||||
|     <!-- quasar:entry-point --> | ||||
|   </body> | ||||
| </html> | ||||
|   | ||||
							
								
								
									
										7407
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7407
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										62
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										62
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "web", | ||||
|   "version": "0.100.0-dev", | ||||
|   "version": "0.101.14-dev", | ||||
|   "private": true, | ||||
|   "productName": "Tactical RMM", | ||||
|   "scripts": { | ||||
| @@ -10,47 +10,31 @@ | ||||
|     "format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@quasar/extras": "1.14.0", | ||||
|     "apexcharts": "3.35.2", | ||||
|     "axios": "0.27.2", | ||||
|     "dotenv": "16.0.0", | ||||
|     "qrcode.vue": "3.3.3", | ||||
|     "quasar": "2.7.1", | ||||
|     "vue": "3.2.31", | ||||
|     "@quasar/extras": "1.15.11", | ||||
|     "apexcharts": "3.37.1", | ||||
|     "axios": "1.3.4", | ||||
|     "dotenv": "16.0.3", | ||||
|     "qrcode.vue": "3.3.4", | ||||
|     "quasar": "2.11.8", | ||||
|     "vue": "3.2.47", | ||||
|     "vue3-ace-editor": "2.2.2", | ||||
|     "vue3-apexcharts": "1.4.1", | ||||
|     "vuedraggable": "4.1.0", | ||||
|     "vue-router": "4.0.15", | ||||
|     "vuex": "4.0.2" | ||||
|     "vue-router": "4.1.6", | ||||
|     "vuex": "4.1.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@quasar/cli": "^1.3.2", | ||||
|     "@intlify/vite-plugin-vue-i18n": "^3.3.1", | ||||
|     "@quasar/app-vite": "^1.0.1", | ||||
|     "@types/node": "^12.20.21", | ||||
|     "@typescript-eslint/eslint-plugin": "^5.10.0", | ||||
|     "@typescript-eslint/parser": "^5.10.0", | ||||
|     "autoprefixer": "^10.4.2", | ||||
|     "eslint": "^8.10.0", | ||||
|     "eslint-config-prettier": "^8.1.0", | ||||
|     "eslint-plugin-vue": "^8.5.0", | ||||
|     "prettier": "^2.5.1", | ||||
|     "typescript": "^4.6.4" | ||||
|   }, | ||||
|   "browserslist": [ | ||||
|     "last 3 Chrome versions", | ||||
|     "last 3 Firefox versions", | ||||
|     "last 3 Edge versions", | ||||
|     "last 2 Safari versions", | ||||
|     "last 3 Android versions", | ||||
|     "last 3 ChromeAndroid versions", | ||||
|     "last 3 FirefoxAndroid versions", | ||||
|     "last 2 iOS versions", | ||||
|     "last 3 Opera versions" | ||||
|   ], | ||||
|   "engines": { | ||||
|     "node": ">= 12.22.1", | ||||
|     "npm": ">= 6.13.4", | ||||
|     "yarn": ">= 1.21.1" | ||||
|     "@quasar/cli": "^2.0.0", | ||||
|     "@intlify/unplugin-vue-i18n": "^0.9.2", | ||||
|     "@quasar/app-vite": "^1.2.1", | ||||
|     "@types/node": "^18.15.3", | ||||
|     "@typescript-eslint/eslint-plugin": "^5.55.0", | ||||
|     "@typescript-eslint/parser": "^5.55.0", | ||||
|     "autoprefixer": "10.4.14", | ||||
|     "eslint": "8.36.0", | ||||
|     "eslint-config-prettier": "8.7.0", | ||||
|     "eslint-plugin-vue": "8.7.1", | ||||
|     "prettier": "2.8.4", | ||||
|     "typescript": "4.9.5" | ||||
|   } | ||||
| } | ||||
| } | ||||
| @@ -4,18 +4,18 @@ | ||||
| module.exports = { | ||||
|   plugins: [ | ||||
|     // https://github.com/postcss/autoprefixer | ||||
|     require('autoprefixer')({ | ||||
|     require("autoprefixer")({ | ||||
|       overrideBrowserslist: [ | ||||
|         'last 4 Chrome versions', | ||||
|         'last 4 Firefox versions', | ||||
|         'last 4 Edge versions', | ||||
|         'last 4 Safari versions', | ||||
|         'last 4 Android versions', | ||||
|         'last 4 ChromeAndroid versions', | ||||
|         'last 4 FirefoxAndroid versions', | ||||
|         'last 4 iOS versions' | ||||
|       ] | ||||
|     }) | ||||
|         "last 4 Chrome versions", | ||||
|         "last 4 Firefox versions", | ||||
|         "last 4 Edge versions", | ||||
|         "last 4 Safari versions", | ||||
|         "last 4 Android versions", | ||||
|         "last 4 ChromeAndroid versions", | ||||
|         "last 4 FirefoxAndroid versions", | ||||
|         "last 4 iOS versions", | ||||
|       ], | ||||
|     }), | ||||
|  | ||||
|     // https://github.com/elchininet/postcss-rtlcss | ||||
|     // If you want to support RTL css, then | ||||
| @@ -23,5 +23,5 @@ module.exports = { | ||||
|     // 2. optionally set quasar.config.js > framework > lang to an RTL language | ||||
|     // 3. uncomment the following line: | ||||
|     // require('postcss-rtlcss') | ||||
|   ] | ||||
| } | ||||
|   ], | ||||
| }; | ||||
|   | ||||
| @@ -51,7 +51,7 @@ module.exports = configure(function (/* ctx */) { | ||||
|     // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build | ||||
|     build: { | ||||
|       target: { | ||||
|         browser: ["es2019", "edge88", "firefox78", "chrome87", "safari13.1"], | ||||
|         browser: ["es2021"], | ||||
|         node: "node16", | ||||
|       }, | ||||
|  | ||||
| @@ -86,7 +86,7 @@ module.exports = configure(function (/* ctx */) { | ||||
|  | ||||
|     // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer | ||||
|     devServer: { | ||||
|       https: false, | ||||
|       https: process.env.USE_HTTPS === "true", | ||||
|       open: false, // opens browser window automatically | ||||
|       host: process.env.DEV_HOST, | ||||
|       port: process.env.DEV_PORT, | ||||
|   | ||||
| @@ -12,6 +12,25 @@ export async function fetchUsers(params = {}) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| export async function resetPass(pass) { | ||||
|   const payload = { password: pass }; | ||||
|   try { | ||||
|     const { data } = await axios.put(`${baseUrl}/resetpw/`, payload); | ||||
|     return data; | ||||
|   } catch (e) { | ||||
|     console.error(e); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export async function resetTwoFactor() { | ||||
|   try { | ||||
|     const { data } = await axios.put(`${baseUrl}/reset2fa/`); | ||||
|     return data; | ||||
|   } catch (e) { | ||||
|     console.error(e); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // role api function | ||||
| export async function fetchRoles(params = {}) { | ||||
|   try { | ||||
|   | ||||
| @@ -31,6 +31,17 @@ export default function ({ app, router, store }) { | ||||
|       return response; | ||||
|     }, | ||||
|     async function (error) { | ||||
|       if (error.code && error.code === "ERR_NETWORK") { | ||||
|         Notify.create({ | ||||
|           color: "negative", | ||||
|           message: "Backend is offline (network error)", | ||||
|           caption: | ||||
|             "Open your browser's dev tools and check the console tab for more detailed error messages", | ||||
|           timeout: 5000, | ||||
|         }); | ||||
|         return Promise.reject({ ...error }); | ||||
|       } | ||||
|  | ||||
|       let text; | ||||
|  | ||||
|       if (!error.response) { | ||||
|   | ||||
| @@ -196,6 +196,14 @@ | ||||
|             > | ||||
|               <q-tooltip>Linux</q-tooltip> | ||||
|             </q-icon> | ||||
|             <q-icon | ||||
|               v-else-if="props.row.plat === 'darwin'" | ||||
|               name="mdi-apple" | ||||
|               size="sm" | ||||
|               color="primary" | ||||
|             > | ||||
|               <q-tooltip>macOS</q-tooltip> | ||||
|             </q-icon> | ||||
|           </q-td> | ||||
|  | ||||
|           <q-td key="checks-status" :props="props"> | ||||
| @@ -356,6 +364,27 @@ export default { | ||||
|   }, | ||||
|   methods: { | ||||
|     filterTable(rows, terms, cols, cellValue) { | ||||
|       const hiddenFields = [ | ||||
|         "version", | ||||
|         "operating_system", | ||||
|         "public_ip", | ||||
|         "cpu_model", | ||||
|         "graphics", | ||||
|         "local_ips", | ||||
|         "make_model", | ||||
|         "physical_disks", | ||||
|       ]; | ||||
|  | ||||
|       // quasar filter only does visible columns so this is a hack to add hidden columns we want to filter | ||||
|       for (const elem of hiddenFields) { | ||||
|         if (!cols.find((o) => o.name === elem)) { | ||||
|           cols.push({ | ||||
|             name: elem, | ||||
|             field: elem, | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       const lowerTerms = terms ? terms.toLowerCase() : ""; | ||||
|       let advancedFilter = false; | ||||
|       let availability = null; | ||||
|   | ||||
| @@ -142,6 +142,10 @@ | ||||
|               <q-item clickable v-close-popup @click="clearCache"> | ||||
|                 <q-item-section>Clear Cache</q-item-section> | ||||
|               </q-item> | ||||
|               <!-- bulk recover agents --> | ||||
|               <q-item clickable v-close-popup @click="bulkRecoverAgents"> | ||||
|                 <q-item-section>Recover All Agents</q-item-section> | ||||
|               </q-item> | ||||
|             </q-list> | ||||
|           </q-menu> | ||||
|         </q-btn> | ||||
| @@ -262,6 +266,20 @@ export default { | ||||
|         .get("/core/clearcache/") | ||||
|         .then((r) => this.notifySuccess(r.data)); | ||||
|     }, | ||||
|     bulkRecoverAgents() { | ||||
|       this.$q | ||||
|         .dialog({ | ||||
|           title: "Bulk Recover All Agents?", | ||||
|           message: | ||||
|             "This will restart the Tactical and Mesh Agent services on all agents", | ||||
|           cancel: true, | ||||
|         }) | ||||
|         .onOk(() => { | ||||
|           this.$axios | ||||
|             .get("/agents/bulkrecovery/") | ||||
|             .then((r) => this.notifySuccess(r.data)); | ||||
|         }); | ||||
|     }, | ||||
|     openHelp(mode) { | ||||
|       let url; | ||||
|       switch (mode) { | ||||
|   | ||||
							
								
								
									
										75
									
								
								src/components/accounts/ResetPass.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/components/accounts/ResetPass.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| <template> | ||||
|   <q-dialog ref="dialogRef" @hide="onDialogHide"> | ||||
|     <q-card class="q-dialog-plugin" style="width: 60vw"> | ||||
|       <q-card-section class="row"> | ||||
|         <div class="col-3">New password:</div> | ||||
|         <div class="col-9"> | ||||
|           <q-input | ||||
|             outlined | ||||
|             dense | ||||
|             v-model="pass" | ||||
|             :type="isPwd ? 'password' : 'text'" | ||||
|             :rules="[(val) => !!val || '*Required']" | ||||
|           > | ||||
|             <template v-slot:append> | ||||
|               <q-icon | ||||
|                 :name="isPwd ? 'visibility_off' : 'visibility'" | ||||
|                 class="cursor-pointer" | ||||
|                 @click="isPwd = !isPwd" | ||||
|               /> | ||||
|             </template> | ||||
|           </q-input> | ||||
|         </div> | ||||
|         <div class="col-3">Confirm password:</div> | ||||
|         <div class="col-9"> | ||||
|           <q-input | ||||
|             outlined | ||||
|             dense | ||||
|             v-model="pass2" | ||||
|             :type="isPwd ? 'password' : 'text'" | ||||
|             :rules="[(val) => val === pass || 'Passwords do not match']" | ||||
|           > | ||||
|             <template v-slot:append> | ||||
|               <q-icon | ||||
|                 :name="isPwd ? 'visibility_off' : 'visibility'" | ||||
|                 class="cursor-pointer" | ||||
|                 @click="isPwd = !isPwd" | ||||
|               /> | ||||
|             </template> | ||||
|           </q-input> | ||||
|         </div> | ||||
|       </q-card-section> | ||||
|       <q-card-actions align="right"> | ||||
|         <q-btn | ||||
|           color="primary" | ||||
|           label="Reset" | ||||
|           @click="onOKClick" | ||||
|           :disable="!pass || pass !== pass2" | ||||
|         /> | ||||
|         <q-btn color="negative" label="Cancel" @click="onDialogCancel" /> | ||||
|       </q-card-actions> | ||||
|     </q-card> | ||||
|   </q-dialog> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { ref } from "vue"; | ||||
| import { useDialogPluginComponent } from "quasar"; | ||||
| import { resetPass } from "@/api/accounts"; | ||||
| import { notifySuccess } from "@/utils/notify"; | ||||
|  | ||||
| const pass = ref(""); | ||||
| const pass2 = ref(""); | ||||
| const isPwd = ref(true); | ||||
|  | ||||
| defineEmits([...useDialogPluginComponent.emits]); | ||||
|  | ||||
| const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = | ||||
|   useDialogPluginComponent(); | ||||
|  | ||||
| async function onOKClick() { | ||||
|   const ret = await resetPass(pass.value); | ||||
|   notifySuccess(ret); | ||||
|   onDialogOK(); | ||||
| } | ||||
| </script> | ||||
| @@ -166,7 +166,7 @@ export default { | ||||
|           type: "textarea", | ||||
|           isValid: (val) => !!val, | ||||
|         }, | ||||
|         style: "width: 30vw; max-width: 50vw;", | ||||
|         style: "width: 90vw; max-width: 90vw", | ||||
|         ok: { label: "Add" }, | ||||
|         cancel: true, | ||||
|       }).onOk(async () => { | ||||
| @@ -193,7 +193,7 @@ export default { | ||||
|           type: "textarea", | ||||
|           isValid: (val) => !!val, | ||||
|         }, | ||||
|         style: "width: 30vw; max-width: 50vw;", | ||||
|         style: "width: 90vw; max-width: 90vw", | ||||
|         ok: { label: "Save" }, | ||||
|         cancel: true, | ||||
|       }).onOk(async (data) => { | ||||
|   | ||||
| @@ -310,9 +310,10 @@ export default { | ||||
|     } | ||||
|  | ||||
|     function showUpdateDetails(update) { | ||||
|       const color = $q.dark.isActive ? "white" : ""; | ||||
|       let support_urls = ""; | ||||
|       update.more_info_urls.forEach((u) => { | ||||
|         support_urls += `<a href='${u}' target='_blank'>${u}</a><br/>`; | ||||
|         support_urls += `<a style='color: ${color}' href='${u}' target='_blank'>${u}</a><br/>`; | ||||
|       }); | ||||
|       let cats = update.categories.join(", "); | ||||
|       $q.dialog({ | ||||
|   | ||||
| @@ -7,6 +7,17 @@ | ||||
|           <q-badge color="primary" class="q-ml-sm text-caption">{{ | ||||
|             v | ||||
|           }}</q-badge> | ||||
|           <q-btn | ||||
|             v-if="!!v" | ||||
|             size="sm" | ||||
|             class="q-ml-xs" | ||||
|             flat | ||||
|             round | ||||
|             icon="content_copy" | ||||
|             @click="copyValueToClip(v)" | ||||
|           > | ||||
|             <q-tooltip>Copy to Clipboard</q-tooltip> | ||||
|           </q-btn> | ||||
|         </div> | ||||
|       </div> | ||||
|       <q-separator v-if="info.length > 1" /> | ||||
| @@ -15,6 +26,8 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { copyToClipboard } from "quasar"; | ||||
| import { notifySuccess } from "@/utils/notify"; | ||||
| // composition imports | ||||
| import { computed } from "vue"; | ||||
| import { useStore } from "vuex"; | ||||
| @@ -28,9 +41,16 @@ export default { | ||||
|     const store = useStore(); | ||||
|     const tabHeight = computed(() => store.state.tabHeight); | ||||
|  | ||||
|     function copyValueToClip(val) { | ||||
|       copyToClipboard(val).then(() => { | ||||
|         notifySuccess("Copied to clipboard"); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|       tabHeight, | ||||
|       uid, | ||||
|       copyValueToClip, | ||||
|     }; | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -39,6 +39,19 @@ | ||||
|             new-value-mode="add" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|         <q-card-section> | ||||
|           <q-select | ||||
|             dense | ||||
|             :label="envVarsLabel" | ||||
|             filled | ||||
|             v-model="state.env_vars" | ||||
|             use-input | ||||
|             use-chips | ||||
|             multiple | ||||
|             hide-dropdown-icon | ||||
|             new-value-mode="add" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|         <q-card-section> | ||||
|           <tactical-dropdown | ||||
|             label="Informational return codes (press Enter after typing each code)" | ||||
| @@ -115,6 +128,7 @@ import { useDialogPluginComponent } from "quasar"; | ||||
| import { useCheckModal } from "@/composables/checks"; | ||||
| import { useScriptDropdown } from "@/composables/scripts"; | ||||
| import { validateRetcode } from "@/utils/validation"; | ||||
| import { envVarsLabel } from "@/constants/constants"; | ||||
|  | ||||
| // ui imports | ||||
| import TacticalDropdown from "@/components/ui/TacticalDropdown.vue"; | ||||
| @@ -132,10 +146,15 @@ export default { | ||||
|     const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent(); | ||||
|  | ||||
|     // setup script dropdown | ||||
|     const { script, scriptOptions, defaultTimeout, defaultArgs } = | ||||
|       useScriptDropdown(props.check ? props.check.script : undefined, { | ||||
|         onMount: true, | ||||
|       }); | ||||
|     const { | ||||
|       script, | ||||
|       scriptOptions, | ||||
|       defaultTimeout, | ||||
|       defaultArgs, | ||||
|       defaultEnvVars, | ||||
|     } = useScriptDropdown(props.check ? props.check.script : undefined, { | ||||
|       onMount: true, | ||||
|     }); | ||||
|  | ||||
|     // check logic | ||||
|     const { state, loading, submit, failOptions, severityOptions } = | ||||
| @@ -145,6 +164,7 @@ export default { | ||||
|           ...props.parent, | ||||
|           script, | ||||
|           script_args: defaultArgs, | ||||
|           env_vars: defaultEnvVars, | ||||
|           timeout: defaultTimeout, | ||||
|           check_type: "script", | ||||
|           fails_b4_alert: 1, | ||||
| @@ -163,6 +183,7 @@ export default { | ||||
|       failOptions, | ||||
|       scriptOptions, | ||||
|       severityOptions, | ||||
|       envVarsLabel, | ||||
|  | ||||
|       // methods | ||||
|       submit, | ||||
|   | ||||
| @@ -61,10 +61,7 @@ | ||||
|             <q-td key="client" :props="props">{{ props.row.client_name }}</q-td> | ||||
|             <q-td key="site" :props="props">{{ props.row.site_name }}</q-td> | ||||
|             <q-td key="mon_type" :props="props">{{ props.row.mon_type }}</q-td> | ||||
|             <q-td key="arch" :props="props" | ||||
|               ><span v-if="props.row.arch === '64'">64 bit</span | ||||
|               ><span v-else>32 bit</span></q-td | ||||
|             > | ||||
|             <q-td key="goarch" :props="props">{{ props.row.goarch }}</q-td> | ||||
|             <q-td key="expiry" :props="props">{{ | ||||
|               formatDate(props.row.expiry) | ||||
|             }}</q-td> | ||||
| @@ -130,7 +127,13 @@ const columns = [ | ||||
|     align: "left", | ||||
|     sortable: true, | ||||
|   }, | ||||
|   { name: "arch", label: "Arch", field: "arch", align: "left", sortable: true }, | ||||
|   { | ||||
|     name: "goarch", | ||||
|     label: "Arch", | ||||
|     field: "goarch", | ||||
|     align: "left", | ||||
|     sortable: true, | ||||
|   }, | ||||
|   { | ||||
|     name: "expiry", | ||||
|     label: "Expiry", | ||||
|   | ||||
| @@ -54,9 +54,9 @@ | ||||
|         /> | ||||
|       </q-card-section> | ||||
|       <q-card-section> | ||||
|         <div class="q-pl-sm">OS</div> | ||||
|         <q-radio v-model="state.arch" val="64" label="64 bit" /> | ||||
|         <q-radio v-model="state.arch" val="32" label="32 bit" /> | ||||
|         <div class="q-pl-sm">Arch</div> | ||||
|         <q-radio v-model="state.goarch" :val="GOARCH_AMD64" label="64 bit" /> | ||||
|         <q-radio v-model="state.goarch" :val="GOARCH_i386" label="32 bit" /> | ||||
|       </q-card-section> | ||||
|       <q-card-actions align="right"> | ||||
|         <q-btn dense flat label="Cancel" v-close-popup /> | ||||
| @@ -84,6 +84,7 @@ import { | ||||
|   formatDateInputField, | ||||
|   formatDateStringwithTimezone, | ||||
| } from "@/utils/format"; | ||||
| import { GOARCH_AMD64, GOARCH_i386 } from "@/constants/constants"; | ||||
|  | ||||
| // ui imports | ||||
| import TacticalDropdown from "@/components/ui/TacticalDropdown.vue"; | ||||
| @@ -108,7 +109,7 @@ export default { | ||||
|       power: false, | ||||
|       rdp: false, | ||||
|       ping: false, | ||||
|       arch: "64", | ||||
|       goarch: GOARCH_AMD64, | ||||
|     }); | ||||
|  | ||||
|     const loading = ref(false); | ||||
| @@ -145,6 +146,10 @@ export default { | ||||
|       // quasar dialog | ||||
|       dialogRef, | ||||
|       onDialogHide, | ||||
|  | ||||
|       // constants | ||||
|       GOARCH_AMD64, | ||||
|       GOARCH_i386, | ||||
|     }; | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -122,7 +122,7 @@ export default { | ||||
|  | ||||
|       try { | ||||
|         const result = props.APIKey | ||||
|           ? await editAPIKey(data) | ||||
|           ? await editAPIKey(data.id, data) | ||||
|           : await saveAPIKey(data); | ||||
|         onDialogOK(); | ||||
|         notifySuccess(result); | ||||
|   | ||||
| @@ -208,7 +208,7 @@ export default { | ||||
|     } | ||||
|  | ||||
|     // component lifecycle hooks | ||||
|     onMounted(getAPIKeys()); | ||||
|     onMounted(getAPIKeys); | ||||
|     return { | ||||
|       // reactive data | ||||
|       keys, | ||||
|   | ||||
| @@ -10,10 +10,13 @@ | ||||
|       </q-card-actions> | ||||
|     </q-card-section> | ||||
|     <q-card-section> | ||||
|       <p class="text-subtitle1"> | ||||
|       <p v-if="info.plat === 'windows'" class="text-subtitle1"> | ||||
|         Download the agent then run the following command from an elevated | ||||
|         command prompt on the device you want to add. | ||||
|       </p> | ||||
|       <p v-else-if="info.plat === 'darwin'" class="text-subtitle1"> | ||||
|         Run the following command from a terminal | ||||
|       </p> | ||||
|       <p> | ||||
|         <q-field outlined :color="$q.dark.isActive ? 'white' : 'black'"> | ||||
|           <code>{{ info.data.cmd }}</code> | ||||
| @@ -37,7 +40,7 @@ | ||||
|           </q-badge> | ||||
|           <span>Do not popup any message boxes during install</span> | ||||
|         </div> | ||||
|         <div class="q-pa-xs q-gutter-xs"> | ||||
|         <div v-if="info.plat === 'windows'" class="q-pa-xs q-gutter-xs"> | ||||
|           <q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> | ||||
|             <code | ||||
|               >-local-mesh "C:\\<some folder or | ||||
| @@ -46,7 +49,7 @@ | ||||
|           </q-badge> | ||||
|           <span> To skip downloading the Mesh Agent during the install.</span> | ||||
|         </div> | ||||
|         <div class="q-pa-xs q-gutter-xs"> | ||||
|         <div v-if="info.plat === 'windows'" class="q-pa-xs q-gutter-xs"> | ||||
|           <q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> | ||||
|             <code | ||||
|               >-meshdir "C:\Program Files\Your Company Name\Mesh Agent"</code | ||||
| @@ -63,7 +66,7 @@ | ||||
|           </q-badge> | ||||
|           <span>Don't install the mesh agent</span> | ||||
|         </div> | ||||
|         <div class="q-pa-xs q-gutter-xs"> | ||||
|         <div v-if="info.plat === 'windows'" class="q-pa-xs q-gutter-xs"> | ||||
|           <q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> | ||||
|             <code>-cert "C:\\<some folder or path>\\ca.pem"</code> | ||||
|           </q-badge> | ||||
| @@ -87,11 +90,12 @@ | ||||
|         Note: the auth token above will be valid for {{ info.expires }} hours. | ||||
|       </p> | ||||
|       <q-btn | ||||
|         v-if="info.plat === 'windows'" | ||||
|         type="a" | ||||
|         :href="info.data.url" | ||||
|         color="primary" | ||||
|         label="Download Agent" | ||||
|       /> | ||||
|       ></q-btn> | ||||
|     </q-card-section> | ||||
|   </q-card> | ||||
| </template> | ||||
|   | ||||
| @@ -102,6 +102,18 @@ | ||||
|             new-value-mode="add" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|         <q-card-section v-if="mode === 'script'" class="q-pt-none"> | ||||
|           <tactical-dropdown | ||||
|             v-model="state.env_vars" | ||||
|             :label="envVarsLabel" | ||||
|             filled | ||||
|             use-input | ||||
|             multiple | ||||
|             hide-dropdown-icon | ||||
|             input-debounce="0" | ||||
|             new-value-mode="add" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|  | ||||
|         <q-card-section v-if="mode === 'command'"> | ||||
|           <p>Shell</p> | ||||
| @@ -135,6 +147,11 @@ | ||||
|             :rules="[(val) => !!val || '*Required']" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|         <q-card-section v-if="supportsRunAsUser()" class="q-pt-none"> | ||||
|           <q-checkbox v-model="state.run_as_user" label="Run As User"> | ||||
|             <q-tooltip>{{ runAsUserToolTip }}</q-tooltip> | ||||
|           </q-checkbox> | ||||
|         </q-card-section> | ||||
|  | ||||
|         <q-card-section v-if="mode === 'script' || mode === 'command'"> | ||||
|           <q-input | ||||
| @@ -203,6 +220,7 @@ 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 | ||||
| import TacticalDropdown from "@/components/ui/TacticalDropdown.vue"; | ||||
| @@ -217,6 +235,7 @@ const monTypeOptions = [ | ||||
| const osTypeOptions = [ | ||||
|   { label: "Windows", value: "windows" }, | ||||
|   { label: "Linux", value: "linux" }, | ||||
|   { label: "macOS", value: "darwin" }, | ||||
|   { label: "All", value: "all" }, | ||||
| ]; | ||||
|  | ||||
| @@ -277,6 +296,7 @@ export default { | ||||
|       scriptOptions, | ||||
|       defaultTimeout, | ||||
|       defaultArgs, | ||||
|       defaultEnvVars, | ||||
|       getScriptOptions, | ||||
|     } = useScriptDropdown(); | ||||
|     const { agents, agentOptions, getAgentOptions } = useAgentDropdown(); | ||||
| @@ -300,6 +320,8 @@ export default { | ||||
|       script, | ||||
|       timeout: defaultTimeout, | ||||
|       args: defaultArgs, | ||||
|       env_vars: defaultEnvVars, | ||||
|       run_as_user: false, | ||||
|     }); | ||||
|     const loading = ref(false); | ||||
|  | ||||
| @@ -316,6 +338,7 @@ export default { | ||||
|       () => state.value.osType, | ||||
|       (newValue) => { | ||||
|         state.value.custom_shell = null; | ||||
|         state.value.run_as_user = false; | ||||
|  | ||||
|         if (newValue === "windows") { | ||||
|           state.value.shell = "cmd"; | ||||
| @@ -337,6 +360,13 @@ export default { | ||||
|       loading.value = false; | ||||
|     } | ||||
|  | ||||
|     const supportsRunAsUser = () => { | ||||
|       const modes = ["script", "command"]; | ||||
|       return ( | ||||
|         state.value.osType === "windows" && modes.includes(state.value.mode) | ||||
|       ); | ||||
|     }; | ||||
|  | ||||
|     // set modal title and caption | ||||
|     const modalTitle = computed(() => { | ||||
|       return props.mode === "command" | ||||
| @@ -387,6 +417,8 @@ export default { | ||||
|       osTypeOptions, | ||||
|       targetOptions, | ||||
|       patchModeOptions, | ||||
|       runAsUserToolTip, | ||||
|       envVarsLabel, | ||||
|  | ||||
|       //computed | ||||
|       modalTitle, | ||||
| @@ -394,6 +426,7 @@ export default { | ||||
|       //methods | ||||
|       submit, | ||||
|       cmdPlaceholder, | ||||
|       supportsRunAsUser, | ||||
|  | ||||
|       // quasar dialog plugin | ||||
|       dialogRef, | ||||
|   | ||||
| @@ -465,8 +465,51 @@ export default { | ||||
|       }); | ||||
|     }, | ||||
|     editAgent() { | ||||
|       delete this.agent.all_timezones; | ||||
|       delete this.agent.timezone; | ||||
|       // TODO we need to fix the serializer to not send this stuff | ||||
|       const toRemove = [ | ||||
|         "created_by", | ||||
|         "created_time", | ||||
|         "modified_by", | ||||
|         "modified_time", | ||||
|         "all_timezones", | ||||
|         "timezone", | ||||
|         "wmi_detail", | ||||
|         "services", | ||||
|         "status", | ||||
|         "cpu_model", | ||||
|         "local_ips", | ||||
|         "make_model", | ||||
|         "physical_disks", | ||||
|         "graphics", | ||||
|         "checks", | ||||
|         "patches_last_installed", | ||||
|         "last_seen", | ||||
|         "applied_policies", | ||||
|         "effective_patch_policy", | ||||
|         "version", | ||||
|         "operating_system", | ||||
|         "plat", | ||||
|         "goarch", | ||||
|         "hostname", | ||||
|         "public_ip", | ||||
|         "total_ram", | ||||
|         "disks", | ||||
|         "boot_time", | ||||
|         "logged_in_username", | ||||
|         "last_logged_in_user", | ||||
|         "needs_reboot", | ||||
|         "choco_installed", | ||||
|         "policy", | ||||
|         "mesh_node_id", | ||||
|         "block_policy_inheritance", | ||||
|         "maintenance_mode", | ||||
|         "alert_template", | ||||
|         "client", | ||||
|         "site_name", | ||||
|       ]; | ||||
|       for (const elem of toRemove) { | ||||
|         delete this.agent[elem]; | ||||
|       } | ||||
|  | ||||
|       // only send the timezone data if it has changed | ||||
|       // this way django will keep the db column as null and inherit from the global setting | ||||
| @@ -503,7 +546,7 @@ export default { | ||||
|         else if (day === 0) result += "Sun, "; | ||||
|       } | ||||
|  | ||||
|       return result.trimRight(","); | ||||
|       return result.trimEnd(","); | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|   | ||||
| @@ -40,7 +40,7 @@ | ||||
|               label="Windows" | ||||
|               @update:model-value=" | ||||
|                 installMethod = 'exe'; | ||||
|                 arch = '64'; | ||||
|                 goarch = GOARCH_AMD64; | ||||
|               " | ||||
|             /> | ||||
|             <q-radio | ||||
| @@ -48,8 +48,17 @@ | ||||
|               val="linux" | ||||
|               label="Linux" | ||||
|               @update:model-value=" | ||||
|                 installMethod = 'linux'; | ||||
|                 arch = 'amd64'; | ||||
|                 installMethod = 'bash'; | ||||
|                 goarch = GOARCH_AMD64; | ||||
|               " | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-model="agentOS" | ||||
|               val="darwin" | ||||
|               label="macOS" | ||||
|               @update:model-value=" | ||||
|                 installMethod = 'mac'; | ||||
|                 goarch = GOARCH_AMD64; | ||||
|               " | ||||
|             /> | ||||
|           </div> | ||||
| @@ -102,40 +111,40 @@ | ||||
|           Arch | ||||
|           <div class="q-gutter-sm"> | ||||
|             <q-radio | ||||
|               v-model="arch" | ||||
|               val="64" | ||||
|               v-model="goarch" | ||||
|               :val="GOARCH_AMD64" | ||||
|               label="64 bit" | ||||
|               v-show="agentOS === 'windows'" | ||||
|               v-show="agentOS === 'windows' || agentOS === 'linux'" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-model="arch" | ||||
|               val="32" | ||||
|               v-model="goarch" | ||||
|               :val="GOARCH_AMD64" | ||||
|               label="Intel 64 bit" | ||||
|               v-show="agentOS === 'darwin'" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-model="goarch" | ||||
|               :val="GOARCH_i386" | ||||
|               label="32 bit" | ||||
|               v-show="agentOS === 'windows'" | ||||
|               v-show="agentOS !== 'darwin'" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-model="arch" | ||||
|               val="amd64" | ||||
|               label="64 bit" | ||||
|               v-show="agentOS !== 'windows'" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-model="arch" | ||||
|               val="386" | ||||
|               label="32 bit" | ||||
|               v-show="agentOS !== 'windows'" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-model="arch" | ||||
|               val="arm64" | ||||
|               v-model="goarch" | ||||
|               :val="GOARCH_ARM64" | ||||
|               label="ARM 64 bit" | ||||
|               v-show="agentOS !== 'windows'" | ||||
|               v-show="agentOS === 'linux'" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-model="arch" | ||||
|               val="arm" | ||||
|               v-model="goarch" | ||||
|               :val="GOARCH_ARM64" | ||||
|               label="Apple Silicon (M1, M2)" | ||||
|               v-show="agentOS === 'darwin'" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-model="goarch" | ||||
|               :val="GOARCH_ARM32" | ||||
|               label="ARM 32 bit (Rasp Pi)" | ||||
|               v-show="agentOS !== 'windows'" | ||||
|               v-show="agentOS === 'linux'" | ||||
|             /> | ||||
|           </div> | ||||
|         </q-card-section> | ||||
| @@ -177,6 +186,12 @@ | ||||
| import mixins from "@/mixins/mixins"; | ||||
| import AgentDownload from "@/components/modals/agents/AgentDownload.vue"; | ||||
| import { getBaseUrl } from "@/boot/axios"; | ||||
| import { | ||||
|   GOARCH_AMD64, | ||||
|   GOARCH_i386, | ||||
|   GOARCH_ARM64, | ||||
|   GOARCH_ARM32, | ||||
| } from "@/constants/constants"; | ||||
|  | ||||
| export default { | ||||
|   name: "InstallAgent", | ||||
| @@ -187,6 +202,10 @@ export default { | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       GOARCH_AMD64: GOARCH_AMD64, | ||||
|       GOARCH_i386: GOARCH_i386, | ||||
|       GOARCH_ARM64: GOARCH_ARM64, | ||||
|       GOARCH_ARM32: GOARCH_ARM32, | ||||
|       client_options: [], | ||||
|       client: null, | ||||
|       site: null, | ||||
| @@ -198,7 +217,7 @@ export default { | ||||
|       showAgentDownload: false, | ||||
|       info: {}, | ||||
|       installMethod: "exe", | ||||
|       arch: "64", | ||||
|       goarch: GOARCH_AMD64, | ||||
|       agentOS: "windows", | ||||
|     }; | ||||
|   }, | ||||
| @@ -239,10 +258,7 @@ export default { | ||||
|         .toLowerCase() | ||||
|         .replace(/([^a-zA-Z0-9]+)/g, ""); | ||||
|  | ||||
|       const fileName = | ||||
|         this.arch === "64" | ||||
|           ? `rmm-${clientStripped}-${siteStripped}-${this.agenttype}.exe` | ||||
|           : `rmm-${clientStripped}-${siteStripped}-${this.agenttype}-x86.exe`; | ||||
|       const fileName = `trmm-${clientStripped}-${siteStripped}-${this.agenttype}-${this.goarch}.exe`; | ||||
|  | ||||
|       const data = { | ||||
|         installMethod: this.installMethod, | ||||
| @@ -253,18 +269,19 @@ export default { | ||||
|         power: this.power ? 1 : 0, | ||||
|         rdp: this.rdp ? 1 : 0, | ||||
|         ping: this.ping ? 1 : 0, | ||||
|         arch: this.arch, | ||||
|         goarch: this.goarch, | ||||
|         api, | ||||
|         fileName, | ||||
|         os: this.agentOS, | ||||
|         plat: this.agentOS, | ||||
|       }; | ||||
|  | ||||
|       if (this.installMethod === "manual") { | ||||
|       if (this.installMethod === "manual" || this.installMethod === "mac") { | ||||
|         this.$axios.post("/agents/installer/", data).then((r) => { | ||||
|           this.info = { | ||||
|             expires: this.expires, | ||||
|             data: r.data, | ||||
|             arch: this.arch, | ||||
|             goarch: this.goarch, | ||||
|             plat: this.agentOS, | ||||
|           }; | ||||
|           this.showAgentDownload = true; | ||||
|         }); | ||||
| @@ -289,7 +306,7 @@ export default { | ||||
|           }); | ||||
|       } else if ( | ||||
|         this.installMethod === "powershell" || | ||||
|         this.installMethod === "linux" | ||||
|         this.installMethod === "bash" | ||||
|       ) { | ||||
|         this.$q.loading.show(); | ||||
|         let ext = this.installMethod === "powershell" ? "ps1" : "sh"; | ||||
| @@ -333,9 +350,12 @@ export default { | ||||
|         case "manual": | ||||
|           text = "Show manual installation instructions"; | ||||
|           break; | ||||
|         case "linux": | ||||
|         case "bash": | ||||
|           text = "Download linux install script"; | ||||
|           break; | ||||
|         case "mac": | ||||
|           text = "Show installation instructions"; | ||||
|           break; | ||||
|       } | ||||
|  | ||||
|       return text; | ||||
|   | ||||
| @@ -129,37 +129,37 @@ | ||||
|       <div class="q-gutter-sm"> | ||||
|         <q-checkbox | ||||
|           v-model="winupdatepolicy.run_time_days" | ||||
|           :val="1" | ||||
|           :val="0" | ||||
|           label="Monday" | ||||
|         /> | ||||
|         <q-checkbox | ||||
|           v-model="winupdatepolicy.run_time_days" | ||||
|           :val="2" | ||||
|           :val="1" | ||||
|           label="Tuesday" | ||||
|         /> | ||||
|         <q-checkbox | ||||
|           v-model="winupdatepolicy.run_time_days" | ||||
|           :val="3" | ||||
|           :val="2" | ||||
|           label="Wednesday" | ||||
|         /> | ||||
|         <q-checkbox | ||||
|           v-model="winupdatepolicy.run_time_days" | ||||
|           :val="4" | ||||
|           :val="3" | ||||
|           label="Thursday" | ||||
|         /> | ||||
|         <q-checkbox | ||||
|           v-model="winupdatepolicy.run_time_days" | ||||
|           :val="5" | ||||
|           :val="4" | ||||
|           label="Friday" | ||||
|         /> | ||||
|         <q-checkbox | ||||
|           v-model="winupdatepolicy.run_time_days" | ||||
|           :val="6" | ||||
|           :val="5" | ||||
|           label="Saturday" | ||||
|         /> | ||||
|         <q-checkbox | ||||
|           v-model="winupdatepolicy.run_time_days" | ||||
|           :val="0" | ||||
|           :val="6" | ||||
|           label="Sunday" | ||||
|         /> | ||||
|       </div> | ||||
|   | ||||
| @@ -63,11 +63,14 @@ export default { | ||||
|       loading.value = true; | ||||
|  | ||||
|       try { | ||||
|         await scheduleAgentReboot(props.agent.agent_id, state.value); | ||||
|         const ret = await scheduleAgentReboot( | ||||
|           props.agent.agent_id, | ||||
|           state.value | ||||
|         ); | ||||
|         $q.dialog({ | ||||
|           title: "Reboot pending", | ||||
|           style: "width: 40vw", | ||||
|           message: `A reboot has been scheduled for <strong>${state.value.datetime}</strong> on ${props.agent.hostname}. | ||||
|           message: `A reboot has been scheduled for <strong>${ret.time}</strong> on ${props.agent.hostname}. | ||||
|             <br />It can be cancelled from the Pending Actions menu until the scheduled time.`, | ||||
|           html: true, | ||||
|         }).onDismiss(onDialogOK); | ||||
|   | ||||
| @@ -77,6 +77,18 @@ | ||||
|             new-value-mode="add" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|         <q-card-section> | ||||
|           <tactical-dropdown | ||||
|             v-model="state.env_vars" | ||||
|             :label="envVarsLabel" | ||||
|             filled | ||||
|             use-input | ||||
|             multiple | ||||
|             hide-dropdown-icon | ||||
|             input-debounce="0" | ||||
|             new-value-mode="add" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|         <q-card-section> | ||||
|           <q-option-group | ||||
|             v-model="state.output" | ||||
| @@ -128,6 +140,11 @@ | ||||
|           /> | ||||
|           <q-checkbox v-model="state.save_all_output" label="Save all output" /> | ||||
|         </q-card-section> | ||||
|         <q-card-section v-if="agent.plat === 'windows'"> | ||||
|           <q-checkbox v-model="state.run_as_user" label="Run As User"> | ||||
|             <q-tooltip>{{ runAsUserToolTip }}</q-tooltip> | ||||
|           </q-checkbox> | ||||
|         </q-card-section> | ||||
|         <q-card-section> | ||||
|           <q-input | ||||
|             v-model.number="state.timeout" | ||||
| @@ -173,6 +190,7 @@ 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, | ||||
| @@ -203,11 +221,18 @@ export default { | ||||
|     const { dialogRef, onDialogHide } = useDialogPluginComponent(); | ||||
|  | ||||
|     // setup dropdowns | ||||
|     const { script, scriptOptions, defaultTimeout, defaultArgs, syntax, link } = | ||||
|       useScriptDropdown(props.script, { | ||||
|         onMount: true, | ||||
|         filterByPlatform: props.agent.plat, | ||||
|       }); | ||||
|     const { | ||||
|       script, | ||||
|       scriptOptions, | ||||
|       defaultTimeout, | ||||
|       defaultArgs, | ||||
|       defaultEnvVars, | ||||
|       syntax, | ||||
|       link, | ||||
|     } = useScriptDropdown(props.script, { | ||||
|       onMount: true, | ||||
|       filterByPlatform: props.agent.plat, | ||||
|     }); | ||||
|     const { customFieldOptions } = useCustomFieldDropdown({ onMount: true }); | ||||
|  | ||||
|     // main run script functionaity | ||||
| @@ -219,7 +244,9 @@ export default { | ||||
|       save_all_output: false, | ||||
|       script, | ||||
|       args: defaultArgs, | ||||
|       env_vars: defaultEnvVars, | ||||
|       timeout: defaultTimeout, | ||||
|       run_as_user: false, | ||||
|     }); | ||||
|  | ||||
|     const ret = ref(null); | ||||
| @@ -273,6 +300,8 @@ export default { | ||||
|  | ||||
|       // non-reactive data | ||||
|       outputOptions, | ||||
|       runAsUserToolTip, | ||||
|       envVarsLabel, | ||||
|  | ||||
|       //methods | ||||
|       formatScriptSyntax, | ||||
|   | ||||
| @@ -51,6 +51,11 @@ | ||||
|             /> | ||||
|           </div> | ||||
|         </q-card-section> | ||||
|         <q-card-section v-if="agent.plat === 'windows'"> | ||||
|           <q-checkbox v-model="state.run_as_user" label="Run As User"> | ||||
|             <q-tooltip>{{ runAsUserToolTip }}</q-tooltip> | ||||
|           </q-checkbox> | ||||
|         </q-card-section> | ||||
|         <q-card-section v-if="state.shell === 'custom'"> | ||||
|           <q-input | ||||
|             v-model="state.custom_shell" | ||||
| @@ -117,6 +122,7 @@ import { ref } from "vue"; | ||||
| import { useDialogPluginComponent } from "quasar"; | ||||
| import { sendAgentCommand } from "@/api/agents"; | ||||
| import { cmdPlaceholder } from "@/composables/agents"; | ||||
| import { runAsUserToolTip } from "@/constants/constants"; | ||||
|  | ||||
| export default { | ||||
|   name: "SendCommand", | ||||
| @@ -134,6 +140,7 @@ export default { | ||||
|       cmd: null, | ||||
|       timeout: 30, | ||||
|       custom_shell: null, | ||||
|       run_as_user: false, | ||||
|     }); | ||||
|  | ||||
|     const loading = ref(false); | ||||
| @@ -156,6 +163,9 @@ export default { | ||||
|       loading, | ||||
|       ret, | ||||
|  | ||||
|       // non reactivete data | ||||
|       runAsUserToolTip, | ||||
|  | ||||
|       // methods | ||||
|       submit, | ||||
|       cmdPlaceholder, | ||||
|   | ||||
| @@ -8,11 +8,12 @@ | ||||
|       </q-btn> | ||||
|     </q-bar> | ||||
|     <q-separator /> | ||||
|     <q-banner class="bg-warning"> | ||||
|     <q-banner class="bg-info"> | ||||
|       <template v-slot:avatar> | ||||
|         <q-icon name="info" /> | ||||
|       </template> | ||||
|       Agents will now automatically self update, this tool is no longer needed. | ||||
|       Agents will automatically self update at 35 min past the hour, every hour. | ||||
|       Use this tool to manually trigger an agent update cycle. | ||||
|     </q-banner> | ||||
|     <q-card-section> | ||||
|       Select Version | ||||
|   | ||||
							
								
								
									
										211
									
								
								src/components/modals/agents/WebsocketSendCommand.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								src/components/modals/agents/WebsocketSendCommand.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,211 @@ | ||||
| <template> | ||||
|   <q-dialog | ||||
|     ref="dialogRef" | ||||
|     @hide="onDialogHide" | ||||
|     persistent | ||||
|     @keydown.esc="onDialogHide" | ||||
|   > | ||||
|     <q-card | ||||
|       class="q-dialog-plugin" | ||||
|       :style="{ 'min-width': !ret ? '40vw' : '70vw' }" | ||||
|     > | ||||
|       <q-bar> | ||||
|         Send command on {{ agent.hostname }} | ||||
|         <q-space /> | ||||
|         <q-chip v-if="!wsConnected" color="red" text-color="white" icon="error" | ||||
|           >Websocket diconnected!</q-chip | ||||
|         > | ||||
|         <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"> | ||||
|         <q-card-section> | ||||
|           <p>Shell</p> | ||||
|           <div class="q-gutter-sm"> | ||||
|             <q-radio | ||||
|               v-if="agent.plat !== 'windows'" | ||||
|               dense | ||||
|               v-model="state.shell" | ||||
|               val="/bin/bash" | ||||
|               label="Bash" | ||||
|               @update:model-value="state.custom_shell = null" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-if="agent.plat !== 'windows'" | ||||
|               dense | ||||
|               v-model="state.shell" | ||||
|               val="custom" | ||||
|               label="Custom" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-if="agent.plat === 'windows'" | ||||
|               dense | ||||
|               v-model="state.shell" | ||||
|               val="cmd" | ||||
|               label="CMD" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-if="agent.plat === 'windows'" | ||||
|               dense | ||||
|               v-model="state.shell" | ||||
|               val="powershell" | ||||
|               label="Powershell" | ||||
|             /> | ||||
|           </div> | ||||
|         </q-card-section> | ||||
|         <q-card-section v-if="state.shell === 'custom'"> | ||||
|           <q-input | ||||
|             v-model="state.custom_shell" | ||||
|             outlined | ||||
|             label="Custom shell" | ||||
|             stack-label | ||||
|             placeholder="/usr/bin/python3" | ||||
|             :rules="[(val) => !!val || '*Required']" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|         <q-card-section> | ||||
|           <q-input | ||||
|             v-model.number="state.timeout" | ||||
|             dense | ||||
|             outlined | ||||
|             type="number" | ||||
|             style="max-width: 150px" | ||||
|             label="Timeout (seconds)" | ||||
|             stack-label | ||||
|             :rules="[ | ||||
|               (val) => !!val || '*Required', | ||||
|               (val) => val >= 10 || 'Minimum is 10 seconds', | ||||
|               (val) => val <= 3600 || 'Maximum is 3600 seconds', | ||||
|             ]" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|         <q-card-section> | ||||
|           <q-input | ||||
|             v-model="state.cmd" | ||||
|             outlined | ||||
|             label="Command" | ||||
|             stack-label | ||||
|             :placeholder="cmdPlaceholder(state.shell)" | ||||
|             :rules="[(val) => !!val || '*Required']" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|         <q-card-actions align="right"> | ||||
|           <q-btn flat dense push label="Cancel" v-close-popup /> | ||||
|           <q-btn | ||||
|             :loading="loading" | ||||
|             :disable="!wsConnected" | ||||
|             flat | ||||
|             dense | ||||
|             push | ||||
|             label="Send" | ||||
|             color="primary" | ||||
|             type="submit" | ||||
|           > | ||||
|           </q-btn> | ||||
|         </q-card-actions> | ||||
|         <q-card-section | ||||
|           v-if="ret !== null" | ||||
|           class="q-pl-md q-pr-md q-pt-none q-ma-none scroll" | ||||
|           style="max-height: 50vh" | ||||
|         > | ||||
|           <pre>{{ ret }}</pre> | ||||
|         </q-card-section> | ||||
|       </q-form> | ||||
|     </q-card> | ||||
|   </q-dialog> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| // composition imports | ||||
| import { ref, computed, onMounted, onBeforeUnmount } from "vue"; | ||||
| import { useStore } from "vuex"; | ||||
| import { useDialogPluginComponent } from "quasar"; | ||||
| import { cmdPlaceholder } from "@/composables/agents"; | ||||
| import { getWSUrl } from "@/websocket/channels"; | ||||
|  | ||||
| export default { | ||||
|   name: "SendCommand", | ||||
|   emits: [...useDialogPluginComponent.emits], | ||||
|   props: { | ||||
|     agent: !Object, | ||||
|   }, | ||||
|   setup(props) { | ||||
|     const store = useStore(); | ||||
|     // setup quasar dialog plugin | ||||
|     const { dialogRef, onDialogHide } = useDialogPluginComponent(); | ||||
|  | ||||
|     // run command logic | ||||
|     const state = ref({ | ||||
|       shell: props.agent.plat === "windows" ? "cmd" : "/bin/bash", | ||||
|       cmd: null, | ||||
|       timeout: 30, | ||||
|       custom_shell: null, | ||||
|       agent_id: props.agent.agent_id, | ||||
|     }); | ||||
|  | ||||
|     const loading = ref(false); | ||||
|     const ret = ref(null); | ||||
|  | ||||
|     // websocket | ||||
|     const ws = ref(null); | ||||
|     const wsConnected = ref(false); | ||||
|  | ||||
|     function setupWS() { | ||||
|       const token = computed(() => store.state.token); | ||||
|       console.log("Starting send command websocket"); | ||||
|       let url = getWSUrl("sendcmd", token.value); | ||||
|       ws.value = new WebSocket(url); | ||||
|       ws.value.onopen = () => { | ||||
|         wsConnected.value = true; | ||||
|         console.log("Send command websocket connected"); | ||||
|       }; | ||||
|       ws.value.onmessage = (e) => { | ||||
|         const data = JSON.parse(e.data); | ||||
|         ret.value = data.ret; | ||||
|         loading.value = false; | ||||
|       }; | ||||
|       ws.value.onclose = () => { | ||||
|         console.log("Send command websocket disconnected"); | ||||
|         wsConnected.value = false; | ||||
|       }; | ||||
|       ws.value.onerror = () => { | ||||
|         wsConnected.value = false; | ||||
|         console.log("Send command websocket error"); | ||||
|         ws.value.onclose(); | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     function submit() { | ||||
|       ret.value = null; | ||||
|       loading.value = true; | ||||
|       ret.value = ws.value.send(JSON.stringify(state.value)); | ||||
|     } | ||||
|  | ||||
|     onMounted(() => { | ||||
|       setupWS(); | ||||
|     }); | ||||
|  | ||||
|     onBeforeUnmount(() => { | ||||
|       ws.value.close(); | ||||
|     }); | ||||
|  | ||||
|     return { | ||||
|       // reactive data | ||||
|       state, | ||||
|       loading, | ||||
|       ret, | ||||
|       wsConnected, | ||||
|  | ||||
|       // methods | ||||
|       submit, | ||||
|       cmdPlaceholder, | ||||
|  | ||||
|       // quasar dialog | ||||
|       dialogRef, | ||||
|       onDialogHide, | ||||
|     }; | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| @@ -204,6 +204,20 @@ | ||||
|                 new-value-mode="add" | ||||
|               /> | ||||
|  | ||||
|               <q-select | ||||
|                 class="q-mb-sm" | ||||
|                 dense | ||||
|                 label="Failure action environment vars (press Enter after typing each key=value pair)" | ||||
|                 filled | ||||
|                 v-model="template.action_env_vars" | ||||
|                 use-input | ||||
|                 use-chips | ||||
|                 multiple | ||||
|                 hide-dropdown-icon | ||||
|                 input-debounce="0" | ||||
|                 new-value-mode="add" | ||||
|               /> | ||||
|  | ||||
|               <q-input | ||||
|                 class="q-mb-sm" | ||||
|                 label="Failure action timeout (seconds)" | ||||
| @@ -277,6 +291,20 @@ | ||||
|                 new-value-mode="add" | ||||
|               /> | ||||
|  | ||||
|               <q-select | ||||
|                 class="q-mb-sm" | ||||
|                 dense | ||||
|                 label="Resolved action environment vars (press Enter after typing each key=value pair)" | ||||
|                 filled | ||||
|                 v-model="template.resolved_action_env_vars" | ||||
|                 use-input | ||||
|                 use-chips | ||||
|                 multiple | ||||
|                 hide-dropdown-icon | ||||
|                 input-debounce="0" | ||||
|                 new-value-mode="add" | ||||
|               /> | ||||
|  | ||||
|               <q-input | ||||
|                 class="q-mb-sm" | ||||
|                 label="Resolved action timeout (seconds)" | ||||
| @@ -696,9 +724,11 @@ export default { | ||||
|         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: "", | ||||
| @@ -762,11 +792,13 @@ export default { | ||||
|           (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() { | ||||
|   | ||||
| @@ -12,11 +12,15 @@ | ||||
|         color="positive" | ||||
|         class="full-width" | ||||
|         @click="doCodeSign" | ||||
|         :loading="loading" | ||||
|       > | ||||
|         <q-tooltip | ||||
|           >Force all existing agents to be updated to the code-signed | ||||
|           version</q-tooltip | ||||
|         > | ||||
|         <template v-slot:loading> | ||||
|           <q-spinner-facebook /> | ||||
|         </template> | ||||
|       </q-btn> | ||||
|     </q-card-section> | ||||
|     <q-form @submit.prevent="editToken"> | ||||
| @@ -33,56 +37,92 @@ | ||||
|       </q-card-section> | ||||
|       <q-card-section class="row items-center"> | ||||
|         <q-btn label="Save" color="primary" type="submit" /> | ||||
|         <q-space /> | ||||
|         <q-btn label="Delete" color="negative" @click="confirmDelete" /> | ||||
|       </q-card-section> | ||||
|     </q-form> | ||||
|   </q-card> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import mixins from "@/mixins/mixins"; | ||||
| import { ref, onMounted } from "vue"; | ||||
| import { useQuasar } from "quasar"; | ||||
| import axios from "axios"; | ||||
| import { notifySuccess } from "@/utils/notify"; | ||||
|  | ||||
| const endpoint = "/core/codesign/"; | ||||
|  | ||||
| export default { | ||||
|   name: "CodeSign", | ||||
|   mixins: [mixins], | ||||
|   data() { | ||||
|     return { | ||||
|       settings: { | ||||
|         token: "", | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     getToken() { | ||||
|       this.$axios.get("/core/codesign/").then((r) => { | ||||
|         this.settings = r.data; | ||||
|   setup() { | ||||
|     const $q = useQuasar(); | ||||
|     const settings = ref({ token: "" }); | ||||
|     const loading = ref(false); | ||||
|  | ||||
|     async function getToken() { | ||||
|       try { | ||||
|         const { data } = await axios.get(endpoint); | ||||
|         settings.value = data; | ||||
|       } catch (e) { | ||||
|         console.error(e); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     async function deleteToken() { | ||||
|       try { | ||||
|         await axios.delete(endpoint); | ||||
|         notifySuccess("Token was deleted!"); | ||||
|         await getToken(); | ||||
|       } catch (e) { | ||||
|         console.error(e); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     function confirmDelete() { | ||||
|       $q.dialog({ | ||||
|         title: "Delete token?", | ||||
|         cancel: true, | ||||
|         persistent: true, | ||||
|       }).onOk(() => { | ||||
|         deleteToken(); | ||||
|       }); | ||||
|     }, | ||||
|     editToken() { | ||||
|       this.$q.loading.show(); | ||||
|       this.$axios | ||||
|         .patch("/core/codesign/", this.settings) | ||||
|         .then((r) => { | ||||
|           this.$q.loading.hide(); | ||||
|           this.notifySuccess(r.data); | ||||
|         }) | ||||
|         .catch(() => { | ||||
|           this.$q.loading.hide(); | ||||
|         }); | ||||
|     }, | ||||
|     doCodeSign() { | ||||
|       this.$q.loading.show(); | ||||
|       this.$axios | ||||
|         .post("/core/codesign/") | ||||
|         .then((r) => { | ||||
|           this.$q.loading.hide(); | ||||
|           this.notifySuccess(r.data); | ||||
|         }) | ||||
|         .catch(() => { | ||||
|           this.$q.loading.hide(); | ||||
|         }); | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.getToken(); | ||||
|     } | ||||
|  | ||||
|     async function doCodeSign() { | ||||
|       loading.value = true; | ||||
|       try { | ||||
|         const { data } = await axios.post(endpoint); | ||||
|         loading.value = false; | ||||
|         notifySuccess(data); | ||||
|       } catch (e) { | ||||
|         loading.value = false; | ||||
|         console.error(e); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     async function editToken() { | ||||
|       $q.loading.show(); | ||||
|       try { | ||||
|         const { data } = await axios.patch(endpoint, settings.value); | ||||
|         $q.loading.hide(); | ||||
|         notifySuccess(data); | ||||
|       } catch (e) { | ||||
|         $q.loading.hide(); | ||||
|         console.error(e); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     onMounted(() => { | ||||
|       getToken(); | ||||
|     }); | ||||
|  | ||||
|     return { | ||||
|       settings, | ||||
|       loading, | ||||
|       confirmDelete, | ||||
|       doCodeSign, | ||||
|       editToken, | ||||
|     }; | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|   | ||||
| @@ -118,6 +118,17 @@ | ||||
|               new-value-mode="add" | ||||
|               :readonly="readonly" | ||||
|             /> | ||||
|             <tactical-dropdown | ||||
|               v-model="formScript.env_vars" | ||||
|               :label="envVarsLabel" | ||||
|               filled | ||||
|               use-input | ||||
|               multiple | ||||
|               hide-dropdown-icon | ||||
|               input-debounce="0" | ||||
|               new-value-mode="add" | ||||
|               :readonly="readonly" | ||||
|             /> | ||||
|             <q-input | ||||
|               type="number" | ||||
|               filled | ||||
| @@ -128,6 +139,18 @@ | ||||
|               :rules="[(val) => val >= 5 || 'Minimum is 5']" | ||||
|               hide-bottom-space | ||||
|             /> | ||||
|             <q-checkbox | ||||
|               v-model="formScript.run_as_user" | ||||
|               label="Run As User (Windows only)" | ||||
|             > | ||||
|               <q-tooltip | ||||
|                 >Setting this value on the script model will always override any | ||||
|                 'Run As User' checkboxes in the UI and force this script to | ||||
|                 always be run in the context of the logged in user. If no user | ||||
|                 is logged in, the script will not run and an error will be | ||||
|                 returned. | ||||
|               </q-tooltip> | ||||
|             </q-checkbox> | ||||
|             <q-input | ||||
|               label="Syntax" | ||||
|               type="textarea" | ||||
| @@ -217,6 +240,7 @@ import "ace-builds/src-noconflict/theme-tomorrow"; | ||||
|  | ||||
| // static data | ||||
| import { shellOptions } from "@/composables/scripts"; | ||||
| import { envVarsLabel } from "@/constants/constants"; | ||||
|  | ||||
| export default { | ||||
|   name: "ScriptFormModal", | ||||
| @@ -253,6 +277,8 @@ export default { | ||||
|           default_timeout: 90, | ||||
|           args: [], | ||||
|           script_body: "", | ||||
|           run_as_user: false, | ||||
|           env_vars: [], | ||||
|         }); | ||||
|  | ||||
|     if (props.clone) script.value.name = `(Copy) ${script.value.name}`; | ||||
| @@ -350,6 +376,7 @@ export default { | ||||
|       // non-reactive data | ||||
|       shellOptions, | ||||
|       agentPlatformOptions, | ||||
|       envVarsLabel, | ||||
|  | ||||
|       //computed | ||||
|       title, | ||||
|   | ||||
| @@ -867,7 +867,7 @@ export default { | ||||
|     } | ||||
|  | ||||
|     // component life cycle hooks | ||||
|     onMounted(getScripts()); | ||||
|     onMounted(getScripts); | ||||
|  | ||||
|     return { | ||||
|       // reactive data | ||||
|   | ||||
| @@ -93,6 +93,20 @@ | ||||
|           /> | ||||
|         </q-card-section> | ||||
|  | ||||
|         <q-card-section> | ||||
|           <tactical-dropdown | ||||
|             v-model="script.env_vars" | ||||
|             label="Environment Variables" | ||||
|             placeholder="(press Enter after typing each key=value pair)" | ||||
|             filled | ||||
|             use-input | ||||
|             multiple | ||||
|             hide-dropdown-icon | ||||
|             input-debounce="0" | ||||
|             new-value-mode="add" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|  | ||||
|         <q-card-section> | ||||
|           <q-input | ||||
|             label="Default Timeout" | ||||
|   | ||||
| @@ -44,6 +44,8 @@ export default { | ||||
|         timeout: props.script.default_timeout, | ||||
|         args: props.script.args, | ||||
|         shell: props.script.shell, | ||||
|         run_as_user: props.script.run_as_user, | ||||
|         env_vars: props.script.env_vars, | ||||
|       }; | ||||
|       try { | ||||
|         ret.value = await testScript(props.agent, data); | ||||
|   | ||||
| @@ -102,7 +102,7 @@ | ||||
|  | ||||
|               <tactical-dropdown | ||||
|                 v-if="actionType === 'script'" | ||||
|                 class="col-4" | ||||
|                 class="col-3" | ||||
|                 label="Select script" | ||||
|                 v-model="script" | ||||
|                 :options="scriptOptions" | ||||
| @@ -113,7 +113,7 @@ | ||||
|  | ||||
|               <q-select | ||||
|                 v-if="actionType === 'script'" | ||||
|                 class="col-5" | ||||
|                 class="col-3" | ||||
|                 dense | ||||
|                 label="Script Arguments (press Enter after typing each argument)" | ||||
|                 filled | ||||
| @@ -126,6 +126,21 @@ | ||||
|                 new-value-mode="add" | ||||
|               /> | ||||
|  | ||||
|               <q-select | ||||
|                 v-if="actionType === 'script'" | ||||
|                 class="col-3" | ||||
|                 dense | ||||
|                 :label="envVarsLabel" | ||||
|                 filled | ||||
|                 v-model="defaultEnvVars" | ||||
|                 use-input | ||||
|                 use-chips | ||||
|                 multiple | ||||
|                 hide-dropdown-icon | ||||
|                 input-debounce="0" | ||||
|                 new-value-mode="add" | ||||
|               /> | ||||
|  | ||||
|               <q-input | ||||
|                 v-if="actionType === 'script'" | ||||
|                 class="col-2" | ||||
| @@ -210,6 +225,9 @@ | ||||
|                     <q-item-label caption> | ||||
|                       Arguments: {{ element.script_args }} | ||||
|                     </q-item-label> | ||||
|                     <q-item-label caption> | ||||
|                       Env Vars: {{ element.env_vars }} | ||||
|                     </q-item-label> | ||||
|                     <q-item-label caption> | ||||
|                       Timeout: {{ element.timeout }} | ||||
|                     </q-item-label> | ||||
| @@ -727,6 +745,7 @@ import { useCheckDropdown } from "@/composables/checks"; | ||||
| import { useCustomFieldDropdown } from "@/composables/core"; | ||||
| import { notifySuccess, notifyError } from "@/utils/notify"; | ||||
| import { validateTimePeriod } from "@/utils/validation"; | ||||
| import { envVarsLabel } from "@/constants/constants"; | ||||
| import { | ||||
|   convertPeriodToSeconds, | ||||
|   convertToBitArray, | ||||
| @@ -817,10 +836,15 @@ export default { | ||||
|     const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent(); | ||||
|  | ||||
|     // setup dropdowns | ||||
|     const { script, scriptOptions, defaultTimeout, defaultArgs } = | ||||
|       useScriptDropdown(undefined, { | ||||
|         onMount: true, | ||||
|       }); | ||||
|     const { | ||||
|       script, | ||||
|       scriptOptions, | ||||
|       defaultTimeout, | ||||
|       defaultArgs, | ||||
|       defaultEnvVars, | ||||
|     } = useScriptDropdown(undefined, { | ||||
|       onMount: true, | ||||
|     }); | ||||
|  | ||||
|     // set defaultTimeout to 30 | ||||
|     defaultTimeout.value = 30; | ||||
| @@ -914,6 +938,7 @@ export default { | ||||
|           script: script.value, | ||||
|           timeout: defaultTimeout.value, | ||||
|           script_args: defaultArgs.value, | ||||
|           env_vars: defaultEnvVars.value, | ||||
|         }); | ||||
|       } else if (actionType.value === "cmd") { | ||||
|         task.value.actions.push({ | ||||
| @@ -927,6 +952,7 @@ export default { | ||||
|       // clear fields after add | ||||
|       script.value = null; | ||||
|       defaultArgs.value = []; | ||||
|       defaultEnvVars.value = []; | ||||
|       defaultTimeout.value = 30; | ||||
|       command.value = ""; | ||||
|     } | ||||
| @@ -991,10 +1017,16 @@ export default { | ||||
|         : []; | ||||
|  | ||||
|       // remove milliseconds and Z to work with native date input | ||||
|       task.value.run_time_date = formatDateInputField(task.value.run_time_date); | ||||
|       task.value.run_time_date = formatDateInputField( | ||||
|         task.value.run_time_date, | ||||
|         true | ||||
|       ); | ||||
|  | ||||
|       if (task.value.expire_date) | ||||
|         task.value.expire_date = formatDateInputField(task.value.expire_date); | ||||
|         task.value.expire_date = formatDateInputField( | ||||
|           task.value.expire_date, | ||||
|           true | ||||
|         ); | ||||
|  | ||||
|       // set task type if monthlydow is being used | ||||
|       if (task.value.task_type === "monthlydow") { | ||||
| @@ -1083,6 +1115,7 @@ export default { | ||||
|       script, | ||||
|       defaultTimeout, | ||||
|       defaultArgs, | ||||
|       defaultEnvVars, | ||||
|       actionType, | ||||
|       command, | ||||
|       shell, | ||||
| @@ -1110,6 +1143,7 @@ export default { | ||||
|       monthOptions, | ||||
|       taskTypeOptions, | ||||
|       taskInstancePolicyOptions, | ||||
|       envVarsLabel, | ||||
|  | ||||
|       // methods | ||||
|       submit, | ||||
|   | ||||
| @@ -31,7 +31,7 @@ export function useUserDropdown(onMount = false) { | ||||
|   } | ||||
|  | ||||
|   if (onMount) { | ||||
|     onMounted(getUserOptions()); | ||||
|     onMounted(getUserOptions); | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|   | ||||
| @@ -37,4 +37,5 @@ export function cmdPlaceholder(shell) { | ||||
| export const agentPlatformOptions = [ | ||||
|   { value: "windows", label: "Windows" }, | ||||
|   { value: "linux", label: "Linux" }, | ||||
|   { value: "darwin", label: "macOS" }, | ||||
| ]; | ||||
|   | ||||
| @@ -8,6 +8,7 @@ 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(""); | ||||
| @@ -29,6 +30,7 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) { | ||||
|       ); | ||||
|       defaultTimeout.value = tmpScript.timeout; | ||||
|       defaultArgs.value = tmpScript.args; | ||||
|       defaultEnvVars.value = tmpScript.env_vars; | ||||
|       syntax.value = tmpScript.syntax; | ||||
|       link.value = | ||||
|         tmpScript.script_type === "builtin" | ||||
| @@ -49,6 +51,7 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) { | ||||
|     scriptOptions, | ||||
|     defaultTimeout, | ||||
|     defaultArgs, | ||||
|     defaultEnvVars, | ||||
|     syntax, | ||||
|     link, | ||||
|  | ||||
|   | ||||
							
								
								
									
										10
									
								
								src/constants/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/constants/constants.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| export const GOARCH_AMD64 = "amd64"; | ||||
| export const GOARCH_i386 = "386"; | ||||
| export const GOARCH_ARM64 = "arm64"; | ||||
| export const GOARCH_ARM32 = "arm"; | ||||
|  | ||||
| export const runAsUserToolTip = | ||||
|   "Run in the context of the logged in user. If no user is logged in, the script will not run and an error will be returned."; | ||||
|  | ||||
| export const envVarsLabel = | ||||
|   "Environment vars (press Enter after typing each key=value pair)"; | ||||
| @@ -14,6 +14,27 @@ | ||||
|           @click="$store.dispatch('reload')" | ||||
|         /> | ||||
|       </q-banner> | ||||
|       <q-banner | ||||
|         v-if="!hosted && tokenExpired" | ||||
|         inline-actions | ||||
|         class="bg-yellow text-black text-center" | ||||
|       > | ||||
|         <q-icon size="xl" name="warning" /> | ||||
|         <span | ||||
|           ><br />Your code signing token is no longer valid.<br /><br /> | ||||
|           If you have downgraded or cancelled your sponsorship, please delete | ||||
|           your token from the Code Signing modal and refresh to get rid of this | ||||
|           banner.<br /><br /> | ||||
|           For any issues or to renew your sponsorship please email | ||||
|           support@amidaware.com<br /><br | ||||
|         /></span> | ||||
|         <q-btn | ||||
|           color="dark" | ||||
|           icon="refresh" | ||||
|           label="Refresh" | ||||
|           @click="$store.dispatch('reload')" | ||||
|         /> | ||||
|       </q-banner> | ||||
|       <q-toolbar> | ||||
|         <q-btn | ||||
|           dense | ||||
| @@ -35,12 +56,7 @@ | ||||
|           Tactical RMM<span class="text-overline q-ml-sm" | ||||
|             >v{{ currentTRMMVersion }}</span | ||||
|           > | ||||
|           <span | ||||
|             class="text-overline q-ml-md" | ||||
|             v-if=" | ||||
|               latestTRMMVersion !== 'error' && | ||||
|               currentTRMMVersion !== latestTRMMVersion | ||||
|             " | ||||
|           <span class="text-overline q-ml-md" v-if="updateAvailable()" | ||||
|             ><q-badge color="warning" | ||||
|               ><a :href="latestReleaseURL" target="_blank" | ||||
|                 >v{{ latestTRMMVersion }} available</a | ||||
| @@ -124,6 +140,32 @@ | ||||
|                 <q-item-label>Preferences</q-item-label> | ||||
|               </q-item-section> | ||||
|             </q-item> | ||||
|             <q-item clickable> | ||||
|               <q-item-section>Account</q-item-section> | ||||
|               <q-item-section side> | ||||
|                 <q-icon name="keyboard_arrow_right" /> | ||||
|               </q-item-section> | ||||
|  | ||||
|               <q-menu anchor="top end" self="top start"> | ||||
|                 <q-list> | ||||
|                   <q-item | ||||
|                     clickable | ||||
|                     v-ripple | ||||
|                     @click="resetPassword" | ||||
|                     v-close-popup | ||||
|                   > | ||||
|                     <q-item-section> | ||||
|                       <q-item-label>Reset Password</q-item-label> | ||||
|                     </q-item-section> | ||||
|                   </q-item> | ||||
|                   <q-item clickable v-ripple @click="reset2FA" v-close-popup> | ||||
|                     <q-item-section> | ||||
|                       <q-item-label>Reset 2FA</q-item-label> | ||||
|                     </q-item-section> | ||||
|                   </q-item> | ||||
|                 </q-list> | ||||
|               </q-menu> | ||||
|             </q-item> | ||||
|             <q-item to="/expired" exact> | ||||
|               <q-item-section> | ||||
|                 <q-item-label>Logout</q-item-label> | ||||
| @@ -144,11 +186,14 @@ import { ref, computed, onMounted, onBeforeUnmount } from "vue"; | ||||
| import { useQuasar } from "quasar"; | ||||
| import { useStore } from "vuex"; | ||||
| import axios from "axios"; | ||||
| import { getBaseUrl } from "@/boot/axios"; | ||||
| import { getWSUrl } from "@/websocket/channels"; | ||||
| import { resetTwoFactor } from "@/api/accounts"; | ||||
| import { notifySuccess } from "@/utils/notify"; | ||||
|  | ||||
| // 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", | ||||
| @@ -171,6 +216,8 @@ export default { | ||||
|     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 latestReleaseURL = computed(() => { | ||||
|       return latestTRMMVersion.value | ||||
| @@ -184,8 +231,24 @@ export default { | ||||
|       }).onOk(() => store.dispatch("getDashInfo")); | ||||
|     } | ||||
|  | ||||
|     function wsUrl() { | ||||
|       return getBaseUrl().split("://")[1]; | ||||
|     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); | ||||
| @@ -200,13 +263,8 @@ export default { | ||||
|       // when ws is closed causing ws to connect with expired token | ||||
|       const token = computed(() => store.state.token); | ||||
|       console.log("Starting websocket"); | ||||
|       const proto = | ||||
|         process.env.NODE_ENV === "production" || process.env.DOCKER_BUILD | ||||
|           ? "wss" | ||||
|           : "ws"; | ||||
|       ws.value = new WebSocket( | ||||
|         `${proto}://${wsUrl()}/ws/dashinfo/?access_token=${token.value}` | ||||
|       ); | ||||
|       let url = getWSUrl("dashinfo", token.value); | ||||
|       ws.value = new WebSocket(url); | ||||
|       ws.value.onopen = () => { | ||||
|         console.log("Connected to ws"); | ||||
|       }; | ||||
| @@ -242,6 +300,11 @@ export default { | ||||
|       }, 60 * 5 * 1000); | ||||
|     } | ||||
|  | ||||
|     function updateAvailable() { | ||||
|       if (latestTRMMVersion.value === "error" || hosted.value) return false; | ||||
|       return currentTRMMVersion.value !== latestTRMMVersion.value; | ||||
|     } | ||||
|  | ||||
|     onMounted(() => { | ||||
|       setupWS(); | ||||
|       store.dispatch("getDashInfo"); | ||||
| @@ -267,9 +330,14 @@ export default { | ||||
|       user, | ||||
|       needRefresh, | ||||
|       darkMode, | ||||
|       hosted, | ||||
|       tokenExpired, | ||||
|  | ||||
|       // methods | ||||
|       showUserPreferences, | ||||
|       resetPassword, | ||||
|       reset2FA, | ||||
|       updateAvailable, | ||||
|     }; | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -193,6 +193,7 @@ export default { | ||||
|               value: script.id, | ||||
|               timeout: script.default_timeout, | ||||
|               args: script.args, | ||||
|               env_vars: script.env_vars, | ||||
|             }); | ||||
|           } else if (cat === "Unassigned" && !script.category) { | ||||
|             tmp.push({ | ||||
| @@ -200,6 +201,7 @@ export default { | ||||
|               value: script.id, | ||||
|               timeout: script.default_timeout, | ||||
|               args: script.args, | ||||
|               env_vars: script.env_vars, | ||||
|             }); | ||||
|           } | ||||
|         }); | ||||
|   | ||||
| @@ -17,6 +17,7 @@ export default function () { | ||||
|         agentPlatform: "windows", | ||||
|         agentTableLoading: false, | ||||
|         needrefresh: false, | ||||
|         tokenExpired: false, | ||||
|         refreshSummaryTab: false, | ||||
|         tableHeight: "300px", | ||||
|         tabHeight: "300px", | ||||
| @@ -83,6 +84,9 @@ export default function () { | ||||
|       SET_REFRESH_NEEDED(state, action) { | ||||
|         state.needrefresh = action; | ||||
|       }, | ||||
|       SET_TOKEN_EXPIRED(state, action) { | ||||
|         state.tokenExpired = action; | ||||
|       }, | ||||
|       SET_SPLITTER(state, val) { | ||||
|         // top toolbar is 50px. Filebar is 40px and agent filter tabs are 44px | ||||
|         state.tableHeight = `${Screen.height - 50 - 40 - 78 - val}px`; | ||||
| @@ -212,6 +216,7 @@ export default function () { | ||||
|         context.commit("SET_URL_ACTION", data.url_action); | ||||
|         context.commit("setShowCommunityScripts", data.show_community_scripts); | ||||
|         context.commit("SET_HOSTED", data.hosted); | ||||
|         context.commit("SET_TOKEN_EXPIRED", data.token_is_expired); | ||||
|  | ||||
|         if (data.date_format && data.date_format !== "") | ||||
|           context.commit("setDateFormat", data.date_format); | ||||
|   | ||||
| @@ -68,6 +68,7 @@ export function formatScriptOptions(data) { | ||||
|           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, | ||||
| @@ -80,6 +81,7 @@ export function formatScriptOptions(data) { | ||||
|           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, | ||||
| @@ -285,7 +287,7 @@ export function formatDateInputField(isoDateString, noTimezone = false) { | ||||
|   if (noTimezone) { | ||||
|     isoDateString = isoDateString.replace("Z", ""); | ||||
|   } | ||||
|   return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm:ss"); | ||||
|   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 | ||||
|   | ||||
| @@ -173,6 +173,18 @@ | ||||
|                         </q-menu> | ||||
|                       </q-item> | ||||
|  | ||||
|                       <!-- Bulk Run Checks --> | ||||
|                       <q-item | ||||
|                         clickable | ||||
|                         v-close-popup | ||||
|                         @click="runChecks(props.node)" | ||||
|                       > | ||||
|                         <q-item-section side> | ||||
|                           <q-icon name="fas fa-check-double" /> | ||||
|                         </q-item-section> | ||||
|                         <q-item-section>Run Checks</q-item-section> | ||||
|                       </q-item> | ||||
|  | ||||
|                       <q-separator></q-separator> | ||||
|  | ||||
|                       <q-item clickable v-close-popup> | ||||
| @@ -690,6 +702,17 @@ export default { | ||||
|         }) | ||||
|         .onOk(() => this.$store.dispatch("refreshDashboard")); | ||||
|     }, | ||||
|     runChecks(node) { | ||||
|       const target = node.children ? "client" : "site"; | ||||
|       this.$axios | ||||
|         .post(`/checks/${target}/${node.id}/csbulkrun/`) | ||||
|         .then((r) => { | ||||
|           this.notifySuccess(r.data); | ||||
|         }) | ||||
|         .catch((e) => { | ||||
|           console.error(e); | ||||
|         }); | ||||
|     }, | ||||
|     showToggleMaintenance(node) { | ||||
|       let data = { | ||||
|         id: node.id, | ||||
|   | ||||
							
								
								
									
										11
									
								
								src/websocket/channels.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/websocket/channels.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| 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}`; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user