Renamed OIDC permissions claim to more commonly used groups. Move jwtk to a more convenient location. Draft admin app OIDC client configuratioon.
This commit is contained in:
@@ -12,6 +12,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(['submitDialog', 'closeDialog'])
|
||||
|
||||
const NAME_EDIT_TYPES = new Set(['org-update', 'role-update', 'user-update-name'])
|
||||
const NO_SUBMIT_TYPES = new Set(['oidc-created'])
|
||||
const rpId = computed(() => props.settings?.rp_id || 'the configured domain')
|
||||
</script>
|
||||
|
||||
@@ -25,6 +26,9 @@ const rpId = computed(() => props.settings?.rp_id || 'the configured domain')
|
||||
<template v-else-if="dialog.type==='user-create'">Add User To Role</template>
|
||||
<template v-else-if="dialog.type==='user-update-name'">Edit User Name</template>
|
||||
<template v-else-if="dialog.type==='perm-create' || dialog.type==='perm-display'">{{ dialog.type === 'perm-create' ? 'Create Permission' : 'Edit Permission' }}</template>
|
||||
<template v-else-if="dialog.type==='oidc-create'">Create OIDC Client</template>
|
||||
<template v-else-if="dialog.type==='oidc-edit'">Edit OIDC Client</template>
|
||||
<template v-else-if="dialog.type==='oidc-created'">OIDC Client Created</template>
|
||||
<template v-else-if="dialog.type==='confirm'">Confirm</template>
|
||||
</h3>
|
||||
<form @submit.prevent="$emit('submitDialog')" class="modal-form">
|
||||
@@ -84,11 +88,32 @@ const rpId = computed(() => props.settings?.rp_id || 'the configured domain')
|
||||
</label>
|
||||
<p class="small muted">If set, this permission is effective only on the specified domain, which can be {{ rpId }} or its subdomain.</p>
|
||||
</template>
|
||||
<template v-else-if="dialog.type==='oidc-create' || dialog.type==='oidc-edit'">
|
||||
<label>Client Name
|
||||
<input v-model="dialog.data.name" placeholder="My Application" required />
|
||||
</label>
|
||||
<label>Redirect URIs (one per line)
|
||||
<textarea v-model="dialog.data.redirect_uris" placeholder="https://example.com/callback https://app.example.com/auth/callback" rows="4" required></textarea>
|
||||
</label>
|
||||
<p class="small muted">Enter the allowed callback URLs for this OIDC client, one per line.</p>
|
||||
</template>
|
||||
<template v-else-if="dialog.type==='oidc-created'">
|
||||
<div class="oidc-success">
|
||||
<p><strong>✅ OIDC Client Created Successfully!</strong></p>
|
||||
<label>Client ID
|
||||
<input v-model="dialog.data.client_id" readonly />
|
||||
</label>
|
||||
<label>Client Secret
|
||||
<input v-model="dialog.data.client_secret" readonly />
|
||||
</label>
|
||||
<p><strong>⚠️ Important:</strong> Save the client secret now. It cannot be retrieved later!</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="dialog.type==='confirm'">
|
||||
<p>{{ dialog.data.message }}</p>
|
||||
</template>
|
||||
<div v-if="dialog.error && !NAME_EDIT_TYPES.has(dialog.type)" class="error small">{{ dialog.error }}</div>
|
||||
<div v-if="!NAME_EDIT_TYPES.has(dialog.type)" class="modal-actions">
|
||||
<div v-if="!NAME_EDIT_TYPES.has(dialog.type) && !NO_SUBMIT_TYPES.has(dialog.type)" class="modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary"
|
||||
@@ -105,10 +130,21 @@ const rpId = computed(() => props.settings?.rp_id || 'the configured domain')
|
||||
{{ dialog.type==='confirm' ? 'OK' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else-if="NO_SUBMIT_TYPES.has(dialog.type)" class="modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary"
|
||||
@click="$emit('closeDialog')"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.optional { font-weight: normal; color: var(--color-text-muted); font-size: 0.85em; }
|
||||
.oidc-success { display: flex; flex-direction: column; gap: var(--space-md); }
|
||||
.oidc-success input { font-family: monospace; }
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { getDirection, navigateButtonRow, focusPreferred, focusAtIndex } from '@/utils/keynav'
|
||||
import { formatDate } from '@/utils/helpers'
|
||||
|
||||
const props = defineProps({
|
||||
info: Object,
|
||||
orgs: Array,
|
||||
permissions: Array,
|
||||
oidcClients: Array,
|
||||
permissionSummary: Object,
|
||||
navigationDisabled: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['createOrg', 'openOrg', 'updateOrg', 'deleteOrg', 'toggleOrgPermission', 'openDialog', 'deletePermission', 'renamePermissionDisplay', 'navigateOut'])
|
||||
const emit = defineEmits(['createOrg', 'openOrg', 'updateOrg', 'deleteOrg', 'toggleOrgPermission', 'openDialog', 'deletePermission', 'renamePermissionDisplay', 'createOidcClient', 'editOidcClient', 'deleteOidcClient', 'navigateOut'])
|
||||
|
||||
// Template refs for navigation
|
||||
const orgSection = ref(null)
|
||||
@@ -19,6 +21,8 @@ const orgTableRef = ref(null)
|
||||
const permMatrixRef = ref(null)
|
||||
const permActionsRef = ref(null)
|
||||
const permTableRef = ref(null)
|
||||
const oidcActionsRef = ref(null)
|
||||
const oidcTableRef = ref(null)
|
||||
|
||||
const sortedOrgs = computed(() => [...props.orgs].sort((a,b)=> {
|
||||
const nameCompare = a.display_name.localeCompare(b.display_name)
|
||||
@@ -348,6 +352,48 @@ defineExpose({ focusFirstElement })
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="isMasterAdmin" class="oidc-clients-section">
|
||||
<div class="section-header">
|
||||
<h2>OAuth2 / OpenID Connect</h2>
|
||||
<p class="section-description">
|
||||
Allow external websites and applications to securely authenticate users through this system.
|
||||
The clients are remote sites or applications that we allow to use Paskia for Single Sign-On.
|
||||
</p>
|
||||
</div>
|
||||
<div ref="oidcActionsRef">
|
||||
<button @click="$emit('createOidcClient')">+ Add Site</button>
|
||||
</div>
|
||||
<table class="org-table" ref="oidcTableRef">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Client</th>
|
||||
<th scope="col">Redirect URI</th>
|
||||
<th scope="col" class="center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="!oidcClients || oidcClients.length === 0">
|
||||
<td colspan="3" class="center muted">No OIDC clients configured</td>
|
||||
</tr>
|
||||
<tr v-for="client in oidcClients" :key="client.uuid">
|
||||
<td class="perm-name-cell">
|
||||
<div class="perm-title">
|
||||
<span class="display-text">{{ client.name }}</span>
|
||||
<button @click="$emit('editOidcClient', client)" class="icon-btn edit-display-btn" aria-label="Edit OIDC client" title="Edit OIDC client">✏️</button>
|
||||
</div>
|
||||
<div class="perm-id-info">
|
||||
<span class="id-text">{{ client.uuid }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="redirect-uris">{{ client.redirect_uris.join(', ') }}</td>
|
||||
<td class="center">
|
||||
<button @click="$emit('deleteOidcClient', client)" class="icon-btn delete-icon" aria-label="Delete OIDC client" title="Delete OIDC client">❌</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -367,4 +413,9 @@ defineExpose({ focusFirstElement })
|
||||
.edit-display-btn { padding: 0.1rem 0.2rem; font-size: 0.8rem; }
|
||||
.edit-org-btn { padding: 0.1rem 0.2rem; font-size: 0.8rem; margin-left: var(--space-xs); }
|
||||
.perm-actions { text-align: center; }
|
||||
|
||||
/* OIDC Clients Section */
|
||||
.oidc-clients-section { margin-bottom: var(--space-xl); margin-top: var(--space-2xl); }
|
||||
.oidc-clients-section .section-header { display: flex; flex-direction: column; gap: 0.4rem; margin-bottom: var(--space-md); }
|
||||
.redirect-uris { font-size: 0.9rem; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user