This commit is contained in:
sadnub
2024-09-15 22:22:28 -04:00
committed by wh1te909
parent ee8aada530
commit 541134a88f
10 changed files with 482 additions and 2 deletions

View File

@@ -22,6 +22,8 @@ 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) {
const auth = useAuthStore();
@@ -60,7 +62,15 @@ export default function ({ app, router }) {
}
// unauthorized
else if (error.response.status === 401) {
router.push({ path: "/expired" });
// bypass redirect for auth check endpoint
if (
error.config.url !== "_allauth/browser/v1/auth/session" ||
error.config.url !== "ws/dashinfo" // TODO once auth is working, need to extend it to websockets
) {
return Promise.reject({ ...error });
} else {
router.push({ path: "/expired" });
}
}
// perms
else if (error.response.status === 403) {

View File

@@ -13,6 +13,7 @@
<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="SSO Integration" />
<!-- <q-tab name="openai" label="Open AI" /> -->
</q-tabs>
</template>
@@ -636,6 +637,11 @@
<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>
@@ -722,6 +728,7 @@ 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",
@@ -731,6 +738,7 @@ export default {
KeyStoreTable,
URLActionsTable,
APIKeysTable,
SSOProvidersTable,
// ServerTasksTable,
},
mixins: [mixins],

97
src/ee/sso/api/sso.ts Normal file
View File

@@ -0,0 +1,97 @@
import axios from "axios";
import { getCookie } from "@/ee/sso/utils/cookies";
import { getBaseUrl } from "@/boot/axios";
import type { SSOProvider } 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 interface AllAuthResponse<T> {
data: T;
status: number;
}
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 getCurrentSession() {
const { data } = await axios.get("_allauth/browser/v1/auth/session");
return data;
}
export interface SSOProviderConfig {
client_id: string;
flows: string[];
id: string;
name: string;
}
export interface SSOConfigResponseProviders {
providers: SSOProviderConfig[];
}
export interface SSOConfigResponse {
socialaccount: SSOConfigResponseProviders;
}
export async function getSSOConfig(): Promise<
AllAuthResponse<SSOConfigResponse>
> {
const { data } = await axios.get("_allauth/browser/v1/config");
return data;
}
export async function openSSOProviderRedirect(id: string) {
postForm(`${getBaseUrl()}/_allauth/browser/v1/auth/provider/redirect`, {
provider: id,
process: "login",
callback_url: `${location.origin}/account/provider/callback`,
csrfmiddlewaretoken: getCSRFToken() || "",
});
}

View File

@@ -0,0 +1,112 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin" style="width: 50">
<q-bar>
{{ props.provider ? "Edit SSO Provider" : "Add SSO 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"
label="Name"
outlined
dense
v-model="localProvider.name"
:rules="[(val) => !!val || '*Required']"
/>
</q-card-section>
<!-- url -->
<q-card-section>
<q-input
label="Server URL"
outlined
dense
v-model="localProvider.server_url"
:rules="[(val) => !!val || '*Required']"
/>
</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
label="Secret"
outlined
dense
v-model="localProvider.secret"
:rules="[(val) => !!val || '*Required']"
/>
</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 { SSOProvider } from "@/types/accounts";
// define emits
defineEmits([...useDialogPluginComponent.emits]);
// define props
const props = defineProps<{ provider?: SSOProvider }>();
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
const loading = ref(false);
const localProvider: SSOProvider = props.provider
? reactive(extend({}, props.provider))
: reactive({
id: 0,
name: "",
client_id: "",
secret: "",
server_url: "",
} 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

@@ -0,0 +1,172 @@
<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 SSO Provider"
@click="addSSOProvider"
/>
</div>
<q-separator />
<q-table
dense
:rows="providers"
:columns="columns"
: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 SSO Providers added yet"
:loading="loading"
>
<!-- 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>
<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>
<!-- server_url -->
<q-td>
{{ props.row.server_url }}
</q-td>
<!-- pattern -->
<q-td>
{{ props.row.client_id }}
</q-td>
</q-tr>
</template>
</q-table>
</div>
</template>
<script setup lang="ts">
// composition imports
import { ref, onMounted } from "vue";
import { QTableColumn, useQuasar } from "quasar";
import { fetchSSOProviders, removeSSOProvider } from "@/ee/sso/api/sso";
import { notifySuccess } from "@/utils/notify";
// ui imports
import SSOProvidersForm from "@/components/modals/coresettings/SSOProvidersForm.vue";
// types
import { type SSOProvider } from "@/types/accounts";
// setup quasar
const $q = useQuasar();
const loading = ref(false);
const providers = ref([] as SSOProvider[]);
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,
},
];
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;
});
}
onMounted(getSSOProviders);
</script>

8
src/ee/sso/types/sso.ts Normal file
View File

@@ -0,0 +1,8 @@
export interface SSOProvider {
id: number;
name: string;
provider_id: string;
client_id: string;
secret: string;
server_url: string;
}

View File

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,28 @@
<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.params.error;
const router = useRouter();
const auth = useAuthStore();
if (!error) {
if (auth.loggedIn) {
router.push({ name: "Dashboard" });
} else {
router.push({ name: "Login" });
}
}
</script>

View File

@@ -75,6 +75,11 @@ const routes = [
name: "SessionExpired",
component: () => import("@/views/SessionExpired.vue"),
},
{
path: "/account/provider/callback",
name: "ProviderCallback",
component: () => import("@/ee/sso/views/ProviderCallback.vue"),
},
{ path: "/:catchAll(.*)", component: () => import("@/views/NotFound.vue") },
];

View File

@@ -49,7 +49,18 @@
</div>
</q-form>
</q-card-section>
<q-card-section v-if="ssoProviders.length > 0">
<q-list dense v-for="provider in ssoProviders" :key="provider.id">
<q-item @click="openSSOProviderRedirect(provider.id)" clickable>
<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">
@@ -84,10 +95,16 @@
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
import { ref, reactive, onMounted } from "vue";
import { type QForm, useQuasar } from "quasar";
import { useAuthStore } from "@/stores/auth";
import { useRouter } from "vue-router";
import {
openSSOProviderRedirect,
getSSOConfig,
getCurrentSession,
type SSOProviderConfig,
} from "@/ee/sso/api/sso";
// setup quasar
const $q = useQuasar();
@@ -107,6 +124,7 @@ 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 {
@@ -135,6 +153,13 @@ async function onSubmit() {
prompt.value = false;
}
}
onMounted(async () => {
const result = await getSSOConfig();
ssoProviders.value = result.data.socialaccount.providers;
await getCurrentSession();
});
</script>
<style>