Skip to content

Commit 3053688

Browse files
committed
Allow revoking and deleting cred
Signed-off-by: Lucas ONeil <lucasoneil@gmail.com>
1 parent f594a33 commit 3053688

File tree

10 files changed

+345
-105
lines changed

10 files changed

+345
-105
lines changed

services/tenant-ui/frontend/src/assets/tenantuiComponents.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
}
3535

3636
/* red */
37+
&.credential-revoked,
3738
&.error,
3839
&.denied,
3940
&.rejected,

services/tenant-ui/frontend/src/components/issuance/IssuedCredentials.vue

Lines changed: 14 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
:rows-per-page-options="TABLE_OPT.ROWS_OPTIONS"
1313
selection-mode="single"
1414
data-key="credential_exchange_id"
15+
sort-field="created_at"
16+
:sort-order="-1"
1517
>
1618
<template #header>
1719
<div class="flex justify-content-between">
@@ -38,27 +40,18 @@
3840
<template #empty> No records found. </template>
3941
<template #loading> Loading data. Please wait... </template>
4042
<Column :expander="true" header-style="width: 3rem" />
41-
<!-- <Column header="Actions">
43+
<Column header="Actions">
4244
<template #body="{ data }">
43-
<Button
44-
title="Delete Credential"
45-
icon="pi pi-trash"
46-
class="p-button-rounded p-button-icon-only p-button-text mr-2"
47-
@click="deleteCredential($event, data)"
45+
<DeleteCredentialExchangeButton
46+
:cred-exch-id="data.credential_exchange_id"
4847
/>
49-
<Button
50-
v-if="
51-
data.credential_template &&
52-
data.credential_template.revocation_enabled &&
53-
!data.revoked
54-
"
55-
title="Revoke Credential"
56-
icon="pi pi-times-circle"
57-
class="p-button-rounded p-button-icon-only p-button-text"
58-
@click="revokeCredential($event, data)"
48+
49+
<RevokeCredentialButton
50+
:cred-exch-record="data"
51+
:connection-display="findConnectionName(data.connection_id)"
5952
/>
6053
</template>
61-
</Column> -->
54+
</Column>
6255
<Column
6356
:sortable="true"
6457
field="credential_definition_id"
@@ -104,14 +97,15 @@ import { useToast } from 'vue-toastification';
10497
import { useConfirm } from 'primevue/useconfirm';
10598
import { useI18n } from 'vue-i18n';
10699
// Other Components
107-
import OfferCredential from './offerCredential/OfferCredential.vue';
108-
import RowExpandData from '../common/RowExpandData.vue';
109100
import { TABLE_OPT, API_PATH } from '@/helpers/constants';
110101
import { formatDateLong } from '@/helpers';
102+
import DeleteCredentialExchangeButton from './deleteCredential/DeleteCredentialExchangeButton.vue';
103+
import OfferCredential from './offerCredential/OfferCredential.vue';
104+
import RevokeCredentialButton from './deleteCredential/RevokeCredentialButton.vue';
105+
import RowExpandData from '../common/RowExpandData.vue';
111106
import StatusChip from '../common/StatusChip.vue';
112107
113108
const toast = useToast();
114-
const confirm = useConfirm();
115109
const { t } = useI18n();
116110
117111
const contactsStore = useContactsStore();
@@ -122,50 +116,6 @@ const { loading, credentials, selectedCredential } = storeToRefs(
122116
useIssuerStore()
123117
);
124118
125-
// Delete a specific cred
126-
// const deleteCredential = (event: any, data: any) => {
127-
// confirm.require({
128-
// target: event.currentTarget,
129-
// message: 'Are you sure you want to DELETE this credential?',
130-
// header: 'Confirmation',
131-
// icon: 'pi pi-exclamation-triangle',
132-
// accept: () => {
133-
// issuerStore
134-
// .deleteCredential(data.issuer_credential_id)
135-
// .then(() => {
136-
// toast.success(`Credential deleted`);
137-
// })
138-
// .catch((err) => {
139-
// console.error(err);
140-
// toast.error(`Failure: ${err}`);
141-
// });
142-
// },
143-
// });
144-
// };
145-
146-
// Revoke a specific cred
147-
const revokeCredential = (event: any, data: any) => {
148-
confirm.require({
149-
target: event.currentTarget,
150-
message: 'Are you sure you want to REVOKE this credential?',
151-
header: 'Confirmation',
152-
icon: 'pi pi-exclamation-triangle',
153-
accept: () => {
154-
issuerStore
155-
.revokeCredential({
156-
issuer_credential_id: data.issuer_credential_id,
157-
})
158-
.then(() => {
159-
toast.success(`Credential revoked`);
160-
})
161-
.catch((err) => {
162-
console.error(err);
163-
toast.error(`Failure: ${err}`);
164-
});
165-
},
166-
});
167-
};
168-
169119
// Get the credentials
170120
const loadTable = async () => {
171121
await issuerStore.listCredentials().catch((err) => {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<template>
2+
<Button
3+
:title="t('issue.delete.removeExchange')"
4+
icon="pi pi-trash"
5+
class="p-button-rounded p-button-icon-only p-button-text mr-2"
6+
@click="deleteCredExchange($event)"
7+
/>
8+
</template>
9+
10+
<script setup lang="ts">
11+
// State
12+
import { useIssuerStore } from '@/store';
13+
// PrimeVue/etc
14+
import Button from 'primevue/button';
15+
import { useToast } from 'vue-toastification';
16+
import { useConfirm } from 'primevue/useconfirm';
17+
import { useI18n } from 'vue-i18n';
18+
19+
const toast = useToast();
20+
const confirm = useConfirm();
21+
const { t } = useI18n();
22+
23+
const issuerStore = useIssuerStore();
24+
25+
// Props
26+
const props = defineProps<{
27+
credExchId: string;
28+
}>();
29+
30+
// Delete a specific cred excch record
31+
const deleteCredExchange = (event: any) => {
32+
confirm.require({
33+
target: event.currentTarget,
34+
message: t('issue.delete.confirm'),
35+
header: 'Confirmation',
36+
icon: 'pi pi-exclamation-triangle',
37+
accept: () => {
38+
issuerStore
39+
.deleteCredentialExchange(props.credExchId)
40+
.then(() => {
41+
toast.success(t('issue.delete.success'));
42+
})
43+
.catch((err) => {
44+
console.error(err);
45+
toast.error(`Failure: ${err}`);
46+
});
47+
},
48+
});
49+
};
50+
</script>
51+
52+
<style scoped></style>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<template>
2+
<Button
3+
v-if="canRevoke"
4+
:title="t('issue.revoke.revokeCred')"
5+
icon="pi pi-times-circle"
6+
class="p-button-rounded p-button-icon-only p-button-text"
7+
@click="openModal"
8+
/>
9+
<Dialog
10+
v-model:visible="displayModal"
11+
:style="{ width: '500px' }"
12+
:header="t('issue.revoke.revokeCred')"
13+
:modal="true"
14+
@update:visible="handleClose"
15+
>
16+
<RevokeCredentialForm
17+
:connection-display="props.connectionDisplay"
18+
:cred-exch-record="props.credExchRecord"
19+
@success="$emit('success')"
20+
@closed="handleClose"
21+
/>
22+
</Dialog>
23+
</template>
24+
25+
<script setup lang="ts">
26+
// Types
27+
import { V10CredentialExchange } from '@/types/acapyApi/acapyInterface';
28+
29+
// Vue/State
30+
import { computed, ref } from 'vue';
31+
// PrimeVue/etc
32+
import Button from 'primevue/button';
33+
import Dialog from 'primevue/dialog';
34+
import { useI18n } from 'vue-i18n';
35+
// Components
36+
import RevokeCredentialForm from './RevokeCredentialForm.vue';
37+
38+
const { t } = useI18n();
39+
40+
defineEmits(['success']);
41+
42+
// Props
43+
const props = defineProps<{
44+
credExchRecord: V10CredentialExchange;
45+
connectionDisplay: string;
46+
}>();
47+
48+
// Check revocation allowed
49+
const canRevoke = computed(() => {
50+
return (
51+
props.credExchRecord.state === 'credential_acked' &&
52+
props.credExchRecord.revocation_id &&
53+
props.credExchRecord.revoc_reg_id
54+
);
55+
});
56+
57+
// Display form
58+
const displayModal = ref(false);
59+
const openModal = async () => {
60+
displayModal.value = true;
61+
};
62+
const handleClose = async () => {
63+
displayModal.value = false;
64+
};
65+
</script>
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<template>
2+
<form @submit.prevent="handleSubmit(!v$.$invalid)">
3+
<!-- Comment -->
4+
<div class="field">
5+
<label
6+
for="comment"
7+
:class="{ 'p-error': v$.comment.$invalid && submitted }"
8+
>
9+
{{ t('issue.revoke.comment') }}
10+
</label>
11+
<Textarea
12+
id="comment"
13+
v-model="v$.comment.$model"
14+
class="w-full"
15+
:class="{ 'p-invalid': v$.comment.$invalid && submitted }"
16+
:auto-resize="true"
17+
rows="2"
18+
/>
19+
<span v-if="v$.comment.$error && submitted">
20+
<span v-for="(error, index) of v$.comment.$errors" :key="index">
21+
<small class="p-error block">{{ error.$message }}</small>
22+
</span>
23+
</span>
24+
<small v-else-if="v$.comment.$invalid && submitted" class="p-error">{{
25+
v$.comment.required.$message
26+
}}</small>
27+
</div>
28+
29+
<div class="rev-details">
30+
<p>
31+
<small>Connection: {{ props.connectionDisplay }}</small>
32+
</p>
33+
<p>
34+
<small>Revocation ID: {{ props.credExchRecord.revocation_id }}</small>
35+
</p>
36+
<p>
37+
<small>
38+
Revocation Registry: {{ props.credExchRecord.revoc_reg_id }}
39+
</small>
40+
</p>
41+
</div>
42+
<Button
43+
type="submit"
44+
:label="t('issue.revoke.action')"
45+
class="mt-5 w-full"
46+
:disabled="loading"
47+
:loading="loading"
48+
/>
49+
</form>
50+
</template>
51+
52+
<script setup lang="ts">
53+
// Types
54+
import {
55+
RevokeRequest,
56+
V10CredentialExchange,
57+
} from '@/types/acapyApi/acapyInterface';
58+
59+
// Vue/State
60+
import { reactive, ref } from 'vue';
61+
import { useIssuerStore } from '@/store';
62+
import { storeToRefs } from 'pinia';
63+
// PrimeVue / Validation
64+
import Button from 'primevue/button';
65+
import Textarea from 'primevue/textarea';
66+
import { useVuelidate } from '@vuelidate/core';
67+
import { useToast } from 'vue-toastification';
68+
import { useI18n } from 'vue-i18n';
69+
70+
const issuerStore = useIssuerStore();
71+
const { loading } = storeToRefs(useIssuerStore());
72+
73+
const toast = useToast();
74+
const { t } = useI18n();
75+
76+
const emit = defineEmits(['closed', 'success']);
77+
78+
// Props
79+
const props = defineProps<{
80+
credExchRecord: V10CredentialExchange;
81+
connectionDisplay: string;
82+
}>();
83+
84+
// Validation
85+
const formFields = reactive({
86+
comment: '',
87+
});
88+
const rules = {
89+
comment: {},
90+
};
91+
const v$ = useVuelidate(rules, formFields);
92+
93+
// Form submission
94+
const submitted = ref(false);
95+
const handleSubmit = async (isFormValid: boolean) => {
96+
submitted.value = true;
97+
if (!isFormValid) {
98+
return;
99+
}
100+
101+
try {
102+
const payload: RevokeRequest = {
103+
comment: formFields.comment,
104+
connection_id: props.credExchRecord.connection_id,
105+
rev_reg_id: props.credExchRecord.revoc_reg_id,
106+
cred_rev_id: props.credExchRecord.revocation_id,
107+
publish: true,
108+
notify: true,
109+
};
110+
await issuerStore.revokeCredential(payload);
111+
emit('success');
112+
// close up on success
113+
emit('closed');
114+
toast.success(t('issue.revoke.success'));
115+
} catch (error) {
116+
console.error(error);
117+
toast.error(`Failure: ${error}`);
118+
} finally {
119+
submitted.value = false;
120+
}
121+
};
122+
</script>
123+
124+
<style scoped lang="scss">
125+
.rev-details {
126+
p {
127+
margin: 0;
128+
small {
129+
word-break: break-all;
130+
}
131+
}
132+
}
133+
</style>

services/tenant-ui/frontend/src/helpers/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const API_PATH = {
5353
`/innkeeper/reservations/${id}/deny`,
5454

5555
ISSUE_CREDENTIAL_RECORDS: 'issue-credential/records',
56+
ISSUE_CREDENTIAL_RECORD: (id: string) => `issue-credential/records/${id}`,
5657
ISSUE_CREDENTIAL_RECORDS_SEND_OFFER: (id: string) =>
5758
`issue-credential/records/${id}/send-offer`,
5859
ISSUE_CREDENTIALS_SEND_OFFER: 'issue-credential/send-offer',

0 commit comments

Comments
 (0)