Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2422ff2174 | ||
|
|
144949e9f4 | ||
|
|
1d8dcb7b1f | ||
|
|
d0a655f570 | ||
|
|
a9e14e4cb4 | ||
|
|
6ff5e7bf94 | ||
|
|
f7b52e506d | ||
|
|
4932997498 | ||
|
|
09ecc36bcd | ||
|
|
4d8abbaa12 | ||
|
|
9f143a7e05 | ||
|
|
e51efaa9e2 | ||
|
|
64edab9df4 | ||
|
|
85f408ae94 | ||
|
|
26ed91cad9 | ||
|
|
bb828b5996 | ||
|
|
2583e9ac9e | ||
|
|
acaced7122 | ||
|
|
795ba12f3a | ||
|
|
face099460 | ||
|
|
2690e9daef | ||
|
|
ec5ef65911 | ||
|
|
237b097684 | ||
|
|
6f6d98fae2 | ||
|
|
583f57f2af | ||
|
|
4270fd0d19 | ||
|
|
02eeea50e3 | ||
|
|
54207d1c0f | ||
|
|
16b9bf1529 | ||
|
|
1adeadd48e | ||
|
|
fada3c2ed7 | ||
|
|
c1cd6114de | ||
|
|
79d02060ef | ||
|
|
3ce67b0701 | ||
|
|
8aab840633 | ||
|
|
0ce8da44c1 | ||
|
|
856a3b8b96 | ||
|
|
0e59f580c3 | ||
|
|
d0cf72bbd2 | ||
|
|
65096e6b88 | ||
|
|
c31ed666b5 | ||
|
|
09e39ef6da | ||
|
|
75a9ef88d1 | ||
|
|
0eb81662d3 | ||
|
|
541134a88f | ||
|
|
561da0496c | ||
|
|
ee8aada530 | ||
|
|
fa2ef65103 | ||
|
|
d73991cb0a | ||
|
|
a8e5203b58 | ||
|
|
bdf7cd7bf4 | ||
|
|
c3bd551b3a | ||
|
|
e045485d8c | ||
|
|
fa0992c49f | ||
|
|
21ea5a1981 | ||
|
|
a53a3b3343 | ||
|
|
ddb7c82575 | ||
|
|
fbb221fcac | ||
|
|
0d832ba833 | ||
|
|
870d70b4f2 | ||
|
|
33dbeb5552 | ||
|
|
9457bf2bc5 | ||
|
|
797b27af13 | ||
|
|
f6bbe3ecd8 | ||
|
|
f0c603d36f | ||
|
|
f87c6b2a10 | ||
|
|
4186b1cbf2 | ||
|
|
393b4fa90a | ||
|
|
dce732ec3c | ||
|
|
0c744eded6 | ||
|
|
4c1a231811 | ||
|
|
c53179892c | ||
|
|
1f5af9ba2d | ||
|
|
b4b63826dc | ||
|
|
45e2690a81 | ||
|
|
2ff504db09 | ||
|
|
0c89e58d8c | ||
|
|
e1dc75e2d8 | ||
|
|
8f1c694071 | ||
|
|
e837c494cb | ||
|
|
13f0f117da | ||
|
|
0b6ae80777 | ||
|
|
cfe1cb2dbf | ||
|
|
e1dc8050e3 | ||
|
|
3e6365574e | ||
|
|
5114ff40aa | ||
|
|
6ea7c92b20 | ||
|
|
20d534eab0 | ||
|
|
1b2286c4f8 | ||
|
|
8207f30234 | ||
|
|
68036f6837 | ||
|
|
03fae45ac5 | ||
|
|
c2591c9e7d | ||
|
|
7fcbe6fbd8 | ||
|
|
a2f472ef9c | ||
|
|
8403ac0e93 | ||
|
|
b7a91563b0 | ||
|
|
ab19afca16 | ||
|
|
f24c6a7a80 | ||
|
|
99490bf859 | ||
|
|
72cdeeaa6a | ||
|
|
1eca4d605b | ||
|
|
52ee98f6f8 | ||
|
|
d270b877c9 | ||
|
|
fd8b2a1d98 | ||
|
|
f518043d8d | ||
|
|
cc2335558d | ||
|
|
a8a171ba2c | ||
|
|
24a63f477e | ||
|
|
ddeb6293a1 |
2
.github/workflows/build-release.yml
vendored
2
.github/workflows/build-release.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "20.11.1"
|
node-version: "20.18.0"
|
||||||
|
|
||||||
- run: touch env-config.js
|
- run: touch env-config.js
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/frontend-linting.yml
vendored
6
.github/workflows/frontend-linting.yml
vendored
@@ -9,11 +9,11 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: "20.18.0"
|
||||||
- run: npm install
|
- run: npm install
|
||||||
|
|
||||||
- name: Run Prettier formatting
|
- name: Run Prettier formatting
|
||||||
|
|||||||
1517
package-lock.json
generated
1517
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.101.46",
|
"version": "0.101.52",
|
||||||
"private": true,
|
"private": true,
|
||||||
"productName": "Tactical RMM",
|
"productName": "Tactical RMM",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -10,38 +10,38 @@
|
|||||||
"format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
|
"format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "1.16.12",
|
"@quasar/extras": "1.16.13",
|
||||||
"@vueuse/core": "10.11.0",
|
"@vueuse/core": "11.2.0",
|
||||||
"@vueuse/integrations": "10.11.0",
|
"@vueuse/integrations": "11.2.0",
|
||||||
"@vueuse/shared": "10.11.0",
|
"@vueuse/shared": "11.2.0",
|
||||||
"apexcharts": "3.49.2",
|
"apexcharts": "3.54.1",
|
||||||
"axios": "1.7.2",
|
"axios": "1.7.7",
|
||||||
"dotenv": "16.4.5",
|
"dotenv": "16.4.5",
|
||||||
"monaco-editor": "0.50.0",
|
"monaco-editor": "0.50.0",
|
||||||
"pinia": "2.1.7",
|
"pinia": "2.2.6",
|
||||||
"qrcode": "1.5.3",
|
"qrcode": "1.5.4",
|
||||||
"quasar": "2.16.5",
|
"quasar": "2.17.2",
|
||||||
"vue": "3.4.31",
|
"vue": "3.5.12",
|
||||||
"vue-router": "4.4.0",
|
"vue-router": "4.4.5",
|
||||||
"vue3-apexcharts": "1.5.3",
|
"vue3-apexcharts": "1.7.0",
|
||||||
"vuedraggable": "4.1.0",
|
"vuedraggable": "4.1.0",
|
||||||
"vuex": "4.1.0",
|
"vuex": "4.1.0",
|
||||||
"@xterm/xterm": "5.5.0",
|
"@xterm/xterm": "5.5.0",
|
||||||
"@xterm/addon-fit": "0.10.0",
|
"@xterm/addon-fit": "0.10.0",
|
||||||
"yaml": "2.4.5"
|
"yaml": "2.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@intlify/unplugin-vue-i18n": "4.0.0",
|
"@intlify/unplugin-vue-i18n": "4.0.0",
|
||||||
"@quasar/app-vite": "1.9.3",
|
"@quasar/app-vite": "1.10.2",
|
||||||
"@quasar/cli": "2.4.1",
|
"@quasar/cli": "2.4.1",
|
||||||
"@types/node": "20.14.10",
|
"@types/node": "22.7.5",
|
||||||
"@typescript-eslint/eslint-plugin": "7.16.0",
|
"@typescript-eslint/eslint-plugin": "7.16.0",
|
||||||
"@typescript-eslint/parser": "7.16.0",
|
"@typescript-eslint/parser": "7.16.0",
|
||||||
"autoprefixer": "10.4.19",
|
"autoprefixer": "10.4.20",
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-plugin-vue": "8.7.1",
|
"eslint-plugin-vue": "8.7.1",
|
||||||
"prettier": "3.2.5",
|
"prettier": "3.3.3",
|
||||||
"typescript": "5.5.3"
|
"typescript": "5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
// Configuration for your app
|
// Configuration for your app
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
|
||||||
|
|
||||||
|
const { mergeConfig } = require("vite");
|
||||||
const { configure } = require("quasar/wrappers");
|
const { configure } = require("quasar/wrappers");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
@@ -78,9 +79,22 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
// polyfillModulePreload: true,
|
// polyfillModulePreload: true,
|
||||||
distDir: "dist/",
|
distDir: "dist/",
|
||||||
|
|
||||||
// extendViteConf (viteConf) {},
|
/* 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 */
|
||||||
// viteVuePluginOptions: {},
|
// viteVuePluginOptions: {},
|
||||||
|
|
||||||
// vitePlugins: []
|
// vitePlugins: []
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,34 @@ 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
|
// role api function
|
||||||
export async function fetchRoles(params = {}) {
|
export async function fetchRoles(params = {}) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -8,8 +8,15 @@ import type {
|
|||||||
TestRunURLActionResponse,
|
TestRunURLActionResponse,
|
||||||
} from "@/types/core/urlactions";
|
} from "@/types/core/urlactions";
|
||||||
|
|
||||||
|
import type { CoreSetting } from "@/types/core/settings";
|
||||||
|
|
||||||
const baseUrl = "/core";
|
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 = {}) {
|
export async function fetchDashboardInfo(params = {}) {
|
||||||
const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params });
|
const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params });
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export function setErrorMessage(data, message) {
|
|||||||
|
|
||||||
export default function ({ app, router }) {
|
export default function ({ app, router }) {
|
||||||
app.config.globalProperties.$axios = axios;
|
app.config.globalProperties.$axios = axios;
|
||||||
|
axios.defaults.withCredentials = true;
|
||||||
|
|
||||||
axios.interceptors.request.use(
|
axios.interceptors.request.use(
|
||||||
function (config) {
|
function (config) {
|
||||||
@@ -65,12 +66,20 @@ export default function ({ app, router }) {
|
|||||||
// perms
|
// perms
|
||||||
else if (error.response.status === 403) {
|
else if (error.response.status === 403) {
|
||||||
// don't notify user if method is GET
|
// don't notify user if method is GET
|
||||||
if (error.config.method === "get" || error.config.method === "patch")
|
if (
|
||||||
|
error.config.method === "get" ||
|
||||||
|
error.config.method === "patch" ||
|
||||||
|
error.config.url === "accounts/ssoproviders/token/"
|
||||||
|
)
|
||||||
return Promise.reject({ ...error });
|
return Promise.reject({ ...error });
|
||||||
text = error.response.data.detail;
|
text = error.response.data.detail;
|
||||||
}
|
}
|
||||||
// catch all for other 400 error messages
|
// catch all for other 400 error messages
|
||||||
else if (error.response.status >= 400 && error.response.status < 500) {
|
else if (
|
||||||
|
error.response.status >= 400 &&
|
||||||
|
error.response.status < 500 &&
|
||||||
|
error.response.status !== 423
|
||||||
|
) {
|
||||||
if (error.config.responseType === "blob") {
|
if (error.config.responseType === "blob") {
|
||||||
text = (await error.response.data.text()).replace(/^"|"$/g, "");
|
text = (await error.response.data.text()).replace(/^"|"$/g, "");
|
||||||
} else if (error.response.data.non_field_errors) {
|
} else if (error.response.data.non_field_errors) {
|
||||||
@@ -85,7 +94,7 @@ export default function ({ app, router }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text || error.response) {
|
if ((text || error.response) && error.response.status !== 423) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
color: "negative",
|
color: "negative",
|
||||||
message: text ? text : "",
|
message: text ? text : "",
|
||||||
|
|||||||
@@ -1,157 +1,202 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="width: 900px; max-width: 90vw">
|
<q-card style="width: 65vw; max-width: 70vw; min-height: 50vh">
|
||||||
<q-card>
|
<q-bar>
|
||||||
<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">
|
||||||
<q-btn
|
<q-btn
|
||||||
ref="refresh"
|
ref="new"
|
||||||
@click="getUsers"
|
label="New"
|
||||||
class="q-mr-sm"
|
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
push
|
push
|
||||||
icon="refresh"
|
unelevated
|
||||||
/>User Administration
|
no-caps
|
||||||
<q-space />
|
icon="add"
|
||||||
<q-btn dense flat icon="close" v-close-popup>
|
@click="showAddUserModal"
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
<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-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>
|
</div>
|
||||||
</q-card>
|
<q-table
|
||||||
</div>
|
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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<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>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import mixins from "@/mixins/mixins";
|
import mixins from "@/mixins/mixins";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { mapState, useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
|
import { useQuasar } from "quasar";
|
||||||
|
|
||||||
|
import { mapState as piniaMapState } from "pinia";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import UserForm from "@/components/modals/admin/UserForm.vue";
|
import UserForm from "@/components/modals/admin/UserForm.vue";
|
||||||
import UserResetPasswordForm from "@/components/modals/admin/UserResetPasswordForm.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 {
|
export default {
|
||||||
name: "AdminManager",
|
name: "AdminManager",
|
||||||
@@ -161,8 +206,30 @@ export default {
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
const formatDate = computed(() => store.getters.formatDate);
|
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 {
|
return {
|
||||||
formatDate,
|
formatDate,
|
||||||
|
showSSOAccounts,
|
||||||
|
showSessions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -175,6 +242,13 @@ export default {
|
|||||||
field: "is_active",
|
field: "is_active",
|
||||||
align: "left",
|
align: "left",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "sso",
|
||||||
|
label: "",
|
||||||
|
field: "sso",
|
||||||
|
align: "left",
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "username",
|
name: "username",
|
||||||
label: "Username",
|
label: "Username",
|
||||||
@@ -316,7 +390,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...piniaMapState(useAuthStore, {
|
||||||
logged_in_user: (state) => state.username,
|
logged_in_user: (state) => state.username,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -46,6 +46,9 @@
|
|||||||
<template v-slot:header-cell-plat="props">
|
<template v-slot:header-cell-plat="props">
|
||||||
<q-th auto-width :props="props"></q-th>
|
<q-th auto-width :props="props"></q-th>
|
||||||
</template>
|
</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">
|
<template v-slot:header-cell-checks-status="props">
|
||||||
<q-th :props="props">
|
<q-th :props="props">
|
||||||
<q-icon name="fas fa-check-double" size="1.2em">
|
<q-icon name="fas fa-check-double" size="1.2em">
|
||||||
@@ -206,6 +209,20 @@
|
|||||||
</q-icon>
|
</q-icon>
|
||||||
</q-td>
|
</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-td key="checks-status" :props="props">
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="props.row.maintenance_mode"
|
v-if="props.row.maintenance_mode"
|
||||||
|
|||||||
@@ -151,6 +151,14 @@
|
|||||||
v-model="localRole.can_edit_core_settings"
|
v-model="localRole.can_edit_core_settings"
|
||||||
label="Edit Global 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
|
<q-checkbox
|
||||||
v-model="localRole.can_do_server_maint"
|
v-model="localRole.can_do_server_maint"
|
||||||
label="Do Server Maintenance"
|
label="Do Server Maintenance"
|
||||||
@@ -477,6 +485,8 @@ export default {
|
|||||||
// settings perms
|
// settings perms
|
||||||
can_view_core_settings: false,
|
can_view_core_settings: false,
|
||||||
can_edit_core_settings: false,
|
can_edit_core_settings: false,
|
||||||
|
can_view_global_keystore: false,
|
||||||
|
can_edit_global_keystore: false,
|
||||||
can_do_server_maint: false,
|
can_do_server_maint: false,
|
||||||
can_code_sign: false,
|
can_code_sign: false,
|
||||||
can_run_urlactions: false,
|
can_run_urlactions: false,
|
||||||
|
|||||||
151
src/components/accounts/UserSessionsTable.vue
Normal file
151
src/components/accounts/UserSessionsTable.vue
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<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>
|
||||||
@@ -302,9 +302,9 @@ export default {
|
|||||||
async function getURLActions() {
|
async function getURLActions() {
|
||||||
menuLoading.value = true;
|
menuLoading.value = true;
|
||||||
try {
|
try {
|
||||||
urlActions.value = (await fetchURLActions()).filter(
|
urlActions.value = (await fetchURLActions())
|
||||||
(action) => action.action_type === "web",
|
.filter((action) => action.action_type === "web")
|
||||||
);
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
if (urlActions.value.length === 0) {
|
if (urlActions.value.length === 0) {
|
||||||
notifyWarning(
|
notifyWarning(
|
||||||
@@ -312,8 +312,11 @@ export default {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
menuLoading.value = true;
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
menuLoading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSendCommand(agent) {
|
function showSendCommand(agent) {
|
||||||
|
|||||||
@@ -295,7 +295,12 @@
|
|||||||
</q-td>
|
</q-td>
|
||||||
<q-td v-else></q-td>
|
<q-td v-else></q-td>
|
||||||
<!-- name -->
|
<!-- name -->
|
||||||
<q-td>{{ props.row.name }}</q-td>
|
<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
|
||||||
|
>
|
||||||
<!-- sync status -->
|
<!-- sync status -->
|
||||||
<q-td v-if="props.row.task_result.sync_status === 'notsynced'"
|
<q-td v-if="props.row.task_result.sync_status === 'notsynced'"
|
||||||
>Will sync on next agent checkin</q-td
|
>Will sync on next agent checkin</q-td
|
||||||
|
|||||||
@@ -370,7 +370,13 @@
|
|||||||
style="cursor: pointer; text-decoration: underline"
|
style="cursor: pointer; text-decoration: underline"
|
||||||
class="text-primary"
|
class="text-primary"
|
||||||
@click="showPingInfo(props.row)"
|
@click="showPingInfo(props.row)"
|
||||||
>Last Output</span
|
>{{
|
||||||
|
grep(props.row.check_result.more_info, [
|
||||||
|
"transmitted",
|
||||||
|
"received",
|
||||||
|
"packet loss",
|
||||||
|
])
|
||||||
|
}}</span
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-else-if="
|
v-else-if="
|
||||||
@@ -379,7 +385,7 @@
|
|||||||
style="cursor: pointer; text-decoration: underline"
|
style="cursor: pointer; text-decoration: underline"
|
||||||
class="text-primary"
|
class="text-primary"
|
||||||
@click="showScriptOutput(props.row.check_result)"
|
@click="showScriptOutput(props.row.check_result)"
|
||||||
>Last Output</span
|
>{{ processOutput(props.row.check_result) }}</span
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-else-if="
|
v-else-if="
|
||||||
@@ -392,7 +398,9 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-else-if="
|
v-else-if="
|
||||||
props.row.check_type === 'diskspace' ||
|
['diskspace', 'cpuload', 'memory'].includes(
|
||||||
|
props.row.check_type,
|
||||||
|
) ||
|
||||||
(props.row.check_type === 'winsvc' && props.row.check_result.id)
|
(props.row.check_type === 'winsvc' && props.row.check_result.id)
|
||||||
"
|
"
|
||||||
>{{ props.row.check_result.more_info }}</span
|
>{{ props.row.check_result.more_info }}</span
|
||||||
@@ -510,6 +518,40 @@ export default {
|
|||||||
descending: false,
|
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) {
|
function getAlertSeverity(check) {
|
||||||
if (check.check_result.alert_severity) {
|
if (check.check_result.alert_severity) {
|
||||||
return check.check_result.alert_severity;
|
return check.check_result.alert_severity;
|
||||||
@@ -707,6 +749,8 @@ export default {
|
|||||||
getAlertSeverity,
|
getAlertSeverity,
|
||||||
runChecks,
|
runChecks,
|
||||||
resetAllChecks,
|
resetAllChecks,
|
||||||
|
grep,
|
||||||
|
processOutput,
|
||||||
|
|
||||||
// dialogs
|
// dialogs
|
||||||
showScriptOutput,
|
showScriptOutput,
|
||||||
|
|||||||
@@ -17,70 +17,85 @@
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
>
|
>
|
||||||
<template v-slot:top>
|
<template v-slot:top>
|
||||||
<q-btn
|
<div class="q-gutter-md flex flex-center items-center">
|
||||||
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
|
<q-btn
|
||||||
:disable="pollInterval === 1"
|
v-if="isPolling"
|
||||||
dense
|
dense
|
||||||
@click="pollIntervalChanged('subtract')"
|
flat
|
||||||
push
|
push
|
||||||
icon="remove"
|
@click="stopPoll"
|
||||||
size="sm"
|
icon="stop"
|
||||||
color="grey"
|
label="Stop Live Refresh"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
|
v-else
|
||||||
dense
|
dense
|
||||||
|
flat
|
||||||
push
|
push
|
||||||
icon="add"
|
@click="startPoll"
|
||||||
size="sm"
|
icon="play_arrow"
|
||||||
color="grey"
|
label="Resume Live Refresh"
|
||||||
@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 />
|
<div class="flex flex-center q-ml-md">
|
||||||
<q-input v-model="filter" outlined label="Search" dense clearable>
|
<q-icon name="fas fa-microchip" class="q-mr-xs" />
|
||||||
<template v-slot:prepend>
|
<div class="text-caption q-mr-sm">
|
||||||
<q-icon name="search" />
|
CPU Usage:
|
||||||
</template>
|
<span class="text-body1 text-weight-medium"
|
||||||
</q-input>
|
>{{ totalCpuUsage }}%</span
|
||||||
<!-- file download doesn't work so disabling -->
|
>
|
||||||
<export-table-btn
|
</div>
|
||||||
v-show="false"
|
|
||||||
class="q-ml-sm"
|
<q-icon name="fas fa-memory" class="q-mr-xs" />
|
||||||
:columns="columns"
|
<div class="text-caption">
|
||||||
:data="processes"
|
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>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
<q-tr :props="props" class="cursor-pointer">
|
<q-tr :props="props" class="cursor-pointer">
|
||||||
@@ -121,9 +136,6 @@ import {
|
|||||||
import { bytes2Human } from "@/utils/format";
|
import { bytes2Human } from "@/utils/format";
|
||||||
import { notifySuccess } from "@/utils/notify";
|
import { notifySuccess } from "@/utils/notify";
|
||||||
|
|
||||||
// ui imports
|
|
||||||
import ExportTableBtn from "@/components/ui/ExportTableBtn.vue";
|
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
name: "name",
|
name: "name",
|
||||||
@@ -164,7 +176,6 @@ const columns = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { ExportTableBtn },
|
|
||||||
name: "ProcessManager",
|
name: "ProcessManager",
|
||||||
props: {
|
props: {
|
||||||
agent_id: !String,
|
agent_id: !String,
|
||||||
@@ -175,52 +186,71 @@ export default {
|
|||||||
const poll = ref(null);
|
const poll = ref(null);
|
||||||
const isPolling = computed(() => !!poll.value);
|
const isPolling = computed(() => !!poll.value);
|
||||||
|
|
||||||
async function startPoll() {
|
function startPoll() {
|
||||||
await getProcesses();
|
stopPoll();
|
||||||
if (processes.value.length > 0) {
|
getProcesses();
|
||||||
refreshProcesses();
|
poll.value = setInterval(() => {
|
||||||
}
|
getProcesses();
|
||||||
|
}, pollInterval.value * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPoll() {
|
function stopPoll() {
|
||||||
clearInterval(poll.value);
|
if (poll.value) {
|
||||||
poll.value = null;
|
clearInterval(poll.value);
|
||||||
|
poll.value = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pollIntervalChanged(action) {
|
function pollIntervalChanged(action) {
|
||||||
if (action === "subtract" && pollInterval.value <= 1) {
|
|
||||||
stopPoll();
|
|
||||||
startPoll();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (action === "add") {
|
if (action === "add") {
|
||||||
pollInterval.value++;
|
pollInterval.value++;
|
||||||
} else {
|
} else if (action === "subtract" && pollInterval.value > 1) {
|
||||||
pollInterval.value--;
|
pollInterval.value--;
|
||||||
}
|
}
|
||||||
stopPoll();
|
if (isPolling.value) {
|
||||||
startPoll();
|
startPoll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// process manager logic
|
// process manager logic
|
||||||
const processes = ref([]);
|
const processes = ref([]);
|
||||||
const filter = ref("");
|
const filter = ref("");
|
||||||
const memory = ref(null);
|
const total_ram = ref(0);
|
||||||
|
|
||||||
const loading = ref(false);
|
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() {
|
async function getProcesses() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
processes.value = await fetchAgentProcesses(props.agent_id);
|
try {
|
||||||
|
processes.value = await fetchAgentProcesses(props.agent_id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshProcesses() {
|
|
||||||
poll.value = setInterval(() => {
|
|
||||||
getProcesses(props.agent_id);
|
|
||||||
}, pollInterval.value * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function killProcess(pid) {
|
async function killProcess(pid) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
let result = "";
|
let result = "";
|
||||||
@@ -235,11 +265,8 @@ export default {
|
|||||||
|
|
||||||
// lifecycle hooks
|
// lifecycle hooks
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
memory.value = await fetchAgent(props.agent_id).total_ram;
|
total_ram.value = (await fetchAgent(props.agent_id)).total_ram;
|
||||||
await getProcesses();
|
startPoll();
|
||||||
if (processes.value.length > 0) {
|
|
||||||
refreshProcesses();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => clearInterval(poll.value));
|
onBeforeUnmount(() => clearInterval(poll.value));
|
||||||
@@ -248,10 +275,12 @@ export default {
|
|||||||
// reactive data
|
// reactive data
|
||||||
processes,
|
processes,
|
||||||
filter,
|
filter,
|
||||||
memory,
|
total_ram,
|
||||||
isPolling,
|
isPolling,
|
||||||
pollInterval,
|
pollInterval,
|
||||||
loading,
|
loading,
|
||||||
|
totalCpuUsage,
|
||||||
|
totalRamUsage,
|
||||||
|
|
||||||
// non-reactive data
|
// non-reactive data
|
||||||
columns,
|
columns,
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
<q-dialog ref="dialog" @hide="onHide">
|
<q-dialog ref="dialog" @hide="onHide">
|
||||||
<q-card class="q-dialog-plugin" style="min-width: 70vw">
|
<q-card class="q-dialog-plugin" style="min-width: 70vw">
|
||||||
<q-bar>
|
<q-bar>
|
||||||
|
<q-btn
|
||||||
|
ref="refresh"
|
||||||
|
@click="refresh"
|
||||||
|
class="q-mr-sm"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
push
|
||||||
|
icon="refresh"
|
||||||
|
/>
|
||||||
{{ title.slice(0, 27) }}
|
{{ title.slice(0, 27) }}
|
||||||
<q-space />
|
<q-space />
|
||||||
<q-btn dense flat icon="close" v-close-popup>
|
<q-btn dense flat icon="close" v-close-popup>
|
||||||
@@ -281,6 +290,13 @@ export default {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
refresh() {
|
||||||
|
if (this.type === "task") {
|
||||||
|
this.getTaskData();
|
||||||
|
} else {
|
||||||
|
this.getCheckData();
|
||||||
|
}
|
||||||
|
},
|
||||||
show() {
|
show() {
|
||||||
this.$refs.dialog.show();
|
this.$refs.dialog.show();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,12 +20,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div v-if="scriptInfo.stdout">
|
<div v-if="scriptInfo.stdout">
|
||||||
Standard Output
|
<script-output-copy-clip
|
||||||
|
label="Standard Output"
|
||||||
|
:data="scriptInfo.stdout"
|
||||||
|
/>
|
||||||
<q-separator />
|
<q-separator />
|
||||||
<pre>{{ scriptInfo.stdout }}</pre>
|
<pre>{{ scriptInfo.stdout }}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="scriptInfo.stderr">
|
<div v-if="scriptInfo.stderr">
|
||||||
Standard Error
|
<script-output-copy-clip
|
||||||
|
label="Standard Error"
|
||||||
|
:data="scriptInfo.stderr"
|
||||||
|
/>
|
||||||
<q-separator />
|
<q-separator />
|
||||||
<pre>{{ scriptInfo.stderr }}</pre>
|
<pre>{{ scriptInfo.stderr }}</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,8 +49,13 @@ import { computed } from "vue";
|
|||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { useDialogPluginComponent } from "quasar";
|
import { useDialogPluginComponent } from "quasar";
|
||||||
|
|
||||||
|
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ScriptOutput",
|
name: "ScriptOutput",
|
||||||
|
components: {
|
||||||
|
ScriptOutputCopyClip,
|
||||||
|
},
|
||||||
emits: [...useDialogPluginComponent.emits],
|
emits: [...useDialogPluginComponent.emits],
|
||||||
props: { scriptInfo: !Object },
|
props: { scriptInfo: !Object },
|
||||||
setup() {
|
setup() {
|
||||||
|
|||||||
@@ -116,7 +116,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from "vuex";
|
import { mapState as piniaMapState } from "pinia";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import mixins from "@/mixins/mixins";
|
import mixins from "@/mixins/mixins";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -145,7 +146,7 @@ export default {
|
|||||||
title() {
|
title() {
|
||||||
return this.user ? "Edit User" : "Add User";
|
return this.user ? "Edit User" : "Add User";
|
||||||
},
|
},
|
||||||
...mapState({
|
...piniaMapState(useAuthStore, {
|
||||||
logged_in_user: (state) => state.username,
|
logged_in_user: (state) => state.username,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -88,7 +88,24 @@
|
|||||||
outlined
|
outlined
|
||||||
mapOptions
|
mapOptions
|
||||||
filterable
|
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>
|
||||||
<q-card-section v-if="mode === 'script'" class="q-pt-none">
|
<q-card-section v-if="mode === 'script'" class="q-pt-none">
|
||||||
<tactical-dropdown
|
<tactical-dropdown
|
||||||
@@ -153,6 +170,39 @@
|
|||||||
</q-checkbox>
|
</q-checkbox>
|
||||||
</q-card-section>
|
</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-card-section v-if="mode === 'script' || mode === 'command'">
|
||||||
<q-input
|
<q-input
|
||||||
v-model.number="state.timeout"
|
v-model.number="state.timeout"
|
||||||
@@ -218,12 +268,14 @@ import {
|
|||||||
onMounted,
|
onMounted,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import { useDialogPluginComponent } from "quasar";
|
import { useDialogPluginComponent, openURL } from "quasar";
|
||||||
import { useScriptDropdown } from "@/composables/scripts";
|
import { useScriptDropdown } from "@/composables/scripts";
|
||||||
import { useAgentDropdown } from "@/composables/agents";
|
import { useAgentDropdown } from "@/composables/agents";
|
||||||
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
|
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
|
||||||
|
import { useCustomFieldDropdown } from "@/composables/core";
|
||||||
import { runBulkAction } from "@/api/agents";
|
import { runBulkAction } from "@/api/agents";
|
||||||
import { notifySuccess } from "@/utils/notify";
|
import { notifySuccess } from "@/utils/notify";
|
||||||
|
import { formatScriptSyntax } from "@/utils/format";
|
||||||
import { cmdPlaceholder } from "@/composables/agents";
|
import { cmdPlaceholder } from "@/composables/agents";
|
||||||
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
|
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
|
||||||
|
|
||||||
@@ -297,11 +349,18 @@ export default defineComponent({
|
|||||||
defaultTimeout,
|
defaultTimeout,
|
||||||
defaultArgs,
|
defaultArgs,
|
||||||
defaultEnvVars,
|
defaultEnvVars,
|
||||||
|
syntax,
|
||||||
|
link,
|
||||||
getScriptOptions,
|
getScriptOptions,
|
||||||
} = useScriptDropdown();
|
} = useScriptDropdown();
|
||||||
const { agents, agentOptions, getAgentOptions } = useAgentDropdown();
|
const { agents, agentOptions, getAgentOptions } = useAgentDropdown();
|
||||||
const { site, siteOptions, getSiteOptions } = useSiteDropdown();
|
const { site, siteOptions, getSiteOptions } = useSiteDropdown();
|
||||||
const { client, clientOptions, getClientOptions } = useClientDropdown();
|
const { client, clientOptions, getClientOptions } = useClientDropdown();
|
||||||
|
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
|
||||||
|
|
||||||
|
function openScriptURL() {
|
||||||
|
link.value ? openURL(link.value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
// bulk action logic
|
// bulk action logic
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@@ -312,6 +371,9 @@ export default defineComponent({
|
|||||||
cmd: "",
|
cmd: "",
|
||||||
shell: "cmd",
|
shell: "cmd",
|
||||||
custom_shell: null,
|
custom_shell: null,
|
||||||
|
custom_field: null,
|
||||||
|
collector_all_output: false,
|
||||||
|
save_to_agent_note: false,
|
||||||
patchMode: "scan",
|
patchMode: "scan",
|
||||||
offlineAgents: false,
|
offlineAgents: false,
|
||||||
client,
|
client,
|
||||||
@@ -324,6 +386,7 @@ export default defineComponent({
|
|||||||
run_as_user: false,
|
run_as_user: false,
|
||||||
});
|
});
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const collector = ref(false);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => state.target,
|
() => state.target,
|
||||||
@@ -395,6 +458,8 @@ export default defineComponent({
|
|||||||
state,
|
state,
|
||||||
agentOptions,
|
agentOptions,
|
||||||
clientOptions,
|
clientOptions,
|
||||||
|
collector,
|
||||||
|
customFieldOptions,
|
||||||
siteOptions,
|
siteOptions,
|
||||||
filterByPlatformOptions,
|
filterByPlatformOptions,
|
||||||
loading,
|
loading,
|
||||||
@@ -408,6 +473,7 @@ export default defineComponent({
|
|||||||
patchModeOptions,
|
patchModeOptions,
|
||||||
runAsUserToolTip,
|
runAsUserToolTip,
|
||||||
envVarsLabel,
|
envVarsLabel,
|
||||||
|
syntax,
|
||||||
|
|
||||||
//computed
|
//computed
|
||||||
modalTitle,
|
modalTitle,
|
||||||
@@ -416,6 +482,8 @@ export default defineComponent({
|
|||||||
submit,
|
submit,
|
||||||
cmdPlaceholder,
|
cmdPlaceholder,
|
||||||
supportsRunAsUser,
|
supportsRunAsUser,
|
||||||
|
openScriptURL,
|
||||||
|
formatScriptSyntax,
|
||||||
|
|
||||||
// quasar dialog plugin
|
// quasar dialog plugin
|
||||||
dialogRef,
|
dialogRef,
|
||||||
|
|||||||
@@ -137,7 +137,7 @@
|
|||||||
<q-radio
|
<q-radio
|
||||||
v-model="goarch"
|
v-model="goarch"
|
||||||
:val="GOARCH_ARM64"
|
:val="GOARCH_ARM64"
|
||||||
label="Apple Silicon (M1, M2, M3)"
|
label="Apple Silicon (M-Series)"
|
||||||
v-show="agentOS === 'darwin'"
|
v-show="agentOS === 'darwin'"
|
||||||
/>
|
/>
|
||||||
<q-radio
|
<q-radio
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
new-value-mode="add"
|
new-value-mode="add"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section>
|
<q-card-section v-if="!state.run_on_server">
|
||||||
<q-option-group
|
<q-option-group
|
||||||
v-model="state.output"
|
v-model="state.output"
|
||||||
:options="outputOptions"
|
:options="outputOptions"
|
||||||
@@ -140,10 +140,30 @@
|
|||||||
/>
|
/>
|
||||||
<q-checkbox v-model="state.save_all_output" label="Save all output" />
|
<q-checkbox v-model="state.save_all_output" label="Save all output" />
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section v-if="agent.plat === 'windows'">
|
<q-card-section>
|
||||||
<q-checkbox v-model="state.run_as_user" label="Run As User">
|
<q-checkbox
|
||||||
|
v-if="agent.plat === 'windows' && !state.run_on_server"
|
||||||
|
v-model="state.run_as_user"
|
||||||
|
label="Run As User"
|
||||||
|
>
|
||||||
<q-tooltip>{{ runAsUserToolTip }}</q-tooltip>
|
<q-tooltip>{{ runAsUserToolTip }}</q-tooltip>
|
||||||
</q-checkbox>
|
</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-card-section>
|
<q-card-section>
|
||||||
<q-input
|
<q-input
|
||||||
@@ -175,7 +195,39 @@
|
|||||||
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
|
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
|
||||||
style="max-height: 50vh"
|
style="max-height: 50vh"
|
||||||
>
|
>
|
||||||
<pre>{{ ret }}</pre>
|
<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>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -184,7 +236,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// composition imports
|
// composition imports
|
||||||
import { ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { useStore } from "vuex";
|
||||||
import { useDialogPluginComponent, openURL } from "quasar";
|
import { useDialogPluginComponent, openURL } from "quasar";
|
||||||
import { useScriptDropdown } from "@/composables/scripts";
|
import { useScriptDropdown } from "@/composables/scripts";
|
||||||
import { useCustomFieldDropdown } from "@/composables/core";
|
import { useCustomFieldDropdown } from "@/composables/core";
|
||||||
@@ -195,10 +248,18 @@ import { formatScriptSyntax } from "@/utils/format";
|
|||||||
|
|
||||||
//ui imports
|
//ui imports
|
||||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||||
|
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type { Agent } from "@/types/agents";
|
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
|
// static data
|
||||||
const outputOptions = [
|
const outputOptions = [
|
||||||
{ label: "Wait for Output", value: "wait" },
|
{ label: "Wait for Output", value: "wait" },
|
||||||
@@ -248,6 +309,7 @@ const state = ref({
|
|||||||
env_vars: defaultEnvVars,
|
env_vars: defaultEnvVars,
|
||||||
timeout: defaultTimeout,
|
timeout: defaultTimeout,
|
||||||
run_as_user: false,
|
run_as_user: false,
|
||||||
|
run_on_server: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ret = ref(null);
|
const ret = ref(null);
|
||||||
|
|||||||
@@ -104,6 +104,9 @@
|
|||||||
type="submit"
|
type="submit"
|
||||||
/>
|
/>
|
||||||
</q-card-actions>
|
</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
|
<q-card-section
|
||||||
v-if="ret !== null"
|
v-if="ret !== null"
|
||||||
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
|
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
|
||||||
@@ -124,8 +127,13 @@ import { sendAgentCommand } from "@/api/agents";
|
|||||||
import { cmdPlaceholder } from "@/composables/agents";
|
import { cmdPlaceholder } from "@/composables/agents";
|
||||||
import { runAsUserToolTip } from "@/constants/constants";
|
import { runAsUserToolTip } from "@/constants/constants";
|
||||||
|
|
||||||
|
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "SendCommand",
|
name: "SendCommand",
|
||||||
|
components: {
|
||||||
|
ScriptOutputCopyClip,
|
||||||
|
},
|
||||||
emits: [...useDialogPluginComponent.emits],
|
emits: [...useDialogPluginComponent.emits],
|
||||||
props: {
|
props: {
|
||||||
agent: !Object,
|
agent: !Object,
|
||||||
|
|||||||
@@ -249,6 +249,20 @@ export default {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
format: (a) => this.formatDate(a),
|
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",
|
name: "hostname",
|
||||||
label: "Agent",
|
label: "Agent",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
<!-- name -->
|
<!-- name -->
|
||||||
<q-td>
|
<q-td>
|
||||||
{{ props.row.name }}
|
{{ props.row.name }}
|
||||||
|
<q-tooltip :delay="600">ID: {{ props.row.id }}</q-tooltip>
|
||||||
</q-td>
|
</q-td>
|
||||||
<!-- type -->
|
<!-- type -->
|
||||||
<q-td>
|
<q-td>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<q-tab name="webhooks" label="Web Hooks" />
|
<q-tab name="webhooks" label="Web Hooks" />
|
||||||
<q-tab name="retention" label="Retention" />
|
<q-tab name="retention" label="Retention" />
|
||||||
<q-tab name="apikeys" label="API Keys" />
|
<q-tab name="apikeys" label="API Keys" />
|
||||||
|
<q-tab name="sso" label="Single Sign-On (SSO)" />
|
||||||
<!-- <q-tab name="openai" label="Open AI" /> -->
|
<!-- <q-tab name="openai" label="Open AI" /> -->
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
</template>
|
</template>
|
||||||
@@ -636,6 +637,11 @@
|
|||||||
<APIKeysTable />
|
<APIKeysTable />
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<!-- sso integration -->
|
||||||
|
<q-tab-panel name="sso">
|
||||||
|
<SSOProvidersTable />
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
<!-- Open AI -->
|
<!-- Open AI -->
|
||||||
<!-- <q-tab-panel name="openai">
|
<!-- <q-tab-panel name="openai">
|
||||||
<div class="text-subtitle2">Open AI</div>
|
<div class="text-subtitle2">Open AI</div>
|
||||||
@@ -685,7 +691,8 @@
|
|||||||
v-show="
|
v-show="
|
||||||
tab !== 'customfields' &&
|
tab !== 'customfields' &&
|
||||||
tab !== 'keystore' &&
|
tab !== 'keystore' &&
|
||||||
tab !== 'urlactions'
|
tab !== 'urlactions' &&
|
||||||
|
tab !== 'sso'
|
||||||
"
|
"
|
||||||
label="Save"
|
label="Save"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -722,6 +729,7 @@ import CustomFields from "@/components/modals/coresettings/CustomFields.vue";
|
|||||||
import KeyStoreTable from "@/components/modals/coresettings/KeyStoreTable.vue";
|
import KeyStoreTable from "@/components/modals/coresettings/KeyStoreTable.vue";
|
||||||
import URLActionsTable from "@/components/modals/coresettings/URLActionsTable.vue";
|
import URLActionsTable from "@/components/modals/coresettings/URLActionsTable.vue";
|
||||||
import APIKeysTable from "@/components/core/APIKeysTable.vue";
|
import APIKeysTable from "@/components/core/APIKeysTable.vue";
|
||||||
|
import SSOProvidersTable from "@/ee/sso/components/SSOProvidersTable.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "EditCoreSettings",
|
name: "EditCoreSettings",
|
||||||
@@ -731,7 +739,7 @@ export default {
|
|||||||
KeyStoreTable,
|
KeyStoreTable,
|
||||||
URLActionsTable,
|
URLActionsTable,
|
||||||
APIKeysTable,
|
APIKeysTable,
|
||||||
// ServerTasksTable,
|
SSOProvidersTable,
|
||||||
},
|
},
|
||||||
mixins: [mixins],
|
mixins: [mixins],
|
||||||
data() {
|
data() {
|
||||||
@@ -767,6 +775,13 @@ export default {
|
|||||||
return this.$store.state.hosted;
|
return this.$store.state.hosted;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
tab(newTab, oldTab) {
|
||||||
|
if (oldTab === "sso") {
|
||||||
|
this.getCoreSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openURL(url) {
|
openURL(url) {
|
||||||
openURL(url);
|
openURL(url);
|
||||||
|
|||||||
@@ -27,8 +27,16 @@
|
|||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
v-model="localKey.value"
|
v-model="localKey.value"
|
||||||
|
:type="isPwd ? 'password' : 'text'"
|
||||||
:rules="[(val) => !!val || '*Required']"
|
: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-section>
|
||||||
|
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
@@ -50,6 +58,7 @@ export default {
|
|||||||
props: { globalKey: Object },
|
props: { globalKey: Object },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
isPwd: true,
|
||||||
localKey: {
|
localKey: {
|
||||||
name: "",
|
name: "",
|
||||||
value: "",
|
value: "",
|
||||||
|
|||||||
@@ -3,6 +3,15 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="text-subtitle2">Global Key Store</div>
|
<div class="text-subtitle2">Global Key Store</div>
|
||||||
<q-space />
|
<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
|
<q-btn
|
||||||
size="sm"
|
size="sm"
|
||||||
color="grey-5"
|
color="grey-5"
|
||||||
@@ -61,7 +70,7 @@
|
|||||||
</q-td>
|
</q-td>
|
||||||
<!-- value -->
|
<!-- value -->
|
||||||
<q-td>
|
<q-td>
|
||||||
{{ props.row.value }}
|
{{ isPwd ? "****" : props.row.value }}
|
||||||
</q-td>
|
</q-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
@@ -79,6 +88,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
keystore: [],
|
keystore: [],
|
||||||
|
isPwd: true,
|
||||||
pagination: {
|
pagination: {
|
||||||
rowsPerPage: 0,
|
rowsPerPage: 0,
|
||||||
sortBy: "name",
|
sortBy: "name",
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
@show="loadEditor"
|
@show="loadEditor"
|
||||||
@before-hide="cleanupEditors"
|
@before-hide="cleanupEditors"
|
||||||
>
|
>
|
||||||
<q-card class="q-dialog-plugin" style="width: 60vw">
|
<q-card
|
||||||
|
class="q-dialog-plugin"
|
||||||
|
:style="`width: ${props.type === 'web' ? 50 : 60}vw; max-width: ${props.type === 'web' ? 60 : 70}vw`"
|
||||||
|
>
|
||||||
<q-bar>
|
<q-bar>
|
||||||
{{
|
{{
|
||||||
props.action
|
props.action
|
||||||
@@ -71,7 +74,6 @@
|
|||||||
|
|
||||||
<q-card-section v-show="type === 'rest'">
|
<q-card-section v-show="type === 'rest'">
|
||||||
<q-toolbar>
|
<q-toolbar>
|
||||||
<q-space />
|
|
||||||
<q-tabs v-model="tab" dense shrink>
|
<q-tabs v-model="tab" dense shrink>
|
||||||
<q-tab
|
<q-tab
|
||||||
name="body"
|
name="body"
|
||||||
|
|||||||
@@ -313,18 +313,19 @@ export default {
|
|||||||
},
|
},
|
||||||
getURLActions() {
|
getURLActions() {
|
||||||
this.$axios.get("/core/urlaction/").then((r) => {
|
this.$axios.get("/core/urlaction/").then((r) => {
|
||||||
if (r.data.length === 0) {
|
|
||||||
this.notifyWarning(
|
|
||||||
"No URL Actions configured. Go to Settings > Global Settings > URL Actions",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.urlActions = r.data
|
this.urlActions = r.data
|
||||||
.filter((action) => action.action_type === "web")
|
.filter((action) => action.action_type === "web")
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map((action) => ({
|
.map((action) => ({
|
||||||
label: action.name,
|
label: action.name,
|
||||||
value: action.id,
|
value: action.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (this.urlActions.length === 0) {
|
||||||
|
this.notifyWarning(
|
||||||
|
"No URL Actions configured. Go to Settings > Global Settings > URL Actions",
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getUserPrefs() {
|
getUserPrefs() {
|
||||||
|
|||||||
@@ -539,6 +539,7 @@
|
|||||||
>
|
>
|
||||||
{{ props.row.name }}
|
{{ props.row.name }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
|
<q-tooltip :delay="600">ID: {{ props.row.id }}</q-tooltip>
|
||||||
</q-td>
|
</q-td>
|
||||||
<!-- args -->
|
<!-- args -->
|
||||||
<q-td key="args" :props="props">
|
<q-td key="args" :props="props">
|
||||||
|
|||||||
26
src/components/scripts/ScriptOutputCopyClip.vue
Normal file
26
src/components/scripts/ScriptOutputCopyClip.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<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>
|
||||||
@@ -42,15 +42,7 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-file
|
<q-file label="Script Upload" v-model="file" filled dense counter>
|
||||||
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>
|
<template v-slot:prepend>
|
||||||
<q-icon name="attach_file" />
|
<q-icon name="attach_file" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -18,12 +18,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div v-if="ret.stdout">
|
<div v-if="ret.stdout">
|
||||||
Standard Output
|
<script-output-copy-clip label="Standard Output" :data="ret.stdout" />
|
||||||
<q-separator />
|
<q-separator />
|
||||||
<pre>{{ ret.stdout }}</pre>
|
<pre>{{ ret.stdout }}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="ret.stderr">
|
<div v-if="ret.stderr">
|
||||||
Standard Error
|
<script-output-copy-clip label="Standard Error" :data="ret.stderr" />
|
||||||
<q-separator />
|
<q-separator />
|
||||||
<pre>{{ ret.stderr }}</pre>
|
<pre>{{ ret.stderr }}</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,9 +38,13 @@
|
|||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { testScript, testScriptOnServer } from "@/api/scripts";
|
import { testScript, testScriptOnServer } from "@/api/scripts";
|
||||||
import { useDialogPluginComponent } from "quasar";
|
import { useDialogPluginComponent } from "quasar";
|
||||||
|
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "TestScriptModal",
|
name: "TestScriptModal",
|
||||||
|
components: {
|
||||||
|
ScriptOutputCopyClip,
|
||||||
|
},
|
||||||
emits: [...useDialogPluginComponent.emits],
|
emits: [...useDialogPluginComponent.emits],
|
||||||
props: {
|
props: {
|
||||||
script: !Object,
|
script: !Object,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { fetchUsers } from "@/api/accounts";
|
import { fetchUsers, fetchRoles } from "@/api/accounts";
|
||||||
import { formatUserOptions } from "@/utils/format";
|
import { formatUserOptions } from "@/utils/format";
|
||||||
|
|
||||||
export function useUserDropdown(onMount = false) {
|
export function useUserDropdown(onMount = false) {
|
||||||
@@ -44,3 +44,26 @@ export function useUserDropdown(onMount = false) {
|
|||||||
getDynamicUserOptions,
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -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").
|
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 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 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.
|
||||||
|
|
||||||
## License Grant
|
## License Grant
|
||||||
|
|
||||||
|
|||||||
144
src/ee/sso/api/sso.ts
Normal file
144
src/ee/sso/api/sso.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
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() || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
142
src/ee/sso/components/SSOAccountsTable.vue
Normal file
142
src/ee/sso/components/SSOAccountsTable.vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
160
src/ee/sso/components/SSOProvidersForm.vue
Normal file
160
src/ee/sso/components/SSOProvidersForm.vue
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
293
src/ee/sso/components/SSOProvidersTable.vue
Normal file
293
src/ee/sso/components/SSOProvidersTable.vue
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
112
src/ee/sso/components/SSOSettings.vue
Normal file
112
src/ee/sso/components/SSOSettings.vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
33
src/ee/sso/types/sso.ts
Normal file
33
src/ee/sso/types/sso.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
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;
|
||||||
|
}
|
||||||
21
src/ee/sso/utils/cookies.ts
Normal file
21
src/ee/sso/utils/cookies.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
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;
|
||||||
|
}
|
||||||
32
src/ee/sso/views/ProviderCallback.vue
Normal file
32
src/ee/sso/views/ProviderCallback.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
<q-item-label header>Servers</q-item-label>
|
<q-item-label header>Servers</q-item-label>
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="fa fa-server" size="sm" color="primary" />
|
<q-icon name="dns" size="sm" color="primary" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section no-wrap>
|
<q-item-section no-wrap>
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
|
|
||||||
<AlertsIcon />
|
<AlertsIcon />
|
||||||
|
|
||||||
<q-btn-dropdown flat no-caps stretch :label="username || ''">
|
<q-btn-dropdown flat no-caps stretch :label="displayName || ''">
|
||||||
<q-list>
|
<q-list>
|
||||||
<q-item
|
<q-item
|
||||||
clickable
|
clickable
|
||||||
@@ -211,7 +211,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// composition imports
|
// composition imports
|
||||||
import { computed, onMounted } from "vue";
|
import { computed, onMounted, onBeforeUnmount, ref } from "vue";
|
||||||
import { useQuasar } from "quasar";
|
import { useQuasar } from "quasar";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { useDashboardStore } from "@/stores/dashboard";
|
import { useDashboardStore } from "@/stores/dashboard";
|
||||||
@@ -240,7 +240,7 @@ const {
|
|||||||
daysUntilCertExpires,
|
daysUntilCertExpires,
|
||||||
} = storeToRefs(useDashboardStore());
|
} = storeToRefs(useDashboardStore());
|
||||||
|
|
||||||
const { username } = storeToRefs(useAuthStore());
|
const { displayName } = storeToRefs(useAuthStore());
|
||||||
|
|
||||||
const darkMode = computed({
|
const darkMode = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
@@ -315,8 +315,25 @@ const updateAvailable = computed(() => {
|
|||||||
return currentTRMMVersion.value !== latestTRMMVersion.value;
|
return currentTRMMVersion.value !== latestTRMMVersion.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const poll = ref(null);
|
||||||
|
|
||||||
|
function livePoll() {
|
||||||
|
poll.value = setInterval(
|
||||||
|
() => {
|
||||||
|
store.dispatch("checkVer");
|
||||||
|
store.dispatch("getDashInfo", false);
|
||||||
|
},
|
||||||
|
60 * 4 * 1000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
store.dispatch("getDashInfo");
|
store.dispatch("getDashInfo");
|
||||||
store.dispatch("checkVer");
|
store.dispatch("checkVer");
|
||||||
|
livePoll();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(poll.value);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export default function () {
|
|||||||
},
|
},
|
||||||
server_scripts_enabled: true,
|
server_scripts_enabled: true,
|
||||||
web_terminal_enabled: true,
|
web_terminal_enabled: true,
|
||||||
|
sso_enabled: false,
|
||||||
|
block_local_user_logon: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
@@ -159,6 +161,12 @@ export default function () {
|
|||||||
setWebTerminalEnabled(state, obj) {
|
setWebTerminalEnabled(state, obj) {
|
||||||
state.web_terminal_enabled = obj;
|
state.web_terminal_enabled = obj;
|
||||||
},
|
},
|
||||||
|
setSSOEnabled(state, obj) {
|
||||||
|
state.sso_enabled = obj;
|
||||||
|
},
|
||||||
|
setBlockLocalUserLogon(state, obj) {
|
||||||
|
state.block_local_user_logon = obj;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setClientTreeSplitter(context, val) {
|
setClientTreeSplitter(context, val) {
|
||||||
@@ -245,6 +253,7 @@ export default function () {
|
|||||||
commit("setRunCmdPlaceholders", data.run_cmd_placeholder_text);
|
commit("setRunCmdPlaceholders", data.run_cmd_placeholder_text);
|
||||||
commit("setServerScriptsEnabled", data.server_scripts_enabled);
|
commit("setServerScriptsEnabled", data.server_scripts_enabled);
|
||||||
commit("setWebTerminalEnabled", data.web_terminal_enabled);
|
commit("setWebTerminalEnabled", data.web_terminal_enabled);
|
||||||
|
commit("setBlockLocalUserLogon", data.block_local_user_logon);
|
||||||
|
|
||||||
if (data?.date_format !== "") commit("setDateFormat", data.date_format);
|
if (data?.date_format !== "") commit("setDateFormat", data.date_format);
|
||||||
else commit("setDateFormat", data.default_date_format);
|
else commit("setDateFormat", data.default_date_format);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { useStorage } from "@vueuse/core";
|
import { useStorage } from "@vueuse/core";
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
interface CheckCredentialsRequest {
|
interface CheckCredentialsRequest {
|
||||||
@@ -27,12 +28,18 @@ interface TOTPSetupResponse {
|
|||||||
export const useAuthStore = defineStore("auth", {
|
export const useAuthStore = defineStore("auth", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
username: useStorage("user_name", null),
|
username: useStorage("user_name", null),
|
||||||
|
name: useStorage("name", null),
|
||||||
token: useStorage("access_token", null),
|
token: useStorage("access_token", null),
|
||||||
|
ssoLoginProvider: useStorage("sso_provider", null),
|
||||||
|
provider_id: useStorage("provider_id", null),
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
loggedIn: (state) => {
|
loggedIn: (state) => {
|
||||||
return state.token !== null;
|
return state.token !== null;
|
||||||
},
|
},
|
||||||
|
displayName: (state) => {
|
||||||
|
return state.name ? state.name : state.username;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
async checkCredentials(
|
async checkCredentials(
|
||||||
@@ -43,13 +50,16 @@ export const useAuthStore = defineStore("auth", {
|
|||||||
if (!data.totp) {
|
if (!data.totp) {
|
||||||
this.token = data.token;
|
this.token = data.token;
|
||||||
this.username = data.username;
|
this.username = data.username;
|
||||||
|
this.name = data.name;
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
async login(credentials: LoginRequest) {
|
async login(credentials: LoginRequest) {
|
||||||
const { data } = await axios.post("/v2/login/", credentials);
|
const { data } = await axios.post("/v2/login/", credentials);
|
||||||
this.username = data.username;
|
this.username = data.username;
|
||||||
|
this.name = data.name;
|
||||||
this.token = data.token;
|
this.token = data.token;
|
||||||
|
this.ssoLoginProvider = null;
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
@@ -61,6 +71,9 @@ export const useAuthStore = defineStore("auth", {
|
|||||||
}
|
}
|
||||||
this.token = null;
|
this.token = null;
|
||||||
this.username = null;
|
this.username = null;
|
||||||
|
this.name = null;
|
||||||
|
this.ssoLoginProvider = null;
|
||||||
|
this.provider_id = null;
|
||||||
},
|
},
|
||||||
async setupTotp(): Promise<TOTPSetupResponse | false> {
|
async setupTotp(): Promise<TOTPSetupResponse | false> {
|
||||||
const { data } = await axios.post("/accounts/users/setup_totp/");
|
const { data } = await axios.post("/accounts/users/setup_totp/");
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthToken {
|
||||||
|
digest: string;
|
||||||
|
created: string;
|
||||||
|
expiry: string;
|
||||||
|
user: string;
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/types/core/settings.ts
Normal file
3
src/types/core/settings.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface CoreSetting {
|
||||||
|
block_local_user_logon: boolean;
|
||||||
|
}
|
||||||
8
src/utils/helpers.ts
Normal file
8
src/utils/helpers.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { copyToClipboard } from "quasar";
|
||||||
|
import { notifySuccess } from "@/utils/notify";
|
||||||
|
|
||||||
|
export function copyOutput(val: string) {
|
||||||
|
copyToClipboard(val).then(() => {
|
||||||
|
notifySuccess("Copied to clipboard");
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -509,6 +509,13 @@ export default {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
align: "left",
|
align: "left",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "mon-type",
|
||||||
|
label: "",
|
||||||
|
field: "monitoring_type",
|
||||||
|
sortable: true,
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "checks-status",
|
name: "checks-status",
|
||||||
align: "left",
|
align: "left",
|
||||||
@@ -600,6 +607,7 @@ export default {
|
|||||||
visibleColumns: [
|
visibleColumns: [
|
||||||
"smsalert",
|
"smsalert",
|
||||||
"plat",
|
"plat",
|
||||||
|
"mon-type",
|
||||||
"emailalert",
|
"emailalert",
|
||||||
"dashboardalert",
|
"dashboardalert",
|
||||||
"checks-status",
|
"checks-status",
|
||||||
@@ -818,15 +826,14 @@ export default {
|
|||||||
},
|
},
|
||||||
getURLActions() {
|
getURLActions() {
|
||||||
this.$axios.get("/core/urlaction/").then((r) => {
|
this.$axios.get("/core/urlaction/").then((r) => {
|
||||||
if (r.data.length === 0) {
|
this.urlActions = r.data
|
||||||
|
.filter((action) => action.action_type === "web")
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
if (this.urlActions.length === 0) {
|
||||||
this.notifyWarning(
|
this.notifyWarning(
|
||||||
"No URL Actions configured. Go to Settings > Global Settings > URL Actions",
|
"No URL Actions configured. Go to Settings > Global Settings > URL Actions",
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
this.urlActions = r.data.filter(
|
|
||||||
(action) => action.action_type === "web",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
runURLAction(id, action, model) {
|
runURLAction(id, action, model) {
|
||||||
|
|||||||
@@ -49,7 +49,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card-section>
|
</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>
|
</q-card>
|
||||||
|
|
||||||
<!-- 2 factor modal -->
|
<!-- 2 factor modal -->
|
||||||
<q-dialog persistent v-model="prompt">
|
<q-dialog persistent v-model="prompt">
|
||||||
<q-card style="min-width: 400px">
|
<q-card style="min-width: 400px">
|
||||||
@@ -84,10 +111,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive } from "vue";
|
import { ref, reactive, onMounted } from "vue";
|
||||||
import { type QForm, useQuasar } from "quasar";
|
import { type QForm, useQuasar } from "quasar";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
import {
|
||||||
|
openSSOProviderRedirect,
|
||||||
|
getSSOConfig,
|
||||||
|
type SSOProviderConfig,
|
||||||
|
} from "@/ee/sso/api/sso";
|
||||||
|
|
||||||
// setup quasar
|
// setup quasar
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
@@ -107,6 +139,7 @@ const credentials = reactive({ username: "", password: "" });
|
|||||||
const twofactor = ref("");
|
const twofactor = ref("");
|
||||||
const prompt = ref(false);
|
const prompt = ref(false);
|
||||||
const showPassword = ref(true);
|
const showPassword = ref(true);
|
||||||
|
const ssoProviders = ref([] as SSOProviderConfig[]);
|
||||||
|
|
||||||
async function checkCreds() {
|
async function checkCreds() {
|
||||||
try {
|
try {
|
||||||
@@ -135,6 +168,15 @@ async function onSubmit() {
|
|||||||
prompt.value = false;
|
prompt.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const result = await getSSOConfig();
|
||||||
|
ssoProviders.value = result.data.socialaccount.providers;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
<q-tab-panels v-model="tab">
|
<q-tab-panels v-model="tab">
|
||||||
<q-tab-panel name="terminal" class="q-pa-none">
|
<q-tab-panel name="terminal" class="q-pa-none">
|
||||||
<iframe
|
<iframe
|
||||||
|
allow="clipboard-read; clipboard-write"
|
||||||
:src="terminal"
|
:src="terminal"
|
||||||
:style="{
|
:style="{
|
||||||
height: `${$q.screen.height - 30}px`,
|
height: `${$q.screen.height - 30}px`,
|
||||||
@@ -66,6 +67,7 @@
|
|||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
<q-tab-panel name="filebrowser" class="q-pa-none">
|
<q-tab-panel name="filebrowser" class="q-pa-none">
|
||||||
<iframe
|
<iframe
|
||||||
|
allow="clipboard-read; clipboard-write"
|
||||||
:src="file"
|
:src="file"
|
||||||
:style="{
|
:style="{
|
||||||
height: `${$q.screen.height - 30}px`,
|
height: `${$q.screen.height - 30}px`,
|
||||||
|
|||||||
@@ -23,12 +23,15 @@
|
|||||||
/>
|
/>
|
||||||
<q-space />
|
<q-space />
|
||||||
</q-bar>
|
</q-bar>
|
||||||
|
<div class="q-video" :style="{ height: `${$q.screen.height - 26}px` }">
|
||||||
<q-video
|
<iframe
|
||||||
v-show="control"
|
v-show="control"
|
||||||
:src="control"
|
:src="control"
|
||||||
:style="{ height: `${$q.screen.height - 26}px` }"
|
allow="clipboard-read; clipboard-write"
|
||||||
></q-video>
|
allowfullscreen
|
||||||
|
frameborder="0"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user