From 11042a092c7bd5677706f686b396f5989861bc7b Mon Sep 17 00:00:00 2001 From: sadnub Date: Wed, 29 Nov 2023 09:55:38 -0500 Subject: [PATCH] wip --- package-lock.json | 65 +++++ package.json | 3 + quasar.config.js | 2 +- src/api/core.js | 27 ++ src/api/core.ts | 32 +++ src/boot/axios.js | 16 +- src/boot/pinia.ts | 11 + src/components/core/TRMMCommandPrompt.vue | 93 +++++++ .../modals/coresettings/URLActionsForm.vue | 241 ++++++++++------ .../modals/coresettings/URLActionsTable.vue | 204 +++++++------- src/layouts/MainLayout.vue | 263 +++++++----------- src/router/index.js | 12 +- src/store/index.js | 54 +--- src/stores/auth.ts | 61 ++++ src/stores/dashboard.ts | 43 +++ src/stores/user.ts | 26 ++ src/types/core/urlactions.ts | 17 ++ src/views/LoginView.vue | 100 ++++--- src/views/SessionExpired.vue | 22 +- src/websocket/channels.js | 11 - src/websocket/websocket.ts | 52 ++++ 21 files changed, 881 insertions(+), 474 deletions(-) create mode 100644 src/api/core.ts create mode 100644 src/boot/pinia.ts create mode 100644 src/components/core/TRMMCommandPrompt.vue create mode 100644 src/stores/auth.ts create mode 100644 src/stores/dashboard.ts create mode 100644 src/stores/user.ts create mode 100644 src/types/core/urlactions.ts delete mode 100644 src/websocket/channels.js create mode 100644 src/websocket/websocket.ts diff --git a/package-lock.json b/package-lock.json index c84abd51..0543a747 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,8 @@ "vue3-apexcharts": "1.4.4", "vuedraggable": "4.1.0", "vuex": "4.1.0", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0", "yaml": "2.3.4" }, "devDependencies": { @@ -5428,6 +5430,56 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pinia": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz", + "integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==", + "dependencies": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": ">=0.14.5" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/pkg-types": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", @@ -7273,6 +7325,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==" + }, + "node_modules/xterm-addon-fit": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz", + "integrity": "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==", + "peerDependencies": { + "xterm": "^5.0.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 4351970c..48f2a80d 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "apexcharts": "3.45.2", "axios": "1.6.7", "dotenv": "16.4.1", + "pinia": "^2.1.7", "qrcode.vue": "3.4.1", "quasar": "2.14.3", "vue": "3.4.15", @@ -24,6 +25,8 @@ "@vueuse/shared": "10.7.2", "monaco-editor": "0.45.0", "vuex": "4.1.0", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0", "yaml": "2.3.4" }, "devDependencies": { diff --git a/quasar.config.js b/quasar.config.js index 9dd8c77a..7c0cdb2b 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -29,7 +29,7 @@ module.exports = configure(function (/* ctx */) { // app boot file (/src/boot) // --> boot files are part of "main.js" // https://v2.quasar.dev/quasar-cli-vite/boot-files - boot: ["axios", "monaco", "integrations"], + boot: ["axios", "monaco", "pinia", "integrations"], // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css css: ["app.sass"], diff --git a/src/api/core.js b/src/api/core.js index 53a32d01..813d2ec2 100644 --- a/src/api/core.js +++ b/src/api/core.js @@ -30,6 +30,33 @@ export async function fetchURLActions(params = {}) { } } +export async function saveURLAction(payload) { + try { + const { data } = await axios.post(`${baseUrl}/urlaction/`, payload); + return data; + } catch (e) { + console.error(e); + } +} + +export async function editURLAction(id, payload) { + try { + const { data } = await axios.put(`${baseUrl}/urlaction/${id}/`, payload); + return data; + } catch (e) { + console.error(e); + } +} + +export async function removeURLAction(id) { + try { + const { data } = await axios.delete(`${baseUrl}/urlaction/${id}/`); + return data; + } catch (e) { + console.error(e); + } +} + export async function runURLAction(payload) { try { const { data } = await axios.patch(`${baseUrl}/urlaction/run/`, payload); diff --git a/src/api/core.ts b/src/api/core.ts new file mode 100644 index 00000000..c2513f3d --- /dev/null +++ b/src/api/core.ts @@ -0,0 +1,32 @@ +import axios from "axios"; + +import type { URLAction, URLActionRunResponse } from "@/types/core/urlactions"; + +const baseUrl = "/core"; + +export async function fetchURLActions(params = {}) { + const { data } = await axios.get(`${baseUrl}/urlaction/`, { + params: params, + }); + return data; +} + +export async function saveURLAction(action: URLAction) { + const { data } = await axios.post(`${baseUrl}/urlaction/`, action); + return data; +} + +export async function editURLAction(id: number, action: URLAction) { + const { data } = await axios.put(`${baseUrl}/urlaction/${id}/`, action); + return data; +} + +export async function removeURLAction(id: number) { + const { data } = await axios.delete(`${baseUrl}/urlaction/${id}/`); + return data; +} + +export async function runURLAction(id: number, payload): Promise { + const { data } = await axios.post(`${baseUrl}/urlaction/${id}/run/`); + return data; +} diff --git a/src/boot/axios.js b/src/boot/axios.js index 760b08c2..9bd758bf 100644 --- a/src/boot/axios.js +++ b/src/boot/axios.js @@ -1,4 +1,5 @@ import axios from "axios"; +import { useAuthStore } from "@/stores/auth"; import { Notify } from "quasar"; export const getBaseUrl = () => { @@ -18,27 +19,22 @@ export function setErrorMessage(data, message) { ]; } -export default function ({ app, router, store }) { +export default function ({ app, router }) { app.config.globalProperties.$axios = axios; axios.interceptors.request.use( function (config) { + const auth = useAuthStore(); config.baseURL = getBaseUrl(); - const token = store.state.token; + const token = auth.token; if (token != null) { config.headers.Authorization = `Token ${token}`; } - // config.transformResponse = [ - // function (data) { - // console.log(data); - // return data; - // }, - // ]; return config; }, function (err) { return Promise.reject(err); - } + }, ); axios.interceptors.response.use( @@ -101,6 +97,6 @@ export default function ({ app, router, store }) { } return Promise.reject({ ...error }); - } + }, ); } diff --git a/src/boot/pinia.ts b/src/boot/pinia.ts new file mode 100644 index 00000000..9daea044 --- /dev/null +++ b/src/boot/pinia.ts @@ -0,0 +1,11 @@ +import { boot } from "quasar/wrappers"; +import { createPinia } from "pinia"; + +export default boot(({ app }) => { + const pinia = createPinia(); + + app.use(pinia); + + // You can add Pinia plugins here + // pinia.use(SomePiniaPlugin) +}); diff --git a/src/components/core/TRMMCommandPrompt.vue b/src/components/core/TRMMCommandPrompt.vue new file mode 100644 index 00000000..4a812c3c --- /dev/null +++ b/src/components/core/TRMMCommandPrompt.vue @@ -0,0 +1,93 @@ + + + diff --git a/src/components/modals/coresettings/URLActionsForm.vue b/src/components/modals/coresettings/URLActionsForm.vue index 9c40bf0d..bb14e94e 100644 --- a/src/components/modals/coresettings/URLActionsForm.vue +++ b/src/components/modals/coresettings/URLActionsForm.vue @@ -1,14 +1,20 @@ - diff --git a/src/components/modals/coresettings/URLActionsTable.vue b/src/components/modals/coresettings/URLActionsTable.vue index d0c0cc21..0fe36184 100644 --- a/src/components/modals/coresettings/URLActionsTable.vue +++ b/src/components/modals/coresettings/URLActionsTable.vue @@ -9,7 +9,7 @@ icon="fas fa-plus" text-color="black" label="Add URL Action" - @click="addAction" + @click="addURLAction" /> @@ -17,7 +17,7 @@ dense :rows="actions" :columns="columns" - v-model:pagination="pagination" + :pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }" row-key="id" binary-state-sort hide-pagination @@ -30,18 +30,22 @@ - + Edit - + @@ -63,6 +67,10 @@ {{ props.row.desc }} + + + {{ props.row.action_type }} + {{ props.row.pattern }} @@ -73,105 +81,99 @@ - diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index f800ae35..37b39875 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -84,7 +84,14 @@ checked-icon="nights_stay" unchecked-icon="wb_sunny" /> - + @@ -200,13 +207,13 @@ - diff --git a/src/router/index.js b/src/router/index.js index 661af680..6471aa3f 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -4,6 +4,8 @@ import { createWebHistory, createWebHashHistory, } from "vue-router"; + +import { useAuthStore } from "@/stores/auth"; import routes from "./routes"; // useful for importing router outside of vue components @@ -13,7 +15,7 @@ export const router = new createRouter({ history: createWebHistory(process.env.VUE_ROUTER_BASE), }); -export default function ({ store }) { +export default function (/* { store } */) { const createHistory = process.env.SERVER ? createMemoryHistory : process.env.VUE_ROUTER_MODE === "history" @@ -24,13 +26,15 @@ export default function ({ store }) { scrollBehavior: () => ({ left: 0, top: 0 }), routes, history: createHistory( - process.env.MODE === "ssr" ? void 0 : process.env.VUE_ROUTER_BASE + process.env.MODE === "ssr" ? void 0 : process.env.VUE_ROUTER_BASE, ), }); Router.beforeEach((to, from, next) => { + const auth = useAuthStore(); + if (to.meta.requireAuth) { - if (!store.getters.loggedIn) { + if (!auth.loggedIn) { next({ name: "Login", }); @@ -38,7 +42,7 @@ export default function ({ store }) { next(); } } else if (to.meta.requiresVisitor) { - if (store.getters.loggedIn) { + if (auth.loggedIn) { next({ name: "Dashboard", }); diff --git a/src/store/index.js b/src/store/index.js index 68d0ac8c..fc0a3278 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -7,8 +7,6 @@ export default function () { const Store = new createStore({ state() { return { - username: localStorage.getItem("user_name") || null, - token: localStorage.getItem("access_token") || null, tree: [], agents: [], treeReady: false, @@ -49,9 +47,6 @@ export default function () { clientTreeSplitterModel(state) { return state.clientTreeSplitter; }, - loggedIn(state) { - return state.token !== null; - }, selectedAgentId(state) { return state.selectedRow; }, @@ -76,14 +71,6 @@ export default function () { setAgentPlatform(state, agentPlatform) { state.agentPlatform = agentPlatform; }, - retrieveToken(state, { token, username }) { - state.token = token; - state.username = username; - }, - destroyCommit(state) { - state.token = null; - state.username = null; - }, loadTree(state, treebar) { state.tree = treebar; state.treeReady = true; @@ -213,7 +200,7 @@ export default function () { } try { const { data } = await axios.get( - `/agents/${localParams ? localParams : ""}` + `/agents/${localParams ? localParams : ""}`, ); commit("setAgents", data); } catch (e) { @@ -232,7 +219,7 @@ export default function () { LoadingBar.setDefaults({ color: data.loading_bar_color }); commit( "setClearSearchWhenSwitching", - data.clear_search_when_switching + data.clear_search_when_switching, ); commit("SET_DEFAULT_AGENT_TBL_TAB", data.default_agent_tbl_tab); commit("SET_CLIENT_TREE_SORT", data.client_tree_sort); @@ -307,15 +294,15 @@ export default function () { } const sorted = output.sort((a, b) => - a.label.localeCompare(b.label) + a.label.localeCompare(b.label), ); if (state.clientTreeSort === "alphafail") { // move failing clients to the top const failing = sorted.filter( - (i) => i.color === "negative" || i.color === "warning" + (i) => i.color === "negative" || i.color === "warning", ); const ok = sorted.filter( - (i) => i.color !== "negative" && i.color !== "warning" + (i) => i.color !== "negative" && i.color !== "warning", ); const sortedByFailing = [...failing, ...ok]; commit("loadTree", sortedByFailing); @@ -349,37 +336,6 @@ export default function () { localStorage.removeItem("rmmver"); location.reload(); }, - retrieveToken(context, credentials) { - return new Promise((resolve) => { - axios.post("/login/", credentials).then((response) => { - const token = response.data.token; - const username = credentials.username; - localStorage.setItem("access_token", token); - localStorage.setItem("user_name", username); - context.commit("retrieveToken", { token, username }); - resolve(response); - }); - }); - }, - destroyToken(context) { - if (context.getters.loggedIn) { - return new Promise((resolve) => { - axios - .post("/logout/") - .then((response) => { - localStorage.removeItem("access_token"); - localStorage.removeItem("user_name"); - context.commit("destroyCommit"); - resolve(response); - }) - .catch(() => { - localStorage.removeItem("access_token"); - localStorage.removeItem("user_name"); - context.commit("destroyCommit"); - }); - }); - } - }, }, }); diff --git a/src/stores/auth.ts b/src/stores/auth.ts new file mode 100644 index 00000000..2dd226ca --- /dev/null +++ b/src/stores/auth.ts @@ -0,0 +1,61 @@ +import { defineStore } from "pinia"; +import { useStorage } from "@vueuse/core"; +import axios from "axios"; + +interface CheckCredentialsRequest { + username: string; + password: string; +} + +interface LoginRequest { + username: string; + password: string; + twofactor: string; +} + +interface CheckCredentialsResponse { + token: string; + username: string; + totp?: boolean; +} + +export const useAuthStore = defineStore("auth", { + state: () => ({ + username: useStorage("username", null), + token: useStorage("token", null), + }), + getters: { + loggedIn: (state) => { + return state.token !== null; + }, + }, + actions: { + async checkCredentials( + credentials: CheckCredentialsRequest, + ): Promise { + const { data } = await axios.post("/checkcreds/", credentials); + + if (!data.totp) { + this.token = data.token; + this.username = data.username; + } + return data; + }, + async login(credentials: LoginRequest) { + const { data } = await axios.post("/login/", credentials); + this.username = data.username; + this.token = data.token; + + return data; + }, + async logout() { + if (this.token !== null) { + try { + await axios.post("/logout/"); + } catch {} + } + this.token = null; + this.username = null; + }, + }, +}); diff --git a/src/stores/dashboard.ts b/src/stores/dashboard.ts new file mode 100644 index 00000000..93bce9b6 --- /dev/null +++ b/src/stores/dashboard.ts @@ -0,0 +1,43 @@ +import { defineStore } from "pinia"; +import { ref, watch } from "vue"; + +import { useDashWSConnection } from "@/websocket/websocket"; + +export const useDashboardStore = defineStore("dashboard", () => { + const loggedInUser = ref(); + + // updated by dashboard.agentcount event + const serverCount = ref(0); + const serverOfflineCount = ref(0); + const workstationCount = ref(0); + const workstationOfflineCount = ref(0); + const daysUntilCertExpires = ref(100); + + const currentTRMMVersion = ref(null); + const latestTRMMVersion = ref(null); + const needRefresh = ref(false); + const hosted = ref(false); + const tokenExpired = ref(false); + + const { data } = useDashWSConnection(); + + // watch for agent count ws data + watch(data, (newValue) => { + if (newValue.action === "dashboard.agentcount") { + serverCount.value = newValue.data.total_server_count; + serverOfflineCount.value = newValue.data.total_server_offline_count; + workstationCount.value = newValue.data.total_workstation_count; + workstationOfflineCount.value = + newValue.data.total_workstation_offline_count; + daysUntilCertExpires.value = newValue.data.days_until_cert_expires; + } + }); + + return { + serverCount, + serverOfflineCount, + workstationCount, + workstationOfflineCount, + daysUntilCertExpires, + }; +}); diff --git a/src/stores/user.ts b/src/stores/user.ts new file mode 100644 index 00000000..1a92741a --- /dev/null +++ b/src/stores/user.ts @@ -0,0 +1,26 @@ +import { defineStore } from "pinia"; +import { ref, watch } from "vue"; + +import { useDashWSConnection } from "@/websocket/websocket"; + +export const useUserStore = defineStore("user", () => { + const { data } = useDashWSConnection(); + + // updated by user.update event + const dashInfoColor = ref("info"); + const dashPositiveColor = ref("positive"); + const dashWarningColor = ref("warning"); + const dashNegativeColor = ref("negative"); + + // watch for agent count ws data + watch(data, (newValue) => { + if (newValue.action === "user.update") { + serverCount.value = newValue.data.total_server_count; + serverOfflineCount.value = newValue.data.total_server_offline_count; + workstationCount.value = newValue.data.total_workstation_count; + workstationOfflineCount.value = + newValue.data.total_workstation_offline_count; + daysUntilCertExpires.value = newValue.data.days_until_cert_expires; + } + }); +}); diff --git a/src/types/core/urlactions.ts b/src/types/core/urlactions.ts new file mode 100644 index 00000000..5e326616 --- /dev/null +++ b/src/types/core/urlactions.ts @@ -0,0 +1,17 @@ +export type ActionType = "web" | "rest"; + +export interface URLAction { + id: number; + name: string; + desc?: string; + action_type: ActionType; + pattern: string; + rest_method: string; + rest_body: string; + rest_headers: string; +} + +export interface URLActionRunResponse { + url: string; + result: string; +} diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index effa22d6..ab5b3ab4 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -11,7 +11,7 @@ - + @@ -53,7 +53,7 @@ - + Two-Factor Token @@ -62,8 +62,8 @@ +import { ref, reactive } from "vue"; +import { type QForm, useQuasar } from "quasar"; +import { useAuthStore } from "@/stores/auth"; +import { useRouter } from "vue-router"; -export default { - name: "LoginView", - mixins: [mixins], - data() { - return { - credentials: {}, - prompt: false, - isPwd: true, - }; - }, +// setup quasar +const $q = useQuasar(); +$q.dark.set(true); - methods: { - checkCreds() { - this.$axios.post("/checkcreds/", this.credentials).then((r) => { - if (r.data.totp === "totp not set") { - // sign in to setup two factor temporarily - const token = r.data.token; - const username = r.data.username; - localStorage.setItem("access_token", token); - localStorage.setItem("user_name", username); - this.$store.commit("retrieveToken", { token, username }); - this.$router.push({ name: "TOTPSetup" }); - } else { - this.prompt = true; - } - }); - }, - onSubmit() { - this.$store - .dispatch("retrieveToken", this.credentials) - .then(() => { - this.credentials = {}; - this.$router.push({ name: "Dashboard" }); - }) - .catch(() => { - this.credentials = {}; - this.prompt = false; - }); - }, - }, - mounted() { - this.$q.dark.set(true); - }, -}; +// setup auth store +const auth = useAuthStore(); + +// setup router +const router = useRouter(); + +const form = ref(null); +const formToken = ref(null); + +// login logic +const credentials = reactive({ username: "", password: "" }); +const twofactor = ref(""); +const prompt = ref(false); +const showPassword = ref(true); + +async function checkCreds() { + const { totp } = await auth.checkCredentials(credentials); + + if (!totp) { + router.push({ name: "TOTPSetup" }); + } else { + prompt.value = true; + } +} + +async function onSubmit() { + try { + await auth.login({ ...credentials, twofactor: twofactor.value }); + router.push({ name: "Dashboard" }); + } finally { + form.value?.reset(); + formToken.value?.reset(); + prompt.value = false; + } +}