Merge branch 'master' into account-import-duplicate-prevention
This commit is contained in:
@@ -26,7 +26,7 @@
|
||||
:weight="2"
|
||||
color="#3b82f6"
|
||||
fill-color="#3b82f6"
|
||||
fill-opacity="0.2"
|
||||
:fill-opacity="0.2"
|
||||
/>
|
||||
</l-map>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||
import { AppString, NotificationIface } from "../constants/app";
|
||||
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
@Component({
|
||||
mixins: [PlatformServiceMixin],
|
||||
@@ -44,26 +45,49 @@ export default class TopMessage extends Vue {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
|
||||
try {
|
||||
// Ultra-concise cached settings loading - replaces 50+ lines of logic!
|
||||
const settings = await this.$accountSettings(undefined, {
|
||||
activeDid: undefined,
|
||||
apiServer: AppString.PROD_ENDORSER_API_SERVER,
|
||||
// Load settings without overriding database values - fixes settings inconsistency
|
||||
logger.debug("[TopMessage] 📥 Loading settings without overrides...");
|
||||
const settings = await this.$accountSettings();
|
||||
|
||||
logger.debug("[TopMessage] 📊 Settings loaded:", {
|
||||
activeDid: settings.activeDid,
|
||||
apiServer: settings.apiServer,
|
||||
warnIfTestServer: settings.warnIfTestServer,
|
||||
warnIfProdServer: settings.warnIfProdServer,
|
||||
component: "TopMessage",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Only show warnings if the user has explicitly enabled them
|
||||
if (
|
||||
settings.warnIfTestServer &&
|
||||
settings.apiServer &&
|
||||
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
||||
) {
|
||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||
this.message = "You're not using prod, user " + didPrefix;
|
||||
logger.debug("[TopMessage] ⚠️ Test server warning displayed:", {
|
||||
apiServer: settings.apiServer,
|
||||
didPrefix: didPrefix,
|
||||
});
|
||||
} else if (
|
||||
settings.warnIfProdServer &&
|
||||
settings.apiServer &&
|
||||
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
|
||||
) {
|
||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||
this.message = "You are using prod, user " + didPrefix;
|
||||
logger.debug("[TopMessage] ⚠️ Production server warning displayed:", {
|
||||
apiServer: settings.apiServer,
|
||||
didPrefix: didPrefix,
|
||||
});
|
||||
} else {
|
||||
logger.debug(
|
||||
"[TopMessage] ℹ️ No warnings displayed - conditions not met",
|
||||
);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
logger.error("[TopMessage] ❌ Error loading settings:", err);
|
||||
this.notify.error(JSON.stringify(err), TIMEOUTS.MODAL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,8 @@ export default class UserNameDialog extends Vue {
|
||||
*/
|
||||
async open(aCallback?: (name?: string) => void) {
|
||||
this.callback = aCallback || this.callback;
|
||||
const settings = await this.$settings();
|
||||
// Load from account-specific settings instead of master settings
|
||||
const settings = await this.$accountSettings();
|
||||
this.givenName = settings.firstName || "";
|
||||
this.visible = true;
|
||||
}
|
||||
@@ -95,7 +96,18 @@ export default class UserNameDialog extends Vue {
|
||||
*/
|
||||
async onClickSaveChanges() {
|
||||
try {
|
||||
await this.$updateSettings({ firstName: this.givenName });
|
||||
// Get the current active DID to save to user-specific settings
|
||||
const settings = await this.$accountSettings();
|
||||
const activeDid = settings.activeDid;
|
||||
|
||||
if (activeDid) {
|
||||
// Save to user-specific settings for the current identity
|
||||
await this.$saveUserSettings(activeDid, { firstName: this.givenName });
|
||||
} else {
|
||||
// Fallback to master settings if no active DID
|
||||
await this.$saveSettings({ firstName: this.givenName });
|
||||
}
|
||||
|
||||
this.visible = false;
|
||||
this.callback(this.givenName);
|
||||
} catch (error) {
|
||||
|
||||
@@ -486,6 +486,15 @@ const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
|
||||
max: 500,
|
||||
});
|
||||
|
||||
/**
|
||||
* Tracks in-flight requests to prevent duplicate API calls for the same plan
|
||||
* @constant {Map}
|
||||
*/
|
||||
const inFlightRequests = new Map<
|
||||
string,
|
||||
Promise<PlanSummaryRecord | undefined>
|
||||
>();
|
||||
|
||||
/**
|
||||
* Retrieves plan data from cache or server
|
||||
* @param {string} handleId - Plan handle ID
|
||||
@@ -505,40 +514,140 @@ export async function getPlanFromCache(
|
||||
if (!handleId) {
|
||||
return undefined;
|
||||
}
|
||||
let cred = planCache.get(handleId);
|
||||
if (!cred) {
|
||||
const url =
|
||||
apiServer +
|
||||
"/api/v2/report/plans?handleId=" +
|
||||
encodeURIComponent(handleId);
|
||||
const headers = await getHeaders(requesterDid);
|
||||
try {
|
||||
const resp = await axios.get(url, { headers });
|
||||
if (resp.status === 200 && resp.data?.data?.length > 0) {
|
||||
cred = resp.data.data[0];
|
||||
planCache.set(handleId, cred);
|
||||
} else {
|
||||
// Use debug level for development to reduce console noise
|
||||
const isDevelopment = process.env.VITE_PLATFORM === "development";
|
||||
const log = isDevelopment ? logger.debug : logger.log;
|
||||
|
||||
log(
|
||||
"[EndorserServer] Plan cache is empty for handle",
|
||||
handleId,
|
||||
" Got data:",
|
||||
JSON.stringify(resp.data),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[EndorserServer] Failed to load plan with handle",
|
||||
handleId,
|
||||
" Got error:",
|
||||
JSON.stringify(error),
|
||||
);
|
||||
}
|
||||
// Check cache first (existing behavior)
|
||||
const cred = planCache.get(handleId);
|
||||
if (cred) {
|
||||
return cred;
|
||||
}
|
||||
|
||||
// Check if request is already in flight (NEW: request deduplication)
|
||||
if (inFlightRequests.has(handleId)) {
|
||||
logger.debug(
|
||||
"[Plan Loading] 🔄 Request already in flight, reusing promise:",
|
||||
{
|
||||
handleId,
|
||||
requesterDid,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
return inFlightRequests.get(handleId);
|
||||
}
|
||||
|
||||
// Create new request promise (NEW: request coordination)
|
||||
const requestPromise = performPlanRequest(
|
||||
handleId,
|
||||
axios,
|
||||
apiServer,
|
||||
requesterDid,
|
||||
);
|
||||
inFlightRequests.set(handleId, requestPromise);
|
||||
|
||||
try {
|
||||
const result = await requestPromise;
|
||||
return result;
|
||||
} finally {
|
||||
// Clean up in-flight request tracking (NEW: cleanup)
|
||||
inFlightRequests.delete(handleId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the actual plan request to the server
|
||||
* @param {string} handleId - Plan handle ID
|
||||
* @param {Axios} axios - Axios instance
|
||||
* @param {string} apiServer - API server URL
|
||||
* @param {string} [requesterDid] - Optional requester DID for private info
|
||||
* @returns {Promise<PlanSummaryRecord|undefined>} Plan data or undefined if not found
|
||||
*
|
||||
* @throws {Error} If server request fails
|
||||
*/
|
||||
async function performPlanRequest(
|
||||
handleId: string,
|
||||
axios: Axios,
|
||||
apiServer: string,
|
||||
requesterDid?: string,
|
||||
): Promise<PlanSummaryRecord | undefined> {
|
||||
const url =
|
||||
apiServer + "/api/v2/report/plans?handleId=" + encodeURIComponent(handleId);
|
||||
const headers = await getHeaders(requesterDid);
|
||||
|
||||
// Enhanced diagnostic logging for plan loading
|
||||
const requestId = `plan_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
logger.debug("[Plan Loading] 🔍 Loading plan from server:", {
|
||||
requestId,
|
||||
handleId,
|
||||
apiServer,
|
||||
endpoint: url,
|
||||
requesterDid,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
try {
|
||||
const resp = await axios.get(url, { headers });
|
||||
|
||||
logger.debug("[Plan Loading] ✅ Plan loaded successfully:", {
|
||||
requestId,
|
||||
handleId,
|
||||
status: resp.status,
|
||||
hasData: !!resp.data?.data,
|
||||
dataLength: resp.data?.data?.length || 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (resp.status === 200 && resp.data?.data?.length > 0) {
|
||||
const cred = resp.data.data[0];
|
||||
planCache.set(handleId, cred);
|
||||
|
||||
logger.debug("[Plan Loading] 💾 Plan cached:", {
|
||||
requestId,
|
||||
handleId,
|
||||
planName: cred?.name,
|
||||
planIssuer: cred?.issuerDid,
|
||||
});
|
||||
|
||||
return cred;
|
||||
} else {
|
||||
// Use debug level for development to reduce console noise
|
||||
const isDevelopment = process.env.VITE_PLATFORM === "development";
|
||||
const log = isDevelopment ? logger.debug : logger.log;
|
||||
|
||||
log(
|
||||
"[Plan Loading] ⚠️ Plan cache is empty for handle",
|
||||
handleId,
|
||||
" Got data:",
|
||||
JSON.stringify(resp.data),
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
// Enhanced error logging for plan loading failures
|
||||
const axiosError = error as {
|
||||
response?: {
|
||||
data?: unknown;
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
};
|
||||
message?: string;
|
||||
};
|
||||
|
||||
logger.error("[Plan Loading] ❌ Failed to load plan:", {
|
||||
requestId,
|
||||
handleId,
|
||||
apiServer,
|
||||
endpoint: url,
|
||||
requesterDid,
|
||||
errorStatus: axiosError.response?.status,
|
||||
errorStatusText: axiosError.response?.statusText,
|
||||
errorData: axiosError.response?.data,
|
||||
errorMessage: axiosError.message || String(error),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
return cred;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1019,19 +1128,82 @@ export async function createAndSubmitClaim(
|
||||
|
||||
const vcJwt: string = await createEndorserJwtForDid(issuerDid, vcPayload);
|
||||
|
||||
// Enhanced diagnostic logging for claim submission
|
||||
const requestId = `claim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
logger.info("[Claim Submission] 🚀 Starting claim submission:", {
|
||||
requestId,
|
||||
apiServer,
|
||||
requesterDid: issuerDid,
|
||||
endpoint: `${apiServer}/api/v2/claim`,
|
||||
timestamp: new Date().toISOString(),
|
||||
jwtLength: vcJwt.length,
|
||||
});
|
||||
|
||||
// Make the xhr request payload
|
||||
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
||||
const url = `${apiServer}/api/v2/claim`;
|
||||
|
||||
logger.debug("[Claim Submission] 📡 Making API request:", {
|
||||
requestId,
|
||||
url,
|
||||
payloadSize: payload.length,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
const response = await axios.post(url, payload, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
logger.info("[Claim Submission] ✅ Claim submitted successfully:", {
|
||||
requestId,
|
||||
status: response.status,
|
||||
handleId: response.data?.handleId,
|
||||
responseSize: JSON.stringify(response.data).length,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return { success: true, handleId: response.data?.handleId };
|
||||
} catch (error: unknown) {
|
||||
logger.error("Error submitting claim:", error);
|
||||
// Enhanced error logging with comprehensive context
|
||||
const requestId = `claim_error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const axiosError = error as {
|
||||
response?: {
|
||||
data?: { error?: { code?: string; message?: string } };
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
config?: {
|
||||
url?: string;
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
message?: string;
|
||||
};
|
||||
|
||||
logger.error("[Claim Submission] ❌ Claim submission failed:", {
|
||||
requestId,
|
||||
apiServer,
|
||||
requesterDid: issuerDid,
|
||||
endpoint: `${apiServer}/api/v2/claim`,
|
||||
errorCode: axiosError.response?.data?.error?.code,
|
||||
errorMessage: axiosError.response?.data?.error?.message,
|
||||
httpStatus: axiosError.response?.status,
|
||||
httpStatusText: axiosError.response?.statusText,
|
||||
responseHeaders: axiosError.response?.headers,
|
||||
requestConfig: {
|
||||
url: axiosError.config?.url,
|
||||
method: axiosError.config?.method,
|
||||
headers: axiosError.config?.headers,
|
||||
},
|
||||
originalError: axiosError.message || String(error),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const errorMessage: string =
|
||||
serverMessageForUser(error) ||
|
||||
(error && typeof error === "object" && "message" in error
|
||||
@@ -1494,14 +1666,56 @@ export async function fetchEndorserRateLimits(
|
||||
) {
|
||||
const url = `${apiServer}/api/report/rateLimits`;
|
||||
const headers = await getHeaders(issuerDid);
|
||||
|
||||
// Enhanced diagnostic logging for user registration tracking
|
||||
logger.debug("[User Registration] Checking user status on server:", {
|
||||
did: issuerDid,
|
||||
server: apiServer,
|
||||
endpoint: url,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await axios.get(url, { headers } as AxiosRequestConfig);
|
||||
|
||||
// Log successful registration check
|
||||
logger.debug("[User Registration] User registration check successful:", {
|
||||
did: issuerDid,
|
||||
server: apiServer,
|
||||
status: response.status,
|
||||
isRegistered: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Enhanced error logging with user registration context
|
||||
const axiosError = error as {
|
||||
response?: {
|
||||
data?: { error?: { code?: string; message?: string } };
|
||||
status?: number;
|
||||
};
|
||||
};
|
||||
const errorCode = axiosError.response?.data?.error?.code;
|
||||
const errorMessage = axiosError.response?.data?.error?.message;
|
||||
const httpStatus = axiosError.response?.status;
|
||||
|
||||
logger.warn("[User Registration] User not registered on server:", {
|
||||
did: issuerDid,
|
||||
server: apiServer,
|
||||
errorCode: errorCode,
|
||||
errorMessage: errorMessage,
|
||||
httpStatus: httpStatus,
|
||||
needsRegistration: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Log the original error for debugging
|
||||
logger.error(
|
||||
`[fetchEndorserRateLimits] Error for DID ${issuerDid}:`,
|
||||
errorStringForLog(error),
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1514,8 +1728,53 @@ export async function fetchEndorserRateLimits(
|
||||
* @param {string} issuerDid - The DID for which to check rate limits.
|
||||
* @returns {Promise<AxiosResponse>} The Axios response object.
|
||||
*/
|
||||
export async function fetchImageRateLimits(axios: Axios, issuerDid: string) {
|
||||
const url = DEFAULT_IMAGE_API_SERVER + "/image-limits";
|
||||
export async function fetchImageRateLimits(
|
||||
axios: Axios,
|
||||
issuerDid: string,
|
||||
imageServer?: string,
|
||||
) {
|
||||
const server = imageServer || DEFAULT_IMAGE_API_SERVER;
|
||||
const url = server + "/image-limits";
|
||||
const headers = await getHeaders(issuerDid);
|
||||
return await axios.get(url, { headers } as AxiosRequestConfig);
|
||||
|
||||
// Enhanced diagnostic logging for image server calls
|
||||
logger.debug("[Image Server] Checking image rate limits:", {
|
||||
did: issuerDid,
|
||||
server: server,
|
||||
endpoint: url,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await axios.get(url, { headers } as AxiosRequestConfig);
|
||||
|
||||
// Log successful image server call
|
||||
logger.debug("[Image Server] Image rate limits check successful:", {
|
||||
did: issuerDid,
|
||||
server: server,
|
||||
status: response.status,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Enhanced error logging for image server failures
|
||||
const axiosError = error as {
|
||||
response?: {
|
||||
data?: { error?: { code?: string; message?: string } };
|
||||
status?: number;
|
||||
};
|
||||
};
|
||||
|
||||
logger.warn("[Image Server] Image rate limits check failed:", {
|
||||
did: issuerDid,
|
||||
server: server,
|
||||
errorCode: axiosError.response?.data?.error?.code,
|
||||
errorMessage: axiosError.response?.data?.error?.message,
|
||||
httpStatus: axiosError.response?.status,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -984,13 +984,16 @@ export async function importFromMnemonic(
|
||||
const firstName = settings[0];
|
||||
const isRegistered = settings[1];
|
||||
|
||||
logger.info("[importFromMnemonic] Test User #0 settings verification", {
|
||||
did: newId.did,
|
||||
firstName,
|
||||
isRegistered,
|
||||
expectedFirstName: "User Zero",
|
||||
expectedIsRegistered: true,
|
||||
});
|
||||
logger.debug(
|
||||
"[importFromMnemonic] Test User #0 settings verification",
|
||||
{
|
||||
did: newId.did,
|
||||
firstName,
|
||||
isRegistered,
|
||||
expectedFirstName: "User Zero",
|
||||
expectedIsRegistered: true,
|
||||
},
|
||||
);
|
||||
|
||||
// If settings weren't saved correctly, try individual updates
|
||||
if (firstName !== "User Zero" || isRegistered !== 1) {
|
||||
@@ -1016,7 +1019,7 @@ export async function importFromMnemonic(
|
||||
|
||||
if (retryResult?.values?.length) {
|
||||
const retrySettings = retryResult.values[0];
|
||||
logger.info(
|
||||
logger.debug(
|
||||
"[importFromMnemonic] Test User #0 settings after retry",
|
||||
{
|
||||
firstName: retrySettings[0],
|
||||
|
||||
@@ -13,6 +13,15 @@ const platform = process.env.VITE_PLATFORM || "web";
|
||||
|
||||
logger.info(`[Main] 🚀 Loading TimeSafari for platform: ${platform}`);
|
||||
|
||||
// Log all relevant environment variables for boot-time debugging
|
||||
logger.info("[Main] 🌍 Boot-time environment configuration:", {
|
||||
platform: process.env.VITE_PLATFORM,
|
||||
defaultEndorserApiServer: process.env.VITE_DEFAULT_ENDORSER_API_SERVER,
|
||||
defaultPartnerApiServer: process.env.VITE_DEFAULT_PARTNER_API_SERVER,
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Dynamically import the appropriate main entry point
|
||||
if (platform === "capacitor") {
|
||||
logger.info(`[Main] 📱 Loading Capacitor-specific entry point`);
|
||||
|
||||
@@ -327,7 +327,7 @@ router.onError(errorHandler); // Assign the error handler to the router instance
|
||||
* @param next - Navigation function
|
||||
*/
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
logger.info(`[Router] 🧭 Navigation guard triggered:`, {
|
||||
logger.debug(`[Router] 🧭 Navigation guard triggered:`, {
|
||||
from: _from?.path || "none",
|
||||
to: to.path,
|
||||
name: to.name,
|
||||
@@ -337,6 +337,22 @@ router.beforeEach(async (to, _from, next) => {
|
||||
});
|
||||
|
||||
try {
|
||||
// Log boot-time configuration on first navigation
|
||||
if (!_from) {
|
||||
logger.info(
|
||||
"[Router] 🚀 First navigation detected - logging boot-time configuration:",
|
||||
{
|
||||
platform: process.env.VITE_PLATFORM,
|
||||
defaultEndorserApiServer:
|
||||
process.env.VITE_DEFAULT_ENDORSER_API_SERVER,
|
||||
defaultPartnerApiServer: process.env.VITE_DEFAULT_PARTNER_API_SERVER,
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
targetRoute: to.path,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Skip identity check for routes that handle identity creation manually
|
||||
const skipIdentityRoutes = [
|
||||
"/start",
|
||||
@@ -352,11 +368,11 @@ router.beforeEach(async (to, _from, next) => {
|
||||
return next();
|
||||
}
|
||||
|
||||
logger.info(`[Router] 🔍 Checking user identity for route: ${to.path}`);
|
||||
logger.debug(`[Router] 🔍 Checking user identity for route: ${to.path}`);
|
||||
|
||||
// Check if user has any identities
|
||||
const allMyDids = await retrieveAccountDids();
|
||||
logger.info(`[Router] 📋 Found ${allMyDids.length} user identities`);
|
||||
logger.debug(`[Router] 📋 Found ${allMyDids.length} user identities`);
|
||||
|
||||
if (allMyDids.length === 0) {
|
||||
logger.info("[Router] ⚠️ No identities found, creating default identity");
|
||||
@@ -366,7 +382,7 @@ router.beforeEach(async (to, _from, next) => {
|
||||
|
||||
logger.info("[Router] ✅ Default identity created successfully");
|
||||
} else {
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`[Router] ✅ User has ${allMyDids.length} identities, proceeding`,
|
||||
);
|
||||
}
|
||||
@@ -392,7 +408,7 @@ router.beforeEach(async (to, _from, next) => {
|
||||
|
||||
// Add navigation success logging
|
||||
router.afterEach((to, from) => {
|
||||
logger.info(`[Router] ✅ Navigation completed:`, {
|
||||
logger.debug(`[Router] ✅ Navigation completed:`, {
|
||||
from: from?.path || "none",
|
||||
to: to.path,
|
||||
name: to.name,
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
/**
|
||||
* ProfileService - Handles user profile operations and API calls
|
||||
* Extracted from AccountViewView.vue to improve separation of concerns
|
||||
*/
|
||||
|
||||
import { AxiosInstance, AxiosError } from "axios";
|
||||
import { UserProfile } from "@/libs/partnerServer";
|
||||
import { UserProfileResponse } from "@/interfaces/accountView";
|
||||
import { getHeaders, errorStringForLog } from "@/libs/endorserServer";
|
||||
import { handleApiError } from "./api";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
||||
|
||||
/**
|
||||
* Profile data interface
|
||||
*/
|
||||
export interface ProfileData {
|
||||
description: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
includeLocation: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile service class
|
||||
*/
|
||||
export class ProfileService {
|
||||
private axios: AxiosInstance;
|
||||
private partnerApiServer: string;
|
||||
|
||||
constructor(axios: AxiosInstance, partnerApiServer: string) {
|
||||
this.axios = axios;
|
||||
this.partnerApiServer = partnerApiServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load user profile from the server
|
||||
* @param activeDid - The user's DID
|
||||
* @returns ProfileData or null if profile doesn't exist
|
||||
*/
|
||||
async loadProfile(activeDid: string): Promise<ProfileData | null> {
|
||||
try {
|
||||
const headers = await getHeaders(activeDid);
|
||||
const response = await this.axios.get<UserProfileResponse>(
|
||||
`${this.partnerApiServer}/api/partner/userProfileForIssuer/${activeDid}`,
|
||||
{ headers },
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
const data = response.data.data;
|
||||
const profileData: ProfileData = {
|
||||
description: data.description || "",
|
||||
latitude: data.locLat || 0,
|
||||
longitude: data.locLon || 0,
|
||||
includeLocation: !!(data.locLat && data.locLon),
|
||||
};
|
||||
return profileData;
|
||||
} else {
|
||||
throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.UNABLE_TO_LOAD_PROFILE);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.isApiError(error) && error.response?.status === 404) {
|
||||
// Profile doesn't exist yet - this is normal
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.error("Error loading profile:", errorStringForLog(error));
|
||||
handleApiError(error as AxiosError, "/api/partner/userProfileForIssuer");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save user profile to the server
|
||||
* @param activeDid - The user's DID
|
||||
* @param profileData - The profile data to save
|
||||
* @returns true if successful, false otherwise
|
||||
*/
|
||||
async saveProfile(
|
||||
activeDid: string,
|
||||
profileData: ProfileData,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const headers = await getHeaders(activeDid);
|
||||
const payload: UserProfile = {
|
||||
description: profileData.description,
|
||||
issuerDid: activeDid,
|
||||
};
|
||||
|
||||
// Add location data if location is included
|
||||
if (
|
||||
profileData.includeLocation &&
|
||||
profileData.latitude &&
|
||||
profileData.longitude
|
||||
) {
|
||||
payload.locLat = profileData.latitude;
|
||||
payload.locLon = profileData.longitude;
|
||||
}
|
||||
|
||||
const response = await this.axios.post(
|
||||
`${this.partnerApiServer}/api/partner/userProfile`,
|
||||
payload,
|
||||
{ headers },
|
||||
);
|
||||
|
||||
if (response.status === 201) {
|
||||
return true;
|
||||
} else {
|
||||
logger.error("Error saving profile:", response);
|
||||
throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_SAVED);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error saving profile:", errorStringForLog(error));
|
||||
handleApiError(error as AxiosError, "/api/partner/userProfile");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user profile from the server
|
||||
* @param activeDid - The user's DID
|
||||
* @returns true if successful, false otherwise
|
||||
*/
|
||||
async deleteProfile(activeDid: string): Promise<boolean> {
|
||||
try {
|
||||
const headers = await getHeaders(activeDid);
|
||||
const url = `${this.partnerApiServer}/api/partner/userProfile`;
|
||||
const response = await this.axios.delete(url, { headers });
|
||||
|
||||
if (response.status === 204 || response.status === 200) {
|
||||
logger.info("Profile deleted successfully");
|
||||
return true;
|
||||
} else {
|
||||
logger.error("Unexpected response status when deleting profile:", {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: response.data,
|
||||
});
|
||||
throw new Error(
|
||||
`Profile not deleted - HTTP ${response.status}: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.isApiError(error) && error.response) {
|
||||
const response = error.response;
|
||||
logger.error("API error deleting profile:", {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: response.data,
|
||||
url: this.getErrorUrl(error),
|
||||
});
|
||||
|
||||
// Handle specific HTTP status codes
|
||||
if (response.status === 204) {
|
||||
logger.debug("Profile deleted successfully (204 No Content)");
|
||||
return true; // 204 is success for DELETE operations
|
||||
} else if (response.status === 404) {
|
||||
logger.warn("Profile not found - may already be deleted");
|
||||
return true; // Consider this a success if profile doesn't exist
|
||||
} else if (response.status === 400) {
|
||||
logger.error("Bad request when deleting profile:", response.data);
|
||||
const errorMessage =
|
||||
typeof response.data === "string"
|
||||
? response.data
|
||||
: response.data?.message || "Bad request";
|
||||
throw new Error(`Profile deletion failed: ${errorMessage}`);
|
||||
} else if (response.status === 401) {
|
||||
logger.error("Unauthorized to delete profile");
|
||||
throw new Error("You are not authorized to delete this profile");
|
||||
} else if (response.status === 403) {
|
||||
logger.error("Forbidden to delete profile");
|
||||
throw new Error("You are not allowed to delete this profile");
|
||||
}
|
||||
}
|
||||
|
||||
logger.error("Error deleting profile:", errorStringForLog(error));
|
||||
handleApiError(error as AxiosError, "/api/partner/userProfile");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update profile location
|
||||
* @param profileData - Current profile data
|
||||
* @param latitude - New latitude
|
||||
* @param longitude - New longitude
|
||||
* @returns Updated profile data
|
||||
*/
|
||||
updateProfileLocation(
|
||||
profileData: ProfileData,
|
||||
latitude: number,
|
||||
longitude: number,
|
||||
): ProfileData {
|
||||
return {
|
||||
...profileData,
|
||||
latitude,
|
||||
longitude,
|
||||
includeLocation: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle location inclusion in profile
|
||||
* @param profileData - Current profile data
|
||||
* @returns Updated profile data
|
||||
*/
|
||||
toggleProfileLocation(profileData: ProfileData): ProfileData {
|
||||
const includeLocation = !profileData.includeLocation;
|
||||
return {
|
||||
...profileData,
|
||||
latitude: includeLocation ? profileData.latitude : 0,
|
||||
longitude: includeLocation ? profileData.longitude : 0,
|
||||
includeLocation,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear profile location
|
||||
* @param profileData - Current profile data
|
||||
* @returns Updated profile data
|
||||
*/
|
||||
clearProfileLocation(profileData: ProfileData): ProfileData {
|
||||
return {
|
||||
...profileData,
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
includeLocation: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset profile to default state
|
||||
* @returns Default profile data
|
||||
*/
|
||||
getDefaultProfile(): ProfileData {
|
||||
return {
|
||||
description: "",
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
includeLocation: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for API errors with proper typing
|
||||
*/
|
||||
private isApiError(error: unknown): error is {
|
||||
response?: {
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
data?: { message?: string } | string;
|
||||
};
|
||||
} {
|
||||
return typeof error === "object" && error !== null && "response" in error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract error URL safely from error object
|
||||
*/
|
||||
private getErrorUrl(error: unknown): string | undefined {
|
||||
if (this.isAxiosError(error)) {
|
||||
return error.config?.url;
|
||||
}
|
||||
if (this.isApiError(error) && this.hasConfigProperty(error)) {
|
||||
const config = this.getConfigProperty(error);
|
||||
return config?.url;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if error has config property
|
||||
*/
|
||||
private hasConfigProperty(
|
||||
error: unknown,
|
||||
): error is { config?: { url?: string } } {
|
||||
return typeof error === "object" && error !== null && "config" in error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely extract config property from error
|
||||
*/
|
||||
private getConfigProperty(error: {
|
||||
config?: { url?: string };
|
||||
}): { url?: string } | undefined {
|
||||
return error.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for AxiosError
|
||||
*/
|
||||
private isAxiosError(error: unknown): error is AxiosError {
|
||||
return error instanceof AxiosError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create a ProfileService instance
|
||||
*/
|
||||
export function createProfileService(
|
||||
axios: AxiosInstance,
|
||||
partnerApiServer: string,
|
||||
): ProfileService {
|
||||
return new ProfileService(axios, partnerApiServer);
|
||||
}
|
||||
@@ -437,17 +437,17 @@ export const PlatformServiceMixin = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Utility method for retrieving and parsing settings
|
||||
* Utility method for retrieving master settings
|
||||
* Common pattern used across many components
|
||||
*/
|
||||
async $getSettings(
|
||||
key: string,
|
||||
async $getMasterSettings(
|
||||
fallback: Settings | null = null,
|
||||
): Promise<Settings | null> {
|
||||
try {
|
||||
// Master settings: query by id
|
||||
const result = await this.$dbQuery(
|
||||
"SELECT * FROM settings WHERE id = ? OR accountDid = ?",
|
||||
[key, key],
|
||||
"SELECT * FROM settings WHERE id = ?",
|
||||
[MASTER_SETTINGS_KEY],
|
||||
);
|
||||
|
||||
if (!result?.values?.length) {
|
||||
@@ -472,8 +472,7 @@ export const PlatformServiceMixin = {
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
logger.error(`[Settings Trace] ❌ Failed to get settings:`, {
|
||||
key,
|
||||
logger.error(`[Settings Trace] ❌ Failed to get master settings:`, {
|
||||
error,
|
||||
});
|
||||
return fallback;
|
||||
@@ -491,10 +490,7 @@ export const PlatformServiceMixin = {
|
||||
): Promise<Settings> {
|
||||
try {
|
||||
// Get default settings
|
||||
const defaultSettings = await this.$getSettings(
|
||||
defaultKey,
|
||||
defaultFallback,
|
||||
);
|
||||
const defaultSettings = await this.$getMasterSettings(defaultFallback);
|
||||
|
||||
// If no account DID, return defaults
|
||||
if (!accountDid) {
|
||||
@@ -757,19 +753,20 @@ export const PlatformServiceMixin = {
|
||||
* @returns Fresh settings object from database
|
||||
*/
|
||||
async $settings(defaults: Settings = {}): Promise<Settings> {
|
||||
const settings = await this.$getSettings(MASTER_SETTINGS_KEY, defaults);
|
||||
const settings = await this.$getMasterSettings(defaults);
|
||||
|
||||
if (!settings) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
// **ELECTRON-SPECIFIC FIX**: Apply platform-specific API server override
|
||||
// This ensures Electron always uses production endpoints regardless of cached settings
|
||||
if (process.env.VITE_PLATFORM === "electron") {
|
||||
// FIXED: Remove forced override - respect user preferences
|
||||
// Only set default if no user preference exists
|
||||
if (!settings.apiServer && process.env.VITE_PLATFORM === "electron") {
|
||||
// Import constants dynamically to get platform-specific values
|
||||
const { DEFAULT_ENDORSER_API_SERVER } = await import(
|
||||
"../constants/app"
|
||||
);
|
||||
// Only set if user hasn't specified a preference
|
||||
settings.apiServer = DEFAULT_ENDORSER_API_SERVER;
|
||||
}
|
||||
|
||||
@@ -789,10 +786,7 @@ export const PlatformServiceMixin = {
|
||||
): Promise<Settings> {
|
||||
try {
|
||||
// Get default settings first
|
||||
const defaultSettings = await this.$getSettings(
|
||||
MASTER_SETTINGS_KEY,
|
||||
defaults,
|
||||
);
|
||||
const defaultSettings = await this.$getMasterSettings(defaults);
|
||||
|
||||
if (!defaultSettings) {
|
||||
return defaults;
|
||||
@@ -813,14 +807,17 @@ export const PlatformServiceMixin = {
|
||||
defaultSettings,
|
||||
);
|
||||
|
||||
// **ELECTRON-SPECIFIC FIX**: Force production API endpoints for Electron
|
||||
// This ensures Electron doesn't use localhost development servers that might be saved in user settings
|
||||
if (process.env.VITE_PLATFORM === "electron") {
|
||||
// FIXED: Remove forced override - respect user preferences
|
||||
// Only set default if no user preference exists
|
||||
if (
|
||||
!mergedSettings.apiServer &&
|
||||
process.env.VITE_PLATFORM === "electron"
|
||||
) {
|
||||
// Import constants dynamically to get platform-specific values
|
||||
const { DEFAULT_ENDORSER_API_SERVER } = await import(
|
||||
"../constants/app"
|
||||
);
|
||||
|
||||
// Only set if user hasn't specified a preference
|
||||
mergedSettings.apiServer = DEFAULT_ENDORSER_API_SERVER;
|
||||
}
|
||||
|
||||
@@ -1574,10 +1571,7 @@ export const PlatformServiceMixin = {
|
||||
async $debugMergedSettings(did: string): Promise<void> {
|
||||
try {
|
||||
// Get default settings
|
||||
const defaultSettings = await this.$getSettings(
|
||||
MASTER_SETTINGS_KEY,
|
||||
{},
|
||||
);
|
||||
const defaultSettings = await this.$getMasterSettings({});
|
||||
logger.info(
|
||||
`[PlatformServiceMixin] Default settings:`,
|
||||
defaultSettings,
|
||||
@@ -1624,10 +1618,7 @@ export interface IPlatformServiceMixin {
|
||||
): Promise<QueryExecResult | undefined>;
|
||||
$dbExec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
|
||||
$dbGetOneRow(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
|
||||
$getSettings(
|
||||
key: string,
|
||||
fallback?: Settings | null,
|
||||
): Promise<Settings | null>;
|
||||
$getMasterSettings(fallback?: Settings | null): Promise<Settings | null>;
|
||||
$getMergedSettings(
|
||||
defaultKey: string,
|
||||
accountDid?: string,
|
||||
@@ -1749,10 +1740,7 @@ declare module "@vue/runtime-core" {
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<unknown[] | undefined>;
|
||||
$getSettings(
|
||||
key: string,
|
||||
defaults?: Settings | null,
|
||||
): Promise<Settings | null>;
|
||||
$getMasterSettings(defaults?: Settings | null): Promise<Settings | null>;
|
||||
$getMergedSettings(
|
||||
key: string,
|
||||
did?: string,
|
||||
|
||||
298
src/utils/errorHandler.ts
Normal file
298
src/utils/errorHandler.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* Standardized Error Handler
|
||||
*
|
||||
* Provides consistent error handling patterns across the TimeSafari codebase
|
||||
* to improve debugging, user experience, and maintainability.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @since 2025-08-25
|
||||
*/
|
||||
|
||||
import { AxiosError } from "axios";
|
||||
import { logger } from "./logger";
|
||||
|
||||
/**
|
||||
* Standard error context for consistent logging
|
||||
*/
|
||||
export interface ErrorContext {
|
||||
component: string;
|
||||
operation: string;
|
||||
timestamp: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced error information for better debugging
|
||||
*/
|
||||
export interface EnhancedErrorInfo {
|
||||
errorType: "AxiosError" | "NetworkError" | "ValidationError" | "UnknownError";
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
errorData?: unknown;
|
||||
errorMessage: string;
|
||||
errorStack?: string;
|
||||
requestContext?: {
|
||||
url?: string;
|
||||
method?: string;
|
||||
headers?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Standardized error handler for API operations
|
||||
*
|
||||
* @param error - The error that occurred
|
||||
* @param context - Context information about the operation
|
||||
* @param operation - Description of the operation being performed
|
||||
* @returns Enhanced error information for consistent handling
|
||||
*/
|
||||
export function handleApiError(
|
||||
error: unknown,
|
||||
context: ErrorContext,
|
||||
operation: string,
|
||||
): EnhancedErrorInfo {
|
||||
const baseContext = {
|
||||
...context,
|
||||
operation,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (error instanceof AxiosError) {
|
||||
const axiosError = error as AxiosError;
|
||||
const status = axiosError.response?.status;
|
||||
const statusText = axiosError.response?.statusText;
|
||||
const errorData = axiosError.response?.data;
|
||||
|
||||
const enhancedError: EnhancedErrorInfo = {
|
||||
errorType: "AxiosError",
|
||||
status,
|
||||
statusText,
|
||||
errorData,
|
||||
errorMessage: axiosError.message,
|
||||
errorStack: axiosError.stack,
|
||||
requestContext: {
|
||||
url: axiosError.config?.url,
|
||||
method: axiosError.config?.method,
|
||||
headers: axiosError.config?.headers,
|
||||
},
|
||||
};
|
||||
|
||||
// Log with consistent format
|
||||
logger.error(
|
||||
`[${context.component}] ❌ ${operation} failed (AxiosError):`,
|
||||
{
|
||||
...baseContext,
|
||||
...enhancedError,
|
||||
},
|
||||
);
|
||||
|
||||
return enhancedError;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
const enhancedError: EnhancedErrorInfo = {
|
||||
errorType: "UnknownError",
|
||||
errorMessage: error.message,
|
||||
errorStack: error.stack,
|
||||
};
|
||||
|
||||
logger.error(`[${context.component}] ❌ ${operation} failed (Error):`, {
|
||||
...baseContext,
|
||||
...enhancedError,
|
||||
});
|
||||
|
||||
return enhancedError;
|
||||
}
|
||||
|
||||
// Handle unknown error types
|
||||
const enhancedError: EnhancedErrorInfo = {
|
||||
errorType: "UnknownError",
|
||||
errorMessage: String(error),
|
||||
};
|
||||
|
||||
logger.error(`[${context.component}] ❌ ${operation} failed (Unknown):`, {
|
||||
...baseContext,
|
||||
...enhancedError,
|
||||
});
|
||||
|
||||
return enhancedError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract human-readable error message from various error response formats
|
||||
*
|
||||
* @param errorData - Error response data
|
||||
* @returns Human-readable error message
|
||||
*/
|
||||
export function extractErrorMessage(errorData: unknown): string {
|
||||
if (typeof errorData === "string") {
|
||||
return errorData;
|
||||
}
|
||||
|
||||
if (typeof errorData === "object" && errorData !== null) {
|
||||
const obj = errorData as Record<string, unknown>;
|
||||
|
||||
// Try common error message fields
|
||||
if (obj.message && typeof obj.message === "string") {
|
||||
return obj.message;
|
||||
}
|
||||
|
||||
if (obj.error && typeof obj.error === "string") {
|
||||
return obj.error;
|
||||
}
|
||||
|
||||
if (obj.detail && typeof obj.detail === "string") {
|
||||
return obj.detail;
|
||||
}
|
||||
|
||||
if (obj.reason && typeof obj.reason === "string") {
|
||||
return obj.reason;
|
||||
}
|
||||
|
||||
// Fallback to stringified object
|
||||
return JSON.stringify(errorData);
|
||||
}
|
||||
|
||||
return String(errorData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create user-friendly error message from enhanced error info
|
||||
*
|
||||
* @param errorInfo - Enhanced error information
|
||||
* @param fallbackMessage - Fallback message if error details are insufficient
|
||||
* @returns User-friendly error message
|
||||
*/
|
||||
export function createUserMessage(
|
||||
errorInfo: EnhancedErrorInfo,
|
||||
fallbackMessage: string,
|
||||
): string {
|
||||
if (errorInfo.errorType === "AxiosError") {
|
||||
const status = errorInfo.status;
|
||||
const statusText = errorInfo.statusText;
|
||||
const errorMessage = extractErrorMessage(errorInfo.errorData);
|
||||
|
||||
if (status && statusText) {
|
||||
if (errorMessage && errorMessage !== "{}") {
|
||||
return `${fallbackMessage}: ${status} ${statusText} - ${errorMessage}`;
|
||||
}
|
||||
return `${fallbackMessage}: ${status} ${statusText}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
errorInfo.errorMessage &&
|
||||
errorInfo.errorMessage !== "Request failed with status code 0"
|
||||
) {
|
||||
return `${fallbackMessage}: ${errorInfo.errorMessage}`;
|
||||
}
|
||||
|
||||
return fallbackMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle specific HTTP status codes with appropriate user messages
|
||||
*
|
||||
* @param status - HTTP status code
|
||||
* @param errorData - Error response data
|
||||
* @param operation - Description of the operation
|
||||
* @returns User-friendly error message
|
||||
*/
|
||||
export function handleHttpStatus(
|
||||
status: number,
|
||||
errorData: unknown,
|
||||
operation: string,
|
||||
): string {
|
||||
const errorMessage = extractErrorMessage(errorData);
|
||||
|
||||
switch (status) {
|
||||
case 400:
|
||||
return errorMessage || `${operation} failed: Bad request`;
|
||||
case 401:
|
||||
return `${operation} failed: Authentication required`;
|
||||
case 403:
|
||||
return `${operation} failed: Access denied`;
|
||||
case 404:
|
||||
return errorMessage || `${operation} failed: Resource not found`;
|
||||
case 409:
|
||||
return errorMessage || `${operation} failed: Conflict with existing data`;
|
||||
case 422:
|
||||
return errorMessage || `${operation} failed: Validation error`;
|
||||
case 429:
|
||||
return `${operation} failed: Too many requests. Please try again later.`;
|
||||
case 500:
|
||||
return `${operation} failed: Server error. Please try again later.`;
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
return `${operation} failed: Service temporarily unavailable. Please try again later.`;
|
||||
default:
|
||||
return errorMessage || `${operation} failed: HTTP ${status}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error is a network-related error
|
||||
*
|
||||
* @param error - The error to check
|
||||
* @returns True if the error is network-related
|
||||
*/
|
||||
export function isNetworkError(error: unknown): boolean {
|
||||
if (error instanceof AxiosError) {
|
||||
return !error.response && !error.request;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
const message = error.message.toLowerCase();
|
||||
return (
|
||||
message.includes("network") ||
|
||||
message.includes("timeout") ||
|
||||
message.includes("connection") ||
|
||||
message.includes("fetch")
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error is a timeout error
|
||||
*
|
||||
* @param error - The error to check
|
||||
* @returns True if the error is a timeout
|
||||
*/
|
||||
export function isTimeoutError(error: unknown): boolean {
|
||||
if (error instanceof AxiosError) {
|
||||
return (
|
||||
error.code === "ECONNABORTED" ||
|
||||
error.message.toLowerCase().includes("timeout")
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.message.toLowerCase().includes("timeout");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create standardized error context for components
|
||||
*
|
||||
* @param component - Component name
|
||||
* @param operation - Operation being performed
|
||||
* @param additionalContext - Additional context information
|
||||
* @returns Standardized error context
|
||||
*/
|
||||
export function createErrorContext(
|
||||
component: string,
|
||||
operation: string,
|
||||
additionalContext: Record<string, unknown> = {},
|
||||
): ErrorContext {
|
||||
return {
|
||||
component,
|
||||
operation,
|
||||
timestamp: new Date().toISOString(),
|
||||
...additionalContext,
|
||||
};
|
||||
}
|
||||
482
src/utils/performanceOptimizer.ts
Normal file
482
src/utils/performanceOptimizer.ts
Normal file
@@ -0,0 +1,482 @@
|
||||
/**
|
||||
* Performance Optimizer
|
||||
*
|
||||
* Provides utilities for optimizing API calls, database queries, and component
|
||||
* rendering to improve TimeSafari application performance.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @since 2025-08-25
|
||||
*/
|
||||
|
||||
import { logger } from "./logger";
|
||||
|
||||
/**
|
||||
* Batch operation configuration
|
||||
*/
|
||||
export interface BatchConfig {
|
||||
maxBatchSize: number;
|
||||
maxWaitTime: number;
|
||||
retryAttempts: number;
|
||||
retryDelay: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default batch configuration
|
||||
*/
|
||||
export const DEFAULT_BATCH_CONFIG: BatchConfig = {
|
||||
maxBatchSize: 10,
|
||||
maxWaitTime: 100, // milliseconds
|
||||
retryAttempts: 3,
|
||||
retryDelay: 1000, // milliseconds
|
||||
};
|
||||
|
||||
/**
|
||||
* Batched operation item
|
||||
*/
|
||||
export interface BatchItem<T, R> {
|
||||
id: string;
|
||||
data: T;
|
||||
resolve: (value: R) => void;
|
||||
reject: (error: Error) => void;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch processor for API operations
|
||||
*
|
||||
* Groups multiple similar operations into batches to reduce
|
||||
* the number of API calls and improve performance.
|
||||
*/
|
||||
export class BatchProcessor<T, R> {
|
||||
private items: BatchItem<T, R>[] = [];
|
||||
private timer: NodeJS.Timeout | null = null;
|
||||
private processing = false;
|
||||
private config: BatchConfig;
|
||||
|
||||
constructor(
|
||||
private batchHandler: (items: T[]) => Promise<R[]>,
|
||||
private itemIdExtractor: (item: T) => string,
|
||||
config: Partial<BatchConfig> = {},
|
||||
) {
|
||||
this.config = { ...DEFAULT_BATCH_CONFIG, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item to the batch
|
||||
*
|
||||
* @param data - Data to process
|
||||
* @returns Promise that resolves when the item is processed
|
||||
*/
|
||||
async add(data: T): Promise<R> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const item: BatchItem<T, R> = {
|
||||
id: this.itemIdExtractor(data),
|
||||
data,
|
||||
resolve,
|
||||
reject,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
this.items.push(item);
|
||||
|
||||
// Start timer if this is the first item
|
||||
if (this.items.length === 1) {
|
||||
this.startTimer();
|
||||
}
|
||||
|
||||
// Process immediately if batch is full
|
||||
if (this.items.length >= this.config.maxBatchSize) {
|
||||
this.processBatch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the batch timer
|
||||
*/
|
||||
private startTimer(): void {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
this.timer = setTimeout(() => {
|
||||
this.processBatch();
|
||||
}, this.config.maxWaitTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the current batch
|
||||
*/
|
||||
private async processBatch(): Promise<void> {
|
||||
if (this.processing || this.items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.processing = true;
|
||||
|
||||
// Clear timer
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
// Get current batch
|
||||
const currentItems = [...this.items];
|
||||
this.items = [];
|
||||
|
||||
try {
|
||||
logger.debug("[BatchProcessor] 🔄 Processing batch:", {
|
||||
batchSize: currentItems.length,
|
||||
itemIds: currentItems.map((item) => item.id),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Process batch
|
||||
const results = await this.batchHandler(
|
||||
currentItems.map((item) => item.data),
|
||||
);
|
||||
|
||||
// Map results back to items
|
||||
const resultMap = new Map<string, R>();
|
||||
results.forEach((result, index) => {
|
||||
const item = currentItems[index];
|
||||
if (item) {
|
||||
resultMap.set(item.id, result);
|
||||
}
|
||||
});
|
||||
|
||||
// Resolve promises
|
||||
currentItems.forEach((item) => {
|
||||
const result = resultMap.get(item.id);
|
||||
if (result !== undefined) {
|
||||
item.resolve(result);
|
||||
} else {
|
||||
item.reject(new Error(`No result found for item ${item.id}`));
|
||||
}
|
||||
});
|
||||
|
||||
logger.debug("[BatchProcessor] ✅ Batch processed successfully:", {
|
||||
batchSize: currentItems.length,
|
||||
resultsCount: results.length,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("[BatchProcessor] ❌ Batch processing failed:", {
|
||||
batchSize: currentItems.length,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Reject all items in the batch
|
||||
currentItems.forEach((item) => {
|
||||
item.reject(error instanceof Error ? error : new Error(String(error)));
|
||||
});
|
||||
} finally {
|
||||
this.processing = false;
|
||||
|
||||
// Start timer for remaining items if any
|
||||
if (this.items.length > 0) {
|
||||
this.startTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current batch status
|
||||
*/
|
||||
getStatus(): {
|
||||
pendingItems: number;
|
||||
isProcessing: boolean;
|
||||
hasTimer: boolean;
|
||||
} {
|
||||
return {
|
||||
pendingItems: this.items.length,
|
||||
isProcessing: this.processing,
|
||||
hasTimer: this.timer !== null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all pending items
|
||||
*/
|
||||
clear(): void {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
// Reject all pending items
|
||||
this.items.forEach((item) => {
|
||||
item.reject(new Error("Batch processor cleared"));
|
||||
});
|
||||
|
||||
this.items = [];
|
||||
this.processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Database query optimizer
|
||||
*
|
||||
* Provides utilities for optimizing database queries and reducing
|
||||
* the number of database operations.
|
||||
*/
|
||||
export class DatabaseOptimizer {
|
||||
/**
|
||||
* Batch multiple SELECT queries into a single query
|
||||
*
|
||||
* @param baseQuery - Base SELECT query
|
||||
* @param ids - Array of IDs to query
|
||||
* @param idColumn - Name of the ID column
|
||||
* @returns Optimized query string
|
||||
*/
|
||||
static batchSelectQuery(
|
||||
baseQuery: string,
|
||||
ids: (string | number)[],
|
||||
idColumn: string,
|
||||
): string {
|
||||
if (ids.length === 0) {
|
||||
return baseQuery;
|
||||
}
|
||||
|
||||
if (ids.length === 1) {
|
||||
return `${baseQuery} WHERE ${idColumn} = ?`;
|
||||
}
|
||||
|
||||
const placeholders = ids.map(() => "?").join(", ");
|
||||
return `${baseQuery} WHERE ${idColumn} IN (${placeholders})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a query plan for multiple operations
|
||||
*
|
||||
* @param operations - Array of database operations
|
||||
* @returns Optimized query plan
|
||||
*/
|
||||
static createQueryPlan(
|
||||
operations: Array<{
|
||||
type: "SELECT" | "INSERT" | "UPDATE" | "DELETE";
|
||||
table: string;
|
||||
priority: number;
|
||||
}>,
|
||||
): Array<{
|
||||
type: "SELECT" | "INSERT" | "UPDATE" | "DELETE";
|
||||
table: string;
|
||||
priority: number;
|
||||
batchable: boolean;
|
||||
}> {
|
||||
return operations
|
||||
.map((op) => ({
|
||||
...op,
|
||||
batchable: op.type === "SELECT" || op.type === "INSERT",
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
// Sort by priority first, then by type
|
||||
if (a.priority !== b.priority) {
|
||||
return b.priority - a.priority;
|
||||
}
|
||||
|
||||
// SELECT operations first, then INSERT, UPDATE, DELETE
|
||||
const typeOrder = { SELECT: 0, INSERT: 1, UPDATE: 2, DELETE: 3 };
|
||||
return typeOrder[a.type] - typeOrder[b.type];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component rendering optimizer
|
||||
*
|
||||
* Provides utilities for optimizing Vue component rendering
|
||||
* and reducing unnecessary re-renders.
|
||||
*/
|
||||
export class ComponentOptimizer {
|
||||
/**
|
||||
* Debounce function calls to prevent excessive execution
|
||||
*
|
||||
* @param func - Function to debounce
|
||||
* @param wait - Wait time in milliseconds
|
||||
* @returns Debounced function
|
||||
*/
|
||||
static debounce<T extends (...args: unknown[]) => unknown>(
|
||||
func: T,
|
||||
wait: number,
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
func(...args);
|
||||
}, wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttle function calls to limit execution frequency
|
||||
*
|
||||
* @param func - Function to throttle
|
||||
* @param limit - Time limit in milliseconds
|
||||
* @returns Throttled function
|
||||
*/
|
||||
static throttle<T extends (...args: unknown[]) => unknown>(
|
||||
func: T,
|
||||
limit: number,
|
||||
): (...args: Parameters<T>) => void {
|
||||
let inThrottle = false;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (!inThrottle) {
|
||||
func(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => {
|
||||
inThrottle = false;
|
||||
}, limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Memoize function results to avoid redundant computation
|
||||
*
|
||||
* @param func - Function to memoize
|
||||
* @param keyGenerator - Function to generate cache keys
|
||||
* @returns Memoized function
|
||||
*/
|
||||
static memoize<T extends (...args: unknown[]) => unknown, K>(
|
||||
func: T,
|
||||
keyGenerator: (...args: Parameters<T>) => K,
|
||||
): T {
|
||||
const cache = new Map<K, unknown>();
|
||||
|
||||
return ((...args: Parameters<T>) => {
|
||||
const key = keyGenerator(...args);
|
||||
|
||||
if (cache.has(key)) {
|
||||
return cache.get(key);
|
||||
}
|
||||
|
||||
const result = func(...args);
|
||||
cache.set(key, result);
|
||||
return result;
|
||||
}) as T;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performance monitoring utility
|
||||
*
|
||||
* Tracks and reports performance metrics for optimization analysis.
|
||||
*/
|
||||
export class PerformanceMonitor {
|
||||
private static instance: PerformanceMonitor;
|
||||
private metrics = new Map<
|
||||
string,
|
||||
Array<{ timestamp: number; duration: number }>
|
||||
>();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
*/
|
||||
static getInstance(): PerformanceMonitor {
|
||||
if (!PerformanceMonitor.instance) {
|
||||
PerformanceMonitor.instance = new PerformanceMonitor();
|
||||
}
|
||||
return PerformanceMonitor.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start timing an operation
|
||||
*
|
||||
* @param operationName - Name of the operation
|
||||
* @returns Function to call when operation completes
|
||||
*/
|
||||
startTiming(operationName: string): () => void {
|
||||
const startTime = performance.now();
|
||||
|
||||
return () => {
|
||||
const duration = performance.now() - startTime;
|
||||
this.recordMetric(operationName, duration);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a performance metric
|
||||
*
|
||||
* @param operationName - Name of the operation
|
||||
* @param duration - Duration in milliseconds
|
||||
*/
|
||||
private recordMetric(operationName: string, duration: number): void {
|
||||
if (!this.metrics.has(operationName)) {
|
||||
this.metrics.set(operationName, []);
|
||||
}
|
||||
|
||||
const operationMetrics = this.metrics.get(operationName)!;
|
||||
operationMetrics.push({
|
||||
timestamp: Date.now(),
|
||||
duration,
|
||||
});
|
||||
|
||||
// Keep only last 100 metrics per operation
|
||||
if (operationMetrics.length > 100) {
|
||||
operationMetrics.splice(0, operationMetrics.length - 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get performance summary for an operation
|
||||
*
|
||||
* @param operationName - Name of the operation
|
||||
* @returns Performance statistics
|
||||
*/
|
||||
getPerformanceSummary(operationName: string): {
|
||||
count: number;
|
||||
average: number;
|
||||
min: number;
|
||||
max: number;
|
||||
recentAverage: number;
|
||||
} | null {
|
||||
const metrics = this.metrics.get(operationName);
|
||||
if (!metrics || metrics.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const durations = metrics.map((m) => m.duration);
|
||||
const recentMetrics = metrics.slice(-10); // Last 10 metrics
|
||||
|
||||
return {
|
||||
count: metrics.length,
|
||||
average: durations.reduce((a, b) => a + b, 0) / durations.length,
|
||||
min: Math.min(...durations),
|
||||
max: Math.max(...durations),
|
||||
recentAverage:
|
||||
recentMetrics.reduce((a, b) => a + b.duration, 0) /
|
||||
recentMetrics.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all performance metrics
|
||||
*/
|
||||
getAllMetrics(): Map<string, Array<{ timestamp: number; duration: number }>> {
|
||||
return new Map(this.metrics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all performance metrics
|
||||
*/
|
||||
clearMetrics(): void {
|
||||
this.metrics.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get the performance monitor
|
||||
*/
|
||||
export const getPerformanceMonitor = (): PerformanceMonitor => {
|
||||
return PerformanceMonitor.getInstance();
|
||||
};
|
||||
@@ -58,8 +58,10 @@
|
||||
v-if="!isRegistered"
|
||||
:passkeys-enabled="PASSKEYS_ENABLED"
|
||||
:given-name="givenName"
|
||||
message="Before you can publicly announce a new project or time commitment,
|
||||
a friend needs to register you."
|
||||
:message="
|
||||
`Before you can publicly announce a new project or time commitment, ` +
|
||||
`a friend needs to register you.`
|
||||
"
|
||||
/>
|
||||
|
||||
<!-- Notifications -->
|
||||
@@ -752,6 +754,7 @@ import "leaflet/dist/leaflet.css";
|
||||
|
||||
import { Buffer } from "buffer/";
|
||||
import "dexie-export-import";
|
||||
|
||||
// @ts-expect-error - they aren't exporting it but it's there
|
||||
import { ImportProgress } from "dexie-export-import";
|
||||
import { LeafletMouseEvent } from "leaflet";
|
||||
@@ -813,11 +816,13 @@ import {
|
||||
isApiError,
|
||||
ImportContent,
|
||||
} from "@/interfaces/accountView";
|
||||
import {
|
||||
ProfileService,
|
||||
createProfileService,
|
||||
ProfileData,
|
||||
} from "@/services/ProfileService";
|
||||
// Profile data interface (inlined from ProfileService)
|
||||
interface ProfileData {
|
||||
description: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
includeLocation: boolean;
|
||||
}
|
||||
|
||||
const inputImportFileNameRef = ref<Blob>();
|
||||
|
||||
@@ -916,7 +921,6 @@ export default class AccountViewView extends Vue {
|
||||
imageLimits: ImageRateLimits | null = null;
|
||||
limitsMessage: string = "";
|
||||
|
||||
private profileService!: ProfileService;
|
||||
private notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
|
||||
created() {
|
||||
@@ -926,7 +930,10 @@ export default class AccountViewView extends Vue {
|
||||
// This prevents the "Cannot read properties of undefined (reading 'Default')" error
|
||||
if (L.Icon.Default) {
|
||||
// Type-safe way to handle Leaflet icon prototype
|
||||
const iconDefault = L.Icon.Default.prototype as Record<string, unknown>;
|
||||
const iconDefault = L.Icon.Default.prototype as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
if ("_getIconUrl" in iconDefault) {
|
||||
delete iconDefault._getIconUrl;
|
||||
}
|
||||
@@ -948,17 +955,21 @@ export default class AccountViewView extends Vue {
|
||||
* @throws Will display specific messages to the user based on different errors.
|
||||
*/
|
||||
async mounted(): Promise<void> {
|
||||
this.profileService = createProfileService(
|
||||
this.axios,
|
||||
this.partnerApiServer,
|
||||
);
|
||||
try {
|
||||
await this.initializeState();
|
||||
await this.processIdentity();
|
||||
|
||||
// Profile service logic now inlined - no need for external service
|
||||
logger.debug(
|
||||
"[AccountViewView] Profile logic ready with partnerApiServer:",
|
||||
{
|
||||
partnerApiServer: this.partnerApiServer,
|
||||
},
|
||||
);
|
||||
|
||||
if (this.isRegistered) {
|
||||
try {
|
||||
const profile = await this.profileService.loadProfile(this.activeDid);
|
||||
const profile = await this.loadProfile(this.activeDid);
|
||||
if (profile) {
|
||||
this.userProfileDesc = profile.description;
|
||||
this.userProfileLatitude = profile.latitude;
|
||||
@@ -1411,21 +1422,24 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
async checkLimits(): Promise<void> {
|
||||
this.loadingLimits = true;
|
||||
const did = this.activeDid;
|
||||
if (!did) {
|
||||
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IDENTIFIER;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const did = this.activeDid;
|
||||
|
||||
if (!did) {
|
||||
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IDENTIFIER;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.$saveUserSettings(did, {
|
||||
apiServer: this.apiServer,
|
||||
partnerApiServer: this.partnerApiServer,
|
||||
webPushServer: this.webPushServer,
|
||||
});
|
||||
|
||||
const imageResp = await fetchImageRateLimits(this.axios, did);
|
||||
const imageResp = await fetchImageRateLimits(
|
||||
this.axios,
|
||||
did,
|
||||
this.DEFAULT_IMAGE_API_SERVER,
|
||||
);
|
||||
|
||||
if (imageResp.status === 200) {
|
||||
this.imageLimits = imageResp.data;
|
||||
@@ -1451,7 +1465,26 @@ export default class AccountViewView extends Vue {
|
||||
} catch (error) {
|
||||
this.limitsMessage =
|
||||
ACCOUNT_VIEW_CONSTANTS.LIMITS.ERROR_RETRIEVING_LIMITS;
|
||||
logger.error("Error retrieving limits: ", error);
|
||||
|
||||
// Enhanced error logging with server context
|
||||
const axiosError = error as {
|
||||
response?: {
|
||||
data?: { error?: { code?: string; message?: string } };
|
||||
status?: number;
|
||||
};
|
||||
};
|
||||
logger.error("[Server Limits] Error retrieving limits:", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
did: did,
|
||||
apiServer: this.apiServer,
|
||||
partnerApiServer: this.partnerApiServer,
|
||||
errorCode: axiosError?.response?.data?.error?.code,
|
||||
errorMessage: axiosError?.response?.data?.error?.message,
|
||||
httpStatus: axiosError?.response?.status,
|
||||
needsUserMigration: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// this.notify.error(this.limitsMessage, TIMEOUTS.STANDARD);
|
||||
} finally {
|
||||
this.loadingLimits = false;
|
||||
@@ -1459,24 +1492,70 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
|
||||
async onClickSaveApiServer(): Promise<void> {
|
||||
await this.$saveSettings({
|
||||
apiServer: this.apiServerInput,
|
||||
// Enhanced diagnostic logging for claim URL changes
|
||||
const previousApiServer = this.apiServer;
|
||||
const newApiServer = this.apiServerInput;
|
||||
|
||||
logger.debug("[Server Switching] Claim URL change initiated:", {
|
||||
did: this.activeDid,
|
||||
previousServer: previousApiServer,
|
||||
newServer: newApiServer,
|
||||
changeType: "apiServer",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
this.apiServer = this.apiServerInput;
|
||||
|
||||
await this.$saveSettings({
|
||||
apiServer: newApiServer,
|
||||
});
|
||||
this.apiServer = newApiServer;
|
||||
|
||||
// Add this line to save to user-specific settings
|
||||
await this.$saveUserSettings(this.activeDid, {
|
||||
apiServer: this.apiServer,
|
||||
});
|
||||
|
||||
// Log successful server switch
|
||||
logger.debug("[Server Switching] Claim URL change completed:", {
|
||||
did: this.activeDid,
|
||||
previousServer: previousApiServer,
|
||||
newServer: newApiServer,
|
||||
changeType: "apiServer",
|
||||
settingsSaved: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
async onClickSavePartnerServer(): Promise<void> {
|
||||
await this.$saveSettings({
|
||||
partnerApiServer: this.partnerApiServerInput,
|
||||
// Enhanced diagnostic logging for partner server changes
|
||||
const previousPartnerServer = this.partnerApiServer;
|
||||
const newPartnerServer = this.partnerApiServerInput;
|
||||
|
||||
logger.debug("[Server Switching] Partner server change initiated:", {
|
||||
did: this.activeDid,
|
||||
previousServer: previousPartnerServer,
|
||||
newServer: newPartnerServer,
|
||||
changeType: "partnerApiServer",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
this.partnerApiServer = this.partnerApiServerInput;
|
||||
|
||||
await this.$saveSettings({
|
||||
partnerApiServer: newPartnerServer,
|
||||
});
|
||||
this.partnerApiServer = newPartnerServer;
|
||||
|
||||
await this.$saveUserSettings(this.activeDid, {
|
||||
partnerApiServer: this.partnerApiServer,
|
||||
});
|
||||
|
||||
// Log successful partner server switch
|
||||
logger.debug("[Server Switching] Partner server change completed:", {
|
||||
did: this.activeDid,
|
||||
previousServer: previousPartnerServer,
|
||||
newServer: newPartnerServer,
|
||||
changeType: "partnerApiServer",
|
||||
settingsSaved: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
async onClickSavePushServer(): Promise<void> {
|
||||
@@ -1550,7 +1629,6 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
onMapReady(map: L.Map): void {
|
||||
try {
|
||||
logger.debug("Map ready event fired, map object:", map);
|
||||
// doing this here instead of on the l-map element avoids a recentering after a drag then zoom at startup
|
||||
const zoom =
|
||||
this.userProfileLatitude && this.userProfileLongitude ? 12 : 2;
|
||||
@@ -1579,19 +1657,15 @@ export default class AccountViewView extends Vue {
|
||||
// Try to set map ready after component is mounted
|
||||
setTimeout(() => {
|
||||
this.isMapReady = true;
|
||||
logger.debug("Map ready set to true after mounted");
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Fallback method to handle map initialization failures
|
||||
private handleMapInitFailure(): void {
|
||||
logger.debug("Starting map initialization timeout (5 seconds)");
|
||||
setTimeout(() => {
|
||||
if (!this.isMapReady) {
|
||||
logger.warn("Map failed to initialize, forcing ready state");
|
||||
this.isMapReady = true;
|
||||
} else {
|
||||
logger.debug("Map initialized successfully, timeout not needed");
|
||||
}
|
||||
}, 5000); // 5 second timeout
|
||||
}
|
||||
@@ -1615,7 +1689,7 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
logger.debug("Saving profile data:", profileData);
|
||||
|
||||
const success = await this.profileService.saveProfile(
|
||||
const success = await this.saveProfileToServer(
|
||||
this.activeDid,
|
||||
profileData,
|
||||
);
|
||||
@@ -1634,7 +1708,7 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
toggleUserProfileLocation(): void {
|
||||
try {
|
||||
const updated = this.profileService.toggleProfileLocation({
|
||||
const updated = this.toggleProfileLocation({
|
||||
description: this.userProfileDesc,
|
||||
latitude: this.userProfileLatitude,
|
||||
longitude: this.userProfileLongitude,
|
||||
@@ -1679,8 +1753,7 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
async deleteProfile(): Promise<void> {
|
||||
try {
|
||||
logger.debug("Attempting to delete profile for DID:", this.activeDid);
|
||||
const success = await this.profileService.deleteProfile(this.activeDid);
|
||||
const success = await this.deleteProfileFromServer(this.activeDid);
|
||||
if (success) {
|
||||
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_DELETED);
|
||||
this.userProfileDesc = "";
|
||||
@@ -1688,7 +1761,6 @@ export default class AccountViewView extends Vue {
|
||||
this.userProfileLongitude = 0;
|
||||
this.includeUserProfileLocation = false;
|
||||
this.isMapReady = false; // Reset map state
|
||||
logger.debug("Profile deleted successfully, UI state reset");
|
||||
} else {
|
||||
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR);
|
||||
}
|
||||
@@ -1734,7 +1806,6 @@ export default class AccountViewView extends Vue {
|
||||
this.isMapReady = false;
|
||||
this.userProfileLatitude = 0;
|
||||
this.userProfileLongitude = 0;
|
||||
logger.debug("Location unchecked, map state reset");
|
||||
} else {
|
||||
// Location checkbox was checked, start map initialization timeout
|
||||
this.isMapReady = false;
|
||||
@@ -1743,7 +1814,6 @@ export default class AccountViewView extends Vue {
|
||||
// Try to set map ready after a short delay to allow Vue to render
|
||||
setTimeout(() => {
|
||||
if (!this.isMapReady) {
|
||||
logger.debug("Setting map ready after timeout");
|
||||
this.isMapReady = true;
|
||||
}
|
||||
}, 1000); // 1 second delay
|
||||
@@ -1796,5 +1866,338 @@ export default class AccountViewView extends Vue {
|
||||
onRecheckLimits() {
|
||||
this.checkLimits();
|
||||
}
|
||||
|
||||
// Inlined profile methods (previously in ProfileService)
|
||||
|
||||
/**
|
||||
* Load user profile from the partner API
|
||||
*/
|
||||
private async loadProfile(did: string): Promise<ProfileData | null> {
|
||||
try {
|
||||
const requestId = `profile_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
logger.debug("[AccountViewView] Loading profile:", {
|
||||
requestId,
|
||||
did,
|
||||
partnerApiServer: this.partnerApiServer,
|
||||
});
|
||||
|
||||
// Get authentication headers
|
||||
const headers = await getHeaders(did);
|
||||
|
||||
const fullUrl = `${this.partnerApiServer}/api/partner/userProfileForIssuer/${did}`;
|
||||
|
||||
logger.debug("[AccountViewView] Making API request:", {
|
||||
requestId,
|
||||
did,
|
||||
fullUrl,
|
||||
hasAuthHeader: !!headers.Authorization,
|
||||
});
|
||||
|
||||
const response = await this.axios.get(fullUrl, { headers });
|
||||
|
||||
logger.debug("[AccountViewView] Profile loaded successfully:", {
|
||||
requestId,
|
||||
status: response.status,
|
||||
hasData: !!response.data,
|
||||
});
|
||||
|
||||
if (response.data && response.data.data) {
|
||||
const profileData = response.data.data;
|
||||
logger.debug("[AccountViewView] Parsing profile data:", {
|
||||
requestId,
|
||||
locLat: profileData.locLat,
|
||||
locLon: profileData.locLon,
|
||||
description: profileData.description,
|
||||
});
|
||||
|
||||
const result = {
|
||||
description: profileData.description || "",
|
||||
latitude: profileData.locLat || 0,
|
||||
longitude: profileData.locLon || 0,
|
||||
includeLocation: !!(profileData.locLat && profileData.locLon),
|
||||
};
|
||||
|
||||
logger.debug("[AccountViewView] Parsed profile result:", {
|
||||
requestId,
|
||||
result,
|
||||
hasLocation: result.includeLocation,
|
||||
});
|
||||
|
||||
return result;
|
||||
} else {
|
||||
logger.debug("[AccountViewView] No profile data found in response:", {
|
||||
requestId,
|
||||
hasData: !!response.data,
|
||||
hasDataData: !!(response.data && response.data.data),
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error: unknown) {
|
||||
// Handle specific HTTP status codes cleanly to suppress console spam
|
||||
if (error && typeof error === "object" && "response" in error) {
|
||||
const axiosError = error as { response?: { status?: number } };
|
||||
|
||||
if (axiosError.response?.status === 404) {
|
||||
logger.info(
|
||||
"[Profile] No profile found - this is normal for new users",
|
||||
{
|
||||
did,
|
||||
server: this.partnerApiServer,
|
||||
status: 404,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (axiosError.response?.status === 400) {
|
||||
logger.warn("[Profile] Bad request - user may not be registered", {
|
||||
did,
|
||||
server: this.partnerApiServer,
|
||||
status: 400,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
axiosError.response?.status === 401 ||
|
||||
axiosError.response?.status === 403
|
||||
) {
|
||||
logger.warn("[Profile] Authentication/authorization issue", {
|
||||
did,
|
||||
server: this.partnerApiServer,
|
||||
status: axiosError.response.status,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Only log full errors for unexpected issues (5xx, network errors, etc.)
|
||||
logger.error("[Profile] Unexpected error loading profile:", {
|
||||
did,
|
||||
server: this.partnerApiServer,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
throw new Error("Failed to load profile");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save user profile to the partner API
|
||||
*/
|
||||
private async saveProfileToServer(
|
||||
did: string,
|
||||
profileData: ProfileData,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const requestId = `profile_save_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
logger.debug("[AccountViewView] Saving profile:", {
|
||||
requestId,
|
||||
did,
|
||||
profileData,
|
||||
});
|
||||
|
||||
// Get authentication headers
|
||||
const headers = await getHeaders(did);
|
||||
|
||||
// Prepare payload in the format expected by the partner API
|
||||
const payload = {
|
||||
description: profileData.description,
|
||||
issuerDid: did,
|
||||
...(profileData.includeLocation &&
|
||||
profileData.latitude &&
|
||||
profileData.longitude
|
||||
? {
|
||||
locLat: profileData.latitude,
|
||||
locLon: profileData.longitude,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
logger.debug("[AccountViewView] Sending payload to server:", {
|
||||
requestId,
|
||||
payload,
|
||||
hasLocation: profileData.includeLocation,
|
||||
});
|
||||
|
||||
const response = await this.axios.post(
|
||||
`${this.partnerApiServer}/api/partner/userProfile`,
|
||||
payload,
|
||||
{ headers },
|
||||
);
|
||||
|
||||
logger.debug("[AccountViewView] Profile saved successfully:", {
|
||||
requestId,
|
||||
status: response.status,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
// Handle specific HTTP status codes cleanly to suppress console spam
|
||||
if (error && typeof error === "object" && "response" in error) {
|
||||
const axiosError = error as { response?: { status?: number } };
|
||||
|
||||
if (axiosError.response?.status === 400) {
|
||||
logger.warn("[Profile] Bad request saving profile", {
|
||||
did,
|
||||
server: this.partnerApiServer,
|
||||
status: 400,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
throw new Error("Invalid profile data");
|
||||
}
|
||||
|
||||
if (
|
||||
axiosError.response?.status === 401 ||
|
||||
axiosError.response?.status === 403
|
||||
) {
|
||||
logger.warn(
|
||||
"[Profile] Authentication/authorization issue saving profile",
|
||||
{
|
||||
did,
|
||||
server: this.partnerApiServer,
|
||||
status: axiosError.response.status,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
throw new Error("Authentication required");
|
||||
}
|
||||
|
||||
if (axiosError.response?.status === 409) {
|
||||
logger.warn("[Profile] Profile conflict - may already exist", {
|
||||
did,
|
||||
server: this.partnerApiServer,
|
||||
status: 409,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
throw new Error("Profile already exists");
|
||||
}
|
||||
}
|
||||
|
||||
// Only log full errors for unexpected issues
|
||||
logger.error("[Profile] Unexpected error saving profile:", {
|
||||
did,
|
||||
server: this.partnerApiServer,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
throw new Error("Failed to save profile");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle profile location visibility
|
||||
*/
|
||||
private toggleProfileLocation(profileData: ProfileData): ProfileData {
|
||||
const includeLocation = !profileData.includeLocation;
|
||||
return {
|
||||
...profileData,
|
||||
latitude: includeLocation ? profileData.latitude : 0,
|
||||
longitude: includeLocation ? profileData.longitude : 0,
|
||||
includeLocation,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear profile location
|
||||
*/
|
||||
private clearProfileLocation(profileData: ProfileData): ProfileData {
|
||||
return {
|
||||
...profileData,
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
includeLocation: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default profile data
|
||||
*/
|
||||
private getDefaultProfile(): ProfileData {
|
||||
return {
|
||||
description: "",
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
includeLocation: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user profile from the partner API
|
||||
*/
|
||||
private async deleteProfileFromServer(did: string): Promise<boolean> {
|
||||
try {
|
||||
const requestId = `profile_delete_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
logger.debug("[AccountViewView] Deleting profile:", {
|
||||
requestId,
|
||||
did,
|
||||
});
|
||||
|
||||
// Get authentication headers
|
||||
const headers = await getHeaders(did);
|
||||
|
||||
const response = await this.axios.delete(
|
||||
`${this.partnerApiServer}/api/partner/userProfile/${did}`,
|
||||
{ headers },
|
||||
);
|
||||
|
||||
logger.debug("[AccountViewView] Profile deleted successfully:", {
|
||||
requestId,
|
||||
status: response.status,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
// Handle specific HTTP status codes cleanly to suppress console spam
|
||||
if (error && typeof error === "object" && "response" in error) {
|
||||
const axiosError = error as { response?: { status?: number } };
|
||||
|
||||
if (axiosError.response?.status === 404) {
|
||||
logger.info(
|
||||
"[Profile] Profile not found for deletion - may already be deleted",
|
||||
{
|
||||
did,
|
||||
server: this.partnerApiServer,
|
||||
status: 404,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
return true; // Consider it successful if already deleted
|
||||
}
|
||||
|
||||
if (
|
||||
axiosError.response?.status === 401 ||
|
||||
axiosError.response?.status === 403
|
||||
) {
|
||||
logger.warn(
|
||||
"[Profile] Authentication/authorization issue deleting profile",
|
||||
{
|
||||
did,
|
||||
server: this.partnerApiServer,
|
||||
status: axiosError.response.status,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Only log full errors for unexpected issues
|
||||
logger.error("[Profile] Unexpected error deleting profile:", {
|
||||
did,
|
||||
server: this.partnerApiServer,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -124,7 +124,7 @@ import {
|
||||
NOTIFY_CONFIRMATION_RESTRICTION,
|
||||
} from "../constants/notifications";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
|
||||
import { GiveSummaryRecord, GiveActionClaim } from "../interfaces";
|
||||
import { AgreeActionClaim } from "../interfaces/claims";
|
||||
import {
|
||||
@@ -223,7 +223,7 @@ export default class ContactAmountssView extends Vue {
|
||||
const contact = await this.$getContact(contactDid);
|
||||
this.contact = contact;
|
||||
|
||||
const settings = await this.$getSettings(MASTER_SETTINGS_KEY);
|
||||
const settings = await this.$getMasterSettings();
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
|
||||
|
||||
@@ -568,10 +568,27 @@ export default class HomeView extends Vue {
|
||||
this.isRegistered = true;
|
||||
}
|
||||
} catch (error) {
|
||||
// Enhanced error logging with server context
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
const axiosError = error as {
|
||||
response?: {
|
||||
data?: { error?: { code?: string; message?: string } };
|
||||
status?: number;
|
||||
};
|
||||
};
|
||||
|
||||
logger.warn(
|
||||
"[HomeView Settings Trace] ⚠️ Registration check failed",
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
error: errorMessage,
|
||||
did: this.activeDid,
|
||||
server: this.apiServer,
|
||||
errorCode: axiosError?.response?.data?.error?.code,
|
||||
errorMessage: axiosError?.response?.data?.error?.message,
|
||||
httpStatus: axiosError?.response?.status,
|
||||
needsUserMigration: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -585,8 +602,7 @@ export default class HomeView extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures API server is correctly set for the current platform
|
||||
* For Electron, always use production endpoint regardless of saved settings
|
||||
* Ensures correct API server configuration
|
||||
*
|
||||
* @internal
|
||||
* Called after loading settings to ensure correct API endpoint
|
||||
@@ -594,12 +610,9 @@ export default class HomeView extends Vue {
|
||||
private async ensureCorrectApiServer() {
|
||||
const { DEFAULT_ENDORSER_API_SERVER } = await import("../constants/app");
|
||||
|
||||
if (process.env.VITE_PLATFORM === "electron") {
|
||||
// **CRITICAL FIX**: Always use production API server for Electron
|
||||
// This prevents the capacitor-electron:// protocol from being used for API calls
|
||||
this.apiServer = DEFAULT_ENDORSER_API_SERVER;
|
||||
} else if (!this.apiServer) {
|
||||
// **FIX**: Set default API server for web/development if not already set
|
||||
// Only set default if no user preference exists
|
||||
if (!this.apiServer) {
|
||||
// Set default API server for any platform if not already set
|
||||
this.apiServer = DEFAULT_ENDORSER_API_SERVER;
|
||||
}
|
||||
}
|
||||
@@ -1161,9 +1174,13 @@ export default class HomeView extends Vue {
|
||||
location: fulfillsPlan
|
||||
? { lat: fulfillsPlan.locLat, lon: fulfillsPlan.locLon }
|
||||
: null,
|
||||
inSearchBox: fulfillsPlan
|
||||
? this.latLongInAnySearchBox(fulfillsPlan.locLat, fulfillsPlan.locLon)
|
||||
: null,
|
||||
inSearchBox:
|
||||
fulfillsPlan?.locLat && fulfillsPlan?.locLon
|
||||
? this.latLongInAnySearchBox(
|
||||
fulfillsPlan.locLat,
|
||||
fulfillsPlan.locLon,
|
||||
)
|
||||
: null,
|
||||
finalResult: anyMatch,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ export default class IdentitySwitcherView extends Vue {
|
||||
if (did) {
|
||||
try {
|
||||
const newSettings = await this.$accountSettings(did);
|
||||
logger.info(
|
||||
logger.debug(
|
||||
"[IdentitySwitcher Settings Trace] ✅ New account settings loaded",
|
||||
{
|
||||
did,
|
||||
@@ -252,7 +252,7 @@ export default class IdentitySwitcherView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
logger.debug(
|
||||
"[IdentitySwitcher Settings Trace] 🔄 Navigating to home to trigger watcher",
|
||||
{
|
||||
newDid: did,
|
||||
|
||||
@@ -110,10 +110,22 @@ export default class NewEditAccountView extends Vue {
|
||||
* @async
|
||||
*/
|
||||
async onClickSaveChanges() {
|
||||
await this.$updateSettings({
|
||||
firstName: this.givenName,
|
||||
lastName: "", // deprecated, pre v 0.1.3
|
||||
});
|
||||
// Get the current active DID to save to user-specific settings
|
||||
const settings = await this.$accountSettings();
|
||||
const activeDid = settings.activeDid;
|
||||
|
||||
if (activeDid) {
|
||||
// Save to user-specific settings for the current identity
|
||||
await this.$saveUserSettings(activeDid, {
|
||||
firstName: this.givenName,
|
||||
});
|
||||
} else {
|
||||
// Fallback to master settings if no active DID
|
||||
await this.$saveSettings({
|
||||
firstName: this.givenName,
|
||||
});
|
||||
}
|
||||
|
||||
this.$router.back();
|
||||
}
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ export default class StartView extends Vue {
|
||||
// Load account count for display logic
|
||||
this.numAccounts = await retrieveAccountCount();
|
||||
|
||||
logger.info("[StartView] Component mounted", {
|
||||
logger.debug("[StartView] Component mounted", {
|
||||
hasGivenName: !!this.givenName,
|
||||
accountCount: this.numAccounts,
|
||||
passkeysEnabled: this.PASSKEYS_ENABLED,
|
||||
@@ -221,7 +221,7 @@ export default class StartView extends Vue {
|
||||
* Routes user to new identifier creation flow with seed-based approach
|
||||
*/
|
||||
public onClickNewSeed() {
|
||||
logger.info("[StartView] User selected new seed generation");
|
||||
logger.debug("[StartView] User selected new seed generation");
|
||||
this.$router.push({ name: "new-identifier" });
|
||||
}
|
||||
|
||||
@@ -235,14 +235,14 @@ export default class StartView extends Vue {
|
||||
const keyName =
|
||||
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : "");
|
||||
|
||||
logger.info("[StartView] Initiating passkey registration", {
|
||||
logger.debug("[StartView] Initiating passkey registration", {
|
||||
keyName,
|
||||
hasGivenName: !!this.givenName,
|
||||
});
|
||||
|
||||
await registerSaveAndActivatePasskey(keyName);
|
||||
|
||||
logger.info("[StartView] Passkey registration successful");
|
||||
logger.debug("[StartView] Passkey registration successful");
|
||||
this.$router.push({ name: "account" });
|
||||
} catch (error) {
|
||||
logger.error("[StartView] Passkey registration failed", error);
|
||||
@@ -255,7 +255,7 @@ export default class StartView extends Vue {
|
||||
* Routes user to account import flow for existing seed phrase
|
||||
*/
|
||||
public onClickNo() {
|
||||
logger.info("[StartView] User selected existing seed import");
|
||||
logger.debug("[StartView] User selected existing seed import");
|
||||
this.$router.push({ name: "import-account" });
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ export default class StartView extends Vue {
|
||||
* Routes user to address derivation flow for existing seed
|
||||
*/
|
||||
public onClickDerive() {
|
||||
logger.info("[StartView] User selected address derivation");
|
||||
logger.debug("[StartView] User selected address derivation");
|
||||
this.$router.push({ name: "import-derive" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,13 +91,95 @@
|
||||
name: 'shared-photo',
|
||||
query: { fileName },
|
||||
}"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
|
||||
data-testId="fileUploadButton"
|
||||
>
|
||||
Go to Shared Page
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- URL Flow Testing Section -->
|
||||
<div class="mt-8">
|
||||
<h2 class="text-xl font-bold mb-4">URL Flow Testing</h2>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Test claim and partner server URL flow from initialization to change
|
||||
propagation.
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 border border-gray-300 rounded-md bg-gray-50">
|
||||
<h3 class="font-semibold mb-2">Current URL State</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div>
|
||||
<strong>API Server:</strong>
|
||||
<span class="font-mono">{{ apiServer || "Not Set" }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Partner API Server:</strong>
|
||||
<span class="font-mono">{{ partnerApiServer || "Not Set" }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Active DID:</strong>
|
||||
<span class="font-mono">{{ activeDid || "Not Set" }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Platform:</strong>
|
||||
<span class="font-mono">{{ getCurrentPlatform() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
:class="primaryButtonClasses"
|
||||
:disabled="isUrlTestRunning"
|
||||
@click="testUrlFlow()"
|
||||
>
|
||||
{{ isUrlTestRunning ? "Testing..." : "Test URL Flow" }}
|
||||
</button>
|
||||
|
||||
<button :class="secondaryButtonClasses" @click="changeApiServer()">
|
||||
Change API Server (Test → Prod)
|
||||
</button>
|
||||
|
||||
<button
|
||||
:class="secondaryButtonClasses"
|
||||
@click="changePartnerApiServer()"
|
||||
>
|
||||
Change Partner API Server (Test → Prod)
|
||||
</button>
|
||||
|
||||
<button :class="warningButtonClasses" @click="resetToDefaults()">
|
||||
Reset to Defaults
|
||||
</button>
|
||||
|
||||
<button :class="secondaryButtonClasses" @click="refreshSettings()">
|
||||
Refresh Settings
|
||||
</button>
|
||||
|
||||
<button
|
||||
:class="secondaryButtonClasses"
|
||||
@click="logEnvironmentState()"
|
||||
>
|
||||
Log Environment State
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border border-gray-300 rounded-md bg-gray-50">
|
||||
<h3 class="font-semibold mb-2">URL Flow Test Results</h3>
|
||||
<div class="max-h-64 overflow-y-auto space-y-2">
|
||||
<div
|
||||
v-for="(result, index) in urlTestResults"
|
||||
:key="index"
|
||||
class="p-2 border border-gray-200 rounded text-xs font-mono bg-white"
|
||||
>
|
||||
{{ result }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<h2 class="text-xl font-bold mb-4">Passkeys</h2>
|
||||
See console for results.
|
||||
@@ -326,6 +408,11 @@ export default class Help extends Vue {
|
||||
showEntityGridTest = false;
|
||||
showPlatformServiceTest = false;
|
||||
|
||||
// for URL flow testing
|
||||
isUrlTestRunning = false;
|
||||
urlTestResults: string[] = [];
|
||||
partnerApiServer: string | undefined;
|
||||
|
||||
/**
|
||||
* Computed properties for template streamlining
|
||||
* Eliminates repeated classes and logic in template
|
||||
@@ -534,24 +621,93 @@ export default class Help extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*
|
||||
* Loads user settings and account information for testing interface
|
||||
* Uses PlatformServiceMixin for database access
|
||||
*/
|
||||
async mounted() {
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.userName = settings.firstName;
|
||||
logger.info(
|
||||
"[TestView] 🚀 Component mounting - starting URL flow tracking",
|
||||
);
|
||||
|
||||
const account = await retrieveAccountMetadata(this.activeDid);
|
||||
if (this.activeDid) {
|
||||
if (account) {
|
||||
this.credIdHex = account.passkeyCredIdHex as string;
|
||||
} else {
|
||||
alert("No account found for DID " + this.activeDid);
|
||||
// Boot-time logging for initial configuration
|
||||
logger.info("[TestView] 🌍 Boot-time configuration detected:", {
|
||||
platform: process.env.VITE_PLATFORM,
|
||||
defaultEndorserApiServer: process.env.VITE_DEFAULT_ENDORSER_API_SERVER,
|
||||
defaultPartnerApiServer: process.env.VITE_DEFAULT_PARTNER_API_SERVER,
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
try {
|
||||
// Track settings loading
|
||||
logger.info("[TestView] 📥 Loading account settings...");
|
||||
const settings = await this.$accountSettings();
|
||||
|
||||
logger.info("[TestView] 📊 Settings loaded:", {
|
||||
activeDid: settings.activeDid,
|
||||
apiServer: settings.apiServer,
|
||||
partnerApiServer: settings.partnerApiServer,
|
||||
isRegistered: settings.isRegistered,
|
||||
firstName: settings.firstName,
|
||||
});
|
||||
|
||||
// Update component state
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.partnerApiServer = settings.partnerApiServer || "";
|
||||
this.userName = settings.firstName;
|
||||
|
||||
logger.info("[TestView] ✅ Component state updated:", {
|
||||
activeDid: this.activeDid,
|
||||
apiServer: this.apiServer,
|
||||
partnerApiServer: this.partnerApiServer,
|
||||
});
|
||||
|
||||
// Load account metadata
|
||||
if (this.activeDid) {
|
||||
logger.info(
|
||||
"[TestView] 🔍 Loading account metadata for DID:",
|
||||
this.activeDid,
|
||||
);
|
||||
const account = await retrieveAccountMetadata(this.activeDid);
|
||||
|
||||
if (account) {
|
||||
this.credIdHex = account.passkeyCredIdHex as string;
|
||||
logger.info("[TestView] ✅ Account metadata loaded:", {
|
||||
did: account.did,
|
||||
hasPasskey: !!account.passkeyCredIdHex,
|
||||
passkeyId: account.passkeyCredIdHex,
|
||||
});
|
||||
} else {
|
||||
logger.warn(
|
||||
"[TestView] ⚠️ No account found for DID:",
|
||||
this.activeDid,
|
||||
);
|
||||
alert("No account found for DID " + this.activeDid);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("[TestView] 🎯 Component initialization complete:", {
|
||||
activeDid: this.activeDid,
|
||||
apiServer: this.apiServer,
|
||||
partnerApiServer: this.partnerApiServer,
|
||||
hasPasskey: !!this.credIdHex,
|
||||
platform: this.getCurrentPlatform(),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[TestView] ❌ Error during component initialization:",
|
||||
error,
|
||||
);
|
||||
this.$notify(
|
||||
{
|
||||
group: "error",
|
||||
type: "error",
|
||||
title: "Initialization Error",
|
||||
text: `Failed to initialize component: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -824,5 +980,276 @@ export default class Help extends Vue {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the URL flow from initialization to change propagation.
|
||||
* This simulates the flow where a user's DID is set, and then the
|
||||
* claim and partner server URLs are updated.
|
||||
*/
|
||||
public async testUrlFlow() {
|
||||
this.isUrlTestRunning = true;
|
||||
this.urlTestResults = [];
|
||||
|
||||
try {
|
||||
logger.info("[TestView] 🔬 Starting comprehensive URL flow test");
|
||||
this.addUrlTestResult("🚀 Starting URL flow test...");
|
||||
|
||||
// Test 1: Current state
|
||||
this.addUrlTestResult(`📊 Current State:`);
|
||||
this.addUrlTestResult(` - API Server: ${this.apiServer || "Not Set"}`);
|
||||
this.addUrlTestResult(
|
||||
` - Partner API Server: ${this.partnerApiServer || "Not Set"}`,
|
||||
);
|
||||
this.addUrlTestResult(` - Active DID: ${this.activeDid || "Not Set"}`);
|
||||
this.addUrlTestResult(` - Platform: ${this.getCurrentPlatform()}`);
|
||||
|
||||
// Test 2: Load fresh settings
|
||||
this.addUrlTestResult(`\n📥 Testing Settings Loading:`);
|
||||
const startTime = Date.now();
|
||||
const settings = await this.$accountSettings();
|
||||
const loadTime = Date.now() - startTime;
|
||||
|
||||
this.addUrlTestResult(` - Settings loaded in ${loadTime}ms`);
|
||||
this.addUrlTestResult(
|
||||
` - API Server from settings: ${settings.apiServer || "Not Set"}`,
|
||||
);
|
||||
this.addUrlTestResult(
|
||||
` - Partner API Server from settings: ${settings.partnerApiServer || "Not Set"}`,
|
||||
);
|
||||
|
||||
// Test 3: Database query
|
||||
this.addUrlTestResult(`\n💾 Testing Database Query:`);
|
||||
const dbStartTime = Date.now();
|
||||
const dbResult = await this.$dbQuery(
|
||||
"SELECT apiServer, partnerApiServer, activeDid FROM settings WHERE id = ? OR accountDid = ?",
|
||||
[1, this.activeDid || ""],
|
||||
);
|
||||
const dbTime = Date.now() - dbStartTime;
|
||||
|
||||
if (dbResult?.values) {
|
||||
this.addUrlTestResult(` - Database query completed in ${dbTime}ms`);
|
||||
this.addUrlTestResult(
|
||||
` - Raw DB values: ${JSON.stringify(dbResult.values)}`,
|
||||
);
|
||||
} else {
|
||||
this.addUrlTestResult(
|
||||
` - Database query failed or returned no results`,
|
||||
);
|
||||
}
|
||||
|
||||
// Test 4: Environment variables
|
||||
this.addUrlTestResult(`\n🌍 Testing Environment Variables:`);
|
||||
this.addUrlTestResult(
|
||||
` - VITE_PLATFORM: ${import.meta.env.VITE_PLATFORM || "Not Set"}`,
|
||||
);
|
||||
this.addUrlTestResult(
|
||||
` - VITE_DEFAULT_ENDORSER_API_SERVER: ${import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER || "Not Set"}`,
|
||||
);
|
||||
this.addUrlTestResult(
|
||||
` - VITE_DEFAULT_PARTNER_API_SERVER: ${import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER || "Not Set"}`,
|
||||
);
|
||||
|
||||
// Test 5: Constants
|
||||
this.addUrlTestResult(`\n📋 Testing App Constants:`);
|
||||
this.addUrlTestResult(
|
||||
` - PROD_ENDORSER_API_SERVER: ${AppString.PROD_ENDORSER_API_SERVER}`,
|
||||
);
|
||||
this.addUrlTestResult(
|
||||
` - PROD_PARTNER_API_SERVER: ${AppString.PROD_PARTNER_API_SERVER}`,
|
||||
);
|
||||
|
||||
// Test 6: Change detection
|
||||
this.addUrlTestResult(`\n🔄 Testing Change Detection:`);
|
||||
const originalApiServer = this.apiServer;
|
||||
const originalPartnerServer = this.partnerApiServer;
|
||||
|
||||
// Simulate a change
|
||||
this.addUrlTestResult(` - Original API Server: ${originalApiServer}`);
|
||||
this.addUrlTestResult(
|
||||
` - Original Partner Server: ${originalPartnerServer}`,
|
||||
);
|
||||
|
||||
// Test 7: Settings update
|
||||
this.addUrlTestResult(`\n💾 Testing Settings Update:`);
|
||||
const testChanges = {
|
||||
apiServer:
|
||||
originalApiServer === "https://api.endorser.ch"
|
||||
? "https://test-api.endorser.ch"
|
||||
: "https://api.endorser.ch",
|
||||
};
|
||||
|
||||
this.addUrlTestResult(
|
||||
` - Attempting to change API Server to: ${testChanges.apiServer}`,
|
||||
);
|
||||
const updateResult = await this.$saveSettings(testChanges);
|
||||
this.addUrlTestResult(
|
||||
` - Update result: ${updateResult ? "Success" : "Failed"}`,
|
||||
);
|
||||
|
||||
// Test 8: Verify change propagation
|
||||
this.addUrlTestResult(`\n✅ Testing Change Propagation:`);
|
||||
const newSettings = await this.$accountSettings();
|
||||
this.addUrlTestResult(
|
||||
` - New API Server from settings: ${newSettings.apiServer || "Not Set"}`,
|
||||
);
|
||||
this.addUrlTestResult(
|
||||
` - Component state API Server: ${this.apiServer || "Not Set"}`,
|
||||
);
|
||||
this.addUrlTestResult(
|
||||
` - Change propagated: ${newSettings.apiServer === this.apiServer ? "Yes" : "No"}`,
|
||||
);
|
||||
|
||||
// Test 9: Revert changes
|
||||
this.addUrlTestResult(`\n🔄 Reverting Changes:`);
|
||||
const revertResult = await this.$saveSettings({
|
||||
apiServer: originalApiServer,
|
||||
});
|
||||
this.addUrlTestResult(
|
||||
` - Revert result: ${revertResult ? "Success" : "Failed"}`,
|
||||
);
|
||||
|
||||
// Test 10: Final verification
|
||||
this.addUrlTestResult(`\n🎯 Final Verification:`);
|
||||
const finalSettings = await this.$accountSettings();
|
||||
this.addUrlTestResult(
|
||||
` - Final API Server: ${finalSettings.apiServer || "Not Set"}`,
|
||||
);
|
||||
this.addUrlTestResult(
|
||||
` - Matches original: ${finalSettings.apiServer === originalApiServer ? "Yes" : "No"}`,
|
||||
);
|
||||
|
||||
this.addUrlTestResult(`\n✅ URL flow test completed successfully!`);
|
||||
logger.info("[TestView] ✅ URL flow test completed successfully");
|
||||
} catch (error) {
|
||||
const errorMsg = `❌ URL flow test failed: ${error instanceof Error ? error.message : String(error)}`;
|
||||
this.addUrlTestResult(errorMsg);
|
||||
logger.error("[TestView] ❌ URL flow test failed:", error);
|
||||
} finally {
|
||||
this.isUrlTestRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a result to the URL test results array.
|
||||
*/
|
||||
private addUrlTestResult(message: string) {
|
||||
this.urlTestResults.push(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the API server to the production URL.
|
||||
*/
|
||||
public changeApiServer() {
|
||||
const currentServer = this.apiServer;
|
||||
const newServer =
|
||||
currentServer === "https://api.endorser.ch"
|
||||
? "https://test-api.endorser.ch"
|
||||
: "https://api.endorser.ch";
|
||||
|
||||
logger.info("[TestView] 🔄 Changing API server:", {
|
||||
from: currentServer,
|
||||
to: newServer,
|
||||
});
|
||||
|
||||
this.apiServer = newServer;
|
||||
this.addUrlTestResult(
|
||||
`API Server changed from ${currentServer} to ${newServer}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the partner API server to the production URL.
|
||||
*/
|
||||
public changePartnerApiServer() {
|
||||
const currentServer = this.partnerApiServer;
|
||||
const newServer =
|
||||
currentServer === "https://partner-api.endorser.ch"
|
||||
? "https://test-partner-api.endorser.ch"
|
||||
: "https://partner-api.endorser.ch";
|
||||
|
||||
logger.info("[TestView] 🔄 Changing partner API server:", {
|
||||
from: currentServer,
|
||||
to: newServer,
|
||||
});
|
||||
|
||||
this.partnerApiServer = newServer;
|
||||
this.addUrlTestResult(
|
||||
`Partner API Server changed from ${currentServer} to ${newServer}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all URL-related settings to their initial values.
|
||||
*/
|
||||
public resetToDefaults() {
|
||||
this.apiServer = AppString.TEST_ENDORSER_API_SERVER;
|
||||
this.partnerApiServer = AppString.TEST_PARTNER_API_SERVER;
|
||||
this.activeDid = "";
|
||||
this.addUrlTestResult("URL Flow Test Results Reset to Defaults.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes settings from the database to verify changes.
|
||||
*/
|
||||
public async refreshSettings() {
|
||||
try {
|
||||
logger.info("[TestView] 🔄 Refreshing settings from database");
|
||||
const settings = await this.$accountSettings();
|
||||
|
||||
// Update component state
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.partnerApiServer = settings.partnerApiServer || "";
|
||||
|
||||
logger.info("[TestView] ✅ Settings refreshed:", {
|
||||
apiServer: this.apiServer,
|
||||
partnerApiServer: this.partnerApiServer,
|
||||
});
|
||||
|
||||
this.addUrlTestResult(
|
||||
`Settings refreshed - API Server: ${this.apiServer}, Partner API Server: ${this.partnerApiServer}`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("[TestView] ❌ Error refreshing settings:", error);
|
||||
this.addUrlTestResult(
|
||||
`Error refreshing settings: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the current environment state to the console.
|
||||
*/
|
||||
public logEnvironmentState() {
|
||||
logger.info("[TestView] 🌐 Current Environment State:", {
|
||||
VITE_PLATFORM: import.meta.env.VITE_PLATFORM,
|
||||
VITE_DEFAULT_ENDORSER_API_SERVER: import.meta.env
|
||||
.VITE_DEFAULT_ENDORSER_API_SERVER,
|
||||
VITE_DEFAULT_PARTNER_API_SERVER: import.meta.env
|
||||
.VITE_DEFAULT_PARTNER_API_SERVER,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
activeDid: this.activeDid,
|
||||
apiServer: this.apiServer,
|
||||
partnerApiServer: this.partnerApiServer,
|
||||
});
|
||||
this.$notify({
|
||||
group: "info",
|
||||
type: "info",
|
||||
title: "Environment State Logged",
|
||||
text: "Current environment state logged to console.",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current platform based on the API server.
|
||||
*/
|
||||
public getCurrentPlatform(): string {
|
||||
if (this.apiServer?.includes(AppString.PROD_ENDORSER_API_SERVER)) {
|
||||
return "Production";
|
||||
} else if (this.apiServer?.includes(AppString.TEST_ENDORSER_API_SERVER)) {
|
||||
return "Test";
|
||||
} else {
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user