@swiss-ai-hub/web 0.319.0 → 0.321.0
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/{pages/auth/login.vue → components/Auth/LoginPanel.vue} +3 -33
- package/components/FormKit/AgentSelector.vue +20 -7
- package/components/FormKit/CronInput.vue +208 -0
- package/components/FormKit/Repeater.vue +1 -4
- package/components/FormKit/VectorStoreInput.vue +43 -6
- package/components/Knowledge/Database/CreateModal.vue +315 -0
- package/components/Knowledge/Database/EmptyCard.vue +34 -0
- package/components/Knowledge/DeleteConfirmModal.vue +87 -0
- package/components/Knowledge/Document/List.vue +20 -9
- package/components/Knowledge/Namespace/Card.vue +18 -0
- package/components/Knowledge/Namespace/CreateModal.vue +11 -3
- package/components/Role/AccessCapabilities.vue +4 -4
- package/components/Role/AccessCapabilityGroup.vue +22 -6
- package/components/Role/AccessRulesEditor.vue +15 -12
- package/composables/app/useAppVersion.ts +37 -20
- package/composables/auth/useAuth.ts +3 -11
- package/composables/document/useCreateDatabase.ts +21 -0
- package/composables/document/useDeleteDatabase.ts +29 -0
- package/composables/document/useDeleteDocument.ts +1 -1
- package/composables/document/useDeleteDocuments.ts +1 -1
- package/composables/document/useDeleteNamespace.ts +30 -0
- package/composables/document/useIngestors.ts +23 -0
- package/composables/document/useScheduledDeletions.ts +16 -8
- package/composables/form/useCreateInstanceForm.ts +2 -2
- package/composables/form/useFormKitTransform.ts +44 -15
- package/formkit.config.ts +57 -3
- package/i18n/locales/de.yaml +84 -5
- package/i18n/locales/en.yaml +80 -3
- package/i18n/locales/fr.yaml +82 -3
- package/i18n/locales/it.yaml +84 -3
- package/middleware/agent-admin.ts +31 -0
- package/middleware/auth.global.ts +34 -16
- package/package.json +1 -1
- package/pages/[tenant]/service/agents.vue +3 -0
- package/pages/[tenant]/service/knowledge/[db]/[namespace]/[document_id].vue +12 -5
- package/pages/[tenant]/service/knowledge.vue +138 -0
- package/pages/auth/login/[idp].vue +57 -0
- package/pages/auth/login/index.vue +46 -0
- package/plugins/0.runtime-config.client.ts +5 -0
- package/plugins/oidc-client.ts +1 -1
- package/sdk/client/client/client.gen.ts +1 -3
- package/sdk/client/client/types.gen.ts +7 -13
- package/sdk/client/client/utils.gen.ts +1 -2
- package/sdk/client/core/queryKeySerializer.gen.ts +1 -6
- package/sdk/client/core/types.gen.ts +5 -3
- package/sdk/client/index.ts +47 -4
- package/sdk/client/schemas.gen.ts +1787 -780
- package/sdk/client/sdk.gen.ts +231 -1
- package/sdk/client/types.gen.ts +1103 -226
- package/plugins/keycloak-client.ts +0 -41
|
@@ -6,20 +6,42 @@ let lastSyncedTenant: string | null = null
|
|
|
6
6
|
|
|
7
7
|
const REDIRECT_KEY = 'aihub_redirect_after_login'
|
|
8
8
|
|
|
9
|
+
const normalize = (path: string) => (path.endsWith('/') ? path.slice(0, -1) : path)
|
|
10
|
+
|
|
11
|
+
const stripLocale = (path: string, localeCodes: string[]): string => {
|
|
12
|
+
const prefix = localeCodes.find(code => path === `/${code}` || path.startsWith(`/${code}/`))
|
|
13
|
+
return prefix ? path.slice(prefix.length + 1) : path
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The whole login subtree is anonymous, not just the login page itself:
|
|
18
|
+
* per-tenant login links live at `/auth/login/<idp-alias>`. No
|
|
19
|
+
* authenticated-only page may ever be nested below that path.
|
|
20
|
+
*
|
|
21
|
+
* The locale prefix is optional. This middleware runs before @nuxtjs/i18n's
|
|
22
|
+
* `locale-changing` middleware (Nuxt orders file-based global middleware ahead
|
|
23
|
+
* of plugin-registered ones), so bouncing a hand-distributed link that dropped
|
|
24
|
+
* `/en/` would strip the tenant before i18n ever restores the prefix.
|
|
25
|
+
*/
|
|
26
|
+
const isAnonymousPath = (path: string, localeCodes: string[]): boolean => {
|
|
27
|
+
const unprefixedPath = stripLocale(normalize(path), localeCodes)
|
|
28
|
+
|
|
29
|
+
return ['/auth/login', '/auth/callback', '/auth/renew'].includes(unprefixedPath)
|
|
30
|
+
|| unprefixedPath.startsWith('/auth/login/')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rememberRedirect = (fullPath: string, isAuthPath: boolean) => {
|
|
34
|
+
if (import.meta.client && fullPath !== '/' && !isAuthPath) {
|
|
35
|
+
sessionStorage.setItem(REDIRECT_KEY, fullPath)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
9
39
|
export default defineNuxtRouteMiddleware(async (to) => {
|
|
10
40
|
const { $auth, $i18n } = useNuxtApp()
|
|
11
41
|
const locale = $i18n.locale.value
|
|
42
|
+
const localeCodes = $i18n.locales.value.map(entry => entry.code)
|
|
12
43
|
|
|
13
|
-
|
|
14
|
-
`/${locale}/auth/login`,
|
|
15
|
-
`/${locale}/auth/callback`,
|
|
16
|
-
`/${locale}/auth/renew`,
|
|
17
|
-
]
|
|
18
|
-
|
|
19
|
-
// No auth check for public paths (normalize trailing slashes on both sides)
|
|
20
|
-
const normalize = (p: string) => (p.endsWith('/') ? p.slice(0, -1) : p)
|
|
21
|
-
const normalizedPath = normalize(to.path)
|
|
22
|
-
if (noAuthPaths.some(p => normalize(p) === normalizedPath)) {
|
|
44
|
+
if (isAnonymousPath(to.path, localeCodes)) {
|
|
23
45
|
return
|
|
24
46
|
}
|
|
25
47
|
|
|
@@ -29,9 +51,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
|
|
29
51
|
const isAuthPath = to.path.includes('/auth/')
|
|
30
52
|
|
|
31
53
|
if (!user) {
|
|
32
|
-
|
|
33
|
-
sessionStorage.setItem(REDIRECT_KEY, to.fullPath)
|
|
34
|
-
}
|
|
54
|
+
rememberRedirect(to.fullPath, isAuthPath)
|
|
35
55
|
return navigateTo(`/${locale}/auth/login`)
|
|
36
56
|
}
|
|
37
57
|
|
|
@@ -41,9 +61,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
|
|
41
61
|
}
|
|
42
62
|
catch {
|
|
43
63
|
await $auth.removeUser()
|
|
44
|
-
|
|
45
|
-
sessionStorage.setItem(REDIRECT_KEY, to.fullPath)
|
|
46
|
-
}
|
|
64
|
+
rememberRedirect(to.fullPath, isAuthPath)
|
|
47
65
|
return navigateTo(`/${locale}/auth/login`)
|
|
48
66
|
}
|
|
49
67
|
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"license": "AGPL-3.0-or-later",
|
|
4
4
|
"author": "bbv Software Services AG (https://www.bbv.ch)",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.321.0",
|
|
7
7
|
"description": "Swiss AI Hub - Admin & Management UI (Nuxt 3 layer)",
|
|
8
8
|
"main": "./nuxt.config.ts",
|
|
9
9
|
"repository": {
|
|
@@ -138,6 +138,9 @@ import { AgentConfigImportError } from '@core/composables/agent/useImportAgentIn
|
|
|
138
138
|
|
|
139
139
|
import type { FullAgentInstanceDto, WorkflowGraph } from '@core/sdk/client'
|
|
140
140
|
|
|
141
|
+
// Nuxt collects middleware from every matched record, so this guards the whole /service/agents subtree.
|
|
142
|
+
definePageMeta({ middleware: 'agent-admin' })
|
|
143
|
+
|
|
141
144
|
type AgentGroup = {
|
|
142
145
|
agentClass: string
|
|
143
146
|
name: string
|
|
@@ -82,20 +82,27 @@ const confirmDelete = () => {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
const handleDelete = async () => {
|
|
85
|
+
const database = route.params.db as string
|
|
86
|
+
const namespace = route.params.namespace as string
|
|
87
|
+
const documentId = route.params.document_id as string
|
|
88
|
+
|
|
85
89
|
try {
|
|
90
|
+
// Leave before deleting: the delete invalidates a key that is a prefix of this document's own query, so a
|
|
91
|
+
// still-mounted page refetches the document it just removed.
|
|
92
|
+
await router.push(tenantPath(`/service/knowledge/${database}/${namespace}`))
|
|
93
|
+
|
|
86
94
|
await deleteDocument({
|
|
87
95
|
tenantId: tenantId.value!,
|
|
88
|
-
database
|
|
89
|
-
namespace
|
|
90
|
-
documentId
|
|
96
|
+
database,
|
|
97
|
+
namespace,
|
|
98
|
+
documentId,
|
|
91
99
|
})
|
|
92
|
-
schedule([
|
|
100
|
+
schedule([documentId])
|
|
93
101
|
toast.add({
|
|
94
102
|
severity: 'success',
|
|
95
103
|
summary: t('document.delete.success'),
|
|
96
104
|
life: 3000,
|
|
97
105
|
})
|
|
98
|
-
router.push(tenantPath(`/service/knowledge/${route.params.db}/${route.params.namespace}`))
|
|
99
106
|
}
|
|
100
107
|
catch (error) {
|
|
101
108
|
toast.add({
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
:loading="databasesAreLoading"
|
|
6
6
|
>
|
|
7
7
|
<div class="flex flex-col gap-12">
|
|
8
|
+
<KnowledgeDatabaseEmptyCard @add="openNewDatabaseModal" />
|
|
8
9
|
<div
|
|
9
10
|
v-for="database in databases"
|
|
10
11
|
:key="database.name"
|
|
@@ -21,6 +22,19 @@
|
|
|
21
22
|
class="pi pi-lock-open text-surface-400 dark:text-surface-500"
|
|
22
23
|
:title="t('knowledge.manual_management.description')"
|
|
23
24
|
/>
|
|
25
|
+
<span class="text-xs text-surface-500 dark:text-surface-400">
|
|
26
|
+
{{ t('knowledge.pipeline', { name: capitalCase(database.ingestor) }) }}
|
|
27
|
+
</span>
|
|
28
|
+
<Button
|
|
29
|
+
v-if="database.deletable"
|
|
30
|
+
v-tooltip.top="t('knowledge.delete_database')"
|
|
31
|
+
icon="pi pi-trash"
|
|
32
|
+
rounded
|
|
33
|
+
text
|
|
34
|
+
size="small"
|
|
35
|
+
severity="danger"
|
|
36
|
+
@click="openDeleteDatabaseModal(database)"
|
|
37
|
+
/>
|
|
24
38
|
</div>
|
|
25
39
|
<div class="grid grid-cols-2 gap-4 2xl:grid-cols-2">
|
|
26
40
|
<KnowledgeNamespaceCard
|
|
@@ -31,6 +45,7 @@
|
|
|
31
45
|
@click="toNamespace(database.name, namespace)"
|
|
32
46
|
@upload="openUploadModal(database, namespace)"
|
|
33
47
|
@edit="openEditNamespaceModal(namespace)"
|
|
48
|
+
@delete="openDeleteNamespaceModal(database, namespace)"
|
|
34
49
|
/>
|
|
35
50
|
<KnowledgeNamespaceEmptyCard
|
|
36
51
|
v-if="!database.auto_sync"
|
|
@@ -63,6 +78,20 @@
|
|
|
63
78
|
:namespace="editingNamespace"
|
|
64
79
|
@success="handleUpdateSuccess"
|
|
65
80
|
/>
|
|
81
|
+
|
|
82
|
+
<KnowledgeDatabaseCreateModal
|
|
83
|
+
v-model="newDatabaseModalVisible"
|
|
84
|
+
@success="handleDatabaseCreationSuccess"
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
<KnowledgeDeleteConfirmModal
|
|
88
|
+
v-model:visible="deleteModalVisible"
|
|
89
|
+
:title="deleteTitle"
|
|
90
|
+
:warning="deleteWarning"
|
|
91
|
+
:expected-name="deleteExpectedName"
|
|
92
|
+
:is-deleting="isDeleting"
|
|
93
|
+
@confirm="handleConfirmDelete"
|
|
94
|
+
/>
|
|
66
95
|
</StructuralScreen>
|
|
67
96
|
</template>
|
|
68
97
|
|
|
@@ -71,12 +100,18 @@ import { capitalCase } from 'change-case'
|
|
|
71
100
|
|
|
72
101
|
import type { DatabaseDto, NamespaceDto } from '@core/sdk/client'
|
|
73
102
|
|
|
103
|
+
const route = useRoute()
|
|
74
104
|
const router = useRouter()
|
|
75
105
|
const tenantPath = useTenantPath()
|
|
76
106
|
const { t } = useI18n()
|
|
107
|
+
const toast = useToast()
|
|
108
|
+
const { tenantId } = useTenant()
|
|
77
109
|
|
|
78
110
|
const { databases, databasesAreLoading } = useDatabases()
|
|
79
111
|
|
|
112
|
+
const { deleteDatabase, isDeleting: isDeletingDatabase } = useDeleteDatabase()
|
|
113
|
+
const { deleteNamespace, isDeleting: isDeletingNamespace } = useDeleteNamespace()
|
|
114
|
+
|
|
80
115
|
const uploadModalVisible = ref(false)
|
|
81
116
|
const selectedDatabaseForUpload = ref('')
|
|
82
117
|
const selectedNamespaceForUpload = ref('')
|
|
@@ -89,6 +124,8 @@ const selectedDatabaseForNewNamespace = ref('')
|
|
|
89
124
|
const editNamespaceModalVisible = ref(false)
|
|
90
125
|
const editingNamespace = ref<NamespaceDto | null>(null)
|
|
91
126
|
|
|
127
|
+
const newDatabaseModalVisible = ref(false)
|
|
128
|
+
|
|
92
129
|
const toNamespace = (database_name: string, namespace: NamespaceDto) => {
|
|
93
130
|
router.push(tenantPath(`/service/knowledge/${database_name}/${namespace.name}`))
|
|
94
131
|
}
|
|
@@ -123,4 +160,105 @@ const openEditNamespaceModal = (namespace: NamespaceDto) => {
|
|
|
123
160
|
const handleUpdateSuccess = () => {
|
|
124
161
|
editingNamespace.value = null
|
|
125
162
|
}
|
|
163
|
+
|
|
164
|
+
const openNewDatabaseModal = () => {
|
|
165
|
+
newDatabaseModalVisible.value = true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const handleDatabaseCreationSuccess = () => {
|
|
169
|
+
newDatabaseModalVisible.value = false
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
type PendingDeletion
|
|
173
|
+
= | { type: 'database', database: string, name: string, count: number }
|
|
174
|
+
| { type: 'namespace', database: string, namespace: string, name: string, count: number }
|
|
175
|
+
|
|
176
|
+
const deleteModalVisible = ref(false)
|
|
177
|
+
const pendingDeletion = ref<PendingDeletion | null>(null)
|
|
178
|
+
|
|
179
|
+
const isDeleting = computed(() => isDeletingDatabase.value || isDeletingNamespace.value)
|
|
180
|
+
|
|
181
|
+
const deleteExpectedName = computed(() => pendingDeletion.value?.name ?? '')
|
|
182
|
+
|
|
183
|
+
const deleteTitle = computed(() =>
|
|
184
|
+
pendingDeletion.value?.type === 'database'
|
|
185
|
+
? t('knowledge.delete.database.title')
|
|
186
|
+
: t('knowledge.delete.namespace.title'),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
const deleteWarning = computed(() => {
|
|
190
|
+
const pending = pendingDeletion.value
|
|
191
|
+
if (!pending) return ''
|
|
192
|
+
const params = { name: pending.name, count: pending.count }
|
|
193
|
+
return pending.type === 'database'
|
|
194
|
+
? t('knowledge.delete.database.warning', params)
|
|
195
|
+
: t('knowledge.delete.namespace.warning', params)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
const documentCountFor = (database: DatabaseDto) =>
|
|
199
|
+
(database.namespaces ?? []).reduce((sum, namespace) => sum + (namespace.number_of_documents ?? 0), 0)
|
|
200
|
+
|
|
201
|
+
const openDeleteDatabaseModal = (database: DatabaseDto) => {
|
|
202
|
+
pendingDeletion.value = {
|
|
203
|
+
type: 'database',
|
|
204
|
+
database: database.name,
|
|
205
|
+
name: database.name,
|
|
206
|
+
count: documentCountFor(database),
|
|
207
|
+
}
|
|
208
|
+
deleteModalVisible.value = true
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const openDeleteNamespaceModal = (database: DatabaseDto, namespace: NamespaceDto) => {
|
|
212
|
+
pendingDeletion.value = {
|
|
213
|
+
type: 'namespace',
|
|
214
|
+
database: database.name,
|
|
215
|
+
namespace: namespace.name,
|
|
216
|
+
name: namespace.name,
|
|
217
|
+
count: namespace.number_of_documents ?? 0,
|
|
218
|
+
}
|
|
219
|
+
deleteModalVisible.value = true
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// The nested document route renders inside this page, so deleting what it points at leaves it mounted on a
|
|
223
|
+
// dead URL. Leave for the nearest surviving ancestor first: navigating deactivates the child's queries, so the
|
|
224
|
+
// delete's invalidation only marks them stale instead of refetching a resource the teardown job is purging.
|
|
225
|
+
// Both cases land on the database list — there is no /service/knowledge/[db] route.
|
|
226
|
+
const isViewingPendingDeletion = (pending: PendingDeletion) => {
|
|
227
|
+
if (route.params.db !== pending.database) return false
|
|
228
|
+
return pending.type === 'database' || route.params.namespace === pending.namespace
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const handleConfirmDelete = async () => {
|
|
232
|
+
const pending = pendingDeletion.value
|
|
233
|
+
if (!pending) return
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
if (isViewingPendingDeletion(pending)) {
|
|
237
|
+
await router.push(tenantPath('/service/knowledge'))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (pending.type === 'database') {
|
|
241
|
+
await deleteDatabase({ tenantId: tenantId.value!, database: pending.database })
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
await deleteNamespace({ tenantId: tenantId.value!, database: pending.database, namespace: pending.namespace })
|
|
245
|
+
}
|
|
246
|
+
toast.add({
|
|
247
|
+
severity: 'success',
|
|
248
|
+
summary: t('knowledge.delete.scheduled.summary'),
|
|
249
|
+
detail: t('knowledge.delete.scheduled.detail'),
|
|
250
|
+
life: 4000,
|
|
251
|
+
})
|
|
252
|
+
deleteModalVisible.value = false
|
|
253
|
+
pendingDeletion.value = null
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
toast.add({
|
|
257
|
+
severity: 'error',
|
|
258
|
+
summary: t('knowledge.delete.error.summary'),
|
|
259
|
+
detail: t('knowledge.delete.error.detail'),
|
|
260
|
+
life: 4000,
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
}
|
|
126
264
|
</script>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<AuthLoginPanel>
|
|
3
|
+
<template #heading>
|
|
4
|
+
<template v-if="provider">
|
|
5
|
+
{{ t('auth.login.welcomeProvider', { provider: provider.display_name }) }}
|
|
6
|
+
</template>
|
|
7
|
+
</template>
|
|
8
|
+
<template #message>
|
|
9
|
+
<template v-if="provider">
|
|
10
|
+
{{ t('auth.login.pleaseLoginWith', { provider: provider.display_name }) }}
|
|
11
|
+
</template>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<Button
|
|
15
|
+
v-if="provider"
|
|
16
|
+
:label="t('auth.login.loginWith', { provider: provider.display_name })"
|
|
17
|
+
:icon="`pi ${provider.icon}`"
|
|
18
|
+
icon-pos="right"
|
|
19
|
+
class="!bg-white !text-black"
|
|
20
|
+
@click="login(provider.alias)"
|
|
21
|
+
/>
|
|
22
|
+
<ProgressSpinner
|
|
23
|
+
v-else
|
|
24
|
+
class="!h-8 !w-8"
|
|
25
|
+
/>
|
|
26
|
+
</AuthLoginPanel>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
definePageMeta({
|
|
31
|
+
layout: 'anonymous',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const { t, locale } = useI18n()
|
|
35
|
+
const route = useRoute()
|
|
36
|
+
const { login } = useAuth()
|
|
37
|
+
const { authProviders, isLoading } = useAuthProviders()
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The empty alias belongs to the synthetic "Keycloak" entry, which has no
|
|
41
|
+
* kc_idp_hint and must therefore not be addressable through a tenant link.
|
|
42
|
+
*/
|
|
43
|
+
const requestedAlias = computed(() => String(route.params.idp ?? ''))
|
|
44
|
+
const provider = computed(() =>
|
|
45
|
+
requestedAlias.value
|
|
46
|
+
? authProviders.value?.find(candidate => candidate.alias === requestedAlias.value)
|
|
47
|
+
: undefined,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// Only decide once the query settled — an unknown, disabled or hidden alias
|
|
51
|
+
// (and a failed provider request) falls back to the all-providers page.
|
|
52
|
+
watch([isLoading, provider], ([providersLoading, matchedProvider]) => {
|
|
53
|
+
if (!providersLoading && !matchedProvider) {
|
|
54
|
+
navigateTo(`/${locale.value}/auth/login`, { replace: true })
|
|
55
|
+
}
|
|
56
|
+
}, { immediate: true })
|
|
57
|
+
</script>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<AuthLoginPanel>
|
|
3
|
+
<template #heading>
|
|
4
|
+
{{ t('auth.login.welcome', { companyName }) }}
|
|
5
|
+
</template>
|
|
6
|
+
<template #message>
|
|
7
|
+
{{ t('auth.login.pleaseLogin') }}
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<ProgressSpinner
|
|
11
|
+
v-if="isLoading"
|
|
12
|
+
class="!h-8 !w-8"
|
|
13
|
+
/>
|
|
14
|
+
<template v-else>
|
|
15
|
+
<Button
|
|
16
|
+
v-for="idp in authProviders ?? []"
|
|
17
|
+
:key="idp.alias"
|
|
18
|
+
:label="t('auth.login.loginWith', { provider: idp.display_name })"
|
|
19
|
+
:icon="`pi ${idp.icon}`"
|
|
20
|
+
icon-pos="right"
|
|
21
|
+
class="!bg-white !text-black"
|
|
22
|
+
@click="login(idp.alias || undefined)"
|
|
23
|
+
/>
|
|
24
|
+
<Button
|
|
25
|
+
v-if="(authProviders?.length ?? 0) === 0"
|
|
26
|
+
:label="t('auth.login.title')"
|
|
27
|
+
icon="pi pi-sign-in"
|
|
28
|
+
icon-pos="right"
|
|
29
|
+
class="!bg-white !text-black"
|
|
30
|
+
@click="login()"
|
|
31
|
+
/>
|
|
32
|
+
</template>
|
|
33
|
+
</AuthLoginPanel>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
definePageMeta({
|
|
38
|
+
layout: 'anonymous',
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const { t } = useI18n()
|
|
42
|
+
const { login } = useAuth()
|
|
43
|
+
const { authProviders, isLoading } = useAuthProviders()
|
|
44
|
+
|
|
45
|
+
const companyName = 'bbv Software Services AG'
|
|
46
|
+
</script>
|
|
@@ -50,6 +50,11 @@ export default defineNuxtPlugin(() => {
|
|
|
50
50
|
assign(group('sysadmin'), 'url', injected.SYSADMIN_URL)
|
|
51
51
|
assign(group('mainApp'), 'url', injected.MAIN_APP_URL)
|
|
52
52
|
if (injected.API_BASE_URL) publicConfig.apiBaseUrl = injected.API_BASE_URL
|
|
53
|
+
// The version actually deployed. A release is promoted by retagging the exact
|
|
54
|
+
// `-rc.N` build that was tested, so whatever was baked into the bundle at build
|
|
55
|
+
// time keeps the candidate's name — the deployment is the only party that knows
|
|
56
|
+
// the tag it pulled. Falls back to the baked value when nothing is injected.
|
|
57
|
+
if (injected.APP_VERSION) publicConfig.appVersion = injected.APP_VERSION
|
|
53
58
|
|
|
54
59
|
isConfigLoaded.value = true
|
|
55
60
|
})
|
package/plugins/oidc-client.ts
CHANGED
|
@@ -12,7 +12,7 @@ export default defineNuxtPlugin(async ({ $i18n, $router }) => {
|
|
|
12
12
|
client_id: config.public.oidc.clientId,
|
|
13
13
|
redirect_uri: `${globalThis.location.origin}/${$i18n.locale.value}/auth/callback`,
|
|
14
14
|
silent_redirect_uri: `${globalThis.location.origin}/${$i18n.locale.value}/auth/renew`,
|
|
15
|
-
post_logout_redirect_uri: globalThis.location.origin
|
|
15
|
+
post_logout_redirect_uri: `${globalThis.location.origin}/${$i18n.locale.value}/auth/login`,
|
|
16
16
|
response_type: 'code',
|
|
17
17
|
scope: 'openid profile email',
|
|
18
18
|
filterProtocolClaims: true,
|
|
@@ -197,9 +197,7 @@ export const createClient = (config: Config = {}): Client => {
|
|
|
197
197
|
method,
|
|
198
198
|
onRequest: undefined,
|
|
199
199
|
serializedBody: getValidRequestBody(opts) as
|
|
200
|
-
|
|
|
201
|
-
| null
|
|
202
|
-
| undefined,
|
|
200
|
+
BodyInit | null | undefined,
|
|
203
201
|
signal: unwrapRefs(opts.signal) as AbortSignal,
|
|
204
202
|
url,
|
|
205
203
|
});
|
|
@@ -33,14 +33,12 @@ export type QuerySerializer = (
|
|
|
33
33
|
|
|
34
34
|
type WithRefs<TData> = {
|
|
35
35
|
[K in keyof TData]: NonNullable<TData[K]> extends object
|
|
36
|
-
?
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
| Ref<NonNullable<TData[K]>>
|
|
43
|
-
| Extract<TData[K], null>;
|
|
36
|
+
? | WithRefs<NonNullable<TData[K]>>
|
|
37
|
+
| Ref<NonNullable<TData[K]>>
|
|
38
|
+
| Extract<TData[K], null>
|
|
39
|
+
: | NonNullable<TData[K]>
|
|
40
|
+
| Ref<NonNullable<TData[K]>>
|
|
41
|
+
| Extract<TData[K], null>;
|
|
44
42
|
};
|
|
45
43
|
|
|
46
44
|
// copied from Nuxt
|
|
@@ -206,8 +204,4 @@ type FetchOptions<TData> = Omit<
|
|
|
206
204
|
>;
|
|
207
205
|
|
|
208
206
|
export type Composable =
|
|
209
|
-
|
|
210
|
-
| "useAsyncData"
|
|
211
|
-
| "useFetch"
|
|
212
|
-
| "useLazyAsyncData"
|
|
213
|
-
| "useLazyFetch";
|
|
207
|
+
"$fetch" | "useAsyncData" | "useFetch" | "useLazyAsyncData" | "useLazyFetch";
|
|
@@ -195,8 +195,7 @@ export const setAuthParams = async ({
|
|
|
195
195
|
options.query = {};
|
|
196
196
|
}
|
|
197
197
|
const queryValue = toValue(options.query) as
|
|
198
|
-
|
|
199
|
-
| undefined;
|
|
198
|
+
Record<string, unknown> | undefined;
|
|
200
199
|
if (queryValue) {
|
|
201
200
|
queryValue[name] = token;
|
|
202
201
|
}
|
|
@@ -4,12 +4,7 @@
|
|
|
4
4
|
* JSON-friendly union that mirrors what Pinia Colada can hash.
|
|
5
5
|
*/
|
|
6
6
|
export type JsonValue =
|
|
7
|
-
|
|
|
8
|
-
| string
|
|
9
|
-
| number
|
|
10
|
-
| boolean
|
|
11
|
-
| JsonValue[]
|
|
12
|
-
| { [key: string]: JsonValue };
|
|
7
|
+
null | string | number | boolean | JsonValue[] | { [key: string]: JsonValue };
|
|
13
8
|
|
|
14
9
|
/**
|
|
15
10
|
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
|
|
@@ -112,7 +112,9 @@ type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
|
|
|
112
112
|
: false;
|
|
113
113
|
|
|
114
114
|
export type OmitNever<T extends Record<string, unknown>> = {
|
|
115
|
-
[
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
[
|
|
116
|
+
K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
|
|
117
|
+
? never
|
|
118
|
+
: K
|
|
119
|
+
]: T[K];
|
|
118
120
|
};
|