Compare commits

...

44 Commits

Author SHA1 Message Date
wh1te909
d270b877c9 Release 0.100.9 2022-08-23 05:04:57 +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
fd8b2a1d98 Release 0.100.8 2022-08-09 20:40:48 +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
f518043d8d Release 0.100.7 2022-08-01 17:36:11 +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
cc2335558d Release 0.100.6 2022-07-27 06:15:49 +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
a8a171ba2c Release 0.100.5 2022-07-10 00:00:08 +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
24a63f477e Release 0.100.4 2022-07-07 16:38: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
wh1te909
ddeb6293a1 init 2022-05-17 20:46:22 +00:00
30 changed files with 1638 additions and 1151 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

2018
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.100.9",
"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",
"@quasar/extras": "1.15.1",
"apexcharts": "3.35.4",
"axios": "0.27.2",
"dotenv": "16.0.0",
"dotenv": "16.0.1",
"qrcode.vue": "3.3.3",
"quasar": "2.7.1",
"vue": "3.2.31",
"quasar": "2.7.7",
"vue": "3.2.37",
"vue3-ace-editor": "2.2.2",
"vue3-apexcharts": "1.4.1",
"vuedraggable": "4.1.0",
"vue-router": "4.0.15",
"vue-router": "4.1.3",
"vuex": "4.0.2"
},
"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",
"@intlify/vite-plugin-vue-i18n": "^6.0.0",
"@quasar/app-vite": "^1.0.6",
"@types/node": "^18.6.5",
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"autoprefixer": "^10.4.7",
"eslint": "^8.21.0",
"eslint-config-prettier": "^8.5.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"
"prettier": "^2.7.1",
"typescript": "^4.7.4"
}
}
}

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

@@ -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

@@ -356,6 +356,27 @@ export default {
},
methods: {
filterTable(rows, terms, cols, cellValue) {
const hiddenFields = [
"version",
"operating_system",
"public_ip",
"cpu_model",
"graphics",
"local_ips",
"make_model",
"physical_disks",
];
// quasar filter only does visible columns so this is a hack to add hidden columns we want to filter
for (const elem of hiddenFields) {
if (!cols.find((o) => o.name === elem)) {
cols.push({
name: elem,
field: elem,
});
}
}
const lowerTerms = terms ? terms.toLowerCase() : "";
let advancedFilter = false;
let availability = null;

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

@@ -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,17 @@ 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

@@ -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

@@ -135,6 +135,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 +208,7 @@ import { runBulkAction } from "@/api/agents";
import { notifySuccess } from "@/utils/notify";
import { cmdPlaceholder } from "@/composables/agents";
import { removeExtraOptionCategories } from "@/utils/format";
import { runAsUserToolTip } from "@/constants/constants";
// ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
@@ -300,6 +306,7 @@ export default {
script,
timeout: defaultTimeout,
args: defaultArgs,
run_as_user: false,
});
const loading = ref(false);
@@ -316,6 +323,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 +345,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 +402,7 @@ export default {
osTypeOptions,
targetOptions,
patchModeOptions,
runAsUserToolTip,
//computed
modalTitle,
@@ -394,6 +410,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,8 @@
val="linux"
label="Linux"
@update:model-value="
installMethod = 'linux';
arch = 'amd64';
installMethod = 'bash';
goarch = GOARCH_AMD64;
"
/>
</div>
@@ -102,38 +102,38 @@
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'"
/>
<q-radio
v-model="arch"
val="32"
v-model="goarch"
:val="GOARCH_i386"
label="32 bit"
v-show="agentOS === 'windows'"
/>
<q-radio
v-model="arch"
val="amd64"
v-model="goarch"
:val="GOARCH_AMD64"
label="64 bit"
v-show="agentOS !== 'windows'"
/>
<q-radio
v-model="arch"
val="386"
v-model="goarch"
:val="GOARCH_i386"
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'"
/>
<q-radio
v-model="arch"
val="arm"
v-model="goarch"
:val="GOARCH_ARM32"
label="ARM 32 bit (Rasp Pi)"
v-show="agentOS !== 'windows'"
/>
@@ -177,6 +177,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 +193,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 +208,7 @@ export default {
showAgentDownload: false,
info: {},
installMethod: "exe",
arch: "64",
goarch: GOARCH_AMD64,
agentOS: "windows",
};
},
@@ -239,10 +249,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,10 +260,10 @@ 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") {
@@ -264,7 +271,7 @@ export default {
this.info = {
expires: this.expires,
data: r.data,
arch: this.arch,
goarch: this.goarch,
};
this.showAgentDownload = true;
});
@@ -289,7 +296,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,7 +340,7 @@ export default {
case "manual":
text = "Show manual installation instructions";
break;
case "linux":
case "bash":
text = "Download linux install script";
break;
}

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

@@ -128,6 +128,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 +178,7 @@ import { useScriptDropdown } from "@/composables/scripts";
import { useCustomFieldDropdown } from "@/composables/core";
import { runScript } from "@/api/agents";
import { notifySuccess } from "@/utils/notify";
import { runAsUserToolTip } from "@/constants/constants";
import {
formatScriptSyntax,
removeExtraOptionCategories,
@@ -220,6 +226,7 @@ export default {
script,
args: defaultArgs,
timeout: defaultTimeout,
run_as_user: false,
});
const ret = ref(null);
@@ -273,6 +280,7 @@ export default {
// non-reactive data
outputOptions,
runAsUserToolTip,
//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

@@ -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

@@ -128,6 +128,18 @@
:rules="[(val) => val >= 5 || 'Minimum is 5']"
hide-bottom-space
/>
<q-checkbox
v-model="formScript.run_as_user"
label="Run As User (Windows only)"
>
<q-tooltip
>Setting this value on the script model will always override any
'Run As User' checkboxes in the UI and force this script to
always be run in the context of the logged in user. If no user
is logged in, the script will not run and an error will be
returned. Not supported on Windows Server.
</q-tooltip>
</q-checkbox>
<q-input
label="Syntax"
type="textarea"
@@ -253,6 +265,7 @@ export default {
default_timeout: 90,
args: [],
script_body: "",
run_as_user: false,
});
if (props.clone) script.value.name = `(Copy) ${script.value.name}`;

View File

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

View File

@@ -991,10 +991,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") {

View 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,
};

View File

@@ -35,12 +35,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
@@ -144,7 +139,7 @@ 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";
// ui imports
import AlertsIcon from "@/components/AlertsIcon.vue";
@@ -171,6 +166,7 @@ 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 latestReleaseURL = computed(() => {
return latestTRMMVersion.value
@@ -184,10 +180,6 @@ export default {
}).onOk(() => store.dispatch("getDashInfo"));
}
function wsUrl() {
return getBaseUrl().split("://")[1];
}
const serverCount = ref(0);
const serverOfflineCount = ref(0);
const workstationCount = ref(0);
@@ -200,13 +192,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 +229,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");
@@ -270,6 +262,7 @@ export default {
// methods
showUserPreferences,
updateAvailable,
};
},
};

View File

@@ -285,7 +285,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

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}`;
}