wip: Improve deep link validation and error handling
- Add comprehensive route validation with zod schema - Create type-safe DeepLinkRoute enum for all valid routes - Add structured error handling for invalid routes - Redirect to error page with detailed feedback - Add better timeout handling in deeplink tests The changes improve robustness by: 1. Validating route paths before navigation 2. Providing detailed error messages for invalid links 3. Redirecting users to dedicated error pages 4. Adding parameter validation with specific feedback 5. Improving type safety across deeplink handling
This commit is contained in:
@@ -99,7 +99,6 @@ export function numberOrZero(str: string): number {
|
||||
return isNumeric(str) ? +str : 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* from https://tools.ietf.org/html/rfc3986#section-3
|
||||
* also useful is https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Definition
|
||||
|
||||
@@ -281,6 +281,15 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: "user-profile",
|
||||
component: () => import("../views/UserProfileView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/deep-link-error",
|
||||
name: "deep-link-error",
|
||||
component: () => import("../views/DeepLinkErrorView.vue"),
|
||||
meta: {
|
||||
title: "Invalid Deep Link",
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const isElectron = window.location.protocol === "file:";
|
||||
|
||||
@@ -29,7 +29,11 @@
|
||||
*/
|
||||
|
||||
import { Router } from "vue-router";
|
||||
import { deepLinkSchemas, baseUrlSchema } from "../types/deepLinks";
|
||||
import {
|
||||
deepLinkSchemas,
|
||||
baseUrlSchema,
|
||||
routeSchema,
|
||||
} from "../types/deepLinks";
|
||||
import { logConsoleAndDb } from "../db";
|
||||
import type { DeepLinkError } from "../interfaces/deepLinks";
|
||||
|
||||
@@ -111,7 +115,7 @@ export class DeepLinkHandler {
|
||||
): Promise<void> {
|
||||
const routeMap: Record<string, string> = {
|
||||
"user-profile": "user-profile",
|
||||
project: "project",
|
||||
"project-details": "project-details",
|
||||
"onboard-meeting-setup": "onboard-meeting-setup",
|
||||
"invite-one-accept": "invite-one-accept",
|
||||
"contact-import": "contact-import",
|
||||
@@ -124,25 +128,63 @@ export class DeepLinkHandler {
|
||||
did: "did",
|
||||
};
|
||||
|
||||
const routeName = routeMap[path];
|
||||
if (!routeName) {
|
||||
// First try to validate the route path
|
||||
let routeName: string;
|
||||
|
||||
try {
|
||||
// Validate route exists
|
||||
const validRoute = routeSchema.parse(path) as DeepLinkRoute;
|
||||
routeName = routeMap[validRoute];
|
||||
} catch (error) {
|
||||
// Log the invalid route attempt
|
||||
logConsoleAndDb(`[DeepLink] Invalid route path: ${path}`, true);
|
||||
|
||||
// Redirect to error page with information about the invalid link
|
||||
await this.router.replace({
|
||||
name: "deep-link-error",
|
||||
query: {
|
||||
originalPath: path,
|
||||
errorCode: "INVALID_ROUTE",
|
||||
message: `The link you followed (${path}) is not supported`,
|
||||
},
|
||||
});
|
||||
|
||||
throw {
|
||||
code: "INVALID_ROUTE",
|
||||
message: `Unsupported route: ${path}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Validate parameters based on route type
|
||||
// Continue with parameter validation as before...
|
||||
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
|
||||
const validatedParams = await schema.parseAsync({
|
||||
...params,
|
||||
...query,
|
||||
});
|
||||
|
||||
await this.router.replace({
|
||||
name: routeName,
|
||||
params: validatedParams,
|
||||
query,
|
||||
});
|
||||
try {
|
||||
const validatedParams = await schema.parseAsync({
|
||||
...params,
|
||||
...query,
|
||||
});
|
||||
|
||||
await this.router.replace({
|
||||
name: routeName,
|
||||
params: validatedParams,
|
||||
query,
|
||||
});
|
||||
} catch (error) {
|
||||
// For parameter validation errors, provide specific error feedback
|
||||
await this.router.replace({
|
||||
name: "deep-link-error",
|
||||
query: {
|
||||
originalPath: path,
|
||||
errorCode: "INVALID_PARAMETERS",
|
||||
message: `The link parameters are invalid: ${(error as Error).message}`,
|
||||
},
|
||||
});
|
||||
|
||||
throw {
|
||||
code: "INVALID_PARAMETERS",
|
||||
message: (error as Error).message,
|
||||
details: error,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +27,35 @@
|
||||
*/
|
||||
import { z } from "zod";
|
||||
|
||||
// Base URL validation schema
|
||||
// Add a union type of all valid route paths
|
||||
export const VALID_DEEP_LINK_ROUTES = [
|
||||
"user-profile",
|
||||
"project-details",
|
||||
"onboard-meeting-setup",
|
||||
"invite-one-accept",
|
||||
"contact-import",
|
||||
"confirm-gift",
|
||||
"claim",
|
||||
"claim-cert",
|
||||
"claim-add-raw",
|
||||
"contact-edit",
|
||||
"contacts",
|
||||
"did",
|
||||
] as const;
|
||||
|
||||
// Create a type from the array
|
||||
export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number];
|
||||
|
||||
// Update your schema definitions to use this type
|
||||
export const baseUrlSchema = z.object({
|
||||
scheme: z.literal("timesafari"),
|
||||
path: z.string(),
|
||||
queryParams: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
// Use the type to ensure route validation
|
||||
export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES);
|
||||
|
||||
// Parameter validation schemas for each route type
|
||||
export const deepLinkSchemas = {
|
||||
"user-profile": z.object({
|
||||
|
||||
67
src/views/DeepLinkErrorView.vue
Normal file
67
src/views/DeepLinkErrorView.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="deep-link-error">
|
||||
<h1>Invalid Deep Link</h1>
|
||||
<div class="error-details">
|
||||
<p>{{ errorMessage }}</p>
|
||||
<div v-if="originalPath" class="original-link">
|
||||
<strong>Link attempted:</strong> timesafari://{{ originalPath }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="primary-button" @click="goHome">Go to Home</button>
|
||||
<button class="secondary-button" @click="reportIssue">
|
||||
Report Issue
|
||||
</button>
|
||||
</div>
|
||||
<div class="supported-links">
|
||||
<h2>Supported Deep Links</h2>
|
||||
<ul>
|
||||
<li v-for="route in validRoutes" :key="route">
|
||||
timesafari://{{ route }}/:id
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { VALID_DEEP_LINK_ROUTES } from "../types/deepLinks";
|
||||
import { logConsoleAndDb } from "../db";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// Extract error information from query params
|
||||
const errorCode = computed(
|
||||
() => (route.query.errorCode as string) || "UNKNOWN_ERROR",
|
||||
);
|
||||
const errorMessage = computed(
|
||||
() =>
|
||||
(route.query.message as string) ||
|
||||
"The deep link you followed is invalid or not supported.",
|
||||
);
|
||||
const originalPath = computed(() => route.query.originalPath as string);
|
||||
const validRoutes = VALID_DEEP_LINK_ROUTES;
|
||||
|
||||
// Navigation methods
|
||||
const goHome = () => router.replace({ name: "home" });
|
||||
const reportIssue = () => {
|
||||
// Open a support form or email
|
||||
window.open(
|
||||
"mailto:support@timesafari.app?subject=Invalid Deep Link&body=" +
|
||||
encodeURIComponent(
|
||||
`I encountered an error with a deep link: timesafari://${originalPath.value}\nError: ${errorMessage.value}`,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
// Log the error for analytics
|
||||
onMounted(() => {
|
||||
logConsoleAndDb(
|
||||
`[DeepLink] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}`,
|
||||
true,
|
||||
);
|
||||
});
|
||||
</script>
|
||||
@@ -372,7 +372,10 @@
|
||||
<!-- Totals section -->
|
||||
<div class="mt-1 flex items-center min-h-[1.5rem]">
|
||||
<div v-if="loadingTotals" class="flex-1">
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse text-blue-500" />
|
||||
<font-awesome
|
||||
icon="spinner"
|
||||
class="fa-spin-pulse text-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="givesTotalsByUnit.length > 0" class="flex-1">
|
||||
<span class="font-semibold mr-2 shrink-0">Totals</span>
|
||||
@@ -451,14 +454,17 @@
|
||||
<div class="text-slate-500">
|
||||
<font-awesome icon="calendar" class="fa-fw text-slate-400" />
|
||||
{{ give.issuedAt?.substring(0, 10) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="give.description" class="text-slate-500">
|
||||
<font-awesome icon="comment" class="fa-fw text-slate-400" />
|
||||
{{ give.description }}
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<a @click="onClickLoadClaim(give.jwtId)">
|
||||
<font-awesome icon="file-lines" class="text-blue-500 cursor-pointer" />
|
||||
<font-awesome
|
||||
icon="file-lines"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a
|
||||
|
||||
Reference in New Issue
Block a user