Spaces:
Sleeping
Sleeping
| import { txasupabase } from '../../../lib/txasupabase.js'; | |
| // Decryption Helper for Browser Token | |
| function decryptToken(token, key) { | |
| if (!token || !token.startsWith('tk_')) return null; | |
| try { | |
| const rawToken = token.substring(3); | |
| let str = rawToken.replace(/-/g, '+').replace(/_/g, '/'); | |
| while (str.length % 4) str += '='; | |
| // Decode base64 to binary string | |
| const text = Buffer.from(str, 'base64').toString('binary'); | |
| let utf8Text = ''; | |
| for (let i = 0; i < text.length; i++) { | |
| const charCode = text.charCodeAt(i); | |
| const keyChar = key.charCodeAt(i % key.length); | |
| const decryptedChar = charCode ^ keyChar; | |
| utf8Text += String.fromCharCode(decryptedChar); | |
| } | |
| return decodeURIComponent(escape(utf8Text)); | |
| } catch (e) { | |
| console.error('[Telemetry] Decryption failed:', e); | |
| return null; | |
| } | |
| } | |
| // User-Agent Parser for Device & Browser | |
| function parseUserAgent(ua) { | |
| let device = 'Thiết bị khác'; | |
| let browser = 'Trình duyệt khác'; | |
| if (!ua) return { device, browser }; | |
| // OS / Device detection | |
| if (ua.includes('Windows NT 10.0')) { | |
| device = 'Windows 10/11'; | |
| } else if (ua.includes('Windows NT 6.1')) { | |
| device = 'Windows 7'; | |
| } else if (ua.includes('Windows NT 6.2') || ua.includes('Windows NT 6.3')) { | |
| device = 'Windows 8/8.1'; | |
| } else if (ua.includes('Android')) { | |
| const androidMatch = ua.match(/Android\s+([^;)]+)/); | |
| const modelMatch = ua.match(/Android\s+[^;]+;\s+([^;)]+)/); | |
| const deviceName = modelMatch ? modelMatch[1].trim() : (androidMatch ? androidMatch[0] : 'Android Device'); | |
| device = `Android (${deviceName})`; | |
| } else if (ua.includes('iPhone')) { | |
| const iphoneMatch = ua.match(/iPhone;\s+CPU\s+iPhone\s+OS\s+([^;)]+)/); | |
| const osVer = iphoneMatch ? iphoneMatch[1].replace(/_/g, '.') : ''; | |
| device = osVer ? `iPhone (iOS ${osVer})` : 'iPhone'; | |
| } else if (ua.includes('iPad')) { | |
| device = 'iPad'; | |
| } else if (ua.includes('Macintosh') || ua.includes('Mac OS X')) { | |
| device = 'macOS'; | |
| } else if (ua.includes('Linux')) { | |
| device = 'Linux'; | |
| } | |
| // Browser detection | |
| if (ua.includes('Edg/')) { | |
| browser = 'Microsoft Edge'; | |
| } else if (ua.includes('Chrome/') && !ua.includes('Chromium')) { | |
| browser = 'Google Chrome'; | |
| } else if (ua.includes('Safari/') && !ua.includes('Chrome')) { | |
| browser = 'Safari'; | |
| } else if (ua.includes('Firefox/')) { | |
| browser = 'Mozilla Firefox'; | |
| } else if (ua.includes('OPR/') || ua.includes('Opera/')) { | |
| browser = 'Opera'; | |
| } else if (ua.includes('Zalo/')) { | |
| browser = 'Zalo Browser'; | |
| } | |
| return { device, browser }; | |
| } | |
| export const POST = async ({ request }) => { | |
| try { | |
| const body = await request.json(); | |
| const { movie_slug, movie_name, episode_name, error_type, error_message, error_details, browser_token } = body; | |
| if (!error_type) { | |
| return new Response(JSON.stringify({ error: 'Error type is required!' }), { | |
| status: 400, | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| } | |
| const userAgent = request.headers.get('user-agent') || ''; | |
| const fallbackIp = request.headers.get('cf-connecting-ip') || | |
| request.headers.get('x-real-ip') || | |
| request.headers.get('x-forwarded-for')?.split(',')[0].trim() || | |
| '127.0.0.1'; | |
| // Decrypt and parse browser, IP, location & device details | |
| let decryptedInfo = null; | |
| let parsedDevice = 'Không rõ'; | |
| let parsedBrowser = 'Không rõ'; | |
| let parsedIp = fallbackIp; | |
| let parsedLocation = null; | |
| if (browser_token) { | |
| if (browser_token.startsWith('tk_')) { | |
| decryptedInfo = decryptToken(browser_token, 'phimtxadinhvaikk'); | |
| if (decryptedInfo) { | |
| const parts = decryptedInfo.split(' - '); | |
| const ua = parts[0] || ''; | |
| // Get IP from token | |
| const ipPart = parts.find(p => p.startsWith('IP: ')); | |
| if (ipPart) { | |
| parsedIp = ipPart.replace('IP: ', ''); | |
| } | |
| // Get Location from token | |
| const locParts = parts.filter(p => !p.startsWith('IP: ') && p !== ua); | |
| if (locParts.length > 0) { | |
| parsedLocation = locParts.join(', '); | |
| } | |
| // Parse UA from token | |
| const parsed = parseUserAgent(ua); | |
| parsedDevice = parsed.device; | |
| parsedBrowser = parsed.browser; | |
| } else { | |
| // Decryption failed but we still have request UA | |
| const parsed = parseUserAgent(userAgent); | |
| parsedDevice = parsed.device; | |
| parsedBrowser = parsed.browser; | |
| } | |
| } else if (browser_token.startsWith('txanot-')) { | |
| // Fallback token format: txanot-[ip]-[uaHash] | |
| const parts = browser_token.split('-'); | |
| if (parts[1]) { | |
| parsedIp = parts[1]; | |
| } | |
| const parsed = parseUserAgent(userAgent); | |
| parsedDevice = parsed.device; | |
| parsedBrowser = parsed.browser; | |
| } | |
| } else { | |
| // No token provided, fallback to request headers | |
| const parsed = parseUserAgent(userAgent); | |
| parsedDevice = parsed.device; | |
| parsedBrowser = parsed.browser; | |
| } | |
| const enrichedDetails = { | |
| ...(error_details || {}), | |
| parsed_device: parsedDevice, | |
| parsed_browser: parsedBrowser, | |
| parsed_ip: parsedIp, | |
| parsed_location: parsedLocation, | |
| decrypted_raw: decryptedInfo | |
| }; | |
| // Insert into txa_player_errors | |
| const { data, error } = await txasupabase.client | |
| .from('txa_player_errors') | |
| .insert({ | |
| movie_slug: movie_slug || null, | |
| movie_name: movie_name || null, | |
| episode_name: episode_name || null, | |
| error_type, | |
| error_message: error_message || null, | |
| error_details: enrichedDetails, | |
| user_agent: userAgent, | |
| ip: parsedIp, | |
| browser_token: browser_token || null | |
| }) | |
| .select() | |
| .single(); | |
| if (error) { | |
| console.error('Error inserting player error into Supabase:', error); | |
| return new Response(JSON.stringify({ error: error.message }), { | |
| status: 500, | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| } | |
| // Write a unified system log featuring decrypted info | |
| const locationStr = parsedLocation ? ` (${parsedLocation})` : ''; | |
| const logMessage = `Lỗi Player (${error_type}) trên phim "${movie_name || 'Không rõ'}" (${episode_name || 'Không rõ'}): ${error_message || ''}. Thiết bị: ${parsedDevice} | Trình duyệt: ${parsedBrowser} | IP: ${parsedIp}${locationStr}`; | |
| try { | |
| if (typeof txasupabase.createLog === 'function') { | |
| await txasupabase.createLog( | |
| 'player_error', | |
| 'error', | |
| logMessage, | |
| { movie_slug, movie_name, episode_name, error_type, error_message, ip: parsedIp, browser_token, parsed_device: parsedDevice, parsed_browser: parsedBrowser } | |
| ); | |
| } else { | |
| await txasupabase.client.from('logs').insert({ | |
| action: 'player_error', | |
| level: 'error', | |
| message: logMessage, | |
| details: JSON.stringify({ movie_slug, movie_name, episode_name, error_type, error_message, ip: parsedIp, browser_token, parsed_device: parsedDevice, parsed_browser: parsedBrowser }) | |
| }); | |
| } | |
| } catch (logErr) { | |
| console.error('Failed to write to logs table:', logErr); | |
| } | |
| return new Response(JSON.stringify({ | |
| success: true, | |
| message: 'Player error reported successfully.', | |
| data | |
| }), { | |
| status: 200, | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| } catch (err) { | |
| console.error('Error in report-error API:', err); | |
| return new Response(JSON.stringify({ error: 'Internal system error!' }), { | |
| status: 500, | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| } | |
| }; | |