sso init
This commit is contained in:
@@ -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,8 +62,16 @@ export default function ({ app, router }) {
|
||||
}
|
||||
// unauthorized
|
||||
else if (error.response.status === 401) {
|
||||
// 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) {
|
||||
// don't notify user if method is GET
|
||||
|
@@ -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
97
src/ee/sso/api/sso.ts
Normal 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() || "",
|
||||
});
|
||||
}
|
112
src/ee/sso/components/SSOProvidersForm.vue
Normal file
112
src/ee/sso/components/SSOProvidersForm.vue
Normal 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>
|
172
src/ee/sso/components/SSOProvidersTable.vue
Normal file
172
src/ee/sso/components/SSOProvidersTable.vue
Normal 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
8
src/ee/sso/types/sso.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface SSOProvider {
|
||||
id: number;
|
||||
name: string;
|
||||
provider_id: string;
|
||||
client_id: string;
|
||||
secret: string;
|
||||
server_url: string;
|
||||
}
|
15
src/ee/sso/utils/cookies.ts
Normal file
15
src/ee/sso/utils/cookies.ts
Normal 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;
|
||||
}
|
28
src/ee/sso/views/ProviderCallback.vue
Normal file
28
src/ee/sso/views/ProviderCallback.vue
Normal 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>
|
@@ -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") },
|
||||
];
|
||||
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user