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.
 
 
 
 
 
 

294 lines
8.5 KiB

<template>
<QuickNav selected="Invite" />
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<div
v-if="checkingInvite"
class="text-lg text-center font-light relative px-7"
>
<font-awesome icon="spinner" class="fa-spin-pulse" />
</div>
<div v-else class="text-center mt-4">
<p>That invitation did not work.</p>
<p class="mt-2">
Go back to your invite message and copy the entire text, then paste it
here.
</p>
<p class="mt-2">
If the data looks correct, try Chrome. (For example, iOS may have cut
off the invite data, or it may have shown a preview that stole your
invite.) If it still complains, you may need the person who invited you
to send a new one.
</p>
<textarea
v-model="inputJwt"
placeholder="Paste invitation..."
class="mt-4 border-2 border-gray-300 p-2 rounded"
cols="30"
@input="() => checkInvite(inputJwt)"
/>
<br />
<button
class="ml-2 p-2 bg-blue-500 text-white rounded"
@click="() => processInvite(inputJwt, true)"
>
Accept
</button>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router, RouteLocationNormalized } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { APP_SERVER, NotificationIface } from "../constants/app";
import {
db,
logConsoleAndDb,
retrieveSettingsForActiveAccount,
} from "../db/index";
import { decodeEndorserJwt } from "../libs/crypto/vc";
import { errorStringForLog } from "../libs/endorserServer";
import { generateSaveAndActivateIdentity } from "../libs/util";
/**
* 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;
/** 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 || "";
if (!this.activeDid) {
this.activeDid = await generateSaveAndActivateIdentity();
}
// Extract JWT from route path
const jwt = (this.$route.params.jwt as string) || "";
await this.processInvite(jwt, false);
this.checkingInvite = false;
}
/**
* 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 {
const jwt = this.extractJwtFromInput(jwtInput);
if (!jwt) {
this.handleMissingJwt(notifyOnFailure);
return;
}
await this.validateAndRedirect(jwt);
} catch (error) {
this.handleError(error, notifyOnFailure);
} finally {
this.checkingInvite = false;
}
}
/**
* 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) ||
jwtInput.endsWith(APP_SERVER + "/") ||
jwtInput.endsWith("invite-one-accept") ||
jwtInput.endsWith("invite-one-accept/")
) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "That is only part of the invite data; it's missing some at the end. Try another way to get the full data.",
},
5000,
);
}
}
}
</script>