feat: add SAML settings UI
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
<script setup>
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import SamlSettings from './components/SamlSettings.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsLayout
|
||||
class="max-w-2xl mx-auto"
|
||||
:loading-message="$t('ATTRIBUTES_MGMT.LOADING')"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('SECURITY_SETTINGS.TITLE')"
|
||||
:description="$t('SECURITY_SETTINGS.DESCRIPTION')"
|
||||
feature-name="security"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<SamlSettings />
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import SectionLayout from '../../account/components/SectionLayout.vue';
|
||||
import WithLabel from 'v3/components/Form/WithLabel.vue';
|
||||
import TextInput from 'next/input/Input.vue';
|
||||
import TextArea from 'next/textarea/TextArea.vue';
|
||||
import Switch from 'next/switch/Switch.vue';
|
||||
import NextButton from 'next/button/Button.vue';
|
||||
import samlSettingsAPI from 'dashboard/api/samlSettings';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { isCloudFeatureEnabled } = useAccount();
|
||||
|
||||
const ssoUrl = ref('');
|
||||
const certificate = ref('');
|
||||
const spEntityId = ref('');
|
||||
const roleMappings = ref({});
|
||||
const isEnabled = ref(false);
|
||||
const isSubmitting = ref(false);
|
||||
const isLoading = ref(true);
|
||||
|
||||
const hasFeature = computed(() => isCloudFeatureEnabled('saml'));
|
||||
|
||||
const loadSamlSettings = async () => {
|
||||
if (!hasFeature.value) return;
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const response = await samlSettingsAPI.get();
|
||||
const settings = response.data;
|
||||
|
||||
if (settings.sso_url) {
|
||||
ssoUrl.value = settings.sso_url;
|
||||
certificate.value = settings.certificate || '';
|
||||
spEntityId.value = settings.sp_entity_id || '';
|
||||
roleMappings.value = settings.role_mappings || {};
|
||||
isEnabled.value = ssoUrl.value !== '';
|
||||
}
|
||||
} catch (error) {
|
||||
// If no settings exist (404), that's expected - just keep defaults
|
||||
if (error.response?.status !== 404) {
|
||||
useAlert(t('SECURITY_SETTINGS.SAML.API.ERROR_LOADING'));
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveSamlSettings = async settings => {
|
||||
try {
|
||||
isSubmitting.value = true;
|
||||
|
||||
if (isEnabled.value && ssoUrl.value) {
|
||||
// Create or update settings
|
||||
const existingSettings = await samlSettingsAPI.get().catch(() => null);
|
||||
|
||||
if (existingSettings?.data?.id) {
|
||||
await samlSettingsAPI.update(settings);
|
||||
} else {
|
||||
await samlSettingsAPI.create(settings);
|
||||
}
|
||||
|
||||
useAlert(t('SECURITY_SETTINGS.SAML.API.SUCCESS'));
|
||||
} else {
|
||||
// Disable/delete settings
|
||||
await samlSettingsAPI.delete();
|
||||
useAlert(t('SECURITY_SETTINGS.SAML.API.DISABLED'));
|
||||
}
|
||||
} catch (error) {
|
||||
useAlert(t('SECURITY_SETTINGS.SAML.API.ERROR'));
|
||||
throw error;
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!ssoUrl.value || !certificate.value) {
|
||||
useAlert(t('SECURITY_SETTINGS.SAML.VALIDATION.REQUIRED_FIELDS'));
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = {
|
||||
sso_url: ssoUrl.value,
|
||||
certificate: certificate.value,
|
||||
sp_entity_id: spEntityId.value,
|
||||
role_mappings: roleMappings.value,
|
||||
};
|
||||
|
||||
await saveSamlSettings(settings);
|
||||
};
|
||||
|
||||
const handleDisable = async () => {
|
||||
ssoUrl.value = '';
|
||||
certificate.value = '';
|
||||
spEntityId.value = '';
|
||||
roleMappings.value = {};
|
||||
|
||||
await saveSamlSettings({});
|
||||
};
|
||||
|
||||
const toggleSaml = async () => {
|
||||
if (!isEnabled.value) {
|
||||
await handleDisable();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadSamlSettings();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SectionLayout
|
||||
:title="t('SECURITY_SETTINGS.SAML.TITLE')"
|
||||
:description="t('SECURITY_SETTINGS.SAML.NOTE')"
|
||||
:hide-content="!hasFeature || !isEnabled || isLoading"
|
||||
with-border
|
||||
>
|
||||
<template #headerActions>
|
||||
<div class="flex justify-end">
|
||||
<Switch
|
||||
v-model="isEnabled"
|
||||
:disabled="isLoading"
|
||||
@change="toggleSaml"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form class="grid gap-5" @submit.prevent="handleSubmit">
|
||||
<WithLabel
|
||||
:label="t('SECURITY_SETTINGS.SAML.SSO_URL.LABEL')"
|
||||
:help-message="t('SECURITY_SETTINGS.SAML.SSO_URL.HELP')"
|
||||
required
|
||||
>
|
||||
<TextInput
|
||||
v-model="ssoUrl"
|
||||
class="w-full"
|
||||
type="url"
|
||||
:placeholder="t('SECURITY_SETTINGS.SAML.SSO_URL.PLACEHOLDER')"
|
||||
required
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel
|
||||
:label="t('SECURITY_SETTINGS.SAML.CERTIFICATE.LABEL')"
|
||||
:help-message="t('SECURITY_SETTINGS.SAML.CERTIFICATE.HELP')"
|
||||
required
|
||||
>
|
||||
<TextArea
|
||||
v-model="certificate"
|
||||
class="w-full"
|
||||
rows="8"
|
||||
:placeholder="t('SECURITY_SETTINGS.SAML.CERTIFICATE.PLACEHOLDER')"
|
||||
required
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel
|
||||
:label="t('SECURITY_SETTINGS.SAML.SP_ENTITY_ID.LABEL')"
|
||||
:help-message="t('SECURITY_SETTINGS.SAML.SP_ENTITY_ID.HELP')"
|
||||
>
|
||||
<TextInput
|
||||
v-model="spEntityId"
|
||||
class="w-full"
|
||||
:placeholder="t('SECURITY_SETTINGS.SAML.SP_ENTITY_ID.PLACEHOLDER')"
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<NextButton
|
||||
blue
|
||||
type="submit"
|
||||
:is-loading="isSubmitting"
|
||||
:label="t('SECURITY_SETTINGS.SAML.UPDATE_BUTTON')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</SectionLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||
import SettingsWrapper from '../SettingsWrapper.vue';
|
||||
import Index from './Index.vue';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/security'),
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
installationTypes: [
|
||||
INSTALLATION_TYPES.CLOUD,
|
||||
INSTALLATION_TYPES.ENTERPRISE,
|
||||
],
|
||||
},
|
||||
component: SettingsWrapper,
|
||||
props: {
|
||||
headerTitle: 'BILLING_SETTINGS.TITLE',
|
||||
icon: 'credit-card-person',
|
||||
showNewButton: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'security_settings_index',
|
||||
component: Index,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
installationTypes: [
|
||||
INSTALLATION_TYPES.CLOUD,
|
||||
INSTALLATION_TYPES.ENTERPRISE,
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -22,6 +22,7 @@ import sla from './sla/sla.routes';
|
||||
import teams from './teams/teams.routes';
|
||||
import customRoles from './customRoles/customRole.routes';
|
||||
import profile from './profile/profile.routes';
|
||||
import security from './security/security.routes';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
@@ -59,5 +60,6 @@ export default {
|
||||
...teams.routes,
|
||||
...customRoles.routes,
|
||||
...profile.routes,
|
||||
...security.routes,
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user