import apiService from './api.service' import { store } from '@/store' import { applicationConfigurationUrl } from './abpConfig.service' import { MigrateLogEntry, MigrateLogLevel, SetupStatusDto } from '@/proxy/setup/models' /** * Sunucunun veritabanı/kurulum durumu. * * Hata durumu her çağıran tarafından ayrıca ele alınır (kurulum moduna geçiş kararı, * durum kartı), bu yüzden global hata diyaloğu bastırılır. */ export const getSetupStatus = () => apiService.fetchData({ method: 'GET', url: '/api/setup/application-status', silentError: true, }) const getMigrateUrl = (): string => { const base = import.meta.env.VITE_API_URL ?? '' return `${base}/api/setup/migrate` } /** * Sunucu yeniden başlatıldıktan sonra tam ABP stack ile ayağa kalkana kadar * application-configuration endpoint'ini poll eder. * * Arka arkaya iki başarılı yanıt beklenir; tek başarılı yanıt sunucu henüz * tüm modülleri yüklemeden de dönebilir. * * @returns Poll'u iptal eden fonksiyon. */ export const pollUntilServerReady = ( onReady: () => void, onAttempt?: (attempt: number) => void, ): (() => void) => { const REQUIRED_CONSECUTIVE_SUCCESS = 2 const INITIAL_DELAY_MS = 3000 const SUCCESS_RETRY_MS = 1000 const FAILURE_RETRY_MS = 2000 let attempt = 0 let successCount = 0 let timerId: ReturnType | null = null let cancelled = false const tick = async () => { if (cancelled) return attempt++ onAttempt?.(attempt) try { const res = await fetch( `${import.meta.env.VITE_API_URL ?? ''}${applicationConfigurationUrl(false)}`, { method: 'GET', headers: { Accept: 'application/json' }, cache: 'no-store' }, ) if (res.status === 200) { const json = await res.json() if (json && typeof json.currentUser === 'object') { successCount++ if (successCount >= REQUIRED_CONSECUTIVE_SUCCESS) { onReady() return } timerId = setTimeout(tick, SUCCESS_RETRY_MS) return } } } catch { /* sunucu henüz ayakta değil */ } successCount = 0 timerId = setTimeout(tick, FAILURE_RETRY_MS) } // Sunucunun kapanması için kısa bir bekleme. timerId = setTimeout(tick, INITIAL_DELAY_MS) return () => { cancelled = true if (timerId) clearTimeout(timerId) } } export interface MigrateStreamOptions { /** * Kurulum parolası. Veritabanı yokken (sunucu kurulum modundayken) zorunludur; * canlı sistemde bunun yerine oturum token'ı ile yetkilendirme yapılır. */ setupPassword?: string signal?: AbortSignal /** Her log satırı için çağrılır. */ onLog?: (entry: MigrateLogEntry) => void /** Migration başarıyla tamamlandığında. */ onSuccess?: () => void /** Sunucu yeniden başlıyor — istemci hazır olana kadar poll etmeli. */ onRestart?: () => void /** Akış hata ile bitti. */ onError?: (error: Error) => void /** Başarılı veya başarısız — akış sona erdi. */ onFinished?: () => void } /** * /api/setup/migrate SSE akışının tek istemcisi. * * Hem /setup sayfası hem SQL Query Manager hem de Wizard dosya yöneticisi bunu kullanır; * SSE ayrıştırma mantığı yalnızca burada bulunur. */ export const streamSetupMigrate = async (options: MigrateStreamOptions = {}): Promise => { const { setupPassword, signal, onLog, onSuccess, onRestart, onError, onFinished } = options // Kurulum parolası kullanılıyorsa oturum token'ı gönderilmez. Sunucu zaten parola // yolunu seçtiğinde token'a bakmaz; token göndermek yalnızca veritabanına bağımlı // ara katmanların (oturum doğrulama, dinamik claim'ler) veritabanı düşükken // devreye girip isteği bloke etmesine yol açar. const token = setupPassword ? null : store.getState().auth.session.token let failed = false const fail = (error: Error) => { if (failed) return failed = true onError?.(error) } try { const response = await fetch(getMigrateUrl(), { method: 'POST', headers: { Accept: 'text/event-stream', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(setupPassword ? { 'X-Setup-Password': setupPassword } : {}), }, signal, }) if (!response.ok || !response.body) { // Sunucu reddetme nedenini gövdede düz metin olarak döner (yanlış parola, // eksik yapılandırma, eksik izin). Bunu yutmak hatayı teşhis edilemez yapıyordu. const serverReason = await response.text().catch(() => '') fail( new Error( serverReason.trim() || (response.status === 403 ? 'Not authorized to run database migration.' : `Server responded with status ${response.status}`), ), ) return } const reader = response.body.getReader() const decoder = new TextDecoder() let buffer = '' let sawSuccess = false const handleEntry = (entry: MigrateLogEntry) => { const level = (entry.level ?? 'info') as MigrateLogLevel if (level === 'done') { // Sunucu "done" gönderdi ama öncesinde success/restart gelmediyse akış başarısızdır. if (!sawSuccess) fail(new Error(entry.message || 'Migration failed.')) return } onLog?.(entry) if (level === 'success') { sawSuccess = true onSuccess?.() } else if (level === 'restart') { sawSuccess = true onRestart?.() } else if (level === 'error') { fail(new Error(entry.message || 'Migration failed.')) } } const flushLines = (chunk: string) => { buffer += chunk const lines = buffer.split('\n') buffer = lines.pop() ?? '' for (const line of lines) { const trimmed = line.trim() if (!trimmed.startsWith('data:')) continue const raw = trimmed.slice(5).trim() if (!raw) continue try { handleEntry(JSON.parse(raw) as MigrateLogEntry) } catch { onLog?.({ level: 'info', message: raw }) } } } while (true) { const { done, value } = await reader.read() if (done) break flushLines(decoder.decode(value, { stream: true })) } } catch (err) { if ((err as Error)?.name !== 'AbortError') { fail(err instanceof Error ? err : new Error(String(err))) } } finally { onFinished?.() } }