import { defineStore } from 'pinia' import { computed, ref } from 'vue' import { apiClient } from '@/lib/axios' import type { AdminUser, ImpersonationStartResponse, ImpersonationStatusResponse, StartImpersonationPayload } from '@/types/admin' const SESSION_STORAGE_KEY = 'crewli_impersonation' const BROADCAST_CHANNEL_NAME = 'crewli_impersonation_sync' interface ImpersonationState { sessionId: string adminId: string targetUserId: string impersonatedUser: AdminUser expiresAt: string } export const useImpersonationStore = defineStore('impersonation', () => { const stored = sessionStorage.getItem(SESSION_STORAGE_KEY) const state = ref(stored ? (JSON.parse(stored) as ImpersonationState) : null) let broadcastChannel: BroadcastChannel | null = null const isImpersonating = computed(() => !!state.value) const originalAdminId = computed(() => state.value?.adminId ?? null) const impersonatedUser = computed(() => state.value?.impersonatedUser ?? null) const sessionId = computed(() => state.value?.sessionId ?? null) const targetUserId = computed(() => state.value?.targetUserId ?? null) const expiresAt = computed(() => state.value?.expiresAt ? new Date(state.value.expiresAt) : null) function persistState(): void { if (state.value) sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(state.value)) else sessionStorage.removeItem(SESSION_STORAGE_KEY) } async function start( userId: string, payload: StartImpersonationPayload, ): Promise { const { data } = await apiClient.post<{ data: ImpersonationStartResponse }>( `/admin/impersonate/${userId}`, payload, ) const result = data.data const session = result.session state.value = { sessionId: session.id, adminId: session.admin_id, targetUserId: session.target_user_id, impersonatedUser: result.user, expiresAt: session.expires_at, } persistState() broadcastChange('started') // Reload to apply impersonated context window.location.href = '/' return result } async function stop(): Promise { try { // Call stop WITHOUT the X-Impersonate-User header // The interceptor won't add it because we clear state first const currentState = state.value state.value = null persistState() if (currentState) await apiClient.post('/admin/stop-impersonation') } catch { // Even if API call fails, state is already cleared } broadcastChange('stopped') // Full reload to restore admin session window.location.href = '/platform' } function clearState(): void { state.value = null persistState() } async function checkStatus(): Promise { try { const { data } = await apiClient.get<{ data: ImpersonationStatusResponse }>( '/admin/impersonate/status', ) if (!data.data.active && state.value) { clearState() window.location.href = '/platform' } else if (data.data.session) { // Update expiry from server if (state.value) { state.value.expiresAt = data.data.session.expires_at persistState() } } } catch { // If status check fails, don't clear — might be a network issue } } function restoreFromStorage(): void { const storedSnapshot = sessionStorage.getItem(SESSION_STORAGE_KEY) if (storedSnapshot) { try { state.value = JSON.parse(storedSnapshot) as ImpersonationState } catch { sessionStorage.removeItem(SESSION_STORAGE_KEY) state.value = null } } } function listenForBroadcasts(): void { if (broadcastChannel) return try { broadcastChannel = new BroadcastChannel(BROADCAST_CHANNEL_NAME) broadcastChannel.onmessage = (event: MessageEvent<{ type: string }>) => { if (event.data.type === 'stopped') { state.value = null persistState() window.location.href = '/platform' } else if (event.data.type === 'started') { restoreFromStorage() } } } catch { // BroadcastChannel not supported — no cross-tab sync } } function broadcastChange(type: string): void { try { broadcastChannel?.postMessage({ type }) } catch { // Ignore broadcast errors } } return { isImpersonating, originalAdminId, impersonatedUser, sessionId, targetUserId, expiresAt, start, stop, clearState, checkStatus, restoreFromStorage, listenForBroadcasts, } })