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