Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions web/src/admin/IdentityDetails.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<template>
<div>
<h1>Identity: {{ $route.params.subjectID }}</h1>
<ErrorMessage v-if="requestCredentialError" title="Credential request failed">
<div class="font-mono text-xs uppercase tracking-wide">{{ requestCredentialError }}</div>
<div v-if="formattedRequestCredentialErrorDescription" class="whitespace-pre-line mt-2">{{ formattedRequestCredentialErrorDescription }}</div>
</ErrorMessage>
<ErrorMessage v-if="fetchError" :message="fetchError"/>
<div v-if="details">
<section>
Expand Down Expand Up @@ -136,19 +140,56 @@ export default {
data() {
return {
fetchError: undefined,
requestCredentialError: undefined,
requestCredentialErrorDescription: undefined,
details: undefined,
shownDIDDocument: undefined,
discoveryServices: {},
credentialProfiles: [],
}
},
computed: {
formattedRequestCredentialErrorDescription() {
if (!this.requestCredentialErrorDescription) {
return ''
}
return this.requestCredentialErrorDescription.replace(/, ([a-zA-Z]+):/g, '\n$1:')
}
},
created() {
this.captureRedirectError()
this.fetchData()
},
methods: {
updateStatus(event) {
this.$emit('statusUpdate', event)
},
captureRedirectError() {
// OAuth-compliant issuers put error params in the URL's query component (window.location.search),
// which is outside the hash-router's view. Check both locations.
const searchParams = new URLSearchParams(window.location.search)
const error = searchParams.get('error') || this.$route.query.error
const errorDescription = searchParams.get('error_description') || this.$route.query.error_description
if (!error) {
return
}
this.requestCredentialError = error
this.requestCredentialErrorDescription = errorDescription

if (searchParams.has('error') || searchParams.has('error_description')) {
searchParams.delete('error')
searchParams.delete('error_description')
const newSearch = searchParams.toString()
const newUrl = window.location.pathname + (newSearch ? '?' + newSearch : '') + window.location.hash
window.history.replaceState(window.history.state, '', newUrl)
}
if (this.$route.query.error || this.$route.query.error_description) {
const rest = Object.fromEntries(
Object.entries(this.$route.query).filter(([k]) => k !== 'error' && k !== 'error_description')
)
this.$router.replace({name: this.$route.name, params: this.$route.params, query: rest})
}
},
fetchData() {
this.$api.get('api/config')
.then(data => {
Expand Down
59 changes: 59 additions & 0 deletions web/src/admin/credentials/RequestCredential.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
<label>Issuer</label>
<p>{{ getIssuerForType(selectedCredentialType) }}</p>
</div>
<details>
<summary>Authorization Details (JSON)</summary>
<textarea v-model="authorizationDetails"
rows="8"
:placeholder="authorizationDetailsPlaceholder"
spellcheck="false"></textarea>
<p v-if="authorizationDetailsError" class="text-red-500 text-sm">{{ authorizationDetailsError }}</p>
</details>
</div>
</modal-window>
</template>
Expand All @@ -43,6 +51,9 @@ export default {
return {
selectedCredentialType: '',
selectedWalletDID: '',
authorizationDetails: '',
authorizationDetailsError: undefined,
authorizationDetailsPlaceholder: '[{"type": "openid_credential", ...}]',
issueError: undefined,
credentialProfiles: [],
walletDIDs: [],
Expand All @@ -53,6 +64,11 @@ export default {
return this.$route.params.subjectID
}
},
watch: {
authorizationDetails() {
this.authorizationDetailsError = undefined
}
},
created() {
// Fetch config for credential profiles
this.$api.get('api/config')
Expand Down Expand Up @@ -93,6 +109,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'
Expand All @@ -104,6 +121,28 @@ 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
}
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}`

const requestBody = {
Expand All @@ -112,6 +151,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 => {
Expand Down Expand Up @@ -147,5 +189,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;
}
</style>

2 changes: 1 addition & 1 deletion web/src/components/ErrorMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
class="appearance-none mb-2 mt-2 px-6 py-3.5 w-full text-sm text-red-500 bg-red-100 placeholder-red-500 outline-hidden border border-red-500 focus:ring-4 focus:ring-blue-200 rounded-md"
role="alert">
<div class="font-semibold mb-2">{{ title }}</div>
<div>{{ message }}</div>
<div><slot>{{ message }}</slot></div>
<div v-if="showReauthenticateButton" class="mt-3">
<button type="button"
class="btn btn-danger"
Expand Down
Loading