Compare commits

...

90 Commits

Author SHA1 Message Date
wh1te909
95df8c1889 update reqs 2023-05-07 02:16:19 +00:00
sadnub
819a364207 Merge pull request #8 from sadnub/develop
open ai integration
2023-04-10 19:06:18 -04:00
sadnub
ed2b07fb0b change wording on default prompt 2023-04-10 19:04:01 -04:00
sadnub
64ed5e8740 open ai integration 2023-04-09 22:36:20 -04:00
wh1te909
cdeaa3d9c4 bump version 2023-04-09 03:28:00 +00:00
wh1te909
8c6ac164ba bump dev ver 2023-04-07 22:46:24 +00:00
wh1te909
dc68b16ff2 format and align the icon 2023-04-07 21:52:53 +00:00
Dan
a4f15fd05a Merge pull request #6 from jpros/custom-fields-in-summary
Add custom fields to summary view
2023-04-07 13:51:50 -07:00
wh1te909
176675abd8 update reqs 2023-04-07 20:50:21 +00:00
João Paulo Ros
73dc278ac4 Hide custom fields with no value 2023-04-04 20:36:53 -07:00
wh1te909
d6b443296b update reqs 2023-04-04 06:34:11 +00:00
Dan
f3c718d29c Merge pull request #7 from jpros/custom-fields-search
Add custom fields to search in the dashboard
2023-04-03 22:46:20 -07:00
João Paulo Ros
5955af08c7 Hide custom fields that are not supposed to appear in the UI. 2023-03-31 15:22:10 -07:00
João Paulo Ros
dec1ccc98a Add Search from query parameter 2023-03-30 15:40:47 -07:00
João Paulo Ros
a78780b837 Add custom fields to summary view 2023-03-30 14:38:55 -07:00
João Paulo Ros
beff8eb10e Add custom fields to search in dashboard 2023-03-30 12:37:26 -07:00
wh1te909
c2f21b70dd bump version 2023-03-22 16:59:35 +00:00
wh1te909
520145e0e3 bump dev ver 2023-03-21 18:49:28 +00:00
wh1te909
6a132187a2 fix phantom column fixes amidaware/tacticalrmm#1264 2023-03-20 14:08:58 +00:00
wh1te909
a63a9ccd76 update reqs 2023-03-20 01:28:54 +00:00
wh1te909
ff1eb791db feat: increase size of notes text box closes amidaware/tacticalrmm#1407 2023-03-10 05:31:50 +00:00
wh1te909
13bd88b979 feat: bulk run checks by client or site amidaware/tacticalrmm@7d017f9494 2023-03-10 00:22:12 +00:00
wh1te909
5b0c244920 update reqs 2023-03-10 00:20:51 +00:00
wh1te909
0318a17cac update reqs 2023-03-05 20:49:03 +00:00
wh1te909
75296ed8ee bump version 2023-01-18 20:04:26 +00:00
wh1te909
09bee45b2f update reqs and dev version 2023-01-16 23:03:30 +00:00
wh1te909
3573c48872 formatting 2023-01-16 08:38:34 +00:00
wh1te909
784841c221 syntax typo 2023-01-16 08:37:15 +00:00
wh1te909
ed788a1861 update reqs 2023-01-16 08:35:07 +00:00
wh1te909
bd6b08505a bump version 2022-12-21 18:44:21 +00:00
wh1te909
acd64f25f2 add ui for self reset amidaware/tacticalrmm#1378 2022-12-20 23:38:19 +00:00
wh1te909
087be2c232 update reqs 2022-12-20 23:35:44 +00:00
wh1te909
91a3272843 format 2022-12-20 23:35:32 +00:00
wh1te909
6e64f0a11b formatting 2022-12-20 21:02:55 +00:00
wh1te909
8f34f76a1d fix edit apikey amidaware/tacticalrmm#1369 2022-12-08 23:27:26 +00:00
wh1te909
d87861c212 bump version 2022-12-04 23:01:35 +00:00
wh1te909
5f56e7017b bump dev 2022-12-03 07:47:28 +00:00
wh1te909
9c033c1c90 feat: env vars 2022-12-01 00:44:56 +00:00
wh1te909
ba14ed348e update reqs 2022-12-01 00:37:44 +00:00
wh1te909
7e25db6622 bump version 2022-11-13 01:20:19 +00:00
wh1te909
78636c436f update reqs 2022-11-08 07:24:55 +00:00
wh1te909
d37122386f bump version 2022-10-25 22:02:23 +00:00
wh1te909
17d960fca9 update reqs 2022-10-25 06:36:07 +00:00
wh1te909
d2e0b8ad9b fix function call 2022-10-23 08:04:32 +00:00
wh1te909
776c27ec26 bump version 2022-10-19 22:33:09 +00:00
wh1te909
41c61ce152 update reqs 2022-10-19 06:50:50 +00:00
wh1te909
8e9de8b6b6 check if token expired 2022-10-19 06:47:16 +00:00
wh1te909
4cf5f7a3cb update reqs 2022-10-18 00:20:02 +00:00
wh1te909
9729492d1c make link readable in dark mode amidaware/tacticalrmm#1314 2022-10-14 02:06:15 +00:00
wh1te909
d6da8b4a96 bump version 2022-09-24 02:10:26 +00:00
wh1te909
9264cf4044 change dl command 2022-09-24 02:10:03 +00:00
wh1te909
3a45c2a309 mac agent 2022-09-23 22:58:56 +00:00
wh1te909
59de35c698 update reqs 2022-09-23 16:18:06 +00:00
wh1te909
5b8ac2c809 bump version 2022-08-23 05:04:43 +00:00
wh1te909
83d0ff1c0a bump dev ver 2022-08-22 06:08:34 +00:00
wh1te909
8a6ec6ceab add copy to clipboard for assets tab closes amidaware/tacticalrmm#1246 2022-08-17 17:42:39 +00:00
wh1te909
93dbc74e33 add more fields to search 2022-08-12 01:05:28 +00:00
wh1te909
5f2add48a9 more fuckery 2022-08-10 07:10:32 +00:00
wh1te909
b7369875af remove stuff from payload 2022-08-10 05:57:12 +00:00
wh1te909
2eb6580fed update reqs 2022-08-10 05:56:53 +00:00
wh1te909
9f85fbb330 bump version 2022-08-09 20:40:39 +00:00
sadnub
ee9715a4cf fix for web for docker dev 2022-08-05 12:07:22 -04:00
wh1te909
76f330fb9c remove seconds 2022-08-05 06:54:23 +00:00
wh1te909
e67c1ff331 bump version 2022-08-01 17:36:00 +00:00
wh1te909
137a5648ce run as user 2022-07-31 22:02:50 +00:00
wh1te909
a944bc50d1 bump version 2022-07-27 06:14:59 +00:00
wh1te909
0a4b00298d update reqs 2022-07-26 08:07:41 +00:00
wh1te909
1eaed284a3 run day off by one day fixes amidaware/tacticalrmm#1193 2022-07-18 15:39:17 +00:00
wh1te909
b278e0bed4 display the nice time from django 2022-07-18 07:02:19 +00:00
wh1te909
6ee3df7e4e fix timezone when editing task amidaware/tacticalrmm#1189 2022-07-18 05:49:42 +00:00
wh1te909
7ee87da3b6 bump version 2022-07-09 23:59:43 +00:00
wh1te909
7bce958633 don't show if hosted 2022-07-09 23:53:14 +00:00
wh1te909
57963f6d1a bump version 2022-07-07 16:33:33 +00:00
wh1te909
c9d76bdddc update reqs 2022-07-07 03:05:57 +00:00
wh1te909
c279a44679 make filename consistent with deployment exe 2022-07-03 04:50:00 +00:00
wh1te909
974ba53926 add delete token and move to comp api 2022-07-03 01:34:29 +00:00
wh1te909
021fbbe14f update reqs 2022-07-03 01:05:49 +00:00
wh1te909
bbd74c34b7 improve error message when backend is offline or dns issues 2022-06-28 00:46:41 +00:00
wh1te909
dfef0a5b4b bump dev version 2022-06-27 07:32:11 +00:00
wh1te909
ee687bf559 remove manual workflow 2022-06-27 07:30:59 +00:00
wh1te909
627d0e91f1 add manual workflow 2022-06-27 07:26:21 +00:00
wh1te909
bffaba1f60 testing sendcmd websocket 2022-06-26 06:59:59 +00:00
wh1te909
fdf28539cb update reqs 2022-06-26 06:59:29 +00:00
wh1te909
ac1246c81c update installer 2022-06-23 05:25:55 +00:00
wh1te909
4feed0c65c update quasar extras 2022-06-23 05:25:34 +00:00
wh1te909
197f2f237b start refactor amidaware/tacticalrmm@b588bab268 2022-06-20 19:43:35 +00:00
wh1te909
0dc0d010bd update reqs 2022-06-20 19:41:58 +00:00
wh1te909
b17aff8c6f add bulk recovery ui amidaware/tacticalrmm@c404ae7ac8 2022-06-03 00:37:51 +00:00
wh1te909
63147ce116 add option for https dev server 2022-05-30 07:46:30 +00:00
wh1te909
ba9f93962a update deps and browserlist 2022-05-29 18:39:26 +00:00
55 changed files with 4908 additions and 5007 deletions

View File

@@ -0,0 +1,7 @@
COMPOSE_PROJECT_NAME=trmm
IMAGE_REPO=tacticalrmm/
VERSION=latest
# DEV SETTINGS
APP_PORT=443
DOCKER_NETWORK=172.21.0.0/24

View File

@@ -0,0 +1,26 @@
version: '3.4'
services:
app-dev:
container_name: trmm-app-dev
image: node:16-alpine
restart: always
command: /bin/sh -c "npm install --cache ~/.npm && npm run serve"
user: 1000:1000
working_dir: /workspace/web
volumes:
- ..:/workspace:cached
ports:
- "8080:443"
networks:
dev:
aliases:
- tactical-frontend
networks:
dev:
driver: bridge
ipam:
driver: default
config:
- subnet: ${DOCKER_NETWORK}

View File

@@ -2,4 +2,5 @@ PROD_URL = "https://api.example.com"
DEV_URL = "https://api.example.com"
APP_URL = "https://app.example.com"
DEV_HOST = 0.0.0.0
DEV_PORT = 80
DEV_PORT = 80
USE_HTTPS = false

1
.gitignore vendored
View File

@@ -33,3 +33,4 @@ yarn-error.log*
*.sln
.env
/public/env-config.js

View File

@@ -5,7 +5,7 @@
"esbenp.prettier-vscode",
"editorconfig.editorconfig",
"vue.volar",
"wayou.vscode-todo-highlight",
"wayou.vscode-todo-highlight"
],
"unwantedRecommendations": [
"octref.vetur",

View File

@@ -4,7 +4,7 @@
"editor.formatOnSave": true,
"[vue][javascript][typescript][javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": ["source.fixAll.eslint"],
"editor.codeActionsOnSave": ["source.fixAll.eslint"]
},
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
"typescript.tsdk": "node_modules/typescript/lib",
@@ -15,7 +15,7 @@
"**/node_modules/": true,
"/node_modules/**": true,
"**/env/": true,
"/env/**": true,
"/env/**": true
}
}
}

View File

@@ -1,24 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<title><%= productName %></title>
<head>
<title>
<%= productName %>
</title>
<meta charset="utf-8" />
<meta name="robots" content="noindex" />
<meta name="description" content="<%= productDescription %>" />
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<meta name="viewport"
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>" />
<link rel="icon" type="image/ico" href="favicon.ico" />
<script src="/env-config.js"></script>
</head>
<body>
<!-- quasar:entry-point -->
</body>
<meta charset="utf-8" />
<meta name="robots" content="noindex" />
<meta name="description" content="<%= productDescription %>" />
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<meta
name="viewport"
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>"
/>
<link rel="icon" type="image/ico" href="favicon.ico" />
<script src="/env-config.js"></script>
</head>
<body>
<!-- quasar:entry-point -->
</body>
</html>

8252
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.100.0-dev",
"version": "0.101.19-dev",
"private": true,
"productName": "Tactical RMM",
"scripts": {
@@ -10,47 +10,31 @@
"format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
},
"dependencies": {
"@quasar/extras": "1.14.0",
"apexcharts": "3.35.2",
"axios": "0.27.2",
"dotenv": "16.0.0",
"qrcode.vue": "3.3.3",
"quasar": "2.7.1",
"vue": "3.2.31",
"@quasar/extras": "1.16.3",
"apexcharts": "3.40.0",
"axios": "1.4.0",
"dotenv": "16.0.3",
"qrcode.vue": "3.3.4",
"quasar": "2.12.0",
"vue": "3.2.47",
"vue3-ace-editor": "2.2.2",
"vue3-apexcharts": "1.4.1",
"vuedraggable": "4.1.0",
"vue-router": "4.0.15",
"vuex": "4.0.2"
"vue-router": "4.1.6",
"vuex": "4.1.0"
},
"devDependencies": {
"@quasar/cli": "^1.3.2",
"@intlify/vite-plugin-vue-i18n": "^3.3.1",
"@quasar/app-vite": "^1.0.1",
"@types/node": "^12.20.21",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"autoprefixer": "^10.4.2",
"eslint": "^8.10.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-vue": "^8.5.0",
"prettier": "^2.5.1",
"typescript": "^4.6.4"
},
"browserslist": [
"last 3 Chrome versions",
"last 3 Firefox versions",
"last 3 Edge versions",
"last 2 Safari versions",
"last 3 Android versions",
"last 3 ChromeAndroid versions",
"last 3 FirefoxAndroid versions",
"last 2 iOS versions",
"last 3 Opera versions"
],
"engines": {
"node": ">= 12.22.1",
"npm": ">= 6.13.4",
"yarn": ">= 1.21.1"
"@quasar/cli": "^2.1.0",
"@intlify/unplugin-vue-i18n": "^0.10.0",
"@quasar/app-vite": "^1.3.0",
"@types/node": "^18.16.5",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"autoprefixer": "10.4.14",
"eslint": "8.40.0",
"eslint-config-prettier": "8.8.0",
"eslint-plugin-vue": "8.7.1",
"prettier": "2.8.8",
"typescript": "5.0.4"
}
}
}

View File

@@ -4,18 +4,18 @@
module.exports = {
plugins: [
// https://github.com/postcss/autoprefixer
require('autoprefixer')({
require("autoprefixer")({
overrideBrowserslist: [
'last 4 Chrome versions',
'last 4 Firefox versions',
'last 4 Edge versions',
'last 4 Safari versions',
'last 4 Android versions',
'last 4 ChromeAndroid versions',
'last 4 FirefoxAndroid versions',
'last 4 iOS versions'
]
})
"last 4 Chrome versions",
"last 4 Firefox versions",
"last 4 Edge versions",
"last 4 Safari versions",
"last 4 Android versions",
"last 4 ChromeAndroid versions",
"last 4 FirefoxAndroid versions",
"last 4 iOS versions",
],
}),
// https://github.com/elchininet/postcss-rtlcss
// If you want to support RTL css, then
@@ -23,5 +23,5 @@ module.exports = {
// 2. optionally set quasar.config.js > framework > lang to an RTL language
// 3. uncomment the following line:
// require('postcss-rtlcss')
]
}
],
};

View File

@@ -51,7 +51,7 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
build: {
target: {
browser: ["es2019", "edge88", "firefox78", "chrome87", "safari13.1"],
browser: ["es2021"],
node: "node16",
},
@@ -86,7 +86,7 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: {
https: false,
https: process.env.USE_HTTPS === "true",
open: false, // opens browser window automatically
host: process.env.DEV_HOST,
port: process.env.DEV_PORT,

View File

@@ -12,6 +12,25 @@ export async function fetchUsers(params = {}) {
}
}
export async function resetPass(pass) {
const payload = { password: pass };
try {
const { data } = await axios.put(`${baseUrl}/resetpw/`, payload);
return data;
} catch (e) {
console.error(e);
}
}
export async function resetTwoFactor() {
try {
const { data } = await axios.put(`${baseUrl}/reset2fa/`);
return data;
} catch (e) {
console.error(e);
}
}
// role api function
export async function fetchRoles(params = {}) {
try {

View File

@@ -38,3 +38,8 @@ export async function runURLAction(payload) {
console.error(e);
}
}
export async function generateScript(payload) {
const { data } = await axios.post(`${baseUrl}/openai/generate/`, payload);
return data;
}

View File

@@ -31,6 +31,17 @@ export default function ({ app, router, store }) {
return response;
},
async function (error) {
if (error.code && error.code === "ERR_NETWORK") {
Notify.create({
color: "negative",
message: "Backend is offline (network error)",
caption:
"Open your browser's dev tools and check the console tab for more detailed error messages",
timeout: 5000,
});
return Promise.reject({ ...error });
}
let text;
if (!error.response) {

View File

@@ -196,6 +196,14 @@
>
<q-tooltip>Linux</q-tooltip>
</q-icon>
<q-icon
v-else-if="props.row.plat === 'darwin'"
name="mdi-apple"
size="sm"
color="primary"
>
<q-tooltip>macOS</q-tooltip>
</q-icon>
</q-td>
<q-td key="checks-status" :props="props">
@@ -356,6 +364,22 @@ export default {
},
methods: {
filterTable(rows, terms, cols, cellValue) {
const hiddenFields = [
"version",
"operating_system",
"public_ip",
"cpu_model",
"graphics",
"local_ips",
"make_model",
"physical_disks",
"custom_fields"
];
// quasar filter only does visible columns so this is a hack to add hidden columns we want to filter
// originally I was modifying cols directly but this led to phantom colum so doing it this way now
// https://github.com/amidaware/tacticalrmm/issues/1264
const allColumns = [...cols, ...hiddenFields.map((field) => ({ field }))];
const lowerTerms = terms ? terms.toLowerCase() : "";
let advancedFilter = false;
let availability = null;
@@ -408,8 +432,12 @@ export default {
}
// Normal text filter
return cols.some((col) => {
const val = cellValue(col, row) + "";
return allColumns.some((col) => {
let valObj = cellValue(col, row);
if (Array.isArray(valObj)) {
valObj = valObj.map((item) => item.value ? item.value : item);
}
const val = valObj + "";
const haystack =
val === "undefined" || val === "null" ? "" : val.toLowerCase();
return haystack.indexOf(search) !== -1;

View File

@@ -142,6 +142,10 @@
<q-item clickable v-close-popup @click="clearCache">
<q-item-section>Clear Cache</q-item-section>
</q-item>
<!-- bulk recover agents -->
<q-item clickable v-close-popup @click="bulkRecoverAgents">
<q-item-section>Recover All Agents</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
@@ -262,6 +266,20 @@ export default {
.get("/core/clearcache/")
.then((r) => this.notifySuccess(r.data));
},
bulkRecoverAgents() {
this.$q
.dialog({
title: "Bulk Recover All Agents?",
message:
"This will restart the Tactical and Mesh Agent services on all agents",
cancel: true,
})
.onOk(() => {
this.$axios
.get("/agents/bulkrecovery/")
.then((r) => this.notifySuccess(r.data));
});
},
openHelp(mode) {
let url;
switch (mode) {

View File

@@ -0,0 +1,75 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin" style="width: 60vw">
<q-card-section class="row">
<div class="col-3">New password:</div>
<div class="col-9">
<q-input
outlined
dense
v-model="pass"
:type="isPwd ? 'password' : 'text'"
:rules="[(val) => !!val || '*Required']"
>
<template v-slot:append>
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
</div>
<div class="col-3">Confirm password:</div>
<div class="col-9">
<q-input
outlined
dense
v-model="pass2"
:type="isPwd ? 'password' : 'text'"
:rules="[(val) => val === pass || 'Passwords do not match']"
>
<template v-slot:append>
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn
color="primary"
label="Reset"
@click="onOKClick"
:disable="!pass || pass !== pass2"
/>
<q-btn color="negative" label="Cancel" @click="onDialogCancel" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup>
import { ref } from "vue";
import { useDialogPluginComponent } from "quasar";
import { resetPass } from "@/api/accounts";
import { notifySuccess } from "@/utils/notify";
const pass = ref("");
const pass2 = ref("");
const isPwd = ref(true);
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
useDialogPluginComponent();
async function onOKClick() {
const ret = await resetPass(pass.value);
notifySuccess(ret);
onDialogOK();
}
</script>

View File

@@ -166,7 +166,7 @@ export default {
type: "textarea",
isValid: (val) => !!val,
},
style: "width: 30vw; max-width: 50vw;",
style: "width: 90vw; max-width: 90vw",
ok: { label: "Add" },
cancel: true,
}).onOk(async () => {
@@ -193,7 +193,7 @@ export default {
type: "textarea",
isValid: (val) => !!val,
},
style: "width: 30vw; max-width: 50vw;",
style: "width: 90vw; max-width: 90vw",
ok: { label: "Save" },
cancel: true,
}).onOk(async (data) => {

View File

@@ -158,6 +158,20 @@
>
</div>
<div v-else>No checks</div>
<span
v-if="customFields.length > 0"
class="text-subtitle2 text-bold block q-mt-xl"
>Custom Fields</span
>
<q-list dense>
<q-item v-for="(field, i) in customFields" :key="field + i">
<q-item-section thumbnail>
<q-icon name="fas fa-user" size="xs" />
</q-item-section>
<q-item-section>{{ field.name }}: {{ field.value }}</q-item-section>
</q-item>
</q-list>
</div>
<div class="col-1"></div>
<!-- right -->
@@ -193,6 +207,7 @@ import {
openAgentWindow,
} from "@/api/agents";
import { notifySuccess } from "@/utils/notify";
import { fetchCustomFields } from "@/api/core";
// ui imports
import AgentActionMenu from "@/components/agents/AgentActionMenu.vue";
@@ -210,6 +225,7 @@ export default {
// summary tab logic
const summary = ref(null);
const customFieldsDefinitions = ref(null);
const loading = ref(false);
function diskBarColor(percent) {
@@ -236,9 +252,37 @@ export default {
return ret;
});
const customFields = computed(() => {
if (!summary.value.custom_fields) {
return [];
}
if (!customFieldsDefinitions.value) {
return [];
}
const ret = [];
for (const customField of summary.value.custom_fields) {
const definition = customFieldsDefinitions.value.find(
(def) => def.id === customField.field
);
if (
definition &&
!definition.hide_in_ui &&
customField.value?.length > 0
) {
ret.push({
name: definition.name,
value: customField.value,
});
}
}
return ret;
});
async function getSummary() {
loading.value = true;
summary.value = await fetchAgent(selectedAgent.value);
customFieldsDefinitions.value = await fetchCustomFields();
store.commit("setRefreshSummaryTab", false);
store.commit("setAgentPlatform", summary.value.plat);
loading.value = false;
@@ -277,6 +321,7 @@ export default {
return {
// reactive data
summary,
customFields,
loading,
selectedAgent,
disks,

View File

@@ -310,9 +310,10 @@ export default {
}
function showUpdateDetails(update) {
const color = $q.dark.isActive ? "white" : "";
let support_urls = "";
update.more_info_urls.forEach((u) => {
support_urls += `<a href='${u}' target='_blank'>${u}</a><br/>`;
support_urls += `<a style='color: ${color}' href='${u}' target='_blank'>${u}</a><br/>`;
});
let cats = update.categories.join(", ");
$q.dialog({

View File

@@ -7,6 +7,17 @@
<q-badge color="primary" class="q-ml-sm text-caption">{{
v
}}</q-badge>
<q-btn
v-if="!!v"
size="sm"
class="q-ml-xs"
flat
round
icon="content_copy"
@click="copyValueToClip(v)"
>
<q-tooltip>Copy to Clipboard</q-tooltip>
</q-btn>
</div>
</div>
<q-separator v-if="info.length > 1" />
@@ -15,6 +26,8 @@
</template>
<script>
import { copyToClipboard } from "quasar";
import { notifySuccess } from "@/utils/notify";
// composition imports
import { computed } from "vue";
import { useStore } from "vuex";
@@ -28,9 +41,16 @@ export default {
const store = useStore();
const tabHeight = computed(() => store.state.tabHeight);
function copyValueToClip(val) {
copyToClipboard(val).then(() => {
notifySuccess("Copied to clipboard");
});
}
return {
tabHeight,
uid,
copyValueToClip,
};
},
};

View File

@@ -39,6 +39,19 @@
new-value-mode="add"
/>
</q-card-section>
<q-card-section>
<q-select
dense
:label="envVarsLabel"
filled
v-model="state.env_vars"
use-input
use-chips
multiple
hide-dropdown-icon
new-value-mode="add"
/>
</q-card-section>
<q-card-section>
<tactical-dropdown
label="Informational return codes (press Enter after typing each code)"
@@ -115,6 +128,7 @@ import { useDialogPluginComponent } from "quasar";
import { useCheckModal } from "@/composables/checks";
import { useScriptDropdown } from "@/composables/scripts";
import { validateRetcode } from "@/utils/validation";
import { envVarsLabel } from "@/constants/constants";
// ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
@@ -132,10 +146,15 @@ export default {
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// setup script dropdown
const { script, scriptOptions, defaultTimeout, defaultArgs } =
useScriptDropdown(props.check ? props.check.script : undefined, {
onMount: true,
});
const {
script,
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
} = useScriptDropdown(props.check ? props.check.script : undefined, {
onMount: true,
});
// check logic
const { state, loading, submit, failOptions, severityOptions } =
@@ -145,6 +164,7 @@ export default {
...props.parent,
script,
script_args: defaultArgs,
env_vars: defaultEnvVars,
timeout: defaultTimeout,
check_type: "script",
fails_b4_alert: 1,
@@ -163,6 +183,7 @@ export default {
failOptions,
scriptOptions,
severityOptions,
envVarsLabel,
// methods
submit,

View File

@@ -61,10 +61,7 @@
<q-td key="client" :props="props">{{ props.row.client_name }}</q-td>
<q-td key="site" :props="props">{{ props.row.site_name }}</q-td>
<q-td key="mon_type" :props="props">{{ props.row.mon_type }}</q-td>
<q-td key="arch" :props="props"
><span v-if="props.row.arch === '64'">64 bit</span
><span v-else>32 bit</span></q-td
>
<q-td key="goarch" :props="props">{{ props.row.goarch }}</q-td>
<q-td key="expiry" :props="props">{{
formatDate(props.row.expiry)
}}</q-td>
@@ -130,7 +127,13 @@ const columns = [
align: "left",
sortable: true,
},
{ name: "arch", label: "Arch", field: "arch", align: "left", sortable: true },
{
name: "goarch",
label: "Arch",
field: "goarch",
align: "left",
sortable: true,
},
{
name: "expiry",
label: "Expiry",

View File

@@ -54,9 +54,9 @@
/>
</q-card-section>
<q-card-section>
<div class="q-pl-sm">OS</div>
<q-radio v-model="state.arch" val="64" label="64 bit" />
<q-radio v-model="state.arch" val="32" label="32 bit" />
<div class="q-pl-sm">Arch</div>
<q-radio v-model="state.goarch" :val="GOARCH_AMD64" label="64 bit" />
<q-radio v-model="state.goarch" :val="GOARCH_i386" label="32 bit" />
</q-card-section>
<q-card-actions align="right">
<q-btn dense flat label="Cancel" v-close-popup />
@@ -84,6 +84,7 @@ import {
formatDateInputField,
formatDateStringwithTimezone,
} from "@/utils/format";
import { GOARCH_AMD64, GOARCH_i386 } from "@/constants/constants";
// ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
@@ -108,7 +109,7 @@ export default {
power: false,
rdp: false,
ping: false,
arch: "64",
goarch: GOARCH_AMD64,
});
const loading = ref(false);
@@ -145,6 +146,10 @@ export default {
// quasar dialog
dialogRef,
onDialogHide,
// constants
GOARCH_AMD64,
GOARCH_i386,
};
},
};

View File

@@ -122,7 +122,7 @@ export default {
try {
const result = props.APIKey
? await editAPIKey(data)
? await editAPIKey(data.id, data)
: await saveAPIKey(data);
onDialogOK();
notifySuccess(result);

View File

@@ -208,7 +208,7 @@ export default {
}
// component lifecycle hooks
onMounted(getAPIKeys());
onMounted(getAPIKeys);
return {
// reactive data
keys,

View File

@@ -10,10 +10,13 @@
</q-card-actions>
</q-card-section>
<q-card-section>
<p class="text-subtitle1">
<p v-if="info.plat === 'windows'" class="text-subtitle1">
Download the agent then run the following command from an elevated
command prompt on the device you want to add.
</p>
<p v-else-if="info.plat === 'darwin'" class="text-subtitle1">
Run the following command from a terminal
</p>
<p>
<q-field outlined :color="$q.dark.isActive ? 'white' : 'black'">
<code>{{ info.data.cmd }}</code>
@@ -37,7 +40,7 @@
</q-badge>
<span>Do not popup any message boxes during install</span>
</div>
<div class="q-pa-xs q-gutter-xs">
<div v-if="info.plat === 'windows'" class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
<code
>-local-mesh "C:\\&lt;some folder or
@@ -46,7 +49,7 @@
</q-badge>
<span> To skip downloading the Mesh Agent during the install.</span>
</div>
<div class="q-pa-xs q-gutter-xs">
<div v-if="info.plat === 'windows'" class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
<code
>-meshdir "C:\Program Files\Your Company Name\Mesh Agent"</code
@@ -63,7 +66,7 @@
</q-badge>
<span>Don't install the mesh agent</span>
</div>
<div class="q-pa-xs q-gutter-xs">
<div v-if="info.plat === 'windows'" class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
<code>-cert "C:\\&lt;some folder or path&gt;\\ca.pem"</code>
</q-badge>
@@ -87,11 +90,12 @@
Note: the auth token above will be valid for {{ info.expires }} hours.
</p>
<q-btn
v-if="info.plat === 'windows'"
type="a"
:href="info.data.url"
color="primary"
label="Download Agent"
/>
></q-btn>
</q-card-section>
</q-card>
</template>

View File

@@ -102,6 +102,18 @@
new-value-mode="add"
/>
</q-card-section>
<q-card-section v-if="mode === 'script'" class="q-pt-none">
<tactical-dropdown
v-model="state.env_vars"
:label="envVarsLabel"
filled
use-input
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section v-if="mode === 'command'">
<p>Shell</p>
@@ -135,6 +147,11 @@
:rules="[(val) => !!val || '*Required']"
/>
</q-card-section>
<q-card-section v-if="supportsRunAsUser()" class="q-pt-none">
<q-checkbox v-model="state.run_as_user" label="Run As User">
<q-tooltip>{{ runAsUserToolTip }}</q-tooltip>
</q-checkbox>
</q-card-section>
<q-card-section v-if="mode === 'script' || mode === 'command'">
<q-input
@@ -203,6 +220,7 @@ import { runBulkAction } from "@/api/agents";
import { notifySuccess } from "@/utils/notify";
import { cmdPlaceholder } from "@/composables/agents";
import { removeExtraOptionCategories } from "@/utils/format";
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
// ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
@@ -217,6 +235,7 @@ const monTypeOptions = [
const osTypeOptions = [
{ label: "Windows", value: "windows" },
{ label: "Linux", value: "linux" },
{ label: "macOS", value: "darwin" },
{ label: "All", value: "all" },
];
@@ -277,6 +296,7 @@ export default {
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
getScriptOptions,
} = useScriptDropdown();
const { agents, agentOptions, getAgentOptions } = useAgentDropdown();
@@ -300,6 +320,8 @@ export default {
script,
timeout: defaultTimeout,
args: defaultArgs,
env_vars: defaultEnvVars,
run_as_user: false,
});
const loading = ref(false);
@@ -316,6 +338,7 @@ export default {
() => state.value.osType,
(newValue) => {
state.value.custom_shell = null;
state.value.run_as_user = false;
if (newValue === "windows") {
state.value.shell = "cmd";
@@ -337,6 +360,13 @@ export default {
loading.value = false;
}
const supportsRunAsUser = () => {
const modes = ["script", "command"];
return (
state.value.osType === "windows" && modes.includes(state.value.mode)
);
};
// set modal title and caption
const modalTitle = computed(() => {
return props.mode === "command"
@@ -387,6 +417,8 @@ export default {
osTypeOptions,
targetOptions,
patchModeOptions,
runAsUserToolTip,
envVarsLabel,
//computed
modalTitle,
@@ -394,6 +426,7 @@ export default {
//methods
submit,
cmdPlaceholder,
supportsRunAsUser,
// quasar dialog plugin
dialogRef,

View File

@@ -465,8 +465,51 @@ export default {
});
},
editAgent() {
delete this.agent.all_timezones;
delete this.agent.timezone;
// TODO we need to fix the serializer to not send this stuff
const toRemove = [
"created_by",
"created_time",
"modified_by",
"modified_time",
"all_timezones",
"timezone",
"wmi_detail",
"services",
"status",
"cpu_model",
"local_ips",
"make_model",
"physical_disks",
"graphics",
"checks",
"patches_last_installed",
"last_seen",
"applied_policies",
"effective_patch_policy",
"version",
"operating_system",
"plat",
"goarch",
"hostname",
"public_ip",
"total_ram",
"disks",
"boot_time",
"logged_in_username",
"last_logged_in_user",
"needs_reboot",
"choco_installed",
"policy",
"mesh_node_id",
"block_policy_inheritance",
"maintenance_mode",
"alert_template",
"client",
"site_name",
];
for (const elem of toRemove) {
delete this.agent[elem];
}
// only send the timezone data if it has changed
// this way django will keep the db column as null and inherit from the global setting
@@ -503,7 +546,7 @@ export default {
else if (day === 0) result += "Sun, ";
}
return result.trimRight(",");
return result.trimEnd(",");
},
},
mounted() {

View File

@@ -40,7 +40,7 @@
label="Windows"
@update:model-value="
installMethod = 'exe';
arch = '64';
goarch = GOARCH_AMD64;
"
/>
<q-radio
@@ -48,8 +48,17 @@
val="linux"
label="Linux"
@update:model-value="
installMethod = 'linux';
arch = 'amd64';
installMethod = 'bash';
goarch = GOARCH_AMD64;
"
/>
<q-radio
v-model="agentOS"
val="darwin"
label="macOS"
@update:model-value="
installMethod = 'mac';
goarch = GOARCH_AMD64;
"
/>
</div>
@@ -102,40 +111,40 @@
Arch
<div class="q-gutter-sm">
<q-radio
v-model="arch"
val="64"
v-model="goarch"
:val="GOARCH_AMD64"
label="64 bit"
v-show="agentOS === 'windows'"
v-show="agentOS === 'windows' || agentOS === 'linux'"
/>
<q-radio
v-model="arch"
val="32"
v-model="goarch"
:val="GOARCH_AMD64"
label="Intel 64 bit"
v-show="agentOS === 'darwin'"
/>
<q-radio
v-model="goarch"
:val="GOARCH_i386"
label="32 bit"
v-show="agentOS === 'windows'"
v-show="agentOS !== 'darwin'"
/>
<q-radio
v-model="arch"
val="amd64"
label="64 bit"
v-show="agentOS !== 'windows'"
/>
<q-radio
v-model="arch"
val="386"
label="32 bit"
v-show="agentOS !== 'windows'"
/>
<q-radio
v-model="arch"
val="arm64"
v-model="goarch"
:val="GOARCH_ARM64"
label="ARM 64 bit"
v-show="agentOS !== 'windows'"
v-show="agentOS === 'linux'"
/>
<q-radio
v-model="arch"
val="arm"
v-model="goarch"
:val="GOARCH_ARM64"
label="Apple Silicon (M1, M2)"
v-show="agentOS === 'darwin'"
/>
<q-radio
v-model="goarch"
:val="GOARCH_ARM32"
label="ARM 32 bit (Rasp Pi)"
v-show="agentOS !== 'windows'"
v-show="agentOS === 'linux'"
/>
</div>
</q-card-section>
@@ -177,6 +186,12 @@
import mixins from "@/mixins/mixins";
import AgentDownload from "@/components/modals/agents/AgentDownload.vue";
import { getBaseUrl } from "@/boot/axios";
import {
GOARCH_AMD64,
GOARCH_i386,
GOARCH_ARM64,
GOARCH_ARM32,
} from "@/constants/constants";
export default {
name: "InstallAgent",
@@ -187,6 +202,10 @@ export default {
},
data() {
return {
GOARCH_AMD64: GOARCH_AMD64,
GOARCH_i386: GOARCH_i386,
GOARCH_ARM64: GOARCH_ARM64,
GOARCH_ARM32: GOARCH_ARM32,
client_options: [],
client: null,
site: null,
@@ -198,7 +217,7 @@ export default {
showAgentDownload: false,
info: {},
installMethod: "exe",
arch: "64",
goarch: GOARCH_AMD64,
agentOS: "windows",
};
},
@@ -239,10 +258,7 @@ export default {
.toLowerCase()
.replace(/([^a-zA-Z0-9]+)/g, "");
const fileName =
this.arch === "64"
? `rmm-${clientStripped}-${siteStripped}-${this.agenttype}.exe`
: `rmm-${clientStripped}-${siteStripped}-${this.agenttype}-x86.exe`;
const fileName = `trmm-${clientStripped}-${siteStripped}-${this.agenttype}-${this.goarch}.exe`;
const data = {
installMethod: this.installMethod,
@@ -253,18 +269,19 @@ export default {
power: this.power ? 1 : 0,
rdp: this.rdp ? 1 : 0,
ping: this.ping ? 1 : 0,
arch: this.arch,
goarch: this.goarch,
api,
fileName,
os: this.agentOS,
plat: this.agentOS,
};
if (this.installMethod === "manual") {
if (this.installMethod === "manual" || this.installMethod === "mac") {
this.$axios.post("/agents/installer/", data).then((r) => {
this.info = {
expires: this.expires,
data: r.data,
arch: this.arch,
goarch: this.goarch,
plat: this.agentOS,
};
this.showAgentDownload = true;
});
@@ -289,7 +306,7 @@ export default {
});
} else if (
this.installMethod === "powershell" ||
this.installMethod === "linux"
this.installMethod === "bash"
) {
this.$q.loading.show();
let ext = this.installMethod === "powershell" ? "ps1" : "sh";
@@ -333,9 +350,12 @@ export default {
case "manual":
text = "Show manual installation instructions";
break;
case "linux":
case "bash":
text = "Download linux install script";
break;
case "mac":
text = "Show installation instructions";
break;
}
return text;

View File

@@ -129,37 +129,37 @@
<div class="q-gutter-sm">
<q-checkbox
v-model="winupdatepolicy.run_time_days"
:val="1"
:val="0"
label="Monday"
/>
<q-checkbox
v-model="winupdatepolicy.run_time_days"
:val="2"
:val="1"
label="Tuesday"
/>
<q-checkbox
v-model="winupdatepolicy.run_time_days"
:val="3"
:val="2"
label="Wednesday"
/>
<q-checkbox
v-model="winupdatepolicy.run_time_days"
:val="4"
:val="3"
label="Thursday"
/>
<q-checkbox
v-model="winupdatepolicy.run_time_days"
:val="5"
:val="4"
label="Friday"
/>
<q-checkbox
v-model="winupdatepolicy.run_time_days"
:val="6"
:val="5"
label="Saturday"
/>
<q-checkbox
v-model="winupdatepolicy.run_time_days"
:val="0"
:val="6"
label="Sunday"
/>
</div>

View File

@@ -63,11 +63,14 @@ export default {
loading.value = true;
try {
await scheduleAgentReboot(props.agent.agent_id, state.value);
const ret = await scheduleAgentReboot(
props.agent.agent_id,
state.value
);
$q.dialog({
title: "Reboot pending",
style: "width: 40vw",
message: `A reboot has been scheduled for <strong>${state.value.datetime}</strong> on ${props.agent.hostname}.
message: `A reboot has been scheduled for <strong>${ret.time}</strong> on ${props.agent.hostname}.
<br />It can be cancelled from the Pending Actions menu until the scheduled time.`,
html: true,
}).onDismiss(onDialogOK);

View File

@@ -77,6 +77,18 @@
new-value-mode="add"
/>
</q-card-section>
<q-card-section>
<tactical-dropdown
v-model="state.env_vars"
:label="envVarsLabel"
filled
use-input
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section>
<q-option-group
v-model="state.output"
@@ -128,6 +140,11 @@
/>
<q-checkbox v-model="state.save_all_output" label="Save all output" />
</q-card-section>
<q-card-section v-if="agent.plat === 'windows'">
<q-checkbox v-model="state.run_as_user" label="Run As User">
<q-tooltip>{{ runAsUserToolTip }}</q-tooltip>
</q-checkbox>
</q-card-section>
<q-card-section>
<q-input
v-model.number="state.timeout"
@@ -173,6 +190,7 @@ import { useScriptDropdown } from "@/composables/scripts";
import { useCustomFieldDropdown } from "@/composables/core";
import { runScript } from "@/api/agents";
import { notifySuccess } from "@/utils/notify";
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
import {
formatScriptSyntax,
removeExtraOptionCategories,
@@ -203,11 +221,18 @@ export default {
const { dialogRef, onDialogHide } = useDialogPluginComponent();
// setup dropdowns
const { script, scriptOptions, defaultTimeout, defaultArgs, syntax, link } =
useScriptDropdown(props.script, {
onMount: true,
filterByPlatform: props.agent.plat,
});
const {
script,
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
syntax,
link,
} = useScriptDropdown(props.script, {
onMount: true,
filterByPlatform: props.agent.plat,
});
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
// main run script functionaity
@@ -219,7 +244,9 @@ export default {
save_all_output: false,
script,
args: defaultArgs,
env_vars: defaultEnvVars,
timeout: defaultTimeout,
run_as_user: false,
});
const ret = ref(null);
@@ -273,6 +300,8 @@ export default {
// non-reactive data
outputOptions,
runAsUserToolTip,
envVarsLabel,
//methods
formatScriptSyntax,

View File

@@ -51,6 +51,11 @@
/>
</div>
</q-card-section>
<q-card-section v-if="agent.plat === 'windows'">
<q-checkbox v-model="state.run_as_user" label="Run As User">
<q-tooltip>{{ runAsUserToolTip }}</q-tooltip>
</q-checkbox>
</q-card-section>
<q-card-section v-if="state.shell === 'custom'">
<q-input
v-model="state.custom_shell"
@@ -117,6 +122,7 @@ import { ref } from "vue";
import { useDialogPluginComponent } from "quasar";
import { sendAgentCommand } from "@/api/agents";
import { cmdPlaceholder } from "@/composables/agents";
import { runAsUserToolTip } from "@/constants/constants";
export default {
name: "SendCommand",
@@ -134,6 +140,7 @@ export default {
cmd: null,
timeout: 30,
custom_shell: null,
run_as_user: false,
});
const loading = ref(false);
@@ -156,6 +163,9 @@ export default {
loading,
ret,
// non reactivete data
runAsUserToolTip,
// methods
submit,
cmdPlaceholder,

View File

@@ -8,11 +8,12 @@
</q-btn>
</q-bar>
<q-separator />
<q-banner class="bg-warning">
<q-banner class="bg-info">
<template v-slot:avatar>
<q-icon name="info" />
</template>
Agents will now automatically self update, this tool is no longer needed.
Agents will automatically self update at 35 min past the hour, every hour.
Use this tool to manually trigger an agent update cycle.
</q-banner>
<q-card-section>
Select Version

View File

@@ -0,0 +1,211 @@
<template>
<q-dialog
ref="dialogRef"
@hide="onDialogHide"
persistent
@keydown.esc="onDialogHide"
>
<q-card
class="q-dialog-plugin"
:style="{ 'min-width': !ret ? '40vw' : '70vw' }"
>
<q-bar>
Send command on {{ agent.hostname }}
<q-space />
<q-chip v-if="!wsConnected" color="red" text-color="white" icon="error"
>Websocket diconnected!</q-chip
>
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-form @submit="submit">
<q-card-section>
<p>Shell</p>
<div class="q-gutter-sm">
<q-radio
v-if="agent.plat !== 'windows'"
dense
v-model="state.shell"
val="/bin/bash"
label="Bash"
@update:model-value="state.custom_shell = null"
/>
<q-radio
v-if="agent.plat !== 'windows'"
dense
v-model="state.shell"
val="custom"
label="Custom"
/>
<q-radio
v-if="agent.plat === 'windows'"
dense
v-model="state.shell"
val="cmd"
label="CMD"
/>
<q-radio
v-if="agent.plat === 'windows'"
dense
v-model="state.shell"
val="powershell"
label="Powershell"
/>
</div>
</q-card-section>
<q-card-section v-if="state.shell === 'custom'">
<q-input
v-model="state.custom_shell"
outlined
label="Custom shell"
stack-label
placeholder="/usr/bin/python3"
:rules="[(val) => !!val || '*Required']"
/>
</q-card-section>
<q-card-section>
<q-input
v-model.number="state.timeout"
dense
outlined
type="number"
style="max-width: 150px"
label="Timeout (seconds)"
stack-label
:rules="[
(val) => !!val || '*Required',
(val) => val >= 10 || 'Minimum is 10 seconds',
(val) => val <= 3600 || 'Maximum is 3600 seconds',
]"
/>
</q-card-section>
<q-card-section>
<q-input
v-model="state.cmd"
outlined
label="Command"
stack-label
:placeholder="cmdPlaceholder(state.shell)"
:rules="[(val) => !!val || '*Required']"
/>
</q-card-section>
<q-card-actions align="right">
<q-btn flat dense push label="Cancel" v-close-popup />
<q-btn
:loading="loading"
:disable="!wsConnected"
flat
dense
push
label="Send"
color="primary"
type="submit"
>
</q-btn>
</q-card-actions>
<q-card-section
v-if="ret !== null"
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
style="max-height: 50vh"
>
<pre>{{ ret }}</pre>
</q-card-section>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
// composition imports
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { useStore } from "vuex";
import { useDialogPluginComponent } from "quasar";
import { cmdPlaceholder } from "@/composables/agents";
import { getWSUrl } from "@/websocket/channels";
export default {
name: "SendCommand",
emits: [...useDialogPluginComponent.emits],
props: {
agent: !Object,
},
setup(props) {
const store = useStore();
// setup quasar dialog plugin
const { dialogRef, onDialogHide } = useDialogPluginComponent();
// run command logic
const state = ref({
shell: props.agent.plat === "windows" ? "cmd" : "/bin/bash",
cmd: null,
timeout: 30,
custom_shell: null,
agent_id: props.agent.agent_id,
});
const loading = ref(false);
const ret = ref(null);
// websocket
const ws = ref(null);
const wsConnected = ref(false);
function setupWS() {
const token = computed(() => store.state.token);
console.log("Starting send command websocket");
let url = getWSUrl("sendcmd", token.value);
ws.value = new WebSocket(url);
ws.value.onopen = () => {
wsConnected.value = true;
console.log("Send command websocket connected");
};
ws.value.onmessage = (e) => {
const data = JSON.parse(e.data);
ret.value = data.ret;
loading.value = false;
};
ws.value.onclose = () => {
console.log("Send command websocket disconnected");
wsConnected.value = false;
};
ws.value.onerror = () => {
wsConnected.value = false;
console.log("Send command websocket error");
ws.value.onclose();
};
}
function submit() {
ret.value = null;
loading.value = true;
ret.value = ws.value.send(JSON.stringify(state.value));
}
onMounted(() => {
setupWS();
});
onBeforeUnmount(() => {
ws.value.close();
});
return {
// reactive data
state,
loading,
ret,
wsConnected,
// methods
submit,
cmdPlaceholder,
// quasar dialog
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@@ -204,6 +204,20 @@
new-value-mode="add"
/>
<q-select
class="q-mb-sm"
dense
label="Failure action environment vars (press Enter after typing each key=value pair)"
filled
v-model="template.action_env_vars"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
<q-input
class="q-mb-sm"
label="Failure action timeout (seconds)"
@@ -277,6 +291,20 @@
new-value-mode="add"
/>
<q-select
class="q-mb-sm"
dense
label="Resolved action environment vars (press Enter after typing each key=value pair)"
filled
v-model="template.resolved_action_env_vars"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
<q-input
class="q-mb-sm"
label="Resolved action timeout (seconds)"
@@ -696,9 +724,11 @@ export default {
is_active: true,
action: null,
action_args: [],
action_env_vars: [],
action_timeout: 15,
resolved_action: null,
resolved_action_args: [],
resolved_action_env_vars: [],
resolved_action_timeout: 15,
email_recipients: [],
email_from: "",
@@ -762,11 +792,13 @@ export default {
(i) => i.value === this.template.action
);
this.template.action_args = script.args;
this.template.action_env_vars = script.env_vars;
} else if (type === "resolved") {
const script = this.scriptOptions.find(
(i) => i.value === this.template.resolved_action
);
this.template.resolved_action_args = script.args;
this.template.resolved_action_env_vars = script.env_vars;
}
},
toggleAddEmail() {

View File

@@ -12,11 +12,15 @@
color="positive"
class="full-width"
@click="doCodeSign"
:loading="loading"
>
<q-tooltip
>Force all existing agents to be updated to the code-signed
version</q-tooltip
>
<template v-slot:loading>
<q-spinner-facebook />
</template>
</q-btn>
</q-card-section>
<q-form @submit.prevent="editToken">
@@ -33,56 +37,92 @@
</q-card-section>
<q-card-section class="row items-center">
<q-btn label="Save" color="primary" type="submit" />
<q-space />
<q-btn label="Delete" color="negative" @click="confirmDelete" />
</q-card-section>
</q-form>
</q-card>
</template>
<script>
import mixins from "@/mixins/mixins";
import { ref, onMounted } from "vue";
import { useQuasar } from "quasar";
import axios from "axios";
import { notifySuccess } from "@/utils/notify";
const endpoint = "/core/codesign/";
export default {
name: "CodeSign",
mixins: [mixins],
data() {
return {
settings: {
token: "",
},
};
},
methods: {
getToken() {
this.$axios.get("/core/codesign/").then((r) => {
this.settings = r.data;
setup() {
const $q = useQuasar();
const settings = ref({ token: "" });
const loading = ref(false);
async function getToken() {
try {
const { data } = await axios.get(endpoint);
settings.value = data;
} catch (e) {
console.error(e);
}
}
async function deleteToken() {
try {
await axios.delete(endpoint);
notifySuccess("Token was deleted!");
await getToken();
} catch (e) {
console.error(e);
}
}
function confirmDelete() {
$q.dialog({
title: "Delete token?",
cancel: true,
persistent: true,
}).onOk(() => {
deleteToken();
});
},
editToken() {
this.$q.loading.show();
this.$axios
.patch("/core/codesign/", this.settings)
.then((r) => {
this.$q.loading.hide();
this.notifySuccess(r.data);
})
.catch(() => {
this.$q.loading.hide();
});
},
doCodeSign() {
this.$q.loading.show();
this.$axios
.post("/core/codesign/")
.then((r) => {
this.$q.loading.hide();
this.notifySuccess(r.data);
})
.catch(() => {
this.$q.loading.hide();
});
},
},
mounted() {
this.getToken();
}
async function doCodeSign() {
loading.value = true;
try {
const { data } = await axios.post(endpoint);
loading.value = false;
notifySuccess(data);
} catch (e) {
loading.value = false;
console.error(e);
}
}
async function editToken() {
$q.loading.show();
try {
const { data } = await axios.patch(endpoint, settings.value);
$q.loading.hide();
notifySuccess(data);
} catch (e) {
$q.loading.hide();
console.error(e);
}
}
onMounted(() => {
getToken();
});
return {
settings,
loading,
confirmDelete,
doCodeSign,
editToken,
};
},
};
</script>

View File

@@ -12,6 +12,7 @@
<q-tab name="urlactions" label="URL Actions" />
<q-tab name="retention" label="Retention" />
<q-tab name="apikeys" label="API Keys" />
<q-tab name="openai" label="Open AI" />
</q-tabs>
</template>
<template v-slot:after>
@@ -508,6 +509,49 @@
<q-tab-panel name="apikeys">
<APIKeysTable />
</q-tab-panel>
<!-- Open AI -->
<q-tab-panel name="openai">
<div class="text-subtitle2">Open AI</div>
<q-separator />
<q-card-section class="row">
<div class="col-4">API Key:</div>
<div class="col-2"></div>
<q-input
dense
outlined
v-model="settings.open_ai_token"
class="col-6"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-4">Open AI Model:</div>
<div class="col-2"></div>
<q-input
dense
outlined
v-model="settings.open_ai_model"
class="col-6"
>
<template v-slot:after>
<q-btn
round
dense
flat
icon="info"
size="sm"
@click="
openURL(
'https://platform.openai.com/docs/models/overview'
)
"
>
<q-tooltip>Click to see available options</q-tooltip>
</q-btn>
</template>
</q-input>
</q-card-section>
</q-tab-panel>
</q-tab-panels>
</q-scroll-area>
<q-card-section class="row items-center">

View File

@@ -11,7 +11,17 @@
:style="maximized ? '' : 'width: 90vw; max-width: 90vw'"
>
<q-bar>
{{ title }}
<span class="q-pr-sm">{{ title }}</span>
<q-btn
v-if="!script && openAIEnabled"
size="xs"
:disable="loading"
dense
label="Generate Script"
color="primary"
no-caps
@click="generateScriptOpenAI"
/>
<q-space />
<q-btn
dense
@@ -57,93 +67,133 @@
><br />Add one to get rid of this warning. Ignore if windows.
</q-banner>
<div class="row q-pa-sm">
<div class="col-4 q-gutter-sm q-pr-sm">
<q-input
filled
dense
:readonly="readonly"
v-model="formScript.name"
label="Name"
:rules="[(val) => !!val || '*Required']"
hide-bottom-space
/>
<q-input
filled
dense
:readonly="readonly"
v-model="formScript.description"
label="Description"
/>
<q-select
:readonly="readonly"
options-dense
filled
dense
v-model="formScript.shell"
:options="shellOptions"
emit-value
map-options
label="Shell Type"
/>
<tactical-dropdown
v-model="formScript.supported_platforms"
:options="agentPlatformOptions"
label="Supported Platforms (All supported if blank)"
clearable
mapOptions
filled
multiple
:readonly="readonly"
/>
<tactical-dropdown
filled
v-model="formScript.category"
:options="categories"
use-input
clearable
new-value-mode="add-unique"
filterable
label="Category"
:readonly="readonly"
hide-bottom-space
/>
<tactical-dropdown
v-model="formScript.args"
label="Script Arguments (press Enter after typing each argument)"
filled
use-input
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
:readonly="readonly"
/>
<q-input
type="number"
filled
dense
:readonly="readonly"
v-model.number="formScript.default_timeout"
label="Timeout (seconds)"
:rules="[(val) => val >= 5 || 'Minimum is 5']"
hide-bottom-space
/>
<q-input
label="Syntax"
type="textarea"
style="height: 150px; overflow-y: auto; resize: none"
v-model="formScript.syntax"
dense
filled
:readonly="readonly"
/>
</div>
<q-scroll-area
:thumb-style="{
right: '4px',
borderRadius: '5px',
width: '5px',
opacity: 0.75,
}"
:bar-style="{
right: '2px',
borderRadius: '9px',
width: '9px',
opacity: 0.2,
}"
class="col-4 q-mb-none q-pb-none"
:style="{ height: `${maximized ? '82vh' : '64vh'}` }"
>
<div class="q-gutter-sm q-pr-sm">
<q-input
filled
dense
:readonly="readonly"
v-model="formScript.name"
label="Name"
:rules="[(val) => !!val || '*Required']"
hide-bottom-space
/>
<q-input
filled
dense
:readonly="readonly"
v-model="formScript.description"
label="Description"
/>
<q-select
:readonly="readonly"
options-dense
filled
dense
v-model="formScript.shell"
:options="shellOptions"
emit-value
map-options
label="Shell Type"
/>
<tactical-dropdown
v-model="formScript.supported_platforms"
:options="agentPlatformOptions"
label="Supported Platforms (All supported if blank)"
clearable
mapOptions
filled
multiple
:readonly="readonly"
/>
<tactical-dropdown
filled
v-model="formScript.category"
:options="categories"
use-input
clearable
new-value-mode="add-unique"
filterable
label="Category"
:readonly="readonly"
hide-bottom-space
/>
<tactical-dropdown
v-model="formScript.args"
label="Script Arguments (press Enter after typing each argument)"
filled
use-input
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
:readonly="readonly"
/>
<tactical-dropdown
v-model="formScript.env_vars"
:label="envVarsLabel"
filled
use-input
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
:readonly="readonly"
/>
<q-input
type="number"
filled
dense
:readonly="readonly"
v-model.number="formScript.default_timeout"
label="Timeout (seconds)"
:rules="[(val) => val >= 5 || 'Minimum is 5']"
hide-bottom-space
/>
<q-checkbox
v-model="formScript.run_as_user"
label="Run As User (Windows only)"
>
<q-tooltip
>Setting this value on the script model will always override
any 'Run As User' checkboxes in the UI and force this script
to always be run in the context of the logged in user. If no
user is logged in, the script will not run and an error will
be returned.
</q-tooltip>
</q-checkbox>
<q-input
label="Syntax"
type="textarea"
style="height: 150px; overflow-y: auto; resize: none"
v-model="formScript.syntax"
dense
filled
:readonly="readonly"
/>
</div>
</q-scroll-area>
<v-ace-editor
v-model:value="formScript.script_body"
class="col-8"
:lang="lang"
:theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
:style="{ height: `${maximized ? '87vh' : '64vh'}` }"
:style="{ height: `${maximized ? '82vh' : '64vh'}` }"
wrap
:printMargin="false"
:options="{ fontSize: '14px' }"
@@ -197,9 +247,11 @@
<script>
// composable imports
import { ref, computed, onMounted } from "vue";
import { useStore } from "vuex";
import { useQuasar, useDialogPluginComponent } from "quasar";
import { saveScript, editScript, downloadScript } from "@/api/scripts";
import { useAgentDropdown, agentPlatformOptions } from "@/composables/agents";
import { generateScript } from "@/api/core";
import { notifySuccess } from "@/utils/notify";
// ui imports
@@ -217,6 +269,7 @@ import "ace-builds/src-noconflict/theme-tomorrow";
// static data
import { shellOptions } from "@/composables/scripts";
import { envVarsLabel } from "@/constants/constants";
export default {
name: "ScriptFormModal",
@@ -242,6 +295,10 @@ export default {
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
const $q = useQuasar();
// setup store
const store = useStore();
const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
// setup agent dropdown
const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
@@ -253,6 +310,8 @@ export default {
default_timeout: 90,
args: [],
script_body: "",
run_as_user: false,
env_vars: [],
});
if (props.clone) script.value.name = `(Copy) ${script.value.name}`;
@@ -329,6 +388,23 @@ export default {
});
}
function generateScriptOpenAI() {
$q.dialog({
title: "Ask ChatGPT what you need!",
prompt: {
model: `${lang.value} code that `,
type: "text",
},
cancel: true,
persistent: true,
}).onOk(async (data) => {
const completion = await generateScript({
prompt: data,
});
script.value.script_body = completion;
});
}
// component life cycle hooks
onMounted(async () => {
agentLoading.value = true;
@@ -350,13 +426,16 @@ export default {
// non-reactive data
shellOptions,
agentPlatformOptions,
envVarsLabel,
//computed
title,
openAIEnabled,
//methods
submitForm,
openTestScriptModal,
generateScriptOpenAI,
// quasar dialog plugin
dialogRef,

View File

@@ -867,7 +867,7 @@ export default {
}
// component life cycle hooks
onMounted(getScripts());
onMounted(getScripts);
return {
// reactive data

View File

@@ -11,7 +11,17 @@
:style="maximized ? '' : 'width: 70vw; max-width: 90vw'"
>
<q-bar>
{{ title }}
<span class="q-pr-sm">{{ title }}</span>
<q-btn
v-if="!snippet && openAIEnabled"
:disable="loading"
dense
size="xs"
label="Generate Script"
color="primary"
no-caps
@click="generateScriptOpenAI"
/>
<q-space />
<q-btn
dense
@@ -97,6 +107,9 @@
<script>
// composable imports
import { ref, computed } from "vue";
import { useStore } from "vuex";
import { useQuasar } from "quasar";
import { generateScript } from "@/api/core";
import { useDialogPluginComponent } from "quasar";
import { saveScriptSnippet, editScriptSnippet } from "@/api/scripts";
import { notifySuccess } from "@/utils/notify";
@@ -128,6 +141,13 @@ export default {
// setup quasar plugins
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// setup quasar
const $q = useQuasar();
// setup store
const store = useStore();
const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
// snippet form logic
const snippet = props.snippet
? ref(Object.assign({}, props.snippet))
@@ -167,6 +187,23 @@ export default {
loading.value = false;
}
function generateScriptOpenAI() {
$q.dialog({
title: "Ask ChatGPT what you need!",
prompt: {
model: `${lang.value} code that `,
type: "text",
},
cancel: true,
persistent: true,
}).onOk(async (data) => {
const completion = await generateScript({
prompt: data,
});
snippet.value.code = completion;
});
}
return {
// reactive data
formSnippet: snippet.value,
@@ -179,9 +216,11 @@ export default {
//computed
title,
openAIEnabled,
//methods
submitForm,
generateScriptOpenAI,
// quasar dialog plugin
dialogRef,

View File

@@ -93,6 +93,20 @@
/>
</q-card-section>
<q-card-section>
<tactical-dropdown
v-model="script.env_vars"
label="Environment Variables"
placeholder="(press Enter after typing each key=value pair)"
filled
use-input
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section>
<q-input
label="Default Timeout"

View File

@@ -44,6 +44,8 @@ export default {
timeout: props.script.default_timeout,
args: props.script.args,
shell: props.script.shell,
run_as_user: props.script.run_as_user,
env_vars: props.script.env_vars,
};
try {
ret.value = await testScript(props.agent, data);

View File

@@ -102,7 +102,7 @@
<tactical-dropdown
v-if="actionType === 'script'"
class="col-4"
class="col-3"
label="Select script"
v-model="script"
:options="scriptOptions"
@@ -113,7 +113,7 @@
<q-select
v-if="actionType === 'script'"
class="col-5"
class="col-3"
dense
label="Script Arguments (press Enter after typing each argument)"
filled
@@ -126,6 +126,21 @@
new-value-mode="add"
/>
<q-select
v-if="actionType === 'script'"
class="col-3"
dense
:label="envVarsLabel"
filled
v-model="defaultEnvVars"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
<q-input
v-if="actionType === 'script'"
class="col-2"
@@ -210,6 +225,9 @@
<q-item-label caption>
Arguments: {{ element.script_args }}
</q-item-label>
<q-item-label caption>
Env Vars: {{ element.env_vars }}
</q-item-label>
<q-item-label caption>
Timeout: {{ element.timeout }}
</q-item-label>
@@ -727,6 +745,7 @@ import { useCheckDropdown } from "@/composables/checks";
import { useCustomFieldDropdown } from "@/composables/core";
import { notifySuccess, notifyError } from "@/utils/notify";
import { validateTimePeriod } from "@/utils/validation";
import { envVarsLabel } from "@/constants/constants";
import {
convertPeriodToSeconds,
convertToBitArray,
@@ -817,10 +836,15 @@ export default {
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// setup dropdowns
const { script, scriptOptions, defaultTimeout, defaultArgs } =
useScriptDropdown(undefined, {
onMount: true,
});
const {
script,
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
} = useScriptDropdown(undefined, {
onMount: true,
});
// set defaultTimeout to 30
defaultTimeout.value = 30;
@@ -914,6 +938,7 @@ export default {
script: script.value,
timeout: defaultTimeout.value,
script_args: defaultArgs.value,
env_vars: defaultEnvVars.value,
});
} else if (actionType.value === "cmd") {
task.value.actions.push({
@@ -927,6 +952,7 @@ export default {
// clear fields after add
script.value = null;
defaultArgs.value = [];
defaultEnvVars.value = [];
defaultTimeout.value = 30;
command.value = "";
}
@@ -991,10 +1017,16 @@ export default {
: [];
// remove milliseconds and Z to work with native date input
task.value.run_time_date = formatDateInputField(task.value.run_time_date);
task.value.run_time_date = formatDateInputField(
task.value.run_time_date,
true
);
if (task.value.expire_date)
task.value.expire_date = formatDateInputField(task.value.expire_date);
task.value.expire_date = formatDateInputField(
task.value.expire_date,
true
);
// set task type if monthlydow is being used
if (task.value.task_type === "monthlydow") {
@@ -1083,6 +1115,7 @@ export default {
script,
defaultTimeout,
defaultArgs,
defaultEnvVars,
actionType,
command,
shell,
@@ -1110,6 +1143,7 @@ export default {
monthOptions,
taskTypeOptions,
taskInstancePolicyOptions,
envVarsLabel,
// methods
submit,

View File

@@ -31,7 +31,7 @@ export function useUserDropdown(onMount = false) {
}
if (onMount) {
onMounted(getUserOptions());
onMounted(getUserOptions);
}
return {

View File

@@ -37,4 +37,5 @@ export function cmdPlaceholder(shell) {
export const agentPlatformOptions = [
{ value: "windows", label: "Windows" },
{ value: "linux", label: "Linux" },
{ value: "darwin", label: "macOS" },
];

View File

@@ -8,6 +8,7 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
const scriptOptions = ref([]);
const defaultTimeout = ref(30);
const defaultArgs = ref([]);
const defaultEnvVars = ref([]);
const script = ref(setScript);
const syntax = ref("");
const link = ref("");
@@ -29,6 +30,7 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
);
defaultTimeout.value = tmpScript.timeout;
defaultArgs.value = tmpScript.args;
defaultEnvVars.value = tmpScript.env_vars;
syntax.value = tmpScript.syntax;
link.value =
tmpScript.script_type === "builtin"
@@ -49,6 +51,7 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
syntax,
link,

View File

@@ -0,0 +1,10 @@
export const GOARCH_AMD64 = "amd64";
export const GOARCH_i386 = "386";
export const GOARCH_ARM64 = "arm64";
export const GOARCH_ARM32 = "arm";
export const runAsUserToolTip =
"Run in the context of the logged in user. If no user is logged in, the script will not run and an error will be returned.";
export const envVarsLabel =
"Environment vars (press Enter after typing each key=value pair)";

View File

@@ -14,6 +14,27 @@
@click="$store.dispatch('reload')"
/>
</q-banner>
<q-banner
v-if="!hosted && tokenExpired"
inline-actions
class="bg-yellow text-black text-center"
>
<q-icon size="xl" name="warning" />
<span
><br />Your code signing token is no longer valid.<br /><br />
If you have downgraded or cancelled your sponsorship, please delete
your token from the Code Signing modal and refresh to get rid of this
banner.<br /><br />
For any issues or to renew your sponsorship please email
support@amidaware.com<br /><br
/></span>
<q-btn
color="dark"
icon="refresh"
label="Refresh"
@click="$store.dispatch('reload')"
/>
</q-banner>
<q-toolbar>
<q-btn
dense
@@ -35,12 +56,7 @@
Tactical RMM<span class="text-overline q-ml-sm"
>v{{ currentTRMMVersion }}</span
>
<span
class="text-overline q-ml-md"
v-if="
latestTRMMVersion !== 'error' &&
currentTRMMVersion !== latestTRMMVersion
"
<span class="text-overline q-ml-md" v-if="updateAvailable()"
><q-badge color="warning"
><a :href="latestReleaseURL" target="_blank"
>v{{ latestTRMMVersion }} available</a
@@ -124,6 +140,32 @@
<q-item-label>Preferences</q-item-label>
</q-item-section>
</q-item>
<q-item clickable>
<q-item-section>Account</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu anchor="top end" self="top start">
<q-list>
<q-item
clickable
v-ripple
@click="resetPassword"
v-close-popup
>
<q-item-section>
<q-item-label>Reset Password</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-ripple @click="reset2FA" v-close-popup>
<q-item-section>
<q-item-label>Reset 2FA</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item to="/expired" exact>
<q-item-section>
<q-item-label>Logout</q-item-label>
@@ -144,11 +186,14 @@ import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { useQuasar } from "quasar";
import { useStore } from "vuex";
import axios from "axios";
import { getBaseUrl } from "@/boot/axios";
import { getWSUrl } from "@/websocket/channels";
import { resetTwoFactor } from "@/api/accounts";
import { notifySuccess } from "@/utils/notify";
// ui imports
import AlertsIcon from "@/components/AlertsIcon.vue";
import UserPreferences from "@/components/modals/coresettings/UserPreferences.vue";
import ResetPass from "@/components/accounts/ResetPass.vue";
export default {
name: "MainLayout",
@@ -171,6 +216,8 @@ export default {
const latestTRMMVersion = computed(() => store.state.latestTRMMVersion);
const needRefresh = computed(() => store.state.needrefresh);
const user = computed(() => store.state.username);
const hosted = computed(() => store.state.hosted);
const tokenExpired = computed(() => store.state.tokenExpired);
const latestReleaseURL = computed(() => {
return latestTRMMVersion.value
@@ -184,8 +231,24 @@ export default {
}).onOk(() => store.dispatch("getDashInfo"));
}
function wsUrl() {
return getBaseUrl().split("://")[1];
function resetPassword() {
$q.dialog({
component: ResetPass,
});
}
function reset2FA() {
$q.dialog({
title: "Reset 2FA",
message: "Are you sure you would like to reset your 2FA token?",
cancel: true,
persistent: true,
}).onOk(async () => {
try {
const ret = await resetTwoFactor();
notifySuccess(ret, 3000);
} catch {}
});
}
const serverCount = ref(0);
@@ -200,13 +263,8 @@ export default {
// when ws is closed causing ws to connect with expired token
const token = computed(() => store.state.token);
console.log("Starting websocket");
const proto =
process.env.NODE_ENV === "production" || process.env.DOCKER_BUILD
? "wss"
: "ws";
ws.value = new WebSocket(
`${proto}://${wsUrl()}/ws/dashinfo/?access_token=${token.value}`
);
let url = getWSUrl("dashinfo", token.value);
ws.value = new WebSocket(url);
ws.value.onopen = () => {
console.log("Connected to ws");
};
@@ -242,6 +300,11 @@ export default {
}, 60 * 5 * 1000);
}
function updateAvailable() {
if (latestTRMMVersion.value === "error" || hosted.value) return false;
return currentTRMMVersion.value !== latestTRMMVersion.value;
}
onMounted(() => {
setupWS();
store.dispatch("getDashInfo");
@@ -267,9 +330,14 @@ export default {
user,
needRefresh,
darkMode,
hosted,
tokenExpired,
// methods
showUserPreferences,
resetPassword,
reset2FA,
updateAvailable,
};
},
};

View File

@@ -193,6 +193,7 @@ export default {
value: script.id,
timeout: script.default_timeout,
args: script.args,
env_vars: script.env_vars,
});
} else if (cat === "Unassigned" && !script.category) {
tmp.push({
@@ -200,6 +201,7 @@ export default {
value: script.id,
timeout: script.default_timeout,
args: script.args,
env_vars: script.env_vars,
});
}
});

View File

@@ -17,6 +17,7 @@ export default function () {
agentPlatform: "windows",
agentTableLoading: false,
needrefresh: false,
tokenExpired: false,
refreshSummaryTab: false,
tableHeight: "300px",
tabHeight: "300px",
@@ -32,6 +33,7 @@ export default function () {
currentTRMMVersion: null,
latestTRMMVersion: null,
dateFormat: "MMM-DD-YYYY - HH:mm",
openAIIntegrationEnabled: false,
};
},
getters: {
@@ -83,6 +85,9 @@ export default function () {
SET_REFRESH_NEEDED(state, action) {
state.needrefresh = action;
},
SET_TOKEN_EXPIRED(state, action) {
state.tokenExpired = action;
},
SET_SPLITTER(state, val) {
// top toolbar is 50px. Filebar is 40px and agent filter tabs are 44px
state.tableHeight = `${Screen.height - 50 - 40 - 78 - val}px`;
@@ -132,6 +137,9 @@ export default function () {
setDateFormat(state, val) {
state.dateFormat = val;
},
setOpenAIIntegrationStatus(state, val) {
state.openAIIntegrationEnabled = val;
},
},
actions: {
setClientTreeSplitter(context, val) {
@@ -212,6 +220,11 @@ export default function () {
context.commit("SET_URL_ACTION", data.url_action);
context.commit("setShowCommunityScripts", data.show_community_scripts);
context.commit("SET_HOSTED", data.hosted);
context.commit("SET_TOKEN_EXPIRED", data.token_is_expired);
context.commit(
"setOpenAIIntegrationStatus",
data.open_ai_integration_enabled
);
if (data.date_format && data.date_format !== "")
context.commit("setDateFormat", data.date_format);

View File

@@ -68,6 +68,7 @@ export function formatScriptOptions(data) {
value: script.id,
timeout: script.default_timeout,
args: script.args,
env_vars: script.env_vars,
filename: script.filename,
syntax: script.syntax,
script_type: script.script_type,
@@ -80,6 +81,7 @@ export function formatScriptOptions(data) {
value: script.id,
timeout: script.default_timeout,
args: script.args,
env_vars: script.env_vars,
filename: script.filename,
syntax: script.syntax,
script_type: script.script_type,
@@ -285,7 +287,7 @@ export function formatDateInputField(isoDateString, noTimezone = false) {
if (noTimezone) {
isoDateString = isoDateString.replace("Z", "");
}
return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm:ss");
return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm");
}
// converts a local date string "YYYY-MM-DDTHH:mm:ss" to an iso date string with the local timezone

View File

@@ -173,6 +173,18 @@
</q-menu>
</q-item>
<!-- Bulk Run Checks -->
<q-item
clickable
v-close-popup
@click="runChecks(props.node)"
>
<q-item-section side>
<q-icon name="fas fa-check-double" />
</q-item-section>
<q-item-section>Run Checks</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup>
@@ -440,7 +452,7 @@ export default {
showInstallAgentModal: false,
sitePk: null,
innerModel: (this.$q.screen.height - 82) / 2,
search: "",
search: (this.$route.query.search ? this.$route.query.search : ""),
filterTextLength: 0,
filterAvailability: "all",
filterPatchesPending: false,
@@ -690,6 +702,17 @@ export default {
})
.onOk(() => this.$store.dispatch("refreshDashboard"));
},
runChecks(node) {
const target = node.children ? "client" : "site";
this.$axios
.post(`/checks/${target}/${node.id}/csbulkrun/`)
.then((r) => {
this.notifySuccess(r.data);
})
.catch((e) => {
console.error(e);
});
},
showToggleMaintenance(node) {
let data = {
id: node.id,

11
src/websocket/channels.js Normal file
View File

@@ -0,0 +1,11 @@
import { getBaseUrl } from "@/boot/axios";
export function getWSUrl(path, token) {
const url = getBaseUrl().split("://")[1];
const proto =
process.env.NODE_ENV === "production" || process.env.DOCKER_BUILD
? "wss"
: "ws";
return `${proto}://${url}/ws/${path}/?access_token=${token}`;
}