From 4ed128bb59609449ed66fd00a06a85f43ef6e256 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Mon, 4 May 2026 16:12:38 +0200 Subject: [PATCH 1/5] Add Authorization Details JSON input to Request Credential form Assisted by AI --- .../admin/credentials/RequestCredential.vue | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/web/src/admin/credentials/RequestCredential.vue b/web/src/admin/credentials/RequestCredential.vue index 54e5a1f..0e2753a 100644 --- a/web/src/admin/credentials/RequestCredential.vue +++ b/web/src/admin/credentials/RequestCredential.vue @@ -28,6 +28,14 @@

{{ getIssuerForType(selectedCredentialType) }}

+
+ Authorization Details (JSON) + +

{{ authorizationDetailsError }}

+
@@ -43,6 +51,8 @@ export default { return { selectedCredentialType: '', selectedWalletDID: '', + authorizationDetails: '', + authorizationDetailsError: undefined, issueError: undefined, credentialProfiles: [], walletDIDs: [], @@ -51,6 +61,9 @@ export default { computed: { subjectID() { return this.$route.params.subjectID + }, + authorizationDetailsPlaceholder() { + return '[{"type": "openid_credential", ...}]' } }, created() { @@ -93,6 +106,7 @@ export default { }, issueCredential() { this.issueError = undefined + this.authorizationDetailsError = undefined const issuerDID = this.getIssuerForType(this.selectedCredentialType) if (!issuerDID) { this.issueError = 'No issuer found for selected credential type' @@ -104,6 +118,17 @@ export default { return } + let parsedAuthorizationDetails + const trimmedAuthorizationDetails = this.authorizationDetails.trim() + if (trimmedAuthorizationDetails) { + try { + parsedAuthorizationDetails = JSON.parse(trimmedAuthorizationDetails) + } catch (e) { + this.authorizationDetailsError = 'Invalid JSON: ' + e.message + return + } + } + const redirectUri = `${window.location.origin}${window.location.pathname}${this.$router.resolve({name: 'admin.identityDetails', params: {subjectID: this.subjectID}}).href}` const requestBody = { @@ -112,6 +137,9 @@ export default { wallet_did: this.selectedWalletDID, redirect_uri: redirectUri, } + if (parsedAuthorizationDetails !== undefined) { + requestBody.authorization_details = parsedAuthorizationDetails + } this.$api.post(`api/proxy/internal/auth/v2/${encodeURIPath(this.subjectID)}/request-credential`, requestBody) .then(data => { @@ -147,5 +175,22 @@ export default { :deep(select) { width: 100%; } + +details { + margin-top: 0.5rem; +} + +details > summary { + cursor: pointer; + user-select: none; + font-weight: 500; +} + +details > textarea { + width: 100%; + margin-top: 0.5rem; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.875rem; +} From d6ce401922e4cf7b76270b3524627ca667b02439 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Tue, 5 May 2026 10:27:37 +0200 Subject: [PATCH 2/5] Validate Authorization Details shape and tidy form state Reject non-array JSON or arrays containing non-object entries with inline errors (matches the Nuts node API contract: array of objects). Clear the parse/validation error reactively while the user edits, and demote the placeholder from a computed property to a static data field. Assisted by AI --- .../admin/credentials/RequestCredential.vue | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/web/src/admin/credentials/RequestCredential.vue b/web/src/admin/credentials/RequestCredential.vue index 0e2753a..5d0fde9 100644 --- a/web/src/admin/credentials/RequestCredential.vue +++ b/web/src/admin/credentials/RequestCredential.vue @@ -53,6 +53,7 @@ export default { selectedWalletDID: '', authorizationDetails: '', authorizationDetailsError: undefined, + authorizationDetailsPlaceholder: '[{"type": "openid_credential", ...}]', issueError: undefined, credentialProfiles: [], walletDIDs: [], @@ -61,9 +62,11 @@ export default { computed: { subjectID() { return this.$route.params.subjectID - }, - authorizationDetailsPlaceholder() { - return '[{"type": "openid_credential", ...}]' + } + }, + watch: { + authorizationDetails() { + this.authorizationDetailsError = undefined } }, created() { @@ -127,6 +130,17 @@ export default { this.authorizationDetailsError = 'Invalid JSON: ' + e.message return } + if (!Array.isArray(parsedAuthorizationDetails)) { + this.authorizationDetailsError = 'Authorization Details must be a JSON array' + return + } + const nonObject = parsedAuthorizationDetails.find( + entry => entry === null || typeof entry !== 'object' || Array.isArray(entry) + ) + if (nonObject !== undefined) { + this.authorizationDetailsError = 'Each Authorization Details entry must be a JSON object' + return + } } const redirectUri = `${window.location.origin}${window.location.pathname}${this.$router.resolve({name: 'admin.identityDetails', params: {subjectID: this.subjectID}}).href}` From 85ec36b8527d0d45d42da2500c0cc912f272c7b9 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Tue, 5 May 2026 11:44:23 +0200 Subject: [PATCH 3/5] Show error banner when credential request flow fails The OpenID4VCI flow redirects back to the identity details page with "error" (and optional "error_description") query parameters when the issuer or Nuts node rejects the request. Surface them in a dedicated banner and strip them from the URL so a refresh doesn't re-show. Assisted by AI --- web/src/admin/IdentityDetails.vue | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/src/admin/IdentityDetails.vue b/web/src/admin/IdentityDetails.vue index 4e5e87d..33585b8 100644 --- a/web/src/admin/IdentityDetails.vue +++ b/web/src/admin/IdentityDetails.vue @@ -1,6 +1,7 @@