You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

828 lines
24 KiB

<template>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Test
</h1>
</div>
<div v-if="isNotProdServer">
<h2 class="text-xl font-bold mb-4">User Registration</h2>
<button :class="primaryButtonClasses" @click="registerMe()">
Register Yourself
</button>
<button :class="primaryButtonClasses" @click="becomeUser0()">
Become User 0 (who can register others)
</button>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">Notiwind Alerts</h2>
<!-- Notification test buttons using computed configuration -->
<button
v-for="config in notificationTestButtons"
:key="config.label"
:class="config.classes"
@click="triggerTestNotification(config)"
>
{{ config.label }}
</button>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">SQL Operations</h2>
<div class="flex gap-2 mt-2">
<button :class="sqlLinkClasses" @click="setAllTablesQuery">
All Tables
</button>
<button :class="sqlLinkClasses" @click="setAccountsQuery">
Accounts
</button>
<button :class="sqlLinkClasses" @click="setContactsQuery">
Contacts
</button>
<button :class="sqlLinkClasses" @click="setSettingsQuery">
Settings
</button>
</div>
<div>
<textarea
v-model="sqlQuery"
class="w-full h-32 p-2 border border-gray-300 rounded-md font-mono"
placeholder="Enter your SQL query here..."
></textarea>
</div>
<div class="mt-4">
<button :class="primaryButtonClasses" @click="executeSql">
Execute
</button>
</div>
<div v-if="sqlResult" class="mt-4">
<h3 class="text-lg font-semibold mb-2">Result:</h3>
<pre class="bg-gray-100 p-4 rounded-md overflow-x-auto">{{
JSON.stringify(sqlResult, null, 2)
}}</pre>
</div>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">Image Sharing</h2>
Populates the "shared-photo" view as if they used "share_target".
<input type="file" data-testId="fileInput" @change="uploadFile" />
<router-link
v-if="showFileNextStep()"
:to="{
name: 'shared-photo',
query: { fileName },
}"
class="block w-full text-center text-md 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-2 rounded-md mb-2 mt-2"
data-testId="fileUploadButton"
>
Go to Shared Page
</router-link>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">Passkeys</h2>
See console for results.
<br />
See existing passkeys in Chrome at: chrome://settings/passkeys
<br />
Active DID: {{ activeDIDDisplay }}
{{ passkeyStatusDisplay }}
<div>
Register Passkey
<button :class="primaryButtonClasses" @click="registerPasskey()">
Simplewebauthn
</button>
</div>
<div>
Create JWT
<button
:class="primaryButtonClasses"
@click="createJwtSimplewebauthn()"
>
Simplewebauthn
</button>
<button :class="primaryButtonClasses" @click="createJwtNavigator()">
Navigator
</button>
</div>
<div v-if="jwt">
Verify New JWT
<button :class="primaryButtonClasses" @click="verifySimplewebauthn()">
Simplewebauthn
</button>
<button :class="primaryButtonClasses" @click="verifyWebCrypto()">
WebCrypto
</button>
<button :class="primaryButtonClasses" @click="verifyP256()">
p256 - broken
</button>
</div>
<div v-else>Verify New JWT -- requires creation first</div>
<button :class="primaryButtonClasses" @click="verifyMyJwt()">
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
:class="primaryButtonClasses"
@click="testMessageEncryptionDecryption()"
>
Run Test for Message Encryption/Decryption
</button>
{{ encryptionTestResultDisplay }}
</div>
<div>
<button
:class="primaryButtonClasses"
@click="testSimpleEncryptionDecryption()"
>
Run Test for Simple Encryption/Decryption
</button>
{{ simpleEncryptionTestResultDisplay }}
</div>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">Component Tests</h2>
Interactive tests for Vue components and their functionality.
<div class="mt-4">
<h3 class="text-lg font-semibold mb-2">EntityGrid Function Props</h3>
<p class="text-sm text-gray-600 mb-3">
Test the new function prop functionality in EntityGrid component.
</p>
<button
:class="primaryButtonClasses"
@click="showEntityGridTest = !showEntityGridTest"
>
{{ showEntityGridTest ? "Hide" : "Show" }} EntityGrid Function Prop
Test
</button>
<div
v-if="showEntityGridTest"
class="mt-4 p-4 border border-gray-300 rounded-md bg-gray-50"
>
<EntityGridFunctionPropTest />
</div>
</div>
<div class="mt-4">
<h3 class="text-lg font-semibold mb-2">Platform Service Mixin</h3>
<p class="text-sm text-gray-600 mb-3">
Test database operations through PlatformServiceMixin.
</p>
<button
:class="primaryButtonClasses"
@click="showPlatformServiceTest = !showPlatformServiceTest"
>
{{ showPlatformServiceTest ? "Hide" : "Show" }} Platform Service Test
</button>
<div
v-if="showPlatformServiceTest"
class="mt-4 p-4 border border-gray-300 rounded-md bg-gray-50"
>
<PlatformServiceMixinTest />
</div>
</div>
</div>
</section>
</template>
<script lang="ts">
import { Buffer } from "buffer/";
import { Base64URLString } from "@simplewebauthn/types";
import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { AppString, NotificationIface } from "../constants/app";
import {
NOTIFY_SQL_ERROR,
createSqlErrorMessage,
createPasskeyNameModal,
} from "../constants/notifications";
import * as vcLib from "../libs/crypto/vc";
import * as cryptoLib from "../libs/crypto";
import {
PeerSetup,
verifyJwtP256,
verifyJwtSimplewebauthn,
verifyJwtWebCrypto,
} from "../libs/crypto/vc/passkeyDidPeer";
import {
blobToBase64,
retrieveAccountMetadata,
registerAndSavePasskey,
SHARED_PHOTO_BASE64_KEY,
} from "../libs/util";
import { testBecomeUser0, testServerRegisterUser } from "@/test";
import { logger } from "../utils/logger";
import { Account } from "../db/tables/accounts";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import EntityGridFunctionPropTest from "../test/EntityGridFunctionPropTest.vue";
import PlatformServiceMixinTest from "../test/PlatformServiceMixinTest.vue";
const inputFileNameRef = ref<Blob>();
const TEST_PAYLOAD = {
vc: {
credentialSubject: {
"@context": "https://schema.org",
"@type": "GiveAction",
description: "pizza",
},
},
};
/**
* TestView Component
*
* Development/testing interface providing comprehensive testing tools for:
* - Notification system testing (8 different types)
* - Interactive SQL operations and database queries
* - File upload and image sharing functionality
* - Passkey registration and JWT verification
* - Encryption/decryption testing
* - Various crypto operations
*
* Features:
* - Raw SQL query execution interface for database testing
* - Notification type demonstrations
* - Passkey and JWT verification workflows
* - File upload with temporary storage
* - Crypto library testing utilities
*
* Security Considerations:
* - Test environment only - not for production use
* - SQL operations are intentionally raw for testing purposes
* - File uploads stored temporarily for testing workflows
*
* @author Matthew Raymer
*/
@Component({
components: {
QuickNav,
EntityGridFunctionPropTest,
PlatformServiceMixinTest,
},
mixins: [PlatformServiceMixin],
})
export default class Help extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
// for encryption/decryption
messageEncryptionTestResult?: boolean;
simpleEncryptionTestResult?: boolean;
// for file import
fileName?: string;
// for passkeys
credIdHex?: string;
activeDid?: string;
apiServer?: string;
jwt?: string;
peerSetup?: PeerSetup;
userName?: string;
// for SQL operations
sqlQuery = "";
sqlResult: unknown = null;
cryptoLib = cryptoLib;
// for component tests
showEntityGridTest = false;
showPlatformServiceTest = false;
/**
* Computed properties for template streamlining
* Eliminates repeated classes and logic in template
*/
/**
* Standard button class for primary actions
*/
get primaryButtonClasses(): string {
return "font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2";
}
/**
* Dark button class for primary test actions
*/
get darkButtonClasses(): string {
return "font-bold capitalize bg-slate-900 text-white px-3 py-2 rounded-md mr-2";
}
/**
* Secondary button class for secondary test actions
*/
get secondaryButtonClasses(): string {
return "font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2";
}
/**
* Success button class for success notifications
*/
get successButtonClasses(): string {
return "font-bold capitalize bg-emerald-600 text-white px-3 py-2 rounded-md mr-2";
}
/**
* Warning button class for warning notifications
*/
get warningButtonClasses(): string {
return "font-bold capitalize bg-amber-600 text-white px-3 py-2 rounded-md mr-2";
}
/**
* Danger button class for danger notifications
*/
get dangerButtonClasses(): string {
return "font-bold capitalize bg-rose-600 text-white px-3 py-2 rounded-md mr-2";
}
/**
* SQL link button class for inline SQL query buttons
*/
get sqlLinkClasses(): string {
return "text-sm text-blue-600 hover:text-blue-800 underline";
}
/**
* Formatted display of active DID status
*/
get activeDIDDisplay(): string {
return this.activeDid || "nothing, which";
}
/**
* Formatted display of passkey status
*/
get passkeyStatusDisplay(): string {
return this.credIdHex ? "has a passkey ID" : "has no passkey ID";
}
/**
* Formatted display of encryption test result
*/
get encryptionTestResultDisplay(): string {
return this.messageEncryptionTestResult !== undefined
? `Result: ${this.messageEncryptionTestResult}`
: "Result: Not tested";
}
/**
* Formatted display of simple encryption test result
*/
get simpleEncryptionTestResultDisplay(): string {
return this.simpleEncryptionTestResult !== undefined
? `Result: ${this.simpleEncryptionTestResult}`
: "Result: Not tested";
}
/**
* SQL query presets for template buttons
* Extracts inline SQL assignments from template for better organization
*/
setAllTablesQuery() {
this.sqlQuery = "SELECT * FROM sqlite_master WHERE type='table';";
this.executeSql();
}
setAccountsQuery() {
this.sqlQuery = "SELECT * FROM accounts;";
this.executeSql();
}
setContactsQuery() {
this.sqlQuery = "SELECT * FROM contacts;";
this.executeSql();
}
setSettingsQuery() {
this.sqlQuery = "SELECT * FROM settings;";
this.executeSql();
}
/**
* Configuration for notification test buttons
* Eliminates repetitive notification button definitions in template
*/
get notificationTestButtons() {
return [
{
label: "Toast",
classes: this.darkButtonClasses,
notification: {
group: "alert",
type: "toast",
title: "Toast",
text: "I'm a toast. Without a timeout, I'm stuck.",
},
timeout: 5000,
},
{
label: "Info",
classes: this.secondaryButtonClasses,
notification: {
group: "alert",
type: "info",
title: "Information Alert",
text: "Just wanted you to know.",
},
timeout: 5000,
},
{
label: "Success",
classes: this.successButtonClasses,
notification: {
group: "alert",
type: "success",
title: "Success Alert",
text: "Congratulations!",
},
timeout: 5000,
},
{
label: "Warning",
classes: this.warningButtonClasses,
notification: {
group: "alert",
type: "warning",
title: "Warning Alert",
text: "You might wanna look at this.",
},
timeout: 5000,
},
{
label: "Danger",
classes: this.dangerButtonClasses,
notification: {
group: "alert",
type: "danger",
title: "Danger Alert",
text: "Something terrible has happened!",
},
timeout: 5000,
},
{
label: "Notif ON",
classes: this.secondaryButtonClasses,
notification: {
group: "modal",
type: "notification-permission",
title: "Notification Permission",
text: "Enable notifications?",
},
timeout: -1,
},
{
label: "Notif MUTE",
classes: this.secondaryButtonClasses,
notification: {
group: "modal",
type: "notification-mute",
title: "Notification Settings",
text: "Notifications muted",
},
timeout: -1,
},
{
label: "Notif OFF",
classes: this.secondaryButtonClasses,
notification: {
group: "modal",
type: "notification-off",
title: "Notifications",
text: "Notifications turned off",
},
timeout: -1,
},
];
}
/**
* Component initialization
*
* Loads user settings and account information for testing interface
* Uses PlatformServiceMixin for database access
*/
async mounted() {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.userName = settings.firstName;
const account = await retrieveAccountMetadata(this.activeDid);
if (this.activeDid) {
if (account) {
this.credIdHex = account.passkeyCredIdHex as string;
} else {
alert("No account found for DID " + this.activeDid);
}
}
}
/**
* Checks if running on production server
*
* @returns True if not on production server (enables test utilities)
*/
public isNotProdServer() {
return this.apiServer !== AppString.PROD_ENDORSER_API_SERVER;
}
async registerMe() {
const response = await testServerRegisterUser();
if (response.status === 201) {
alert("Registration successful.");
this.$router.push({ name: "home" }); // because this page checks for registered status and sets things if it detects a change
} else {
logger.error("Registration failure response:", response);
alert("Registration failed: " + (response.data.error || response.data));
}
}
async becomeUser0() {
await testBecomeUser0();
alert("You are now User 0.");
this.$router.push({ name: "home" }); // because this page checks for registered status and sets things if it detects a change
}
/**
* Method to trigger notification test
* Centralizes notification testing logic
*/
triggerTestNotification(config: {
notification: NotificationIface;
timeout?: number;
}) {
this.$notify(config.notification, config.timeout);
}
/**
* Handles file upload for image sharing tests
*
* Processes uploaded files and stores them in temp table for shared photo testing
* Uses PlatformServiceMixin service methods for temp table operations
*/
async uploadFile(event: Event) {
const target = event.target as HTMLInputElement;
inputFileNameRef.value = target.files?.[0];
// https://developer.mozilla.org/en-US/docs/Web/API/File
// ... plus it has a `type` property from my testing
const file = inputFileNameRef.value;
if (file != null) {
const reader = new FileReader();
reader.onload = async (e) => {
const data = e.target?.result as ArrayBuffer;
if (data) {
const blob = new Blob([new Uint8Array(data)], {
type: file.type,
});
const blobB64 = await blobToBase64(blob);
this.fileName = (file as File).name;
// Use service methods for temp table operations
const temp = await this.$getTemp(SHARED_PHOTO_BASE64_KEY);
if (temp) {
await this.$updateEntity("temp", { blobB64 }, "id = ?", [
SHARED_PHOTO_BASE64_KEY,
]);
} else {
await this.$insertEntity(
"temp",
{ id: SHARED_PHOTO_BASE64_KEY, blobB64 },
["id", "blobB64"],
);
}
}
};
reader.readAsArrayBuffer(file as Blob);
}
}
/**
* Checks if file upload next step should be shown
*/
showFileNextStep() {
return !!inputFileNameRef.value;
}
/**
* Handles passkey registration for testing
*
* Creates new passkey with user name or default test name
* Includes validation and user confirmation workflow
* Uses notification helpers for consistent messaging
*/
public async registerPasskey() {
const DEFAULT_USERNAME = AppString.APP_NAME + " Tester";
if (!this.userName) {
const modalConfig = createPasskeyNameModal(
DEFAULT_USERNAME,
async () => {
this.userName = DEFAULT_USERNAME;
},
async () => {
this.$router.push({ name: "new-edit-account" });
},
);
this.$notify(modalConfig, -1);
return;
}
const account = await registerAndSavePasskey(
AppString.APP_NAME + " - " + this.userName,
);
this.activeDid = account.did;
this.credIdHex = account.passkeyCredIdHex;
}
/**
* Tests message encryption/decryption functionality
*/
public async testMessageEncryptionDecryption() {
this.messageEncryptionTestResult =
await cryptoLib.testMessageEncryptionDecryption();
}
/**
* Tests simple encryption/decryption functionality
*/
public async testSimpleEncryptionDecryption() {
this.simpleEncryptionTestResult =
await cryptoLib.testSimpleEncryptionDecryption();
}
/**
* Creates JWT using SimpleWebAuthn for testing
*/
public async createJwtSimplewebauthn() {
const account: Account | undefined = await retrieveAccountMetadata(
this.activeDid || "",
);
if (!vcLib.isFromPasskey(account)) {
alert(`The DID ${this.activeDid} is not passkey-enabled.`);
return;
}
this.peerSetup = new PeerSetup();
this.jwt = await this.peerSetup.createJwtSimplewebauthn(
this.activeDid as string,
TEST_PAYLOAD,
this.credIdHex as string,
);
logger.log("simple jwt4url", this.jwt);
}
/**
* Creates JWT using Navigator API for testing
*/
public async createJwtNavigator() {
const account: Account | undefined = await retrieveAccountMetadata(
this.activeDid || "",
);
if (!vcLib.isFromPasskey(account)) {
alert(`The DID ${this.activeDid} is not passkey-enabled.`);
return;
}
this.peerSetup = new PeerSetup();
this.jwt = await this.peerSetup.createJwtNavigator(
this.activeDid as string,
TEST_PAYLOAD,
this.credIdHex as string,
);
logger.log("lower jwt4url", this.jwt);
}
/**
* Verifies JWT using P256 algorithm for testing
*/
public async verifyP256() {
const decoded = await verifyJwtP256(
this.activeDid as string,
this.peerSetup?.authenticatorData as ArrayBuffer,
this.peerSetup?.challenge as Uint8Array,
this.peerSetup?.signature as Base64URLString,
);
logger.log("decoded", decoded);
}
/**
* Verifies JWT using SimpleWebAuthn for testing
*/
public async verifySimplewebauthn() {
const decoded = await verifyJwtSimplewebauthn(
this.credIdHex as string,
this.activeDid as string,
this.peerSetup?.authenticatorData as ArrayBuffer,
this.peerSetup?.challenge as Uint8Array,
this.peerSetup?.clientDataJsonBase64Url as Base64URLString,
this.peerSetup?.signature as Base64URLString,
);
logger.log("decoded", decoded);
}
/**
* Verifies JWT using WebCrypto for testing
*/
public async verifyWebCrypto() {
const decoded = await verifyJwtWebCrypto(
this.activeDid as string,
this.peerSetup?.authenticatorData as ArrayBuffer,
this.peerSetup?.challenge as Uint8Array,
this.peerSetup?.signature as Base64URLString,
);
logger.log("decoded", decoded);
}
/**
* Verifies hard-coded JWT for testing purposes
*/
public async verifyMyJwt() {
const did =
"did:peer:0zKMFjvUgYrM1hXwDciYHiA9MxXtJPXnRLJvqoMNAKoDLX9pKMWLb3VDsgua1p2zW1xXRsjZSTNsfvMnNyMS7dB4k7NAhFwL3pXBrBXgyYJ9ri";
const jwt =
"eyJ0eXAiOiJKV0FOVCIsImFsZyI6IkVTMjU2In0.eyJBdXRoZW50aWNhdGlvbkRhdGFCNjRVUkwiOiJTWllONVlnT2pHaDBOQmNQWkhaZ1c0X2tycm1paGpMSG1Wenp1b01kbDJNRkFBQUFBQSIsIkNsaWVudERhdGFKU09OQjY0VVJMIjoiZXlKMGVYQmxJam9pZDJWaVlYVjBhRzR1WjJWMElpd2lZMmhoYkd4bGJtZGxJam9pWlhsS01sbDVTVFpsZVVwcVkyMVdhMXBYTlRCaFYwWnpWVE5XYVdGdFZtcGtRMGsyWlhsS1FWa3lPWFZrUjFZMFpFTkpOa2x0YURCa1NFSjZUMms0ZG1NeVRtOWFWekZvVEcwNWVWcDVTWE5KYTBJd1pWaENiRWxxYjJsU01td3lXbFZHYW1SSGJIWmlhVWx6U1cxU2JHTXlUbmxoV0VJd1lWYzVkVWxxYjJsalIydzJaVzFGYVdaWU1ITkpiV3hvWkVOSk5rMVVZM2hQUkZVMFRtcHJOVTFEZDJsaFdFNTZTV3B2YVZwSGJHdFBia0pzV2xoSk5rMUljRXhVVlZweFpHeFdibGRZU2s1TlYyaFpaREJTYW1GV2JFbGhWVVUxVkZob1dXUkZjRkZYUnpWVFZFVndNbU5YT1U1VWEwWk1ZakJTVFZkRWJIZFRNREZZVkVkSmVsWnJVbnBhTTFab1RWaEJlV1ZzWTNobFJtaFRZekp3WVZVeFVrOWpNbG95VkZjMVQyVlZNVlJPTWxKRFRrZHpNMVJyUm05U2JtUk5UVE5DV1ZGdVNrTlhSMlExVjFWdk5XTnRhMmxtVVNJc0ltOXlhV2RwYmlJNkltaDBkSEE2THk5c2IyTmhiR2h2YzNRNk9EQTRNQ0lzSW1OeWIzTnpUM0pwWjJsdUlqcG1ZV3h6WlgwIiwiaWF0IjoxNzE4NTg2OTkyLCJpc3MiOiJkaWQ6cGVlcjowektNRmp2VWdZck0xaFh3RGNpWUhpQTlNeFh0SlBYblJMSnZxb01OQUtvRExYOXBLTVdMYjNWRHNndWExcDJ6VzF4WFJzalpTVE5zZnZNbk55TVM3ZEI0azdOQWhGd0wzcFhCckJYZ3lZSjlyaSJ9.MEUCIQDJyCTbMPIFnuBoW3FYnlgtDEIHZ2OrkCEvqVnHU7kJDQIgVxjBjfW1TwQfcSOYwK8Z7AdCWGJlyxtLEsrnPif7caE";
const pieces = jwt.split(".");
const payload = JSON.parse(Buffer.from(pieces[1], "base64").toString());
const authData = Buffer.from(payload["AuthenticationDataB64URL"], "base64");
const clientJSON = Buffer.from(
payload["ClientDataJSONB64URL"],
"base64",
).toString();
const clientData = JSON.parse(clientJSON);
const challenge = clientData.challenge;
const signatureB64URL = pieces[2];
const decoded = await verifyJwtWebCrypto(
did,
authData,
challenge,
signatureB64URL,
);
logger.log("decoded", decoded);
}
/**
* Executes SQL queries for testing database operations
*
* Supports both SELECT queries (dbQuery) and other SQL commands (dbExec)
* Provides interface for testing raw SQL operations
* Uses PlatformServiceMixin for database access and notification helpers for errors
*/
async executeSql() {
try {
const isSelect = this.sqlQuery.trim().toLowerCase().startsWith("select");
if (isSelect) {
this.sqlResult = await this.$query(this.sqlQuery);
} else {
this.sqlResult = await this.$exec(this.sqlQuery);
}
logger.log("Test SQL Result:", this.sqlResult);
} catch (error) {
logger.error("Test SQL Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: NOTIFY_SQL_ERROR.title,
text: createSqlErrorMessage(error),
},
5000,
);
}
}
}
</script>