2026-04-23 10:36:51 +00:00
|
|
|
|
import apiService from './api.service'
|
2026-04-28 17:12:14 +00:00
|
|
|
|
import { store } from '@/store'
|
2026-08-09 08:52:28 +00:00
|
|
|
|
import { applicationConfigurationUrl } from './abpConfig.service'
|
|
|
|
|
|
import { MigrateLogEntry, MigrateLogLevel, SetupStatusDto } from '@/proxy/setup/models'
|
2026-04-23 10:36:51 +00:00
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 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.
|
|
|
|
|
|
*/
|
2026-04-23 10:36:51 +00:00
|
|
|
|
export const getSetupStatus = () =>
|
|
|
|
|
|
apiService.fetchData<SetupStatusDto>({
|
|
|
|
|
|
method: 'GET',
|
2026-05-24 15:48:55 +00:00
|
|
|
|
url: '/api/setup/application-status',
|
2026-08-09 08:52:28 +00:00
|
|
|
|
silentError: true,
|
2026-04-23 10:36:51 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
const getMigrateUrl = (): string => {
|
2026-04-23 10:36:51 +00:00
|
|
|
|
const base = import.meta.env.VITE_API_URL ?? ''
|
|
|
|
|
|
return `${base}/api/setup/migrate`
|
|
|
|
|
|
}
|
2026-04-28 14:53:58 +00:00
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 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<typeof setTimeout> | 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<void> => {
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 17:12:14 +00:00
|
|
|
|
try {
|
2026-08-09 08:52:28 +00:00
|
|
|
|
const response = await fetch(getMigrateUrl(), {
|
2026-04-28 17:12:14 +00:00
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
Accept: 'text/event-stream',
|
2026-08-09 08:52:28 +00:00
|
|
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
|
|
|
|
...(setupPassword ? { 'X-Setup-Password': setupPassword } : {}),
|
2026-04-28 17:12:14 +00:00
|
|
|
|
},
|
2026-08-09 08:52:28 +00:00
|
|
|
|
signal,
|
2026-04-28 17:12:14 +00:00
|
|
|
|
})
|
2026-08-09 08:52:28 +00:00
|
|
|
|
|
|
|
|
|
|
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}`),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
2026-04-28 17:12:14 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-09 08:52:28 +00:00
|
|
|
|
|
|
|
|
|
|
const reader = response.body.getReader()
|
2026-04-28 17:12:14 +00:00
|
|
|
|
const decoder = new TextDecoder()
|
|
|
|
|
|
let buffer = ''
|
2026-08-09 08:52:28 +00:00
|
|
|
|
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
|
2026-04-28 17:12:14 +00:00
|
|
|
|
const lines = buffer.split('\n')
|
|
|
|
|
|
buffer = lines.pop() ?? ''
|
2026-08-09 08:52:28 +00:00
|
|
|
|
|
2026-04-28 17:12:14 +00:00
|
|
|
|
for (const line of lines) {
|
2026-08-09 08:52:28 +00:00
|
|
|
|
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 })
|
2026-04-28 17:12:14 +00:00
|
|
|
|
}
|
2026-04-28 14:53:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-09 08:52:28 +00:00
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
|
const { done, value } = await reader.read()
|
|
|
|
|
|
if (done) break
|
|
|
|
|
|
flushLines(decoder.decode(value, { stream: true }))
|
|
|
|
|
|
}
|
2026-04-28 17:12:14 +00:00
|
|
|
|
} catch (err) {
|
2026-08-09 08:52:28 +00:00
|
|
|
|
if ((err as Error)?.name !== 'AbortError') {
|
|
|
|
|
|
fail(err instanceof Error ? err : new Error(String(err)))
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
onFinished?.()
|
2026-04-28 14:53:58 +00:00
|
|
|
|
}
|
2026-04-28 17:12:14 +00:00
|
|
|
|
}
|