Phoe2004 commited on
Commit
ca66794
ยท
verified ยท
1 Parent(s): 97008cc

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +93 -28
  2. index.html +8 -2
app.py CHANGED
@@ -347,23 +347,66 @@ def save_payments_db(db):
347
 
348
  def hp(p): return hashlib.sha256(p.encode()).hexdigest()
349
 
350
- # โ”€โ”€ BRUTE-FORCE PROTECTION โ€” login rate limit โ”€โ”€
351
- _login_attempts = {} # ip โ†’ [timestamp, ...]
352
- _login_lock = threading.Lock()
353
- LOGIN_MAX_TRIES = 10 # max attempts per window
354
- LOGIN_WINDOW = 300 # 5-minute rolling window
355
-
356
- def _check_login_rate(ip):
357
- """Return True if allowed, False if rate-limited."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  now = time.time()
359
- with _login_lock:
360
- times = _login_attempts.get(ip, [])
361
- times = [t for t in times if now - t < LOGIN_WINDOW]
362
- if len(times) >= LOGIN_MAX_TRIES:
363
- _login_attempts[ip] = times
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  return False
365
  times.append(now)
366
- _login_attempts[ip] = times
367
  return True
368
 
369
  ADJ = ['Red','Blue','Gold','Star','Sky','Fire','Moon','Cool','Ice','Dark','Neon','Wild']
@@ -518,7 +561,7 @@ GEMINI_MODELS_TRANSCRIPT = [
518
  'gemini-2.5-flash', # primary
519
  'gemini-3-flash', # fallback
520
  ]
521
- # โ”€โ”€ Caption: 2.5-flash-lite primary โ†’ 3.1-flash-lite fallback (lucky spinner)
522
  GEMINI_MODELS_CAPTION = [
523
  'gemini-2.5-flash-lite-preview-06-17', # primary
524
  'gemini-3.1-flash-lite', # fallback
@@ -529,15 +572,14 @@ _mdl_cap_idx = 0
529
  _mdl_lock = threading.Lock()
530
 
531
  def next_model(purpose='transcript'):
532
- """Lucky spinner โ€” random API key, model order fixed: 2.5-flash first, 3-flash fallback."""
533
  models = GEMINI_MODELS_TRANSCRIPT if purpose == 'transcript' else GEMINI_MODELS_CAPTION
534
  return list(models)
535
 
536
  def call_api(msgs, api='Gemini', purpose='transcript'):
537
  """
538
- purpose='transcript' โ†’ 2.5-flash primary, 3-flash fallback
539
- purpose='caption' โ†’ 2.5-flash-lite primary, 3.1-flash-lite fallback
540
- API keys: random shuffle (lucky spinner) every call.
541
  """
542
  if api == 'DeepSeek':
543
  keys, base = DEEPSEEK_KEYS, 'https://api.deepseek.com'
@@ -547,8 +589,7 @@ def call_api(msgs, api='Gemini', purpose='transcript'):
547
  models = next_model(purpose) # spinner: returns rotation-ordered list
548
  valid = [(i+1, k) for i, k in enumerate(keys) if k]
549
  if not valid: raise Exception('No API Key available')
550
- # Lucky spinner โ€” random key order every call
551
- random.shuffle(valid)
552
  last_err = None
553
  for n, k in valid:
554
  for mdl in models:
@@ -1010,20 +1051,44 @@ def sitemap():
1010
  @app.route('/api/login', methods=['POST'])
1011
  def api_login():
1012
  try:
1013
- d = request.get_json(force=True) or {}
1014
- ok, msg, coins = login_user(d.get('username',''), d.get('password',''))
1015
- return jsonify(ok=ok, msg=msg, coins=coins, is_admin=(d.get('username','')==ADMIN_U and ok))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1016
  except Exception as e:
1017
  return jsonify(ok=False, msg=str(e))
1018
 
1019
  @app.route('/api/register', methods=['POST'])
1020
  def api_register():
1021
  try:
1022
- d = request.get_json(force=True) or {}
 
 
 
 
 
1023
  uname = (d.get('username') or '').strip() or gen_uname()
1024
- pw = d.get('password', '')
1025
- db = load_db()
1026
- if uname in db['users']: return jsonify(ok=False, msg='โŒ Already exists')
 
1027
  db['users'][uname] = {'password': hp(pw) if pw else '', 'coins': 1,
1028
  'created_at': datetime.now().isoformat(), 'last_login': None,
1029
  'total_transcripts': 0, 'total_videos': 0,
 
347
 
348
  def hp(p): return hashlib.sha256(p.encode()).hexdigest()
349
 
350
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
351
+ # SECURITY: Brute-force & rate-limit protection
352
+ # (1) Wrong login โ†’ fail counter per IP โ†’ lock 5 min after 5 fails
353
+ # (2) Register โ†’ max 1 account per IP per 24 hours
354
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
355
+ import threading as _thr
356
+
357
+ _pw_fails = {} # ip -> {'count':int, 'locked_until':float}
358
+ _pw_lock = _thr.Lock()
359
+ PW_MAX_FAILS = 5
360
+ PW_LOCK_SECS = 300 # 5 minutes
361
+
362
+ _reg_log = {} # ip -> [ts, ...]
363
+ _reg_lock = _thr.Lock()
364
+ REG_WINDOW = 86400 # 24 hours
365
+ REG_MAX = 1 # accounts per IP per day
366
+
367
+ def _ip(req):
368
+ return (req.headers.get('X-Forwarded-For') or req.remote_addr or '').split(',')[0].strip()
369
+
370
+ def _pw_check(ip):
371
+ """(allowed:bool, wait_secs:int)"""
372
  now = time.time()
373
+ with _pw_lock:
374
+ rec = _pw_fails.get(ip, {})
375
+ lu = rec.get('locked_until', 0)
376
+ if lu > now:
377
+ return False, int(lu - now)
378
+ return True, 0
379
+
380
+ def _pw_fail(ip):
381
+ """Record one failed attempt; lock if threshold reached."""
382
+ now = time.time()
383
+ with _pw_lock:
384
+ rec = _pw_fails.get(ip, {'count': 0, 'locked_until': 0})
385
+ if rec.get('locked_until', 0) < now: # not currently locked
386
+ rec['count'] = rec.get('count', 0) + 1
387
+ if rec['count'] >= PW_MAX_FAILS:
388
+ rec['locked_until'] = now + PW_LOCK_SECS
389
+ rec['count'] = 0
390
+ _pw_fails[ip] = rec
391
+
392
+ def _pw_remaining(ip):
393
+ with _pw_lock:
394
+ return max(0, PW_MAX_FAILS - _pw_fails.get(ip, {}).get('count', 0))
395
+
396
+ def _pw_clear(ip):
397
+ with _pw_lock:
398
+ _pw_fails.pop(ip, None)
399
+
400
+ def _reg_check(ip):
401
+ """True = allowed to register."""
402
+ now = time.time()
403
+ with _reg_lock:
404
+ times = [t for t in _reg_log.get(ip, []) if now - t < REG_WINDOW]
405
+ if len(times) >= REG_MAX:
406
+ _reg_log[ip] = times
407
  return False
408
  times.append(now)
409
+ _reg_log[ip] = times
410
  return True
411
 
412
  ADJ = ['Red','Blue','Gold','Star','Sky','Fire','Moon','Cool','Ice','Dark','Neon','Wild']
 
561
  'gemini-2.5-flash', # primary
562
  'gemini-3-flash', # fallback
563
  ]
564
+ # โ”€โ”€ Caption: 2.5-flash-lite primary โ†’ 3.1-flash-lite fallback
565
  GEMINI_MODELS_CAPTION = [
566
  'gemini-2.5-flash-lite-preview-06-17', # primary
567
  'gemini-3.1-flash-lite', # fallback
 
572
  _mdl_lock = threading.Lock()
573
 
574
  def next_model(purpose='transcript'):
575
+ """Lucky spinner โ€” 2.5-flash first, 3-flash fallback."""
576
  models = GEMINI_MODELS_TRANSCRIPT if purpose == 'transcript' else GEMINI_MODELS_CAPTION
577
  return list(models)
578
 
579
  def call_api(msgs, api='Gemini', purpose='transcript'):
580
  """
581
+ purpose='transcript' โ†’ GEMINI_MODELS_TRANSCRIPT round-robin (gemini-3-flash โ†” gemini-2.5-flash)
582
+ purpose='caption' โ†’ GEMINI_MODELS_CAPTION round-robin (gemini-3.1-flash-lite โ†” gemini-2.5-flash-lite)
 
583
  """
584
  if api == 'DeepSeek':
585
  keys, base = DEEPSEEK_KEYS, 'https://api.deepseek.com'
 
589
  models = next_model(purpose) # spinner: returns rotation-ordered list
590
  valid = [(i+1, k) for i, k in enumerate(keys) if k]
591
  if not valid: raise Exception('No API Key available')
592
+ random.shuffle(valid) # lucky spinner โ€” random key order every call
 
593
  last_err = None
594
  for n, k in valid:
595
  for mdl in models:
 
1051
  @app.route('/api/login', methods=['POST'])
1052
  def api_login():
1053
  try:
1054
+ ip = _ip(request)
1055
+ # โ”€โ”€ (1) Check lockout โ”€โ”€
1056
+ allowed, wait = _pw_check(ip)
1057
+ if not allowed:
1058
+ m, s = divmod(wait, 60)
1059
+ return jsonify(ok=False,
1060
+ msg=f'โŒ {PW_MAX_FAILS} แ€€แ€ผแ€ญแ€™แ€บ แ€™แ€พแ€ฌแ€ธแ€žแ€ฑแ€ฌแ€€แ€ผแ€ฑแ€ฌแ€„แ€ทแ€บ {m} แ€™แ€ญแ€”แ€…แ€บ {s} แ€…แ€€แ€นแ€€แ€”แ€ทแ€บ แ€…แ€ฑแ€ฌแ€„แ€ทแ€บแ€•แ€ซแ‹'), 429
1061
+ d = request.get_json(force=True) or {}
1062
+ uname = d.get('username', '')
1063
+ pw = d.get('password', '')
1064
+ ok, msg, coins = login_user(uname, pw)
1065
+ if ok:
1066
+ _pw_clear(ip) # reset on success
1067
+ else:
1068
+ _pw_fail(ip)
1069
+ rem = _pw_remaining(ip)
1070
+ if rem == 0:
1071
+ msg = f'โŒ {PW_MAX_FAILS} แ€€แ€ผแ€ญแ€™แ€บ แ€™แ€พแ€ฌแ€ธแ€žแ€ฑแ€ฌแ€€แ€ผแ€ฑแ€ฌแ€„แ€ทแ€บ 5 แ€™แ€ญแ€”แ€…แ€บ แ€…แ€ฑแ€ฌแ€„แ€ทแ€บแ€•แ€ซแ‹'
1072
+ else:
1073
+ msg = f'โŒ Username (แ€žแ€ญแ€ฏแ€ท) Password แ€™แ€พแ€ฌแ€ธแ€žแ€Šแ€บแ‹ ({rem} แ€€แ€ผแ€ญแ€™แ€บ แ€€แ€ปแ€”แ€บแ€žแ€Šแ€บ)'
1074
+ return jsonify(ok=ok, msg=msg, coins=coins, is_admin=(uname==ADMIN_U and ok))
1075
  except Exception as e:
1076
  return jsonify(ok=False, msg=str(e))
1077
 
1078
  @app.route('/api/register', methods=['POST'])
1079
  def api_register():
1080
  try:
1081
+ ip = _ip(request)
1082
+ # โ”€โ”€ (2) 1 account per IP per day โ”€โ”€
1083
+ if not _reg_check(ip):
1084
+ return jsonify(ok=False,
1085
+ msg='โŒ แ€แ€…แ€บแ€›แ€€แ€บแ€แ€ฝแ€„แ€บ account แ€แ€…แ€บแ€แ€ฏแ€žแ€ฌ แ€–แ€”แ€บแ€แ€ฎแ€ธแ€”แ€ญแ€ฏแ€„แ€บแ€žแ€Šแ€บแ‹ แ€™แ€”แ€€แ€บแ€–แ€ผแ€”แ€บ แ€‘แ€•แ€บแ€€แ€ผแ€ญแ€ฏแ€ธแ€…แ€ฌแ€ธแ€•แ€ซแ‹'), 429
1086
+ d = request.get_json(force=True) or {}
1087
  uname = (d.get('username') or '').strip() or gen_uname()
1088
+ pw = d.get('password', '')
1089
+ db = load_db()
1090
+ if uname in db['users']:
1091
+ return jsonify(ok=False, msg='โŒ Already exists')
1092
  db['users'][uname] = {'password': hp(pw) if pw else '', 'coins': 1,
1093
  'created_at': datetime.now().isoformat(), 'last_login': None,
1094
  'total_transcripts': 0, 'total_videos': 0,
index.html CHANGED
@@ -1639,7 +1639,10 @@ async function doLogin(){
1639
  sam(d.msg||'โŒ Login failed',false);
1640
  if(btn){btn.disabled=false;btn.textContent='Login';}
1641
  }
1642
- }catch{sam('โŒ Connection error',false);if(btn){btn.disabled=false;btn.textContent='Login';}}
 
 
 
1643
  }
1644
  async function doReg(){
1645
  const u=document.getElementById('r-u').value.trim(),p=document.getElementById('r-p').value;
@@ -1655,7 +1658,10 @@ async function doReg(){
1655
  sam(d.msg||'โŒ Failed',false);
1656
  if(btn){btn.disabled=false;btn.textContent='Register';}
1657
  }
1658
- }catch{sam('โŒ Connection error',false);if(btn){btn.disabled=false;btn.textContent='Register';}}
 
 
 
1659
  }
1660
  async function fetchAndUpdateCoins(){
1661
  if(!U||ISADM)return;
 
1639
  sam(d.msg||'โŒ Login failed',false);
1640
  if(btn){btn.disabled=false;btn.textContent='Login';}
1641
  }
1642
+ } catch{
1643
+ sam('โŒ Connection error',false);
1644
+ if(btn){btn.disabled=false;btn.textContent='Login';}
1645
+ }
1646
  }
1647
  async function doReg(){
1648
  const u=document.getElementById('r-u').value.trim(),p=document.getElementById('r-p').value;
 
1658
  sam(d.msg||'โŒ Failed',false);
1659
  if(btn){btn.disabled=false;btn.textContent='Register';}
1660
  }
1661
+ } catch{
1662
+ sam('โŒ Connection error',false);
1663
+ if(btn){btn.disabled=false;btn.textContent='Register';}
1664
+ }
1665
  }
1666
  async function fetchAndUpdateCoins(){
1667
  if(!U||ISADM)return;