Refactor authentication and streaming cookie handling: implement secure cookie logic based on environment in AuthEndpoints and StreamingEndpoints, enhance rate limiting policy in Program.cs, and update logging configuration in appsettings.json. Fix validation behavior to use asynchronous validation methods and improve error handling in frontend components.
build / backend (push) Successful in 1m52s
build / frontend (push) Successful in 1m0s
tests / backend-tests (push) Successful in 2m27s

This commit is contained in:
Leonid Pershin
2026-07-25 21:18:31 +03:00
parent 53e0eeb776
commit 058cbc6994
15 changed files with 282 additions and 99 deletions
+9 -2
View File
@@ -44,11 +44,18 @@ export function AirPage() {
.then(() => {
if (!cancelled) setWatchReady(true)
})
.catch(() => undefined)
.catch(() => {
// Выдача stream-cookie не удалась (403/500/сеть) — показываем offline-панель с кнопкой ретрая,
// а не бесконечный скелетон. Ретрай (attempt) заново дёрнет watchChannel.
if (!cancelled) {
setWatchReady(true)
setPlayerError(true)
}
})
return () => {
cancelled = true
}
}, [selected])
}, [selected, attempt])
const { data: epg } = useQuery({
queryKey: ['air', 'epg', selected],
@@ -97,24 +97,40 @@ export function ChannelPlayer({
})
}
// Слушатели нативной ветки — держим ссылки, чтобы снять их в cleanup (симметрично hls.destroy()).
const onNativeError = () => onUnavailable?.()
if (Hls.isSupported()) {
hls = new Hls({ liveSyncDurationCount: 3, enableWorker: true, lowLatencyMode: false })
hlsRef.current = hls
hls.loadSource(src)
hls.attachMedia(video)
hls.on(Hls.Events.MANIFEST_PARSED, startPlayback)
// Живой эфир: краткий сетевой сбой сегмента или media-ошибку сперва пробуем восстановить
// (hls.js рекомендует startLoad / recoverMediaError), и только исчерпав попытки — уходим в offline.
let recoverAttempts = 0
hls.on(Hls.Events.ERROR, (_event, data) => {
if (data.fatal) {
hls?.destroy()
hls = null
hlsRef.current = null
onUnavailable?.()
if (!data.fatal) return
if (data.type === Hls.ErrorTypes.NETWORK_ERROR && recoverAttempts < 3) {
recoverAttempts += 1
hls?.startLoad()
return
}
if (data.type === Hls.ErrorTypes.MEDIA_ERROR && recoverAttempts < 3) {
recoverAttempts += 1
hls?.recoverMediaError()
return
}
hls?.destroy()
hls = null
hlsRef.current = null
onUnavailable?.()
})
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = src
video.addEventListener('loadedmetadata', startPlayback)
video.addEventListener('error', () => onUnavailable?.())
video.addEventListener('error', onNativeError)
} else {
onUnavailable?.()
}
@@ -122,6 +138,8 @@ export function ChannelPlayer({
return () => {
hls?.destroy()
hlsRef.current = null
video.removeEventListener('loadedmetadata', startPlayback)
video.removeEventListener('error', onNativeError)
}
}, [slug, onUnavailable])
+9 -4
View File
@@ -30,7 +30,8 @@ export async function refreshAccessToken(): Promise<boolean> {
try {
const response = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' })
if (!response.ok) return false
const data = (await response.json()) as { accessToken: string }
const data = (await response.json()) as { accessToken?: unknown }
if (typeof data?.accessToken !== 'string') return false
setAccessToken(data.accessToken)
return true
} catch {
@@ -77,9 +78,13 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
})
if (response.status === 401 && !options.skipRefresh) {
const refreshed = await refreshAccessToken()
if (refreshed) return apiRequest<T>(path, { ...options, skipRefresh: true })
if (response.status === 401) {
if (!options.skipRefresh) {
const refreshed = await refreshAccessToken()
if (refreshed) return apiRequest<T>(path, { ...options, skipRefresh: true })
}
// 401 и освежить токен нельзя/не помогло (включая повторный 401 уже после успешного refresh —
// токен приняли, но прав нет / он тут же отозван): сессия мертва, чистим авторизацию.
onUnauthorized?.()
throw await parseError(response)
}