added user session tracking, social accoutn tracking, and implemented local user logon blocking
This commit is contained in:
@@ -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
|
||||
export async function fetchRoles(params = {}) {
|
||||
try {
|
||||
|
@@ -8,8 +8,15 @@ 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;
|
||||
|
@@ -1,159 +1,191 @@
|
||||
<template>
|
||||
<div style="width: 900px; max-width: 90vw">
|
||||
<q-card>
|
||||
<q-bar>
|
||||
<q-card style="width: 900px; max-width: 90vw; 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">
|
||||
<q-btn
|
||||
ref="refresh"
|
||||
@click="getUsers"
|
||||
class="q-mr-sm"
|
||||
ref="new"
|
||||
label="New"
|
||||
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
|
||||
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>
|
||||
unelevated
|
||||
no-caps
|
||||
icon="add"
|
||||
@click="showAddUserModal"
|
||||
/>
|
||||
</div>
|
||||
</q-card>
|
||||
</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"
|
||||
:disable="localLogonDisabled"
|
||||
>
|
||||
<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="localLogonDisabled"
|
||||
>
|
||||
<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>{{ 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>
|
||||
|
||||
<script>
|
||||
import mixins from "@/mixins/mixins";
|
||||
import { computed } from "vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar } from "quasar";
|
||||
import { fetchCoreSettings } from "@/api/core";
|
||||
|
||||
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",
|
||||
@@ -163,8 +195,41 @@ export default {
|
||||
const store = useStore();
|
||||
const formatDate = computed(() => store.getters.formatDate);
|
||||
|
||||
const $q = useQuasar();
|
||||
|
||||
const localLogonDisabled = ref(false);
|
||||
|
||||
function showSSOAccounts(user) {
|
||||
$q.dialog({
|
||||
component: SSOAccountsTable,
|
||||
componentProps: {
|
||||
user,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function getCoreSettings() {
|
||||
const result = await fetchCoreSettings();
|
||||
|
||||
localLogonDisabled.value = result.block_local_user_logon;
|
||||
}
|
||||
|
||||
async function showSessions(user) {
|
||||
$q.dialog({
|
||||
component: UserSessionsTable,
|
||||
componentProps: {
|
||||
user,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(getCoreSettings);
|
||||
|
||||
return {
|
||||
localLogonDisabled,
|
||||
formatDate,
|
||||
showSSOAccounts,
|
||||
showSessions,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
|
150
src/components/accounts/UserSessionsTable.vue
Normal file
150
src/components/accounts/UserSessionsTable.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<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>{{ props.row.created }}</td>
|
||||
<td>{{ 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 {
|
||||
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}?`,
|
||||
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>
|
@@ -739,7 +739,6 @@ export default {
|
||||
URLActionsTable,
|
||||
APIKeysTable,
|
||||
SSOProvidersTable,
|
||||
// ServerTasksTable,
|
||||
},
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
|
@@ -34,16 +34,7 @@ function postForm(url: string, data: FormData) {
|
||||
f.submit();
|
||||
}
|
||||
|
||||
export interface MetaIsAuthenticated {
|
||||
is_authenticated: boolean;
|
||||
}
|
||||
|
||||
// sso providers
|
||||
export interface AllAuthResponse<T> {
|
||||
data: T;
|
||||
status: number;
|
||||
meta?: MetaIsAuthenticated;
|
||||
}
|
||||
|
||||
export async function fetchSSOProviders(): Promise<SSOProvider> {
|
||||
const { data } = await axios.get(`${baseUrl}/ssoproviders/`);
|
||||
@@ -65,6 +56,24 @@ export async function removeSSOProvider(id: number) {
|
||||
return data;
|
||||
}
|
||||
|
||||
export interface SSOSettings {
|
||||
block_local_user_logon: boolean;
|
||||
}
|
||||
|
||||
export async function fetchSSOSettings(): Promise<SSOSettings> {
|
||||
const { data } = await axios.get(`${baseUrl}/ssoproviders/settings/`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateSSOSettings(settings: SSOSettings) {
|
||||
console.log(settings);
|
||||
const { data } = await axios.post(
|
||||
`${baseUrl}/ssoproviders/settings/`,
|
||||
settings,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getSSOProviderToken() {
|
||||
const { data } = await axios.post(
|
||||
`${baseUrl}/ssoproviders/token/`,
|
||||
@@ -76,6 +85,17 @@ export async function getSSOProviderToken() {
|
||||
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[];
|
||||
@@ -92,12 +112,29 @@ export interface SSOConfigResponse {
|
||||
export async function getSSOConfig(): Promise<
|
||||
AllAuthResponse<SSOConfigResponse>
|
||||
> {
|
||||
const { data } = await axios.get("_allauth/browser/v1/config");
|
||||
const { data } = await axios.get(`${allauthBase}/config`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export interface SSOAccountsResponse {
|
||||
uid: string;
|
||||
display: string;
|
||||
provider: SSOProviderConfig;
|
||||
}
|
||||
|
||||
export async function disconnectSSOAccount(
|
||||
provider: string,
|
||||
account: string,
|
||||
): Promise<AllAuthResponse<SSOAccountsResponse[]>> {
|
||||
const { data } = await axios.delete(`${allauthBase}/account/providers`, {
|
||||
data: { provider, account },
|
||||
headers: { "X-CSRFToken": getCSRFToken() },
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function openSSOProviderRedirect(id: string) {
|
||||
postForm(`${getBaseUrl()}/_allauth/browser/v1/auth/provider/redirect`, {
|
||||
postForm(`${getBaseUrl()}/${allauthBase}/auth/provider/redirect`, {
|
||||
provider: id,
|
||||
process: "login",
|
||||
callback_url: `${location.origin}/account/provider/callback`,
|
||||
|
141
src/ee/sso/components/SSOAccountsTable.vue
Normal file
141
src/ee/sso/components/SSOAccountsTable.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<!--
|
||||
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>{{ props.row.last_login }}</td>
|
||||
<td>{{ 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";
|
||||
|
||||
//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}? If you are signed in with this account you will be logged off`,
|
||||
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>
|
@@ -1,3 +1,9 @@
|
||||
<!--
|
||||
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">
|
||||
|
@@ -1,3 +1,9 @@
|
||||
<!--
|
||||
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">
|
||||
@@ -26,6 +32,23 @@
|
||||
no-data-label="No SSO Providers added yet"
|
||||
:loading="loading"
|
||||
>
|
||||
<template v-slot:top>
|
||||
<q-space />
|
||||
<q-btn
|
||||
@click="
|
||||
changeSSOSettings({
|
||||
block_local_user_logon: !ssoSettings.block_local_user_logon,
|
||||
})
|
||||
"
|
||||
:label="
|
||||
ssoSettings.block_local_user_logon
|
||||
? 'Allow Local Logon'
|
||||
: 'Block Local Logon'
|
||||
"
|
||||
no-caps
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
<!-- body slots -->
|
||||
<template v-slot:body="props">
|
||||
<q-tr
|
||||
@@ -86,7 +109,13 @@
|
||||
// composition imports
|
||||
import { ref, onMounted } from "vue";
|
||||
import { QTableColumn, useQuasar } from "quasar";
|
||||
import { fetchSSOProviders, removeSSOProvider } from "@/ee/sso/api/sso";
|
||||
import {
|
||||
fetchSSOProviders,
|
||||
removeSSOProvider,
|
||||
fetchSSOSettings,
|
||||
updateSSOSettings,
|
||||
type SSOSettings,
|
||||
} from "@/ee/sso/api/sso";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
|
||||
// ui imports
|
||||
@@ -101,7 +130,7 @@ const $q = useQuasar();
|
||||
const loading = ref(false);
|
||||
|
||||
const providers = ref([] as SSOProvider[]);
|
||||
|
||||
const ssoSettings = ref({} as SSOSettings);
|
||||
const columns: QTableColumn[] = [
|
||||
{
|
||||
name: "name",
|
||||
@@ -136,6 +165,28 @@ async function getSSOProviders() {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function getSSOSettings() {
|
||||
loading.value = true;
|
||||
try {
|
||||
ssoSettings.value = await fetchSSOSettings();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function changeSSOSettings(data: SSOSettings) {
|
||||
loading.value = true;
|
||||
try {
|
||||
ssoSettings.value = await updateSSOSettings(data);
|
||||
await getSSOSettings();
|
||||
notifySuccess("Settings updated successfully");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function addSSOProvider() {
|
||||
$q.dialog({
|
||||
component: SSOProvidersForm,
|
||||
@@ -168,5 +219,9 @@ function deleteSSOProvider(provider: SSOProvider) {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
onMounted(getSSOProviders);
|
||||
|
||||
onMounted(async () => {
|
||||
await getSSOProviders();
|
||||
await getSSOSettings();
|
||||
});
|
||||
</script>
|
||||
|
@@ -1,3 +1,10 @@
|
||||
/*
|
||||
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;
|
||||
@@ -6,3 +13,15 @@ export interface SSOProvider {
|
||||
secret: string;
|
||||
server_url: string;
|
||||
}
|
||||
|
||||
export interface SSOAccount {
|
||||
uid: string;
|
||||
display: string;
|
||||
provider: string;
|
||||
last_login: string;
|
||||
date_joined: string;
|
||||
}
|
||||
|
||||
export interface SSOUser extends User {
|
||||
social_accounts: SSOAccount[];
|
||||
}
|
||||
|
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
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 !== "") {
|
||||
|
@@ -1,3 +1,9 @@
|
||||
<!--
|
||||
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>
|
||||
|
@@ -29,6 +29,7 @@ export const useAuthStore = defineStore("auth", {
|
||||
state: () => ({
|
||||
username: useStorage("user_name", null),
|
||||
token: useStorage("access_token", null),
|
||||
ssoLoginProvider: useStorage("sso_provider", null),
|
||||
}),
|
||||
getters: {
|
||||
loggedIn: (state) => {
|
||||
@@ -51,6 +52,7 @@ export const useAuthStore = defineStore("auth", {
|
||||
const { data } = await axios.post("/v2/login/", credentials);
|
||||
this.username = data.username;
|
||||
this.token = data.token;
|
||||
this.ssoLoginProvider = null;
|
||||
|
||||
return data;
|
||||
},
|
||||
@@ -62,6 +64,7 @@ export const useAuthStore = defineStore("auth", {
|
||||
}
|
||||
this.token = null;
|
||||
this.username = null;
|
||||
this.ssoLoginProvider = null;
|
||||
},
|
||||
async setupTotp(): Promise<TOTPSetupResponse | false> {
|
||||
const { data } = await axios.post("/accounts/users/setup_totp/");
|
||||
|
@@ -1,4 +1,13 @@
|
||||
export interface User {
|
||||
id: number;
|
||||
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;
|
||||
}
|
Reference in New Issue
Block a user