forked from jsnbuchanan/crowd-funder-for-time-pwa
test: enhance deep link testing with real JWT examples
Changes: - Add real JWT example for invite testing - Add detailed JWT payload documentation - Update test-deeplinks.sh with valid claim IDs - Add test case for single contact invite - Improve test descriptions and organization This improves test coverage by using real-world JWT examples and valid claim identifiers.
This commit is contained in:
@@ -39,7 +39,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { Router, RouteLocationNormalized } from "vue-router";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||
@@ -52,19 +52,69 @@ import { decodeEndorserJwt } from "../libs/crypto/vc";
|
||||
import { errorStringForLog } from "../libs/endorserServer";
|
||||
import { generateSaveAndActivateIdentity } from "../libs/util";
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
/**
|
||||
* Invite One Accept View Component
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* This component handles accepting single-use invitations to join the platform.
|
||||
* It supports multiple invitation formats and provides user feedback during the process.
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Component loads with JWT from route or user input
|
||||
* 2. Validates JWT format and signature
|
||||
* 3. Processes invite data and redirects to contacts page
|
||||
* 4. Handles errors with user feedback
|
||||
*
|
||||
* Supported Invite Formats:
|
||||
* 1. Direct JWT in URL path: /invite-one-accept/{jwt}
|
||||
* 2. JWT in text message URL: https://app.example.com/invite-one-accept/{jwt}
|
||||
* 3. JWT surrounded by other text: "Your invite code is {jwt}"
|
||||
*
|
||||
* Security Features:
|
||||
* - JWT validation
|
||||
* - Identity generation if needed
|
||||
* - Error handling for invalid/expired invites
|
||||
*
|
||||
* @see ContactsView for completion of invite process
|
||||
*/
|
||||
@Component({
|
||||
components: { QuickNav },
|
||||
})
|
||||
export default class InviteOneAcceptView extends Vue {
|
||||
/** Notification function injected by Vue */
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
/** Router instance for navigation */
|
||||
$router!: Router;
|
||||
/** Route instance for current route */
|
||||
$route!: RouteLocationNormalized;
|
||||
|
||||
activeDid: string = "";
|
||||
apiServer: string = "";
|
||||
checkingInvite: boolean = true;
|
||||
inputJwt: string = "";
|
||||
/** Active user's DID */
|
||||
activeDid = "";
|
||||
/** API server endpoint */
|
||||
apiServer = "";
|
||||
/** Loading state for invite processing */
|
||||
checkingInvite = true;
|
||||
/** User input for manual JWT entry */
|
||||
inputJwt = "";
|
||||
|
||||
/**
|
||||
* Component lifecycle hook that initializes invite processing
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Opens database connection
|
||||
* 2. Retrieves account settings
|
||||
* 3. Ensures active DID exists or generates one
|
||||
* 4. Extracts JWT from URL path
|
||||
* 5. Processes invite automatically
|
||||
*
|
||||
* @throws Will not throw but logs errors
|
||||
* @emits Notifications on errors
|
||||
*/
|
||||
async mounted() {
|
||||
this.checkingInvite = true;
|
||||
await db.open();
|
||||
|
||||
// Load or generate identity
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
@@ -73,81 +123,155 @@ export default class InviteOneAcceptView extends Vue {
|
||||
this.activeDid = await generateSaveAndActivateIdentity();
|
||||
}
|
||||
|
||||
const jwt = window.location.pathname.substring(
|
||||
"/invite-one-accept/".length,
|
||||
);
|
||||
// Extract JWT from route path
|
||||
const jwt = (this.$route.params.jwt as string) || "";
|
||||
await this.processInvite(jwt, false);
|
||||
|
||||
this.checkingInvite = false;
|
||||
}
|
||||
|
||||
// process the invite JWT and/or text message containing the URL with the JWT
|
||||
/**
|
||||
* Processes an invite JWT and/or text containing the invite
|
||||
*
|
||||
* Handles multiple input formats:
|
||||
* 1. Direct JWT:
|
||||
* - Raw JWT string starting with "ey"
|
||||
* - Example: eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ...
|
||||
*
|
||||
* 2. URL containing JWT:
|
||||
* - Full URL with JWT in path
|
||||
* - Pattern: /invite-one-accept/{jwt}
|
||||
* - Example: https://app.example.com/invite-one-accept/eyJ0eXAiOiJKV1Q...
|
||||
*
|
||||
* 3. Text with embedded JWT:
|
||||
* - JWT surrounded by other text
|
||||
* - Uses regex to extract JWT pattern
|
||||
* - Example: "Your invite code is eyJ0eXAiOiJKV1Q... Click to accept"
|
||||
*
|
||||
* Extraction Process:
|
||||
* 1. First attempts URL pattern match
|
||||
* 2. If no URL found, looks for JWT pattern (ey...)
|
||||
* 3. Validates extracted JWT format
|
||||
* 4. Redirects to contacts page on success
|
||||
*
|
||||
* Error Handling:
|
||||
* - Missing JWT: Shows "Missing Invite" notification
|
||||
* - Invalid JWT: Logs error and shows generic error message
|
||||
* - Network Issues: Captured in try/catch block
|
||||
*
|
||||
* @param jwtInput Raw input that may contain a JWT
|
||||
* @param notifyOnFailure Whether to show error notifications
|
||||
* - true: Shows UI notifications for errors
|
||||
* - false: Silently logs errors (used for auto-processing)
|
||||
* @throws Will not throw but logs errors
|
||||
* @emits Notifications on errors if notifyOnFailure is true
|
||||
* @emits Router navigation on success to /contacts?inviteJwt={jwt}
|
||||
*/
|
||||
async processInvite(jwtInput: string, notifyOnFailure: boolean) {
|
||||
this.checkingInvite = true;
|
||||
|
||||
try {
|
||||
let jwt: string = jwtInput ?? "";
|
||||
|
||||
// parse the string: extract the URL or JWT if surrounded by spaces
|
||||
// and then extract the JWT from the URL
|
||||
// (For another approach used with contacts, see getContactPayloadFromJwtUrl)
|
||||
const urlMatch = jwtInput.match(/(https?:\/\/[^\s]+)/);
|
||||
if (urlMatch && urlMatch[1]) {
|
||||
// extract the JWT from the URL, meaning any character except "?"
|
||||
const internalMatch = urlMatch[1].match(/\/invite-one-accept\/([^?]+)/);
|
||||
if (internalMatch && internalMatch[1]) {
|
||||
jwt = internalMatch[1];
|
||||
}
|
||||
} else {
|
||||
// extract the JWT (which starts with "ey") if it is surrounded by other input
|
||||
const spaceMatch = jwtInput.match(/(ey[\w.-]+)/);
|
||||
if (spaceMatch && spaceMatch[1]) {
|
||||
jwt = spaceMatch[1];
|
||||
}
|
||||
}
|
||||
const jwt = this.extractJwtFromInput(jwtInput);
|
||||
|
||||
if (!jwt) {
|
||||
if (notifyOnFailure) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Missing Invite",
|
||||
text: "There was no invite. Paste the entire text that has the data.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
//const payload: JWTPayload =
|
||||
decodeEndorserJwt(jwt);
|
||||
this.handleMissingJwt(notifyOnFailure);
|
||||
return;
|
||||
}
|
||||
|
||||
// That's good enough for an initial check.
|
||||
// Send them to the contacts page to finish, with inviteJwt in the query string.
|
||||
this.$router.push({
|
||||
name: "contacts",
|
||||
query: { inviteJwt: jwt },
|
||||
});
|
||||
}
|
||||
await this.validateAndRedirect(jwt);
|
||||
} catch (error) {
|
||||
const fullError = "Error accepting invite: " + errorStringForLog(error);
|
||||
logConsoleAndDb(fullError, true);
|
||||
if (notifyOnFailure) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was an error processing that invite.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
this.handleError(error, notifyOnFailure);
|
||||
} finally {
|
||||
this.checkingInvite = false;
|
||||
}
|
||||
this.checkingInvite = false;
|
||||
}
|
||||
|
||||
// check the invite JWT
|
||||
/**
|
||||
* Extracts JWT from various input formats
|
||||
* @param input Raw input text
|
||||
* @returns Extracted JWT or empty string
|
||||
*/
|
||||
private extractJwtFromInput(input: string): string {
|
||||
const jwtInput = input ?? "";
|
||||
|
||||
// Try URL format first
|
||||
const urlMatch = jwtInput.match(/(https?:\/\/[^\s]+)/);
|
||||
if (urlMatch?.[1]) {
|
||||
const internalMatch = urlMatch[1].match(/\/invite-one-accept\/([^?]+)/);
|
||||
if (internalMatch?.[1]) return internalMatch[1];
|
||||
}
|
||||
|
||||
// Try direct JWT format
|
||||
const spaceMatch = jwtInput.match(/(ey[\w.-]+)/);
|
||||
if (spaceMatch?.[1]) return spaceMatch[1];
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates JWT and redirects to contacts page
|
||||
* @param jwt JWT to validate
|
||||
*/
|
||||
private async validateAndRedirect(jwt: string) {
|
||||
decodeEndorserJwt(jwt);
|
||||
this.$router.push({
|
||||
name: "contacts",
|
||||
query: { inviteJwt: jwt },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles missing JWT error
|
||||
* @param notify Whether to show notification
|
||||
*/
|
||||
private handleMissingJwt(notify: boolean) {
|
||||
if (notify) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Missing Invite",
|
||||
text: "There was no invite. Paste the entire text that has the data.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles processing errors
|
||||
* @param error Error that occurred
|
||||
* @param notify Whether to show notification
|
||||
*/
|
||||
private handleError(error: unknown, notify: boolean) {
|
||||
const fullError = "Error accepting invite: " + errorStringForLog(error);
|
||||
logConsoleAndDb(fullError, true);
|
||||
|
||||
if (notify) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was an error processing that invite.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates invite data format
|
||||
*
|
||||
* Checks for common error cases:
|
||||
* - Truncated URLs
|
||||
* - Missing JWT data
|
||||
* - Invalid URL formats
|
||||
*
|
||||
* @param jwtInput Raw input to validate
|
||||
* @throws Will not throw but shows notifications
|
||||
* @emits Notifications on validation errors
|
||||
*/
|
||||
async checkInvite(jwtInput: string) {
|
||||
if (
|
||||
jwtInput.endsWith(APP_SERVER) ||
|
||||
|
||||
Reference in New Issue
Block a user