/** * 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 { try { const headers = await getHeaders(activeDid); const response = await this.axios.get( `${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 { 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 { 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) && (error as any).config) { const config = (error as any).config as { url?: string }; return config.url; } return undefined; } /** * 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); }