import { defineStore } from 'pinia'; import { computed, ref } from 'vue'; import type { AuthUserDto, CreateOrganizationRequest, LoginChallengeResponse, LoginOrganizationChoiceResponse, LoginResponse, LoginResult, OrganizationMembershipDto, RegisterChallengeResponse, RegisterOrganizationRequest, UpdateProfileRequest, } from '@mento/shared'; import { isLoginOrganizationChoice } from '@mento/shared'; import { api } from '@/lib/api'; const TOKEN_KEY = 'mento_token'; const USER_KEY = 'mento_user'; const LEGACY_KEYS = ['ment_token', 'ment_user'] as const; const DEFAULT_AVATAR = '/brand/user-avatar.png'; function readStoredToken(): string | null { return localStorage.getItem(TOKEN_KEY) ?? localStorage.getItem('ment_token'); } function readStoredUser(): AuthUserDto | null { const raw = localStorage.getItem(USER_KEY) ?? localStorage.getItem('ment_user'); if (!raw) return null; try { return JSON.parse(raw) as AuthUserDto; } catch { return null; } } function clearLegacy() { for (const key of LEGACY_KEYS) localStorage.removeItem(key); } export const useAuthStore = defineStore('auth', () => { const token = ref(readStoredToken()); const user = ref(readStoredUser()); const avatarObjectUrl = ref(null); const isAuthenticated = computed(() => Boolean(token.value)); const avatarSrc = computed(() => avatarObjectUrl.value || DEFAULT_AVATAR); function clearAvatarUrl() { if (avatarObjectUrl.value) { URL.revokeObjectURL(avatarObjectUrl.value); avatarObjectUrl.value = null; } } async function refreshAvatar() { if (!token.value || !user.value?.hasAvatar) { clearAvatarUrl(); return; } try { const { data } = await api.get('/auth/me/avatar', { responseType: 'blob', }); if (!data || data.type.includes('json') || data.size === 0) { clearAvatarUrl(); return; } const next = URL.createObjectURL(data); clearAvatarUrl(); avatarObjectUrl.value = next; } catch { clearAvatarUrl(); } } function persist(payload: LoginResponse) { token.value = payload.accessToken; user.value = payload.user; localStorage.setItem(TOKEN_KEY, payload.accessToken); localStorage.setItem(USER_KEY, JSON.stringify(payload.user)); clearLegacy(); void refreshAvatar(); } async function login(identifier: string, password: string) { const { data } = await api.post('/auth/login', { identifier, password, }); return applyLoginResult(data); } async function requestLoginOtp(mobile: string) { const { data } = await api.post('/auth/login/otp', { mobile, }); return data; } async function verifyLoginOtp(challengeId: string, code: string) { const { data } = await api.post('/auth/login/otp/verify', { challengeId, code, }); return applyLoginResult(data); } function applyLoginResult(data: LoginResult): LoginOrganizationChoiceResponse | void { if (isLoginOrganizationChoice(data)) return data; persist(data); } async function selectOrganization(selectionToken: string, organizationId: string) { const { data } = await api.post('/auth/select-organization', { selectionToken, organizationId, }); persist(data); } async function switchOrganization(organizationId: string) { const { data } = await api.post('/auth/switch-organization', { organizationId, }); persist(data); } async function listOrganizations() { const { data } = await api.get('/auth/organizations'); return data; } async function createOrganization(payload: CreateOrganizationRequest) { const { data } = await api.post('/organizations', payload); persist(data); } async function startRegister(payload: RegisterOrganizationRequest) { const { data } = await api.post( '/auth/register', payload, ); return data; } async function verifyRegister( challengeId: string, mobileCode: string, emailCode?: string, ) { const { data } = await api.post('/auth/register/verify', { challengeId, mobileCode, ...(emailCode ? { emailCode } : {}), }); persist(data); } async function logout() { try { if (token.value) await api.post('/auth/logout'); } catch { /* نشست ممکن است از قبل بسته شده باشد */ } token.value = null; user.value = null; clearAvatarUrl(); localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(USER_KEY); clearLegacy(); } function setUser(next: AuthUserDto) { user.value = next; localStorage.setItem(USER_KEY, JSON.stringify(next)); void refreshAvatar(); } async function hydrate() { if (!token.value) return; try { const { data } = await api.get('/auth/me'); setUser(data); } catch { /* interceptor handles 401 */ } } async function updateProfile(payload: UpdateProfileRequest) { const { data } = await api.patch('/auth/me', payload); setUser(data); } async function uploadAvatar(file: File) { const body = new FormData(); body.append('file', file); const { data } = await api.post('/auth/me/avatar', body); setUser(data); } async function removeAvatar() { const { data } = await api.delete('/auth/me/avatar'); setUser(data); } /** مشاهده فقط‌خواندنی به‌جای کاربر، یا بازگشت با targetUserId = خود مالک */ async function viewAs(targetUserId: string) { const { data } = await api.post('/auth/view-as', { targetUserId, }); persist(data); } if (token.value) void hydrate(); if (typeof document !== 'undefined') { document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') void hydrate(); }); } return { token, user, isAuthenticated, avatarSrc, login, requestLoginOtp, verifyLoginOtp, selectOrganization, switchOrganization, listOrganizations, createOrganization, startRegister, verifyRegister, logout, setUser, hydrate, updateProfile, uploadAvatar, removeAvatar, viewAs, }; });