feat: add additional SAML information

This commit is contained in:
Shivam Mishra
2025-09-01 13:28:55 +05:30
parent 5227b4169f
commit f6b3cb6295
4 changed files with 150 additions and 13 deletions
@@ -390,16 +390,23 @@
"SAML": {
"TITLE": "SAML SSO",
"NOTE": "Configure SAML single sign-on for your account. Users will authenticate through your identity provider instead of using email/password.",
"ACS_URL": {
"LABEL": "ACS URL"
},
"SSO_URL": {
"LABEL": "SSO URL",
"HELP": "The URL where SAML authentication requests will be sent",
"PLACEHOLDER": "https://your-idp.com/saml/sso"
},
"CERTIFICATE": {
"LABEL": "X.509 Certificate",
"LABEL": "Signing certificate in PEM format",
"HELP": "The public certificate from your identity provider used to verify SAML responses",
"PLACEHOLDER": "-----BEGIN CERTIFICATE-----\nMIIC..."
},
"FINGERPRINT": {
"LABEL": "Fingerprint"
},
"COPY_SUCCESS": "Copied to clipboard",
"SP_ENTITY_ID": {
"LABEL": "Service Provider Entity ID",
"HELP": "Unique identifier for this application as a service provider. Leave blank to use default.",
@@ -426,6 +433,10 @@
"UPGRADE_PROMPT": "Upgrade your plan to get access to SAML single sign-on and other advanced features.",
"UPGRADE_NOW": "Upgrade now",
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
},
"ATTRIBUTE_MAPPING": {
"TITLE": "SAML Attribute Setup",
"DESCRIPTION": "The following attribute mappings must be configured in your identity provider"
}
}
},
@@ -0,0 +1,48 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const isExpanded = ref(false);
const toggleExpanded = () => {
isExpanded.value = !isExpanded.value;
};
</script>
<template>
<section
class="rounded-xl border border-n-weak bg-n-solid-1 w-full text-sm text-n-slate-12 mb-5 overflow-hidden"
>
<button
type="button"
class="w-full px-4 py-3 flex items-center justify-between text-left hover:bg-n-solid-2 transition-colors"
@click="toggleExpanded"
>
<h4 class="font-medium text-n-slate-12">
{{ t('SECURITY_SETTINGS.SAML.ATTRIBUTE_MAPPING.TITLE') }}
</h4>
<i
class="i-lucide-chevron-down transition-transform duration-200"
:class="{ 'rotate-180': isExpanded }"
/>
</button>
<div
class="transition-[height] duration-200 ease-in-out overflow-hidden"
:class="isExpanded ? 'h-auto' : 'h-0'"
>
<div class="px-4 pb-3">
<p class="text-n-slate-11 mb-2">
{{ t('SECURITY_SETTINGS.SAML.ATTRIBUTE_MAPPING.DESCRIPTION') }}
</p>
<!-- eslint-disable vue/no-bare-strings-in-template -->
<ul class="list-none text-n-slate-12 space-y-1">
<li><code class="px-1 rounded bg-n-slate-3">email</code></li>
<li><code class="px-1 rounded bg-n-slate-3">first_name</code></li>
<li><code class="px-1 rounded bg-n-slate-3">last_name</code></li>
</ul>
</div>
</div>
</section>
</template>
@@ -0,0 +1,70 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import { useAlert } from 'dashboard/composables';
import { useAccount } from 'dashboard/composables/useAccount';
import NextButton from 'next/button/Button.vue';
defineProps({
fingerprint: {
type: String,
default: '',
},
});
const { t } = useI18n();
const { accountId } = useAccount();
const acsUrl = computed(() => {
const currentHost = window.location.origin;
return `${currentHost}/omniauth/saml/callback?account_id=${accountId.value}`;
});
const handleCopy = async text => {
await copyTextToClipboard(text);
useAlert(t('SECURITY_SETTINGS.SAML.COPY_SUCCESS'));
};
</script>
<template>
<section
class="rounded-xl border border-n-weak bg-n-solid-1 w-full text-sm text-n-slate-12 divide-y divide-n-weak"
>
<div
v-if="fingerprint"
class="pl-4 pr-1 py-1 flex justify-between items-center"
>
<div class="flex">
<span class="text-n-slate-11 w-24">
{{ t('SECURITY_SETTINGS.SAML.FINGERPRINT.LABEL') }}
</span>
{{ fingerprint }}
</div>
<NextButton
type="button"
ghost
sm
slate
icon="i-lucide-copy"
@click="handleCopy(fingerprint)"
/>
</div>
<div class="pl-4 pr-1 py-1 flex justify-between items-center">
<div class="flex">
<span class="text-n-slate-11 w-24">
{{ t('SECURITY_SETTINGS.SAML.ACS_URL.LABEL') }}
</span>
{{ acsUrl }}
</div>
<NextButton
type="button"
ghost
sm
slate
icon="i-lucide-copy"
@click="handleCopy(acsUrl)"
/>
</div>
</section>
</template>
@@ -3,19 +3,23 @@ import { ref, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import { useAccount } from 'dashboard/composables/useAccount';
import samlSettingsAPI from 'dashboard/api/samlSettings';
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';
import SamlInfoSection from './SamlInfoSection.vue';
import SamlAttributeMap from './SamlAttributeMap.vue';
const { t } = useI18n();
const { isCloudFeatureEnabled } = useAccount();
const ssoUrl = ref('');
const certificate = ref('');
const fingerprint = ref('');
const spEntityId = ref('');
const roleMappings = ref({});
const isEnabled = ref(false);
@@ -37,6 +41,7 @@ const loadSamlSettings = async () => {
certificate.value = settings.certificate || '';
spEntityId.value = settings.sp_entity_id || '';
roleMappings.value = settings.role_mappings || {};
fingerprint.value = settings.fingerprint || '';
isEnabled.value = ssoUrl.value !== '';
}
} catch (error) {
@@ -129,6 +134,9 @@ onMounted(() => {
</div>
</template>
<SamlInfoSection class="mb-5" :fingerprint="fingerprint" />
<SamlAttributeMap class="mb-5" />
<form class="grid gap-5" @submit.prevent="handleSubmit">
<WithLabel
:label="t('SECURITY_SETTINGS.SAML.SSO_URL.LABEL')"
@@ -144,6 +152,17 @@ onMounted(() => {
/>
</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>
<WithLabel
:label="t('SECURITY_SETTINGS.SAML.CERTIFICATE.LABEL')"
:help-message="t('SECURITY_SETTINGS.SAML.CERTIFICATE.HELP')"
@@ -158,17 +177,6 @@ onMounted(() => {
/>
</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