@nuxtagentic/identity 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,12 +34,7 @@ Identity image uploads share the existing `avatar` storage model and retain thei
34
34
  uploaded asset when saving fails, so retries do not upload the same file again.
35
35
  `useProfileNamePreview` keeps live name previews within Identity's state ownership.
36
36
 
37
- ## Single sign-on and provisioning
38
-
39
- With `auth.sso: true` (and `@better-auth/sso` installed), **Settings → Organization →
40
- Single sign-on** lets owners and admins register an OIDC or SAML identity provider and
41
- verify their email domain with a DNS TXT record. People on that domain choose **Use single
42
- sign-on** on the sign-in page and join the organization.
37
+ ## Directory provisioning
43
38
 
44
39
  With `auth.scim: true`, **Directory provisioning** mints a SCIM token and shows the base
45
40
  URL to paste into Okta, Entra ID or Google Workspace. Provisioned people join the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxtagentic/identity",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "module.ts",
@@ -21,15 +21,15 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@better-auth/passkey": "^1.7.7",
24
- "@cloudconfig/core": "0.2.0-beta.2",
25
- "@cloudconfig/nuxt": "0.2.0-beta.2",
24
+ "@cloudconfig/core": "0.2.0-beta.6",
25
+ "@cloudconfig/nuxt": "0.2.0-beta.6",
26
26
  "@iconify-json/lucide": "^1.2.132",
27
27
  "@iconify-json/simple-icons": "^1.2.96",
28
28
  "@nuxt/kit": "4.6.1",
29
29
  "@nuxt/ui": "^4.11.1",
30
30
  "@vueuse/core": "^14.4.0",
31
31
  "better-auth": "^1.7.7",
32
- "@nuxtagentic/kit": "0.1.1"
32
+ "@nuxtagentic/kit": "0.1.2"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "nuxt": "^4.6.1",
@@ -1,8 +1,17 @@
1
1
  <template>
2
- <span
3
- class="shrink-0 font-mono text-[22px] font-normal leading-none"
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ width="24"
5
+ height="24"
6
+ viewBox="48 48 160 160"
7
+ fill="none"
4
8
  aria-hidden="true"
9
+ class="shrink-0"
5
10
  >
6
- [c]
7
- </span>
11
+ <path
12
+ fill="#00DC82"
13
+ fill-rule="evenodd"
14
+ d="m95.847 148.193l52.206-52.594A31.86 31.86 0 0 1 144 80c0-17.673 14.327-32 32-32s32 14.327 32 32s-14.327 32-32 32a31.85 31.85 0 0 1-16.89-4.815l-52.068 51.699A31.85 31.85 0 0 1 112 176c0 17.673-14.327 32-32 32s-32-14.327-32-32s14.327-32 32-32a31.85 31.85 0 0 1 15.847 4.193M176 96c8.837 0 16-7.163 16-16s-7.163-16-16-16s-16 7.163-16 16s7.163 16 16 16m-96 96c8.837 0 16-7.163 16-16s-7.163-16-16-16s-16 7.163-16 16s7.163 16 16 16"
15
+ />
16
+ </svg>
8
17
  </template>
@@ -1,7 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { useCloudConfigAuth, useRuntimeConfig } from '#imports'
3
3
  import { ref } from 'vue'
4
- import { useSsoClient } from '../composables/useOrganizationAccess'
5
4
 
6
5
  const { title, description, label } = defineProps<{
7
6
  title: string
@@ -14,36 +13,8 @@ const isGoogleEnabled = (useRuntimeConfig().public.authProviders ?? ['github', '
14
13
  'google',
15
14
  )
16
15
  const isBusy = ref(false)
17
- const provider = ref<'google' | 'github' | 'sso'>('google')
16
+ const provider = ref<'google' | 'github'>('google')
18
17
  const error = ref('')
19
- const sso = useSsoClient()
20
- const showSso = ref(false)
21
- const email = ref('')
22
-
23
- /** Enterprise sign-in: the email domain picks the organization's identity provider. */
24
- async function continueWithSso() {
25
- if (isBusy.value || !email.value.includes('@')) {
26
- return
27
- }
28
- isBusy.value = true
29
- provider.value = 'sso'
30
- error.value = ''
31
- try {
32
- window.location.assign(
33
- await sso.signIn({
34
- email: email.value.trim(),
35
- callbackURL: '/dashboard',
36
- errorCallbackURL: '/sign-in',
37
- }),
38
- )
39
- } catch (cause) {
40
- error.value =
41
- cause instanceof Error && !/not found/i.test(cause.message)
42
- ? cause.message
43
- : 'No single sign-on is set up for that email domain.'
44
- isBusy.value = false
45
- }
46
- }
47
18
 
48
19
  async function continueWithProvider(value: 'google' | 'github') {
49
20
  if (isBusy.value) {
@@ -69,7 +40,7 @@ async function continueWithProvider(value: 'google' | 'github') {
69
40
 
70
41
  <template>
71
42
  <section class="flex w-full flex-col items-center text-center">
72
- <CloudMascot class="h-[90px] w-[100px]" />
43
+ <BrandMark class="size-20" />
73
44
  <h1
74
45
  class="mt-7 mb-2 text-[32px] leading-[1.15] font-normal tracking-[-0.03em] text-highlighted"
75
46
  >
@@ -98,38 +69,6 @@ async function continueWithProvider(value: 'google' | 'github') {
98
69
  :disabled="isBusy"
99
70
  @click="continueWithProvider('github')"
100
71
  />
101
- <form
102
- v-if="showSso"
103
- class="mt-3 flex w-full gap-2"
104
- @submit.prevent="continueWithSso"
105
- >
106
- <UInput
107
- v-model="email"
108
- type="email"
109
- required
110
- autocomplete="email"
111
- placeholder="you@company.com"
112
- aria-label="Work email"
113
- class="min-w-0 flex-1"
114
- :ui="{ base: 'h-10' }"
115
- />
116
- <UButton
117
- type="submit"
118
- label="Continue"
119
- color="neutral"
120
- :loading="isBusy && provider === 'sso'"
121
- :disabled="isBusy"
122
- class="h-10 px-4"
123
- />
124
- </form>
125
- <UButton
126
- v-else
127
- label="Use single sign-on"
128
- color="neutral"
129
- variant="link"
130
- class="mt-3 text-[13px]"
131
- @click="showSso = true"
132
- />
133
72
  <p
134
73
  v-if="error"
135
74
  role="alert"
@@ -6,7 +6,7 @@ import { initialsOf } from '../utils/format'
6
6
 
7
7
  const {
8
8
  name,
9
- identity: avatarIdentity,
9
+ identity: avatarIdentity = undefined,
10
10
  src = null,
11
11
  size = 'sm',
12
12
  } = defineProps<{
@@ -2,8 +2,6 @@
2
2
  import { ref, watch } from 'vue'
3
3
  import { useWorkspaces } from '../composables/useWorkspaces'
4
4
 
5
- import type { Organization } from '@cloudconfig/core/client'
6
-
7
5
  const { collapsed = false } = defineProps<{ collapsed?: boolean }>()
8
6
 
9
7
  const isOpen = ref(false)
@@ -11,14 +9,6 @@ const { activeOrganization, menu, switching, error, refresh } = await useWorkspa
11
9
  isOpen.value = false
12
10
  })
13
11
 
14
- function shouldShowDefaultLogo(organization: Organization | null) {
15
- return (
16
- !organization ||
17
- (!organization.logo &&
18
- (organization.name.toLowerCase() === 'cloudconfig' || organization.slug === 'cloudconfig'))
19
- )
20
- }
21
-
22
12
  watch(isOpen, (value) => {
23
13
  if (value) {
24
14
  void refresh()
@@ -57,7 +47,7 @@ watch(
57
47
  size="md"
58
48
  :square="collapsed"
59
49
  :trailing-icon="collapsed ? undefined : 'i-lucide-chevron-down'"
60
- :label="collapsed ? undefined : (activeOrganization?.name ?? 'Cloudconfig')"
50
+ :label="collapsed ? undefined : (activeOrganization?.name ?? 'Personal workspace')"
61
51
  class="h-10 w-auto max-w-full min-w-0 gap-2.5 rounded-lg py-0 pr-2.5 pl-2 text-[15px] leading-[normal] font-medium text-highlighted duration-160 hover:bg-(--gray-400) data-[state=isOpen]:bg-(--gray-400) motion-reduce:transition-none motion-reduce:active:scale-100"
62
52
  :ui="{
63
53
  label: 'min-w-0 truncate',
@@ -67,15 +57,16 @@ watch(
67
57
  aria-label="Open organization menu"
68
58
  >
69
59
  <template #leading>
70
- <CloudconfigLogo
71
- v-if="shouldShowDefaultLogo(activeOrganization)"
72
- class="size-6"
60
+ <UIcon
61
+ v-if="!activeOrganization"
62
+ name="i-lucide-user-round"
63
+ class="size-4 text-muted"
73
64
  />
74
65
  <UAvatar
75
66
  v-else
76
67
  :src="activeOrganization?.logo ?? undefined"
77
- :alt="activeOrganization?.name ?? 'Cloudconfig'"
78
- :text="(activeOrganization?.name ?? 'Cloudconfig').charAt(0).toUpperCase()"
68
+ :alt="activeOrganization?.name"
69
+ :text="activeOrganization?.name.charAt(0).toUpperCase()"
79
70
  size="xs"
80
71
  :ui="{
81
72
  root: 'rounded-md bg-(--gray-500)',
@@ -87,9 +78,10 @@ watch(
87
78
  </UButton>
88
79
 
89
80
  <template #workspace-leading="{ item }">
90
- <CloudconfigLogo
91
- v-if="shouldShowDefaultLogo(item.workspace ?? null)"
92
- class="size-5"
81
+ <UIcon
82
+ v-if="!item.workspace"
83
+ name="i-lucide-user-round"
84
+ class="size-4 text-muted"
93
85
  />
94
86
  <UAvatar
95
87
  v-else
@@ -7,7 +7,7 @@ const file = defineModel<File | null>('file', { required: true })
7
7
  const image = defineModel<string | null>('image', { required: true })
8
8
  const {
9
9
  name,
10
- identity,
10
+ identity = undefined,
11
11
  disabled,
12
12
  kind = 'avatar',
13
13
  } = defineProps<{
@@ -1,10 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  import { onMounted, ref, watch } from 'vue'
3
3
  import type { ScimToken } from '@cloudconfig/core/client'
4
- import { useOrganizationAdmin, useSsoClient } from '../../composables/useOrganizationAccess'
4
+ import { useOrganizationAdmin, useScimClient } from '../../composables/useOrganizationAccess'
5
5
  import { fieldUi } from '../../settings-ui'
6
6
 
7
- const sso = useSsoClient()
7
+ defineOptions({ name: 'SettingsProvisioning' })
8
+
9
+ const scim = useScimClient()
8
10
  const { organization, isAdmin } = useOrganizationAdmin()
9
11
  const tokens = ref<ScimToken[]>([])
10
12
  const baseUrl = ref('')
@@ -18,7 +20,7 @@ async function load() {
18
20
  return
19
21
  }
20
22
  try {
21
- const result = await sso.scim.list()
23
+ const result = await scim.list()
22
24
  tokens.value = result.tokens
23
25
  baseUrl.value = result.baseUrl
24
26
  } catch (cause) {
@@ -50,7 +52,7 @@ async function run(work: () => Promise<unknown>) {
50
52
 
51
53
  const create = () =>
52
54
  run(async () => {
53
- const secret = await sso.scim.create('Directory')
55
+ const secret = await scim.create('Directory')
54
56
  created.value = secret.token
55
57
  baseUrl.value = secret.baseUrl
56
58
  await load()
@@ -58,7 +60,7 @@ const create = () =>
58
60
 
59
61
  const revoke = (token: ScimToken) =>
60
62
  run(async () => {
61
- await sso.scim.revoke(token.uid)
63
+ await scim.revoke(token.uid)
62
64
  await load()
63
65
  })
64
66
 
@@ -299,7 +299,6 @@ onMounted(() => {
299
299
  />
300
300
  </SettingsSection>
301
301
  <template v-if="active">
302
- <SettingsSingleSignOn />
303
302
  <SettingsProvisioning />
304
303
  </template>
305
304
  </template>
@@ -1,13 +1,13 @@
1
1
  import { useCloudConfigOrganizations, useRequestEvent, useRuntimeConfig, useState } from '#imports'
2
2
  import { computed, ref, watch } from 'vue'
3
- import { createSsoClient, type Organization } from '@cloudconfig/core/client'
3
+ import { createScimClient, type Organization } from '@cloudconfig/core/client'
4
4
  import { useCurrentUser } from './useCurrentUser'
5
5
 
6
- /** The SSO and SCIM client, pointed at the auth mount and SSR-safe. */
7
- export function useSsoClient() {
6
+ /** The SCIM client, pointed at the auth mount and SSR-safe. */
7
+ export function useScimClient() {
8
8
  const config = useRuntimeConfig().public.cloudconfig as { authBasePath?: string } | undefined
9
9
  const event = useRequestEvent()
10
- return createSsoClient({
10
+ return createScimClient({
11
11
  baseUrl: config?.authBasePath ?? '/api/auth',
12
12
  // During SSR the request event's fetch keeps the caller's cookies.
13
13
  fetch: (input, init) => (event ? event.fetch(String(input), init) : fetch(input, init)),
@@ -74,7 +74,7 @@ export async function useWorkspaces(onSelected: () => void = () => {}) {
74
74
 
75
75
  function workspaceItem(workspace: Organization | null): WorkspaceMenuItem {
76
76
  return {
77
- label: workspace?.name ?? 'Cloudconfig',
77
+ label: workspace?.name ?? 'Personal workspace',
78
78
  workspace,
79
79
  slot: 'workspace',
80
80
  type: 'checkbox',
@@ -7,8 +7,8 @@
7
7
  to="/"
8
8
  class="flex items-center gap-2 text-sm font-medium text-muted transition-colors duration-140 hover:text-highlighted focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-primary motion-reduce:transition-none"
9
9
  >
10
- <BrandMark />
11
- <span class="mt-1">CloudConfig</span>
10
+ <BrandMark class="size-6" />
11
+ <span>Nuxt Agentic</span>
12
12
  </NuxtLink>
13
13
  </header>
14
14
  <main class="flex flex-1 items-center justify-center px-6 pt-6 pb-24">
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <GithubAuthPanel
3
- title="Sign in to CloudConfig"
3
+ title="Sign in to Nuxt Agentic"
4
4
  description="Connect your work in one place."
5
5
  label="Continue with GitHub"
6
6
  />
@@ -1,34 +0,0 @@
1
- <template>
2
- <svg
3
- viewBox="0 0 80 72"
4
- fill="none"
5
- shape-rendering="crispEdges"
6
- aria-hidden="true"
7
- class="shrink-0"
8
- >
9
- <path
10
- d="M26 8h24v4h8v8h4v8h8v4h4v20h-4v4H10v-4H6V36h4v-4h12V20h4Z"
11
- class="fill-(--ui-text-highlighted)"
12
- />
13
- <path
14
- d="M30 12h16v4h8v8h4v8h10v4h2v12h-4v4H14v-4h-4V38h4v-2h12V24h4Z"
15
- class="fill-(--ui-bg)"
16
- />
17
- <path
18
- d="M14 44h12v4h36v-4h8v4h-4v4H14Z"
19
- class="fill-(--gray-300)"
20
- />
21
- <path
22
- d="M32 30h4v8h-4zm16 0h4v8h-4zM38 42h10v4H38z"
23
- class="fill-(--ui-text-highlighted)"
24
- />
25
- <path
26
- d="M60 10h4v4h4v4h-4v4h-4v-4h-4v-4h4Z"
27
- class="fill-[#f97316]"
28
- />
29
- <path
30
- d="M28 64h24v4H28zM22 66h36v2H22z"
31
- class="fill-(--gray-600)"
32
- />
33
- </svg>
34
- </template>
@@ -1,34 +0,0 @@
1
- <template>
2
- <svg
3
- viewBox="0 0 80 72"
4
- fill="none"
5
- shape-rendering="crispEdges"
6
- aria-hidden="true"
7
- class="shrink-0"
8
- >
9
- <path
10
- d="M26 8h24v4h8v8h4v8h8v4h4v20h-4v4H10v-4H6V36h4v-4h12V20h4Z"
11
- class="fill-(--ui-text-highlighted)"
12
- />
13
- <path
14
- d="M30 12h16v4h8v8h4v8h10v4h2v12h-4v4H14v-4h-4V38h4v-2h12V24h4Z"
15
- class="fill-(--ui-bg)"
16
- />
17
- <path
18
- d="M14 44h12v4h36v-4h8v4h-4v4H14Z"
19
- class="fill-(--ui-bg-accented)"
20
- />
21
- <path
22
- d="M32 30h4v8h-4zm16 0h4v8h-4zM38 42h10v4H38z"
23
- class="fill-(--ui-text-highlighted)"
24
- />
25
- <path
26
- d="M60 10h4v4h4v4h-4v4h-4v-4h-4v-4h4Z"
27
- class="fill-orange-500"
28
- />
29
- <path
30
- d="M28 64h24v4H28zM22 66h36v2H22z"
31
- class="fill-(--ui-border-accented)"
32
- />
33
- </svg>
34
- </template>
@@ -1,396 +0,0 @@
1
- <script setup lang="ts">
2
- import { useRuntimeConfig } from '#imports'
3
- import { computed, onMounted, ref } from 'vue'
4
- import type { SsoProvider } from '@cloudconfig/core/client'
5
- import { useOrganizationAdmin, useSsoClient } from '../../composables/useOrganizationAccess'
6
- import { fieldUi, formFieldUi } from '../../settings-ui'
7
-
8
- const sso = useSsoClient()
9
- const { organization, isAdmin } = useOrganizationAdmin()
10
- const authBase = String(
11
- (useRuntimeConfig().public.cloudconfig as { authBasePath?: string } | undefined)?.authBasePath ??
12
- '/api/auth',
13
- )
14
- const providers = ref<SsoProvider[]>([])
15
- const available = ref(true)
16
- const isBusy = ref(false)
17
- const error = ref('')
18
- const adding = ref(false)
19
- const protocol = ref<'oidc' | 'saml'>('oidc')
20
- const form = ref({
21
- providerId: '',
22
- issuer: '',
23
- domain: '',
24
- clientId: '',
25
- clientSecret: '',
26
- entryPoint: '',
27
- cert: '',
28
- metadata: '',
29
- })
30
- const verification = ref<{ providerId: string; name: string; value: string }>()
31
- const origin = computed(() => (import.meta.client ? location.origin : ''))
32
- const own = computed(() =>
33
- providers.value.filter(
34
- (provider) => String(provider.organizationId) === String(organization.value?.id),
35
- ),
36
- )
37
-
38
- async function load() {
39
- try {
40
- providers.value = await sso.providers()
41
- } catch (cause) {
42
- const status = (cause as { status?: number }).status
43
- // Without the SSO plugin the routes do not exist; hide the section.
44
- if (status === 404) {
45
- available.value = false
46
- } else {
47
- error.value = cause instanceof Error ? cause.message : 'Could not load identity providers.'
48
- }
49
- }
50
- }
51
- onMounted(load)
52
-
53
- async function run(work: () => Promise<unknown>) {
54
- if (isBusy.value) {
55
- return
56
- }
57
- isBusy.value = true
58
- error.value = ''
59
- try {
60
- await work()
61
- } catch (cause) {
62
- error.value = cause instanceof Error ? cause.message : 'Something went wrong.'
63
- } finally {
64
- isBusy.value = false
65
- }
66
- }
67
-
68
- function slugify(value: string) {
69
- return value
70
- .toLowerCase()
71
- .replace(/[^a-z0-9]+/g, '-')
72
- .replace(/(^-|-$)/g, '')
73
- }
74
-
75
- async function register() {
76
- const organizationId = Number(organization.value?.id)
77
- if (!organizationId) {
78
- return
79
- }
80
- await run(async () => {
81
- const providerId = slugify(
82
- form.value.providerId || `${organization.value?.slug}-${protocol.value}`,
83
- )
84
- if (protocol.value === 'oidc') {
85
- await sso.registerOidc({
86
- providerId,
87
- issuer: form.value.issuer.trim(),
88
- domain: form.value.domain.trim().toLowerCase(),
89
- clientId: form.value.clientId.trim(),
90
- clientSecret: form.value.clientSecret.trim(),
91
- organizationId,
92
- })
93
- } else {
94
- await sso.registerSaml({
95
- providerId,
96
- issuer: form.value.issuer.trim(),
97
- domain: form.value.domain.trim().toLowerCase(),
98
- entryPoint: form.value.entryPoint.trim(),
99
- organizationId,
100
- ...(form.value.metadata.trim() ? { idpMetadata: form.value.metadata.trim() } : {}),
101
- ...(form.value.cert.trim() ? { cert: form.value.cert.trim() } : {}),
102
- })
103
- }
104
- adding.value = false
105
- form.value = {
106
- providerId: '',
107
- issuer: '',
108
- domain: '',
109
- clientId: '',
110
- clientSecret: '',
111
- entryPoint: '',
112
- cert: '',
113
- metadata: '',
114
- }
115
- await load()
116
- })
117
- }
118
-
119
- async function requestVerification(provider: SsoProvider) {
120
- await run(async () => {
121
- const domain = provider.domain.split(',')[0]!.trim()
122
- verification.value = {
123
- providerId: provider.providerId,
124
- ...(await sso.requestDomainVerification(provider.providerId, domain)),
125
- }
126
- })
127
- }
128
-
129
- async function verify(provider: SsoProvider) {
130
- await run(async () => {
131
- await sso.verifyDomain(provider.providerId)
132
- verification.value = undefined
133
- await load()
134
- })
135
- }
136
-
137
- async function remove(provider: SsoProvider) {
138
- await run(async () => {
139
- await sso.remove(provider.providerId)
140
- await load()
141
- })
142
- }
143
-
144
- async function copy(value: string) {
145
- error.value = ''
146
- try {
147
- await navigator.clipboard.writeText(value)
148
- } catch (cause) {
149
- error.value = cause instanceof Error ? cause.message : 'Could not copy to the clipboard.'
150
- }
151
- }
152
- </script>
153
-
154
- <template>
155
- <SettingsSection
156
- v-if="available && organization"
157
- title="Single sign-on"
158
- description="People on your verified email domain sign in through your identity provider and join this organization."
159
- >
160
- <p
161
- v-if="error"
162
- role="alert"
163
- class="text-[13px] text-error"
164
- >
165
- {{ error }}
166
- </p>
167
- <p
168
- v-if="!isAdmin"
169
- class="text-sm text-muted"
170
- >
171
- Organization owners and admins set up single sign-on.
172
- </p>
173
- <template v-else>
174
- <div
175
- v-for="provider in own"
176
- :key="provider.providerId"
177
- class="grid gap-2 rounded-md border border-default p-3"
178
- >
179
- <div class="flex flex-wrap items-center justify-between gap-2">
180
- <div class="min-w-0">
181
- <p class="truncate text-sm text-highlighted">{{ provider.issuer }}</p>
182
- <p class="mt-0.5 truncate text-[13px] text-muted">
183
- {{ provider.domain }} ·
184
- {{ provider.domainVerified ? 'domain verified' : 'domain not verified yet' }}
185
- </p>
186
- </div>
187
- <div class="flex shrink-0 gap-2">
188
- <UButton
189
- v-if="!provider.domainVerified"
190
- label="Verify domain"
191
- color="neutral"
192
- variant="outline"
193
- :disabled="isBusy"
194
- class="h-9 rounded-md px-3 text-sm font-normal"
195
- @click="requestVerification(provider)"
196
- />
197
- <UButton
198
- label="Remove"
199
- color="neutral"
200
- variant="ghost"
201
- :disabled="isBusy"
202
- class="h-9 rounded-md px-3 text-sm font-normal"
203
- @click="remove(provider)"
204
- />
205
- </div>
206
- </div>
207
- <div
208
- v-if="verification?.providerId === provider.providerId"
209
- class="grid gap-2"
210
- >
211
- <p class="text-[13px] text-muted">
212
- Add this TXT record at your DNS host, then verify. Sign-in through this provider starts
213
- working once the domain is verified.
214
- </p>
215
- <UInput
216
- :model-value="verification.name"
217
- readonly
218
- aria-label="TXT record name"
219
- :ui="fieldUi"
220
- />
221
- <UInput
222
- :model-value="verification.value"
223
- readonly
224
- aria-label="TXT record value"
225
- :ui="fieldUi"
226
- />
227
- <div class="flex gap-2">
228
- <UButton
229
- label="Copy value"
230
- color="neutral"
231
- variant="outline"
232
- size="sm"
233
- @click="copy(verification.value)"
234
- />
235
- <UButton
236
- label="Verify"
237
- color="neutral"
238
- size="sm"
239
- :loading="isBusy"
240
- @click="verify(provider)"
241
- />
242
- </div>
243
- </div>
244
- </div>
245
- <form
246
- v-if="adding"
247
- class="grid gap-4"
248
- @submit.prevent="register"
249
- >
250
- <div
251
- class="grid grid-cols-2 gap-1 rounded-lg border border-default p-0.5"
252
- role="radiogroup"
253
- aria-label="Protocol"
254
- >
255
- <UButton
256
- v-for="value in ['oidc', 'saml'] as const"
257
- :key="value"
258
- :label="value === 'oidc' ? 'OpenID Connect' : 'SAML 2.0'"
259
- color="neutral"
260
- :variant="protocol === value ? 'soft' : 'ghost'"
261
- role="radio"
262
- :aria-checked="protocol === value"
263
- class="h-8 justify-center rounded-md text-[13px]"
264
- @click="protocol = value"
265
- />
266
- </div>
267
- <UFormField
268
- label="Email domain"
269
- description="Comma-separate several domains served by the same provider."
270
- :ui="formFieldUi"
271
- >
272
- <UInput
273
- v-model="form.domain"
274
- required
275
- placeholder="acme.com"
276
- :ui="fieldUi"
277
- />
278
- </UFormField>
279
- <UFormField
280
- :label="protocol === 'oidc' ? 'Issuer URL' : 'IdP entity ID'"
281
- :ui="formFieldUi"
282
- >
283
- <UInput
284
- v-model="form.issuer"
285
- required
286
- :placeholder="
287
- protocol === 'oidc' ? 'https://acme.okta.com' : 'http://www.okta.com/exk…'
288
- "
289
- :ui="fieldUi"
290
- />
291
- </UFormField>
292
- <template v-if="protocol === 'oidc'">
293
- <UFormField
294
- label="Client ID"
295
- :ui="formFieldUi"
296
- >
297
- <UInput
298
- v-model="form.clientId"
299
- required
300
- :ui="fieldUi"
301
- />
302
- </UFormField>
303
- <UFormField
304
- label="Client secret"
305
- :ui="formFieldUi"
306
- >
307
- <UInput
308
- v-model="form.clientSecret"
309
- required
310
- type="password"
311
- autocomplete="off"
312
- :ui="fieldUi"
313
- />
314
- </UFormField>
315
- <p class="text-[13px] text-muted">
316
- Redirect URI for your identity provider:
317
- <code class="break-all text-highlighted">
318
- {{ origin }}{{ authBase }}/sso/callback/{{
319
- slugify(form.providerId || `${organization.slug}-oidc`)
320
- }}
321
- </code>
322
- </p>
323
- </template>
324
- <template v-else>
325
- <UFormField
326
- label="Single sign-on URL"
327
- :ui="formFieldUi"
328
- >
329
- <UInput
330
- v-model="form.entryPoint"
331
- required
332
- type="url"
333
- :ui="fieldUi"
334
- />
335
- </UFormField>
336
- <UFormField
337
- label="IdP metadata XML"
338
- description="Or paste the signing certificate below."
339
- :ui="formFieldUi"
340
- >
341
- <UTextarea
342
- v-model="form.metadata"
343
- :rows="3"
344
- class="w-full"
345
- />
346
- </UFormField>
347
- <UFormField
348
- label="Signing certificate (PEM)"
349
- :ui="formFieldUi"
350
- >
351
- <UTextarea
352
- v-model="form.cert"
353
- :rows="3"
354
- class="w-full"
355
- />
356
- </UFormField>
357
- <p class="text-[13px] text-muted">
358
- ACS URL for your identity provider:
359
- <code class="break-all text-highlighted">
360
- {{ origin }}{{ authBase }}/sso/saml2/sp/acs/{{
361
- slugify(form.providerId || `${organization.slug}-saml`)
362
- }}
363
- </code>
364
- </p>
365
- </template>
366
- <div class="flex flex-wrap gap-2">
367
- <UButton
368
- type="submit"
369
- label="Add identity provider"
370
- :loading="isBusy"
371
- class="h-10 rounded-md px-4 text-sm font-medium"
372
- />
373
- <UButton
374
- type="button"
375
- color="neutral"
376
- variant="outline"
377
- label="Cancel"
378
- :disabled="isBusy"
379
- class="h-10 rounded-md px-4 text-sm font-normal"
380
- @click="adding = false"
381
- />
382
- </div>
383
- </form>
384
- <UButton
385
- v-else
386
- type="button"
387
- color="neutral"
388
- variant="outline"
389
- icon="i-lucide-plus"
390
- label="Add identity provider"
391
- class="h-10 w-fit rounded-md px-4 text-sm font-normal"
392
- @click="adding = true"
393
- />
394
- </template>
395
- </SettingsSection>
396
- </template>