Skip to content

Commit

Permalink
Merge pull request #502 from bcgov/feature/revokeUi
Browse files Browse the repository at this point in the history
Credential revocation (and exchange delete)
  • Loading branch information
loneil authored Mar 7, 2023
2 parents 3eee28d + 3053688 commit c7d8aa0
Show file tree
Hide file tree
Showing 10 changed files with 345 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
}

/* red */
&.credential-revoked,
&.error,
&.denied,
&.rejected,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
:rows-per-page-options="TABLE_OPT.ROWS_OPTIONS"
selection-mode="single"
data-key="credential_exchange_id"
sort-field="created_at"
:sort-order="-1"
>
<template #header>
<div class="flex justify-content-between">
Expand All @@ -38,27 +40,18 @@
<template #empty> No records found. </template>
<template #loading> Loading data. Please wait... </template>
<Column :expander="true" header-style="width: 3rem" />
<!-- <Column header="Actions">
<Column header="Actions">
<template #body="{ data }">
<Button
title="Delete Credential"
icon="pi pi-trash"
class="p-button-rounded p-button-icon-only p-button-text mr-2"
@click="deleteCredential($event, data)"
<DeleteCredentialExchangeButton
:cred-exch-id="data.credential_exchange_id"
/>
<Button
v-if="
data.credential_template &&
data.credential_template.revocation_enabled &&
!data.revoked
"
title="Revoke Credential"
icon="pi pi-times-circle"
class="p-button-rounded p-button-icon-only p-button-text"
@click="revokeCredential($event, data)"

<RevokeCredentialButton
:cred-exch-record="data"
:connection-display="findConnectionName(data.connection_id)"
/>
</template>
</Column> -->
</Column>
<Column
:sortable="true"
field="credential_definition_id"
Expand Down Expand Up @@ -104,14 +97,15 @@ import { useToast } from 'vue-toastification';
import { useConfirm } from 'primevue/useconfirm';
import { useI18n } from 'vue-i18n';
// Other Components
import OfferCredential from './offerCredential/OfferCredential.vue';
import RowExpandData from '../common/RowExpandData.vue';
import { TABLE_OPT, API_PATH } from '@/helpers/constants';
import { formatDateLong } from '@/helpers';
import DeleteCredentialExchangeButton from './deleteCredential/DeleteCredentialExchangeButton.vue';
import OfferCredential from './offerCredential/OfferCredential.vue';
import RevokeCredentialButton from './deleteCredential/RevokeCredentialButton.vue';
import RowExpandData from '../common/RowExpandData.vue';
import StatusChip from '../common/StatusChip.vue';
const toast = useToast();
const confirm = useConfirm();
const { t } = useI18n();
const contactsStore = useContactsStore();
Expand All @@ -122,50 +116,6 @@ const { loading, credentials, selectedCredential } = storeToRefs(
useIssuerStore()
);
// Delete a specific cred
// const deleteCredential = (event: any, data: any) => {
// confirm.require({
// target: event.currentTarget,
// message: 'Are you sure you want to DELETE this credential?',
// header: 'Confirmation',
// icon: 'pi pi-exclamation-triangle',
// accept: () => {
// issuerStore
// .deleteCredential(data.issuer_credential_id)
// .then(() => {
// toast.success(`Credential deleted`);
// })
// .catch((err) => {
// console.error(err);
// toast.error(`Failure: ${err}`);
// });
// },
// });
// };
// Revoke a specific cred
const revokeCredential = (event: any, data: any) => {
confirm.require({
target: event.currentTarget,
message: 'Are you sure you want to REVOKE this credential?',
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle',
accept: () => {
issuerStore
.revokeCredential({
issuer_credential_id: data.issuer_credential_id,
})
.then(() => {
toast.success(`Credential revoked`);
})
.catch((err) => {
console.error(err);
toast.error(`Failure: ${err}`);
});
},
});
};
// Get the credentials
const loadTable = async () => {
await issuerStore.listCredentials().catch((err) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<template>
<Button
:title="t('issue.delete.removeExchange')"
icon="pi pi-trash"
class="p-button-rounded p-button-icon-only p-button-text mr-2"
@click="deleteCredExchange($event)"
/>
</template>

<script setup lang="ts">
// State
import { useIssuerStore } from '@/store';
// PrimeVue/etc
import Button from 'primevue/button';
import { useToast } from 'vue-toastification';
import { useConfirm } from 'primevue/useconfirm';
import { useI18n } from 'vue-i18n';
const toast = useToast();
const confirm = useConfirm();
const { t } = useI18n();
const issuerStore = useIssuerStore();
// Props
const props = defineProps<{
credExchId: string;
}>();
// Delete a specific cred excch record
const deleteCredExchange = (event: any) => {
confirm.require({
target: event.currentTarget,
message: t('issue.delete.confirm'),
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle',
accept: () => {
issuerStore
.deleteCredentialExchange(props.credExchId)
.then(() => {
toast.success(t('issue.delete.success'));
})
.catch((err) => {
console.error(err);
toast.error(`Failure: ${err}`);
});
},
});
};
</script>

<style scoped></style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<template>
<Button
v-if="canRevoke"
:title="t('issue.revoke.revokeCred')"
icon="pi pi-times-circle"
class="p-button-rounded p-button-icon-only p-button-text"
@click="openModal"
/>
<Dialog
v-model:visible="displayModal"
:style="{ width: '500px' }"
:header="t('issue.revoke.revokeCred')"
:modal="true"
@update:visible="handleClose"
>
<RevokeCredentialForm
:connection-display="props.connectionDisplay"
:cred-exch-record="props.credExchRecord"
@success="$emit('success')"
@closed="handleClose"
/>
</Dialog>
</template>

<script setup lang="ts">
// Types
import { V10CredentialExchange } from '@/types/acapyApi/acapyInterface';
// Vue/State
import { computed, ref } from 'vue';
// PrimeVue/etc
import Button from 'primevue/button';
import Dialog from 'primevue/dialog';
import { useI18n } from 'vue-i18n';
// Components
import RevokeCredentialForm from './RevokeCredentialForm.vue';
const { t } = useI18n();
defineEmits(['success']);
// Props
const props = defineProps<{
credExchRecord: V10CredentialExchange;
connectionDisplay: string;
}>();
// Check revocation allowed
const canRevoke = computed(() => {
return (
props.credExchRecord.state === 'credential_acked' &&
props.credExchRecord.revocation_id &&
props.credExchRecord.revoc_reg_id
);
});
// Display form
const displayModal = ref(false);
const openModal = async () => {
displayModal.value = true;
};
const handleClose = async () => {
displayModal.value = false;
};
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<template>
<form @submit.prevent="handleSubmit(!v$.$invalid)">
<!-- Comment -->
<div class="field">
<label
for="comment"
:class="{ 'p-error': v$.comment.$invalid && submitted }"
>
{{ t('issue.revoke.comment') }}
</label>
<Textarea
id="comment"
v-model="v$.comment.$model"
class="w-full"
:class="{ 'p-invalid': v$.comment.$invalid && submitted }"
:auto-resize="true"
rows="2"
/>
<span v-if="v$.comment.$error && submitted">
<span v-for="(error, index) of v$.comment.$errors" :key="index">
<small class="p-error block">{{ error.$message }}</small>
</span>
</span>
<small v-else-if="v$.comment.$invalid && submitted" class="p-error">{{
v$.comment.required.$message
}}</small>
</div>

<div class="rev-details">
<p>
<small>Connection: {{ props.connectionDisplay }}</small>
</p>
<p>
<small>Revocation ID: {{ props.credExchRecord.revocation_id }}</small>
</p>
<p>
<small>
Revocation Registry: {{ props.credExchRecord.revoc_reg_id }}
</small>
</p>
</div>
<Button
type="submit"
:label="t('issue.revoke.action')"
class="mt-5 w-full"
:disabled="loading"
:loading="loading"
/>
</form>
</template>

<script setup lang="ts">
// Types
import {
RevokeRequest,
V10CredentialExchange,
} from '@/types/acapyApi/acapyInterface';
// Vue/State
import { reactive, ref } from 'vue';
import { useIssuerStore } from '@/store';
import { storeToRefs } from 'pinia';
// PrimeVue / Validation
import Button from 'primevue/button';
import Textarea from 'primevue/textarea';
import { useVuelidate } from '@vuelidate/core';
import { useToast } from 'vue-toastification';
import { useI18n } from 'vue-i18n';
const issuerStore = useIssuerStore();
const { loading } = storeToRefs(useIssuerStore());
const toast = useToast();
const { t } = useI18n();
const emit = defineEmits(['closed', 'success']);
// Props
const props = defineProps<{
credExchRecord: V10CredentialExchange;
connectionDisplay: string;
}>();
// Validation
const formFields = reactive({
comment: '',
});
const rules = {
comment: {},
};
const v$ = useVuelidate(rules, formFields);
// Form submission
const submitted = ref(false);
const handleSubmit = async (isFormValid: boolean) => {
submitted.value = true;
if (!isFormValid) {
return;
}
try {
const payload: RevokeRequest = {
comment: formFields.comment,
connection_id: props.credExchRecord.connection_id,
rev_reg_id: props.credExchRecord.revoc_reg_id,
cred_rev_id: props.credExchRecord.revocation_id,
publish: true,
notify: true,
};
await issuerStore.revokeCredential(payload);
emit('success');
// close up on success
emit('closed');
toast.success(t('issue.revoke.success'));
} catch (error) {
console.error(error);
toast.error(`Failure: ${error}`);
} finally {
submitted.value = false;
}
};
</script>

<style scoped lang="scss">
.rev-details {
p {
margin: 0;
small {
word-break: break-all;
}
}
}
</style>
1 change: 1 addition & 0 deletions services/tenant-ui/frontend/src/helpers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const API_PATH = {
`/innkeeper/reservations/${id}/deny`,

ISSUE_CREDENTIAL_RECORDS: 'issue-credential/records',
ISSUE_CREDENTIAL_RECORD: (id: string) => `issue-credential/records/${id}`,
ISSUE_CREDENTIAL_RECORDS_SEND_OFFER: (id: string) =>
`issue-credential/records/${id}/send-offer`,
ISSUE_CREDENTIALS_SEND_OFFER: 'issue-credential/send-offer',
Expand Down
Loading

0 comments on commit c7d8aa0

Please sign in to comment.