mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-03-02 22:19:40 +00:00
Revamp OIDC auth: remove Passport wrapper, add schema-driven settings UI
- Remove Passport.js wrapper from OIDC auth, use openid-client directly - Add schema-driven OIDC settings UI (OidcSettingsSchema.js drives form rendering) - Add group mapping with KeyValueEditor (explicit mapping or legacy direct name match) - Add scopes configuration (authOpenIDScopes) - Add verified email enforcement option (authOpenIDRequireVerifiedEmail) - Fix group claim validation rejecting URN-style claims (#4744) - Add auto-discover endpoint for OIDC provider configuration - Store oidcIdToken in sessions table instead of cookie - Add AuthError class for structured error handling in auth flows - Migration v2.33.0 adds oidcIdToken column and new settings fields
This commit is contained in:
parent
fe13456a2b
commit
33bee70a12
16 changed files with 1554 additions and 571 deletions
79
client/components/app/KeyValueEditor.vue
Normal file
79
client/components/app/KeyValueEditor.vue
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<div class="w-full">
|
||||
<p v-if="label" class="text-sm font-semibold px-1 mb-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
||||
<div v-for="(entry, index) in entries" :key="index" class="flex items-center gap-2 mb-2">
|
||||
<div class="flex-grow">
|
||||
<input type="text" :value="entry.key" :disabled="disabled" class="w-full rounded-sm bg-primary border border-gray-600 text-sm px-3 py-2" placeholder="Group name" @input="updateKey(index, $event.target.value)" />
|
||||
</div>
|
||||
<div class="w-32">
|
||||
<select :value="entry.value" :disabled="disabled" class="w-full rounded-sm bg-primary border border-gray-600 text-sm px-2 py-2" @change="updateValue(index, $event.target.value)">
|
||||
<option v-for="opt in valueOptions" :key="opt" :value="opt">{{ opt }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" :disabled="disabled" class="text-gray-400 hover:text-error p-1" @click="removeEntry(index)">
|
||||
<span class="material-symbols text-xl">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" :disabled="disabled" class="flex items-center text-sm text-gray-300 hover:text-white" @click="addEntry">
|
||||
<span class="material-symbols text-lg mr-1">add</span>
|
||||
<span>Add mapping</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
valueOptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
label: String,
|
||||
disabled: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
entries: Object.entries(this.value || {}).map(([key, value]) => ({ key, value }))
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler(newVal) {
|
||||
this.entries = Object.entries(newVal || {}).map(([key, value]) => ({ key, value }))
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
emitUpdate() {
|
||||
const obj = {}
|
||||
for (const entry of this.entries) {
|
||||
if (entry.key) {
|
||||
obj[entry.key] = entry.value
|
||||
}
|
||||
}
|
||||
this.$emit('input', obj)
|
||||
},
|
||||
updateKey(index, newKey) {
|
||||
this.$set(this.entries, index, { ...this.entries[index], key: newKey })
|
||||
this.emitUpdate()
|
||||
},
|
||||
updateValue(index, newValue) {
|
||||
this.$set(this.entries, index, { ...this.entries[index], value: newValue })
|
||||
this.emitUpdate()
|
||||
},
|
||||
removeEntry(index) {
|
||||
this.entries.splice(index, 1)
|
||||
this.emitUpdate()
|
||||
},
|
||||
addEntry() {
|
||||
this.entries.push({ key: '', value: this.valueOptions[0] || '' })
|
||||
this.emitUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
102
client/components/app/OidcSettings.vue
Normal file
102
client/components/app/OidcSettings.vue
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<template>
|
||||
<div class="w-full">
|
||||
<div v-for="group in sortedGroups" :key="group.id" class="mb-4">
|
||||
<p class="text-sm font-semibold text-gray-200 uppercase tracking-wide mb-2 px-1">{{ group.label }}</p>
|
||||
<div class="flex flex-wrap">
|
||||
<template v-for="field in fieldsForGroup(group.id)">
|
||||
<!-- Action button (e.g., Auto-populate) -->
|
||||
<div v-if="field.type === 'action'" :key="field.key" class="w-36 mx-1 mt-[1.375rem] mb-2">
|
||||
<ui-btn class="h-[2.375rem] text-sm inline-flex items-center justify-center w-full" type="button" :padding-y="0" :padding-x="4" :disabled="isFieldDisabled(field)" @click.stop="$emit('action', field.key)">
|
||||
<span class="material-symbols text-base">auto_fix_high</span>
|
||||
<span class="whitespace-nowrap break-keep pl-1">{{ field.label }}</span>
|
||||
</ui-btn>
|
||||
</div>
|
||||
|
||||
<!-- Text input -->
|
||||
<ui-text-input-with-label v-else-if="field.type === 'text'" :key="field.key" :value="values[field.key]" :disabled="disabled || isFieldDisabled(field)" :label="field.label" class="mb-2" @input="onFieldChange(field.key, $event)" />
|
||||
|
||||
<!-- Password input -->
|
||||
<ui-text-input-with-label v-else-if="field.type === 'password'" :key="field.key" :value="values[field.key]" :disabled="disabled || isFieldDisabled(field)" :label="field.label" type="password" class="mb-2" @input="onFieldChange(field.key, $event)" />
|
||||
|
||||
<!-- Boolean toggle -->
|
||||
<div v-else-if="field.type === 'boolean'" :key="field.key" class="flex items-center py-4 px-1 w-full">
|
||||
<ui-toggle-switch :value="!!values[field.key]" :disabled="disabled || isFieldDisabled(field)" @input="onFieldChange(field.key, $event)" />
|
||||
<p class="pl-4 whitespace-nowrap">{{ field.label }}</p>
|
||||
<p v-if="field.description" class="pl-4 text-sm text-gray-300">{{ field.description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Select dropdown -->
|
||||
<div v-else-if="field.type === 'select'" :key="field.key" class="flex sm:items-center flex-col sm:flex-row pt-1 mb-2">
|
||||
<div class="w-44">
|
||||
<ui-dropdown :value="values[field.key]" small :items="getDropdownItems(field)" :label="field.label" :disabled="disabled || isFieldDisabled(field)" @input="onFieldChange(field.key, $event)" />
|
||||
</div>
|
||||
<p v-if="field.description" class="sm:pl-4 text-sm text-gray-300 mt-2 sm:mt-5">{{ field.description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Array (multi-select) -->
|
||||
<div v-else-if="field.type === 'array'" :key="field.key" class="w-full mb-2">
|
||||
<ui-multi-select :value="values[field.key] || []" :items="values[field.key] || []" :label="field.label" :disabled="disabled || isFieldDisabled(field)" :menuDisabled="true" @input="onFieldChange(field.key, $event)" />
|
||||
<p v-if="field.description" class="sm:pl-4 text-sm text-gray-300 mb-2">{{ field.description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Key-value editor -->
|
||||
<div v-else-if="field.type === 'keyvalue'" :key="field.key" class="w-full mb-2">
|
||||
<app-key-value-editor :value="values[field.key] || {}" :value-options="field.valueOptions || []" :label="field.label" :disabled="disabled || isFieldDisabled(field)" @input="onFieldChange(field.key, $event)" />
|
||||
<p v-if="field.description" class="sm:pl-4 text-sm text-gray-300 mt-1">{{ field.description }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
schema: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
groups: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
values: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
schemaOverrides: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
disabled: Boolean
|
||||
},
|
||||
computed: {
|
||||
sortedGroups() {
|
||||
return [...this.groups].sort((a, b) => a.order - b.order)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fieldsForGroup(groupId) {
|
||||
return this.schema.filter((f) => f.group === groupId).sort((a, b) => a.order - b.order)
|
||||
},
|
||||
isFieldDisabled(field) {
|
||||
if (!field.dependsOn) return false
|
||||
const depValue = this.values[field.dependsOn]
|
||||
return !depValue
|
||||
},
|
||||
getDropdownItems(field) {
|
||||
// Use schema overrides if available (e.g., from discover)
|
||||
const override = this.schemaOverrides[field.key]
|
||||
const options = override?.options || field.options || []
|
||||
return options.map((opt) => ({
|
||||
text: opt.label,
|
||||
value: opt.value
|
||||
}))
|
||||
},
|
||||
onFieldChange(key, value) {
|
||||
this.$emit('update', { key, value })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue