forked from trent_larson/crowd-funder-for-time-pwa
make screen where user can create a group onboarding meeting
This commit is contained in:
@@ -90,7 +90,7 @@
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
|
||||
import { NotificationIface } from "@/constants/app";
|
||||
import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
|
||||
import { createAndSubmitGive, didInfo, serverMessageForUser } from "@/libs/endorserServer";
|
||||
import * as libsUtil from "@/libs/util";
|
||||
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
@@ -336,7 +336,7 @@ export default class GiftedDialog extends Vue {
|
||||
console.error("Error with give recordation caught:", error);
|
||||
const errorMessage =
|
||||
error.userMessage ||
|
||||
error.response?.data?.error?.message ||
|
||||
serverMessageForUser(error) ||
|
||||
"There was an error recording the give.";
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
|
||||
import { NotificationIface } from "@/constants/app";
|
||||
import { createAndSubmitOffer } from "@/libs/endorserServer";
|
||||
import { createAndSubmitOffer, serverMessageForUser } from "@/libs/endorserServer";
|
||||
import * as libsUtil from "@/libs/util";
|
||||
import { retrieveSettingsForActiveAccount } from "@/db/index";
|
||||
|
||||
@@ -304,9 +304,9 @@ export default class OfferDialog extends Vue {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getOfferCreationErrorMessage(result: any) {
|
||||
return (
|
||||
serverMessageForUser(result) ||
|
||||
result.error?.userMessage ||
|
||||
result.error?.error ||
|
||||
result.response?.data?.error?.message
|
||||
result.error?.error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export const newIdentifier = (
|
||||
*
|
||||
*
|
||||
* @param {string} mnemonic
|
||||
* @return {*} {[string, string, string, string]}
|
||||
* @return {[string, string, string, string]} address, privateHex, publicHex, derivationPath
|
||||
*/
|
||||
export const deriveAddress = (
|
||||
mnemonic: string,
|
||||
@@ -88,7 +88,8 @@ export const generateSeed = (): string => {
|
||||
/**
|
||||
* Retrieve an access token, or "" if no DID is provided.
|
||||
*
|
||||
* @return {*}
|
||||
* @param {string} did
|
||||
* @return {string} JWT with basic payload
|
||||
*/
|
||||
export const accessToken = async (did?: string) => {
|
||||
if (did) {
|
||||
@@ -147,3 +148,156 @@ export const nextDerivationPath = (origDerivPath: string) => {
|
||||
.join("/");
|
||||
return newDerivPath;
|
||||
};
|
||||
|
||||
// Base64 encoding/decoding utilities for browser
|
||||
function base64ToArrayBuffer(base64: string): Uint8Array {
|
||||
const binaryString = atob(base64);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
const binary = String.fromCharCode(...new Uint8Array(buffer));
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
const SALT_LENGTH = 16;
|
||||
const IV_LENGTH = 12;
|
||||
const KEY_LENGTH = 256;
|
||||
const ITERATIONS = 100000;
|
||||
|
||||
// Encryption helper function
|
||||
export async function encryptMessage(message: string, password: string) {
|
||||
const encoder = new TextEncoder();
|
||||
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
||||
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
||||
|
||||
// Derive key from password using PBKDF2
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(password),
|
||||
'PBKDF2',
|
||||
false,
|
||||
['deriveBits', 'deriveKey']
|
||||
);
|
||||
|
||||
const key = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt,
|
||||
iterations: ITERATIONS,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
{ name: 'AES-GCM', length: KEY_LENGTH },
|
||||
false,
|
||||
['encrypt']
|
||||
);
|
||||
|
||||
// Encrypt the message
|
||||
const encryptedContent = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv
|
||||
},
|
||||
key,
|
||||
encoder.encode(message)
|
||||
);
|
||||
|
||||
// Return a JSON structure with base64-encoded components
|
||||
const result = {
|
||||
salt: arrayBufferToBase64(salt),
|
||||
iv: arrayBufferToBase64(iv),
|
||||
encrypted: arrayBufferToBase64(encryptedContent)
|
||||
};
|
||||
|
||||
return btoa(JSON.stringify(result));
|
||||
}
|
||||
|
||||
// Decryption helper function
|
||||
export async function decryptMessage(encryptedJson: string, password: string) {
|
||||
const decoder = new TextDecoder();
|
||||
const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson));
|
||||
|
||||
// Convert base64 components back to Uint8Arrays
|
||||
const saltArray = base64ToArrayBuffer(salt);
|
||||
const ivArray = base64ToArrayBuffer(iv);
|
||||
const encryptedContent = base64ToArrayBuffer(encrypted);
|
||||
|
||||
// Derive the same key using PBKDF2 with the extracted salt
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(password),
|
||||
'PBKDF2',
|
||||
false,
|
||||
['deriveBits', 'deriveKey']
|
||||
);
|
||||
|
||||
const key = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: saltArray,
|
||||
iterations: ITERATIONS,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
{ name: 'AES-GCM', length: KEY_LENGTH },
|
||||
false,
|
||||
['decrypt']
|
||||
);
|
||||
|
||||
// Decrypt the content
|
||||
const decryptedContent = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: ivArray
|
||||
},
|
||||
key,
|
||||
encryptedContent
|
||||
);
|
||||
|
||||
// Convert the decrypted content back to a string
|
||||
return decoder.decode(decryptedContent);
|
||||
}
|
||||
|
||||
// Test function to verify encryption/decryption
|
||||
export async function testEncryptionDecryption() {
|
||||
try {
|
||||
const testMessage = "Hello, this is a test message! 🚀";
|
||||
const testPassword = "myTestPassword123";
|
||||
|
||||
console.log("Original message:", testMessage);
|
||||
|
||||
// Test encryption
|
||||
console.log("Encrypting...");
|
||||
const encrypted = await encryptMessage(testMessage, testPassword);
|
||||
console.log("Encrypted result:", encrypted);
|
||||
|
||||
// Test decryption
|
||||
console.log("Decrypting...");
|
||||
const decrypted = await decryptMessage(encrypted, testPassword);
|
||||
console.log("Decrypted result:", decrypted);
|
||||
|
||||
// Verify
|
||||
const success = testMessage === decrypted;
|
||||
console.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
|
||||
console.log("Messages match:", success);
|
||||
|
||||
// Test with wrong password
|
||||
console.log("\nTesting with wrong password...");
|
||||
try {
|
||||
const wrongDecrypted = await decryptMessage(encrypted, "wrongPassword");
|
||||
console.log("Should not reach here");
|
||||
} catch (error) {
|
||||
console.log("Correctly failed with wrong password ✅");
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.error("Test failed with error:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,41 +621,6 @@ const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
|
||||
max: 500,
|
||||
});
|
||||
|
||||
/**
|
||||
* Helpful for server errors, to get all the info -- including stuff skipped by toString & JSON.stringify
|
||||
*
|
||||
* @param error
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function errorStringForLog(error: any) {
|
||||
let stringifiedError = "" + error;
|
||||
try {
|
||||
stringifiedError = JSON.stringify(error);
|
||||
} catch (e) {
|
||||
// can happen with Dexie, eg:
|
||||
// TypeError: Converting circular structure to JSON
|
||||
// --> starting at object with constructor 'DexieError2'
|
||||
// | property '_promise' -> object with constructor 'DexiePromise'
|
||||
// --- property '_value' closes the circle
|
||||
}
|
||||
let fullError = "" + error + " - JSON: " + stringifiedError;
|
||||
const errorResponseText = JSON.stringify(error.response);
|
||||
// for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions)
|
||||
if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) {
|
||||
// add error.response stuff
|
||||
if (R.equals(error?.config, error?.response?.config)) {
|
||||
// but exclude "config" because it's already in there
|
||||
const newErrorResponseText = JSON.stringify(
|
||||
R.omit(["config"] as never[], error.response),
|
||||
);
|
||||
fullError += " - .response w/o same config JSON: " + newErrorResponseText;
|
||||
} else {
|
||||
fullError += " - .response JSON: " + errorResponseText;
|
||||
}
|
||||
}
|
||||
return fullError;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param handleId nullable, in which case "undefined" will be returned
|
||||
* @param requesterDid optional, in which case no private info will be returned
|
||||
@@ -710,6 +675,54 @@ export async function setPlanInCache(
|
||||
planCache.set(handleId, planSummary);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param error that is thrown from an Endorser server call by Axios
|
||||
* @returns user-friendly message, or undefined if none found
|
||||
*/
|
||||
export function serverMessageForUser(error: any) {
|
||||
return (
|
||||
// this is how most user messages are returned
|
||||
error?.response?.data?.error?.message
|
||||
// some are returned as "error" with a string, but those are more for devs and are less helpful to the user
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helpful for server errors, to get all the info -- including stuff skipped by toString & JSON.stringify
|
||||
*
|
||||
* @param error
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function errorStringForLog(error: any) {
|
||||
let stringifiedError = "" + error;
|
||||
try {
|
||||
stringifiedError = JSON.stringify(error);
|
||||
} catch (e) {
|
||||
// can happen with Dexie, eg:
|
||||
// TypeError: Converting circular structure to JSON
|
||||
// --> starting at object with constructor 'DexieError2'
|
||||
// | property '_promise' -> object with constructor 'DexiePromise'
|
||||
// --- property '_value' closes the circle
|
||||
}
|
||||
let fullError = "" + error + " - JSON: " + stringifiedError;
|
||||
const errorResponseText = JSON.stringify(error.response);
|
||||
// for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions)
|
||||
if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) {
|
||||
// add error.response stuff
|
||||
if (R.equals(error?.config, error?.response?.config)) {
|
||||
// but exclude "config" because it's already in there
|
||||
const newErrorResponseText = JSON.stringify(
|
||||
R.omit(["config"] as never[], error.response),
|
||||
);
|
||||
fullError += " - .response w/o same config JSON: " + newErrorResponseText;
|
||||
} else {
|
||||
fullError += " - .response JSON: " + errorResponseText;
|
||||
}
|
||||
}
|
||||
return fullError;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns { data: Array<OfferSummaryRecord>, hitLimit: boolean true if maximum was hit and there may be more }
|
||||
@@ -1113,7 +1126,7 @@ export async function createAndSubmitClaim(
|
||||
} catch (error: any) {
|
||||
console.error("Error submitting claim:", error);
|
||||
const errorMessage: string =
|
||||
error.response?.data?.error?.message ||
|
||||
serverMessageForUser(error) ||
|
||||
error.message ||
|
||||
"Got some error submitting the claim. Check your permissions, network, and error logs.";
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
faCalendar,
|
||||
faCamera,
|
||||
faCaretDown,
|
||||
faChair,
|
||||
faCheck,
|
||||
faChevronDown,
|
||||
faChevronLeft,
|
||||
@@ -100,6 +101,7 @@ library.add(
|
||||
faCalendar,
|
||||
faCamera,
|
||||
faCaretDown,
|
||||
faChair,
|
||||
faCheck,
|
||||
faChevronDown,
|
||||
faChevronLeft,
|
||||
|
||||
@@ -179,6 +179,11 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: "offer-details",
|
||||
component: () => import("../views/OfferDetailsView.vue"),
|
||||
},
|
||||
{
|
||||
path: '/onboard-meeting',
|
||||
name: 'onboard-meeting',
|
||||
component: () => import('../views/OnboardMeetingView.vue'),
|
||||
},
|
||||
{
|
||||
path: "/project/:id?",
|
||||
name: "project",
|
||||
|
||||
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