forked from trent_larson/crowd-funder-for-time-pwa
make screen where user can create a group onboarding meeting
This commit is contained in:
190
src/views/ClaimReportCertificateView.vue
Normal file
190
src/views/ClaimReportCertificateView.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<section id="Content">
|
||||
<div v-if="claimData">
|
||||
<canvas ref="claimCanvas"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { nextTick } from "vue";
|
||||
import QRCode from "qrcode";
|
||||
|
||||
import { APP_SERVER, NotificationIface } from "@/constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
|
||||
import * as endorserServer from "@/libs/endorserServer";
|
||||
|
||||
@Component
|
||||
export default class ClaimReportCertificateView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
allMyDids: Array<string> = [];
|
||||
apiServer = "";
|
||||
claimId = "";
|
||||
claimData = null;
|
||||
|
||||
endorserServer = endorserServer;
|
||||
|
||||
async created() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
const pathParams = window.location.pathname.substring(
|
||||
"/claim-cert/".length,
|
||||
);
|
||||
this.claimId = pathParams;
|
||||
await this.fetchClaim();
|
||||
}
|
||||
|
||||
async fetchClaim() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${this.apiServer}/api/claim/${this.claimId}`,
|
||||
);
|
||||
if (response.ok) {
|
||||
this.claimData = await response.json();
|
||||
await nextTick(); // Wait for the DOM to update
|
||||
if (this.claimData) {
|
||||
this.drawCanvas(this.claimData);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Error fetching claim: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load claim:", error);
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was a problem loading the claim.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async drawCanvas(
|
||||
claimData: endorserServer.GenericCredWrapper<endorserServer.GenericVerifiableCredential>,
|
||||
) {
|
||||
await db.open();
|
||||
const allContacts = await db.contacts.toArray();
|
||||
|
||||
const canvas = this.$refs.claimCanvas as HTMLCanvasElement;
|
||||
if (canvas) {
|
||||
const CANVAS_WIDTH = 1100;
|
||||
const CANVAS_HEIGHT = 850;
|
||||
|
||||
// size to approximate portrait of 8.5"x11"
|
||||
canvas.width = CANVAS_WIDTH;
|
||||
canvas.height = CANVAS_HEIGHT;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
// Load the background image
|
||||
const backgroundImage = new Image();
|
||||
backgroundImage.src = "/img/background/cert-frame-2.jpg";
|
||||
backgroundImage.onload = async () => {
|
||||
// Draw the background image
|
||||
ctx.drawImage(backgroundImage, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||
|
||||
// Set font and styles
|
||||
ctx.fillStyle = "black";
|
||||
|
||||
// Draw claim type
|
||||
ctx.font = "bold 20px Arial";
|
||||
const claimTypeText =
|
||||
this.endorserServer.capitalizeAndInsertSpacesBeforeCaps(
|
||||
claimData.claimType || "",
|
||||
);
|
||||
const claimTypeWidth = ctx.measureText(claimTypeText).width;
|
||||
ctx.fillText(
|
||||
claimTypeText,
|
||||
(CANVAS_WIDTH - claimTypeWidth) / 2, // Center horizontally
|
||||
CANVAS_HEIGHT * 0.33,
|
||||
);
|
||||
|
||||
if (claimData.claim.agent) {
|
||||
const presentedText = "Presented to ";
|
||||
ctx.font = "14px Arial";
|
||||
const presentedWidth = ctx.measureText(presentedText).width;
|
||||
ctx.fillText(
|
||||
presentedText,
|
||||
(CANVAS_WIDTH - presentedWidth) / 2, // Center horizontally
|
||||
CANVAS_HEIGHT * 0.37,
|
||||
);
|
||||
const agentText = endorserServer.didInfoForCertificate(
|
||||
claimData.claim.agent,
|
||||
allContacts,
|
||||
);
|
||||
ctx.font = "bold 20px Arial";
|
||||
const agentWidth = ctx.measureText(agentText).width;
|
||||
ctx.fillText(
|
||||
agentText,
|
||||
(CANVAS_WIDTH - agentWidth) / 2, // Center horizontally
|
||||
CANVAS_HEIGHT * 0.4,
|
||||
);
|
||||
}
|
||||
|
||||
const descriptionText =
|
||||
claimData.claim.name || claimData.claim.description;
|
||||
if (descriptionText) {
|
||||
const descriptionLine =
|
||||
descriptionText.length > 50
|
||||
? descriptionText.substring(0, 75) + "..."
|
||||
: descriptionText;
|
||||
ctx.font = "14px Arial";
|
||||
const descriptionWidth = ctx.measureText(descriptionLine).width;
|
||||
ctx.fillText(
|
||||
descriptionLine,
|
||||
(CANVAS_WIDTH - descriptionWidth) / 2,
|
||||
CANVAS_HEIGHT * 0.45,
|
||||
);
|
||||
}
|
||||
|
||||
// Draw claim issuer & recipient
|
||||
if (claimData.issuer) {
|
||||
ctx.font = "14px Arial";
|
||||
const issuerText =
|
||||
"Issued by " +
|
||||
endorserServer.didInfoForCertificate(
|
||||
claimData.issuer,
|
||||
allContacts,
|
||||
);
|
||||
ctx.fillText(issuerText, CANVAS_WIDTH * 0.3, CANVAS_HEIGHT * 0.6);
|
||||
}
|
||||
|
||||
// Draw claim ID
|
||||
ctx.font = "14px Arial";
|
||||
ctx.fillText(this.claimId, CANVAS_WIDTH * 0.3, CANVAS_HEIGHT * 0.7);
|
||||
ctx.fillText(
|
||||
"via EndorserSearch.com",
|
||||
CANVAS_WIDTH * 0.3,
|
||||
CANVAS_HEIGHT * 0.73,
|
||||
);
|
||||
|
||||
// Generate and draw QR code
|
||||
const qrCodeCanvas = document.createElement("canvas");
|
||||
await QRCode.toCanvas(
|
||||
qrCodeCanvas,
|
||||
APP_SERVER + "/claim/" + this.claimId,
|
||||
{
|
||||
width: 150,
|
||||
color: { light: "#0000" /* Transparent background */ },
|
||||
},
|
||||
);
|
||||
ctx.drawImage(qrCodeCanvas, CANVAS_WIDTH * 0.6, CANVAS_HEIGHT * 0.55);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -23,27 +23,48 @@
|
||||
|
||||
<!-- New Contact -->
|
||||
<div id="formAddNewContact" class="mt-4 mb-4 flex items-stretch">
|
||||
<router-link
|
||||
v-if="isRegistered"
|
||||
:to="{ name: 'invite-one' }"
|
||||
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
|
||||
>
|
||||
<fa icon="envelope-open-text" class="fa-fw text-2xl" />
|
||||
</router-link>
|
||||
<span class="flex" v-if="isRegistered">
|
||||
<router-link
|
||||
:to="{ name: 'invite-one' }"
|
||||
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
|
||||
>
|
||||
<fa icon="envelope-open-text" class="fa-fw text-2xl" />
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'onboard-meeting' }"
|
||||
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
|
||||
>
|
||||
<fa icon="chair" class="fa-fw text-2xl" />
|
||||
</router-link>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="flex items-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
|
||||
class="flex"
|
||||
>
|
||||
<fa
|
||||
<span class="flex items-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md">
|
||||
<fa
|
||||
icon="envelope-open-text"
|
||||
class="fa-fw text-2xl"
|
||||
@click="
|
||||
danger(
|
||||
'You must get registered before you can invite others.',
|
||||
warning(
|
||||
'You must get registered before you can create invites.',
|
||||
'Not Registered',
|
||||
)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span class="flex items-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md">
|
||||
<fa
|
||||
icon="chair"
|
||||
class="fa-fw text-2xl"
|
||||
@click="
|
||||
warning(
|
||||
'You must get registered before you can initiate an onboarding meeting.',
|
||||
'Not Registered',
|
||||
)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<router-link
|
||||
@@ -587,6 +608,18 @@ export default class ContactsView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
private warning(message: string, title: string = "Error", timeout = 5000) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: title,
|
||||
text: message,
|
||||
},
|
||||
timeout,
|
||||
);
|
||||
}
|
||||
|
||||
private showOnboardingInfo() {
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
@@ -204,7 +204,11 @@ import { AxiosError, AxiosRequestHeaders } from "axios";
|
||||
import { DateTime } from "luxon";
|
||||
import { hexToBytes } from "@noble/hashes/utils";
|
||||
// these core imports could also be included as "import type ..."
|
||||
import { EventTemplate, UnsignedEvent, VerifiedEvent } from "nostr-tools/core";
|
||||
import {
|
||||
EventTemplate,
|
||||
UnsignedEvent,
|
||||
VerifiedEvent,
|
||||
} from "nostr-tools/lib/types/core";
|
||||
import {
|
||||
accountFromExtendedKey,
|
||||
extendedKeysFromSeedWords,
|
||||
|
||||
355
src/views/OnboardMeetingView.vue
Normal file
355
src/views/OnboardMeetingView.vue
Normal file
@@ -0,0 +1,355 @@
|
||||
<template>
|
||||
<QuickNav selected="Contacts" />
|
||||
<TopMessage />
|
||||
|
||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||
<!-- Heading -->
|
||||
<h1 id="ViewHeading" class="text-4xl text-center font-light">
|
||||
Onboarding Meeting
|
||||
</h1>
|
||||
|
||||
<!-- Existing Meeting Section -->
|
||||
<div v-if="existingMeeting" class="mt-8 p-4 border rounded-lg bg-white shadow">
|
||||
<h2 class="text-2xl mb-4">Current Meeting</h2>
|
||||
<div class="space-y-2">
|
||||
<p><strong>Name:</strong> {{ existingMeeting.name }}</p>
|
||||
<p><strong>Expires:</strong> {{ formatExpirationTime(existingMeeting.expiresAt) }}</p>
|
||||
<p class="mt-4 text-sm text-gray-600">Share the the password with the people you want to onboard.</p>
|
||||
</div>
|
||||
<div class="flex justify-end mt-4">
|
||||
<button
|
||||
@click="confirmDelete"
|
||||
class="flex items-center justify-center w-10 h-10 rounded-full bg-red-100 hover:bg-red-200 transition-colors duration-200"
|
||||
:disabled="isDeleting"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': isDeleting }"
|
||||
title="Delete Meeting"
|
||||
>
|
||||
<fa icon="trash-can" class="fa-fw text-red-600" />
|
||||
<span class="sr-only">{{ isDeleting ? 'Deleting...' : 'Delete Meeting' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<div v-if="showDeleteConfirm" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-lg p-6 max-w-sm w-full">
|
||||
<h3 class="text-lg font-medium mb-4">Delete Meeting?</h3>
|
||||
<p class="text-gray-600 mb-6">This action cannot be undone. Are you sure you want to delete this meeting?</p>
|
||||
<div class="flex justify-between space-x-4">
|
||||
<button
|
||||
@click="showDeleteConfirm = false"
|
||||
class="px-4 py-2 bg-slate-500 text-white rounded hover:bg-slate-700"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="deleteMeeting"
|
||||
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create New Meeting Form -->
|
||||
<div v-if="!existingMeeting && !isLoading" class="mt-8">
|
||||
<h2 class="text-2xl mb-4">Create New Meeting</h2>
|
||||
<form @submit.prevent="createMeeting" class="space-y-4">
|
||||
<div>
|
||||
<label for="meetingName" class="block text-sm font-medium text-gray-700">Meeting Name</label>
|
||||
<input
|
||||
id="meetingName"
|
||||
v-model="currentMeeting.name"
|
||||
type="text"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none"
|
||||
placeholder="Enter meeting name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="expirationTime" class="block text-sm font-medium text-gray-700">Meeting Expiration Time</label>
|
||||
<input
|
||||
id="expirationTime"
|
||||
v-model="currentMeeting.expiresAt"
|
||||
type="datetime-local"
|
||||
required
|
||||
:min="minDateTime"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">Meeting Password</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="text"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none"
|
||||
placeholder="Enter meeting password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="userName" class="block text-sm font-medium text-gray-700">Your Name</label>
|
||||
<input
|
||||
id="userName"
|
||||
v-model="currentMeeting.userFullName"
|
||||
type="text"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none"
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md hover:from-green-500 hover:to-green-800"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
{{ isLoading ? 'Creating...' : 'Create Meeting' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else-if="isLoading">
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<fa icon="spinner" class="fa-spin-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-facing-decorator';
|
||||
import QuickNav from '@/components/QuickNav.vue';
|
||||
import TopMessage from '@/components/TopMessage.vue';
|
||||
import { retrieveSettingsForActiveAccount } from '@/db/index';
|
||||
import { getHeaders, serverMessageForUser } from '@/libs/endorserServer';
|
||||
import { encryptMessage } from '@/libs/crypto';
|
||||
|
||||
interface Meeting {
|
||||
groupId?: string;
|
||||
name: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
interface NewMeeting extends Meeting {
|
||||
userFullName: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
QuickNav,
|
||||
TopMessage,
|
||||
},
|
||||
})
|
||||
export default class OnboardMeetingView extends Vue {
|
||||
$notify!: (notification: { group: string; type: string; title: string; text: string }, timeout?: number) => void;
|
||||
|
||||
existingMeeting: Meeting | null = null;
|
||||
currentMeeting: NewMeeting = {
|
||||
name: '',
|
||||
expiresAt: this.getDefaultExpirationTime(),
|
||||
userFullName: '',
|
||||
};
|
||||
password = '';
|
||||
isLoading = false;
|
||||
activeDid = '';
|
||||
apiServer = '';
|
||||
isDeleting = false;
|
||||
showDeleteConfirm = false;
|
||||
|
||||
get minDateTime() {
|
||||
const now = new Date();
|
||||
now.setMinutes(now.getMinutes() + 5); // Set minimum 5 minutes in the future
|
||||
return this.formatDateForInput(now);
|
||||
}
|
||||
|
||||
async created() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || '';
|
||||
this.apiServer = settings.apiServer || '';
|
||||
this.currentMeeting.userFullName = settings.firstName || '';
|
||||
|
||||
await this.fetchExistingMeeting();
|
||||
}
|
||||
|
||||
getDefaultExpirationTime(): string {
|
||||
const date = new Date();
|
||||
// Round up to the next hour
|
||||
date.setMinutes(0);
|
||||
date.setSeconds(0);
|
||||
date.setMilliseconds(0);
|
||||
date.setHours(date.getHours() + 1); // Round up to next hour
|
||||
date.setHours(date.getHours() + 2); // Add 2 more hours
|
||||
return this.formatDateForInput(date);
|
||||
}
|
||||
|
||||
// Format a date object to YYYY-MM-DDTHH:mm format for datetime-local input
|
||||
private formatDateForInput(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
async fetchExistingMeeting() {
|
||||
try {
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
const response = await this.axios.get(
|
||||
this.apiServer + '/api/partner/groupOnboard',
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (response.data && response.data.data) {
|
||||
this.existingMeeting = response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error fetching existing meeting:', error);
|
||||
this.$notify(
|
||||
{
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: serverMessageForUser(error) || 'Failed to fetch existing meeting.',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async createMeeting() {
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
// Convert local time to UTC for comparison and server submission
|
||||
const localExpiresAt = new Date(this.currentMeeting.expiresAt);
|
||||
const now = new Date();
|
||||
if (localExpiresAt <= now) {
|
||||
this.$notify(
|
||||
{
|
||||
group: 'alert',
|
||||
type: 'warning',
|
||||
title: 'Invalid Time',
|
||||
text: 'Select a future time for the meeting expiration.',
|
||||
},
|
||||
5000
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// create content with user's name and DID encrypted with password
|
||||
const content = {
|
||||
name: this.currentMeeting.userFullName,
|
||||
did: this.activeDid,
|
||||
};
|
||||
const encryptedContent = await encryptMessage(JSON.stringify(content), this.password);
|
||||
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
const response = await this.axios.post(
|
||||
this.apiServer + '/api/partner/groupOnboard',
|
||||
{
|
||||
name: this.currentMeeting.name,
|
||||
expiresAt: localExpiresAt.toISOString(),
|
||||
content: encryptedContent,
|
||||
},
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
this.existingMeeting = {
|
||||
groupId: response.data.groupId,
|
||||
name: this.currentMeeting.name,
|
||||
expiresAt: localExpiresAt.toISOString(),
|
||||
};
|
||||
this.$notify(
|
||||
{
|
||||
group: 'alert',
|
||||
type: 'success',
|
||||
title: 'Success',
|
||||
text: 'Meeting created.',
|
||||
},
|
||||
3000
|
||||
);
|
||||
} else {
|
||||
throw new Error('Failed to create meeting due to unexpected response data: ' + JSON.stringify(response.data));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating meeting:', error);
|
||||
const errorMessage = serverMessageForUser(error);
|
||||
this.$notify(
|
||||
{
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: errorMessage || 'Failed to create meeting. Try reloading or submitting again.',
|
||||
},
|
||||
5000
|
||||
);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
formatExpirationTime(expiresAt: string): string {
|
||||
const expiration = new Date(expiresAt); // Server time is in UTC
|
||||
const now = new Date();
|
||||
const diffHours = Math.round((expiration.getTime() - now.getTime()) / (1000 * 60 * 60));
|
||||
|
||||
if (diffHours < 0) {
|
||||
return 'Expired';
|
||||
} else if (diffHours < 1) {
|
||||
return 'Less than an hour';
|
||||
} else if (diffHours === 1) {
|
||||
return '1 hour';
|
||||
} else {
|
||||
return `${diffHours} hours`;
|
||||
}
|
||||
}
|
||||
|
||||
confirmDelete() {
|
||||
this.showDeleteConfirm = true;
|
||||
}
|
||||
|
||||
async deleteMeeting() {
|
||||
this.isDeleting = true;
|
||||
try {
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
await this.axios.delete(
|
||||
this.apiServer + '/api/partner/groupOnboard',
|
||||
{ headers }
|
||||
);
|
||||
|
||||
this.existingMeeting = null;
|
||||
this.showDeleteConfirm = false;
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: 'alert',
|
||||
type: 'success',
|
||||
title: 'Success',
|
||||
text: 'Meeting deleted successfully.',
|
||||
},
|
||||
3000
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error deleting meeting:', error);
|
||||
this.$notify(
|
||||
{
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: serverMessageForUser(error) || 'Failed to delete meeting.',
|
||||
},
|
||||
5000
|
||||
);
|
||||
} finally {
|
||||
this.isDeleting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -22,7 +22,7 @@
|
||||
<div>
|
||||
<h2 class="text-2xl m-2">Confirm</h2>
|
||||
<div v-if="loadingConfirms" class="flex justify-center">
|
||||
<fa icon="spinner" class="animate-spin" />
|
||||
<fa icon="spinner" class="fa-spin-pulse" />
|
||||
</div>
|
||||
<div v-else-if="claimsToConfirm.length === 0">
|
||||
There are no claims yet today for you to confirm.
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
5000,
|
||||
)
|
||||
"
|
||||
class="font-bold uppercase bg-slate-900 text-white px-3 py-2 rounded-md mr-2"
|
||||
class="font-bold capitalize bg-slate-900 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Toast
|
||||
</button>
|
||||
@@ -52,7 +52,7 @@
|
||||
5000,
|
||||
)
|
||||
"
|
||||
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Info
|
||||
</button>
|
||||
@@ -69,7 +69,7 @@
|
||||
5000,
|
||||
)
|
||||
"
|
||||
class="font-bold uppercase bg-emerald-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
class="font-bold capitalize bg-emerald-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Success
|
||||
</button>
|
||||
@@ -86,7 +86,7 @@
|
||||
5000,
|
||||
)
|
||||
"
|
||||
class="font-bold uppercase bg-amber-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
class="font-bold capitalize bg-amber-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Warning
|
||||
</button>
|
||||
@@ -103,7 +103,7 @@
|
||||
5000,
|
||||
)
|
||||
"
|
||||
class="font-bold uppercase bg-rose-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
class="font-bold capitalize bg-rose-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Danger
|
||||
</button>
|
||||
@@ -118,7 +118,7 @@
|
||||
-1,
|
||||
)
|
||||
"
|
||||
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Notif ON
|
||||
</button>
|
||||
@@ -133,7 +133,7 @@
|
||||
-1,
|
||||
)
|
||||
"
|
||||
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Notif MUTE
|
||||
</button>
|
||||
@@ -148,7 +148,7 @@
|
||||
-1,
|
||||
)
|
||||
"
|
||||
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Notif OFF
|
||||
</button>
|
||||
@@ -184,7 +184,7 @@
|
||||
Register Passkey
|
||||
<button
|
||||
@click="register()"
|
||||
class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Simplewebauthn
|
||||
</button>
|
||||
@@ -194,13 +194,13 @@
|
||||
Create JWT
|
||||
<button
|
||||
@click="createJwtSimplewebauthn()"
|
||||
class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Simplewebauthn
|
||||
</button>
|
||||
<button
|
||||
@click="createJwtNavigator()"
|
||||
class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Navigator
|
||||
</button>
|
||||
@@ -210,19 +210,19 @@
|
||||
Verify New JWT
|
||||
<button
|
||||
@click="verifySimplewebauthn()"
|
||||
class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Simplewebauthn
|
||||
</button>
|
||||
<button
|
||||
@click="verifyWebCrypto()"
|
||||
class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
WebCrypto
|
||||
</button>
|
||||
<button
|
||||
@click="verifyP256()"
|
||||
class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
p256 - broken
|
||||
</button>
|
||||
@@ -230,11 +230,25 @@
|
||||
<div v-else>Verify New JWT -- requires creation first</div>
|
||||
<button
|
||||
@click="verifyMyJwt()"
|
||||
class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Verify Hard-Coded JWT
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<h2 class="text-xl font-bold mb-4">Encryption & Decryption</h2>
|
||||
See console for more output.
|
||||
<div>
|
||||
<button
|
||||
@click="testEncryptionDecryption()"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Run Test
|
||||
</button>
|
||||
Result: {{ encryptionTestResult }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -248,6 +262,7 @@ import { Router } from "vue-router";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import { AppString, NotificationIface } from "@/constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
|
||||
import * as cryptoLib from "@/libs/crypto";
|
||||
import * as vcLib from "@/libs/crypto/vc";
|
||||
import {
|
||||
PeerSetup,
|
||||
@@ -279,6 +294,9 @@ const TEST_PAYLOAD = {
|
||||
export default class Help extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
// for encryption/decryption
|
||||
encryptionTestResult?: boolean;
|
||||
|
||||
// for file import
|
||||
fileName?: string;
|
||||
|
||||
@@ -289,6 +307,8 @@ export default class Help extends Vue {
|
||||
peerSetup?: PeerSetup;
|
||||
userName?: string;
|
||||
|
||||
cryptoLib = cryptoLib;
|
||||
|
||||
async mounted() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
@@ -363,6 +383,10 @@ export default class Help extends Vue {
|
||||
this.credIdHex = account.passkeyCredIdHex;
|
||||
}
|
||||
|
||||
public async testEncryptionDecryption() {
|
||||
this.encryptionTestResult = await cryptoLib.testEncryptionDecryption();
|
||||
}
|
||||
|
||||
public async createJwtSimplewebauthn() {
|
||||
const account: AccountKeyInfo | undefined = await retrieveAccountMetadata(
|
||||
this.activeDid || "",
|
||||
|
||||
Reference in New Issue
Block a user