Compare commits

..

31 Commits

Author SHA1 Message Date
wh1te909
e837c494cb Release 0.101.43 2024-03-25 17:38:24 +00:00
wh1te909
13f0f117da Release 0.101.40 2024-02-03 01:44:41 +00:00
wh1te909
0b6ae80777 Release 0.101.39 2024-02-02 00:55:23 +00:00
wh1te909
cfe1cb2dbf Release 0.101.38 2023-12-22 17:50:19 +00:00
wh1te909
e1dc8050e3 Release 0.101.37 2023-12-01 18:55:37 +00:00
wh1te909
3e6365574e Release 0.101.36 2023-11-23 00:03:20 +00:00
wh1te909
5114ff40aa Release 0.101.35 2023-11-07 17:25:24 +00:00
wh1te909
6ea7c92b20 Release 0.101.34 2023-10-31 17:51:19 +00:00
wh1te909
20d534eab0 Release 0.101.31 2023-10-01 17:36:52 +00:00
wh1te909
1b2286c4f8 Release 0.101.30 2023-09-30 21:59:09 +00:00
wh1te909
8207f30234 Release 0.101.29 2023-08-30 04:10:11 +00:00
wh1te909
68036f6837 Release 0.101.28 2023-08-14 06:39:49 +00:00
wh1te909
03fae45ac5 Release 0.101.25 2023-07-04 18:49:46 +00:00
wh1te909
c2591c9e7d Release 0.101.22 2023-05-30 22:11:30 +00:00
wh1te909
7fcbe6fbd8 Release 0.101.20 2023-05-09 21:09:45 +00:00
wh1te909
a2f472ef9c Release 0.101.18 2023-04-09 03:28:23 +00:00
wh1te909
8403ac0e93 Release 0.101.16 2023-03-22 17:00:29 +00:00
wh1te909
b7a91563b0 Release 0.101.13 2023-01-18 20:05:20 +00:00
wh1te909
ab19afca16 Release 0.101.11 2022-12-21 18:44:46 +00:00
wh1te909
f24c6a7a80 Release 0.101.9 2022-12-04 23:01:59 +00:00
wh1te909
99490bf859 Release 0.101.7 2022-11-13 01:20:33 +00:00
wh1te909
72cdeeaa6a Release 0.101.5 2022-10-25 22:02:34 +00:00
wh1te909
1eca4d605b Release 0.101.3 2022-10-19 22:35:54 +00:00
wh1te909
52ee98f6f8 Release 0.101.0 2022-09-24 02:43:53 +00:00
wh1te909
d270b877c9 Release 0.100.9 2022-08-23 05:04:57 +00:00
wh1te909
fd8b2a1d98 Release 0.100.8 2022-08-09 20:40:48 +00:00
wh1te909
f518043d8d Release 0.100.7 2022-08-01 17:36:11 +00:00
wh1te909
cc2335558d Release 0.100.6 2022-07-27 06:15:49 +00:00
wh1te909
a8a171ba2c Release 0.100.5 2022-07-10 00:00:08 +00:00
wh1te909
24a63f477e Release 0.100.4 2022-07-07 16:38:14 +00:00
wh1te909
ddeb6293a1 init 2022-05-17 20:46:22 +00:00
93 changed files with 2774 additions and 7164 deletions

View File

@@ -3,9 +3,10 @@ version: '3.7'
services:
app-dev:
container_name: trmm-app-dev
image: node:20-alpine
image: node:18-alpine
restart: always
command: /bin/sh -c "npm install --cache ~/.npm && npm i -g @quasar/cli && npm run serve"
command: /bin/sh -c "npm install --cache ~/.npm && npm run serve"
user: 1000:1000
working_dir: /workspace/web
volumes:
- ..:/workspace:cached

View File

@@ -15,7 +15,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: "20.18.0"
node-version: "20.11.1"
- run: touch env-config.js

View File

@@ -9,11 +9,11 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: "20.18.0"
node-version: 18
- run: npm install
- name: Run Prettier formatting

3199
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.101.50",
"version": "0.101.43",
"private": true,
"productName": "Tactical RMM",
"scripts": {
@@ -10,38 +10,37 @@
"format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
},
"dependencies": {
"@quasar/extras": "1.16.13",
"@vueuse/core": "11.2.0",
"@vueuse/integrations": "11.2.0",
"@vueuse/shared": "11.2.0",
"apexcharts": "3.54.1",
"axios": "1.7.7",
"@quasar/extras": "1.16.9",
"apexcharts": "3.48.0",
"axios": "1.6.8",
"dotenv": "16.4.5",
"monaco-editor": "0.50.0",
"pinia": "2.2.6",
"qrcode": "1.5.4",
"quasar": "2.17.2",
"vue": "3.5.12",
"vue-router": "4.4.5",
"vue3-apexcharts": "1.7.0",
"pinia": "^2.1.7",
"qrcode.vue": "3.4.1",
"quasar": "2.15.1",
"vue": "3.4.21",
"vue3-apexcharts": "1.5.2",
"vuedraggable": "4.1.0",
"vue-router": "4.3.0",
"@vueuse/core": "10.9.0",
"@vueuse/shared": "10.9.0",
"monaco-editor": "0.47.0",
"vuex": "4.1.0",
"@xterm/xterm": "5.5.0",
"@xterm/addon-fit": "0.10.0",
"yaml": "2.6.0"
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
"yaml": "2.4.1"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "4.0.0",
"@quasar/app-vite": "1.10.2",
"@quasar/cli": "2.4.1",
"@types/node": "22.7.5",
"@typescript-eslint/eslint-plugin": "7.16.0",
"@typescript-eslint/parser": "7.16.0",
"autoprefixer": "10.4.20",
"@quasar/cli": "2.4.0",
"@intlify/unplugin-vue-i18n": "3.0.1",
"@quasar/app-vite": "1.8.0",
"@types/node": "20.11.30",
"@typescript-eslint/eslint-plugin": "7.3.1",
"@typescript-eslint/parser": "7.3.1",
"autoprefixer": "10.4.18",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-vue": "8.7.1",
"prettier": "3.3.3",
"typescript": "5.6.2"
"prettier": "3.2.5",
"typescript": "5.4.3"
}
}

View File

@@ -8,7 +8,6 @@
// Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
const { mergeConfig } = require("vite");
const { configure } = require("quasar/wrappers");
const path = require("path");
require("dotenv").config();
@@ -30,7 +29,7 @@ module.exports = configure(function (/* ctx */) {
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: ["pinia", "axios", "monaco", "integrations"],
boot: ["axios", "monaco", "integrations"],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ["app.sass"],
@@ -38,7 +37,7 @@ module.exports = configure(function (/* ctx */) {
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
"ionicons-v4",
"mdi-v7",
"mdi-v5",
"fontawesome-v6",
// 'eva-icons',
// 'themify',
@@ -79,22 +78,9 @@ module.exports = configure(function (/* ctx */) {
// polyfillModulePreload: true,
distDir: "dist/",
/* eslint-disable quotes */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
extendViteConf(viteConf, { isServer, isClient }) {
viteConf.build = mergeConfig(viteConf.build, {
chunkSizeWarningLimit: 1600,
rollupOptions: {
output: {
entryFileNames: `[hash].js`,
chunkFileNames: `[hash].js`,
assetFileNames: `[hash].[ext]`,
},
},
});
},
/* eslint-enable quotes */
// extendViteConf (viteConf) {},
// viteVuePluginOptions: {},
// vitePlugins: []
},

View File

@@ -31,34 +31,6 @@ export async function resetTwoFactor() {
}
}
// sessions api
export async function fetchUserSessions(id) {
try {
const { data } = await axios.get(`${baseUrl}/users/${id}/sessions/`);
return data;
} catch (e) {
console.error(e);
}
}
export async function deleteAllUserSessions(id) {
try {
const { data } = await axios.delete(`${baseUrl}/users/${id}/sessions/`);
return data;
} catch (e) {
console.error(e);
}
}
export async function deleteUserSession(id) {
try {
const { data } = await axios.delete(`${baseUrl}/sessions/${id}/`);
return data;
} catch (e) {
console.error(e);
}
}
// role api function
export async function fetchRoles(params = {}) {
try {

View File

@@ -34,7 +34,7 @@ export function openAgentWindow(agent_id) {
export function runRemoteBackground(agent_id, agentPlatform) {
const url = router.resolve(
`/remotebackground/${agent_id}?agentPlatform=${agentPlatform}`,
`/remotebackground/${agent_id}?agentPlatform=${agentPlatform}`
).href;
openURL(url, null, {
popup: true,
@@ -129,7 +129,7 @@ export async function refreshAgentWMI(agent_id) {
export async function runScript(agent_id, payload) {
const { data } = await axios.post(
`${baseUrl}/${agent_id}/runscript/`,
payload,
payload
);
return data;
}
@@ -153,7 +153,7 @@ export async function fetchAgentProcesses(agent_id, params = {}) {
export async function killAgentProcess(agent_id, pid, params = {}) {
const { data } = await axios.delete(
`${baseUrl}/${agent_id}/processes/${pid}/`,
{ params: params },
{ params: params }
);
return data;
}
@@ -162,7 +162,7 @@ export async function fetchAgentEventLog(agent_id, logType, days, params = {}) {
try {
const { data } = await axios.get(
`${baseUrl}/${agent_id}/eventlog/${logType}/${days}/`,
{ params: params },
{ params: params }
);
return data;
} catch (e) {
@@ -199,7 +199,7 @@ export async function agentShutdown(agent_id) {
export async function sendAgentRecoverMesh(agent_id, params = {}) {
const { data } = await axios.post(
`${baseUrl}/${agent_id}/meshcentral/recover/`,
{ params: params },
{ params: params }
);
return data;
}

View File

@@ -1,13 +0,0 @@
import axios from "axios";
import type { AlertTemplate } from "@/types/alerts";
export async function saveAlertTemplate(id: number, payload: AlertTemplate) {
const { data } = await axios.put(`alerts/templates/${id}/`, payload);
return data;
}
export async function addAlertTemplate(payload: AlertTemplate) {
const { data } = await axios.post("alerts/templates/", payload);
return data;
}

45
src/api/core.js Normal file
View File

@@ -0,0 +1,45 @@
import axios from "axios";
import { openURL } from "quasar";
const baseUrl = "/core";
export async function fetchCustomFields(params = {}) {
try {
const { data } = await axios.get(`${baseUrl}/customfields/`, {
params: params,
});
return data;
} catch (e) {
console.error(e);
}
}
export async function fetchDashboardInfo(params = {}) {
const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params });
return data;
}
export async function fetchURLActions(params = {}) {
try {
const { data } = await axios.get(`${baseUrl}/urlaction/`, {
params: params,
});
return data;
} catch (e) {
console.error(e);
}
}
export async function runURLAction(payload) {
try {
const { data } = await axios.patch(`${baseUrl}/urlaction/run/`, payload);
openURL(data);
} catch (e) {
console.error(e);
}
}
export async function generateScript(payload) {
const { data } = await axios.post(`${baseUrl}/openai/generate/`, payload);
return data;
}

View File

@@ -1,104 +0,0 @@
import axios from "axios";
import { openURL } from "quasar";
import { router } from "@/router";
import type {
URLAction,
TestRunURLActionRequest,
TestRunURLActionResponse,
} from "@/types/core/urlactions";
import type { CoreSetting } from "@/types/core/settings";
const baseUrl = "/core";
export async function fetchCoreSettings(params = {}): Promise<CoreSetting> {
const { data } = await axios.get("/core/settings/", { params: params });
return data;
}
export async function fetchDashboardInfo(params = {}) {
const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params });
return data;
}
export async function fetchCustomFields(params = {}) {
try {
const { data } = await axios.get(`${baseUrl}/customfields/`, {
params: params,
});
return data;
} catch (e) {
console.error(e);
}
}
export async function fetchURLActions(params = {}): Promise<URLAction[]> {
const { data } = await axios.get(`${baseUrl}/urlaction/`, {
params: params,
});
return data;
}
export async function saveURLAction(action: URLAction) {
const { data } = await axios.post(`${baseUrl}/urlaction/`, action);
return data;
}
export async function editURLAction(id: number, action: URLAction) {
const { data } = await axios.put(`${baseUrl}/urlaction/${id}/`, action);
return data;
}
export async function removeURLAction(id: number) {
const { data } = await axios.delete(`${baseUrl}/urlaction/${id}/`);
return data;
}
interface RunURLActionRequest {
agent_id?: string;
client?: number;
site?: number;
action: number;
}
export async function runURLAction(payload: RunURLActionRequest) {
const { data } = await axios.patch(`${baseUrl}/urlaction/run/`, payload);
openURL(data);
}
export async function runTestURLAction(
payload: TestRunURLActionRequest,
): Promise<TestRunURLActionResponse> {
const { data } = await axios.post(`${baseUrl}/urlaction/run/test/`, payload);
return data;
}
export async function checkWebTermPerms(): Promise<{
message: string;
status: number;
}> {
const ret = await axios.post(`${baseUrl}/webtermperms/`);
return { message: ret.data, status: ret.status };
}
export function openWebTerminal(): void {
const url: string = router.resolve("/webterm").href;
openURL(url, undefined, {
popup: true,
scrollbars: false,
location: false,
status: false,
toolbar: false,
menubar: false,
width: 1280,
height: 720,
});
}
// TODO: Build out type for openai payload
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function generateScript(payload: any) {
const { data } = await axios.post(`${baseUrl}/openai/generate/`, payload);
return data;
}

View File

@@ -13,11 +13,6 @@ export async function testScript(agent_id, payload) {
return data;
}
export async function testScriptOnServer(payload) {
const { data } = await axios.post("core/serverscript/test/", payload);
return data;
}
export async function saveScript(payload) {
const { data } = await axios.post(`${baseUrl}/`, payload);
return data;
@@ -61,7 +56,7 @@ export async function fetchScriptSnippet(id, params = {}) {
export async function editScriptSnippet(payload) {
const { data } = await axios.put(
`${baseUrl}/snippets/${payload.id}/`,
payload,
payload
);
return data;
}

View File

@@ -1,5 +1,4 @@
import axios from "axios";
import { useAuthStore } from "@/stores/auth";
import { Notify } from "quasar";
export const getBaseUrl = () => {
@@ -19,23 +18,27 @@ export function setErrorMessage(data, message) {
];
}
export default function ({ app, router }) {
export default function ({ app, router, store }) {
app.config.globalProperties.$axios = axios;
axios.defaults.withCredentials = true;
axios.interceptors.request.use(
function (config) {
const auth = useAuthStore();
config.baseURL = getBaseUrl();
const token = auth.token;
const token = store.state.token;
if (token != null) {
config.headers.Authorization = `Token ${token}`;
}
// config.transformResponse = [
// function (data) {
// console.log(data);
// return data;
// },
// ];
return config;
},
function (err) {
return Promise.reject(err);
},
}
);
axios.interceptors.response.use(
@@ -66,20 +69,12 @@ export default function ({ app, router }) {
// perms
else if (error.response.status === 403) {
// don't notify user if method is GET
if (
error.config.method === "get" ||
error.config.method === "patch" ||
error.config.url === "accounts/ssoproviders/token/"
)
if (error.config.method === "get" || error.config.method === "patch")
return Promise.reject({ ...error });
text = error.response.data.detail;
}
// catch all for other 400 error messages
else if (
error.response.status >= 400 &&
error.response.status < 500 &&
error.response.status !== 423
) {
else if (error.response.status >= 400 && error.response.status < 500) {
if (error.config.responseType === "blob") {
text = (await error.response.data.text()).replace(/^"|"$/g, "");
} else if (error.response.data.non_field_errors) {
@@ -94,7 +89,7 @@ export default function ({ app, router }) {
}
}
if ((text || error.response) && error.response.status !== 423) {
if (text || error.response) {
Notify.create({
color: "negative",
message: text ? text : "",
@@ -106,6 +101,6 @@ export default function ({ app, router }) {
}
return Promise.reject({ ...error });
},
}
);
}

View File

@@ -1,11 +0,0 @@
import { boot } from "quasar/wrappers";
import { createPinia } from "pinia";
export default boot(({ app }) => {
const pinia = createPinia();
app.use(pinia);
// You can add Pinia plugins here
// pinia.use(SomePiniaPlugin)
});

View File

@@ -1,202 +1,157 @@
<template>
<q-card style="width: 65vw; max-width: 70vw; min-height: 50vh">
<q-bar>
<q-btn
ref="refresh"
@click="getUsers"
class="q-mr-sm"
dense
flat
push
icon="refresh"
/>User Administration
<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>
<div class="q-pa-md">
<div class="q-gutter-sm">
<div style="width: 900px; max-width: 90vw">
<q-card>
<q-bar>
<q-btn
ref="new"
label="New"
ref="refresh"
@click="getUsers"
class="q-mr-sm"
dense
flat
push
unelevated
no-caps
icon="add"
@click="showAddUserModal"
/>
</div>
<q-table
dense
:rows="users"
:columns="columns"
v-model:pagination="pagination"
row-key="id"
binary-state-sort
hide-pagination
virtual-scroll
>
<!-- header slots -->
<template v-slot:header-cell-is_active="props">
<q-th :props="props" auto-width>
<q-icon name="power_settings_new" size="1.5em">
<q-tooltip>Enable User</q-tooltip>
</q-icon>
</q-th>
</template>
icon="refresh"
/>User Administration
<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>
<div class="q-pa-md">
<div class="q-gutter-sm">
<q-btn
ref="new"
label="New"
dense
flat
push
unelevated
no-caps
icon="add"
@click="showAddUserModal"
/>
</div>
<q-table
dense
:rows="users"
:columns="columns"
v-model:pagination="pagination"
row-key="id"
binary-state-sort
hide-pagination
virtual-scroll
>
<!-- header slots -->
<template v-slot:header-cell-is_active="props">
<q-th :props="props" auto-width>
<q-icon name="power_settings_new" size="1.5em">
<q-tooltip>Enable User</q-tooltip>
</q-icon>
</q-th>
</template>
<template v-slot:header-cell-sso="props">
<q-th :props="props" auto-width></q-th>
</template>
<!-- No data Slot -->
<template v-slot:no-data>
<div class="full-width row flex-center q-gutter-sm">
<span v-if="users.length === 0">No Users</span>
</div>
</template>
<!-- No data Slot -->
<template v-slot:no-data>
<div class="full-width row flex-center q-gutter-sm">
<span v-if="users.length === 0">No Users</span>
</div>
</template>
<!-- body slots -->
<template v-slot:body="props">
<q-tr
:props="props"
class="cursor-pointer"
@dblclick="showEditUserModal(props.row)"
>
<!-- context menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item
clickable
v-close-popup
@click="showEditUserModal(props.row)"
>
<q-item-section side>
<q-icon name="edit" />
</q-item-section>
<q-item-section>Edit</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="deleteUser(props.row)"
:disable="props.row.username === logged_in_user"
>
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
<q-item-section>Delete</q-item-section>
</q-item>
<!-- body slots -->
<template v-slot:body="props">
<q-tr
:props="props"
class="cursor-pointer"
@dblclick="showEditUserModal(props.row)"
>
<!-- context menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item
clickable
v-close-popup
@click="showEditUserModal(props.row)"
>
<q-item-section side>
<q-icon name="edit" />
</q-item-section>
<q-item-section>Edit</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="deleteUser(props.row)"
<q-separator></q-separator>
<q-item
clickable
v-close-popup
@click="ResetPassword(props.row)"
id="context-reset"
>
<q-item-section side>
<q-icon name="autorenew" />
</q-item-section>
<q-item-section>Reset Password</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="reset2FA(props.row)"
id="context-reset"
>
<q-item-section side>
<q-icon name="autorenew" />
</q-item-section>
<q-item-section>Reset Two-Factor Auth</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
</q-menu>
<!-- enabled checkbox -->
<q-td>
<q-checkbox
dense
@update:model-value="toggleEnabled(props.row)"
v-model="props.row.is_active"
:disable="props.row.username === logged_in_user"
>
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
<q-item-section>Delete</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item
clickable
v-close-popup
@click="ResetPassword(props.row)"
id="context-reset"
:disable="props.row.social_accounts.length !== 0"
>
<q-item-section side>
<q-icon name="autorenew" />
</q-item-section>
<q-item-section>Reset Password</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="reset2FA(props.row)"
id="context-reset"
:disable="props.row.social_accounts.length !== 0"
>
<q-item-section side>
<q-icon name="autorenew" />
</q-item-section>
<q-item-section>Reset Two-Factor Auth</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item
clickable
v-close-popup
@click="showSSOAccounts(props.row)"
id="context-reset"
:disable="props.row.social_accounts.length === 0"
>
<q-item-section side>
<q-icon name="groups" />
</q-item-section>
<q-item-section>Show Connected SSO Accounts</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="showSessions(props.row)"
id="context-reset"
>
<q-item-section side>
<q-icon name="groups" />
</q-item-section>
<q-item-section>Show Active Sessions</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
</q-menu>
<!-- enabled checkbox -->
<q-td>
<q-checkbox
dense
@update:model-value="toggleEnabled(props.row)"
v-model="props.row.is_active"
:disable="props.row.username === logged_in_user"
/>
</q-td>
<q-td>
<q-chip
v-if="props.row.social_accounts.length > 0"
color="primary"
dense
>SSO</q-chip
>
</q-td>
<q-td>{{ props.row.username }}</q-td>
<q-td>{{ props.row.first_name }} {{ props.row.last_name }}</q-td>
<q-td>{{ props.row.email }}</q-td>
<q-td v-if="props.row.last_login">{{
formatDate(props.row.last_login)
}}</q-td>
<q-td v-else>Never</q-td>
<q-td>{{ props.row.last_login_ip }}</q-td>
</q-tr>
</template>
</q-table>
</div>
</q-card>
/>
</q-td>
<q-td>{{ props.row.username }}</q-td>
<q-td>{{ props.row.first_name }} {{ props.row.last_name }}</q-td>
<q-td>{{ props.row.email }}</q-td>
<q-td v-if="props.row.last_login">{{
formatDate(props.row.last_login)
}}</q-td>
<q-td v-else>Never</q-td>
<q-td>{{ props.row.last_login_ip }}</q-td>
</q-tr>
</template>
</q-table>
</div>
</q-card>
</div>
</template>
<script>
import mixins from "@/mixins/mixins";
import { computed } from "vue";
import { useStore } from "vuex";
import { useQuasar } from "quasar";
import { mapState as piniaMapState } from "pinia";
import { useAuthStore } from "@/stores/auth";
import { mapState, useStore } from "vuex";
import UserForm from "@/components/modals/admin/UserForm.vue";
import UserResetPasswordForm from "@/components/modals/admin/UserResetPasswordForm.vue";
import SSOAccountsTable from "@/ee/sso/components/SSOAccountsTable.vue";
import UserSessionsTable from "@/components/accounts/UserSessionsTable.vue";
export default {
name: "AdminManager",
@@ -206,30 +161,8 @@ export default {
const store = useStore();
const formatDate = computed(() => store.getters.formatDate);
const $q = useQuasar();
function showSSOAccounts(user) {
$q.dialog({
component: SSOAccountsTable,
componentProps: {
user,
},
});
}
async function showSessions(user) {
$q.dialog({
component: UserSessionsTable,
componentProps: {
user,
},
});
}
return {
formatDate,
showSSOAccounts,
showSessions,
};
},
data() {
@@ -242,13 +175,6 @@ export default {
field: "is_active",
align: "left",
},
{
name: "sso",
label: "",
field: "sso",
align: "left",
sortable: true,
},
{
name: "username",
label: "Username",
@@ -390,7 +316,7 @@ export default {
},
},
computed: {
...piniaMapState(useAuthStore, {
...mapState({
logged_in_user: (state) => state.username,
}),
},

View File

@@ -46,9 +46,6 @@
<template v-slot:header-cell-plat="props">
<q-th auto-width :props="props"></q-th>
</template>
<template v-slot:header-cell-mon-type="props">
<q-th auto-width :props="props"></q-th>
</template>
<template v-slot:header-cell-checks-status="props">
<q-th :props="props">
<q-icon name="fas fa-check-double" size="1.2em">
@@ -209,20 +206,6 @@
</q-icon>
</q-td>
<q-td key="mon-type" :props="props">
<q-icon
v-if="props.row.monitoring_type === 'server'"
name="dns"
size="sm"
color="primary"
>
<q-tooltip>Server</q-tooltip>
</q-icon>
<q-icon v-else name="computer" size="sm" color="primary">
<q-tooltip>Workstation</q-tooltip>
</q-icon>
</q-td>
<q-td key="checks-status" :props="props">
<q-icon
v-if="props.row.maintenance_mode"
@@ -448,8 +431,8 @@ export default {
return false;
else if (availability === "expired") {
let now = new Date();
let last_seen = new Date(row.last_seen);
let diff = date.getDateDiff(now, last_seen, "days");
let last_seen_unix = new Date(row.boot_time * 1000);
let diff = date.getDateDiff(now, last_seen_unix, "days");
if (diff < 30) return false;
}
}

View File

@@ -278,7 +278,7 @@ export default {
},
{
name: "resolved_action_name",
label: "Resolved Action",
label: "Resolve Action",
field: "resolved_action_name",
align: "left",
},
@@ -326,7 +326,7 @@ export default {
this.refresh();
this.$q.loading.hide();
this.notifySuccess(
`Alert template ${template.name} was deleted!`,
`Alert template ${template.name} was deleted!`
);
})
.catch(() => {

View File

@@ -151,14 +151,6 @@
v-model="localRole.can_edit_core_settings"
label="Edit Global Settings"
/>
<q-checkbox
v-model="localRole.can_view_global_keystore"
label="View Global Key Store"
/>
<q-checkbox
v-model="localRole.can_edit_global_keystore"
label="Edit Global Key Store"
/>
<q-checkbox
v-model="localRole.can_do_server_maint"
label="Do Server Maintenance"
@@ -187,11 +179,6 @@
v-model="localRole.can_manage_customfields"
label="Edit Custom Fields"
/>
<q-checkbox
v-if="!hosted"
v-model="localRole.can_use_webterm"
label="Use TRMM Server Web Terminal"
/>
</div>
</q-card-section>
@@ -341,11 +328,6 @@
v-model="localRole.can_manage_scripts"
label="Manage Scripts"
/>
<q-checkbox
v-if="!hosted"
v-model="localRole.can_run_server_scripts"
label="Run Scripts on TRMM Server"
/>
</div>
</q-card-section>
@@ -427,8 +409,7 @@
<script>
// composition imports
import { computed, ref, watch } from "vue";
import { useStore } from "vuex";
import { ref, watch } from "vue";
import { useDialogPluginComponent } from "quasar";
import { saveRole, editRole } from "@/api/accounts";
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
@@ -446,10 +427,6 @@ export default {
// quasar setup
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// store
const store = useStore();
const hosted = computed(() => store.state.hosted);
// dropdown setup
const { clientOptions } = useClientDropdown(true);
const { siteOptions } = useSiteDropdown(true);
@@ -485,8 +462,6 @@ export default {
// settings perms
can_view_core_settings: false,
can_edit_core_settings: false,
can_view_global_keystore: false,
can_edit_global_keystore: false,
can_do_server_maint: false,
can_code_sign: false,
can_run_urlactions: false,
@@ -536,9 +511,6 @@ export default {
can_manage_roles: false,
can_view_clients: [],
can_view_sites: [],
// server scripts and web terminal
can_run_server_scripts: false,
can_use_webterm: false,
// reporting perms
can_view_reports: false,
can_manage_reports: false,
@@ -578,7 +550,6 @@ export default {
loading,
clientOptions,
siteOptions,
hosted,
onSubmit,

View File

@@ -1,151 +0,0 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card style="width: 60vw; max-width: 90vw; min-height: 40vh">
<q-bar>
User Sessions for {{ user.username }}
<q-space />
<q-btn v-close-popup dense flat icon="close">
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-table
dense
:table-class="{
'table-bgcolor': !$q.dark.isActive,
'table-bgcolor-dark': $q.dark.isActive,
}"
:style="{ 'max-height': `${$q.screen.height - 24}px` }"
class="tbl-sticky"
:rows="sessions"
:columns="columns"
:loading="loading"
:pagination="{ rowsPerPage: 0, sortBy: 'display', descending: true }"
row-key="id"
binary-state-sort
virtual-scroll
:rows-per-page-options="[0]"
>
<template #top>
<q-space />
<q-btn
label="Remove All Sessions"
@click="removeAllSessions"
size="sm"
color="negative"
/>
</template>
<template #body="props">
<q-tr>
<!-- rows -->
<td>{{ formatDate(props.row.created) }}</td>
<td>{{ formatDate(props.row.expiry) }}</td>
<td>
<q-btn
size="sm"
@click="removeSession(props.row)"
label="Disconnect"
color="negative"
></q-btn>
</td>
</q-tr>
</template>
</q-table>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
// composition imports
import { onMounted, ref } from "vue";
import { useDialogPluginComponent, useQuasar, type QTableColumn } from "quasar";
import { notifySuccess } from "@/utils/notify";
import { formatDate } from "@/utils/format";
import {
fetchUserSessions,
deleteAllUserSessions,
deleteUserSession,
} from "@/api/accounts";
//types
import type { SSOUser } from "@/ee/sso/types/sso";
import type { AuthToken } from "@/types/accounts";
const columns: QTableColumn[] = [
{
name: "created",
label: "Created",
field: "created",
align: "left",
sortable: true,
},
{
name: "expiry",
label: "Expires",
field: "expiry",
align: "left",
sortable: true,
},
{
name: "action",
label: "",
field: "action",
align: "left",
sortable: true,
},
];
// emits
defineEmits([...useDialogPluginComponent.emits]);
// props
const props = defineProps<{
user: SSOUser;
}>();
const { dialogRef, onDialogHide } = useDialogPluginComponent();
const $q = useQuasar();
const sessions = ref([] as AuthToken[]);
const loading = ref(false);
function removeSession(token: AuthToken) {
$q.dialog({
title: `Disconnect session for ${token.user}?`,
message: "This user will be signed out immediately.",
cancel: true,
ok: { label: "Delete", color: "negative" },
}).onOk(async () => {
loading.value = true;
try {
await deleteUserSession(token.digest);
notifySuccess("Login session deleted successfully");
} finally {
loading.value = false;
await getSessions();
}
});
}
function removeAllSessions() {
$q.dialog({
title: `Disconnect all sessions for ${props.user.username}?`,
cancel: true,
ok: { label: "Delete", color: "negative" },
}).onOk(async () => {
loading.value = true;
try {
await deleteAllUserSessions(props.user.id);
notifySuccess("Login sessions deleted successfully");
} finally {
loading.value = false;
onDialogHide();
}
});
}
async function getSessions() {
sessions.value = await fetchUserSessions(props.user.id);
}
onMounted(getSessions);
</script>

View File

@@ -302,9 +302,7 @@ export default {
async function getURLActions() {
menuLoading.value = true;
try {
urlActions.value = (await fetchURLActions())
.filter((action) => action.action_type === "web")
.sort((a, b) => a.name.localeCompare(b.name));
urlActions.value = await fetchURLActions();
if (urlActions.value.length === 0) {
notifyWarning(
@@ -312,11 +310,8 @@ export default {
);
return;
}
} catch (e) {
console.error(e);
} finally {
menuLoading.value = false;
}
} catch (e) {}
menuLoading.value = true;
}
function showSendCommand(agent) {

View File

@@ -1,5 +1,8 @@
<template>
<div v-if="!selectedAgent" class="q-pa-sm">No agent selected</div>
<div v-else-if="agentPlatform.toLowerCase() !== 'windows'" class="q-pa-sm">
Only supported for Windows agents at this time
</div>
<div v-else>
<q-table
dense
@@ -292,12 +295,7 @@
</q-td>
<q-td v-else></q-td>
<!-- name -->
<q-td
>{{ props.row.name
}}<q-tooltip v-if="props.row?.win_task_name" :delay="700">{{
props.row.win_task_name
}}</q-tooltip></q-td
>
<q-td>{{ props.row.name }}</q-td>
<!-- sync status -->
<q-td v-if="props.row.task_result.sync_status === 'notsynced'"
>Will sync on next agent checkin</q-td
@@ -443,7 +441,7 @@ export default {
try {
const result = await fetchAgentTasks(selectedAgent.value);
tasks.value = result.filter(
(task) => task.sync_status !== "pendingdeletion",
(task) => task.sync_status !== "pendingdeletion"
);
} catch (e) {
console.error(e);
@@ -497,7 +495,7 @@ export default {
try {
const result = await runTask(
task.id,
task.policy ? { agent_id: selectedAgent.value } : {},
task.policy ? { agent_id: selectedAgent.value } : {}
);
notifySuccess(result);
} catch (e) {
@@ -511,7 +509,6 @@ export default {
component: AutomatedTaskForm,
componentProps: {
parent: { agent: selectedAgent.value },
plat: agentPlatform.value,
},
}).onOk(() => {
getTasks();
@@ -526,7 +523,6 @@ export default {
componentProps: {
task: task,
parent: { agent: selectedAgent.value },
plat: agentPlatform.value,
},
}).onOk(() => {
getTasks();

View File

@@ -370,13 +370,7 @@
style="cursor: pointer; text-decoration: underline"
class="text-primary"
@click="showPingInfo(props.row)"
>{{
grep(props.row.check_result.more_info, [
"transmitted",
"received",
"packet loss",
])
}}</span
>Last Output</span
>
<span
v-else-if="
@@ -385,7 +379,7 @@
style="cursor: pointer; text-decoration: underline"
class="text-primary"
@click="showScriptOutput(props.row.check_result)"
>{{ processOutput(props.row.check_result) }}</span
>Last Output</span
>
<span
v-else-if="
@@ -398,9 +392,7 @@
>
<span
v-else-if="
['diskspace', 'cpuload', 'memory'].includes(
props.row.check_type,
) ||
props.row.check_type === 'diskspace' ||
(props.row.check_type === 'winsvc' && props.row.check_result.id)
"
>{{ props.row.check_result.more_info }}</span
@@ -518,40 +510,6 @@ export default {
descending: false,
});
// TODO this will break when we add translations
function grep(text, stringsToMatch) {
try {
const lines = text.split("\n");
const matched = [];
for (const line of lines) {
if (stringsToMatch.every((str) => line.includes(str))) {
matched.push(line);
}
}
return matched.length > 0 ? matched.join("\n") : "Last Output";
} catch (e) {
console.error(e);
return "Last Output";
}
}
function processOutput(result) {
try {
if (result.stdout && result.stdout.trim() !== "") {
return result.stdout.substring(0, 60);
} else if (result.stderr && result.stderr.trim() !== "") {
return result.stderr.substring(0, 60);
} else {
return "Last Output";
}
} catch (e) {
console.error(e);
return "Last Output";
}
}
function getAlertSeverity(check) {
if (check.check_result.alert_severity) {
return check.check_result.alert_severity;
@@ -708,7 +666,6 @@ export default {
componentProps: {
check: check,
parent: !check ? { agent: selectedAgent.value } : undefined,
plat: type === "script" ? agentPlatform.value : undefined,
},
}).onOk(getChecks);
}
@@ -749,8 +706,6 @@ export default {
getAlertSeverity,
runChecks,
resetAllChecks,
grep,
processOutput,
// dialogs
showScriptOutput,

View File

@@ -17,85 +17,70 @@
:loading="loading"
>
<template v-slot:top>
<div class="q-gutter-md flex flex-center items-center">
<q-btn
v-if="isPolling"
dense
flat
push
@click="stopPoll"
icon="stop"
label="Stop Live Refresh"
/>
<q-btn
v-else
dense
flat
push
@click="startPoll"
icon="play_arrow"
label="Resume Live Refresh"
/>
<q-space />
<div class="q-pa-md q-gutter-sm">
<q-btn
v-if="isPolling"
:disable="pollInterval === 1"
dense
flat
@click="pollIntervalChanged('subtract')"
push
@click="stopPoll"
icon="stop"
label="Stop Live Refresh"
icon="remove"
size="sm"
color="grey"
/>
<q-btn
v-else
dense
flat
push
@click="startPoll"
icon="play_arrow"
label="Resume Live Refresh"
icon="add"
size="sm"
color="grey"
@click="pollIntervalChanged('add')"
/>
<div class="flex flex-center q-ml-md">
<q-icon name="fas fa-microchip" class="q-mr-xs" />
<div class="text-caption q-mr-sm">
CPU Usage:
<span class="text-body1 text-weight-medium"
>{{ totalCpuUsage }}%</span
>
</div>
<q-icon name="fas fa-memory" class="q-mr-xs" />
<div class="text-caption">
RAM Usage:
<span class="text-body1 text-weight-medium"
>{{ bytes2Human(totalRamUsage) }}/{{ total_ram }} GB</span
>
</div>
</div>
<q-space />
<div class="q-pa-md q-gutter-sm">
<q-btn
:disable="pollInterval === 1"
dense
@click="pollIntervalChanged('subtract')"
push
icon="remove"
size="sm"
color="grey"
/>
<q-btn
dense
push
icon="add"
size="sm"
color="grey"
@click="pollIntervalChanged('add')"
/>
</div>
<div class="text-overline">
<q-badge
align="middle"
size="sm"
class="text-h6"
color="blue"
:label="pollInterval"
/>
Refresh interval (seconds)
</div>
<q-space />
<q-input v-model="filter" outlined label="Search" dense clearable>
<template v-slot:prepend>
<q-icon name="search" />
</template>
</q-input>
</div>
<div class="text-overline">
<q-badge
align="middle"
size="sm"
class="text-h6"
color="blue"
:label="pollInterval"
/>
Refresh interval (seconds)
</div>
<q-space />
<q-input v-model="filter" outlined label="Search" dense clearable>
<template v-slot:prepend>
<q-icon name="search" />
</template>
</q-input>
<!-- file download doesn't work so disabling -->
<export-table-btn
v-show="false"
class="q-ml-sm"
:columns="columns"
:data="processes"
/>
</template>
<template v-slot:body="props">
<q-tr :props="props" class="cursor-pointer">
@@ -136,6 +121,9 @@ import {
import { bytes2Human } from "@/utils/format";
import { notifySuccess } from "@/utils/notify";
// ui imports
import ExportTableBtn from "@/components/ui/ExportTableBtn.vue";
const columns = [
{
name: "name",
@@ -176,6 +164,7 @@ const columns = [
];
export default {
components: { ExportTableBtn },
name: "ProcessManager",
props: {
agent_id: !String,
@@ -186,71 +175,52 @@ export default {
const poll = ref(null);
const isPolling = computed(() => !!poll.value);
function startPoll() {
stopPoll();
getProcesses();
poll.value = setInterval(() => {
getProcesses();
}, pollInterval.value * 1000);
async function startPoll() {
await getProcesses();
if (processes.value.length > 0) {
refreshProcesses();
}
}
function stopPoll() {
if (poll.value) {
clearInterval(poll.value);
poll.value = null;
}
clearInterval(poll.value);
poll.value = null;
}
function pollIntervalChanged(action) {
if (action === "subtract" && pollInterval.value <= 1) {
stopPoll();
startPoll();
return;
}
if (action === "add") {
pollInterval.value++;
} else if (action === "subtract" && pollInterval.value > 1) {
} else {
pollInterval.value--;
}
if (isPolling.value) {
startPoll();
}
stopPoll();
startPoll();
}
// process manager logic
const processes = ref([]);
const filter = ref("");
const total_ram = ref(0);
const memory = ref(null);
const loading = ref(false);
const totalCpuUsage = computed(() => {
if (!Array.isArray(processes.value) || processes.value.length === 0) {
return "0.00";
}
const total = processes.value.reduce((acc, proc) => {
const cpuPercent = parseFloat(proc.cpu_percent);
if (isNaN(cpuPercent)) {
return acc;
}
return acc + cpuPercent;
}, 0);
return total.toFixed(2);
});
const totalRamUsage = computed(() => {
return processes.value.reduce((acc, proc) => acc + proc.membytes, 0);
});
async function getProcesses() {
loading.value = true;
try {
processes.value = await fetchAgentProcesses(props.agent_id);
} catch (error) {
console.error(error);
}
processes.value = await fetchAgentProcesses(props.agent_id);
loading.value = false;
}
function refreshProcesses() {
poll.value = setInterval(() => {
getProcesses(props.agent_id);
}, pollInterval.value * 1000);
}
async function killProcess(pid) {
loading.value = true;
let result = "";
@@ -265,8 +235,11 @@ export default {
// lifecycle hooks
onMounted(async () => {
total_ram.value = (await fetchAgent(props.agent_id)).total_ram;
startPoll();
memory.value = await fetchAgent(props.agent_id).total_ram;
await getProcesses();
if (processes.value.length > 0) {
refreshProcesses();
}
});
onBeforeUnmount(() => clearInterval(poll.value));
@@ -275,12 +248,10 @@ export default {
// reactive data
processes,
filter,
total_ram,
memory,
isPolling,
pollInterval,
loading,
totalCpuUsage,
totalRamUsage,
// non-reactive data
columns,

View File

@@ -2,15 +2,6 @@
<q-dialog ref="dialog" @hide="onHide">
<q-card class="q-dialog-plugin" style="min-width: 70vw">
<q-bar>
<q-btn
ref="refresh"
@click="refresh"
class="q-mr-sm"
dense
flat
push
icon="refresh"
/>
{{ title.slice(0, 27) }}
<q-space />
<q-btn dense flat icon="close" v-close-popup>
@@ -290,13 +281,6 @@ export default {
},
});
},
refresh() {
if (this.type === "task") {
this.getTaskData();
} else {
this.getCheckData();
}
},
show() {
this.$refs.dialog.show();
},

View File

@@ -8,7 +8,7 @@
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-card-section v-if="filterByPlatformOptions.length === 0">
<q-card-section v-if="scriptOptions.length === 0">
<p>You need to upload a script first</p>
<p>Settings -> Script Manager</p>
</q-card-section>
@@ -19,7 +19,7 @@
:rules="[(val) => !!val || '*Required']"
outlined
v-model="state.script"
:options="filterByPlatformOptions"
:options="scriptOptions"
label="Select script"
mapOptions
:disable="!!check"
@@ -140,7 +140,6 @@ export default {
props: {
check: Object,
parent: Object, // {agent: agent.agent_id} or {policy: policy.id}
plat: String,
},
setup(props) {
// setup quasar dialog
@@ -149,13 +148,11 @@ export default {
// setup script dropdown
const {
script,
filterByPlatformOptions,
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
} = useScriptDropdown({
script: props.check ? props.check.script : undefined,
plat: props.plat,
} = useScriptDropdown(props.check ? props.check.script : undefined, {
onMount: true,
});
@@ -184,7 +181,7 @@ export default {
// non-reactive data
failOptions,
filterByPlatformOptions,
scriptOptions,
severityOptions,
envVarsLabel,

View File

@@ -20,18 +20,12 @@
</div>
<br />
<div v-if="scriptInfo.stdout">
<script-output-copy-clip
label="Standard Output"
:data="scriptInfo.stdout"
/>
Standard Output
<q-separator />
<pre>{{ scriptInfo.stdout }}</pre>
</div>
<div v-if="scriptInfo.stderr">
<script-output-copy-clip
label="Standard Error"
:data="scriptInfo.stderr"
/>
Standard Error
<q-separator />
<pre>{{ scriptInfo.stderr }}</pre>
</div>
@@ -49,13 +43,8 @@ import { computed } from "vue";
import { useStore } from "vuex";
import { useDialogPluginComponent } from "quasar";
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
export default {
name: "ScriptOutput",
components: {
ScriptOutputCopyClip,
},
emits: [...useDialogPluginComponent.emits],
props: { scriptInfo: !Object },
setup() {

View File

@@ -116,8 +116,7 @@
</template>
<script>
import { mapState as piniaMapState } from "pinia";
import { useAuthStore } from "@/stores/auth";
import { mapState } from "vuex";
import mixins from "@/mixins/mixins";
export default {
@@ -146,7 +145,7 @@ export default {
title() {
return this.user ? "Edit User" : "Add User";
},
...piniaMapState(useAuthStore, {
...mapState({
logged_in_user: (state) => state.username,
}),
},

View File

@@ -83,29 +83,12 @@
<tactical-dropdown
:rules="[(val) => !!val || '*Required']"
v-model="state.script"
:options="filterByPlatformOptions"
:options="filteredScriptOptions"
label="Select Script"
outlined
mapOptions
filterable
>
<template v-slot:after>
<q-btn
size="sm"
round
dense
flat
icon="info"
@click="openScriptURL"
>
<q-tooltip
v-if="syntax"
class="bg-white text-primary text-body1"
v-html="formatScriptSyntax(syntax)"
/>
</q-btn>
</template>
</tactical-dropdown>
/>
</q-card-section>
<q-card-section v-if="mode === 'script'" class="q-pt-none">
<tactical-dropdown
@@ -170,39 +153,6 @@
</q-checkbox>
</q-card-section>
<q-card-section v-if="mode === 'script'" class="q-pt-none">
<div class="q-gutter-sm">
<q-checkbox
label="Save results to Custom Field"
v-model="collector"
@update:model-value="
state.custom_field = null;
state.collector_all_output = false;
"
/>
<q-checkbox
v-model="state.save_to_agent_note"
label="Save results to Agent Note"
/>
</div>
</q-card-section>
<q-card-section v-if="mode === 'script' && collector">
<tactical-dropdown
:rules="[(val) => !!val || '*Required']"
outlined
v-model="state.custom_field"
:options="customFieldOptions"
label="Select custom field"
mapOptions
filterable
/>
<q-checkbox
v-model="state.collector_all_output"
label="Save all output"
/>
</q-card-section>
<q-card-section v-if="mode === 'script' || mode === 'command'">
<q-input
v-model.number="state.timeout"
@@ -260,23 +210,16 @@
<script>
// composition imports
import {
ref,
reactive,
computed,
watch,
onMounted,
defineComponent,
} from "vue";
import { useDialogPluginComponent, openURL } from "quasar";
import { ref, computed, watch, onMounted } from "vue";
import { useStore } from "vuex";
import { useDialogPluginComponent } from "quasar";
import { useScriptDropdown } from "@/composables/scripts";
import { useAgentDropdown } from "@/composables/agents";
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
import { useCustomFieldDropdown } from "@/composables/core";
import { runBulkAction } from "@/api/agents";
import { notifySuccess } from "@/utils/notify";
import { formatScriptSyntax } from "@/utils/format";
import { cmdPlaceholder } from "@/composables/agents";
import { removeExtraOptionCategories } from "@/utils/format";
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
// ui imports
@@ -308,7 +251,7 @@ const patchModeOptions = [
{ label: "Install", value: "install" },
];
export default defineComponent({
export default {
name: "BulkAction",
components: { TacticalDropdown },
emits: [...useDialogPluginComponent.emits],
@@ -316,8 +259,14 @@ export default defineComponent({
mode: !String,
},
setup(props) {
// setup vuex store
const store = useStore();
const showCommunityScripts = computed(
() => store.state.showCommunityScripts
);
const shellOptions = computed(() => {
if (state.osType === "windows") {
if (state.value.osType === "windows") {
return [
{ label: "CMD", value: "cmd" },
{ label: "Powershell", value: "powershell" },
@@ -344,26 +293,18 @@ export default defineComponent({
// dropdown setup
const {
script,
plat,
filterByPlatformOptions,
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
syntax,
link,
getScriptOptions,
} = useScriptDropdown();
const { agents, agentOptions, getAgentOptions } = useAgentDropdown();
const { site, siteOptions, getSiteOptions } = useSiteDropdown();
const { client, clientOptions, getClientOptions } = useClientDropdown();
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
function openScriptURL() {
link.value ? openURL(link.value) : null;
}
// bulk action logic
const state = reactive({
const state = ref({
mode: props.mode,
target: "client",
monType: "all",
@@ -371,9 +312,6 @@ export default defineComponent({
cmd: "",
shell: "cmd",
custom_shell: null,
custom_field: null,
collector_all_output: false,
save_to_agent_note: false,
patchMode: "scan",
offlineAgents: false,
client,
@@ -386,42 +324,35 @@ export default defineComponent({
run_as_user: false,
});
const loading = ref(false);
const collector = ref(false);
watch(
() => state.target,
() => state.value.target,
() => {
client.value = null;
site.value = null;
agents.value = [];
},
}
);
plat.value = state.osType;
watch(
() => state.osType,
() => state.value.osType,
(newValue) => {
state.custom_shell = null;
state.run_as_user = false;
state.value.custom_shell = null;
state.value.run_as_user = false;
if (newValue === "windows") {
state.shell = "cmd";
state.value.shell = "cmd";
} else {
state.shell = "/bin/bash";
state.value.shell = "/bin/bash";
}
// set plat to filter script options
if (newValue === "all") plat.value = undefined;
else plat.value = newValue;
},
}
);
async function submit() {
loading.value = true;
try {
const data = await runBulkAction(state);
const data = await runBulkAction(state.value);
notifySuccess(data);
onDialogHide();
} catch (e) {}
@@ -431,7 +362,9 @@ export default defineComponent({
const supportsRunAsUser = () => {
const modes = ["script", "command"];
return state.osType === "windows" && modes.includes(state.mode);
return (
state.value.osType === "windows" && modes.includes(state.value.mode)
);
};
// set modal title and caption
@@ -439,10 +372,25 @@ export default defineComponent({
return props.mode === "command"
? "Run Bulk Command"
: props.mode === "script"
? "Run Bulk Script"
: props.mode === "patch"
? "Bulk Patch Management"
: "";
? "Run Bulk Script"
: props.mode === "patch"
? "Bulk Patch Management"
: "";
});
const filteredScriptOptions = computed(() => {
if (props.mode !== "script") return [];
if (state.value.osType === "all") return scriptOptions.value;
return removeExtraOptionCategories(
scriptOptions.value.filter(
(script) =>
script.category ||
!script.supported_platforms ||
script.supported_platforms.length === 0 ||
script.supported_platforms.includes(state.value.osType)
)
);
});
// component lifecycle hooks
@@ -450,7 +398,7 @@ export default defineComponent({
getAgentOptions();
getSiteOptions();
getClientOptions();
if (props.mode === "script") getScriptOptions();
if (props.mode === "script") getScriptOptions(showCommunityScripts.value);
});
return {
@@ -458,10 +406,8 @@ export default defineComponent({
state,
agentOptions,
clientOptions,
collector,
customFieldOptions,
siteOptions,
filterByPlatformOptions,
filteredScriptOptions,
loading,
shellOptions,
filteredOsTypeOptions,
@@ -473,7 +419,6 @@ export default defineComponent({
patchModeOptions,
runAsUserToolTip,
envVarsLabel,
syntax,
//computed
modalTitle,
@@ -482,13 +427,11 @@ export default defineComponent({
submit,
cmdPlaceholder,
supportsRunAsUser,
openScriptURL,
formatScriptSyntax,
// quasar dialog plugin
dialogRef,
onDialogHide,
};
},
});
};
</script>

View File

@@ -137,7 +137,7 @@
<q-radio
v-model="goarch"
:val="GOARCH_ARM64"
label="Apple Silicon (M-Series)"
label="Apple Silicon (M1, M2, M3)"
v-show="agentOS === 'darwin'"
/>
<q-radio

View File

@@ -39,9 +39,9 @@
<q-form @submit.prevent="sendScript">
<q-card-section>
<tactical-dropdown
:rules="[(val: number) => !!val || '*Required']"
:rules="[(val) => !!val || '*Required']"
v-model="state.script"
:options="filterByPlatformOptions"
:options="filteredScriptOptions"
label="Select script"
outlined
mapOptions
@@ -89,7 +89,7 @@
new-value-mode="add"
/>
</q-card-section>
<q-card-section v-if="!state.run_on_server">
<q-card-section>
<q-option-group
v-model="state.output"
:options="outputOptions"
@@ -130,7 +130,7 @@
</q-card-section>
<q-card-section v-if="state.output === 'collector'">
<tactical-dropdown
:rules="[(val: number) => !!val || '*Required']"
:rules="[(val) => !!val || '*Required']"
outlined
v-model="state.custom_field"
:options="customFieldOptions"
@@ -140,30 +140,10 @@
/>
<q-checkbox v-model="state.save_all_output" label="Save all output" />
</q-card-section>
<q-card-section>
<q-checkbox
v-if="agent.plat === 'windows' && !state.run_on_server"
v-model="state.run_as_user"
label="Run As User"
>
<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-checkbox
v-if="!hosted"
:disable="!server_scripts_enabled"
v-model="state.run_on_server"
label="Run On Server"
@update:model-value="ret = null"
>
<q-tooltip v-if="!server_scripts_enabled"
>Enable server side scripts globally to activate this
feature.</q-tooltip
>
<q-tooltip v-else
>Run the script on the Tactical RMM server in the context of this
agent.</q-tooltip
>
</q-checkbox>
</q-card-section>
<q-card-section>
<q-input
@@ -195,70 +175,29 @@
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
style="max-height: 50vh"
>
<script-output-copy-clip
v-if="!state.run_on_server"
label="Output"
:data="ret"
/>
<q-separator />
<pre v-if="!state.run_on_server">{{ ret }}</pre>
<q-card-section v-if="state.run_on_server" class="scroll">
<div>
Run Time:
<code>{{ ret.execution_time }} seconds</code>
<br />Return Code:
<code>{{ ret.retcode }}</code>
<br />
</div>
<br />
<div v-if="ret.stdout">
<script-output-copy-clip
label="Standard Output"
:data="ret.stdout"
/>
<q-separator />
<pre>{{ ret.stdout }}</pre>
</div>
<div v-if="ret.stderr">
<script-output-copy-clip
label="Standard Error"
:data="ret.stderr"
/>
<q-separator />
<pre>{{ ret.stderr }}</pre>
</div>
</q-card-section>
<pre>{{ ret }}</pre>
</q-card-section>
</q-form>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
<script>
// composition imports
import { computed, ref, watch } from "vue";
import { useStore } from "vuex";
import { ref, watch, computed } from "vue";
import { useDialogPluginComponent, openURL } from "quasar";
import { useScriptDropdown } from "@/composables/scripts";
import { useCustomFieldDropdown } from "@/composables/core";
import { runScript } from "@/api/agents";
import { notifySuccess } from "@/utils/notify";
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
import { formatScriptSyntax } from "@/utils/format";
import {
formatScriptSyntax,
removeExtraOptionCategories,
} from "@/utils/format";
//ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
// types
import type { Agent } from "@/types/agents";
// store
const store = useStore();
const hosted = computed(() => store.state.hosted);
const server_scripts_enabled = computed(
() => store.state.server_scripts_enabled,
);
// static data
const outputOptions = [
@@ -269,72 +208,110 @@ const outputOptions = [
{ label: "Save results to Agent Notes", value: "note" },
];
// emits
defineEmits([...useDialogPluginComponent.emits]);
export default {
name: "RunScript",
emits: [...useDialogPluginComponent.emits],
components: { TacticalDropdown },
props: {
agent: !Object,
script: Number,
},
setup(props) {
// setup quasar dialog plugin
const { dialogRef, onDialogHide } = useDialogPluginComponent();
// props
const props = defineProps<{
agent: Agent;
script?: number;
}>();
// setup dropdowns
const {
script,
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
syntax,
link,
} = useScriptDropdown(props.script, {
onMount: true,
filterByPlatform: props.agent.plat,
});
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
// setup quasar dialog plugin
const { dialogRef, onDialogHide } = useDialogPluginComponent();
// main run script functionaity
const state = ref({
output: "wait",
emails: [],
emailMode: "default",
custom_field: null,
save_all_output: false,
script,
args: defaultArgs,
env_vars: defaultEnvVars,
timeout: defaultTimeout,
run_as_user: false,
});
// setup dropdowns
const {
script,
filterByPlatformOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
syntax,
link,
} = useScriptDropdown({
script: props.script,
plat: props.agent.plat,
onMount: true,
});
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
const ret = ref(null);
const loading = ref(false);
const maximized = ref(false);
// main run script functionaity
const state = ref({
output: "wait",
emails: [],
emailMode: "default",
custom_field: null,
save_all_output: false,
script,
args: defaultArgs,
env_vars: defaultEnvVars,
timeout: defaultTimeout,
run_as_user: false,
run_on_server: false,
});
async function sendScript() {
ret.value = null;
loading.value = true;
const ret = ref(null);
const loading = ref(false);
const maximized = ref(false);
ret.value = await runScript(props.agent.agent_id, state.value);
loading.value = false;
if (state.value.output === "forget") {
onDialogHide();
notifySuccess(ret.value);
}
}
async function sendScript() {
ret.value = null;
loading.value = true;
function openScriptURL() {
link.value ? openURL(link.value) : null;
}
ret.value = await runScript(props.agent.agent_id, state.value);
loading.value = false;
if (state.value.output === "forget") {
onDialogHide();
if (ret.value) notifySuccess(ret.value);
}
}
const filteredScriptOptions = computed(() => {
return removeExtraOptionCategories(
scriptOptions.value.filter(
(script) =>
script.category ||
!script.supported_platforms ||
script.supported_platforms.length === 0 ||
script.supported_platforms.includes(props.agent.plat)
)
);
});
function openScriptURL() {
link.value ? openURL(link.value) : null;
}
// watchers
watch(
[() => state.value.output, () => state.value.emailMode],
() => (state.value.emails = [])
);
// watchers
watch(
[() => state.value.output, () => state.value.emailMode],
() => (state.value.emails = []),
);
return {
// reactive data
state,
loading,
filteredScriptOptions,
link,
syntax,
ret,
maximized,
customFieldOptions,
// non-reactive data
outputOptions,
runAsUserToolTip,
envVarsLabel,
//methods
formatScriptSyntax,
sendScript,
openScriptURL,
// quasar dialog plugin
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@@ -104,9 +104,6 @@
type="submit"
/>
</q-card-actions>
<q-card-section v-if="ret !== null"
><script-output-copy-clip label="Output" :data="ret" /> <q-separator
/></q-card-section>
<q-card-section
v-if="ret !== null"
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
@@ -127,13 +124,8 @@ import { sendAgentCommand } from "@/api/agents";
import { cmdPlaceholder } from "@/composables/agents";
import { runAsUserToolTip } from "@/constants/constants";
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
export default {
name: "SendCommand",
components: {
ScriptOutputCopyClip,
},
emits: [...useDialogPluginComponent.emits],
props: {
agent: !Object,

View File

@@ -1,8 +1,8 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-dialog ref="dialog" @hide="onHide">
<q-card style="width: 90vw; max-width: 90vw">
<q-bar>
{{ alertTemplate ? "Edit Alert Template" : "Add Alert Template" }}
{{ title }}
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
@@ -150,62 +150,50 @@
<span style="text-decoration: underline; cursor: help"
>Alert Failure Settings
<q-tooltip>
The selected action will run when an alert is triggered.
The selected script will run when an alert is triggered. This
script will run on any online agent.
</q-tooltip>
</span>
</div>
<q-card-section>
<q-option-group
v-model="template.action_type"
class="q-pb-sm"
:options="actionTypeOptions"
dense
inline
/>
<tactical-dropdown
v-if="template.action_type == 'script'"
<q-select
class="q-mb-sm"
label="Failure script"
label="Failure action"
dense
options-dense
outlined
clearable
v-model="template.action"
:options="scriptOptions"
mapOptions
filterable
:rules="[(val) => !!val || '*Required']"
/>
<tactical-dropdown
v-else-if="template.action_type == 'server'"
class="q-mb-sm"
label="Failure script"
outlined
clearable
v-model="template.action"
:options="serverScriptOptions"
mapOptions
filterable
/>
<tactical-dropdown
v-else
class="q-mb-sm"
label="Failure Web Hook"
outlined
clearable
v-model="template.action_rest"
:options="restActionOptions"
mapOptions
filterable
/>
map-options
emit-value
@update:model-value="setScriptDefaults('failure')"
>
<template v-slot:option="scope">
<q-item
v-if="!scope.opt.category"
v-bind="scope.itemProps"
class="q-pl-lg"
>
<q-item-section>
<q-item-label v-html="scope.opt.label"></q-item-label>
</q-item-section>
</q-item>
<q-item-label
v-if="scope.opt.category"
v-bind="scope.itemProps"
header
class="q-pa-sm"
>{{ scope.opt.category }}</q-item-label
>
</template>
</q-select>
<q-select
v-if="template.action_type !== 'rest'"
class="q-mb-sm"
dense
label="Failure script arguments (press Enter after typing each argument)"
label="Failure action arguments (press Enter after typing each argument)"
filled
v-model="template.action_args"
use-input
@@ -217,10 +205,9 @@
/>
<q-select
v-if="template.action_type !== 'rest'"
class="q-mb-sm"
dense
label="Failure script environment vars (press Enter after typing each key=value pair)"
label="Failure action environment vars (press Enter after typing each key=value pair)"
filled
v-model="template.action_env_vars"
use-input
@@ -232,15 +219,16 @@
/>
<q-input
v-if="template.action_type !== 'rest'"
class="q-mb-sm"
label="Failure script timeout (seconds)"
label="Failure action timeout (seconds)"
outlined
type="number"
v-model.number="template.action_timeout"
dense
:rules="[
(val) => !!val || 'Failure script timeout is required',
(val) => !!val || 'Failure action timeout is required',
(val) => val > 0 || 'Timeout must be greater than 0',
(val) => val <= 60 || 'Timeout must be 60 or less',
]"
/>
</q-card-section>
@@ -249,61 +237,50 @@
<span style="text-decoration: underline; cursor: help"
>Alert Resolved Settings
<q-tooltip>
The selected action will run when an alert is resolved.
The selected script will run when an alert is resolved. This
script will run on any online agent.
</q-tooltip>
</span>
</div>
<q-card-section>
<q-option-group
v-model="template.resolved_action_type"
class="q-pb-sm"
:options="actionTypeOptions"
dense
inline
/>
<tactical-dropdown
v-if="template.resolved_action_type === 'script'"
<q-select
class="q-mb-sm"
label="Resolved Script"
label="Resolved Action"
dense
options-dense
outlined
clearable
v-model="template.resolved_action"
:options="scriptOptions"
mapOptions
filterable
/>
<tactical-dropdown
v-else-if="template.resolved_action_type === 'server'"
class="q-mb-sm"
label="Resolved Script"
outlined
clearable
v-model="template.resolved_action"
:options="serverScriptOptions"
mapOptions
filterable
/>
<tactical-dropdown
v-else
class="q-mb-sm"
label="Resolved Web Hook"
outlined
clearable
v-model="template.resolved_action_rest"
:options="restActionOptions"
mapOptions
filterable
/>
map-options
emit-value
@update:model-value="setScriptDefaults('resolved')"
>
<template v-slot:option="scope">
<q-item
v-if="!scope.opt.category"
v-bind="scope.itemProps"
class="q-pl-lg"
>
<q-item-section>
<q-item-label v-html="scope.opt.label"></q-item-label>
</q-item-section>
</q-item>
<q-item-label
v-if="scope.opt.category"
v-bind="scope.itemProps"
header
class="q-pa-sm"
>{{ scope.opt.category }}</q-item-label
>
</template>
</q-select>
<q-select
v-if="template.resolved_action_type !== 'rest'"
class="q-mb-sm"
dense
label="Resolved script arguments (press Enter after typing each argument)"
label="Resolved action arguments (press Enter after typing each argument)"
filled
v-model="template.resolved_action_args"
use-input
@@ -315,7 +292,6 @@
/>
<q-select
v-if="template.resolved_action_type !== 'rest'"
class="q-mb-sm"
dense
label="Resolved action environment vars (press Enter after typing each key=value pair)"
@@ -330,15 +306,16 @@
/>
<q-input
v-if="template.resolved_action_type !== 'rest'"
class="q-mb-sm"
label="Resolved script timeout (seconds)"
label="Resolved action timeout (seconds)"
outlined
type="number"
v-model.number="template.resolved_action_timeout"
dense
:rules="[
(val) => !!val || 'Resolved script timeout is required',
(val) => !!val || 'Resolved action timeout is required',
(val) => val > 0 || 'Timeout must be greater than 0',
(val) => val <= 60 || 'Timeout must be 60 or less',
]"
/>
</q-card-section>
@@ -347,7 +324,7 @@
<span style="text-decoration: underline; cursor: help"
>Run actions only on
<q-tooltip>
The selected action will only run on the following types of
The selected script will only run on the following types of
alerts
</q-tooltip>
</span>
@@ -697,7 +674,7 @@
left-label
/>
<q-toggle
v-model="template.task_text_on_resolved"
v-model="template.check_text_on_resolved"
label="Text"
color="green"
left-label
@@ -711,23 +688,18 @@
v-if="step > 1"
flat
color="primary"
@click="stepper?.previous()"
@click="$refs.stepper.previous()"
label="Back"
class="q-mr-xs"
/>
<q-btn
v-if="step < 5"
@click="stepper?.next()"
@click="$refs.stepper.next()"
color="primary"
label="Next"
/>
<q-space />
<q-btn
@click="onSubmit"
color="primary"
label="Submit"
:loading="loading"
/>
<q-btn @click="onSubmit" color="primary" label="Submit" />
</q-stepper-navigation>
</template>
</q-stepper>
@@ -735,279 +707,195 @@
</q-dialog>
</template>
<script setup lang="ts">
import { computed, ref, reactive, watch, nextTick } from "vue";
import { useStore } from "vuex";
import { useQuasar, useDialogPluginComponent, type QStepper } from "quasar";
import { useScriptDropdown } from "@/composables/scripts";
import { useURLActionDropdown } from "@/composables/core";
import { notifyError, notifySuccess } from "@/utils/notify";
import { addAlertTemplate, saveAlertTemplate } from "@/api/alerts";
import { isValidEmail } from "@/utils/validation";
<script>
import mixins from "@/mixins/mixins";
import { mapGetters } from "vuex";
// components
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
// types
import type { AlertTemplate, AlertSeverity } from "@/types/alerts";
// store
const store = useStore();
const hosted = computed(() => store.state.hosted);
const server_scripts_enabled = computed(
() => store.state.server_scripts_enabled,
);
// props
const props = defineProps<{
alertTemplate?: AlertTemplate;
}>();
// emits
defineEmits([...useDialogPluginComponent.emits]);
// setup quasar plugins
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
const $q = useQuasar();
const step = ref(1);
// setup script dropdowns
const {
script: failureAction,
defaultArgs: failureArgs,
defaultEnvVars: failureEnvVars,
defaultTimeout: failureTimeout,
serverScriptOptions,
scriptOptions,
} = useScriptDropdown({ script: props.alertTemplate?.action, onMount: true });
const {
script: resolvedAction,
defaultArgs: resolvedArgs,
defaultEnvVars: resolvedEnvVars,
defaultTimeout: resolvedTimeout,
} = useScriptDropdown({
script: props.alertTemplate?.resolved_action,
onMount: true,
});
// setup custom field dropdown
const { restActionOptions } = useURLActionDropdown({ onMount: true });
// alert template form logic
const template: AlertTemplate = props.alertTemplate
? reactive(Object.assign({}, { ...props.alertTemplate }))
: reactive({
id: 0,
name: "",
is_active: true,
action_type: "script",
action: failureAction,
action_rest: undefined,
action_args: failureArgs,
action_env_vars: failureEnvVars,
action_timeout: failureTimeout,
resolved_action_type: "script",
resolved_action: resolvedAction,
resolved_action_rest: undefined,
resolved_action_args: resolvedArgs,
resolved_action_env_vars: resolvedEnvVars,
resolved_action_timeout: resolvedTimeout,
email_recipients: [] as string[],
email_from: "",
text_recipients: [] as string[],
agent_email_on_resolved: false,
agent_text_on_resolved: false,
agent_always_email: null,
agent_always_text: null,
agent_always_alert: null,
agent_periodic_alert_days: 0,
agent_script_actions: true,
check_email_alert_severity: [] as AlertSeverity[],
check_text_alert_severity: [] as AlertSeverity[],
check_dashboard_alert_severity: [] as AlertSeverity[],
check_email_on_resolved: false,
check_text_on_resolved: false,
check_always_email: null,
check_always_text: null,
check_always_alert: null,
check_periodic_alert_days: 0,
check_script_actions: true,
task_email_alert_severity: [] as AlertSeverity[],
task_text_alert_severity: [] as AlertSeverity[],
task_dashboard_alert_severity: [] as AlertSeverity[],
task_email_on_resolved: false,
task_text_on_resolved: false,
task_always_email: null,
task_always_text: null,
task_always_alert: null,
task_periodic_alert_days: 0,
task_script_actions: true,
});
// reset selected script if action type is changed
watch(
() => template.action_type,
() => {
template.action_rest = undefined;
template.action = undefined;
template.action_args = [];
template.action_env_vars = [];
template.action_timeout = 30;
export default {
name: "AlertTemplateForm",
emits: ["hide", "ok", "cancel"],
mixins: [mixins],
props: { alertTemplate: Object },
data() {
return {
step: 1,
template: {
name: "",
is_active: true,
action: null,
action_args: [],
action_env_vars: [],
action_timeout: 15,
resolved_action: null,
resolved_action_args: [],
resolved_action_env_vars: [],
resolved_action_timeout: 15,
email_recipients: [],
email_from: "",
text_recipients: [],
agent_email_on_resolved: false,
agent_text_on_resolved: false,
agent_always_email: null,
agent_always_text: null,
agent_always_alert: null,
agent_periodic_alert_days: 0,
agent_script_actions: true,
check_email_alert_severity: [],
check_text_alert_severity: [],
check_dashboard_alert_severity: [],
check_email_on_resolved: false,
check_text_on_resolved: false,
check_always_email: null,
check_always_text: null,
check_always_alert: null,
check_periodic_alert_days: 0,
check_script_actions: true,
task_email_alert_severity: [],
task_text_alert_severity: [],
task_dashboard_alert_severity: [],
task_email_on_resolved: false,
task_text_on_resolved: false,
task_always_email: null,
task_always_text: null,
task_always_alert: null,
task_periodic_alert_days: 0,
task_script_actions: true,
},
scriptOptions: [],
severityOptions: [
{ label: "Error", value: "error" },
{ label: "Warning", value: "warning" },
{ label: "Informational", value: "info" },
],
thumbStyle: {
right: "2px",
borderRadius: "5px",
backgroundColor: "#027be3",
width: "5px",
opacity: 0.75,
},
};
},
);
watch(
() => template.resolved_action_type,
() => {
template.resolved_action_rest = undefined;
template.resolved_action = undefined;
template.resolved_action_args = [];
template.resolved_action_env_vars = [];
template.resolved_action_timeout = 30;
computed: {
...mapGetters(["showCommunityScripts"]),
title() {
return this.editing ? "Edit Alert Template" : "Add Alert Template";
},
editing() {
return !!this.alertTemplate;
},
},
);
// sync selected script to scriptdropdown
// only add watchers if editting template
if (props.alertTemplate) {
watch(
() => template.action,
(newValue) => {
if (newValue) {
failureAction.value = newValue;
// wait for the script change to happen
nextTick(() => {
template.action_args = failureArgs.value;
template.action_env_vars = failureEnvVars.value;
template.action_timeout = failureTimeout.value;
});
methods: {
setScriptDefaults(type) {
if (type === "failure") {
const script = this.scriptOptions.find(
(i) => i.value === this.template.action
);
this.template.action_args = script.args;
this.template.action_env_vars = script.env_vars;
} else if (type === "resolved") {
const script = this.scriptOptions.find(
(i) => i.value === this.template.resolved_action
);
this.template.resolved_action_args = script.args;
this.template.resolved_action_env_vars = script.env_vars;
}
},
);
watch(
() => template.resolved_action,
(newValue) => {
if (newValue) {
resolvedAction.value = newValue;
// wait for the script change to happen
nextTick(() => {
template.resolved_action_args = resolvedArgs.value;
template.resolved_action_env_vars = resolvedEnvVars.value;
template.resolved_action_timeout = resolvedTimeout.value;
toggleAddEmail() {
this.$q
.dialog({
title: "Add email",
prompt: {
model: "",
isValid: (val) => this.isValidEmail(val),
type: "email",
},
cancel: true,
ok: { label: "Add", color: "primary" },
persistent: false,
})
.onOk((data) => {
this.template.email_recipients.push(data);
});
},
toggleAddSMSNumber() {
this.$q
.dialog({
title: "Add number",
message:
"Use E.164 format: must have the <b>+</b> symbol and <span class='text-red'>country code</span>, followed by the <span class='text-green'>phone number</span> e.g. <b>+<span class='text-red'>1</span><span class='text-green'>2131231234</span></b>",
prompt: {
model: "",
},
html: true,
cancel: true,
ok: { label: "Add", color: "primary" },
persistent: false,
})
.onOk((data) => {
this.template.text_recipients.push(data);
});
},
removeEmail(email) {
const removed = this.template.email_recipients.filter((k) => k !== email);
this.template.email_recipients = removed;
},
removeSMSNumber(num) {
const removed = this.template.text_recipients.filter((k) => k !== num);
this.template.text_recipients = removed;
},
onSubmit() {
if (!this.template.name) {
this.notifyError("Name needs to be set");
return;
}
this.$q.loading.show();
if (this.editing) {
this.$axios
.put(`alerts/templates/${this.template.id}/`, this.template)
.then(() => {
this.$q.loading.hide();
this.onOk();
this.notifySuccess("Alert Template edited!");
})
.catch(() => {
this.$q.loading.hide();
});
} else {
this.$axios
.post("alerts/templates/", this.template)
.then(() => {
this.$q.loading.hide();
this.onOk();
this.notifySuccess("Alert Template was added!");
})
.catch(() => {
this.$q.loading.hide();
});
}
},
);
}
const severityOptions = [
{ label: "Error", value: "error" },
{ label: "Warning", value: "warning" },
{ label: "Informational", value: "info" },
];
const staticActionTypeOptions = [
{ label: "Send a Web Hook", value: "rest" },
{ label: "Run script on Agent", value: "script" },
{ label: "Run script on TRMM Server", value: "server" },
];
const actionTypeOptions = computed(() => {
// don't show for hosted at all
if (hosted.value) {
return staticActionTypeOptions.filter(
(option) => option.value !== "server",
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
onOk() {
this.$emit("ok");
this.hide();
},
},
mounted() {
this.getScriptOptions(this.showCommunityScripts).then(
(options) => (this.scriptOptions = Object.freeze(options))
);
}
// disable the server script radio button if feature is disabled globally
const modifiedOptions = staticActionTypeOptions.map((option) => {
if (!server_scripts_enabled.value && option.value === "server") {
return { ...option, disable: true };
}
return option;
});
return modifiedOptions;
});
const stepper = ref<QStepper | null>(null);
function toggleAddEmail() {
$q.dialog({
title: "Add email",
prompt: {
model: "",
isValid: (val) => isValidEmail(val),
type: "email",
},
cancel: true,
ok: { label: "Add", color: "primary" },
persistent: false,
}).onOk((data) => {
template.email_recipients.push(data);
});
}
function toggleAddSMSNumber() {
$q.dialog({
title: "Add number",
message:
"Use E.164 format: must have the <b>+</b> symbol and <span class='text-red'>country code</span>, followed by the <span class='text-green'>phone number</span> e.g. <b>+<span class='text-red'>1</span><span class='text-green'>2131231234</span></b>",
prompt: {
model: "",
},
html: true,
cancel: true,
ok: { label: "Add", color: "primary" },
persistent: false,
}).onOk((data: string) => {
template.text_recipients.push(data);
});
}
function removeEmail(email: string) {
const removed = template.email_recipients.filter((k) => k !== email);
template.email_recipients = removed;
}
function removeSMSNumber(num: string) {
const removed = template.text_recipients.filter((k) => k !== num);
template.text_recipients = removed;
}
const loading = ref(false);
async function onSubmit() {
// TODO rework this ghetto form validation
if (!template.name) {
notifyError("Name needs to be set");
return;
}
loading.value = true;
if (props.alertTemplate) {
try {
await saveAlertTemplate(template.id, template);
notifySuccess("Alert Template edited!");
onDialogOK();
} catch {
} finally {
loading.value = false;
}
} else {
try {
await addAlertTemplate(template);
notifySuccess("Alert Template edited!");
onDialogOK();
} catch {
} finally {
loading.value = false;
}
}
}
// Copy alertTemplate prop locally
if (this.editing) Object.assign(this.template, this.alertTemplate);
},
};
</script>

View File

@@ -191,6 +191,24 @@
}}</q-badge>
</q-td>
</template>
<template v-slot:body-cell-alert_time="props">
<q-td :props="props">
{{ formatDate(props.value) }}
</q-td>
</template>
<template v-slot:body-cell-resolve_on="props">
<q-td :props="props">
{{ formatDate(props.value) }}
</q-td>
</template>
<template v-slot:body-cell-snoozed_until="props">
<q-td :props="props">
{{ formatDate(props.value) }}
</q-td>
</template>
</q-table>
</q-card-section>
</q-card>
@@ -247,21 +265,6 @@ export default {
field: "alert_time",
align: "left",
sortable: true,
format: (a) => this.formatDate(a),
},
{
name: "client",
label: "Client",
field: "client",
align: "left",
sortable: true,
},
{
name: "site",
label: "Site",
field: "site",
align: "left",
sortable: true,
},
{
name: "hostname",
@@ -293,12 +296,11 @@ export default {
sortable: true,
},
{
name: "resolved_on",
name: "resolve_on",
label: "Resolved On",
field: "resolved_on",
field: "resolve_on",
align: "left",
sortable: true,
format: (a) => this.formatDate(a),
},
{
name: "snoozed_until",
@@ -306,7 +308,6 @@ export default {
field: "snoozed_until",
align: "left",
sortable: true,
format: (a) => this.formatDate(a),
},
{ name: "actions", label: "Actions", align: "left" },
],
@@ -327,7 +328,7 @@ export default {
return this.columns.map((column) => {
if (column.name === "snoozed_until") {
if (this.includeSnoozed) return column.name;
} else if (column.name === "resolved_on") {
} else if (column.name === "resolve_on") {
if (this.includeResolved) return column.name;
} else {
return column.name;
@@ -339,7 +340,7 @@ export default {
getClients() {
this.$axios.get("/clients/").then((r) => {
this.clientsOptions = Object.freeze(
r.data.map((client) => ({ label: client.name, value: client.id })),
r.data.map((client) => ({ label: client.name, value: client.id }))
);
});
},

View File

@@ -48,7 +48,6 @@
<!-- name -->
<q-td>
{{ props.row.name }}
<q-tooltip :delay="600">ID: {{ props.row.id }}</q-tooltip>
</q-td>
<!-- type -->
<q-td>

View File

@@ -10,10 +10,8 @@
<q-tab name="customfields" label="Custom Fields" />
<q-tab name="keystore" label="Key Store" />
<q-tab name="urlactions" label="URL Actions" />
<q-tab name="webhooks" label="Web Hooks" />
<q-tab name="retention" label="Retention" />
<q-tab name="apikeys" label="API Keys" />
<q-tab name="sso" label="Single Sign-On (SSO)" />
<!-- <q-tab name="openai" label="Open AI" /> -->
</q-tabs>
</template>
@@ -43,51 +41,6 @@
<q-tooltip> Runs at 35mins past every hour </q-tooltip>
</q-checkbox>
</q-card-section>
<q-card-section v-if="!hosted" class="row">
<q-checkbox
v-model="settings.enable_server_scripts"
label="Enable server side scripts"
>
<q-tooltip
>Allow running scripts on TRMM server for alert
failure/resolve actions</q-tooltip
>
</q-checkbox>
<q-btn
size="sm"
round
dense
flat
icon="warning"
@click="
openURL(
'https://docs.tacticalrmm.com/functions/permissions/#permissions-with-extra-security-implications',
)
"
>
</q-btn>
</q-card-section>
<q-card-section v-if="!hosted" class="row">
<q-checkbox
v-model="settings.enable_server_webterminal"
label="Enable web terminal"
>
<q-tooltip>Enable the web terminal</q-tooltip>
</q-checkbox>
<q-btn
size="sm"
roundenable_server_webterminal
dense
flat
icon="warning"
@click="
openURL(
'https://docs.tacticalrmm.com/functions/permissions/#permissions-with-extra-security-implications',
)
"
>
</q-btn>
</q-card-section>
<q-card-section class="row">
<div class="col-4">Default agent timezone:</div>
<div class="col-2"></div>
@@ -172,24 +125,6 @@
class="col-6"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-4 flex items-center">
Receive notifications on:
</div>
<div class="col-2"></div>
<q-checkbox
dense
v-model="settings.notify_on_info_alerts"
class="col-3"
label="Informational Alerts"
/>
<q-checkbox
dense
v-model="settings.notify_on_warning_alerts"
class="col-3"
label="Warning Alerts"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-4">Agent Debug Level:</div>
<div class="col-2"></div>
@@ -553,28 +488,17 @@
</q-input>
</q-card-section>
</q-tab-panel>
<!-- custom fields -->
<q-tab-panel name="customfields">
<CustomFields />
</q-tab-panel>
<!-- key store -->
<q-tab-panel name="keystore">
<KeyStoreTable />
</q-tab-panel>
<!-- url actions -->
<q-tab-panel name="urlactions">
<URLActionsTable type="web" />
<URLActionsTable />
</q-tab-panel>
<!-- web hooks -->
<q-tab-panel name="webhooks">
<URLActionsTable type="rest" />
</q-tab-panel>
<!-- retention -->
<q-tab-panel name="retention">
<q-card-section class="row">
<div class="col-4">Check History (days):</div>
@@ -637,11 +561,6 @@
<APIKeysTable />
</q-tab-panel>
<!-- sso integration -->
<q-tab-panel name="sso">
<SSOProvidersTable />
</q-tab-panel>
<!-- Open AI -->
<!-- <q-tab-panel name="openai">
<div class="text-subtitle2">Open AI</div>
@@ -691,8 +610,7 @@
v-show="
tab !== 'customfields' &&
tab !== 'keystore' &&
tab !== 'urlactions' &&
tab !== 'sso'
tab !== 'urlactions'
"
label="Save"
color="primary"
@@ -729,7 +647,6 @@ import CustomFields from "@/components/modals/coresettings/CustomFields.vue";
import KeyStoreTable from "@/components/modals/coresettings/KeyStoreTable.vue";
import URLActionsTable from "@/components/modals/coresettings/URLActionsTable.vue";
import APIKeysTable from "@/components/core/APIKeysTable.vue";
import SSOProvidersTable from "@/ee/sso/components/SSOProvidersTable.vue";
export default {
name: "EditCoreSettings",
@@ -739,7 +656,6 @@ export default {
KeyStoreTable,
URLActionsTable,
APIKeysTable,
SSOProvidersTable,
},
mixins: [mixins],
data() {
@@ -775,13 +691,6 @@ export default {
return this.$store.state.hosted;
},
},
watch: {
tab(newTab, oldTab) {
if (oldTab === "sso") {
this.getCoreSettings();
}
},
},
methods: {
openURL(url) {
openURL(url);
@@ -918,7 +827,6 @@ export default {
});
} else {
this.$emit("close");
this.$store.dispatch("getDashInfo", false);
this.notifySuccess("Settings were edited!");
}
})

View File

@@ -27,16 +27,8 @@
outlined
dense
v-model="localKey.value"
: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>
/>
</q-card-section>
<q-card-actions align="right">
@@ -58,7 +50,6 @@ export default {
props: { globalKey: Object },
data() {
return {
isPwd: true,
localKey: {
name: "",
value: "",

View File

@@ -3,15 +3,6 @@
<div class="row">
<div class="text-subtitle2">Global Key Store</div>
<q-space />
<q-btn
size="sm"
color="grey-5"
text-color="black"
class="q-mr-sm"
:label="isPwd ? 'Show values' : 'Hide values'"
:icon="isPwd ? 'visibility_off' : 'visibility'"
@click="isPwd = !isPwd"
/>
<q-btn
size="sm"
color="grey-5"
@@ -70,7 +61,7 @@
</q-td>
<!-- value -->
<q-td>
{{ isPwd ? "****" : props.row.value }}
{{ props.row.value }}
</q-td>
</q-tr>
</template>
@@ -88,7 +79,6 @@ export default {
data() {
return {
keystore: [],
isPwd: true,
pagination: {
rowsPerPage: 0,
sortBy: "name",

View File

@@ -1,160 +0,0 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin" style="width: 80vw">
<q-bar>
Testing {{ urlAction.name }}
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-card-section>
<q-option-group
v-model="runAgainst"
:options="runAgainstOptions"
inline
dense
/>
</q-card-section>
<q-card-section v-if="runAgainst === 'agent'">
<tactical-dropdown
v-model="agent"
:options="agentOptions"
label="Agents"
mapOptions
filterable
dense
filled
/>
</q-card-section>
<q-card-section v-else-if="runAgainst === 'site'">
<tactical-dropdown
v-model="site"
:options="siteOptions"
label="Sites"
mapOptions
filterable
dense
filled
/>
</q-card-section>
<q-card-section v-else-if="runAgainst === 'client'">
<tactical-dropdown
v-model="client"
:options="clientOptions"
label="Client"
mapOptions
filterable
dense
filled
/>
</q-card-section>
<q-card-section style="height: 60vh" class="scroll">
<div>
URL:
<code>{{ return_url }}</code>
</div>
<br />
<div>
Body
<q-separator />
<code>{{ return_request }}</code>
</div>
<br />
<div>
Response
<q-separator />
<code>{{ return_result }}</code>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Close" v-close-popup />
<q-btn
:loading="loading"
flat
label="Run"
color="primary"
@click="submit"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
// composition imports
import { ref, reactive, computed } from "vue";
import { useDialogPluginComponent } from "quasar";
import { useAgentDropdown } from "@/composables/agents";
import { useSiteDropdown, useClientDropdown } from "@/composables/clients";
import { runTestURLAction } from "@/api/core";
import { URLAction } from "@/types/core/urlactions";
// ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
// define emits
defineEmits([...useDialogPluginComponent.emits]);
// define props
const props = defineProps<{ urlAction: URLAction }>();
// setup quasar
const { dialogRef, onDialogHide } = useDialogPluginComponent();
// setup dropdowns
const { agent, agentOptions } = useAgentDropdown({ onMount: true });
const { client, clientOptions } = useClientDropdown(true);
const { site, siteOptions } = useSiteDropdown(true);
const runAgainst = ref<"agent" | "site" | "client" | "none">("none");
const runAgainstOptions = [
{ label: "Agent", value: "agent" },
{ label: "Site", value: "site" },
{ label: "Client", value: "client" },
{ label: "None", value: "none" },
];
const loading = ref(false);
const runAgainstID = computed(() => {
if (runAgainst.value === "agent") return agent.value;
else if (runAgainst.value === "site") return site.value;
else if (runAgainst.value === "client") return client.value;
else return 0;
});
const state = reactive({
pattern: props.urlAction.pattern,
rest_body: props.urlAction.rest_body,
rest_headers: props.urlAction.rest_headers,
rest_method: props.urlAction.rest_method,
run_instance_type: runAgainst,
run_instance_id: runAgainstID,
});
const return_url = ref("");
const return_result = ref("");
const return_request = ref("");
async function submit() {
loading.value = true;
try {
const { url, result, body } = await runTestURLAction(state);
return_result.value = result;
return_url.value = url;
return_request.value = body;
} catch (e) {
console.error(e);
} finally {
loading.value = false;
}
}
</script>

View File

@@ -1,31 +1,14 @@
<template>
<q-dialog
ref="dialogRef"
@hide="onDialogHide"
@show="loadEditor"
@before-hide="cleanupEditors"
>
<q-card
class="q-dialog-plugin"
:style="`width: ${props.type === 'web' ? 50 : 60}vw; max-width: ${props.type === 'web' ? 60 : 70}vw`"
>
<q-dialog ref="dialog" @hide="onHide">
<q-card class="q-dialog-plugin" style="width: 60vw">
<q-bar>
{{
props.action
? props.type === "web"
? "Edit URL Action"
: "Edit Web Hook"
: props.type === "web"
? "Add URL Action"
: "Add Web Hook"
}}
{{ title }}
<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>
<div style="max-height: 80vh" class="scroll">
<q-form @submit="submit">
<!-- name -->
<q-card-section>
<q-input
@@ -43,8 +26,6 @@
label="Description"
outlined
dense
type="textarea"
rows="2"
v-model="localAction.desc"
/>
</q-card-section>
@@ -60,186 +41,89 @@
/>
</q-card-section>
<q-card-section v-if="type === 'rest'">
<q-select
v-model="localAction.rest_method"
label="Method"
:options="URLActionMethods"
outlined
dense
map-options
emit-value
/>
</q-card-section>
<q-card-section v-show="type === 'rest'">
<q-toolbar>
<q-tabs v-model="tab" dense shrink>
<q-tab
name="body"
label="Request Body"
:ripple="false"
:disable="disableBodyTab"
/>
<q-tab name="headers" label="Request Headers" :ripple="false" />
</q-tabs>
</q-toolbar>
<div ref="editorDiv" :style="{ height: '30vh' }"></div>
</q-card-section>
</div>
<q-card-actions align="right">
<q-btn
v-if="type === 'rest'"
flat
label="Test"
color="primary"
@click="testWebHook"
/>
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat label="Submit" color="primary" @click="submit" />
</q-card-actions>
<q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat label="Submit" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
// composition imports
import { ref, computed, reactive, watch } from "vue";
import { useDialogPluginComponent, useQuasar, extend } from "quasar";
import { editURLAction, saveURLAction } from "@/api/core";
import { notifySuccess } from "@/utils/notify";
import { URLAction, URLActionType } from "@/types/core/urlactions";
<script>
import mixins from "@/mixins/mixins";
// ui imports
import TestURLAction from "@/components/modals/coresettings/TestURLAction.vue";
import * as monaco from "monaco-editor";
// define emits
defineEmits([...useDialogPluginComponent.emits]);
// define props
const props = defineProps<{ type: URLActionType; action?: URLAction }>();
// setup quasar
const $q = useQuasar();
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// static data
const URLActionMethods = [
{ value: "get", label: "GET" },
{ value: "post", label: "POST" },
{ value: "put", label: "PUT" },
{ value: "delete", label: "DELETE" },
{ value: "patch", label: "PATCH" },
];
const localAction: URLAction = props.action
? reactive(extend({}, props.action))
: reactive({
name: "",
desc: "",
pattern: "",
action_type: props.type,
rest_body: "{\n \n}",
rest_method: "post",
rest_headers: `{\n "Content-Type": "application/json"\n}`, // eslint-disable-line
} as URLAction);
const disableBodyTab = computed(() =>
["get", "delete"].includes(localAction.rest_method),
);
const tab = ref(disableBodyTab.value ? "headers" : "body");
watch(
() => localAction.rest_method,
() => {
disableBodyTab.value ? (tab.value = "headers") : undefined;
export default {
name: "URLActionsForm",
emits: ["hide", "ok", "cancel"],
mixins: [mixins],
props: { action: Object },
data() {
return {
localAction: {
name: "",
desc: "",
pattern: "",
},
};
},
);
async function submit() {
$q.loading.show();
try {
props.action
? await editURLAction(localAction.id, localAction)
: await saveURLAction(localAction);
onDialogOK();
notifySuccess("Url Action was edited!");
} catch (e) {}
$q.loading.hide();
}
const editorDiv = ref<HTMLElement | null>(null);
let editor: monaco.editor.IStandaloneCodeEditor;
var modelBodyUri = monaco.Uri.parse("model://body"); // a made up unique URI for our model
var modelHeadersUri = monaco.Uri.parse("model://headers"); // a made up unique URI for our model
var modelBody = monaco.editor.createModel(
localAction.rest_body,
"json",
modelBodyUri,
);
var modelHeaders = monaco.editor.createModel(
localAction.rest_headers,
"json",
modelHeadersUri,
);
function testWebHook() {
$q.dialog({
component: TestURLAction,
componentProps: {
urlAction: localAction,
computed: {
title() {
return this.editing ? "Edit URL Action" : "Add URL Action";
},
});
}
editing() {
return !!this.action;
},
},
methods: {
submit() {
this.$q.loading.show();
// watch tab change and change model
watch(tab, (newValue, oldValue) => {
if (oldValue === "body") {
localAction.rest_body = editor.getValue();
} else if (oldValue === "headers") {
localAction.rest_headers = editor.getValue();
}
let data = {
...this.localAction,
};
if (newValue === "body") {
editor.setModel(modelBody);
editor.setValue(localAction.rest_body);
} else if (newValue === "headers") {
editor.setModel(modelHeaders);
editor.setValue(localAction.rest_headers);
}
});
function loadEditor() {
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
if (!editorDiv.value) return;
editor = monaco.editor.create(editorDiv.value, {
model: tab.value === "body" ? modelBody : modelHeaders,
theme: theme,
automaticLayout: true,
minimap: { enabled: false },
quickSuggestions: false,
});
editor.onDidChangeModelContent(() => {
if (tab.value === "body") {
localAction.rest_body = editor.getValue();
} else if (tab.value === "headers") {
localAction.rest_headers = editor.getValue();
}
});
}
function cleanupEditors() {
modelBody.dispose();
modelHeaders.dispose();
editor.dispose();
}
if (this.editing) {
this.$axios
.put(`/core/urlaction/${data.id}/`, data)
.then(() => {
this.$q.loading.hide();
this.onOk();
this.notifySuccess("Url Action was edited!");
})
.catch(() => {
this.$q.loading.hide();
});
} else {
this.$axios
.post("/core/urlaction/", data)
.then(() => {
this.$q.loading.hide();
this.onOk();
this.notifySuccess("URL Action was added!");
})
.catch(() => {
this.$q.loading.hide();
});
}
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
onOk() {
this.$emit("ok");
this.hide();
},
},
mounted() {
// If pk prop is set that means we are editing
if (this.action) Object.assign(this.localAction, this.action);
},
};
</script>

View File

@@ -1,21 +1,15 @@
<template>
<div>
<div class="row">
<div class="text-subtitle2">
{{
props.type === "web"
? "URL Actions"
: "Web Hooks for Alert Failure/Resolved Actions"
}}
</div>
<div class="text-subtitle2">URL Actions</div>
<q-space />
<q-btn
size="sm"
color="grey-5"
icon="fas fa-plus"
text-color="black"
:label="`Add ${props.type === 'web' ? 'URL Action' : 'Web Hook'}`"
@click="addURLAction"
label="Add URL Action"
@click="addAction"
/>
</div>
<q-separator />
@@ -23,36 +17,31 @@
dense
:rows="actions"
:columns="columns"
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
v-model:pagination="pagination"
row-key="id"
binary-state-sort
hide-pagination
virtual-scroll
:rows-per-page-options="[0]"
:no-data-label="`No ${props.type === 'web' ? 'URL Actions' : 'Web Hooks'} added yet`"
:loading="loading"
no-data-label="No URL Actions added yet"
>
<!-- body slots -->
<template v-slot:body="props">
<q-tr
:props="props"
class="cursor-pointer"
@dblclick="editURLAction(props.row)"
@dblclick="editAction(props.row)"
>
<!-- context menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item clickable v-close-popup @click="editURLAction(props.row)">
<q-item clickable v-close-popup @click="editAction(props.row)">
<q-item-section side>
<q-icon name="edit" />
</q-item-section>
<q-item-section>Edit</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="deleteURLAction(props.row)"
>
<q-item clickable v-close-popup @click="deleteAction(props.row)">
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
@@ -68,15 +57,15 @@
</q-menu>
<!-- name -->
<q-td>
{{ truncateText(props.row.name, 30) }}
{{ props.row.name }}
</q-td>
<!-- desc -->
<q-td>
{{ truncateText(props.row.desc, 20) }}
{{ props.row.desc }}
</q-td>
<!-- pattern -->
<q-td>
{{ truncateText(props.row.pattern, 20) }}
{{ props.row.pattern }}
</q-td>
</q-tr>
</template>
@@ -84,103 +73,105 @@
</div>
</template>
<script setup lang="ts">
// composition imports
import { ref, onMounted } from "vue";
import { QTableColumn, useQuasar } from "quasar";
import { fetchURLActions, removeURLAction } from "@/api/core";
import { notifySuccess } from "@/utils/notify";
import { truncateText } from "@/utils/format";
// ui imports
<script>
import URLActionsForm from "@/components/modals/coresettings/URLActionsForm.vue";
import mixins from "@/mixins/mixins";
// types
import { type URLActionType, type URLAction } from "@/types/core/urlactions";
// define props
const props = defineProps<{ type: URLActionType }>();
// setup quasar
const $q = useQuasar();
const loading = ref(false);
const actions = ref([] as URLAction[]);
const columns: QTableColumn[] = [
{
name: "name",
label: "Name",
field: "name",
align: "left",
sortable: true,
export default {
name: "URLActionTable",
mixins: [mixins],
data() {
return {
actions: [],
pagination: {
rowsPerPage: 0,
sortBy: "name",
descending: true,
},
columns: [
{
name: "name",
label: "Name",
field: "name",
align: "left",
sortable: true,
},
{
name: "desc",
label: "Description",
field: "desc",
align: "left",
sortable: true,
},
{
name: "pattern",
label: "Pattern",
field: "pattern",
align: "left",
sortable: true,
},
],
};
},
{
name: "desc",
label: "Description",
field: "desc",
align: "left",
sortable: true,
},
{
name: "pattern",
label: "URL Pattern",
field: "pattern",
align: "left",
sortable: true,
},
];
methods: {
getURLActions() {
this.$q.loading.show();
async function getURLActions() {
$q.loading.show();
try {
const result = await fetchURLActions();
actions.value = result.filter(
(action) => action.action_type === props.type,
);
} catch (e) {
console.error(e);
}
$q.loading.hide();
}
function addURLAction() {
$q.dialog({
component: URLActionsForm,
componentProps: {
type: props.type,
this.$axios
.get("/core/urlaction/")
.then((r) => {
this.$q.loading.hide();
this.actions = r.data;
})
.catch(() => {
this.$q.loading.hide();
});
},
}).onOk(getURLActions);
}
function editURLAction(action: URLAction) {
$q.dialog({
component: URLActionsForm,
componentProps: {
type: props.type,
action: action,
addAction() {
this.$q
.dialog({
component: URLActionsForm,
})
.onOk(() => {
this.getURLActions();
});
},
}).onOk(getURLActions);
}
function deleteURLAction(action: URLAction) {
$q.dialog({
title: `Delete URL Action: ${action.name}?`,
cancel: true,
ok: { label: "Delete", color: "negative" },
}).onOk(async () => {
loading.value = true;
try {
await removeURLAction(action.id);
await getURLActions();
notifySuccess(`URL Action: ${action.name} was deleted!`);
} catch (e) {
console.error(e);
}
loading.value = false;
});
}
onMounted(getURLActions);
editAction(action) {
this.$q
.dialog({
component: URLActionsForm,
componentProps: {
action: action,
},
})
.onOk(() => {
this.getURLActions();
});
},
deleteAction(action) {
this.$q
.dialog({
title: `Delete URL Action: ${action.name}?`,
cancel: true,
ok: { label: "Delete", color: "negative" },
})
.onOk(() => {
this.$q.loading.show();
this.$axios
.delete(`/core/urlaction/${action.id}/`)
.then(() => {
this.getURLActions();
this.$q.loading.hide();
this.notifySuccess(`URL Action: ${action.name} was deleted!`);
})
.catch(() => {
this.$q.loading.hide();
});
});
},
},
mounted() {
this.getURLActions();
},
};
</script>

View File

@@ -313,19 +313,16 @@ export default {
},
getURLActions() {
this.$axios.get("/core/urlaction/").then((r) => {
this.urlActions = r.data
.filter((action) => action.action_type === "web")
.sort((a, b) => a.name.localeCompare(b.name))
.map((action) => ({
label: action.name,
value: action.id,
}));
if (this.urlActions.length === 0) {
if (r.data.length === 0) {
this.notifyWarning(
"No URL Actions configured. Go to Settings > Global Settings > URL Actions",
);
return;
}
this.urlActions = r.data.map((action) => ({
label: action.name,
value: action.id,
}));
});
},
getUserPrefs() {

View File

@@ -71,8 +71,6 @@
:readonly="readonly"
v-model="script.description"
label="Description"
type="textarea"
rows="2"
/>
<q-select
:readonly="readonly"
@@ -169,7 +167,7 @@
</div>
<q-card-actions>
<tactical-dropdown
style="width: 450px"
style="width: 350px"
dense
:loading="agentLoading"
filled
@@ -189,21 +187,7 @@
:disable="
!agent || !script.script_body || !script.default_timeout
"
@click="openTestScriptModal('agent')"
/>
<q-btn
v-if="!hosted"
size="md"
color="secondary"
dense
flat
label="Test on Server"
:disable="
!script.script_body ||
!script.default_timeout ||
!server_scripts_enabled
"
@click="openTestScriptModal('server')"
@click="openTestScriptModal"
/>
</template>
</tactical-dropdown>
@@ -231,7 +215,7 @@ import { useQuasar, useDialogPluginComponent } from "quasar";
import { saveScript, editScript, downloadScript } from "@/api/scripts";
import { useAgentDropdown, agentPlatformOptions } from "@/composables/agents";
import { generateScript } from "@/api/core";
import { notifyError, notifySuccess } from "@/utils/notify";
import { notifySuccess } from "@/utils/notify";
// ui imports
import TestScriptModal from "@/components/scripts/TestScriptModal.vue";
@@ -301,10 +285,6 @@ const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
// setup agent dropdown
const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
const hosted = computed(() => store.state.hosted);
const server_scripts_enabled = computed(
() => store.state.server_scripts_enabled,
);
// script form logic
const script: Script = props.script
@@ -325,7 +305,7 @@ const agentLoading = ref(false);
const missingShebang = computed(() => {
if (script.shell === "shell" || script.shell === "python") {
return !script.script_body.startsWith("#!");
return !script.script_body.includes("#!");
} else {
return false;
}
@@ -384,20 +364,12 @@ async function submit() {
loading.value = false;
}
function openTestScriptModal(ctx: string) {
if (ctx === "server" && !script.script_body.startsWith("#!")) {
notifyError(
"A shebang is required at the top of the script to specify the interpreter's path. Please ensure your script begins with a shebang line.",
7000,
);
return;
}
function openTestScriptModal() {
$q.dialog({
component: TestScriptModal,
componentProps: {
script: { ...script },
agent: agent.value,
ctx: ctx,
},
});
}

View File

@@ -539,7 +539,6 @@
>
{{ props.row.name }}
</q-tooltip>
<q-tooltip :delay="600">ID: {{ props.row.id }}</q-tooltip>
</q-td>
<!-- args -->
<q-td key="args" :props="props">

View File

@@ -1,26 +0,0 @@
<template>
<div class="row q-gutter-sm items-center">
<div class="col-auto">{{ label }}</div>
<div class="col-auto">
<q-btn dense flat size="md" icon="content_copy" @click="copyText">
<q-tooltip>Copy to Clipboard</q-tooltip>
</q-btn>
</div>
</div>
</template>
<script setup lang="ts">
import { copyOutput } from "@/utils/helpers";
const props = defineProps({
label: String,
data: {
type: String,
required: true,
},
});
const copyText = () => {
copyOutput(props.data);
};
</script>

View File

@@ -42,7 +42,15 @@
</q-card-section>
<q-card-section>
<q-file label="Script Upload" v-model="file" filled dense counter>
<q-file
label="Script Upload"
v-model="file"
hint="Supported file types: .ps1, .bat, .py, .sh"
filled
dense
counter
accept=".ps1, .bat, .py, .sh"
>
<template v-slot:prepend>
<q-icon name="attach_file" />
</template>

View File

@@ -18,12 +18,12 @@
</div>
<br />
<div v-if="ret.stdout">
<script-output-copy-clip label="Standard Output" :data="ret.stdout" />
Standard Output
<q-separator />
<pre>{{ ret.stdout }}</pre>
</div>
<div v-if="ret.stderr">
<script-output-copy-clip label="Standard Error" :data="ret.stderr" />
Standard Error
<q-separator />
<pre>{{ ret.stderr }}</pre>
</div>
@@ -36,20 +36,15 @@
<script>
// composition imports
import { ref, onMounted } from "vue";
import { testScript, testScriptOnServer } from "@/api/scripts";
import { testScript } from "@/api/scripts";
import { useDialogPluginComponent } from "quasar";
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
export default {
name: "TestScriptModal",
components: {
ScriptOutputCopyClip,
},
emits: [...useDialogPluginComponent.emits],
props: {
script: !Object,
agent: !String,
ctx: !String,
},
setup(props) {
// setup quasar dialog plugin
@@ -75,11 +70,7 @@ export default {
env_vars: props.script.env_vars,
};
try {
if (props.ctx === "server") {
ret.value = await testScriptOnServer(data);
} else {
ret.value = await testScript(props.agent, data);
}
ret.value = await testScript(props.agent, data);
} catch (e) {
console.error(e);
}

View File

@@ -158,7 +158,7 @@
v-model="command"
dense
filled
class="col-5"
class="col-7"
/>
<q-input
v-if="actionType === 'cmd'"
@@ -171,14 +171,12 @@
/>
<q-option-group
v-if="actionType === 'cmd'"
class="col-4 q-pl-sm"
class="col-2 q-pl-sm"
inline
v-model="shell"
:options="[
{ label: 'CMD', value: 'cmd' },
{ label: 'Batch', value: 'cmd' },
{ label: 'Powershell', value: 'powershell' },
{ label: 'Bash', value: '/bin/bash' },
{ label: 'Custom', value: 'custom' },
]"
/>
<q-btn
@@ -188,20 +186,10 @@
flat
dense
icon="add"
label="Add"
color="primary"
/>
</div>
</q-form>
<div v-if="shell === 'custom'" class="col-5">
<q-input
v-model="custom_shell"
outlined
label="Custom shell"
stack-label
placeholder="/usr/bin/python3"
/>
</div>
<div class="text-subtitle2 q-pa-sm">
Actions:
<q-checkbox
@@ -317,7 +305,6 @@
<!-- expires on input -->
<q-input
v-if="!isPosix"
class="col-6 q-pa-sm"
type="datetime-local"
dense
@@ -346,10 +333,7 @@
</q-card-section>
<!-- daily options -->
<q-card-section
v-if="!isPosix && state.task_type === 'daily'"
class="row"
>
<q-card-section v-if="state.task_type === 'daily'" class="row">
<!-- daily interval -->
<q-input
:rules="[
@@ -376,7 +360,6 @@
<q-card-section v-if="state.task_type === 'weekly'" class="row">
<!-- weekly interval -->
<q-input
v-if="!isPosix"
:rules="[
(val) => !!val || '*Required',
(val) =>
@@ -619,11 +602,8 @@
"
class="row"
>
<div v-if="!isPosix" class="col-12 text-h6">
Advanced Settings (Windows only)
</div>
<div class="col-12 text-h6">Advanced Settings</div>
<q-input
v-if="!isPosix"
class="col-6 q-pa-sm"
dense
label="Repeat task every"
@@ -640,7 +620,6 @@
/>
<q-input
v-if="!isPosix"
:disable="!state.task_repetition_interval"
class="col-6 q-pa-sm"
dense
@@ -664,7 +643,6 @@
/>
<q-checkbox
v-if="!isPosix"
:disable="!state.task_repetition_interval"
class="col-6 q-pa-sm"
dense
@@ -674,7 +652,6 @@
<div class="col-6"></div>
<q-input
v-if="!isPosix"
class="col-6 q-pa-sm"
dense
label="Random task delay"
@@ -691,7 +668,6 @@
/>
<div class="col-6"></div>
<q-checkbox
v-if="!isPosix"
:disable="!state.expire_date"
class="col-6 q-pa-sm"
dense
@@ -702,7 +678,6 @@
</q-checkbox>
<div class="col-6"></div>
<q-checkbox
v-if="!isPosix"
:disable="state.task_type === 'runonce'"
class="col-6 q-pa-sm"
dense
@@ -713,7 +688,6 @@
<div class="col-6"></div>
<tactical-dropdown
v-if="!isPosix"
class="col-6 q-pa-sm"
label="Task instance policy"
:options="taskInstancePolicyOptions"
@@ -781,7 +755,7 @@
<script>
// composition imports
import { computed, ref, watch, onMounted, defineComponent } from "vue";
import { ref, watch, onMounted } from "vue";
import { useDialogPluginComponent } from "quasar";
import draggable from "vuedraggable";
import { saveTask, updateTask } from "@/api/tasks";
@@ -869,14 +843,13 @@ const taskInstancePolicyOptions = [
{ label: "Stop Existing", value: 3 },
];
export default defineComponent({
export default {
components: { TacticalDropdown, draggable },
name: "AddAutomatedTask",
emits: [...useDialogPluginComponent.emits],
props: {
parent: Object, // parent policy or agent for task
task: Object, // only for editing
plat: String,
},
setup(props) {
// setup quasar dialog
@@ -885,25 +858,20 @@ export default defineComponent({
// setup dropdowns
const {
script,
scriptName,
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
} = useScriptDropdown({
} = useScriptDropdown(undefined, {
onMount: true,
});
// set defaultTimeout to 30
defaultTimeout.value = 30;
const { checkOptions, getCheckOptions } = useCheckDropdown(props.parent);
const { checkOptions, getCheckOptions } = useCheckDropdown();
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
const isPosix = computed(() => {
return !!props.plat && props.plat !== "windows";
});
// add task logic
const task = props.task
? ref(Object.assign({}, props.task))
@@ -937,7 +905,6 @@ export default defineComponent({
const actionType = ref("script");
const command = ref("");
const shell = ref("cmd");
const custom_shell = ref("");
const monthlyType = ref("days");
const collector = ref(false);
const loading = ref(false);
@@ -985,23 +952,19 @@ export default defineComponent({
if (actionType.value === "script") {
task.value.actions.push({
type: "script",
name: scriptName.value,
name: scriptOptions.value.find(
(option) => option.value === script.value,
).label,
script: script.value,
timeout: defaultTimeout.value,
script_args: defaultArgs.value,
env_vars: defaultEnvVars.value,
});
} else if (actionType.value === "cmd") {
let tempShell = shell.value;
if (shell.value === "custom" && !!custom_shell.value) {
tempShell = custom_shell.value;
} else {
tempShell = shell.value;
}
task.value.actions.push({
type: "cmd",
command: command.value,
shell: tempShell,
shell: shell.value,
timeout: defaultTimeout.value,
});
}
@@ -1176,7 +1139,6 @@ export default defineComponent({
actionType,
command,
shell,
custom_shell,
allMonthsCheckbox,
allMonthDaysCheckbox,
allWeekDaysCheckbox,
@@ -1190,7 +1152,6 @@ export default defineComponent({
scriptOptions,
checkOptions,
customFieldOptions,
isPosix,
// non-reactive data
validateTimePeriod,
@@ -1218,7 +1179,7 @@ export default defineComponent({
onDialogHide,
};
},
});
};
</script>
<style scoped>

View File

@@ -1,5 +1,5 @@
import { ref, onMounted } from "vue";
import { fetchUsers, fetchRoles } from "@/api/accounts";
import { fetchUsers } from "@/api/accounts";
import { formatUserOptions } from "@/utils/format";
export function useUserDropdown(onMount = false) {
@@ -44,26 +44,3 @@ export function useUserDropdown(onMount = false) {
getDynamicUserOptions,
};
}
export function useRoleDropdown(opts = {}) {
const roleOptions = ref([]);
async function getRoleOptions() {
const roles = await fetchRoles();
roleOptions.value = roles.map((role) => ({
value: role.id,
label: role.name,
}));
}
if (opts.onMount) {
onMounted(getRoleOptions);
}
return {
//data
roleOptions,
//methods
getRoleOptions,
};
}

View File

@@ -1,10 +1,10 @@
import { ref, computed, onMounted } from "vue";
import { computed, ref } from "vue";
import { useStore } from "vuex";
import { fetchAgents } from "@/api/agents";
import { formatAgentOptions } from "@/utils/format";
// agent dropdown
export function useAgentDropdown(opts = {}) {
export function useAgentDropdown() {
const agent = ref(null);
const agents = ref([]);
const agentOptions = ref([]);
@@ -13,14 +13,10 @@ export function useAgentDropdown(opts = {}) {
async function getAgentOptions(flat = false) {
agentOptions.value = formatAgentOptions(
await fetchAgents({ detail: false }),
flat,
flat
);
}
if (opts.onMount) {
onMounted(getAgentOptions);
}
return {
//data
agent,

28
src/composables/core.js Normal file
View File

@@ -0,0 +1,28 @@
import { ref, onMounted } from "vue";
import { fetchCustomFields } from "@/api/core";
import { formatCustomFieldOptions } from "@/utils/format";
export function useCustomFieldDropdown({ onMount = false }) {
const customFieldOptions = ref([]);
// type can be "client", "site", or "agent"
async function getCustomFieldOptions(model = null, flat = false) {
const params = {};
if (model) params[model] = model;
customFieldOptions.value = formatCustomFieldOptions(
await fetchCustomFields(params),
flat
);
}
if (onMount) onMounted(getCustomFieldOptions);
return {
//data
customFieldOptions,
//methods
getCustomFieldOptions,
};
}

View File

@@ -1,88 +0,0 @@
import { ref, computed, onMounted } from "vue";
import { fetchCustomFields, fetchURLActions } from "@/api/core";
import {
formatCustomFieldOptions,
formatURLActionOptions,
} from "@/utils/format";
import type { CustomField } from "@/types/core/customfields";
import type { URLAction } from "@/types/core/urlactions";
export interface URLActionOption extends URLAction {
value: number;
label: string;
}
export interface CustomFieldOption extends CustomField {
value: number;
label: string;
}
export interface UseCustomFieldDropdownParams {
onMount?: boolean;
}
export function useCustomFieldDropdown(opts: UseCustomFieldDropdownParams) {
const customFieldOptions = ref([] as CustomFieldOption[]);
// type can be "client", "site", or "agent"
async function getCustomFieldOptions(model = null, flat = false) {
const params = {};
if (model) params[model] = model;
customFieldOptions.value = formatCustomFieldOptions(
await fetchCustomFields(params),
flat,
);
}
const restActionOptions = computed(() =>
customFieldOptions.value.filter((option) => option.type === "rest"),
);
if (opts.onMount) onMounted(getCustomFieldOptions);
return {
customFieldOptions,
restActionOptions,
//methods
getCustomFieldOptions,
};
}
export interface UseURLActionDropdownParams {
onMount?: boolean;
}
export function useURLActionDropdown(opts: UseURLActionDropdownParams) {
const urlActionOptions = ref([] as URLActionOption[]);
// type can be "client", "site", or "agent"
async function getURLActionOptions(flat = false) {
const params = {};
urlActionOptions.value = formatURLActionOptions(
await fetchURLActions(params),
flat,
);
}
const webActionOptions = computed(() =>
urlActionOptions.value.filter((action) => action.action_type === "web"),
);
const restActionOptions = computed(() =>
urlActionOptions.value.filter((action) => action.action_type === "rest"),
);
if (opts?.onMount) onMounted(getURLActionOptions);
return {
urlActionOptions,
restActionOptions,
webActionOptions,
//methods
getURLActionOptions,
};
}

View File

@@ -0,0 +1,70 @@
import { ref, watch, computed, onMounted } from "vue";
import { useStore } from "vuex";
import { fetchScripts } from "@/api/scripts";
import { formatScriptOptions } from "@/utils/format";
// script dropdown
export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
const scriptOptions = ref([]);
const defaultTimeout = ref(30);
const defaultArgs = ref([]);
const defaultEnvVars = ref([]);
const script = ref(setScript);
const syntax = ref("");
const link = ref("");
const baseUrl =
"https://github.com/amidaware/community-scripts/blob/main/scripts/";
// specify parameters to filter out community scripts
async function getScriptOptions(showCommunityScripts = false) {
scriptOptions.value = Object.freeze(
formatScriptOptions(await fetchScripts({ showCommunityScripts })),
);
}
// watch scriptPk for changes and update the default timeout and args
watch([script, scriptOptions], () => {
if (script.value && scriptOptions.value.length > 0) {
const tmpScript = scriptOptions.value.find(
(i) => i.value === script.value,
);
defaultTimeout.value = tmpScript.timeout;
defaultArgs.value = tmpScript.args;
defaultEnvVars.value = tmpScript.env_vars;
syntax.value = tmpScript.syntax;
link.value =
tmpScript.script_type === "builtin"
? `${baseUrl}${tmpScript.filename}`
: null;
}
});
// vuex show community scripts
const store = useStore();
const showCommunityScripts = computed(() => store.state.showCommunityScripts);
if (onMount) onMounted(() => getScriptOptions(showCommunityScripts.value));
return {
//data
script,
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
syntax,
link,
//methods
getScriptOptions,
};
}
export const shellOptions = [
{ label: "Powershell", value: "powershell" },
{ label: "Batch", value: "cmd" },
{ label: "Python", value: "python" },
{ label: "Shell", value: "shell" },
{ label: "Nushell", value: "nushell" },
{ label: "Deno", value: "deno" },
];

View File

@@ -1,141 +0,0 @@
import { ref, watch, computed, onMounted } from "vue";
import { useStore } from "vuex";
import { fetchScripts } from "@/api/scripts";
import {
formatScriptOptions,
removeExtraOptionCategories,
} from "@/utils/format";
import type { Script } from "@/types/scripts";
import { AgentPlatformType } from "@/types/agents";
export interface ScriptOption extends Script {
label: string;
value: number;
}
export interface useScriptDropdownParams {
script?: number; // set a selected script on init
plat?: AgentPlatformType; // set a platform for filterByPlatform
onMount?: boolean; // loads script options on mount
}
// script dropdown
export function useScriptDropdown(opts?: useScriptDropdownParams) {
const scriptOptions = ref([] as ScriptOption[]);
const defaultTimeout = ref(30);
const defaultArgs = ref([] as string[]);
const defaultEnvVars = ref([] as string[]);
const script = ref(opts?.script);
const scriptName = ref("");
const syntax = ref<string | undefined>("");
const link = ref<string | undefined>("");
const plat = ref<AgentPlatformType | undefined>(opts?.plat);
const baseUrl =
"https://github.com/amidaware/community-scripts/blob/main/scripts/";
// specify parameters to filter out community scripts
async function getScriptOptions() {
scriptOptions.value = Object.freeze(
formatScriptOptions(
await fetchScripts({
showCommunityScripts: showCommunityScripts.value,
}),
),
) as ScriptOption[];
}
// watch scriptPk for changes and update the default timeout and args
watch([script, scriptOptions], () => {
if (script.value && scriptOptions.value.length > 0) {
const tmpScript = scriptOptions.value.find(
(i) => i.value === script.value,
);
if (tmpScript) {
defaultTimeout.value = tmpScript.default_timeout;
defaultArgs.value = tmpScript.args;
defaultEnvVars.value = tmpScript.env_vars;
syntax.value = tmpScript.syntax;
scriptName.value = tmpScript.label;
link.value =
tmpScript.script_type === "builtin"
? `${baseUrl}${tmpScript.filename}`
: undefined;
}
}
});
// vuex show community scripts
const store = useStore();
const showCommunityScripts = computed(() => store.state.showCommunityScripts);
// filter for only getting server tasks
const serverScriptOptions = computed(
() =>
removeExtraOptionCategories(
scriptOptions.value.filter(
(script) =>
script.category ||
!script.supported_platforms ||
script.supported_platforms.length === 0 ||
script.supported_platforms.includes("linux"),
),
) as ScriptOption[],
);
const filterByPlatformOptions = computed(() => {
if (!plat.value) {
return scriptOptions.value;
} else {
return removeExtraOptionCategories(
scriptOptions.value.filter(
(script) =>
script.category ||
!script.supported_platforms ||
script.supported_platforms.length === 0 ||
script.supported_platforms.includes(plat.value!),
),
) as ScriptOption[];
}
});
function reset() {
defaultTimeout.value = 30;
defaultArgs.value = [];
defaultEnvVars.value = [];
script.value = undefined;
syntax.value = "";
link.value = "";
}
if (opts?.onMount) onMounted(() => getScriptOptions());
return {
//data
script,
defaultTimeout,
defaultArgs,
defaultEnvVars,
scriptName,
syntax,
link,
plat,
scriptOptions, // unfiltered options
serverScriptOptions, // only scripts that can run on server
filterByPlatformOptions, // use the returned plat to change options
//methods
getScriptOptions,
reset, // resets dropdown selection state
};
}
export const shellOptions = [
{ label: "Powershell", value: "powershell" },
{ label: "Batch", value: "cmd" },
{ label: "Python", value: "python" },
{ label: "Shell", value: "shell" },
{ label: "Nushell", value: "nushell" },
{ label: "Deno", value: "deno" },
];

View File

@@ -4,7 +4,7 @@ Copyright (c) 2023 Amidaware Inc. All rights reserved.
This Agreement is entered into between the licensee ("You" or the "Licensee") and Amidaware Inc. ("Amidaware") and governs the use of the enterprise features of the Tactical RMM Software (hereinafter referred to as the "Software").
The EE features of the Software, including but not limited to SSO (Single Sign-On), Reporting and White-labeling, are exclusively contained within directories named "ee," "enterprise," or "premium" in Amidaware's repositories, or in any files bearing the EE License header. The use of the Software is also governed by the terms and conditions set forth in the Tactical RMM License, available at https://license.tacticalrmm.com, which terms are incorporated herein by reference.
The EE features of the Software, including but not limited to Reporting and White-labeling, are exclusively contained within directories named "ee," "enterprise," or "premium" in Amidaware's repositories, or in any files bearing the EE License header. The use of the Software is also governed by the terms and conditions set forth in the Tactical RMM License, available at https://license.tacticalrmm.com, which terms are incorporated herein by reference.
## License Grant

View File

@@ -1,144 +0,0 @@
/*
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
*/
import axios from "axios";
import { getCookie } from "@/ee/sso/utils/cookies";
import { getBaseUrl } from "@/boot/axios";
import { useStorage } from "@vueuse/core";
import type {
SSOAccount,
SSOProvider,
SSOSettingsType,
} from "@/ee/sso/types/sso";
const baseUrl = "accounts";
interface FormData {
provider: string;
process: string;
callback_url: string;
csrfmiddlewaretoken: string;
}
export function getCSRFToken() {
return getCookie("csrftoken");
}
// needed for sso provider redirect
function postForm(url: string, data: FormData) {
const f = document.createElement("form");
f.method = "POST";
f.action = url;
for (const key in data) {
const d = document.createElement("input");
d.type = "hidden";
d.name = key;
d.value = data[key];
f.appendChild(d);
}
document.body.appendChild(f);
f.submit();
}
// sso providers
export async function fetchSSOProviders(): Promise<SSOProvider[]> {
const { data } = await axios.get(`${baseUrl}/ssoproviders/`);
return data;
}
export async function addSSOProvider(payload: SSOProvider) {
const { data } = await axios.post(`${baseUrl}/ssoproviders/`, payload);
return data;
}
export async function editSSOProvider(id: number, payload: SSOProvider) {
const { data } = await axios.put(`${baseUrl}/ssoproviders/${id}/`, payload);
return data;
}
export async function removeSSOProvider(id: number) {
const { data } = await axios.delete(`${baseUrl}/ssoproviders/${id}/`);
return data;
}
export async function fetchSSOSettings(): Promise<SSOSettingsType> {
const { data } = await axios.get(`${baseUrl}/ssoproviders/settings/`);
return data;
}
export async function updateSSOSettings(settings: SSOSettingsType) {
const { data } = await axios.post(
`${baseUrl}/ssoproviders/settings/`,
settings,
);
return data;
}
export async function getSSOProviderToken() {
const { data } = await axios.post(
`${baseUrl}/ssoproviders/token/`,
{},
{
headers: { "X-CSRFToken": getCSRFToken() },
},
);
return data;
}
export async function disconnectSSOAccount(
provider: string,
account: string,
): Promise<SSOAccount> {
const { data } = await axios.delete(`${baseUrl}/ssoproviders/account/`, {
data: { provider, account },
});
return data;
}
// allauth
const allauthBase = "_allauth/browser/v1";
export interface AllAuthResponse<T> {
data: T;
status: number;
meta?: {
is_autheticated: boolean;
};
}
export interface SSOProviderConfig {
client_id: string;
flows: string[];
id: string;
name: string;
}
export interface SSOConfigResponse {
socialaccount: {
providers: SSOProviderConfig[];
};
}
export async function getSSOConfig(): Promise<
AllAuthResponse<SSOConfigResponse>
> {
const { data } = await axios.get(`${allauthBase}/config/`);
return data;
}
export async function openSSOProviderRedirect(id: string) {
//save provider to local storage
useStorage("provider_id", id);
postForm(`${getBaseUrl()}/${allauthBase}/auth/provider/redirect/`, {
provider: id,
process: "login",
callback_url: `${location.origin}/account/provider/callback`,
csrfmiddlewaretoken: getCSRFToken() || "",
});
}

View File

@@ -1,142 +0,0 @@
<!--
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
-->
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card style="width: 60vw; max-width: 90vw; min-height: 40vh">
<q-bar>
Connected Social Accounts for {{ user.username }}
<q-space />
<q-btn v-close-popup dense flat icon="close">
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-table
dense
:table-class="{
'table-bgcolor': !$q.dark.isActive,
'table-bgcolor-dark': $q.dark.isActive,
}"
:style="{ 'max-height': `${$q.screen.height - 24}px` }"
class="tbl-sticky"
:rows="user.social_accounts"
:columns="columns"
:loading="loading"
:pagination="{ rowsPerPage: 0, sortBy: 'display', descending: true }"
row-key="id"
binary-state-sort
virtual-scroll
:rows-per-page-options="[0]"
>
<template #body="props">
<q-tr>
<!-- rows -->
<td>{{ props.row.display }}</td>
<td>{{ props.row.provider }}</td>
<td>{{ formatDate(props.row.last_login) }}</td>
<td>{{ formatDate(props.row.date_joined) }}</td>
<td>
<q-btn
size="sm"
@click="removeSSOAccount(props.row)"
label="Disconnect"
color="negative"
></q-btn>
</td>
</q-tr>
</template>
</q-table>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
// composition imports
import { ref } from "vue";
import { useDialogPluginComponent, useQuasar, type QTableColumn } from "quasar";
import { disconnectSSOAccount } from "@/ee/sso/api/sso";
import { notifySuccess } from "@/utils/notify";
import { useAuthStore } from "@/stores/auth";
import { formatDate } from "@/utils/format";
//types
import type { SSOAccount, SSOUser } from "../types/sso";
const columns: QTableColumn[] = [
{
name: "display",
label: "Display Name",
field: "display",
align: "left",
sortable: true,
},
{
name: "provider",
label: "Provider",
field: "provider",
align: "left",
sortable: true,
},
{
name: "last_login",
label: "Last Login",
field: "last_login",
align: "left",
sortable: true,
},
{
name: "date_joined",
label: "Date Joined",
field: "date_joined",
align: "left",
sortable: true,
},
{
name: "action",
label: "",
field: "action",
align: "left",
sortable: true,
},
];
// emits
defineEmits([...useDialogPluginComponent.emits]);
// props
const props = defineProps<{
user: SSOUser;
}>();
const { dialogRef, onDialogHide } = useDialogPluginComponent();
const $q = useQuasar();
const auth = useAuthStore();
const loading = ref(false);
function removeSSOAccount(account: SSOAccount) {
$q.dialog({
title: `Disconnect social account: ${account.display}?`,
cancel: true,
ok: { label: "Delete", color: "negative" },
}).onOk(async () => {
loading.value = true;
try {
await disconnectSSOAccount(account.provider, account.uid);
notifySuccess("Social account disconnected successfully");
if (
auth.username === props.user.username &&
auth.ssoLoginProvider === account.provider
) {
await auth.logout();
}
} finally {
loading.value = false;
onDialogHide();
}
});
}
</script>

View File

@@ -1,160 +0,0 @@
<!--
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
-->
<template>
<q-dialog persistent ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin" style="width: 35vw; max-width: 35vw">
<q-bar>
{{ props.provider ? "Edit OIDC Provider" : "Add OIDC Provider" }}
<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>
<!-- name -->
<q-card-section>
<q-input
:readonly="!!props.provider"
:disable="!!props.provider"
label="Provider Name"
outlined
dense
v-model="localProvider.name"
:rules="[
(val) => !!val || '*Required',
(val) =>
/^[a-zA-Z0-9_-]+$/.test(val) ||
'Only letters, numbers, hyphens, and underscores are allowed',
]"
hint="A unique identifier for the SSO provider. Avoid spaces and special characters, as this will be part of the callback URL."
/>
</q-card-section>
<!-- url -->
<q-card-section>
<q-input
label="Issuer URL"
outlined
dense
v-model="localProvider.server_url"
:rules="[(val) => !!val || '*Required']"
hint="The OpenID Connect Issuer URL provided by the SSO provider. This is typically the base URL where the provider hosts their OIDC configuration."
/>
</q-card-section>
<!-- client id -->
<q-card-section>
<q-input
label="Client ID"
outlined
dense
v-model="localProvider.client_id"
:rules="[(val) => !!val || '*Required']"
/>
</q-card-section>
<!-- secret -->
<q-card-section>
<q-input
v-model="localProvider.secret"
filled
:type="hideSecret ? 'password' : 'text'"
label="Secret"
outlined
dense
:rules="[(val) => !!val || '*Required']"
>
<template v-slot:append>
<q-icon
:name="hideSecret ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="hideSecret = !hideSecret"
/>
</template>
</q-input>
</q-card-section>
<q-card-section>
<tactical-dropdown
label="Default User Role"
:options="roleOptions"
outlined
dense
clearable
mapOptions
filled
v-model="localProvider.role"
hint="The role assigned to users upon first sign-in through this provider."
/>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup />
<q-btn
flat
label="Submit"
color="primary"
:loading="loading"
@click="submit"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
// composition imports
import { ref, reactive } from "vue";
import { useDialogPluginComponent, extend } from "quasar";
import { editSSOProvider, addSSOProvider } from "@/ee/sso/api/sso";
import { notifySuccess } from "@/utils/notify";
import { useRoleDropdown } from "@/composables/accounts";
// components
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
// types
import type { SSOProvider } from "@/ee/sso/types/sso";
// define emits
defineEmits([...useDialogPluginComponent.emits]);
// define props
const props = defineProps<{ provider?: SSOProvider }>();
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
const loading = ref(false);
const { roleOptions } = useRoleDropdown({ onMount: true });
const hideSecret = ref(true);
const localProvider: SSOProvider = props.provider
? reactive(extend({}, props.provider))
: reactive({
id: 0,
name: "",
client_id: "",
secret: "",
server_url: "",
role: null,
} as SSOProvider);
async function submit() {
loading.value = true;
try {
props.provider
? await editSSOProvider(localProvider.id, localProvider)
: await addSSOProvider(localProvider);
onDialogOK();
notifySuccess("SSO Provider was edited!");
} catch (e) {}
loading.value = false;
}
</script>

View File

@@ -1,293 +0,0 @@
<!--
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
-->
<template>
<div>
<div class="row">
<div class="text-subtitle2">SSO Providers</div>
<q-space />
<q-btn
size="sm"
color="grey-5"
icon="fas fa-plus"
text-color="black"
label="Add OIDC Provider"
@click="addSSOProvider"
:disable="!ssoSettings.sso_enabled"
>
<q-tooltip v-if="!ssoSettings.sso_enabled" class="text-caption"
>Enable SSO in the settings to allow adding a provider.</q-tooltip
>
</q-btn>
</div>
<q-separator />
<q-table
dense
:rows="providers"
:columns="columns"
:visible-columns="visibleColumns"
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
row-key="id"
binary-state-sort
hide-pagination
virtual-scroll
:rows-per-page-options="[0]"
no-data-label="No OIDC Providers added yet"
:loading="loading"
>
<template v-slot:top>
<q-btn
@click="openSSOSettings"
label="SSO Settings"
no-caps
color="primary"
size="md"
/>
</template>
<!-- body slots -->
<template v-slot:body="props">
<q-tr
:props="props"
class="cursor-pointer"
@dblclick="editSSOProvider(props.row)"
>
<!-- context menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item
clickable
v-close-popup
@click="editSSOProvider(props.row)"
>
<q-item-section side>
<q-icon name="edit" />
</q-item-section>
<q-item-section>Edit</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="deleteSSOProvider(props.row)"
>
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
<q-item-section>Delete</q-item-section>
</q-item>
<q-separator></q-separator>
<!-- callback url -->
<q-item
clickable
v-close-popup
@click="getCallbackURL(props.row.callback_url)"
>
<q-item-section side>
<q-icon name="description" />
</q-item-section>
<q-item-section>Copy Callback URL</q-item-section>
</q-item>
<!-- javascript origin url (used by google oauth) -->
<q-item
clickable
v-close-popup
@click="getCallbackURL(props.row.javascript_origin_url)"
>
<q-item-section side>
<q-icon name="description" />
</q-item-section>
<q-item-section
>Copy Authorized JavaScript origin</q-item-section
>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
</q-menu>
<!-- name -->
<q-td>
{{ truncateText(props.row.name, 25) }}
<q-tooltip>{{ props.row.name }}</q-tooltip>
</q-td>
<!-- server_url -->
<q-td>
{{ truncateText(props.row.server_url, 20) }}
<q-tooltip>{{ props.row.server_url }}</q-tooltip>
</q-td>
<!-- pattern -->
<q-td>
{{ truncateText(props.row.client_id, 20) }}
<q-tooltip>{{ props.row.client_id }}</q-tooltip>
</q-td>
<q-td>
<q-icon
size="sm"
name="content_copy"
@click="getCallbackURL(props.row.callback_url)"
>
<q-tooltip>Copy Callback URL to Clipboard</q-tooltip>
</q-icon>
</q-td>
</q-tr>
</template>
</q-table>
</div>
</template>
<script setup lang="ts">
// composition imports
import { computed, ref, onMounted } from "vue";
import { useStore } from "vuex";
import { QTableColumn, useQuasar, copyToClipboard } from "quasar";
import {
fetchSSOProviders,
removeSSOProvider,
fetchSSOSettings,
} from "@/ee/sso/api/sso";
import { notifySuccess } from "@/utils/notify";
import { truncateText } from "@/utils/format";
// ui imports
import SSOProvidersForm from "@/ee/sso/components/SSOProvidersForm.vue";
// types
import { type SSOProvider, SSOSettingsType } from "@/ee/sso/types/sso";
import SSOSettings from "@/ee/sso/components/SSOSettings.vue";
// setup quasar
const $q = useQuasar();
// setup vuew store
const store = useStore();
const loading = ref(false);
const providers = ref([] as SSOProvider[]);
const ssoSettings = ref({} as SSOSettingsType);
const columns: QTableColumn[] = [
{
name: "name",
label: "Name",
field: "name",
align: "left",
sortable: true,
},
{
name: "server_url",
label: "Server Url",
field: "server_url",
align: "left",
sortable: true,
},
{
name: "client_id",
label: "Client ID",
field: "client_id",
align: "left",
sortable: true,
},
{
name: "callback_url",
label: "Callback URL",
field: "callback_url",
align: "left",
sortable: false,
},
{
name: "javascript_origin_url",
label: "Javascript Origin URL",
field: "javascript_origin_url",
align: "left",
sortable: false,
},
];
const visibleColumns = computed(() => {
return columns
.map((column) => column.name)
.filter((name) => name !== "javascript_origin_url");
});
async function getSSOSettings() {
try {
ssoSettings.value = await fetchSSOSettings();
} catch (e) {
console.error(e);
}
}
async function getSSOProviders() {
loading.value = true;
try {
providers.value = await fetchSSOProviders();
} catch (e) {
console.error(e);
}
loading.value = false;
}
function addSSOProvider() {
$q.dialog({
component: SSOProvidersForm,
}).onOk(getSSOProviders);
}
function editSSOProvider(provider: SSOProvider) {
$q.dialog({
component: SSOProvidersForm,
componentProps: {
provider: provider,
},
}).onOk(getSSOProviders);
}
function deleteSSOProvider(provider: SSOProvider) {
$q.dialog({
title: `Delete SSO Provider: ${provider.name}?`,
cancel: true,
ok: { label: "Delete", color: "negative" },
}).onOk(async () => {
loading.value = true;
try {
await removeSSOProvider(provider.id);
await getSSOProviders();
notifySuccess(`SSO Provider: ${provider.name} was deleted!`);
} catch (e) {
console.error(e);
}
loading.value = false;
});
}
function getCallbackURL(url: string) {
copyToClipboard(url).then(() => {
notifySuccess("URL copied!");
});
}
function openSSOSettings() {
$q.dialog({
component: SSOSettings,
}).onOk((updatedSSOSettings: SSOSettingsType) => {
store.commit(
"setBlockLocalUserLogon",
updatedSSOSettings.block_local_user_logon,
);
ssoSettings.value = { ...updatedSSOSettings };
});
}
onMounted(async () => {
await getSSOSettings();
await getSSOProviders();
});
</script>

View File

@@ -1,112 +0,0 @@
<!--
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
-->
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin" style="width: 50">
<q-bar>
SSO Settings
<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>
<!-- disable sso-->
<q-card-section>
<q-checkbox
dense
label="Enable SSO"
v-model="ssoSettings.sso_enabled"
/>
</q-card-section>
<!-- block local user logon -->
<q-card-section>
<q-checkbox
dense
label="Block Local User Login"
v-model="ssoSettings.block_local_user_logon"
:disable="!ssoSettings.sso_enabled"
hint="When enabled, only users with SSO accounts can log in, with the exception of local superuser accounts."
>
<q-tooltip class="text-caption"
>When enabled, only users with SSO accounts can log in, with the
exception of local superuser accounts.</q-tooltip
>
</q-checkbox>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup />
<q-btn
flat
label="Submit"
color="primary"
:loading="loading"
@click="submit"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
// composition imports
import { ref, watch, onMounted } from "vue";
import { useDialogPluginComponent } from "quasar";
import { notifySuccess, notifyWarning } from "@/utils/notify";
import { fetchSSOSettings, updateSSOSettings } from "@/ee/sso/api/sso";
// types
import { SSOSettingsType } from "../types/sso";
// define emits
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
const ssoSettings = ref({} as SSOSettingsType);
const loading = ref(false);
async function getSSOSettings() {
loading.value = true;
try {
ssoSettings.value = await fetchSSOSettings();
} catch (e) {
console.error(e);
}
loading.value = false;
}
async function submit() {
loading.value = true;
try {
await updateSSOSettings(ssoSettings.value);
notifySuccess("Settings updated successfully");
onDialogOK(ssoSettings.value);
} catch (e) {
if (e.status === 423) {
notifyWarning(e.response.data, 7000);
}
console.error(e);
}
loading.value = false;
}
onMounted(async () => {
await getSSOSettings();
// watcher to disable block local login if sso is disabled
watch(
() => ssoSettings.value.sso_enabled,
(newValue) => {
if (!newValue) {
ssoSettings.value.block_local_user_logon = false;
}
},
);
});
</script>

View File

@@ -1,33 +0,0 @@
/*
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
*/
import { User } from "@/types/accounts";
export interface SSOProvider {
id: number;
name: string;
provider_id: string;
client_id: string;
secret: string;
server_url: string;
role: number | null;
}
export interface SSOAccount {
uid: string;
display: string;
provider: string;
last_login: string;
date_joined: string;
}
export interface SSOUser extends User {
social_accounts: SSOAccount[];
}
export interface SSOSettingsType {
sso_enabled: boolean;
block_local_user_logon: boolean;
}

View File

@@ -1,21 +0,0 @@
/*
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
*/
export function getCookie(name: string) {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === name + "=") {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}

View File

@@ -1,32 +0,0 @@
<!--
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
-->
<template>
<div class="fixed-center text-center" v-if="error">
<p class="text-faded">There was an error logging into your provider.</p>
<q-btn color="secondary" style="width: 200px" to="/login"
>Go back to Login</q-btn
>
</div>
</template>
<script lang="ts" setup>
import { useRoute, useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth";
const route = useRoute();
const error = route.query.error;
const router = useRouter();
const auth = useAuthStore();
if (!error) {
if (auth.loggedIn) {
router.push({ name: "Dashboard" });
} else {
router.push({ name: "Login" });
}
}
</script>

View File

@@ -84,16 +84,7 @@
checked-icon="nights_stay"
unchecked-icon="wb_sunny"
/>
<!-- web terminal button -->
<q-btn
v-if="!hosted"
label=">_"
dense
flat
@click="openWebTerm"
class="q-mr-sm"
style="font-size: 16px"
/>
<!-- Devices Chip -->
<q-chip class="cursor-pointer">
<q-avatar size="md" icon="devices" color="primary" />
@@ -106,7 +97,7 @@
<q-item-label header>Servers</q-item-label>
<q-item>
<q-item-section avatar>
<q-icon name="dns" size="sm" color="primary" />
<q-icon name="fa fa-server" size="sm" color="primary" />
</q-item-section>
<q-item-section no-wrap>
@@ -157,7 +148,7 @@
<AlertsIcon />
<q-btn-dropdown flat no-caps stretch :label="displayName || ''">
<q-btn-dropdown flat no-caps stretch :label="user">
<q-list>
<q-item
clickable
@@ -209,131 +200,187 @@
</q-page-container>
</q-layout>
</template>
<script setup lang="ts">
<script>
// composition imports
import { computed, onMounted, onBeforeUnmount, ref } from "vue";
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { useQuasar } from "quasar";
import { useStore } from "vuex";
import { useDashboardStore } from "@/stores/dashboard";
import { useAuthStore } from "@/stores/auth";
import { storeToRefs } from "pinia";
import { resetTwoFactor } from "@/api/accounts";
import { notifyError, notifySuccess } from "@/utils/notify";
import axios from "axios";
// webtermn
import { checkWebTermPerms, openWebTerminal } from "@/api/core";
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";
const store = useStore();
const $q = useQuasar();
export default {
name: "MainLayout",
components: { AlertsIcon },
setup() {
const store = useStore();
const $q = useQuasar();
const {
serverCount,
serverOfflineCount,
workstationCount,
workstationOfflineCount,
daysUntilCertExpires,
} = storeToRefs(useDashboardStore());
const darkMode = computed({
get: () => {
return $q.dark.isActive;
},
set: (value) => {
axios.patch("/accounts/users/ui/", { dark_mode: value });
$q.dark.set(value);
},
});
const { displayName } = storeToRefs(useAuthStore());
const currentTRMMVersion = computed(() => store.state.currentTRMMVersion);
const latestTRMMVersion = computed(() => store.state.latestTRMMVersion);
const needRefresh = computed(() => store.state.needrefresh);
const user = computed(() => store.state.username);
const hosted = computed(() => store.state.hosted);
const tokenExpired = computed(() => store.state.tokenExpired);
const dash_warning_color = computed(() => store.state.dash_warning_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const darkMode = computed({
get: () => {
return $q.dark.isActive;
},
set: (value) => {
axios.patch("/accounts/users/ui/", { dark_mode: value });
$q.dark.set(value);
},
});
const latestReleaseURL = computed(() => {
return latestTRMMVersion.value
? `https://github.com/amidaware/tacticalrmm/releases/tag/v${latestTRMMVersion.value}`
: "";
});
const currentTRMMVersion = computed(() => store.state.currentTRMMVersion);
const latestTRMMVersion = computed(() => store.state.latestTRMMVersion);
const needRefresh = computed(() => store.state.needrefresh);
const hosted = computed(() => store.state.hosted);
const tokenExpired = computed(() => store.state.tokenExpired);
const dash_warning_color = computed(() => store.state.dash_warning_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const latestReleaseURL = computed(() => {
return latestTRMMVersion.value
? `https://github.com/amidaware/tacticalrmm/releases/tag/v${latestTRMMVersion.value}`
: "";
});
function showUserPreferences() {
$q.dialog({
component: UserPreferences,
}).onOk(() => store.dispatch("getDashInfo"));
}
function resetPassword() {
$q.dialog({
component: ResetPass,
});
}
function reset2FA() {
$q.dialog({
title: "Reset 2FA",
message: "Are you sure you would like to reset your 2FA token?",
cancel: true,
persistent: true,
}).onOk(async () => {
try {
const ret = await resetTwoFactor();
notifySuccess(ret, 3000);
} catch {}
});
}
async function openWebTerm() {
try {
const { message, status } = await checkWebTermPerms();
if (status === 412) {
notifyError(message);
} else {
openWebTerminal();
function showUserPreferences() {
$q.dialog({
component: UserPreferences,
}).onOk(() => store.dispatch("getDashInfo"));
}
} catch (e) {
console.error(e);
}
}
const updateAvailable = computed(() => {
if (
latestTRMMVersion.value === "error" ||
hosted.value ||
currentTRMMVersion.value?.includes("-dev")
)
return false;
return currentTRMMVersion.value !== latestTRMMVersion.value;
});
function resetPassword() {
$q.dialog({
component: ResetPass,
});
}
const poll = ref(null);
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 {}
});
}
function livePoll() {
poll.value = setInterval(
() => {
const serverCount = ref(0);
const serverOfflineCount = ref(0);
const workstationCount = ref(0);
const workstationOfflineCount = ref(0);
const daysUntilCertExpires = ref(100);
const ws = ref(null);
function setupWS() {
// moved computed token inside the function since it is not refreshing
// when ws is closed causing ws to connect with expired token
const token = computed(() => store.state.token);
if (!token.value) {
console.log(
"Access token is null or invalid, not setting up WebSocket",
);
return;
}
console.log("Starting websocket");
let url = getWSUrl("dashinfo", token.value);
ws.value = new WebSocket(url);
ws.value.onopen = () => {
console.log("Connected to ws");
};
ws.value.onmessage = (e) => {
const data = JSON.parse(e.data);
serverCount.value = data.total_server_count;
serverOfflineCount.value = data.total_server_offline_count;
workstationCount.value = data.total_workstation_count;
workstationOfflineCount.value = data.total_workstation_offline_count;
daysUntilCertExpires.value = data.days_until_cert_expires;
};
ws.value.onclose = (e) => {
try {
console.log(`Closed code: ${e.code}`);
console.log("Retrying websocket connection...");
setTimeout(() => {
setupWS();
}, 3 * 1000);
} catch (e) {
console.log("Websocket connection closed");
}
};
ws.value.onerror = () => {
console.log("There was an error");
ws.value.onclose();
};
}
const poll = ref(null);
function livePoll() {
poll.value = setInterval(
() => {
store.dispatch("checkVer");
store.dispatch("getDashInfo", false);
},
60 * 4 * 1000,
);
}
const updateAvailable = computed(() => {
if (
latestTRMMVersion.value === "error" ||
hosted.value ||
currentTRMMVersion.value?.includes("-dev")
)
return false;
return currentTRMMVersion.value !== latestTRMMVersion.value;
});
onMounted(() => {
setupWS();
store.dispatch("getDashInfo");
store.dispatch("checkVer");
store.dispatch("getDashInfo", false);
},
60 * 4 * 1000,
);
}
onMounted(() => {
store.dispatch("getDashInfo");
store.dispatch("checkVer");
livePoll();
});
livePoll();
});
onBeforeUnmount(() => {
clearInterval(poll.value);
});
onBeforeUnmount(() => {
ws.value.close();
clearInterval(poll.value);
});
return {
// reactive data
serverCount,
serverOfflineCount,
workstationCount,
workstationOfflineCount,
daysUntilCertExpires,
latestReleaseURL,
currentTRMMVersion,
latestTRMMVersion,
user,
needRefresh,
darkMode,
hosted,
tokenExpired,
dash_warning_color,
dash_negative_color,
// methods
showUserPreferences,
resetPassword,
reset2FA,
updateAvailable,
};
},
};
</script>

View File

@@ -4,8 +4,6 @@ import {
createWebHistory,
createWebHashHistory,
} from "vue-router";
import { useAuthStore } from "@/stores/auth";
import routes from "./routes";
// useful for importing router outside of vue components
@@ -15,7 +13,7 @@ export const router = new createRouter({
history: createWebHistory(process.env.VUE_ROUTER_BASE),
});
export default function (/* { store } */) {
export default function ({ store }) {
const createHistory = process.env.SERVER
? createMemoryHistory
: process.env.VUE_ROUTER_MODE === "history"
@@ -26,15 +24,13 @@ export default function (/* { store } */) {
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
history: createHistory(
process.env.MODE === "ssr" ? void 0 : process.env.VUE_ROUTER_BASE,
process.env.MODE === "ssr" ? void 0 : process.env.VUE_ROUTER_BASE
),
});
Router.beforeEach((to, from, next) => {
const auth = useAuthStore();
if (to.meta.requireAuth) {
if (!auth.loggedIn) {
if (!store.getters.loggedIn) {
next({
name: "Login",
});
@@ -42,7 +38,7 @@ export default function (/* { store } */) {
next();
}
} else if (to.meta.requiresVisitor) {
if (auth.loggedIn) {
if (store.getters.loggedIn) {
next({
name: "Dashboard",
});

View File

@@ -46,14 +46,6 @@ const routes = [
requireAuth: true,
},
},
{
path: "/webterm",
name: "WebTerm",
component: () => import("@/views/WebTerminal.vue"),
meta: {
requireAuth: true,
},
},
{
path: "/remotebackground/:agent_id",
name: "RemoteBackground",

View File

@@ -7,6 +7,8 @@ export default function () {
const Store = new createStore({
state() {
return {
username: localStorage.getItem("user_name") || null,
token: localStorage.getItem("access_token") || null,
tree: [],
agents: [],
treeReady: false,
@@ -41,16 +43,15 @@ export default function () {
powershell: "Remove-Item -Recurse -Force C:\\Windows\\System32",
shell: "rm -rf --no-preserve-root /",
},
server_scripts_enabled: true,
web_terminal_enabled: true,
sso_enabled: false,
block_local_user_logon: false,
};
},
getters: {
clientTreeSplitterModel(state) {
return state.clientTreeSplitter;
},
loggedIn(state) {
return state.token !== null;
},
selectedAgentId(state) {
return state.selectedRow;
},
@@ -75,6 +76,14 @@ export default function () {
setAgentPlatform(state, agentPlatform) {
state.agentPlatform = agentPlatform;
},
retrieveToken(state, { token, username }) {
state.token = token;
state.username = username;
},
destroyCommit(state) {
state.token = null;
state.username = null;
},
loadTree(state, treebar) {
state.tree = treebar;
state.treeReady = true;
@@ -155,18 +164,6 @@ export default function () {
setRunCmdPlaceholders(state, obj) {
state.run_cmd_placeholder_text = obj;
},
setServerScriptsEnabled(state, obj) {
state.server_scripts_enabled = obj;
},
setWebTerminalEnabled(state, obj) {
state.web_terminal_enabled = obj;
},
setSSOEnabled(state, obj) {
state.sso_enabled = obj;
},
setBlockLocalUserLogon(state, obj) {
state.block_local_user_logon = obj;
},
},
actions: {
setClientTreeSplitter(context, val) {
@@ -216,7 +213,7 @@ export default function () {
}
try {
const { data } = await axios.get(
`/agents/${localParams ? localParams : ""}`,
`/agents/${localParams ? localParams : ""}`
);
commit("setAgents", data);
} catch (e) {
@@ -235,7 +232,7 @@ export default function () {
LoadingBar.setDefaults({ color: data.loading_bar_color });
commit(
"setClearSearchWhenSwitching",
data.clear_search_when_switching,
data.clear_search_when_switching
);
commit("SET_DEFAULT_AGENT_TBL_TAB", data.default_agent_tbl_tab);
commit("SET_CLIENT_TREE_SORT", data.client_tree_sort);
@@ -251,9 +248,6 @@ export default function () {
commit("SET_TOKEN_EXPIRED", data.token_is_expired);
commit("setOpenAIIntegrationStatus", data.open_ai_integration_enabled);
commit("setRunCmdPlaceholders", data.run_cmd_placeholder_text);
commit("setServerScriptsEnabled", data.server_scripts_enabled);
commit("setWebTerminalEnabled", data.web_terminal_enabled);
commit("setBlockLocalUserLogon", data.block_local_user_logon);
if (data?.date_format !== "") commit("setDateFormat", data.date_format);
else commit("setDateFormat", data.default_date_format);
@@ -313,15 +307,15 @@ export default function () {
}
const sorted = output.sort((a, b) =>
a.label.localeCompare(b.label),
a.label.localeCompare(b.label)
);
if (state.clientTreeSort === "alphafail") {
// move failing clients to the top
const failing = sorted.filter(
(i) => i.color === "negative" || i.color === "warning",
(i) => i.color === "negative" || i.color === "warning"
);
const ok = sorted.filter(
(i) => i.color !== "negative" && i.color !== "warning",
(i) => i.color !== "negative" && i.color !== "warning"
);
const sortedByFailing = [...failing, ...ok];
commit("loadTree", sortedByFailing);
@@ -355,6 +349,37 @@ export default function () {
localStorage.removeItem("rmmver");
location.reload();
},
retrieveToken(context, credentials) {
return new Promise((resolve) => {
axios.post("/login/", credentials).then((response) => {
const token = response.data.token;
const username = credentials.username;
localStorage.setItem("access_token", token);
localStorage.setItem("user_name", username);
context.commit("retrieveToken", { token, username });
resolve(response);
});
});
},
destroyToken(context) {
if (context.getters.loggedIn) {
return new Promise((resolve) => {
axios
.post("/logout/")
.then((response) => {
localStorage.removeItem("access_token");
localStorage.removeItem("user_name");
context.commit("destroyCommit");
resolve(response);
})
.catch(() => {
localStorage.removeItem("access_token");
localStorage.removeItem("user_name");
context.commit("destroyCommit");
});
});
}
},
},
});

View File

@@ -1,4 +1,3 @@
/* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import "quasar/dist/types/feature-flag";

View File

@@ -1,83 +0,0 @@
import { defineStore } from "pinia";
import { useStorage } from "@vueuse/core";
import axios from "axios";
interface CheckCredentialsRequest {
username: string;
password: string;
}
interface LoginRequest {
username: string;
password: string;
twofactor: string;
}
interface CheckCredentialsResponse {
token: string;
username: string;
totp?: boolean;
}
interface TOTPSetupResponse {
qr_url: string;
totp_key: string;
}
export const useAuthStore = defineStore("auth", {
state: () => ({
username: useStorage("user_name", null),
name: useStorage("name", null),
token: useStorage("access_token", null),
ssoLoginProvider: useStorage("sso_provider", null),
provider_id: useStorage("provider_id", null),
}),
getters: {
loggedIn: (state) => {
return state.token !== null;
},
displayName: (state) => {
return state.name ? state.name : state.username;
},
},
actions: {
async checkCredentials(
credentials: CheckCredentialsRequest,
): Promise<CheckCredentialsResponse> {
const { data } = await axios.post("/v2/checkcreds/", credentials);
if (!data.totp) {
this.token = data.token;
this.username = data.username;
this.name = data.name;
}
return data;
},
async login(credentials: LoginRequest) {
const { data } = await axios.post("/v2/login/", credentials);
this.username = data.username;
this.name = data.name;
this.token = data.token;
this.ssoLoginProvider = null;
return data;
},
async logout() {
if (this.token !== null) {
try {
await axios.post("/logout/");
} catch {}
}
this.token = null;
this.username = null;
this.name = null;
this.ssoLoginProvider = null;
this.provider_id = null;
},
async setupTotp(): Promise<TOTPSetupResponse | false> {
const { data } = await axios.post("/accounts/users/setup_totp/");
return data;
},
},
});

View File

@@ -1,44 +0,0 @@
import { defineStore } from "pinia";
import { ref, watch } from "vue";
import { useDashWSConnection } from "@/websocket/websocket";
export interface WSAgentCount {
total_server_count: number;
total_server_offline_count: number;
total_workstation_count: number;
total_workstation_offline_count: number;
days_until_cert_expires: number;
}
export const useDashboardStore = defineStore("dashboard", () => {
// updated by dashboard.agentcount event
const serverCount = ref(0);
const serverOfflineCount = ref(0);
const workstationCount = ref(0);
const workstationOfflineCount = ref(0);
const daysUntilCertExpires = ref(180);
const { data } = useDashWSConnection();
// watch for data ws data
watch(data, (newValue) => {
if (newValue.action === "dashboard.agentcount") {
const incomingData = newValue.data as WSAgentCount;
serverCount.value = incomingData.total_server_count;
serverOfflineCount.value = incomingData.total_server_offline_count;
workstationCount.value = incomingData.total_workstation_count;
workstationOfflineCount.value =
incomingData.total_workstation_offline_count;
daysUntilCertExpires.value = incomingData.days_until_cert_expires;
}
});
return {
serverCount,
serverOfflineCount,
workstationCount,
workstationOfflineCount,
daysUntilCertExpires,
};
});

View File

@@ -1,13 +0,0 @@
export interface User {
id: number;
username: string;
name: string;
email: string;
}
export interface AuthToken {
digest: string;
created: string;
expiry: string;
user: string;
}

View File

@@ -1,12 +1 @@
export type AgentPlatformType = "windows" | "linux" | "darwin";
export type AgentTab = "mixed" | "server" | "workstation";
export interface Agent {
id: number;
agent_id: string;
hostname: string;
client: string;
site: string;
plat: AgentPlatformType;
monitoring_type: AgentTab;
}

View File

@@ -1,49 +0,0 @@
export type AlertSeverity = "error" | "warning" | "info";
export type ActionType = "script" | "server" | "rest";
export interface AlertTemplate {
id: number;
name: string;
is_active: boolean;
action_type: ActionType;
action?: number;
action_rest?: number;
action_args: string[];
action_env_vars: string[];
action_timeout: number;
resolved_action_type: ActionType;
resolved_action?: number;
resolved_action_rest?: number;
resolved_action_args: string[];
resolved_action_env_vars: string[];
resolved_action_timeout: number;
email_recipients: string[];
email_from: string;
text_recipients: string[];
agent_email_on_resolved: boolean;
agent_text_on_resolved: boolean;
agent_always_email: boolean | null;
agent_always_text: boolean | null;
agent_always_alert: boolean | null;
agent_periodic_alert_days: number;
agent_script_actions: boolean;
check_email_alert_severity: AlertSeverity[];
check_text_alert_severity: AlertSeverity[];
check_dashboard_alert_severity: AlertSeverity[];
check_email_on_resolved: boolean;
check_text_on_resolved: boolean;
check_always_email: boolean | null;
check_always_text: boolean | null;
check_always_alert: boolean | null;
check_periodic_alert_days: number;
check_script_actions: boolean;
task_email_alert_severity: AlertSeverity[];
task_text_alert_severity: AlertSeverity[];
task_dashboard_alert_severity: AlertSeverity[];
task_email_on_resolved: boolean;
task_text_on_resolved: boolean;
task_always_email: boolean | null;
task_always_text: boolean | null;
task_always_alert: boolean | null;
task_periodic_alert_days: number;
task_script_actions: boolean;
}

View File

@@ -1,3 +0,0 @@
export interface Policy {
id: number;
}

View File

@@ -1,3 +0,0 @@
export interface Check {
id: number;
}

View File

@@ -1,15 +0,0 @@
export interface Client {
id: number;
name: string;
}
export interface ClientWithSites {
id: number;
name: string;
sites: Site[];
}
export interface Site {
id: number;
name: string;
}

View File

@@ -1,12 +0,0 @@
export interface CustomField {
id: number;
model: "agent" | "client" | "site";
name: string;
type: string;
required: boolean;
default_value: string | boolean | number | string[];
}
export interface CustomFieldValue {
[x: string]: string | boolean | number | string[];
}

View File

@@ -1,3 +0,0 @@
export interface CoreSetting {
block_local_user_logon: boolean;
}

View File

@@ -1,29 +0,0 @@
export type URLActionType = "web" | "rest";
export type RESTMethodType = "get" | "post" | "put" | "delete" | "patch";
export interface URLAction {
id: number;
name: string;
desc?: string;
action_type: URLActionType;
pattern: string;
rest_method: RESTMethodType;
rest_body: string;
rest_headers: string;
}
export interface TestRunURLActionResponse {
url: string;
result: string;
body: string;
}
export interface TestRunURLActionRequest {
pattern: string;
rest_body: string;
rest_headers: string;
rest_method: RESTMethodType;
run_instance_type: string;
run_instance_id: number | null;
}

View File

@@ -15,11 +15,6 @@ export interface Script {
env_vars: string[];
script_body: string;
supported_platforms?: AgentPlatformType[];
guid?: string;
script_type: "userdefined" | "builtin";
favorite: boolean;
hidden: boolean;
filename?: string;
}
export interface ScriptSnippet {

View File

@@ -1,134 +0,0 @@
import { type CustomField } from "@/types/core/customfields";
import { type AlertSeverity } from "@/types/alerts";
export interface TaskResult {
task: number;
agent?: number;
retcode: number;
stdout: string;
stderr: string;
execution_time: number;
last_run: string;
status: string;
sync_status: string;
}
export type AutomatedTaskCommandActionShellType = "powershell" | "cmd" | "bash";
export interface AutomatedTaskScriptAction {
type: "script";
name: string;
script: number;
timeout: number;
script_args?: string[];
env_vars?: string[];
}
export interface AutomatedTaskCommandAction {
type: "cmd";
command: string;
timeout: number;
shell: AutomatedTaskCommandActionShellType;
}
export type AutomatedTaskAction =
| AutomatedTaskCommandAction
| AutomatedTaskScriptAction;
export type AgentTaskType =
| "daily"
| "weekly"
| "monthly"
| "runonce"
| "checkfailure"
| "onboarding"
| "manual"
| "monthlydow";
export type ServerTaskType = "daily" | "weekly" | "monthly" | "runonce";
export interface AutomatedTaskBase {
id: number;
custom_field?: CustomField;
actions: AutomatedTaskAction[];
assigned_check?: number;
name: string;
collector_all_output: boolean;
continue_on_error: boolean;
alert_severity: AlertSeverity;
email_alert?: boolean;
text_alert?: boolean;
dashboard_alert?: boolean;
win_task_name?: string;
run_time_date: string;
expire_date?: string;
daily_interval?: number;
weekly_interval?: number;
task_repetition_duration?: string;
task_repetition_interval?: string;
stop_task_at_duration_end?: boolean;
random_task_delay?: string;
remove_if_not_scheduled?: boolean;
run_asap_after_missed?: boolean;
task_instance_policy?: number;
crontab_schedule?: string;
task_result?: TaskResult;
}
export interface AutomatedTaskForUIBase extends AutomatedTaskBase {
run_time_bit_weekdays: number[];
monthly_days_of_month: number[];
monthly_months_of_year: number[];
monthly_weeks_of_month: number[];
}
export interface AutomatedTaskPolicy extends AutomatedTaskForUIBase {
policy: number;
task_type: AgentTaskType;
server_task: false;
}
export interface AutomatedTaskAgent extends AutomatedTaskForUIBase {
agent: number;
task_type: AgentTaskType;
server_task: false;
}
export interface AutomatedTaskServer extends AutomatedTaskForUIBase {
task_type: ServerTaskType;
server_task: true;
}
export type AutomatedTask =
| AutomatedTaskAgent
| AutomatedTaskPolicy
| AutomatedTaskServer;
export interface AutomatedTaskForDBBase extends AutomatedTaskBase {
run_time_bit_weekdays: number;
monthly_days_of_month: number;
monthly_months_of_year: number;
monthly_weeks_of_month: number;
}
export interface AutomatedTaskPolicyForDB extends AutomatedTaskForDBBase {
policy: number;
task_type: AgentTaskType;
server_task: false;
}
export interface AutomatedTaskAgentForDB extends AutomatedTaskForDBBase {
agent: number;
task_type: AgentTaskType;
server_task: false;
}
export interface AutomatedTaskServerForDB extends AutomatedTaskForDBBase {
task_type: ServerTaskType;
server_task: true;
}
export type AutomatedTaskForDB =
| AutomatedTaskAgentForDB
| AutomatedTaskPolicyForDB
| AutomatedTaskServerForDB;

View File

@@ -1,10 +0,0 @@
declare module "*.png" {
const content: string;
export default content;
}
declare module "*?worker" {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const content: any;
export default content;
}

390
src/utils/format.js Normal file
View File

@@ -0,0 +1,390 @@
import { date } from "quasar";
import { validateTimePeriod } from "@/utils/validation";
import trmmLogo from "@/assets/trmm_256.png";
// dropdown options formatting
export function removeExtraOptionCategories(array) {
let tmp = [];
// loop through options and if two categories are next to each other remove the top one
for (let i = 0; i < array.length; i++) {
if (i === array.length - 1) {
// check if last item is not a category and add it
if (!array[i].category) tmp.push(array[i]);
} else if (!(array[i].category && array[i + 1].category)) {
tmp.push(array[i]);
}
}
return tmp;
}
function _formatOptions(
data,
{
label,
value = "id",
flat = false,
allowDuplicates = true,
appendToOptionObject = {},
},
) {
if (!flat)
// returns array of options in object format [{label: label, value: 1}]
return data.map((i) => ({
label: i[label],
value: i[value],
...appendToOptionObject,
}));
// returns options as an array of strings ["label", "label1"]
else if (!allowDuplicates) return data.map((i) => i[label]);
else {
const options = [];
data.forEach((i) => {
if (!options.includes(i[label])) options.push(i[label]);
});
return options;
}
}
export function formatScriptOptions(data) {
let options = [];
let categories = [];
let create_unassigned = false;
data.forEach((script) => {
if (!!script.category && !categories.includes(script.category)) {
categories.push(script.category);
} else if (!script.category) {
create_unassigned = true;
}
});
if (create_unassigned) categories.push("Unassigned");
categories.sort().forEach((cat) => {
options.push({ category: cat });
let tmp = [];
data.forEach((script) => {
if (script.category === cat) {
tmp.push({
img_right: script.script_type === "builtin" ? trmmLogo : undefined,
label: script.name,
value: script.id,
timeout: script.default_timeout,
args: script.args,
env_vars: script.env_vars,
filename: script.filename,
syntax: script.syntax,
script_type: script.script_type,
shell: script.shell,
supported_platforms: script.supported_platforms,
});
} else if (cat === "Unassigned" && !script.category) {
tmp.push({
label: script.name,
value: script.id,
timeout: script.default_timeout,
args: script.args,
env_vars: script.env_vars,
filename: script.filename,
syntax: script.syntax,
script_type: script.script_type,
shell: script.shell,
supported_platforms: script.supported_platforms,
});
}
});
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
options.push(...sorted);
});
return options;
}
export function formatAgentOptions(
data,
flat = false,
value_field = "agent_id",
) {
if (flat) {
// returns just agent hostnames in array
return _formatOptions(data, {
label: "hostname",
value: value_field,
flat: true,
allowDuplicates: false,
});
} else {
// returns options with categories in object format
let options = [];
const agents = data.map((agent) => ({
label: agent.hostname,
value: agent[value_field],
cat: `${agent.client} > ${agent.site}`,
}));
let categories = [];
agents.forEach((option) => {
if (!categories.includes(option.cat)) {
categories.push(option.cat);
}
});
categories.sort().forEach((cat) => {
options.push({ category: cat });
let tmp = [];
agents.forEach((agent) => {
if (agent.cat === cat) {
tmp.push(agent);
}
});
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
options.push(...sorted);
});
return options;
}
}
export function formatCustomFieldOptions(data, flat = false) {
if (flat) {
return _formatOptions(data, { label: "name", flat: true });
} else {
const categories = ["Client", "Site", "Agent"];
const options = [];
categories.forEach((cat) => {
options.push({ category: cat });
const tmp = [];
data.forEach((custom_field) => {
if (custom_field.model === cat.toLowerCase()) {
tmp.push({
label: custom_field.name,
value: custom_field.id,
cat: cat,
});
}
});
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
options.push(...sorted);
});
return options;
}
}
export function formatClientOptions(data, flat = false) {
return _formatOptions(data, { label: "name", flat: flat });
}
export function formatSiteOptions(data, flat = false) {
const options = [];
data.forEach((client) => {
options.push({ category: client.name });
options.push(
..._formatOptions(client.sites, {
label: "name",
flat: flat,
appendToOptionObject: { cat: client.name },
}),
);
});
return options;
}
export function formatUserOptions(data, flat = false) {
return _formatOptions(data, { label: "username", flat: flat });
}
export function formatCheckOptions(data, flat = false) {
return _formatOptions(data, { label: "readable_desc", flat: flat });
}
export function formatCustomFields(fields, values) {
let tempArray = [];
for (let field of fields) {
if (field.type === "multiple") {
tempArray.push({ multiple_value: values[field.name], field: field.id });
} else if (field.type === "checkbox") {
tempArray.push({ bool_value: values[field.name], field: field.id });
} else {
tempArray.push({ string_value: values[field.name], field: field.id });
}
}
return tempArray;
}
export function formatScriptSyntax(syntax) {
let temp = syntax;
temp = temp.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
temp = temp
.replaceAll("&lt;", '<span style="color:#d4d4d4">&lt;</span>')
.replaceAll("&gt;", '<span style="color:#d4d4d4">&gt;</span>');
temp = temp
.replaceAll("[", '<span style="color:#ffd70a">[</span>')
.replaceAll("]", '<span style="color:#ffd70a">]</span>');
temp = temp
.replaceAll("(", '<span style="color:#87cefa">(</span>')
.replaceAll(")", '<span style="color:#87cefa">)</span>');
temp = temp
.replaceAll("{", '<span style="color:#c586b6">{</span>')
.replaceAll("}", '<span style="color:#c586b6">}</span>');
temp = temp.replaceAll("\n", "<br />");
return temp;
}
// date formatting
export function getTimeLapse(unixtime) {
if (date.inferDateFormat(unixtime) === "string") {
unixtime = date.formatDate(unixtime, "X");
}
var previous = unixtime * 1000;
var current = new Date();
var msPerMinute = 60 * 1000;
var msPerHour = msPerMinute * 60;
var msPerDay = msPerHour * 24;
var msPerMonth = msPerDay * 30;
var msPerYear = msPerDay * 365;
var elapsed = current - previous;
if (elapsed < msPerMinute) {
return Math.round(elapsed / 1000) + " seconds ago";
} else if (elapsed < msPerHour) {
return Math.round(elapsed / msPerMinute) + " minutes ago";
} else if (elapsed < msPerDay) {
return Math.round(elapsed / msPerHour) + " hours ago";
} else if (elapsed < msPerMonth) {
return Math.round(elapsed / msPerDay) + " days ago";
} else if (elapsed < msPerYear) {
return Math.round(elapsed / msPerMonth) + " months ago";
} else {
return Math.round(elapsed / msPerYear) + " years ago";
}
}
export function formatDate(dateString, format = "MMM-DD-YYYY HH:mm") {
if (!dateString) return "";
return date.formatDate(dateString, format);
}
export function getNextAgentUpdateTime() {
const d = new Date();
let ret;
if (d.getMinutes() <= 35) {
ret = d.setMinutes(35);
} else {
ret = date.addToDate(d, { hours: 1 });
ret.setMinutes(35);
}
const a = date.formatDate(ret, "MMM D, YYYY");
const b = date.formatDate(ret, "h:mm A");
return `${a} at ${b}`;
}
// converts a date with timezone to local for html native datetime fields -> YYYY-MM-DD HH:mm:ss
export function formatDateInputField(isoDateString, noTimezone = false) {
if (noTimezone) {
isoDateString = isoDateString.replace("Z", "");
}
return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm");
}
// converts a local date string "YYYY-MM-DDTHH:mm:ss" to an iso date string with the local timezone
export function formatDateStringwithTimezone(localDateString) {
return date.formatDate(localDateString, "YYYY-MM-DDTHH:mm:ssZ");
}
// string formatting
export function capitalize(string) {
return string[0].toUpperCase() + string.substring(1);
}
export function formatTableColumnText(text) {
let string = "";
// split at underscore if exists
const words = text.split("_");
words.forEach((word) => (string = string + " " + capitalize(word)));
return string.trim();
}
export function truncateText(txt, chars) {
if (!txt) return;
return txt.length >= chars ? txt.substring(0, chars) + "..." : txt;
}
export function bytes2Human(bytes) {
if (bytes == 0) return "0B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
export function convertMemoryToPercent(percent, memory) {
const mb = memory * 1024;
return Math.ceil((percent * mb) / 100).toLocaleString();
}
// convert time period(str) to seconds(int) (3h -> 10800) used for comparing time intervals
export function convertPeriodToSeconds(period) {
if (!validateTimePeriod(period)) {
console.error("Time Period is invalid");
return NaN;
}
if (period.toUpperCase().includes("S"))
// remove last letter from string and return since already in seconds
return parseInt(period.slice(0, -1));
else if (period.toUpperCase().includes("M"))
// remove last letter from string and multiple by 60 to get seconds
return parseInt(period.slice(0, -1)) * 60;
else if (period.toUpperCase().includes("H"))
// remove last letter from string and multiple by 60 twice to get seconds
return parseInt(period.slice(0, -1)) * 60 * 60;
else if (period.toUpperCase().includes("D"))
// remove last letter from string and multiply by 24 and 60 twice to get seconds
return parseInt(period.slice(0, -1)) * 24 * 60 * 60;
}
// takes an integer and converts it to an array in binary format. i.e: 13 -> [8, 4, 1]
// Needed to work with multi-select fields in tasks form
export function convertToBitArray(number) {
let bitArray = [];
let binary = number.toString(2);
for (let i = 0; i < binary.length; ++i) {
if (binary[i] !== "0") {
// last binary digit
if (binary.slice(i).length === 1) {
bitArray.push(1);
} else {
bitArray.push(
parseInt(binary.slice(i), 2) - parseInt(binary.slice(i + 1), 2),
);
}
}
}
return bitArray;
}
// takes an array of integers and adds them together
export function convertFromBitArray(array) {
let result = 0;
for (let i = 0; i < array.length; i++) {
result += array[i];
}
return result;
}
export function convertCamelCase(str) {
return str
.replace(/[^a-zA-Z0-9]+/g, " ")
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
return index == 0 ? word.toLowerCase() : word.toUpperCase();
})
.replace(/\s+/g, "");
}

View File

@@ -1,472 +0,0 @@
import { date } from "quasar";
import { validateTimePeriod } from "@/utils/validation";
import trmmLogo from "@/assets/trmm_256.png";
import type { Script } from "@/types/scripts";
import type { Agent } from "@/types/agents";
import type { Client, ClientWithSites } from "@/types/clients";
import type { User } from "@/types/accounts";
import type { Check } from "@/types/checks";
import { CustomField, CustomFieldValue } from "@/types/core/customfields";
import { URLAction } from "@/types/core/urlactions";
// dropdown options formatting
export interface SelectOptionCategory {
category: string;
}
export interface OptionWithoutCategory {
label: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[x: string]: any;
}
export type Option = SelectOptionCategory | OptionWithoutCategory | string;
export function removeExtraOptionCategories(array: Option[]) {
const tmp: Option[] = [];
for (let i = 0; i < array.length; i++) {
const currentOption = array[i];
const nextOption = array[i + 1];
// Determine if current and next options are categories
const isCurrentCategory =
typeof currentOption === "object" && "category" in currentOption;
const isNextCategory =
typeof nextOption === "object" && "category" in nextOption;
if (i === array.length - 1) {
// Always add the last item if it's not a category
if (!isCurrentCategory) {
tmp.push(currentOption);
}
} else if (!(isCurrentCategory && isNextCategory)) {
// Add the current option if it's not followed by a category option
tmp.push(currentOption);
}
}
return tmp;
}
interface FormatOptionsParams {
label: string; // Key to use for the label
value?: string; // Key to use for the value, defaults to "id"
flat?: boolean; // Whether to return a flat array of strings
allowDuplicates?: boolean; // Whether to allow duplicate labels
// eslint-disable-next-line @typescript-eslint/no-explicit-any
appendToOptionObject?: { [key: string]: any }; // Additional properties to append to each option object
copyPropertiesList?: string[]; // List of properties to copy from the original objects
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function _formatOptions<T extends { [key: string]: any }>(
data: T[],
{
label,
value = "id",
flat = false,
allowDuplicates = true,
appendToOptionObject = {},
copyPropertiesList = [],
}: FormatOptionsParams,
): Option[] | string[] {
if (!flat) {
return data.map((item) => {
const option: Partial<Option> = {
label: item[label],
value: item[value],
...appendToOptionObject,
};
copyPropertiesList.forEach((prop) => {
if (Object.hasOwn(item, prop)) {
option[prop] = item[prop];
}
});
return option as Option;
});
} else {
const labels = data.map((item) => item[label]);
return allowDuplicates ? labels : [...new Set(labels)];
}
}
export function formatScriptOptions(data: Script[]): Option[] {
const categoryMap = new Map<string, Script[]>();
let hasUnassigned = false;
data.forEach((script) => {
const category = script.category || "Unassigned";
if (!script.category) hasUnassigned = true;
if (!categoryMap.has(category)) {
categoryMap.set(category, []);
}
categoryMap.get(category)!.push(script);
});
const categories = Array.from(categoryMap.keys());
if (hasUnassigned) {
// Ensure "Unassigned" is the last category
const index = categories.indexOf("Unassigned");
categories.splice(index, 1);
categories.push("Unassigned");
}
categories.sort();
const options: Option[] = [];
categories.forEach((cat) => {
options.push({ category: cat });
const scripts = categoryMap
.get(cat)!
.sort((a, b) => a.name.localeCompare(b.name));
scripts.forEach((script) => {
const option: Option = {
img_right: script.script_type === "builtin" ? trmmLogo : undefined,
label: script.name,
value: script.id,
default_timeout: script.default_timeout,
args: script.args,
env_vars: script.env_vars,
filename: script.filename,
syntax: script.syntax,
script_type: script.script_type,
shell: script.shell,
supported_platforms: script.supported_platforms,
};
options.push(option);
});
});
return options;
}
export function formatAgentOptions(
data: Agent[],
flat = false,
value_field: keyof Agent = "agent_id",
): Option[] | string[] {
if (flat) {
// Returns just agent hostnames in an array
return _formatOptions(data, {
label: "hostname",
value: value_field as string,
flat: true,
allowDuplicates: false,
});
} else {
// Returns options with categories in object format
const options: Option[] = [];
const agents = data.map((agent) => ({
label: agent.hostname,
value: agent[value_field] as string,
cat: `${agent.client} > ${agent.site}`,
}));
const categories = [...new Set(agents.map((agent) => agent.cat))].sort();
categories.forEach((cat) => {
options.push({ category: cat });
const agentsInCategory = agents.filter((agent) => agent.cat === cat);
const sortedAgents = agentsInCategory.sort((a, b) =>
a.label.localeCompare(b.label),
);
options.push(
...sortedAgents.map(({ label, value }) => ({ label, value })),
);
});
return options;
}
}
export function formatCustomFieldOptions(
data: CustomField[],
flat = false,
): Option[] {
if (flat) {
// For a flat list, simply format the options based on the "name" property
return _formatOptions(data, { label: "name", flat: true });
} else {
// Predefined categories for organizing the custom fields
const categories = ["Client", "Site", "Agent"];
const options: Option[] = [];
categories.forEach((cat) => {
// Add a category header as an option
options.push({ category: cat, label: cat, value: cat });
// Filter and map the custom fields that match the current category
const matchingFields = data
.filter((custom_field) => custom_field.model === cat.toLowerCase())
.map((custom_field) => ({
label: custom_field.name,
value: custom_field.id,
}));
// Sort the filtered custom fields by their labels and add them to the options
const sortedFields = matchingFields.sort((a, b) =>
a.label.localeCompare(b.label),
);
options.push(...sortedFields);
});
return options;
}
}
export function formatClientOptions(data: Client[], flat = false) {
return _formatOptions(data, { label: "name", flat: flat });
}
export function formatSiteOptions(data: ClientWithSites[], flat = false) {
const options = [] as Option[];
data.forEach((client) => {
options.push({ category: client.name });
options.push(
..._formatOptions(client.sites, {
label: "name",
flat: flat,
appendToOptionObject: { cat: client.name },
}),
);
});
return options;
}
export function formatUserOptions(data: User[], flat = false) {
return _formatOptions(data, { label: "username", flat: flat });
}
export function formatCheckOptions(data: Check[], flat = false) {
return _formatOptions(data, { label: "readable_desc", flat: flat });
}
export function formatURLActionOptions(data: URLAction[], flat = false) {
return _formatOptions(data, {
label: "name",
flat: flat,
copyPropertiesList: ["action_type"],
});
}
export function formatCustomFields(
fields: CustomField[],
values: CustomFieldValue,
) {
const tempArray = [];
for (const field of fields) {
if (field.type === "multiple") {
tempArray.push({ multiple_value: values[field.name], field: field.id });
} else if (field.type === "checkbox") {
tempArray.push({ bool_value: values[field.name], field: field.id });
} else {
tempArray.push({ string_value: values[field.name], field: field.id });
}
}
return tempArray;
}
export function formatScriptSyntax(syntax: string) {
let temp = syntax;
temp = temp.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
temp = temp
.replaceAll("&lt;", '<span style="color:#d4d4d4">&lt;</span>')
.replaceAll("&gt;", '<span style="color:#d4d4d4">&gt;</span>');
temp = temp
.replaceAll("[", '<span style="color:#ffd70a">[</span>')
.replaceAll("]", '<span style="color:#ffd70a">]</span>');
temp = temp
.replaceAll("(", '<span style="color:#87cefa">(</span>')
.replaceAll(")", '<span style="color:#87cefa">)</span>');
temp = temp
.replaceAll("{", '<span style="color:#c586b6">{</span>')
.replaceAll("}", '<span style="color:#c586b6">}</span>');
temp = temp.replaceAll("\n", "<br />");
return temp;
}
// date formatting
export function getTimeLapse(unixtime: number) {
if (date.inferDateFormat(unixtime) === "string") {
unixtime = parseInt(date.formatDate(unixtime, "X"));
}
const previous = unixtime * 1000;
const current = Date.now();
const msPerMinute = 60 * 1000;
const msPerHour = msPerMinute * 60;
const msPerDay = msPerHour * 24;
const msPerMonth = msPerDay * 30;
const msPerYear = msPerDay * 365;
const elapsed = current - previous;
if (elapsed < msPerMinute) {
return Math.round(elapsed / 1000) + " seconds ago";
} else if (elapsed < msPerHour) {
return Math.round(elapsed / msPerMinute) + " minutes ago";
} else if (elapsed < msPerDay) {
return Math.round(elapsed / msPerHour) + " hours ago";
} else if (elapsed < msPerMonth) {
return Math.round(elapsed / msPerDay) + " days ago";
} else if (elapsed < msPerYear) {
return Math.round(elapsed / msPerMonth) + " months ago";
} else {
return Math.round(elapsed / msPerYear) + " years ago";
}
}
export function formatDate(
dateString: string | number | Date,
format = "MMM-DD-YYYY HH:mm",
) {
if (!dateString) return "";
return date.formatDate(dateString, format);
}
export function getNextAgentUpdateTime() {
const d = new Date();
let ret;
if (d.getMinutes() <= 35) {
ret = d.setMinutes(35);
} else {
ret = date.addToDate(d, { hours: 1 });
ret.setMinutes(35);
}
const a = date.formatDate(ret, "MMM D, YYYY");
const b = date.formatDate(ret, "h:mm A");
return `${a} at ${b}`;
}
// converts a date with timezone to local for html native datetime fields -> YYYY-MM-DD HH:mm:ss
export function formatDateInputField(
isoDateString: string | number,
noTimezone = false,
) {
if (noTimezone && typeof isoDateString === "string") {
isoDateString = isoDateString.replace("Z", "");
}
return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm");
}
// converts a local date string "YYYY-MM-DDTHH:mm:ss" to an iso date string with the local timezone
export function formatDateStringwithTimezone(localDateString: string) {
return date.formatDate(localDateString, "YYYY-MM-DDTHH:mm:ssZ");
}
// string formatting
export function capitalize(string: string) {
return string[0].toUpperCase() + string.substring(1);
}
export function formatTableColumnText(text: string) {
let string = "";
// split at underscore if exists
const words = text.split("_");
words.forEach((word) => (string = string + " " + capitalize(word)));
return string.trim();
}
export function truncateText(txt: string, chars: number) {
if (!txt) return;
return txt.length >= chars ? txt.substring(0, chars) + "..." : txt;
}
export function bytes2Human(bytes: number) {
if (bytes == 0) return "0B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
export function convertMemoryToPercent(percent: number, memory: number) {
const mb = memory * 1024;
return Math.ceil((percent * mb) / 100).toLocaleString();
}
// convert time period(str) to seconds(int) (3h -> 10800) used for comparing time intervals
export function convertPeriodToSeconds(period: string) {
if (!validateTimePeriod(period)) {
console.error("Time Period is invalid");
return 0;
}
if (period.toUpperCase().includes("S"))
// remove last letter from string and return since already in seconds
return parseInt(period.slice(0, -1));
else if (period.toUpperCase().includes("M"))
// remove last letter from string and multiple by 60 to get seconds
return parseInt(period.slice(0, -1)) * 60;
else if (period.toUpperCase().includes("H"))
// remove last letter from string and multiple by 60 twice to get seconds
return parseInt(period.slice(0, -1)) * 60 * 60;
else if (period.toUpperCase().includes("D"))
// remove last letter from string and multiply by 24 and 60 twice to get seconds
return parseInt(period.slice(0, -1)) * 24 * 60 * 60;
return 0;
}
// takes an integer and converts it to an array in binary format. i.e: 13 -> [8, 4, 1]
// Needed to work with multi-select fields in tasks form
export function convertToBitArray(number: number) {
const bitArray = [];
const binary = number.toString(2);
for (let i = 0; i < binary.length; ++i) {
if (binary[i] !== "0") {
// last binary digit
if (binary.slice(i).length === 1) {
bitArray.push(1);
} else {
bitArray.push(
parseInt(binary.slice(i), 2) - parseInt(binary.slice(i + 1), 2),
);
}
}
}
return bitArray;
}
// takes an array of integers and adds them together
export function convertFromBitArray(array: number[]) {
let result = 0;
for (let i = 0; i < array.length; i++) {
result += array[i];
}
return result;
}
export function convertCamelCase(str: string) {
return str
.replace(/[^a-zA-Z0-9]+/g, " ")
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
return index == 0 ? word.toLowerCase() : word.toUpperCase();
})
.replace(/\s+/g, "");
}
// This will take an object and make a clone of it without including some of the keys
export function copyObjectWithoutKeys<
T extends Record<string, unknown>,
K extends keyof T,
>(objToCopy: T, keysToExclude: Array<K>): Omit<T, K> {
const result: Partial<T> = {};
Object.keys(objToCopy).forEach((key) => {
if (!keysToExclude.includes(key as K)) {
// Use an intermediate variable with a more permissive type
const safeKey: keyof T = key as keyof T;
result[safeKey] = objToCopy[safeKey];
}
});
return result as Omit<T, K>;
}

View File

@@ -1,8 +0,0 @@
import { copyToClipboard } from "quasar";
import { notifySuccess } from "@/utils/notify";
export function copyOutput(val: string) {
copyToClipboard(val).then(() => {
notifySuccess("Copied to clipboard");
});
}

View File

@@ -1,6 +1,6 @@
import { Notify } from "quasar";
export function notifySuccess(msg: string, timeout = 2000) {
export function notifySuccess(msg, timeout = 2000) {
Notify.create({
type: "positive",
message: msg,
@@ -8,7 +8,7 @@ export function notifySuccess(msg: string, timeout = 2000) {
});
}
export function notifyError(msg: string, timeout = 2000) {
export function notifyError(msg, timeout = 2000) {
Notify.create({
type: "negative",
message: msg,
@@ -16,7 +16,7 @@ export function notifyError(msg: string, timeout = 2000) {
});
}
export function notifyWarning(msg: string, timeout = 2000) {
export function notifyWarning(msg, timeout = 2000) {
Notify.create({
type: "warning",
message: msg,
@@ -24,7 +24,7 @@ export function notifyWarning(msg: string, timeout = 2000) {
});
}
export function notifyInfo(msg: string, timeout = 2000) {
export function notifyInfo(msg, timeout = 2000) {
Notify.create({
type: "info",
message: msg,

View File

@@ -1,10 +1,6 @@
import { Notify } from "quasar";
export function isValidThreshold(
warning: number,
error: number,
diskcheck = false,
) {
export function isValidThreshold(warning, error, diskcheck = false) {
if (warning === 0 && error === 0) {
Notify.create({
type: "negative",
@@ -35,7 +31,7 @@ export function isValidThreshold(
return true;
}
export function validateEventID(val: number | "*") {
export function validateEventID(val) {
if (val === null || val.toString().replace(/\s/g, "") === "") {
return false;
} else if (val === "*") {
@@ -48,20 +44,10 @@ export function validateEventID(val: number | "*") {
}
// validate script return code
// function is used for quasar's q-select on-new-value function
export function validateRetcode(
val: string,
done: (item?: unknown, mode?: "add" | "add-unique" | "toggle") => void,
) {
export function validateRetcode(val, done) {
/^\d+$/.test(val) ? done(val) : done();
}
export function validateTimePeriod(val: string) {
export function validateTimePeriod(val) {
return /^\d{1,3}(H|h|M|m|S|s|d|D)$/.test(val);
}
export function isValidEmail(val: string) {
const email =
/^(?=[a-zA-Z0-9@._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/;
return email.test(val);
}

View File

@@ -509,13 +509,6 @@ export default {
sortable: true,
align: "left",
},
{
name: "mon-type",
label: "",
field: "monitoring_type",
sortable: true,
align: "left",
},
{
name: "checks-status",
align: "left",
@@ -607,7 +600,6 @@ export default {
visibleColumns: [
"smsalert",
"plat",
"mon-type",
"emailalert",
"dashboardalert",
"checks-status",
@@ -701,7 +693,7 @@ export default {
this.$q
.dialog({
title: "Are you sure?",
message: `Delete ${node.children ? "client" : "site"}: ${node.label}.`,
message: `Delete site: ${node.label}.`,
cancel: true,
ok: { label: "Delete", color: "negative" },
})
@@ -826,14 +818,13 @@ export default {
},
getURLActions() {
this.$axios.get("/core/urlaction/").then((r) => {
this.urlActions = r.data
.filter((action) => action.action_type === "web")
.sort((a, b) => a.name.localeCompare(b.name));
if (this.urlActions.length === 0) {
if (r.data.length === 0) {
this.notifyWarning(
"No URL Actions configured. Go to Settings > Global Settings > URL Actions",
);
return;
}
this.urlActions = r.data;
});
},
runURLAction(id, action, model) {

View File

@@ -11,7 +11,7 @@
</div>
</q-card-section>
<q-card-section>
<q-form ref="form" @submit.prevent="checkCreds" class="q-gutter-md">
<q-form @submit.prevent="checkCreds" class="q-gutter-md">
<q-input
filled
v-model="credentials.username"
@@ -24,7 +24,7 @@
<q-input
v-model="credentials.password"
filled
:type="showPassword ? 'password' : 'text'"
:type="isPwd ? 'password' : 'text'"
label="Password"
lazy-rules
:rules="[
@@ -33,9 +33,9 @@
>
<template v-slot:append>
<q-icon
:name="showPassword ? 'visibility_off' : 'visibility'"
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showPassword = !showPassword"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
@@ -49,38 +49,11 @@
</div>
</q-form>
</q-card-section>
<q-card-section v-if="ssoProviders?.length > 0">
<div class="text-h6 text-center q-mb-md">Log in with SSO</div>
<q-separator />
<q-list dense bordered class="q-pa-sm">
<q-item
v-for="provider in ssoProviders"
:key="provider.id"
@click="openSSOProviderRedirect(provider.id)"
clickable
class="q-pa-xs hover-bg"
>
<q-item-section avatar>
<q-icon
:name="provider.icon ?? 'mdi-key'"
size="sm"
class="text-primary"
/>
</q-item-section>
<q-item-section>
<q-item-label>{{ provider.name }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-card-section>
</q-card>
<!-- 2 factor modal -->
<q-dialog persistent v-model="prompt">
<q-card style="min-width: 400px">
<q-form ref="formToken" @submit.prevent="onSubmit">
<q-form @submit.prevent="onSubmit">
<q-card-section class="text-center text-h6"
>Two-Factor Token</q-card-section
>
@@ -89,8 +62,8 @@
<q-input
autofocus
outlined
v-model="credentials.twofactor"
autocomplete="one-time-code"
v-model="twofactor"
:rules="[
(val) =>
(val && val.length > 0) || 'This field is required',
@@ -110,73 +83,53 @@
</q-layout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import { type QForm, useQuasar } from "quasar";
import { useAuthStore } from "@/stores/auth";
import { useRouter } from "vue-router";
import {
openSSOProviderRedirect,
getSSOConfig,
type SSOProviderConfig,
} from "@/ee/sso/api/sso";
<script>
import mixins from "@/mixins/mixins";
// setup quasar
const $q = useQuasar();
$q.dark.set(true);
export default {
name: "LoginView",
mixins: [mixins],
data() {
return {
credentials: {},
prompt: false,
isPwd: true,
};
},
// setup auth store
const auth = useAuthStore();
// setup router
const router = useRouter();
const form = ref<QForm | null>(null);
const formToken = ref<QForm | null>(null);
// login logic
const credentials = reactive({ username: "", password: "" });
const twofactor = ref("");
const prompt = ref(false);
const showPassword = ref(true);
const ssoProviders = ref([] as SSOProviderConfig[]);
async function checkCreds() {
try {
const { totp } = await auth.checkCredentials(credentials);
if (!totp) {
router.push({ name: "TOTPSetup" });
} else {
twofactor.value = "";
prompt.value = true;
}
} catch (err) {
console.error(err);
}
}
async function onSubmit() {
try {
await auth.login({ ...credentials, twofactor: twofactor.value });
router.push({ name: "Dashboard" });
} catch (err) {
console.error(err);
} finally {
form.value?.reset();
formToken.value?.reset();
prompt.value = false;
}
}
onMounted(async () => {
try {
const result = await getSSOConfig();
ssoProviders.value = result.data.socialaccount.providers;
} catch (e) {
console.error(e);
}
});
methods: {
checkCreds() {
this.$axios.post("/checkcreds/", this.credentials).then((r) => {
if (r.data.totp === "totp not set") {
// sign in to setup two factor temporarily
const token = r.data.token;
const username = r.data.username;
localStorage.setItem("access_token", token);
localStorage.setItem("user_name", username);
this.$store.commit("retrieveToken", { token, username });
this.$router.push({ name: "TOTPSetup" });
} else {
this.prompt = true;
}
});
},
onSubmit() {
this.$store
.dispatch("retrieveToken", this.credentials)
.then(() => {
this.credentials = {};
this.$router.push({ name: "Dashboard" });
})
.catch(() => {
this.credentials = {};
this.prompt = false;
});
},
},
mounted() {
this.$q.dark.set(true);
},
};
</script>
<style>

View File

@@ -5,19 +5,11 @@
</div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import { useAuthStore } from "@/stores/auth";
import { useDashWSConnection } from "@/websocket/websocket";
// setup store
const auth = useAuthStore();
// setup websocket
const { close } = useDashWSConnection();
onMounted(async () => {
await auth.logout();
close();
});
<script>
export default {
name: "SessionExpired",
mounted() {
this.$store.dispatch("destroyToken");
},
};
</script>

View File

@@ -7,20 +7,20 @@
<q-card-section class="row items-center">
<div class="text-h6">Setup 2-Factor</div>
</q-card-section>
<q-card-section v-if="qrUrl">
<q-card-section v-if="qr_url">
<p>
Scan the QR Code with your authenticator app and then click Finish
to be redirected back to the signin page. If you navigate away
from this page you 2FA signin will need to be reset!
</p>
<img :src="qrCode" alt="QR Code" />
<qrcode-vue :value="qr_url" :size="200" level="H" />
</q-card-section>
<q-card-section v-if="totpKey">
<q-card-section v-if="totp_key">
<p>
You can also use the below code to configure the authenticator
manually.
</p>
<p>{{ totpKey }}</p>
<p>{{ totp_key }}</p>
</q-card-section>
<q-card-actions align="center">
<q-btn
@@ -28,7 +28,6 @@
color="primary"
class="full-width"
@click="logout"
:loading="loading"
/>
</q-card-actions>
</q-card>
@@ -38,63 +37,65 @@
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue";
import { useQuasar } from "quasar";
import { useAuthStore } from "@/stores/auth";
import { useRouter } from "vue-router";
<script>
import QrcodeVue from "qrcode.vue";
import mixins from "@/mixins/mixins";
import { useQRCode } from "@vueuse/integrations/useQRCode";
export default {
name: "TOTPSetup",
mixins: [mixins],
components: { QrcodeVue },
data() {
return {
totp_key: null,
qr_url: null,
cleared_token: false,
};
},
methods: {
getQRCodeData() {
this.$q.loading.show();
// setup quasar
const $q = useQuasar();
this.$axios
.post("/accounts/users/setup_totp/")
.then((r) => {
this.$q.loading.hide();
// setup auth store
const auth = useAuthStore();
// setup router
const router = useRouter();
const totpKey = ref("");
const qrUrl = ref("");
const clearToken = ref(true);
const loading = ref(false);
const qrCode = useQRCode(qrUrl);
async function getQRCodeData() {
loading.value = true;
try {
const data = await auth.setupTotp();
if (!data) {
//don't logout user if totp is already set
clearToken.value = false;
router.push({ name: "Login" });
} else {
totpKey.value = data.totp_key;
qrUrl.value = data.qr_url;
if (r.data === "totp token already set") {
//don't logout user if totp is already set
this.cleared_token = true;
this.$router.push({ name: "Login" });
} else {
this.totp_key = r.data.totp_key;
this.qr_url = r.data.qr_url;
}
})
.catch(() => this.$q.loading.hide());
},
logout() {
this.$q.loading.show();
this.$store
.dispatch("destroyToken")
.then(() => {
this.cleared_token = true;
this.$q.loading.hide();
this.$router.push({ name: "Login" });
})
.catch(() => {
this.cleared_token = true;
this.$q.loading.hide();
this.$router.push({ name: "Login" });
});
},
},
mounted() {
this.getQRCodeData();
this.$q.dark.set(false);
},
beforeUnmount() {
if (!this.cleared_token) {
this.logout();
}
} finally {
loading.value = false;
}
}
async function logout() {
await auth.logout();
clearToken.value = false;
router.push({ name: "Login" });
}
onMounted(() => {
getQRCodeData();
$q.dark.set(false);
});
onBeforeUnmount(async () => {
if (clearToken.value) {
await auth.logout();
}
});
},
};
</script>

View File

@@ -1,88 +0,0 @@
<template>
<div class="full-page-terminal">
<div ref="xtermContainer" class="xterm-container"></div>
</div>
</template>
<style>
.full-page-terminal {
height: 100vh;
display: flex;
flex-direction: column;
}
.xterm-container {
flex-grow: 1;
overflow: hidden;
}
</style>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { useResizeObserver, useDebounceFn } from "@vueuse/core";
import { useCliWSConnection } from "@/websocket/websocket";
import "@xterm/xterm/css/xterm.css";
const xtermContainer = ref<HTMLElement | null>(null);
let term: Terminal;
const fit = new FitAddon();
const { data, send, close } = useCliWSConnection();
onMounted(() => {
setupXTerm();
useResizeObserver(xtermContainer, () => {
resizeWindow();
});
});
onBeforeUnmount(() => {
disconnect();
});
function setupXTerm() {
term = new Terminal({
convertEol: true,
fontFamily: "Menlo, Monaco, Courier New, monospace",
fontSize: 15,
fontWeight: 400,
cursorBlink: true,
theme: {
background: "#333",
},
});
term.loadAddon(fit);
term.open(xtermContainer.value!);
fit.fit();
term.onData((data) => {
send(JSON.stringify({ action: "trmmcli.input", data: { input: data } }));
});
}
const resizeWindow = useDebounceFn(() => {
fit.fit();
const dims = { cols: term.cols, rows: term.rows };
send(JSON.stringify({ action: "trmmcli.resize", data: dims }));
}, 300);
function disconnect() {
term.dispose();
close();
send(JSON.stringify({ action: "trmmcli.disconnect" }));
}
interface WSTrmmCliOutput {
output: string;
messageId: string;
}
watch(data, (newValue) => {
if (newValue.action === "trmmcli.output") {
const incomingData = newValue.data as WSTrmmCliOutput;
term.write(incomingData.output);
}
});
</script>

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

View File

@@ -1,81 +0,0 @@
import { ref, watch } from "vue";
import { UseWebSocketReturn, useWebSocket } from "@vueuse/core";
import { getBaseUrl } from "@/boot/axios";
import { useAuthStore } from "@/stores/auth";
export function getWSUrl(path: string, token: string | null) {
const url = getBaseUrl().split("://")[1];
const proto =
process.env.NODE_ENV === "production" || process.env.DOCKER_BUILD
? "wss"
: "ws";
return `${proto}://${url}/ws/${path}/?access_token=${token}`;
}
interface WSReturn {
action: string;
data: unknown;
}
let WSConnection: UseWebSocketReturn<string> | undefined = undefined;
export function useDashWSConnection() {
const auth = useAuthStore();
if (WSConnection === undefined) {
const url = getWSUrl("dashinfo", auth.token);
WSConnection = useWebSocket(url, {
autoReconnect: true,
});
}
const { status, data, send, open, close } = WSConnection;
const parsedData = ref<WSReturn>({ action: "", data: {} });
watch(data, (newValue) => {
if (newValue) parsedData.value = JSON.parse(newValue);
});
function closeConnection() {
WSConnection = undefined;
close();
}
return {
status,
data: parsedData,
send,
open,
close: closeConnection,
};
}
let WSCliConnection: UseWebSocketReturn<string> | undefined = undefined;
export function useCliWSConnection() {
const auth = useAuthStore();
if (WSCliConnection === undefined) {
const url = getWSUrl("trmmcli", auth.token);
WSCliConnection = useWebSocket(url);
}
const { status, data, send, open, close } = WSCliConnection;
const parsedData = ref<WSReturn>({ action: "", data: {} });
watch(data, (newValue) => {
if (newValue) parsedData.value = JSON.parse(newValue);
});
function closeConnection() {
WSCliConnection = undefined;
close();
}
return {
status,
data: parsedData,
send,
open,
close: closeConnection,
};
}