Compare commits
44 Commits
v0.100.0-d
...
v0.100.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d270b877c9 | ||
|
|
5b8ac2c809 | ||
|
|
83d0ff1c0a | ||
|
|
8a6ec6ceab | ||
|
|
93dbc74e33 | ||
|
|
5f2add48a9 | ||
|
|
b7369875af | ||
|
|
2eb6580fed | ||
|
|
fd8b2a1d98 | ||
|
|
9f85fbb330 | ||
|
|
ee9715a4cf | ||
|
|
76f330fb9c | ||
|
|
f518043d8d | ||
|
|
e67c1ff331 | ||
|
|
137a5648ce | ||
|
|
cc2335558d | ||
|
|
a944bc50d1 | ||
|
|
0a4b00298d | ||
|
|
1eaed284a3 | ||
|
|
b278e0bed4 | ||
|
|
6ee3df7e4e | ||
|
|
a8a171ba2c | ||
|
|
7ee87da3b6 | ||
|
|
7bce958633 | ||
|
|
24a63f477e | ||
|
|
57963f6d1a | ||
|
|
c9d76bdddc | ||
|
|
c279a44679 | ||
|
|
974ba53926 | ||
|
|
021fbbe14f | ||
|
|
bbd74c34b7 | ||
|
|
dfef0a5b4b | ||
|
|
ee687bf559 | ||
|
|
627d0e91f1 | ||
|
|
bffaba1f60 | ||
|
|
fdf28539cb | ||
|
|
ac1246c81c | ||
|
|
4feed0c65c | ||
|
|
197f2f237b | ||
|
|
0dc0d010bd | ||
|
|
b17aff8c6f | ||
|
|
63147ce116 | ||
|
|
ba9f93962a | ||
|
|
ddeb6293a1 |
7
.devcontainer/.env.example
Normal file
7
.devcontainer/.env.example
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
COMPOSE_PROJECT_NAME=trmm
|
||||||
|
IMAGE_REPO=tacticalrmm/
|
||||||
|
VERSION=latest
|
||||||
|
|
||||||
|
# DEV SETTINGS
|
||||||
|
APP_PORT=443
|
||||||
|
DOCKER_NETWORK=172.21.0.0/24
|
||||||
26
.devcontainer/docker-compose.yml
Normal file
26
.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
version: '3.4'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app-dev:
|
||||||
|
container_name: trmm-app-dev
|
||||||
|
image: node:16-alpine
|
||||||
|
restart: always
|
||||||
|
command: /bin/sh -c "npm install --cache ~/.npm && npm run serve"
|
||||||
|
user: 1000:1000
|
||||||
|
working_dir: /workspace/web
|
||||||
|
volumes:
|
||||||
|
- ..:/workspace:cached
|
||||||
|
ports:
|
||||||
|
- "8080:443"
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
aliases:
|
||||||
|
- tactical-frontend
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dev:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
driver: default
|
||||||
|
config:
|
||||||
|
- subnet: ${DOCKER_NETWORK}
|
||||||
@@ -3,3 +3,4 @@ DEV_URL = "https://api.example.com"
|
|||||||
APP_URL = "https://app.example.com"
|
APP_URL = "https://app.example.com"
|
||||||
DEV_HOST = 0.0.0.0
|
DEV_HOST = 0.0.0.0
|
||||||
DEV_PORT = 80
|
DEV_PORT = 80
|
||||||
|
USE_HTTPS = false
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,3 +33,4 @@ yarn-error.log*
|
|||||||
*.sln
|
*.sln
|
||||||
|
|
||||||
.env
|
.env
|
||||||
|
/public/env-config.js
|
||||||
|
|||||||
2018
package-lock.json
generated
2018
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.100.0-dev",
|
"version": "0.100.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"productName": "Tactical RMM",
|
"productName": "Tactical RMM",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -10,47 +10,31 @@
|
|||||||
"format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
|
"format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "1.14.0",
|
"@quasar/extras": "1.15.1",
|
||||||
"apexcharts": "3.35.2",
|
"apexcharts": "3.35.4",
|
||||||
"axios": "0.27.2",
|
"axios": "0.27.2",
|
||||||
"dotenv": "16.0.0",
|
"dotenv": "16.0.1",
|
||||||
"qrcode.vue": "3.3.3",
|
"qrcode.vue": "3.3.3",
|
||||||
"quasar": "2.7.1",
|
"quasar": "2.7.7",
|
||||||
"vue": "3.2.31",
|
"vue": "3.2.37",
|
||||||
"vue3-ace-editor": "2.2.2",
|
"vue3-ace-editor": "2.2.2",
|
||||||
"vue3-apexcharts": "1.4.1",
|
"vue3-apexcharts": "1.4.1",
|
||||||
"vuedraggable": "4.1.0",
|
"vuedraggable": "4.1.0",
|
||||||
"vue-router": "4.0.15",
|
"vue-router": "4.1.3",
|
||||||
"vuex": "4.0.2"
|
"vuex": "4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/cli": "^1.3.2",
|
"@quasar/cli": "^1.3.2",
|
||||||
"@intlify/vite-plugin-vue-i18n": "^3.3.1",
|
"@intlify/vite-plugin-vue-i18n": "^6.0.0",
|
||||||
"@quasar/app-vite": "^1.0.1",
|
"@quasar/app-vite": "^1.0.6",
|
||||||
"@types/node": "^12.20.21",
|
"@types/node": "^18.6.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
"@typescript-eslint/eslint-plugin": "^5.33.0",
|
||||||
"@typescript-eslint/parser": "^5.10.0",
|
"@typescript-eslint/parser": "^5.33.0",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.7",
|
||||||
"eslint": "^8.10.0",
|
"eslint": "^8.21.0",
|
||||||
"eslint-config-prettier": "^8.1.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-vue": "^8.5.0",
|
"eslint-plugin-vue": "^8.5.0",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.7.1",
|
||||||
"typescript": "^4.6.4"
|
"typescript": "^4.7.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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
||||||
build: {
|
build: {
|
||||||
target: {
|
target: {
|
||||||
browser: ["es2019", "edge88", "firefox78", "chrome87", "safari13.1"],
|
browser: ["es2021"],
|
||||||
node: "node16",
|
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
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
|
||||||
devServer: {
|
devServer: {
|
||||||
https: false,
|
https: process.env.USE_HTTPS === "true",
|
||||||
open: false, // opens browser window automatically
|
open: false, // opens browser window automatically
|
||||||
host: process.env.DEV_HOST,
|
host: process.env.DEV_HOST,
|
||||||
port: process.env.DEV_PORT,
|
port: process.env.DEV_PORT,
|
||||||
|
|||||||
@@ -31,6 +31,17 @@ export default function ({ app, router, store }) {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
async function (error) {
|
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;
|
let text;
|
||||||
|
|
||||||
if (!error.response) {
|
if (!error.response) {
|
||||||
|
|||||||
@@ -356,6 +356,27 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
filterTable(rows, terms, cols, cellValue) {
|
filterTable(rows, terms, cols, cellValue) {
|
||||||
|
const hiddenFields = [
|
||||||
|
"version",
|
||||||
|
"operating_system",
|
||||||
|
"public_ip",
|
||||||
|
"cpu_model",
|
||||||
|
"graphics",
|
||||||
|
"local_ips",
|
||||||
|
"make_model",
|
||||||
|
"physical_disks",
|
||||||
|
];
|
||||||
|
|
||||||
|
// quasar filter only does visible columns so this is a hack to add hidden columns we want to filter
|
||||||
|
for (const elem of hiddenFields) {
|
||||||
|
if (!cols.find((o) => o.name === elem)) {
|
||||||
|
cols.push({
|
||||||
|
name: elem,
|
||||||
|
field: elem,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const lowerTerms = terms ? terms.toLowerCase() : "";
|
const lowerTerms = terms ? terms.toLowerCase() : "";
|
||||||
let advancedFilter = false;
|
let advancedFilter = false;
|
||||||
let availability = null;
|
let availability = null;
|
||||||
|
|||||||
@@ -142,6 +142,10 @@
|
|||||||
<q-item clickable v-close-popup @click="clearCache">
|
<q-item clickable v-close-popup @click="clearCache">
|
||||||
<q-item-section>Clear Cache</q-item-section>
|
<q-item-section>Clear Cache</q-item-section>
|
||||||
</q-item>
|
</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-list>
|
||||||
</q-menu>
|
</q-menu>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
@@ -262,6 +266,20 @@ export default {
|
|||||||
.get("/core/clearcache/")
|
.get("/core/clearcache/")
|
||||||
.then((r) => this.notifySuccess(r.data));
|
.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) {
|
openHelp(mode) {
|
||||||
let url;
|
let url;
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
|
|||||||
@@ -7,6 +7,17 @@
|
|||||||
<q-badge color="primary" class="q-ml-sm text-caption">{{
|
<q-badge color="primary" class="q-ml-sm text-caption">{{
|
||||||
v
|
v
|
||||||
}}</q-badge>
|
}}</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>
|
||||||
</div>
|
</div>
|
||||||
<q-separator v-if="info.length > 1" />
|
<q-separator v-if="info.length > 1" />
|
||||||
@@ -15,6 +26,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { copyToClipboard } from "quasar";
|
||||||
|
import { notifySuccess } from "@/utils/notify";
|
||||||
// composition imports
|
// composition imports
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
@@ -28,9 +41,17 @@ export default {
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
const tabHeight = computed(() => store.state.tabHeight);
|
const tabHeight = computed(() => store.state.tabHeight);
|
||||||
|
|
||||||
|
function copyValueToClip(val) {
|
||||||
|
copyToClipboard(val)
|
||||||
|
.then(() => {
|
||||||
|
notifySuccess("Copied to clipboard");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tabHeight,
|
tabHeight,
|
||||||
uid,
|
uid,
|
||||||
|
copyValueToClip,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -61,10 +61,7 @@
|
|||||||
<q-td key="client" :props="props">{{ props.row.client_name }}</q-td>
|
<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="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="mon_type" :props="props">{{ props.row.mon_type }}</q-td>
|
||||||
<q-td key="arch" :props="props"
|
<q-td key="goarch" :props="props">{{ props.row.goarch }}</q-td>
|
||||||
><span v-if="props.row.arch === '64'">64 bit</span
|
|
||||||
><span v-else>32 bit</span></q-td
|
|
||||||
>
|
|
||||||
<q-td key="expiry" :props="props">{{
|
<q-td key="expiry" :props="props">{{
|
||||||
formatDate(props.row.expiry)
|
formatDate(props.row.expiry)
|
||||||
}}</q-td>
|
}}</q-td>
|
||||||
@@ -130,7 +127,13 @@ const columns = [
|
|||||||
align: "left",
|
align: "left",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
{ name: "arch", label: "Arch", field: "arch", align: "left", sortable: true },
|
{
|
||||||
|
name: "goarch",
|
||||||
|
label: "Arch",
|
||||||
|
field: "goarch",
|
||||||
|
align: "left",
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "expiry",
|
name: "expiry",
|
||||||
label: "Expiry",
|
label: "Expiry",
|
||||||
|
|||||||
@@ -54,9 +54,9 @@
|
|||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="q-pl-sm">OS</div>
|
<div class="q-pl-sm">Arch</div>
|
||||||
<q-radio v-model="state.arch" val="64" label="64 bit" />
|
<q-radio v-model="state.goarch" :val="GOARCH_AMD64" label="64 bit" />
|
||||||
<q-radio v-model="state.arch" val="32" label="32 bit" />
|
<q-radio v-model="state.goarch" :val="GOARCH_i386" label="32 bit" />
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn dense flat label="Cancel" v-close-popup />
|
<q-btn dense flat label="Cancel" v-close-popup />
|
||||||
@@ -84,6 +84,7 @@ import {
|
|||||||
formatDateInputField,
|
formatDateInputField,
|
||||||
formatDateStringwithTimezone,
|
formatDateStringwithTimezone,
|
||||||
} from "@/utils/format";
|
} from "@/utils/format";
|
||||||
|
import { GOARCH_AMD64, GOARCH_i386 } from "@/constants/constants";
|
||||||
|
|
||||||
// ui imports
|
// ui imports
|
||||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||||
@@ -108,7 +109,7 @@ export default {
|
|||||||
power: false,
|
power: false,
|
||||||
rdp: false,
|
rdp: false,
|
||||||
ping: false,
|
ping: false,
|
||||||
arch: "64",
|
goarch: GOARCH_AMD64,
|
||||||
});
|
});
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -145,6 +146,10 @@ export default {
|
|||||||
// quasar dialog
|
// quasar dialog
|
||||||
dialogRef,
|
dialogRef,
|
||||||
onDialogHide,
|
onDialogHide,
|
||||||
|
|
||||||
|
// constants
|
||||||
|
GOARCH_AMD64,
|
||||||
|
GOARCH_i386,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -135,6 +135,11 @@
|
|||||||
:rules="[(val) => !!val || '*Required']"
|
:rules="[(val) => !!val || '*Required']"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</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-card-section v-if="mode === 'script' || mode === 'command'">
|
||||||
<q-input
|
<q-input
|
||||||
@@ -203,6 +208,7 @@ import { runBulkAction } from "@/api/agents";
|
|||||||
import { notifySuccess } from "@/utils/notify";
|
import { notifySuccess } from "@/utils/notify";
|
||||||
import { cmdPlaceholder } from "@/composables/agents";
|
import { cmdPlaceholder } from "@/composables/agents";
|
||||||
import { removeExtraOptionCategories } from "@/utils/format";
|
import { removeExtraOptionCategories } from "@/utils/format";
|
||||||
|
import { runAsUserToolTip } from "@/constants/constants";
|
||||||
|
|
||||||
// ui imports
|
// ui imports
|
||||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||||
@@ -300,6 +306,7 @@ export default {
|
|||||||
script,
|
script,
|
||||||
timeout: defaultTimeout,
|
timeout: defaultTimeout,
|
||||||
args: defaultArgs,
|
args: defaultArgs,
|
||||||
|
run_as_user: false,
|
||||||
});
|
});
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
@@ -316,6 +323,7 @@ export default {
|
|||||||
() => state.value.osType,
|
() => state.value.osType,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
state.value.custom_shell = null;
|
state.value.custom_shell = null;
|
||||||
|
state.value.run_as_user = false;
|
||||||
|
|
||||||
if (newValue === "windows") {
|
if (newValue === "windows") {
|
||||||
state.value.shell = "cmd";
|
state.value.shell = "cmd";
|
||||||
@@ -337,6 +345,13 @@ export default {
|
|||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const supportsRunAsUser = () => {
|
||||||
|
const modes = ["script", "command"];
|
||||||
|
return (
|
||||||
|
state.value.osType === "windows" && modes.includes(state.value.mode)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// set modal title and caption
|
// set modal title and caption
|
||||||
const modalTitle = computed(() => {
|
const modalTitle = computed(() => {
|
||||||
return props.mode === "command"
|
return props.mode === "command"
|
||||||
@@ -387,6 +402,7 @@ export default {
|
|||||||
osTypeOptions,
|
osTypeOptions,
|
||||||
targetOptions,
|
targetOptions,
|
||||||
patchModeOptions,
|
patchModeOptions,
|
||||||
|
runAsUserToolTip,
|
||||||
|
|
||||||
//computed
|
//computed
|
||||||
modalTitle,
|
modalTitle,
|
||||||
@@ -394,6 +410,7 @@ export default {
|
|||||||
//methods
|
//methods
|
||||||
submit,
|
submit,
|
||||||
cmdPlaceholder,
|
cmdPlaceholder,
|
||||||
|
supportsRunAsUser,
|
||||||
|
|
||||||
// quasar dialog plugin
|
// quasar dialog plugin
|
||||||
dialogRef,
|
dialogRef,
|
||||||
|
|||||||
@@ -465,8 +465,51 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
editAgent() {
|
editAgent() {
|
||||||
delete this.agent.all_timezones;
|
// TODO we need to fix the serializer to not send this stuff
|
||||||
delete this.agent.timezone;
|
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
|
// 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
|
// 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, ";
|
else if (day === 0) result += "Sun, ";
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.trimRight(",");
|
return result.trimEnd(",");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
label="Windows"
|
label="Windows"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
installMethod = 'exe';
|
installMethod = 'exe';
|
||||||
arch = '64';
|
goarch = GOARCH_AMD64;
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<q-radio
|
<q-radio
|
||||||
@@ -48,8 +48,8 @@
|
|||||||
val="linux"
|
val="linux"
|
||||||
label="Linux"
|
label="Linux"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
installMethod = 'linux';
|
installMethod = 'bash';
|
||||||
arch = 'amd64';
|
goarch = GOARCH_AMD64;
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,38 +102,38 @@
|
|||||||
Arch
|
Arch
|
||||||
<div class="q-gutter-sm">
|
<div class="q-gutter-sm">
|
||||||
<q-radio
|
<q-radio
|
||||||
v-model="arch"
|
v-model="goarch"
|
||||||
val="64"
|
:val="GOARCH_AMD64"
|
||||||
label="64 bit"
|
label="64 bit"
|
||||||
v-show="agentOS === 'windows'"
|
v-show="agentOS === 'windows'"
|
||||||
/>
|
/>
|
||||||
<q-radio
|
<q-radio
|
||||||
v-model="arch"
|
v-model="goarch"
|
||||||
val="32"
|
:val="GOARCH_i386"
|
||||||
label="32 bit"
|
label="32 bit"
|
||||||
v-show="agentOS === 'windows'"
|
v-show="agentOS === 'windows'"
|
||||||
/>
|
/>
|
||||||
<q-radio
|
<q-radio
|
||||||
v-model="arch"
|
v-model="goarch"
|
||||||
val="amd64"
|
:val="GOARCH_AMD64"
|
||||||
label="64 bit"
|
label="64 bit"
|
||||||
v-show="agentOS !== 'windows'"
|
v-show="agentOS !== 'windows'"
|
||||||
/>
|
/>
|
||||||
<q-radio
|
<q-radio
|
||||||
v-model="arch"
|
v-model="goarch"
|
||||||
val="386"
|
:val="GOARCH_i386"
|
||||||
label="32 bit"
|
label="32 bit"
|
||||||
v-show="agentOS !== 'windows'"
|
v-show="agentOS !== 'windows'"
|
||||||
/>
|
/>
|
||||||
<q-radio
|
<q-radio
|
||||||
v-model="arch"
|
v-model="goarch"
|
||||||
val="arm64"
|
:val="GOARCH_ARM64"
|
||||||
label="ARM 64 bit"
|
label="ARM 64 bit"
|
||||||
v-show="agentOS !== 'windows'"
|
v-show="agentOS !== 'windows'"
|
||||||
/>
|
/>
|
||||||
<q-radio
|
<q-radio
|
||||||
v-model="arch"
|
v-model="goarch"
|
||||||
val="arm"
|
:val="GOARCH_ARM32"
|
||||||
label="ARM 32 bit (Rasp Pi)"
|
label="ARM 32 bit (Rasp Pi)"
|
||||||
v-show="agentOS !== 'windows'"
|
v-show="agentOS !== 'windows'"
|
||||||
/>
|
/>
|
||||||
@@ -177,6 +177,12 @@
|
|||||||
import mixins from "@/mixins/mixins";
|
import mixins from "@/mixins/mixins";
|
||||||
import AgentDownload from "@/components/modals/agents/AgentDownload.vue";
|
import AgentDownload from "@/components/modals/agents/AgentDownload.vue";
|
||||||
import { getBaseUrl } from "@/boot/axios";
|
import { getBaseUrl } from "@/boot/axios";
|
||||||
|
import {
|
||||||
|
GOARCH_AMD64,
|
||||||
|
GOARCH_i386,
|
||||||
|
GOARCH_ARM64,
|
||||||
|
GOARCH_ARM32,
|
||||||
|
} from "@/constants/constants";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "InstallAgent",
|
name: "InstallAgent",
|
||||||
@@ -187,6 +193,10 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
GOARCH_AMD64: GOARCH_AMD64,
|
||||||
|
GOARCH_i386: GOARCH_i386,
|
||||||
|
GOARCH_ARM64: GOARCH_ARM64,
|
||||||
|
GOARCH_ARM32: GOARCH_ARM32,
|
||||||
client_options: [],
|
client_options: [],
|
||||||
client: null,
|
client: null,
|
||||||
site: null,
|
site: null,
|
||||||
@@ -198,7 +208,7 @@ export default {
|
|||||||
showAgentDownload: false,
|
showAgentDownload: false,
|
||||||
info: {},
|
info: {},
|
||||||
installMethod: "exe",
|
installMethod: "exe",
|
||||||
arch: "64",
|
goarch: GOARCH_AMD64,
|
||||||
agentOS: "windows",
|
agentOS: "windows",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -239,10 +249,7 @@ export default {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/([^a-zA-Z0-9]+)/g, "");
|
.replace(/([^a-zA-Z0-9]+)/g, "");
|
||||||
|
|
||||||
const fileName =
|
const fileName = `trmm-${clientStripped}-${siteStripped}-${this.agenttype}-${this.goarch}.exe`;
|
||||||
this.arch === "64"
|
|
||||||
? `rmm-${clientStripped}-${siteStripped}-${this.agenttype}.exe`
|
|
||||||
: `rmm-${clientStripped}-${siteStripped}-${this.agenttype}-x86.exe`;
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
installMethod: this.installMethod,
|
installMethod: this.installMethod,
|
||||||
@@ -253,10 +260,10 @@ export default {
|
|||||||
power: this.power ? 1 : 0,
|
power: this.power ? 1 : 0,
|
||||||
rdp: this.rdp ? 1 : 0,
|
rdp: this.rdp ? 1 : 0,
|
||||||
ping: this.ping ? 1 : 0,
|
ping: this.ping ? 1 : 0,
|
||||||
arch: this.arch,
|
goarch: this.goarch,
|
||||||
api,
|
api,
|
||||||
fileName,
|
fileName,
|
||||||
os: this.agentOS,
|
plat: this.agentOS,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.installMethod === "manual") {
|
if (this.installMethod === "manual") {
|
||||||
@@ -264,7 +271,7 @@ export default {
|
|||||||
this.info = {
|
this.info = {
|
||||||
expires: this.expires,
|
expires: this.expires,
|
||||||
data: r.data,
|
data: r.data,
|
||||||
arch: this.arch,
|
goarch: this.goarch,
|
||||||
};
|
};
|
||||||
this.showAgentDownload = true;
|
this.showAgentDownload = true;
|
||||||
});
|
});
|
||||||
@@ -289,7 +296,7 @@ export default {
|
|||||||
});
|
});
|
||||||
} else if (
|
} else if (
|
||||||
this.installMethod === "powershell" ||
|
this.installMethod === "powershell" ||
|
||||||
this.installMethod === "linux"
|
this.installMethod === "bash"
|
||||||
) {
|
) {
|
||||||
this.$q.loading.show();
|
this.$q.loading.show();
|
||||||
let ext = this.installMethod === "powershell" ? "ps1" : "sh";
|
let ext = this.installMethod === "powershell" ? "ps1" : "sh";
|
||||||
@@ -333,7 +340,7 @@ export default {
|
|||||||
case "manual":
|
case "manual":
|
||||||
text = "Show manual installation instructions";
|
text = "Show manual installation instructions";
|
||||||
break;
|
break;
|
||||||
case "linux":
|
case "bash":
|
||||||
text = "Download linux install script";
|
text = "Download linux install script";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,37 +129,37 @@
|
|||||||
<div class="q-gutter-sm">
|
<div class="q-gutter-sm">
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
v-model="winupdatepolicy.run_time_days"
|
v-model="winupdatepolicy.run_time_days"
|
||||||
:val="1"
|
:val="0"
|
||||||
label="Monday"
|
label="Monday"
|
||||||
/>
|
/>
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
v-model="winupdatepolicy.run_time_days"
|
v-model="winupdatepolicy.run_time_days"
|
||||||
:val="2"
|
:val="1"
|
||||||
label="Tuesday"
|
label="Tuesday"
|
||||||
/>
|
/>
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
v-model="winupdatepolicy.run_time_days"
|
v-model="winupdatepolicy.run_time_days"
|
||||||
:val="3"
|
:val="2"
|
||||||
label="Wednesday"
|
label="Wednesday"
|
||||||
/>
|
/>
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
v-model="winupdatepolicy.run_time_days"
|
v-model="winupdatepolicy.run_time_days"
|
||||||
:val="4"
|
:val="3"
|
||||||
label="Thursday"
|
label="Thursday"
|
||||||
/>
|
/>
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
v-model="winupdatepolicy.run_time_days"
|
v-model="winupdatepolicy.run_time_days"
|
||||||
:val="5"
|
:val="4"
|
||||||
label="Friday"
|
label="Friday"
|
||||||
/>
|
/>
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
v-model="winupdatepolicy.run_time_days"
|
v-model="winupdatepolicy.run_time_days"
|
||||||
:val="6"
|
:val="5"
|
||||||
label="Saturday"
|
label="Saturday"
|
||||||
/>
|
/>
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
v-model="winupdatepolicy.run_time_days"
|
v-model="winupdatepolicy.run_time_days"
|
||||||
:val="0"
|
:val="6"
|
||||||
label="Sunday"
|
label="Sunday"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,11 +63,14 @@ export default {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await scheduleAgentReboot(props.agent.agent_id, state.value);
|
const ret = await scheduleAgentReboot(
|
||||||
|
props.agent.agent_id,
|
||||||
|
state.value
|
||||||
|
);
|
||||||
$q.dialog({
|
$q.dialog({
|
||||||
title: "Reboot pending",
|
title: "Reboot pending",
|
||||||
style: "width: 40vw",
|
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.`,
|
<br />It can be cancelled from the Pending Actions menu until the scheduled time.`,
|
||||||
html: true,
|
html: true,
|
||||||
}).onDismiss(onDialogOK);
|
}).onDismiss(onDialogOK);
|
||||||
|
|||||||
@@ -128,6 +128,11 @@
|
|||||||
/>
|
/>
|
||||||
<q-checkbox v-model="state.save_all_output" label="Save all output" />
|
<q-checkbox v-model="state.save_all_output" label="Save all output" />
|
||||||
</q-card-section>
|
</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-card-section>
|
||||||
<q-input
|
<q-input
|
||||||
v-model.number="state.timeout"
|
v-model.number="state.timeout"
|
||||||
@@ -173,6 +178,7 @@ import { useScriptDropdown } from "@/composables/scripts";
|
|||||||
import { useCustomFieldDropdown } from "@/composables/core";
|
import { useCustomFieldDropdown } from "@/composables/core";
|
||||||
import { runScript } from "@/api/agents";
|
import { runScript } from "@/api/agents";
|
||||||
import { notifySuccess } from "@/utils/notify";
|
import { notifySuccess } from "@/utils/notify";
|
||||||
|
import { runAsUserToolTip } from "@/constants/constants";
|
||||||
import {
|
import {
|
||||||
formatScriptSyntax,
|
formatScriptSyntax,
|
||||||
removeExtraOptionCategories,
|
removeExtraOptionCategories,
|
||||||
@@ -220,6 +226,7 @@ export default {
|
|||||||
script,
|
script,
|
||||||
args: defaultArgs,
|
args: defaultArgs,
|
||||||
timeout: defaultTimeout,
|
timeout: defaultTimeout,
|
||||||
|
run_as_user: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ret = ref(null);
|
const ret = ref(null);
|
||||||
@@ -273,6 +280,7 @@ export default {
|
|||||||
|
|
||||||
// non-reactive data
|
// non-reactive data
|
||||||
outputOptions,
|
outputOptions,
|
||||||
|
runAsUserToolTip,
|
||||||
|
|
||||||
//methods
|
//methods
|
||||||
formatScriptSyntax,
|
formatScriptSyntax,
|
||||||
|
|||||||
@@ -51,6 +51,11 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</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-card-section v-if="state.shell === 'custom'">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="state.custom_shell"
|
v-model="state.custom_shell"
|
||||||
@@ -117,6 +122,7 @@ import { ref } from "vue";
|
|||||||
import { useDialogPluginComponent } from "quasar";
|
import { useDialogPluginComponent } from "quasar";
|
||||||
import { sendAgentCommand } from "@/api/agents";
|
import { sendAgentCommand } from "@/api/agents";
|
||||||
import { cmdPlaceholder } from "@/composables/agents";
|
import { cmdPlaceholder } from "@/composables/agents";
|
||||||
|
import { runAsUserToolTip } from "@/constants/constants";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "SendCommand",
|
name: "SendCommand",
|
||||||
@@ -134,6 +140,7 @@ export default {
|
|||||||
cmd: null,
|
cmd: null,
|
||||||
timeout: 30,
|
timeout: 30,
|
||||||
custom_shell: null,
|
custom_shell: null,
|
||||||
|
run_as_user: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -156,6 +163,9 @@ export default {
|
|||||||
loading,
|
loading,
|
||||||
ret,
|
ret,
|
||||||
|
|
||||||
|
// non reactivete data
|
||||||
|
runAsUserToolTip,
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
submit,
|
submit,
|
||||||
cmdPlaceholder,
|
cmdPlaceholder,
|
||||||
|
|||||||
@@ -8,11 +8,12 @@
|
|||||||
</q-btn>
|
</q-btn>
|
||||||
</q-bar>
|
</q-bar>
|
||||||
<q-separator />
|
<q-separator />
|
||||||
<q-banner class="bg-warning">
|
<q-banner class="bg-info">
|
||||||
<template v-slot:avatar>
|
<template v-slot:avatar>
|
||||||
<q-icon name="info" />
|
<q-icon name="info" />
|
||||||
</template>
|
</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-banner>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
Select Version
|
Select Version
|
||||||
|
|||||||
211
src/components/modals/agents/WebsocketSendCommand.vue
Normal file
211
src/components/modals/agents/WebsocketSendCommand.vue
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<q-dialog
|
||||||
|
ref="dialogRef"
|
||||||
|
@hide="onDialogHide"
|
||||||
|
persistent
|
||||||
|
@keydown.esc="onDialogHide"
|
||||||
|
>
|
||||||
|
<q-card
|
||||||
|
class="q-dialog-plugin"
|
||||||
|
:style="{ 'min-width': !ret ? '40vw' : '70vw' }"
|
||||||
|
>
|
||||||
|
<q-bar>
|
||||||
|
Send command on {{ agent.hostname }}
|
||||||
|
<q-space />
|
||||||
|
<q-chip v-if="!wsConnected" color="red" text-color="white" icon="error"
|
||||||
|
>Websocket diconnected!</q-chip
|
||||||
|
>
|
||||||
|
<q-space />
|
||||||
|
<q-btn dense flat icon="close" v-close-popup>
|
||||||
|
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</q-bar>
|
||||||
|
<q-form @submit="submit">
|
||||||
|
<q-card-section>
|
||||||
|
<p>Shell</p>
|
||||||
|
<div class="q-gutter-sm">
|
||||||
|
<q-radio
|
||||||
|
v-if="agent.plat !== 'windows'"
|
||||||
|
dense
|
||||||
|
v-model="state.shell"
|
||||||
|
val="/bin/bash"
|
||||||
|
label="Bash"
|
||||||
|
@update:model-value="state.custom_shell = null"
|
||||||
|
/>
|
||||||
|
<q-radio
|
||||||
|
v-if="agent.plat !== 'windows'"
|
||||||
|
dense
|
||||||
|
v-model="state.shell"
|
||||||
|
val="custom"
|
||||||
|
label="Custom"
|
||||||
|
/>
|
||||||
|
<q-radio
|
||||||
|
v-if="agent.plat === 'windows'"
|
||||||
|
dense
|
||||||
|
v-model="state.shell"
|
||||||
|
val="cmd"
|
||||||
|
label="CMD"
|
||||||
|
/>
|
||||||
|
<q-radio
|
||||||
|
v-if="agent.plat === 'windows'"
|
||||||
|
dense
|
||||||
|
v-model="state.shell"
|
||||||
|
val="powershell"
|
||||||
|
label="Powershell"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section v-if="state.shell === 'custom'">
|
||||||
|
<q-input
|
||||||
|
v-model="state.custom_shell"
|
||||||
|
outlined
|
||||||
|
label="Custom shell"
|
||||||
|
stack-label
|
||||||
|
placeholder="/usr/bin/python3"
|
||||||
|
:rules="[(val) => !!val || '*Required']"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-input
|
||||||
|
v-model.number="state.timeout"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
type="number"
|
||||||
|
style="max-width: 150px"
|
||||||
|
label="Timeout (seconds)"
|
||||||
|
stack-label
|
||||||
|
:rules="[
|
||||||
|
(val) => !!val || '*Required',
|
||||||
|
(val) => val >= 10 || 'Minimum is 10 seconds',
|
||||||
|
(val) => val <= 3600 || 'Maximum is 3600 seconds',
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-input
|
||||||
|
v-model="state.cmd"
|
||||||
|
outlined
|
||||||
|
label="Command"
|
||||||
|
stack-label
|
||||||
|
:placeholder="cmdPlaceholder(state.shell)"
|
||||||
|
:rules="[(val) => !!val || '*Required']"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn flat dense push label="Cancel" v-close-popup />
|
||||||
|
<q-btn
|
||||||
|
:loading="loading"
|
||||||
|
:disable="!wsConnected"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
push
|
||||||
|
label="Send"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
<q-card-section
|
||||||
|
v-if="ret !== null"
|
||||||
|
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
|
||||||
|
style="max-height: 50vh"
|
||||||
|
>
|
||||||
|
<pre>{{ ret }}</pre>
|
||||||
|
</q-card-section>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// composition imports
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||||
|
import { useStore } from "vuex";
|
||||||
|
import { useDialogPluginComponent } from "quasar";
|
||||||
|
import { cmdPlaceholder } from "@/composables/agents";
|
||||||
|
import { getWSUrl } from "@/websocket/channels";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "SendCommand",
|
||||||
|
emits: [...useDialogPluginComponent.emits],
|
||||||
|
props: {
|
||||||
|
agent: !Object,
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const store = useStore();
|
||||||
|
// setup quasar dialog plugin
|
||||||
|
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||||
|
|
||||||
|
// run command logic
|
||||||
|
const state = ref({
|
||||||
|
shell: props.agent.plat === "windows" ? "cmd" : "/bin/bash",
|
||||||
|
cmd: null,
|
||||||
|
timeout: 30,
|
||||||
|
custom_shell: null,
|
||||||
|
agent_id: props.agent.agent_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const ret = ref(null);
|
||||||
|
|
||||||
|
// websocket
|
||||||
|
const ws = ref(null);
|
||||||
|
const wsConnected = ref(false);
|
||||||
|
|
||||||
|
function setupWS() {
|
||||||
|
const token = computed(() => store.state.token);
|
||||||
|
console.log("Starting send command websocket");
|
||||||
|
let url = getWSUrl("sendcmd", token.value);
|
||||||
|
ws.value = new WebSocket(url);
|
||||||
|
ws.value.onopen = () => {
|
||||||
|
wsConnected.value = true;
|
||||||
|
console.log("Send command websocket connected");
|
||||||
|
};
|
||||||
|
ws.value.onmessage = (e) => {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
ret.value = data.ret;
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
ws.value.onclose = () => {
|
||||||
|
console.log("Send command websocket disconnected");
|
||||||
|
wsConnected.value = false;
|
||||||
|
};
|
||||||
|
ws.value.onerror = () => {
|
||||||
|
wsConnected.value = false;
|
||||||
|
console.log("Send command websocket error");
|
||||||
|
ws.value.onclose();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
ret.value = null;
|
||||||
|
loading.value = true;
|
||||||
|
ret.value = ws.value.send(JSON.stringify(state.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setupWS();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
ws.value.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
// reactive data
|
||||||
|
state,
|
||||||
|
loading,
|
||||||
|
ret,
|
||||||
|
wsConnected,
|
||||||
|
|
||||||
|
// methods
|
||||||
|
submit,
|
||||||
|
cmdPlaceholder,
|
||||||
|
|
||||||
|
// quasar dialog
|
||||||
|
dialogRef,
|
||||||
|
onDialogHide,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -12,11 +12,15 @@
|
|||||||
color="positive"
|
color="positive"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
@click="doCodeSign"
|
@click="doCodeSign"
|
||||||
|
:loading="loading"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
>Force all existing agents to be updated to the code-signed
|
>Force all existing agents to be updated to the code-signed
|
||||||
version</q-tooltip
|
version</q-tooltip
|
||||||
>
|
>
|
||||||
|
<template v-slot:loading>
|
||||||
|
<q-spinner-facebook />
|
||||||
|
</template>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-form @submit.prevent="editToken">
|
<q-form @submit.prevent="editToken">
|
||||||
@@ -33,56 +37,92 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section class="row items-center">
|
<q-card-section class="row items-center">
|
||||||
<q-btn label="Save" color="primary" type="submit" />
|
<q-btn label="Save" color="primary" type="submit" />
|
||||||
|
<q-space />
|
||||||
|
<q-btn label="Delete" color="negative" @click="confirmDelete" />
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card>
|
</q-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<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 {
|
export default {
|
||||||
name: "CodeSign",
|
name: "CodeSign",
|
||||||
mixins: [mixins],
|
setup() {
|
||||||
data() {
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
settings: {
|
settings,
|
||||||
token: "",
|
loading,
|
||||||
},
|
confirmDelete,
|
||||||
|
doCodeSign,
|
||||||
|
editToken,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
getToken() {
|
|
||||||
this.$axios.get("/core/codesign/").then((r) => {
|
|
||||||
this.settings = r.data;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
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();
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -128,6 +128,18 @@
|
|||||||
:rules="[(val) => val >= 5 || 'Minimum is 5']"
|
:rules="[(val) => val >= 5 || 'Minimum is 5']"
|
||||||
hide-bottom-space
|
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. Not supported on Windows Server.
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
<q-input
|
<q-input
|
||||||
label="Syntax"
|
label="Syntax"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
@@ -253,6 +265,7 @@ export default {
|
|||||||
default_timeout: 90,
|
default_timeout: 90,
|
||||||
args: [],
|
args: [],
|
||||||
script_body: "",
|
script_body: "",
|
||||||
|
run_as_user: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (props.clone) script.value.name = `(Copy) ${script.value.name}`;
|
if (props.clone) script.value.name = `(Copy) ${script.value.name}`;
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export default {
|
|||||||
timeout: props.script.default_timeout,
|
timeout: props.script.default_timeout,
|
||||||
args: props.script.args,
|
args: props.script.args,
|
||||||
shell: props.script.shell,
|
shell: props.script.shell,
|
||||||
|
run_as_user: props.script.run_as_user,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
ret.value = await testScript(props.agent, data);
|
ret.value = await testScript(props.agent, data);
|
||||||
|
|||||||
@@ -991,10 +991,16 @@ export default {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
// remove milliseconds and Z to work with native date input
|
// 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)
|
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
|
// set task type if monthlydow is being used
|
||||||
if (task.value.task_type === "monthlydow") {
|
if (task.value.task_type === "monthlydow") {
|
||||||
|
|||||||
15
src/constants/constants.ts
Normal file
15
src/constants/constants.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const GOARCH_AMD64 = "amd64";
|
||||||
|
const GOARCH_i386 = "386";
|
||||||
|
const GOARCH_ARM64 = "arm64";
|
||||||
|
const GOARCH_ARM32 = "arm";
|
||||||
|
|
||||||
|
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. Not supported on Windows Server.";
|
||||||
|
|
||||||
|
export {
|
||||||
|
GOARCH_AMD64,
|
||||||
|
GOARCH_i386,
|
||||||
|
GOARCH_ARM64,
|
||||||
|
GOARCH_ARM32,
|
||||||
|
runAsUserToolTip,
|
||||||
|
};
|
||||||
@@ -35,12 +35,7 @@
|
|||||||
Tactical RMM<span class="text-overline q-ml-sm"
|
Tactical RMM<span class="text-overline q-ml-sm"
|
||||||
>v{{ currentTRMMVersion }}</span
|
>v{{ currentTRMMVersion }}</span
|
||||||
>
|
>
|
||||||
<span
|
<span class="text-overline q-ml-md" v-if="updateAvailable()"
|
||||||
class="text-overline q-ml-md"
|
|
||||||
v-if="
|
|
||||||
latestTRMMVersion !== 'error' &&
|
|
||||||
currentTRMMVersion !== latestTRMMVersion
|
|
||||||
"
|
|
||||||
><q-badge color="warning"
|
><q-badge color="warning"
|
||||||
><a :href="latestReleaseURL" target="_blank"
|
><a :href="latestReleaseURL" target="_blank"
|
||||||
>v{{ latestTRMMVersion }} available</a
|
>v{{ latestTRMMVersion }} available</a
|
||||||
@@ -144,7 +139,7 @@ import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
|||||||
import { useQuasar } from "quasar";
|
import { useQuasar } from "quasar";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { getBaseUrl } from "@/boot/axios";
|
import { getWSUrl } from "@/websocket/channels";
|
||||||
|
|
||||||
// ui imports
|
// ui imports
|
||||||
import AlertsIcon from "@/components/AlertsIcon.vue";
|
import AlertsIcon from "@/components/AlertsIcon.vue";
|
||||||
@@ -171,6 +166,7 @@ export default {
|
|||||||
const latestTRMMVersion = computed(() => store.state.latestTRMMVersion);
|
const latestTRMMVersion = computed(() => store.state.latestTRMMVersion);
|
||||||
const needRefresh = computed(() => store.state.needrefresh);
|
const needRefresh = computed(() => store.state.needrefresh);
|
||||||
const user = computed(() => store.state.username);
|
const user = computed(() => store.state.username);
|
||||||
|
const hosted = computed(() => store.state.hosted);
|
||||||
|
|
||||||
const latestReleaseURL = computed(() => {
|
const latestReleaseURL = computed(() => {
|
||||||
return latestTRMMVersion.value
|
return latestTRMMVersion.value
|
||||||
@@ -184,10 +180,6 @@ export default {
|
|||||||
}).onOk(() => store.dispatch("getDashInfo"));
|
}).onOk(() => store.dispatch("getDashInfo"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function wsUrl() {
|
|
||||||
return getBaseUrl().split("://")[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverCount = ref(0);
|
const serverCount = ref(0);
|
||||||
const serverOfflineCount = ref(0);
|
const serverOfflineCount = ref(0);
|
||||||
const workstationCount = ref(0);
|
const workstationCount = ref(0);
|
||||||
@@ -200,13 +192,8 @@ export default {
|
|||||||
// when ws is closed causing ws to connect with expired token
|
// when ws is closed causing ws to connect with expired token
|
||||||
const token = computed(() => store.state.token);
|
const token = computed(() => store.state.token);
|
||||||
console.log("Starting websocket");
|
console.log("Starting websocket");
|
||||||
const proto =
|
let url = getWSUrl("dashinfo", token.value);
|
||||||
process.env.NODE_ENV === "production" || process.env.DOCKER_BUILD
|
ws.value = new WebSocket(url);
|
||||||
? "wss"
|
|
||||||
: "ws";
|
|
||||||
ws.value = new WebSocket(
|
|
||||||
`${proto}://${wsUrl()}/ws/dashinfo/?access_token=${token.value}`
|
|
||||||
);
|
|
||||||
ws.value.onopen = () => {
|
ws.value.onopen = () => {
|
||||||
console.log("Connected to ws");
|
console.log("Connected to ws");
|
||||||
};
|
};
|
||||||
@@ -242,6 +229,11 @@ export default {
|
|||||||
}, 60 * 5 * 1000);
|
}, 60 * 5 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateAvailable() {
|
||||||
|
if (latestTRMMVersion.value === "error" || hosted.value) return false;
|
||||||
|
return currentTRMMVersion.value !== latestTRMMVersion.value;
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setupWS();
|
setupWS();
|
||||||
store.dispatch("getDashInfo");
|
store.dispatch("getDashInfo");
|
||||||
@@ -270,6 +262,7 @@ export default {
|
|||||||
|
|
||||||
// methods
|
// methods
|
||||||
showUserPreferences,
|
showUserPreferences,
|
||||||
|
updateAvailable,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ export function formatDateInputField(isoDateString, noTimezone = false) {
|
|||||||
if (noTimezone) {
|
if (noTimezone) {
|
||||||
isoDateString = isoDateString.replace("Z", "");
|
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
|
// converts a local date string "YYYY-MM-DDTHH:mm:ss" to an iso date string with the local timezone
|
||||||
|
|||||||
11
src/websocket/channels.js
Normal file
11
src/websocket/channels.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { getBaseUrl } from "@/boot/axios";
|
||||||
|
|
||||||
|
export function getWSUrl(path, token) {
|
||||||
|
const url = getBaseUrl().split("://")[1];
|
||||||
|
|
||||||
|
const proto =
|
||||||
|
process.env.NODE_ENV === "production" || process.env.DOCKER_BUILD
|
||||||
|
? "wss"
|
||||||
|
: "ws";
|
||||||
|
return `${proto}://${url}/ws/${path}/?access_token=${token}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user