mirror of
https://github.com/jpros/tacticalrmm-web.git
synced 2026-02-26 22:31:28 +00:00
allow for creating special tokens for api access and bypassing two factor auth
This commit is contained in:
@@ -2,9 +2,34 @@ import axios from "axios"
|
||||
|
||||
const baseUrl = "/accounts"
|
||||
|
||||
// user api functions
|
||||
export async function fetchUsers(params = {}) {
|
||||
try {
|
||||
const { data } = await axios.get(`${baseUrl}/users/`, { params: params })
|
||||
return data
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// api key api functions
|
||||
export async function fetchAPIKeys(params = {}) {
|
||||
try {
|
||||
const { data } = await axios.get(`${baseUrl}/apikeys/`, { params: params })
|
||||
return data
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
export async function saveAPIKey(payload) {
|
||||
const { data } = await axios.post(`${baseUrl}/apikeys/`, payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function editAPIKey(payload) {
|
||||
const { data } = await axios.put(`${baseUrl}/apikeys/${payload.id}/`, payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function removeAPIKey(id) {
|
||||
const { data } = await axios.delete(`${baseUrl}/apikeys/${id}/`)
|
||||
return data
|
||||
}
|
||||
|
||||
122
src/components/core/APIKeysForm.vue
Normal file
122
src/components/core/APIKeysForm.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||
<q-card class="q-dialog-plugin" style="width: 60vw">
|
||||
<q-bar>
|
||||
{{ title }}
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-form @submit.prevent="submitForm">
|
||||
<q-card-section>
|
||||
<span v-if="!APIKey">API Key will be generated on save</span>
|
||||
</q-card-section>
|
||||
<!-- name -->
|
||||
<q-card-section>
|
||||
<q-input label="Name" outlined dense v-model="localKey.name" :rules="[val => !!val || '*Required']" />
|
||||
</q-card-section>
|
||||
|
||||
<!-- user -->
|
||||
<q-card-section>
|
||||
<tactical-dropdown outlined v-model="localKey.user" label="User" :options="userOptions" mapOptions />
|
||||
</q-card-section>
|
||||
|
||||
<!-- key -->
|
||||
<q-card-section v-if="APIKey">
|
||||
<q-input readonly label="Key" outlined dense v-model="localKey.key" />
|
||||
</q-card-section>
|
||||
|
||||
<!-- expiration -->
|
||||
<q-card-section>
|
||||
<q-input dense label="Key Expiration (Not required) " filled v-model="localKey.expiration">
|
||||
<template v-slot:append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy transition-show="scale" transition-hide="scale">
|
||||
<q-date v-model="localKey.expiration" mask="YYYY-MM-DD HH:mm">
|
||||
<div class="row items-center justify-end">
|
||||
<q-btn v-close-popup label="Close" color="primary" flat />
|
||||
</div>
|
||||
</q-date>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
<q-icon name="access_time" class="cursor-pointer">
|
||||
<q-popup-proxy transition-show="scale" transition-hide="scale">
|
||||
<q-time v-model="localKey.expiration" mask="YYYY-MM-DD HH:mm">
|
||||
<div class="row items-center justify-end">
|
||||
<q-btn v-close-popup label="Close" color="primary" flat />
|
||||
</div>
|
||||
</q-time>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Cancel" v-close-popup />
|
||||
<q-btn flat label="Submit" color="primary" type="submit" :loading="loading" />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// composition imports
|
||||
import { ref, computed } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { saveAPIKey, editAPIKey } from "@/api/accounts";
|
||||
import { useUserDropdown } from "@/composables/accounts";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
|
||||
export default {
|
||||
components: { TacticalDropdown },
|
||||
name: "APIKeysForm",
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
props: { APIKey: Object },
|
||||
setup(props) {
|
||||
// setup quasar plugins
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
|
||||
// setup dropdowns
|
||||
const { userOptions } = useUserDropdown(true);
|
||||
|
||||
// setup api key form logic
|
||||
const key = props.APIKey ? ref(Object.assign({}, props.APIKey)) : ref({ name: "", expiration: null });
|
||||
const loading = ref(false);
|
||||
|
||||
const title = computed(() => (props.APIKey ? "Edit API Key" : "Add API Key"));
|
||||
|
||||
async function submitForm() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = props.APIKey ? await editAPIKey(key.value) : await saveAPIKey(key.value);
|
||||
onDialogOK();
|
||||
notifySuccess(result);
|
||||
loading.value = false;
|
||||
} catch (e) {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
return {
|
||||
// reactive data
|
||||
localKey: key,
|
||||
loading,
|
||||
userOptions,
|
||||
|
||||
// computed
|
||||
title,
|
||||
|
||||
// methods
|
||||
submitForm,
|
||||
|
||||
// quasar dialog
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
onDialogOK,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
212
src/components/core/APIKeysTable.vue
Normal file
212
src/components/core/APIKeysTable.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="text-subtitle2">API Keys</div>
|
||||
<q-space />
|
||||
<q-btn size="sm" color="grey-5" icon="fas fa-plus" text-color="black" label="Add key" @click="addAPIKey" />
|
||||
</div>
|
||||
<hr />
|
||||
<q-table
|
||||
dense
|
||||
:rows="keys"
|
||||
:columns="columns"
|
||||
v-model:pagination="pagination"
|
||||
row-key="id"
|
||||
binary-state-sort
|
||||
hide-pagination
|
||||
virtual-scroll
|
||||
:rows-per-page-options="[0]"
|
||||
no-data-label="No API tokens added yet"
|
||||
>
|
||||
<!-- header slots -->
|
||||
<template v-slot:header-cell-actions="props">
|
||||
<q-th :props="props" auto-width> </q-th>
|
||||
</template>
|
||||
|
||||
<!-- body slots -->
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props" class="cursor-pointer" @dblclick="editAPIKey(props.row)">
|
||||
<!-- context menu -->
|
||||
<q-menu context-menu>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item clickable v-close-popup @click="editAPIKey(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="deleteAPIKey(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>
|
||||
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>Close</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
<!-- name -->
|
||||
<q-td>
|
||||
{{ props.row.name }}
|
||||
</q-td>
|
||||
<q-td>
|
||||
{{ props.row.user }}
|
||||
</q-td>
|
||||
<!-- expiration -->
|
||||
<q-td>
|
||||
{{ props.row.expiration }}
|
||||
</q-td>
|
||||
<!-- created time -->
|
||||
<q-td>
|
||||
{{ props.row.created_time }}
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-icon size="sm" name="content_copy" @click="copyKeyToClipboard(props.row.key)">
|
||||
<q-tooltip>Copy API Key to clipboard</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// composition imports
|
||||
import { ref, onMounted } from "vue";
|
||||
import { fetchAPIKeys, removeAPIKey } from "@/api/accounts";
|
||||
import { useQuasar, copyToClipboard } from "quasar";
|
||||
import { notifySuccess, notifyError } from "@/utils/notify";
|
||||
import APIKeysForm from "@/components/core/APIKeysForm";
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: "name",
|
||||
label: "Name",
|
||||
field: "name",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "user",
|
||||
label: "User",
|
||||
field: "user",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "expiration",
|
||||
label: "Expiration",
|
||||
field: "expiration",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "created_time",
|
||||
label: "Created",
|
||||
field: "created_time",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "actions",
|
||||
label: "",
|
||||
field: "actions",
|
||||
},
|
||||
];
|
||||
export default {
|
||||
name: "APIKeysTable",
|
||||
setup() {
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
|
||||
// setup api keys logic
|
||||
const keys = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// setup table
|
||||
const pagination = ref({
|
||||
rowsPerPage: 0,
|
||||
sortBy: "name",
|
||||
descending: true,
|
||||
});
|
||||
|
||||
function copyKeyToClipboard(apikey) {
|
||||
copyToClipboard(apikey)
|
||||
.then(() => {
|
||||
notifySuccess("Key was copied to clipboard!");
|
||||
})
|
||||
.catch(() => {
|
||||
notifyError("Unable to copy to clipboard!");
|
||||
});
|
||||
}
|
||||
|
||||
// api functions
|
||||
async function getAPIKeys() {
|
||||
loading.value = true;
|
||||
keys.value = await fetchAPIKeys();
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function deleteAPIKey(key) {
|
||||
$q.dialog({
|
||||
title: `Delete API key: ${key.name}?`,
|
||||
cancel: true,
|
||||
ok: { label: "Delete", color: "negative" },
|
||||
}).onOk(async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await removeAPIKey(key.id);
|
||||
notifySuccess(result);
|
||||
getAPIKeys();
|
||||
loading.value = false;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// quasar dialog functions
|
||||
function editAPIKey(key) {
|
||||
$q.dialog({
|
||||
component: APIKeysForm,
|
||||
componentProps: {
|
||||
APIKey: key,
|
||||
},
|
||||
}).onOk(() => getAPIKeys());
|
||||
}
|
||||
|
||||
function addAPIKey() {
|
||||
$q.dialog({
|
||||
component: APIKeysForm,
|
||||
}).onOk(() => getAPIKeys());
|
||||
}
|
||||
|
||||
// component lifecycle hooks
|
||||
onMounted(getAPIKeys());
|
||||
return {
|
||||
// reactive data
|
||||
keys,
|
||||
loading,
|
||||
pagination,
|
||||
|
||||
// non-reactive data
|
||||
columns,
|
||||
|
||||
//methods
|
||||
getAPIKeys,
|
||||
deleteAPIKey,
|
||||
copyKeyToClipboard,
|
||||
|
||||
//dialogs
|
||||
editAPIKey,
|
||||
addAPIKey,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -56,6 +56,7 @@
|
||||
<q-checkbox v-model="role.can_edit_core_settings" label="Edit Global Settings" />
|
||||
<q-checkbox v-model="role.can_do_server_maint" label="Do Server Maintenance" />
|
||||
<q-checkbox v-model="role.can_code_sign" label="Manage Code Signing" />
|
||||
<q-checkbox v-model="role.can_manage_api_keys" label="Manage API Keys" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
@@ -180,6 +181,7 @@ export default {
|
||||
can_manage_notes: false,
|
||||
can_view_core_settings: false,
|
||||
can_edit_core_settings: false,
|
||||
can_manage_api_keys: false,
|
||||
can_do_server_maint: false,
|
||||
can_code_sign: false,
|
||||
can_manage_checks: false,
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Active:</div>
|
||||
<div class="col-10">
|
||||
<q-toggle v-model="localUser.is_active" color="green" :disable="localUser.username === logged_in_user" />
|
||||
<q-checkbox v-model="localUser.is_active" :disable="localUser.username === logged_in_user" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
@@ -88,6 +88,14 @@
|
||||
class="col-10"
|
||||
/></template>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-checkbox
|
||||
label="Deny Dashboard Logins"
|
||||
left-label
|
||||
v-model="localUser.deny_dashboard_login"
|
||||
:disable="localUser.username === logged_in_user"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="row items-center">
|
||||
<q-btn :disable="!disableSave" label="Save" color="primary" type="submit" />
|
||||
</q-card-section>
|
||||
@@ -109,6 +117,7 @@ export default {
|
||||
return {
|
||||
localUser: {
|
||||
is_active: true,
|
||||
deny_dashboard_login: false,
|
||||
},
|
||||
roles: [],
|
||||
isPwd: true,
|
||||
@@ -146,6 +155,7 @@ export default {
|
||||
// dont allow updating is_active if username is same as logged in user
|
||||
if (this.localUser.username === this.logged_in_user) {
|
||||
delete this.localUser.is_active;
|
||||
delete this.localUser.deny_dashboard_login;
|
||||
}
|
||||
|
||||
this.$axios
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<q-tab name="keystore" label="Key Store" />
|
||||
<q-tab name="urlactions" label="URL Actions" />
|
||||
<q-tab name="retention" label="Retention" />
|
||||
<q-tab name="apikeys" label="API Keys" />
|
||||
</q-tabs>
|
||||
</template>
|
||||
<template v-slot:after>
|
||||
@@ -384,6 +385,10 @@
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="apikeys">
|
||||
<APIKeysTable />
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-scroll-area>
|
||||
<q-card-section class="row items-center">
|
||||
@@ -422,6 +427,7 @@ import ResetPatchPolicy from "@/components/modals/coresettings/ResetPatchPolicy"
|
||||
import CustomFields from "@/components/modals/coresettings/CustomFields";
|
||||
import KeyStoreTable from "@/components/modals/coresettings/KeyStoreTable";
|
||||
import URLActionsTable from "@/components/modals/coresettings/URLActionsTable";
|
||||
import APIKeysTable from "@/components/core/APIKeysTable";
|
||||
|
||||
export default {
|
||||
name: "EditCoreSettings",
|
||||
@@ -430,6 +436,7 @@ export default {
|
||||
CustomFields,
|
||||
KeyStoreTable,
|
||||
URLActionsTable,
|
||||
APIKeysTable,
|
||||
},
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
|
||||
import { ref } from "vue"
|
||||
import { ref, onMounted } from "vue"
|
||||
import { fetchUsers } from "@/api/accounts"
|
||||
import { formatUserOptions } from "@/utils/format"
|
||||
|
||||
export function useUserDropdown() {
|
||||
export function useUserDropdown(onMount = false) {
|
||||
|
||||
const userOptions = ref([])
|
||||
const userDropdownLoading = ref(false)
|
||||
@@ -32,6 +32,10 @@ export function useUserDropdown() {
|
||||
})
|
||||
}
|
||||
|
||||
if (onMount) {
|
||||
onMounted(getUserOptions())
|
||||
}
|
||||
|
||||
return {
|
||||
//data
|
||||
userOptions,
|
||||
|
||||
Reference in New Issue
Block a user