Initial Version
This commit is contained in:
427
src/components/ConfigPanel.vue
Normal file
427
src/components/ConfigPanel.vue
Normal file
@@ -0,0 +1,427 @@
|
||||
<template>
|
||||
<div class="config-panel">
|
||||
<h3>OpenID Connect Configuration</h3>
|
||||
|
||||
<div class="config-section">
|
||||
<p class="description">
|
||||
Configure your OIDC identity provider settings. This provider enables single sign-on (SSO)
|
||||
with services like Google, Azure AD, Okta, Keycloak, and other OIDC-compliant identity providers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<h4>Provider Settings</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="enabled">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
v-model="config.enabled"
|
||||
@change="emitUpdate"
|
||||
/>
|
||||
Enable Provider
|
||||
</label>
|
||||
<p class="help-text">Allow users to authenticate with this OIDC provider</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="priority">Display Priority</label>
|
||||
<input
|
||||
type="number"
|
||||
id="priority"
|
||||
v-model.number="config.priority"
|
||||
min="1"
|
||||
max="100"
|
||||
@change="emitUpdate"
|
||||
/>
|
||||
<p class="help-text">Lower numbers appear first on the login page</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="label">Button Label</label>
|
||||
<input
|
||||
type="text"
|
||||
id="label"
|
||||
v-model="config.label"
|
||||
placeholder="Sign in with SSO"
|
||||
@input="emitUpdate"
|
||||
/>
|
||||
<p class="help-text">Text displayed on the SSO login button</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<h4>OIDC Configuration</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="issuer">Issuer URL *</label>
|
||||
<input
|
||||
type="url"
|
||||
id="issuer"
|
||||
v-model="config.config.issuer"
|
||||
placeholder="https://accounts.google.com"
|
||||
required
|
||||
@input="emitUpdate"
|
||||
/>
|
||||
<p class="help-text">
|
||||
The OIDC issuer URL. Common values:
|
||||
<br>• Google: https://accounts.google.com
|
||||
<br>• Azure AD: https://login.microsoftonline.com/{tenant}/v2.0
|
||||
<br>• Okta: https://{domain}.okta.com
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="client_id">Client ID *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="client_id"
|
||||
v-model="config.config.client_id"
|
||||
placeholder="your-client-id"
|
||||
required
|
||||
@input="emitUpdate"
|
||||
/>
|
||||
<p class="help-text">OAuth2 client ID from your identity provider</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="client_secret">Client Secret *</label>
|
||||
<input
|
||||
type="password"
|
||||
id="client_secret"
|
||||
v-model="config.config.client_secret"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
@input="emitUpdate"
|
||||
/>
|
||||
<p class="help-text">OAuth2 client secret from your identity provider</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="scopes">Scopes</label>
|
||||
<input
|
||||
type="text"
|
||||
id="scopes"
|
||||
v-model="scopesString"
|
||||
placeholder="openid email profile"
|
||||
@input="updateScopes"
|
||||
/>
|
||||
<p class="help-text">Space-separated list of OIDC scopes to request</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<h4>Provisioning</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="provisioning">Provisioning Mode</label>
|
||||
<select id="provisioning" v-model="config.provisioning" @change="emitUpdate">
|
||||
<option value="manual">Manual - Admin creates users before first login</option>
|
||||
<option value="auto">Automatic - Create users on first login</option>
|
||||
</select>
|
||||
<p class="help-text">
|
||||
Automatic provisioning creates local user accounts when users authenticate for the first time.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" v-if="config.provisioning === 'auto'">
|
||||
<label for="default_roles">Default Roles</label>
|
||||
<input
|
||||
type="text"
|
||||
id="default_roles"
|
||||
v-model="defaultRolesString"
|
||||
placeholder="role-id-1, role-id-2"
|
||||
@input="updateDefaultRoles"
|
||||
/>
|
||||
<p class="help-text">Comma-separated role IDs to assign to auto-provisioned users</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<h4>Attribute Mapping</h4>
|
||||
<p class="help-text">Map OIDC claims to user profile fields</p>
|
||||
|
||||
<div class="attribute-map">
|
||||
<div class="attribute-row" v-for="(target, source) in config.config.attribute_map" :key="source">
|
||||
<input
|
||||
type="text"
|
||||
:value="source"
|
||||
placeholder="OIDC claim"
|
||||
@input="updateAttributeSource($event, source)"
|
||||
/>
|
||||
<span class="arrow">→</span>
|
||||
<input
|
||||
type="text"
|
||||
:value="target"
|
||||
placeholder="User field"
|
||||
@input="updateAttributeTarget($event, source)"
|
||||
/>
|
||||
<button type="button" class="btn-remove" @click="removeAttribute(source)">×</button>
|
||||
</div>
|
||||
<button type="button" class="btn-add" @click="addAttribute">+ Add Mapping</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-section callback-info">
|
||||
<h4>Callback URL</h4>
|
||||
<p class="help-text">
|
||||
Configure this URL as the redirect/callback URI in your identity provider:
|
||||
</p>
|
||||
<code class="callback-url">{{ callbackUrl }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed, watch } from 'vue'
|
||||
|
||||
interface OidcConfig {
|
||||
issuer: string
|
||||
client_id: string
|
||||
client_secret: string
|
||||
scopes: string[]
|
||||
attribute_map: Record<string, string>
|
||||
default_roles: string[]
|
||||
}
|
||||
|
||||
interface ProviderConfig {
|
||||
enabled: boolean
|
||||
priority: number
|
||||
label: string
|
||||
provisioning: 'manual' | 'auto'
|
||||
config: OidcConfig
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Partial<ProviderConfig>
|
||||
tenantDomain?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: ProviderConfig): void
|
||||
}>()
|
||||
|
||||
const defaultAttributeMap: Record<string, string> = {
|
||||
'email': 'identity',
|
||||
'name': 'label',
|
||||
'given_name': 'profile.name_first',
|
||||
'family_name': 'profile.name_last'
|
||||
}
|
||||
|
||||
const config = reactive<ProviderConfig>({
|
||||
enabled: props.modelValue.enabled ?? false,
|
||||
priority: props.modelValue.priority ?? 2,
|
||||
label: props.modelValue.label ?? 'Sign in with SSO',
|
||||
provisioning: props.modelValue.provisioning ?? 'manual',
|
||||
config: {
|
||||
issuer: props.modelValue.config?.issuer ?? '',
|
||||
client_id: props.modelValue.config?.client_id ?? '',
|
||||
client_secret: props.modelValue.config?.client_secret ?? '',
|
||||
scopes: props.modelValue.config?.scopes ?? ['openid', 'email', 'profile'],
|
||||
attribute_map: props.modelValue.config?.attribute_map ?? { ...defaultAttributeMap },
|
||||
default_roles: props.modelValue.config?.default_roles ?? [],
|
||||
}
|
||||
})
|
||||
|
||||
const scopesString = computed({
|
||||
get: () => config.config.scopes.join(' '),
|
||||
set: () => {} // Handled by updateScopes
|
||||
})
|
||||
|
||||
const defaultRolesString = computed({
|
||||
get: () => config.config.default_roles.join(', '),
|
||||
set: () => {} // Handled by updateDefaultRoles
|
||||
})
|
||||
|
||||
const callbackUrl = computed(() => {
|
||||
const domain = props.tenantDomain || window.location.origin
|
||||
return `${domain}/security/provider/oidc/callback`
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
config.enabled = newValue.enabled ?? false
|
||||
config.priority = newValue.priority ?? 2
|
||||
config.label = newValue.label ?? 'Sign in with SSO'
|
||||
config.provisioning = newValue.provisioning ?? 'manual'
|
||||
if (newValue.config) {
|
||||
config.config.issuer = newValue.config.issuer ?? ''
|
||||
config.config.client_id = newValue.config.client_id ?? ''
|
||||
config.config.client_secret = newValue.config.client_secret ?? ''
|
||||
config.config.scopes = newValue.config.scopes ?? ['openid', 'email', 'profile']
|
||||
config.config.attribute_map = newValue.config.attribute_map ?? { ...defaultAttributeMap }
|
||||
config.config.default_roles = newValue.config.default_roles ?? []
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
function emitUpdate() {
|
||||
emit('update:modelValue', JSON.parse(JSON.stringify(config)))
|
||||
}
|
||||
|
||||
function updateScopes(event: Event) {
|
||||
const value = (event.target as HTMLInputElement).value
|
||||
config.config.scopes = value.split(/\s+/).filter(s => s.length > 0)
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
function updateDefaultRoles(event: Event) {
|
||||
const value = (event.target as HTMLInputElement).value
|
||||
config.config.default_roles = value.split(',').map(s => s.trim()).filter(s => s.length > 0)
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
function updateAttributeSource(event: Event, oldSource: string) {
|
||||
const newSource = (event.target as HTMLInputElement).value
|
||||
if (newSource !== oldSource) {
|
||||
const target = config.config.attribute_map[oldSource]
|
||||
delete config.config.attribute_map[oldSource]
|
||||
if (newSource) {
|
||||
config.config.attribute_map[newSource] = target
|
||||
}
|
||||
emitUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
function updateAttributeTarget(event: Event, source: string) {
|
||||
const newTarget = (event.target as HTMLInputElement).value
|
||||
config.config.attribute_map[source] = newTarget
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
function addAttribute() {
|
||||
config.config.attribute_map[''] = ''
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
function removeAttribute(source: string) {
|
||||
delete config.config.attribute_map[source]
|
||||
emitUpdate()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.config-panel {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.config-panel h3 {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
.config-panel h4 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--color-heading);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group > label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="url"],
|
||||
.form-group input[type="password"],
|
||||
.form-group input[type="number"],
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-group input[type="checkbox"] {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.attribute-map {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.attribute-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.attribute-row input {
|
||||
flex: 1;
|
||||
padding: 0.4rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.attribute-row .arrow {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-danger);
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
background: var(--color-background-soft);
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn-add:hover {
|
||||
background: var(--color-background-mute);
|
||||
}
|
||||
|
||||
.callback-info {
|
||||
background: var(--color-background-soft);
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.callback-url {
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--color-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
12
src/main.ts
Normal file
12
src/main.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Identity Provider OIDC Module Entry
|
||||
// Exports components for admin configuration UI
|
||||
|
||||
import ConfigPanel from './components/ConfigPanel.vue'
|
||||
|
||||
export {
|
||||
ConfigPanel
|
||||
}
|
||||
|
||||
export default {
|
||||
ConfigPanel
|
||||
}
|
||||
Reference in New Issue
Block a user