Compare commits

..

35 Commits

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

68
package-lock.json generated
View File

@@ -1,26 +1,26 @@
{
"name": "web",
"version": "0.101.52",
"version": "0.101.49",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "web",
"version": "0.101.52",
"version": "0.101.48",
"dependencies": {
"@quasar/extras": "1.16.13",
"@vueuse/core": "11.2.0",
"@vueuse/integrations": "11.2.0",
"@vueuse/shared": "11.2.0",
"@quasar/extras": "1.16.12",
"@vueuse/core": "11.1.0",
"@vueuse/integrations": "11.1.0",
"@vueuse/shared": "11.1.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0",
"apexcharts": "3.54.1",
"axios": "1.7.7",
"dotenv": "16.4.5",
"monaco-editor": "0.50.0",
"pinia": "2.2.6",
"pinia": "2.2.4",
"qrcode": "1.5.4",
"quasar": "2.17.2",
"quasar": "2.17.1",
"vue": "3.5.12",
"vue-router": "4.4.5",
"vue3-apexcharts": "1.7.0",
@@ -972,9 +972,9 @@
}
},
"node_modules/@quasar/extras": {
"version": "1.16.13",
"resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.16.13.tgz",
"integrity": "sha512-6QdnYbFYhgeWFAwytUWTDgpP/mcJxydBmgO91cHr9MMTx0GLaVJY6d10m/G/XS9TzMnSsZgqO7kbVHf3Hvml3w==",
"version": "1.16.12",
"resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.16.12.tgz",
"integrity": "sha512-hLlb3Buxo38Xg/2w0BTkz98RBh/VH8apZ2r6Fl8YpPgrVQ0diHyN/BVTvIOk5Kch2y38L2kvwOIddsB2UcCuIg==",
"license": "MIT",
"funding": {
"type": "github",
@@ -1606,14 +1606,14 @@
"license": "MIT"
},
"node_modules/@vueuse/core": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.2.0.tgz",
"integrity": "sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.1.0.tgz",
"integrity": "sha512-P6dk79QYA6sKQnghrUz/1tHi0n9mrb/iO1WTMk/ElLmTyNqgDeSZ3wcDf6fRBGzRJbeG1dxzEOvLENMjr+E3fg==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
"@vueuse/metadata": "11.2.0",
"@vueuse/shared": "11.2.0",
"@vueuse/metadata": "11.1.0",
"@vueuse/shared": "11.1.0",
"vue-demi": ">=0.14.10"
},
"funding": {
@@ -1647,13 +1647,13 @@
}
},
"node_modules/@vueuse/integrations": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-11.2.0.tgz",
"integrity": "sha512-zGXz3dsxNHKwiD9jPMvR3DAxQEOV6VWIEYTGVSB9PNpk4pTWR+pXrHz9gvXWcP2sTk3W2oqqS6KwWDdntUvNVA==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-11.1.0.tgz",
"integrity": "sha512-O2ZgrAGPy0qAjpoI2YR3egNgyEqwG85fxfwmA9BshRIGjV4G6yu6CfOPpMHAOoCD+UfsIl7Vb1bXJ6ifrHYDDA==",
"license": "MIT",
"dependencies": {
"@vueuse/core": "11.2.0",
"@vueuse/shared": "11.2.0",
"@vueuse/core": "11.1.0",
"@vueuse/shared": "11.1.0",
"vue-demi": ">=0.14.10"
},
"funding": {
@@ -1739,18 +1739,18 @@
}
},
"node_modules/@vueuse/metadata": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.2.0.tgz",
"integrity": "sha512-L0ZmtRmNx+ZW95DmrgD6vn484gSpVeRbgpWevFKXwqqQxW9hnSi2Ppuh2BzMjnbv4aJRiIw8tQatXT9uOB23dQ==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.1.0.tgz",
"integrity": "sha512-l9Q502TBTaPYGanl1G+hPgd3QX5s4CGnpXriVBR5fEZ/goI6fvDaVmIl3Td8oKFurOxTmbXvBPSsgrd6eu6HYg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.2.0.tgz",
"integrity": "sha512-VxFjie0EanOudYSgMErxXfq6fo8vhr5ICI+BuE3I9FnX7ePllEsVrRQ7O6Q1TLgApeLuPKcHQxAXpP+KnlrJsg==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.1.0.tgz",
"integrity": "sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==",
"license": "MIT",
"dependencies": {
"vue-demi": ">=0.14.10"
@@ -6520,9 +6520,9 @@
}
},
"node_modules/pinia": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.6.tgz",
"integrity": "sha512-vIsR8JkDN5Ga2vAxqOE2cJj4VtsHnzpR1Fz30kClxlh0yCHfec6uoMeM3e/ddqmwFUejK3NlrcQa/shnpyT4hA==",
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.4.tgz",
"integrity": "sha512-K7ZhpMY9iJ9ShTC0cR2+PnxdQRuwVIsXDO/WIEV/RnMC/vmSoKDTKW/exNQYPI+4ij10UjXqdNiEHwn47McANQ==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.3",
@@ -6534,7 +6534,7 @@
"peerDependencies": {
"@vue/composition-api": "^1.4.0",
"typescript": ">=4.4.4",
"vue": "^2.6.14 || ^3.5.11"
"vue": "^2.6.14 || ^3.3.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
@@ -6768,9 +6768,9 @@
}
},
"node_modules/quasar": {
"version": "2.17.2",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-2.17.2.tgz",
"integrity": "sha512-wycJcrjXsNxyNFYaw7eviKAo0I3LaQap0GCHXUEiaAi4H+a9LJbgkoZSZKCP4M0UO4iVnkWVkQzsFodyHk5uHQ==",
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-2.17.1.tgz",
"integrity": "sha512-4tnprxb+oEFChqcHGeQnprTmsr9gRK/R4N2O6Gg0AxpP22G9ttKeeqhH7+Soyjdi9RUcc6d5Y49T6rRg4hGJXw==",
"license": "MIT",
"engines": {
"node": ">= 10.18.1",

View File

@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.101.52",
"version": "0.101.49",
"private": true,
"productName": "Tactical RMM",
"scripts": {
@@ -10,17 +10,17 @@
"format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
},
"dependencies": {
"@quasar/extras": "1.16.13",
"@vueuse/core": "11.2.0",
"@vueuse/integrations": "11.2.0",
"@vueuse/shared": "11.2.0",
"@quasar/extras": "1.16.12",
"@vueuse/core": "11.1.0",
"@vueuse/integrations": "11.1.0",
"@vueuse/shared": "11.1.0",
"apexcharts": "3.54.1",
"axios": "1.7.7",
"dotenv": "16.4.5",
"monaco-editor": "0.50.0",
"pinia": "2.2.6",
"pinia": "2.2.4",
"qrcode": "1.5.4",
"quasar": "2.17.2",
"quasar": "2.17.1",
"vue": "3.5.12",
"vue-router": "4.4.5",
"vue3-apexcharts": "1.7.0",

View File

@@ -8,7 +8,6 @@
// Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
const { mergeConfig } = require("vite");
const { configure } = require("quasar/wrappers");
const path = require("path");
require("dotenv").config();
@@ -79,22 +78,9 @@ module.exports = configure(function (/* ctx */) {
// polyfillModulePreload: true,
distDir: "dist/",
/* eslint-disable quotes */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
extendViteConf(viteConf, { isServer, isClient }) {
viteConf.build = mergeConfig(viteConf.build, {
chunkSizeWarningLimit: 1600,
rollupOptions: {
output: {
entryFileNames: `[hash].js`,
chunkFileNames: `[hash].js`,
assetFileNames: `[hash].[ext]`,
},
},
});
},
/* eslint-enable quotes */
// extendViteConf (viteConf) {},
// viteVuePluginOptions: {},
// vitePlugins: []
},

View File

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

View File

@@ -8,15 +8,8 @@ import type {
TestRunURLActionResponse,
} from "@/types/core/urlactions";
import type { CoreSetting } from "@/types/core/settings";
const baseUrl = "/core";
export async function fetchCoreSettings(params = {}): Promise<CoreSetting> {
const { data } = await axios.get("/core/settings/", { params: params });
return data;
}
export async function fetchDashboardInfo(params = {}) {
const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params });
return data;

View File

@@ -21,7 +21,6 @@ export function setErrorMessage(data, message) {
export default function ({ app, router }) {
app.config.globalProperties.$axios = axios;
axios.defaults.withCredentials = true;
axios.interceptors.request.use(
function (config) {
@@ -66,20 +65,12 @@ export default function ({ app, router }) {
// perms
else if (error.response.status === 403) {
// don't notify user if method is GET
if (
error.config.method === "get" ||
error.config.method === "patch" ||
error.config.url === "accounts/ssoproviders/token/"
)
if (error.config.method === "get" || error.config.method === "patch")
return Promise.reject({ ...error });
text = error.response.data.detail;
}
// catch all for other 400 error messages
else if (
error.response.status >= 400 &&
error.response.status < 500 &&
error.response.status !== 423
) {
else if (error.response.status >= 400 && error.response.status < 500) {
if (error.config.responseType === "blob") {
text = (await error.response.data.text()).replace(/^"|"$/g, "");
} else if (error.response.data.non_field_errors) {
@@ -94,7 +85,7 @@ export default function ({ app, router }) {
}
}
if ((text || error.response) && error.response.status !== 423) {
if (text || error.response) {
Notify.create({
color: "negative",
message: text ? text : "",

View File

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

View File

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

View File

@@ -140,9 +140,9 @@
/>
<q-checkbox v-model="state.save_all_output" label="Save all output" />
</q-card-section>
<q-card-section>
<q-card-section v-if="agent.plat === 'windows'">
<q-checkbox
v-if="agent.plat === 'windows' && !state.run_on_server"
v-if="!state.run_on_server"
v-model="state.run_as_user"
label="Run As User"
>

View File

@@ -249,20 +249,6 @@ export default {
sortable: true,
format: (a) => this.formatDate(a),
},
{
name: "client",
label: "Client",
field: "client",
align: "left",
sortable: true,
},
{
name: "site",
label: "Site",
field: "site",
align: "left",
sortable: true,
},
{
name: "hostname",
label: "Agent",

View File

@@ -13,7 +13,6 @@
<q-tab name="webhooks" label="Web Hooks" />
<q-tab name="retention" label="Retention" />
<q-tab name="apikeys" label="API Keys" />
<q-tab name="sso" label="Single Sign-On (SSO)" />
<!-- <q-tab name="openai" label="Open AI" /> -->
</q-tabs>
</template>
@@ -637,11 +636,6 @@
<APIKeysTable />
</q-tab-panel>
<!-- sso integration -->
<q-tab-panel name="sso">
<SSOProvidersTable />
</q-tab-panel>
<!-- Open AI -->
<!-- <q-tab-panel name="openai">
<div class="text-subtitle2">Open AI</div>
@@ -691,8 +685,7 @@
v-show="
tab !== 'customfields' &&
tab !== 'keystore' &&
tab !== 'urlactions' &&
tab !== 'sso'
tab !== 'urlactions'
"
label="Save"
color="primary"
@@ -729,7 +722,6 @@ import CustomFields from "@/components/modals/coresettings/CustomFields.vue";
import KeyStoreTable from "@/components/modals/coresettings/KeyStoreTable.vue";
import URLActionsTable from "@/components/modals/coresettings/URLActionsTable.vue";
import APIKeysTable from "@/components/core/APIKeysTable.vue";
import SSOProvidersTable from "@/ee/sso/components/SSOProvidersTable.vue";
export default {
name: "EditCoreSettings",
@@ -739,7 +731,7 @@ export default {
KeyStoreTable,
URLActionsTable,
APIKeysTable,
SSOProvidersTable,
// ServerTasksTable,
},
mixins: [mixins],
data() {
@@ -775,13 +767,6 @@ export default {
return this.$store.state.hosted;
},
},
watch: {
tab(newTab, oldTab) {
if (oldTab === "sso") {
this.getCoreSettings();
}
},
},
methods: {
openURL(url) {
openURL(url);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -157,7 +157,7 @@
<AlertsIcon />
<q-btn-dropdown flat no-caps stretch :label="displayName || ''">
<q-btn-dropdown flat no-caps stretch :label="username || ''">
<q-list>
<q-item
clickable
@@ -211,7 +211,7 @@
</template>
<script setup lang="ts">
// composition imports
import { computed, onMounted, onBeforeUnmount, ref } from "vue";
import { computed, onMounted } from "vue";
import { useQuasar } from "quasar";
import { useStore } from "vuex";
import { useDashboardStore } from "@/stores/dashboard";
@@ -240,7 +240,7 @@ const {
daysUntilCertExpires,
} = storeToRefs(useDashboardStore());
const { displayName } = storeToRefs(useAuthStore());
const { username } = storeToRefs(useAuthStore());
const darkMode = computed({
get: () => {
@@ -315,25 +315,8 @@ const updateAvailable = computed(() => {
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(() => {
store.dispatch("getDashInfo");
store.dispatch("checkVer");
livePoll();
});
onBeforeUnmount(() => {
clearInterval(poll.value);
});
</script>

View File

@@ -43,8 +43,6 @@ export default function () {
},
server_scripts_enabled: true,
web_terminal_enabled: true,
sso_enabled: false,
block_local_user_logon: false,
};
},
getters: {
@@ -161,12 +159,6 @@ export default function () {
setWebTerminalEnabled(state, obj) {
state.web_terminal_enabled = obj;
},
setSSOEnabled(state, obj) {
state.sso_enabled = obj;
},
setBlockLocalUserLogon(state, obj) {
state.block_local_user_logon = obj;
},
},
actions: {
setClientTreeSplitter(context, val) {
@@ -253,7 +245,6 @@ export default function () {
commit("setRunCmdPlaceholders", data.run_cmd_placeholder_text);
commit("setServerScriptsEnabled", data.server_scripts_enabled);
commit("setWebTerminalEnabled", data.web_terminal_enabled);
commit("setBlockLocalUserLogon", data.block_local_user_logon);
if (data?.date_format !== "") commit("setDateFormat", data.date_format);
else commit("setDateFormat", data.default_date_format);

View File

@@ -1,6 +1,5 @@
import { defineStore } from "pinia";
import { useStorage } from "@vueuse/core";
import axios from "axios";
interface CheckCredentialsRequest {
@@ -28,18 +27,12 @@ interface TOTPSetupResponse {
export const useAuthStore = defineStore("auth", {
state: () => ({
username: useStorage("user_name", null),
name: useStorage("name", null),
token: useStorage("access_token", null),
ssoLoginProvider: useStorage("sso_provider", null),
provider_id: useStorage("provider_id", null),
}),
getters: {
loggedIn: (state) => {
return state.token !== null;
},
displayName: (state) => {
return state.name ? state.name : state.username;
},
},
actions: {
async checkCredentials(
@@ -50,16 +43,13 @@ export const useAuthStore = defineStore("auth", {
if (!data.totp) {
this.token = data.token;
this.username = data.username;
this.name = data.name;
}
return data;
},
async login(credentials: LoginRequest) {
const { data } = await axios.post("/v2/login/", credentials);
this.username = data.username;
this.name = data.name;
this.token = data.token;
this.ssoLoginProvider = null;
return data;
},
@@ -71,9 +61,6 @@ export const useAuthStore = defineStore("auth", {
}
this.token = null;
this.username = null;
this.name = null;
this.ssoLoginProvider = null;
this.provider_id = null;
},
async setupTotp(): Promise<TOTPSetupResponse | false> {
const { data } = await axios.post("/accounts/users/setup_totp/");

View File

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

View File

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

View File

@@ -49,34 +49,7 @@
</div>
</q-form>
</q-card-section>
<q-card-section v-if="ssoProviders?.length > 0">
<div class="text-h6 text-center q-mb-md">Log in with SSO</div>
<q-separator />
<q-list dense bordered class="q-pa-sm">
<q-item
v-for="provider in ssoProviders"
:key="provider.id"
@click="openSSOProviderRedirect(provider.id)"
clickable
class="q-pa-xs hover-bg"
>
<q-item-section avatar>
<q-icon
:name="provider.icon ?? 'mdi-key'"
size="sm"
class="text-primary"
/>
</q-item-section>
<q-item-section>
<q-item-label>{{ provider.name }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-card-section>
</q-card>
<!-- 2 factor modal -->
<q-dialog persistent v-model="prompt">
<q-card style="min-width: 400px">
@@ -111,15 +84,10 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import { ref, reactive } from "vue";
import { type QForm, useQuasar } from "quasar";
import { useAuthStore } from "@/stores/auth";
import { useRouter } from "vue-router";
import {
openSSOProviderRedirect,
getSSOConfig,
type SSOProviderConfig,
} from "@/ee/sso/api/sso";
// setup quasar
const $q = useQuasar();
@@ -139,7 +107,6 @@ const credentials = reactive({ username: "", password: "" });
const twofactor = ref("");
const prompt = ref(false);
const showPassword = ref(true);
const ssoProviders = ref([] as SSOProviderConfig[]);
async function checkCreds() {
try {
@@ -168,15 +135,6 @@ async function onSubmit() {
prompt.value = false;
}
}
onMounted(async () => {
try {
const result = await getSSOConfig();
ssoProviders.value = result.data.socialaccount.providers;
} catch (e) {
console.error(e);
}
});
</script>
<style>

View File

@@ -34,7 +34,6 @@
<q-tab-panels v-model="tab">
<q-tab-panel name="terminal" class="q-pa-none">
<iframe
allow="clipboard-read; clipboard-write"
:src="terminal"
:style="{
height: `${$q.screen.height - 30}px`,
@@ -67,7 +66,6 @@
</q-tab-panel>
<q-tab-panel name="filebrowser" class="q-pa-none">
<iframe
allow="clipboard-read; clipboard-write"
:src="file"
:style="{
height: `${$q.screen.height - 30}px`,

View File

@@ -23,15 +23,12 @@
/>
<q-space />
</q-bar>
<div class="q-video" :style="{ height: `${$q.screen.height - 26}px` }">
<iframe
v-show="control"
:src="control"
allow="clipboard-read; clipboard-write"
allowfullscreen
frameborder="0"
></iframe>
</div>
<q-video
v-show="control"
:src="control"
:style="{ height: `${$q.screen.height - 26}px` }"
></q-video>
</div>
</template>