File size: 18,627 Bytes
e25342c
fa0cf00
2994b77
fa0cf00
27c1d5a
fa0cf00
aae0ad3
5e88a7b
2994b77
fa0cf00
3b07699
fa0cf00
2994b77
fa0cf00
6b70c13
5d6f8d0
2994b77
fa0cf00
2994b77
 
 
 
 
6b67f7f
 
d728620
 
2994b77
 
 
 
fa0cf00
70a379d
357d9e8
04c5a2e
fa0cf00
2994b77
875d513
2994b77
10916e3
2994b77
dba1755
 
2994b77
d728620
 
2994b77
c988445
2994b77
 
70a379d
c988445
2994b77
 
 
f1023a7
e24dad0
8239c6c
 
 
e24dad0
2994b77
 
 
382194c
2994b77
 
 
 
994a59d
2994b77
 
 
 
ee4bb81
2994b77
 
 
 
eb47989
2994b77
 
 
 
 
 
 
 
 
bce6c68
fa0cf00
 
2994b77
 
 
70a379d
2994b77
fa0cf00
2994b77
 
 
70a379d
2994b77
fa0cf00
f519751
 
 
 
 
 
 
fa0cf00
6b70c13
 
f519751
6b70c13
f519751
 
 
 
 
ae5532c
 
 
 
 
6b70c13
 
 
2994b77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa0cf00
8239c6c
 
 
 
 
 
 
 
 
 
283fe35
8239c6c
 
70a379d
5da8fef
fa0cf00
 
 
5e88a7b
fa0cf00
875d513
 
 
 
 
5e88a7b
875d513
10916e3
c7e9d08
2994b77
759de70
6ec32f4
759de70
6ec32f4
2994b77
 
c7e9d08
e8547b3
 
c988445
 
2994b77
c988445
2994b77
 
 
c988445
 
 
f1023a7
2968524
2994b77
2968524
 
382194c
797f38a
2994b77
797f38a
 
2994b77
797f38a
 
382194c
2994b77
1c22f36
60cc0d2
382194c
1ab8b1e
 
909810e
 
 
 
 
63d2bb7
 
431e5f6
994a59d
431e5f6
994a59d
 
 
 
1ab8b1e
 
 
 
 
63d2bb7
 
 
 
e14f098
 
1ab8b1e
 
 
 
e14f098
63d2bb7
1ab8b1e
 
 
 
aae0ad3
1ab8b1e
 
aae0ad3
1ab8b1e
 
 
 
 
 
 
 
 
 
 
 
 
 
63d2bb7
1ab8b1e
 
 
 
 
63d2bb7
1ab8b1e
 
 
63d2bb7
1ab8b1e
 
 
 
 
 
 
63d2bb7
1ab8b1e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63d2bb7
811855e
d728620
 
 
49dcb09
 
 
d728620
 
 
 
 
 
0fc729f
 
 
49dcb09
0fc729f
 
 
 
 
 
 
 
49dcb09
0fc729f
 
 
49dcb09
0fc729f
 
d728620
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d6f8d0
ee4bb81
2994b77
ee4bb81
 
2994b77
 
ee4bb81
 
 
 
 
eb47989
2994b77
eb47989
 
2994b77
 
eb47989
 
 
 
3b07699
bce6c68
2994b77
bce6c68
2994b77
 
 
8d003a6
 
 
 
 
 
2994b77
bce6c68
2994b77
 
 
bce6c68
c3b1a33
5e88a7b
2994b77
c3b1a33
8d003a6
 
 
 
 
2994b77
8d003a6
2994b77
8d003a6
2994b77
 
8d003a6
2994b77
 
 
8d003a6
bce6c68
5e88a7b
24eba9a
2994b77
 
fa0cf00
2994b77
 
 
 
 
51cdc66
2994b77
 
 
f7ef511
357d9e8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8439a1c
94a4549
 
 
 
 
 
 
 
 
 
 
357d9e8
 
04c5a2e
 
e81950c
 
 
04c5a2e
6022cd0
 
 
 
 
04c5a2e
6022cd0
04c5a2e
 
6022cd0
 
 
 
 
 
04c5a2e
 
 
 
 
8264987
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
# main.py
import os
import time
import uuid
import httpx
import psutil
import ffmpeg
import asyncio
import platform
from datetime import datetime
from typing import List, Optional
from contextlib import asynccontextmanager
from cachetools import TTLCache
from pydantic import BaseModel
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, Response, StreamingResponse
from fastapi import FastAPI, Query, HTTPException, Body, BackgroundTasks, Header

from app.xs_dl import XoSoApp
from app.cl_lq import ClLqApp
from app.ht_rd import HentaiRandomApp
from app.ht_noti import HentaiApp
from app.ios_app import IOSAppFetcher
from app.tik_dl import TiktokDl
from app.tik_cdn import TiktokCdn
from app.yt_dl import YouTubeDl
from app.yt_cdn import YouTubeCdn
from app.byps_uv import BypassUV
from app.gs_code import GSCodeApp
from app.gm_crate import GmailLogic
from app.gs_daily import GSDailyApp
from app.invite_logic import DiscordInviteLogic
from app.noi_tu import NoiTuApp
from app.panel_svc import PanelManager, PanelActionRequest
from app.panel_login import PanelLogin

xs_app = XoSoApp()
lq_app = ClLqApp()
gm_app = GmailLogic()
gs_app = GSDailyApp()
bypass_app = BypassUV()
tik_app = TiktokDl()
tik_direct = TiktokCdn()
hentai_app = HentaiApp()
yt_app = YouTubeDl()
yt_cdn = YouTubeCdn()
ios_app = IOSAppFetcher()
gs_code_app = GSCodeApp()
invite_app = DiscordInviteLogic()
ht_random_app = HentaiRandomApp()
noitu_app = NoiTuApp()

API_KEY = os.getenv("API_KEY")
START_TIME = time.time()
order_cache = TTLCache(maxsize=100, ttl=86400)

class NoiTuRequest(BaseModel):
    before: str
    answer: str
    list: List[str]

class ProductItem(BaseModel):
    name: str
    price: str

class DailyRequest(BaseModel):
    cookie: str
    discord_id: str
    server: str

class RedeemRequest(BaseModel):
    cookie: str
    server: str
    code: str

class CompleteRequest(BaseModel):
    order_id: str
    tk: str
    mk: str

class GmailRequest(BaseModel):
    to_email: str
    subject: str
    customer_name: str
    status: str
    total_price: str
    products: List[ProductItem]
    order_id: Optional[str] = None
    from_name: Optional[str] = "Celeste Store"

@asynccontextmanager
async def lifespan(app: FastAPI):
    await asyncio.gather(
        invite_app.start(), lq_app.start(), gs_app.start(),
        gs_code_app.start(), hentai_app.start(), ht_random_app.start(),
        bypass_app.start(), ios_app.start(), xs_app.start(), tik_direct.start(), yt_cdn.start(), noitu_app.start()
    )
    yield
    await asyncio.gather(
        invite_app.stop(), lq_app.stop(), gs_app.stop(),
        gs_code_app.stop(), hentai_app.stop(), ht_random_app.stop(),
        bypass_app.stop(), ios_app.stop(), xs_app.stop(), tik_direct.stop(), yt_cdn.stop(), noitu_app.stop()
    )

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

app = FastAPI(
    lifespan=lifespan,
    docs_url="/api-docs",
    redoc_url=None
)

app.mount("/static", StaticFiles(directory="docs"), name="static")

@app.get("/", include_in_schema=False)
async def home():
    index_path = os.path.join(BASE_DIR, "docs", "api", "index.html")
    
    if os.path.exists(index_path):
        return FileResponse(index_path)
    
    return {"error": f"File not found at: {index_path}"}

@app.get("/docs/api", include_in_schema=False)
async def home_alias():
    return await home()

@app.get("/api/v1/system/status")
def system_status():
    memory = psutil.virtual_memory()
    cpu_percent = psutil.cpu_percent(interval=0.5)
    uptime_seconds = int(time.time() - START_TIME)
    uptime_string = str(datetime.utcfromtimestamp(uptime_seconds).strftime("%H:%M:%S"))
    
    return {
        "status": "online",
        "process_id": os.getpid(),
        "hostname": platform.node(),
        "uptime_hms": uptime_string,
        "platform": platform.system(),
        "uptime_seconds": uptime_seconds,
        "ram_usage_percent": memory.percent,
        "cpu_usage_percent": cpu_percent,
        "platform_release": platform.release(),
        "python_version": platform.python_version(),
        "used_ram_mb": round(memory.used / 1024 / 1024, 2),
        "total_ram_mb": round(memory.total / 1024 / 1024, 2)
    }

@app.post("/api/v1/noitu/check")
async def api_noitu_check(
    data: NoiTuRequest, 
    apikey: Optional[str] = Query(None), 
    x_api_key: Optional[str] = Header(None, alias="X-API-Key")
):
    # Kiểm tra API KEY
    key = x_api_key or apikey
    if not API_KEY or key != API_KEY:
        raise HTTPException(status_code=403, detail="API key invalid. Go touch grass")
    
    # Trả về kết quả check từ trọng tài
    return noitu_app.validate_turn(data.before, data.answer, data.list)

@app.get("/api/v1/invites/discord")
async def get_new_invite():
    res = await invite_app.get_invite()
    if "error" in res:
        raise HTTPException(status_code=500, detail="500: Yo mama called, said stop hacking or she'll whoop ur ass. Listen to her for once.")
    return res

@app.get("/api/v1/get/lqAov")
async def get_free_account():
    res = await lq_app.fetch_acc()
    if not res["ok"]:
        raise HTTPException(status_code=500, detail="500: Yo mama called, said stop hacking or she'll whoop ur ass. Listen to her for once.")
    return res

@app.post("/api/v1/genshin/daily")
async def genshin_daily(data: DailyRequest, x_api_key: str = Header(None)):
    if not API_KEY:
        raise HTTPException(status_code=500, detail="500: Yo mama called, said stop hacking or she'll whoop ur ass. Listen to her for once.")
    if x_api_key != API_KEY:
        raise HTTPException(status_code=403, detail="API key invalid. Go touch grass")
    
    res = await gs_app.run_daily_and_capture(data.cookie, data.discord_id, data.server)
    if not res["ok"]:
        raise HTTPException(status_code=400, detail=res["error"])
    return res

@app.post("/api/v1/genshin/redeem")
async def genshin_redeem(data: RedeemRequest, x_api_key: str = Header(None)):
    if x_api_key != API_KEY:
        raise HTTPException(status_code=403, detail="API key invalid. Go touch grass")
    
    res = await gs_code_app.redeem_code(data.cookie, data.server, data.code)
    if not res["ok"]:
        raise HTTPException(status_code=400, detail=res)
    return res

@app.get("/api/v1/hentai/newest")
async def hentai_newest(apikey: Optional[str] = Query(None), x_api_key: Optional[str] = Header(None, alias="X-API-Key")):
    key_to_check = x_api_key or apikey
    return await hentai_app.get_new(key_to_check, API_KEY)

@app.get("/api/v1/hentai/info")
async def hentai_info(slug: str = Query(...), apikey: Optional[str] = Query(None), x_api_key: Optional[str] = Header(None, alias="X-API-Key")):
    key = x_api_key or apikey
    if not API_KEY or key != API_KEY:
        raise HTTPException(status_code=403, detail="API key invalid. Go touch grass")
    return await hentai_app.fetch_info(slug)

@app.get("/api/v1/hentai/random")
async def hentai_random(apikey: Optional[str] = Query(None), limit: str = Query("1"), x_api_key: Optional[str] = Header(None, alias="X-API-Key")):
    key = x_api_key or apikey
    return await ht_random_app.get_random(key, API_KEY, limit)

@app.get("/api/v1/tiktok/dl")
async def tiktok_dl(
    url: str = Query(...), 
    hd: int = Query(1), 
    apikey: Optional[str] = Query(None), 
    x_api_key: Optional[str] = Header(None, alias="X-API-Key")
):
    key = x_api_key or apikey
    if not API_KEY or key != API_KEY:
        raise HTTPException(status_code=403, detail="API key invalid")
    
    res = await tik_app.fetch_video(url, tik_direct, hd)
    if not res["ok"]:
        raise HTTPException(status_code=400, detail=res["error"])
    return res

@app.get("/api/v1/tiktok/cdn")
async def tiktok_cdn(
    token: str = Query(None, description="Token generated from /dl endpoint"),
    url: str = Query(None, description="Direct URL to proxy (Requires API Key)"),
    type: str = Query("mp4", description="File extension type"),
    apikey: Optional[str] = Query(None),
    x_api_key: Optional[str] = Header(None, alias="X-API-Key")
):
    if url:
        key = x_api_key or apikey
        if not API_KEY or key != API_KEY:
            raise HTTPException(status_code=403, detail="Proxying via URL requires a valid API Key")
        data = {"url": url, "type": type}
    
    elif token:
        data = tik_direct.consume_token(token)
        if not data:
            raise HTTPException(status_code=404, detail="Token invalid, expired or limit reached")
    
    else:
        raise HTTPException(status_code=400, detail="Either 'token' or 'url' must be provided")

    target_url = data["url"]
    media_type_req = data["type"]

    if media_type_req in ["mp3_convert", "wav_convert"]:
        is_wav = media_type_req == "wav_convert"
        ext = "wav" if is_wav else "mp3"
        acodec = "pcm_s16le" if is_wav else "libmp3lame"
        fmt = "wav" if is_wav else "mp3"
        media_header = "audio/wav" if is_wav else "audio/mpeg"
        
        try:
            process = (
                ffmpeg
                .input(target_url)
                .output('pipe:', format=fmt, acodec=acodec, ar='44100')
                .run_async(pipe_stdout=True, pipe_stderr=True)
            )

            async def stream_ffmpeg():
                try:
                    while True:
                        chunk = await asyncio.to_thread(process.stdout.read, 8192)
                        if not chunk: break
                        yield chunk
                finally:
                    try: process.kill()
                    except: pass

            return StreamingResponse(
                stream_ffmpeg(),
                media_type=media_header,
                headers={"Content-Disposition": f"attachment; filename=Celeskry_{int(time.time())}.{ext}"}
            )
        except Exception as e:
            raise HTTPException(status_code=500, detail=f"Conversion Error: {str(e)}")

    async def stream_file():
        async with httpx.AsyncClient(follow_redirects=True, timeout=60.0) as client:
            async with client.stream("GET", target_url) as r:
                if r.status_code != 200:
                    yield b"Error: Resource not found"
                    return
                async for chunk in r.aiter_bytes(chunk_size=1024*1024):
                    yield chunk
                    
    content_types = {
        "mp4": "video/mp4",
        "mp3": "audio/mpeg",
        "jpg": "image/jpeg",
        "png": "image/png"
    }
    final_type = content_types.get(media_type_req, "application/octet-stream")
    
    return StreamingResponse(
        stream_file(),
        media_type=final_type,
        headers={"Content-Disposition": f"attachment; filename=Celeste_{int(time.time())}.{media_type_req}"}
    )

    raise HTTPException(status_code=400, detail="Provide either 'url' or 'token'")

@app.get("/api/v1/youtube/dl")
async def youtube_dl(
    url: str = Query(...), 
    format: str = Query("1080"), 
    apikey: Optional[str] = Query(None), 
    x_api_key: Optional[str] = Header(None, alias="X-API-Key")
):
    key = x_api_key or apikey
    if not API_KEY or key != API_KEY:
        raise HTTPException(status_code=403, detail="API key invalid")
    
    job_id = await yt_app.create_job(url, format, yt_cdn)
    return {
        "ok": True,
        "message": "Job created.",
        "job_id": job_id,
        "status_url": f"/api/v1/youtube/status?job_id={job_id}"
    }

@app.get("/api/v1/youtube/status")
async def youtube_status(job_id: str = Query(...)):
    job = yt_app.jobs.get(job_id)
    if not job:
        raise HTTPException(status_code=404, detail="Job not found or expired")
    
    return {
        "ok": True,
        "meta": yt_app.meta_info,
        "data": job
    }

@app.get("/api/v1/youtube/cdn")
async def youtube_cdn_proxy(
    token: str = Query(...),
):
    data = yt_cdn.consume_token(token)
    if not data:
        raise HTTPException(status_code=404, detail="Token invalid or expired")

    async def stream_file():
        async with httpx.AsyncClient(follow_redirects=True, timeout=120.0) as client:
            async with client.stream("GET", data["url"]) as r:
                async for chunk in r.aiter_bytes(chunk_size=1024*1024):
                    yield chunk

    media_types = {
        "mp4": "video/mp4",
        "mp3": "audio/mpeg",
        "wav": "audio/wav",
        "jpg": "image/jpeg"
    }
    
    return StreamingResponse(
        stream_file(),
        media_type=media_types.get(data["type"], "application/octet-stream"),
        headers={"Content-Disposition": f"attachment; filename=Celeskry_YT_{int(time.time())}.{data['type']}"}
    )

@app.get("/api/v1/bypass")
async def bypass_link(url: str = Query(...), apikey: Optional[str] = Query(None), x_api_key: Optional[str] = Header(None, alias="X-API-Key")):
    key = x_api_key or apikey
    if not API_KEY or key != API_KEY:
        raise HTTPException(status_code=403, detail="API key invalid. Go touch grass")
    
    res = await bypass_app.bypass(url)
    if not res["ok"]:
        raise HTTPException(status_code=400, detail=res["error"])
    return res

@app.get("/api/v1/ios/app")
async def ios_app_fetch(n: str = Query(...), apikey: Optional[str] = Query(None), x_api_key: Optional[str] = Header(None, alias="X-API-Key")):
    key = x_api_key or apikey
    if not API_KEY or key != API_KEY:
        raise HTTPException(status_code=403, detail="API key invalid. Go touch grass")
    
    res = await ios_app.get_app_accounts(n)
    if not res["ok"]:
        raise HTTPException(status_code=404, detail=res["error"])
    return res

@app.post("/api/v1/gmail/create")
async def create_gmail_notif(data: GmailRequest, x_api_key: str = Header(None)):
    if x_api_key != API_KEY:
        raise HTTPException(status_code=403, detail="API key invalid. Go touch grass")
    
    final_order_id = data.order_id or gm_app.generate_random_order_id()
    order_cache[final_order_id] = {
        "email": data.to_email,
        "name": data.customer_name,
        "product": data.products[0].name if data.products else "Sản phẩm",
        "price": data.total_price
    }
    
    res = await gm_app.send_order_email(
        to_email=data.to_email, subject=data.subject, customer_name=data.customer_name,
        order_id=final_order_id, status=data.status, products=[p.dict() for p in data.products],
        total_price=data.total_price, from_name=data.from_name
    )
    if not res["ok"]:
        raise HTTPException(status_code=500, detail="500: Yo mama called, said stop hacking or she'll whoop ur ass. Listen to her for once.")
    
    res["order_id"] = final_order_id
    return res

@app.post("/api/v1/gmail/done")
async def complete_order(data: CompleteRequest, x_api_key: str = Header(None)):
    if x_api_key != API_KEY:
        raise HTTPException(status_code=403, detail="API key invalid. Go touch grass")
    
    cached_order = order_cache.get(data.order_id)
    if not cached_order:
        raise HTTPException(status_code=404, detail="Order not found or expired")
    
    res = await gm_app.send_complete_email(
        to_email=cached_order["email"], customer_name=cached_order["name"],
        order_id=data.order_id, product_name=cached_order["product"],
        price=cached_order["price"], tk=data.tk, mk=data.mk
    )
    if not res["ok"]:
        raise HTTPException(status_code=500, detail="500: Yo mama called, said stop hacking or she'll whoop ur ass. Listen to her for once.")
    
    del order_cache[data.order_id]
    return {"ok": True, "message": f"Sent the account to {cached_order['email']}"}

@app.get("/api/v1/xoso/xsmn")
async def get_xsmn(type: str = Query("latest"), date: Optional[str] = Query(None), apikey: Optional[str] = Query(None), x_api_key: Optional[str] = Header(None, alias="X-API-Key")):
    key = x_api_key or apikey
    if not API_KEY or key != API_KEY:
        raise HTTPException(status_code=403, detail="API key invalid. Go touch grass")
    
    res = await xs_app.fetch_mn(date if type == "date" else None)
    if not res["ok"]:
        raise HTTPException(status_code=400, detail=res["error"])
    return res

panel_app = PanelManager(
    ws_url="wss://stratos.pikamc.vn:8080/api/servers/45feca4d-3f79-4bcc-ae11-114335a1c77e/ws",
    origin="https://cp.pikamc.vn"
)

@app.post("/api/v1/panel/token")
async def update_token(token: str = Body(..., embed=True), x_api_key: str = Header(None)):
    if x_api_key != API_KEY: raise HTTPException(status_code=403)
    panel_app.update_token(token)
    return {"ok": True, "message": "Token updated successfully"}

@app.post("/api/v1/panel/control")
async def control_panel(data: PanelActionRequest, x_api_key: str = Header(None)):
    if x_api_key != API_KEY: raise HTTPException(status_code=403)
    
    res = await panel_app.execute(command=data.command, action=data.action)
    
    if not res["ok"] and ("token" in res["error"].lower() or "auth" in res["error"].lower()):
        print("[!] Token error, automatically refreshing...")
        
        login_svc = PanelLogin(PANEL_USER, PANEL_PASS, "45feca4d-3f79-4bcc-ae11-114335a1c77e")
        refresh_res = await login_svc.fetch_ws_credentials()
        
        if refresh_res["ok"]:
            panel_app.update_token(refresh_res["token"])
            res = await panel_app.execute(command=data.command, action=data.action)
            
    if not res["ok"]:
        raise HTTPException(status_code=500, detail=res["error"])
    return res

PANEL_USER = os.getenv("PANEL_USER")
PANEL_PASS = os.getenv("PANEL_PASS") 

@app.get("/api/v1/panel/refresh")
async def refresh_panel_token(apikey: str = Query(None), x_api_key: str = Header(None, alias="X-API-Key")):
    # Kiểm tra API Key (Sửa lại logic so sánh cho chắc chắn)
    key = x_api_key or apikey
    if not API_KEY or key != API_KEY:
        raise HTTPException(status_code=403, detail="API key invalid. Check your params.")
    
    login_svc = PanelLogin(PANEL_USER, PANEL_PASS, "45feca4d-3f79-4bcc-ae11-114335a1c77e")
    result = await login_svc.fetch_ws_credentials()
    
    if not result["ok"]:
        # Trả về lỗi chi tiết từ Playwright để debug
        return {"ok": False, "error": result["error"], "debug_img": "/api/v1/panel/debug-img"}
    
    panel_app.update_token(result["token"])
    return {"ok": True, "token_preview": result["token"][:20] + "..."}

@app.get("/api/v1/panel/debug-img")
async def get_debug_img():
    if os.path.exists("debug_screenshot.png"):
        return FileResponse("debug_screenshot.png")
    return {"error": "No debug image found"}