feat: centralize identity creation with router navigation guard

Migrate automatic identity creation from scattered view components to centralized
router navigation guard for consistent behavior across all entry points.

**Key Changes:**
- Add global beforeEach navigation guard in router/index.ts
- Remove automatic identity creation from HomeView, ContactsView, InviteOneAcceptView,
  and OnboardMeetingMembersView
- Keep minimal fallback logic in deep link scenarios with logging
- Exclude manual identity creation routes (/start, /new-identifier, /import-account)

**Benefits:**
- Eliminates code duplication and race conditions
- Ensures consistent identity creation regardless of entry point
- Centralizes error handling with fallback to manual creation
- Improves maintainability with single point of change

**Files Modified:**
- src/router/index.ts: Add navigation guard with identity creation logic
- src/views/HomeView.vue: Remove automatic creation, simplify initializeIdentity()
- src/views/ContactsView.vue: Add fallback with logging
- src/views/InviteOneAcceptView.vue: Add fallback with logging
- src/views/OnboardMeetingMembersView.vue: Add fallback with logging

**Testing:**
- Verified first-time user navigation creates identity automatically
- Confirmed existing users bypass creation logic
- Validated manual creation routes remain unaffected
- Tested deep link scenarios with fallback logic

**Documentation:**
- Created docs/identity-creation-migration.md with comprehensive details
- Includes migration rationale, implementation details, testing scenarios
- Documents security considerations and rollback plan

Resolves inconsistent identity creation behavior across different app entry points.
This commit is contained in:
Matthew Raymer
2025-07-17 04:03:05 +00:00
parent 5c7f58b5c8
commit d355d51ea3
15 changed files with 1121 additions and 99 deletions

View File

@@ -463,7 +463,7 @@ export default class MembersList extends Vue {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
// registration failure is likely explained by a message from the server
// registration failure is likely explained by a message from the server$notif
const additionalInfo =
serverMessageForUser(error) || error?.error || "";
this.notify.warning(

View File

@@ -1686,3 +1686,10 @@ export const PUSH_NOTIFICATION_TIMEOUT_PERSISTENT = -1;
// InviteOneAcceptView.vue timeout constants
export const INVITE_TIMEOUT_STANDARD = 3000; // Standard error messages
export const INVITE_TIMEOUT_LONG = 5000; // Missing invite and invalid data errors
// ContactsView.vue specific constants
// Used in: ContactsView.vue (showOnboardingInfo method - simple confirmation dialog)
export const NOTIFY_CONTACTS_ADDED_CONFIRM = {
title: "They're Added To Your List",
message: "Would you like to go to the main page now?",
};

View File

@@ -6,6 +6,10 @@ import {
RouteRecordRaw,
} from "vue-router";
import { logger } from "../utils/logger";
import {
retrieveAccountDids,
generateSaveAndActivateIdentity,
} from "../libs/util";
const routes: Array<RouteRecordRaw> = [
{
@@ -316,6 +320,65 @@ const errorHandler = (
router.onError(errorHandler); // Assign the error handler to the router instance
/**
* Global navigation guard to ensure user identity exists
*
* This guard checks if the user has any identities before navigating to most routes.
* If no identity exists, it automatically creates one using the default seed-based method.
*
* Routes that are excluded from this check:
* - /start - Manual identity creation selection
* - /new-identifier - Manual seed-based creation
* - /import-account - Manual import flow
* - /import-derive - Manual derivation flow
* - /database-migration - Migration utilities
* - /deep-link-error - Error page
*
* @param to - Target route
* @param from - Source route
* @param next - Navigation function
*/
router.beforeEach(async (to, from, next) => {
try {
// Skip identity check for routes that handle identity creation manually
const skipIdentityRoutes = [
"/start",
"/new-identifier",
"/import-account",
"/import-derive",
"/database-migration",
"/deep-link-error",
];
if (skipIdentityRoutes.includes(to.path)) {
return next();
}
// Check if user has any identities
const allMyDids = await retrieveAccountDids();
if (allMyDids.length === 0) {
logger.info("[Router] No identities found, creating default identity");
// Create identity automatically using seed-based method
await generateSaveAndActivateIdentity();
logger.info("[Router] Default identity created successfully");
}
next();
} catch (error) {
logger.error(
"[Router] Identity creation failed in navigation guard:",
error,
);
// Redirect to start page if identity creation fails
// This allows users to manually create an identity or troubleshoot
next("/start");
}
});
// router.beforeEach((to, from, next) => {
// console.log("Navigating to view:", to.name);
// console.log("From view:", from.name);

View File

@@ -124,7 +124,7 @@ export const PlatformServiceMixin = {
/**
* Access to in-memory logs array
* Provides direct access to memoryLogs
* Provides direct access to memoryLogs without requiring databaseUtil import
*/
$memoryLogs(): string[] {
return _memoryLogs;
@@ -159,7 +159,7 @@ export const PlatformServiceMixin = {
methods: {
// =================================================
// SELF-CONTAINED UTILITY METHODS
// SELF-CONTAINED UTILITY METHODS (no databaseUtil dependency)
// =================================================
/**

View File

@@ -168,6 +168,7 @@ import {
NOTIFY_CONTACT_INVALID_DID,
NOTIFY_CONTACTS_ADDED_VISIBLE,
NOTIFY_CONTACTS_ADDED,
NOTIFY_CONTACTS_ADDED_CONFIRM,
NOTIFY_CONTACT_IMPORT_ERROR,
NOTIFY_CONTACT_IMPORT_CONFLICT,
NOTIFY_CONTACT_IMPORT_CONSTRAINT,
@@ -328,8 +329,11 @@ export default class ContactsView extends Vue {
// this happens when a platform (eg iOS) doesn't include anything after the "=" in a shared link.
this.notify.error(NOTIFY_BLANK_INVITE.message, TIMEOUTS.VERY_LONG);
} else if (importedInviteJwt) {
// make sure user is created
// Identity creation should be handled by router guard, but keep as fallback for invite processing
if (!this.activeDid) {
logger.info(
"[ContactsView] No active DID found, creating identity as fallback for invite processing",
);
this.activeDid = await generateSaveAndActivateIdentity();
}
// send invite directly to server, with auth for this user
@@ -408,15 +412,10 @@ export default class ContactsView extends Vue {
// Legacy danger() and warning() methods removed - now using this.notify.error() and this.notify.warning()
private showOnboardingInfo() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "They're Added To Your List",
text: "Would you like to go to the main page now?",
onYes: async () => {
this.$router.push({ name: "home" });
},
this.notify.confirm(
NOTIFY_CONTACTS_ADDED_CONFIRM.message,
async () => {
this.$router.push({ name: "home" });
},
-1,
);

View File

@@ -77,80 +77,70 @@ Raymer * @version 1.0.0 */
</div>
<div class="mb-8">
<div v-if="isCreatingIdentifier">
<p class="text-slate-500 text-center italic mt-4 mb-4">
<font-awesome icon="spinner" class="fa-spin-pulse" />
Loading&hellip;
</p>
</div>
<!--
They should have an identifier, even if it's an auto-generated one that they'll never use.
Identity creation is now handled by router navigation guard.
-->
<div class="mb-4">
<div
v-if="!isRegistered"
id="noticeSomeoneMustRegisterYou"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
To share, someone must register you.
<div class="block text-center">
<button
class="text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
@click="showNameThenIdDialog()"
>
Show them {{ PASSKEYS_ENABLED ? "default" : "your" }} identifier
info
</button>
</div>
<UserNameDialog ref="userNameDialog" />
<div class="flex justify-end w-full">
<router-link
:to="{ name: 'start' }"
class="block text-right text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
>
See advanced options
</router-link>
</div>
</div>
<div v-else>
<!-- !isCreatingIdentifier -->
<!--
They should have an identifier, even if it's an auto-generated one that they'll never use.
-->
<div class="mb-4">
<div
v-if="!isRegistered"
id="noticeSomeoneMustRegisterYou"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
<!-- !isCreatingIdentifier && !isRegistered -->
To share, someone must register you.
<div class="block text-center">
<div v-else id="sectionRecordSomethingGiven">
<!-- Record Quick-Action -->
<div class="mb-6">
<div class="flex gap-2 items-center mb-2">
<h2 class="text-xl font-bold">Record something given by:</h2>
<button
class="text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
@click="showNameThenIdDialog()"
class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
@click="openGiftedPrompts()"
>
Show them {{ PASSKEYS_ENABLED ? "default" : "your" }} identifier
info
<font-awesome
icon="lightbulb"
class="block text-center w-[1em]"
/>
</button>
</div>
<UserNameDialog ref="userNameDialog" />
<div class="flex justify-end w-full">
<router-link
:to="{ name: 'start' }"
class="block text-right text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
<div class="grid grid-cols-2 gap-2">
<button
type="button"
class="text-center text-base uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
@click="openDialogPerson()"
>
See advanced options
</router-link>
</div>
</div>
<div v-else id="sectionRecordSomethingGiven">
<!-- Record Quick-Action -->
<div class="mb-6">
<div class="flex gap-2 items-center mb-2">
<h2 class="text-xl font-bold">Record something given by:</h2>
<button
class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
@click="openGiftedPrompts()"
>
<font-awesome
icon="lightbulb"
class="block text-center w-[1em]"
/>
</button>
</div>
<div class="grid grid-cols-2 gap-2">
<button
type="button"
class="text-center text-base uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
@click="openDialogPerson()"
>
<font-awesome icon="user" />
Person
</button>
<button
type="button"
class="text-center text-base uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
@click="openProjectDialog()"
>
<font-awesome icon="folder-open" />
Project
</button>
</div>
<font-awesome icon="user" />
Person
</button>
<button
type="button"
class="text-center text-base uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
@click="openProjectDialog()"
>
<font-awesome icon="folder-open" />
Project
</button>
</div>
</div>
</div>
@@ -303,7 +293,6 @@ import {
getPlanFromCache,
} from "../libs/endorserServer";
import {
generateSaveAndActivateIdentity,
retrieveAccountDids,
GiverReceiverInputInfo,
OnboardPage,
@@ -424,7 +413,7 @@ export default class HomeView extends Vue {
feedLastViewedClaimId?: string;
givenName = "";
isAnyFeedFilterOn = false;
isCreatingIdentifier = false;
// isCreatingIdentifier removed - identity creation now handled by router guard
isFeedFilteredByVisible = false;
isFeedFilteredByNearby = false;
isFeedLoading = true;
@@ -508,22 +497,14 @@ export default class HomeView extends Vue {
);
}
// Create new DID if needed
// Identity creation is now handled by router navigation guard
// If we reach here, an identity should already exist
if (this.allMyDids.length === 0) {
try {
this.isCreatingIdentifier = true;
const newDid = await generateSaveAndActivateIdentity();
this.isCreatingIdentifier = false;
this.allMyDids = [newDid];
this.$logAndConsole(`[HomeView] Created new identity: ${newDid}`);
} catch (error) {
this.isCreatingIdentifier = false;
this.$logAndConsole(
`[HomeView] Failed to create new identity: ${error}`,
true,
);
throw new Error("Failed to create new identity. Please try again.");
}
this.$logAndConsole(
`[HomeView] No identities found - this should not happen with router guard`,
true,
);
throw new Error("No identity found. Please try refreshing the page.");
}
// Load settings with better error context using ultra-concise mixin

View File

@@ -123,7 +123,11 @@ export default class InviteOneAcceptView extends Vue {
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
// Identity creation should be handled by router guard, but keep as fallback for deep links
if (!this.activeDid) {
logger.info(
"[InviteOneAcceptView] No active DID found, creating identity as fallback",
);
this.activeDid = await generateSaveAndActivateIdentity();
}

View File

@@ -111,7 +111,11 @@ export default class OnboardMeetingMembersView extends Vue {
this.firstName = settings?.firstName || "";
this.isRegistered = !!settings?.isRegistered;
try {
// Identity creation should be handled by router guard, but keep as fallback for meeting setup
if (!this.activeDid) {
logger.info(
"[OnboardMeetingMembersView] No active DID found, creating identity as fallback for meeting setup",
);
this.activeDid = await generateSaveAndActivateIdentity();
this.isRegistered = false;
}