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.
336 lines
10 KiB
336 lines
10 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="handleInputChange"
|
|
/>
|
|
<br />
|
|
<button
|
|
class="ml-2 p-2 bg-blue-500 text-white rounded"
|
|
@click="handleAcceptClick"
|
|
>
|
|
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 { logConsoleAndDb } from "../db/index";
|
|
import { decodeEndorserJwt } from "../libs/crypto/vc";
|
|
import { errorStringForLog } from "../libs/endorserServer";
|
|
import { generateSaveAndActivateIdentity } from "../libs/util";
|
|
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
|
import { logger } from "../utils/logger";
|
|
import {
|
|
NOTIFY_INVITE_MISSING,
|
|
NOTIFY_INVITE_PROCESSING_ERROR,
|
|
NOTIFY_INVITE_TRUNCATED_DATA,
|
|
} from "../constants/notifications";
|
|
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
|
|
|
|
/**
|
|
* @file InviteOneAcceptView.vue
|
|
* @description Invitation acceptance flow for single-use invitations to join the platform.
|
|
* Processes JWTs from various sources (URL, text input) and redirects to contacts page
|
|
* for completion of the invitation process.
|
|
* @author Matthew Raymer
|
|
*/
|
|
|
|
/**
|
|
* 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 },
|
|
mixins: [PlatformServiceMixin],
|
|
})
|
|
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;
|
|
|
|
// Notification helper system
|
|
private notify = createNotifyHelpers(this.$notify);
|
|
|
|
/** 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. Loads account settings using PlatformServiceMixin
|
|
* 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;
|
|
|
|
try {
|
|
logger.debug(
|
|
"[InviteOneAcceptView] Component mounted - processing invitation",
|
|
);
|
|
|
|
// Load or generate identity using PlatformServiceMixin
|
|
const settings = await this.$accountSettings();
|
|
this.activeDid = settings.activeDid || "";
|
|
this.apiServer = settings.apiServer || "";
|
|
|
|
logger.debug("[InviteOneAcceptView] Account settings loaded", {
|
|
hasActiveDid: !!this.activeDid,
|
|
hasApiServer: !!this.apiServer,
|
|
});
|
|
|
|
if (!this.activeDid) {
|
|
logger.debug(
|
|
"[InviteOneAcceptView] No active DID found, generating new identity",
|
|
);
|
|
this.activeDid = await generateSaveAndActivateIdentity();
|
|
logger.debug("[InviteOneAcceptView] New identity generated", {
|
|
newActiveDid: !!this.activeDid,
|
|
});
|
|
}
|
|
|
|
// Extract JWT from route path
|
|
const jwt = (this.$route.params.jwt as string) || "";
|
|
logger.debug("[InviteOneAcceptView] Processing invite from route", {
|
|
hasJwt: !!jwt,
|
|
jwtLength: jwt.length,
|
|
});
|
|
|
|
await this.processInvite(jwt, false);
|
|
} catch (error) {
|
|
logger.error("[InviteOneAcceptView] Error during mount:", error);
|
|
} finally {
|
|
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.error(NOTIFY_INVITE_MISSING.message, TIMEOUTS.LONG);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.error(NOTIFY_INVITE_PROCESSING_ERROR.message, TIMEOUTS.BRIEF);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.error(NOTIFY_INVITE_TRUNCATED_DATA.message, TIMEOUTS.LONG);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Template handler for input change events
|
|
*
|
|
* Called when user types in the invitation text input field.
|
|
* Validates the input for common error patterns.
|
|
*
|
|
* @throws Will not throw but shows notifications
|
|
* @emits Notifications on validation errors
|
|
*/
|
|
handleInputChange() {
|
|
this.checkInvite(this.inputJwt);
|
|
}
|
|
|
|
/**
|
|
* Template handler for Accept button click
|
|
*
|
|
* Processes the invitation with user notification enabled.
|
|
* This is the explicit user action to accept an invitation.
|
|
*
|
|
* @throws Will not throw but logs errors
|
|
* @emits Notifications on errors
|
|
* @emits Router navigation on success
|
|
*/
|
|
handleAcceptClick() {
|
|
this.processInvite(this.inputJwt, true);
|
|
}
|
|
}
|
|
</script>
|
|
|