EdiBianco commited on
Commit
4efa1cd
·
1 Parent(s): 6e9392f

Replace Basic Auth with cookie login form (HF proxy fix)

Browse files
Files changed (1) hide show
  1. app.py +102 -26
app.py CHANGED
@@ -4,6 +4,8 @@ FastAPI + faster-whisper + yt-dlp
4
  """
5
  import asyncio
6
  import glob
 
 
7
  import json
8
  import os
9
  import secrets
@@ -15,9 +17,9 @@ from typing import Optional
15
 
16
  import yt_dlp
17
  from faster_whisper import WhisperModel
18
- from fastapi import Depends, FastAPI, File, Form, HTTPException, UploadFile
19
- from fastapi.responses import StreamingResponse
20
- from fastapi.security import HTTPBasic, HTTPBasicCredentials
21
 
22
  # ---------------------------------------------------------------------------
23
  # Config
@@ -25,12 +27,34 @@ from fastapi.security import HTTPBasic, HTTPBasicCredentials
25
  PASSWORD = os.getenv("APP_PASSWORD", "changeme")
26
  MAX_UPLOAD_BYTES = 500 * 1024 * 1024 # 500 MB
27
  WHISPER_MODEL = "base.en"
 
 
28
 
29
  # ---------------------------------------------------------------------------
30
- # App + auth
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  # ---------------------------------------------------------------------------
32
  app = FastAPI()
33
- security = HTTPBasic()
34
  executor = ThreadPoolExecutor(max_workers=2)
35
 
36
  _model: Optional[WhisperModel] = None
@@ -46,15 +70,74 @@ def get_model() -> WhisperModel:
46
  return _model
47
 
48
 
49
- def check_auth(credentials: HTTPBasicCredentials = Depends(security)) -> str:
50
- ok = secrets.compare_digest(credentials.password.encode(), PASSWORD.encode())
51
- if not ok:
52
- raise HTTPException(
53
- status_code=401,
54
- detail="Unauthorized",
55
- headers={"WWW-Authenticate": "Basic"},
56
- )
57
- return credentials.username
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
 
60
  # ---------------------------------------------------------------------------
@@ -107,17 +190,13 @@ def transcribe_file(jid: str, path: str, cleanup_paths: list[str]):
107
  # ---------------------------------------------------------------------------
108
  # Routes
109
  # ---------------------------------------------------------------------------
110
- @app.get("/", dependencies=[Depends(check_auth)])
111
  async def index():
112
- from fastapi.responses import HTMLResponse
113
- return HTMLResponse(HTML)
114
 
115
 
116
  @app.post("/transcribe/file")
117
- async def transcribe_upload(
118
- file: UploadFile = File(...),
119
- _: str = Depends(check_auth),
120
- ):
121
  # Size check via content-length header
122
  size = int(file.headers.get("content-length", 0))
123
  if size > MAX_UPLOAD_BYTES:
@@ -142,10 +221,7 @@ async def transcribe_upload(
142
 
143
 
144
  @app.post("/transcribe/url")
145
- async def transcribe_url(
146
- url: str = Form(...),
147
- _: str = Depends(check_auth),
148
- ):
149
  jid = new_job()
150
  push(jid, f"Fetching URL: {url}")
151
 
@@ -199,7 +275,7 @@ async def transcribe_url(
199
 
200
 
201
  @app.get("/progress/{jid}")
202
- async def progress(jid: str, _: str = Depends(check_auth)):
203
  if jid not in jobs:
204
  raise HTTPException(404, "Job not found.")
205
 
 
4
  """
5
  import asyncio
6
  import glob
7
+ import hashlib
8
+ import hmac
9
  import json
10
  import os
11
  import secrets
 
17
 
18
  import yt_dlp
19
  from faster_whisper import WhisperModel
20
+ from fastapi import FastAPI, File, Form, Request, UploadFile
21
+ from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse
22
+ from starlette.middleware.base import BaseHTTPMiddleware
23
 
24
  # ---------------------------------------------------------------------------
25
  # Config
 
27
  PASSWORD = os.getenv("APP_PASSWORD", "changeme")
28
  MAX_UPLOAD_BYTES = 500 * 1024 * 1024 # 500 MB
29
  WHISPER_MODEL = "base.en"
30
+ COOKIE_NAME = "auth"
31
+ _SECRET = os.urandom(32) # ephemeral signing key
32
 
33
  # ---------------------------------------------------------------------------
34
+ # Auth helpers
35
+ # ---------------------------------------------------------------------------
36
+
37
+ def _make_token(pw: str) -> str:
38
+ return hmac.new(_SECRET, pw.encode(), hashlib.sha256).hexdigest()
39
+
40
+ def _is_authed(request: Request) -> bool:
41
+ token = request.cookies.get(COOKIE_NAME, "")
42
+ return secrets.compare_digest(token, _make_token(PASSWORD))
43
+
44
+
45
+ class AuthMiddleware(BaseHTTPMiddleware):
46
+ async def dispatch(self, request: Request, call_next):
47
+ if request.url.path in ("/login",):
48
+ return await call_next(request)
49
+ if not _is_authed(request):
50
+ return RedirectResponse("/login", status_code=302)
51
+ return await call_next(request)
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # App
55
  # ---------------------------------------------------------------------------
56
  app = FastAPI()
57
+ app.add_middleware(AuthMiddleware)
58
  executor = ThreadPoolExecutor(max_workers=2)
59
 
60
  _model: Optional[WhisperModel] = None
 
70
  return _model
71
 
72
 
73
+ # ---------------------------------------------------------------------------
74
+ # Login routes
75
+ # ---------------------------------------------------------------------------
76
+ LOGIN_HTML = """<!DOCTYPE html>
77
+ <html lang="en">
78
+ <head>
79
+ <meta charset="UTF-8">
80
+ <meta name="viewport" content="width=device-width, initial-scale=1">
81
+ <title>VOD Transcriber — Login</title>
82
+ <style>
83
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
84
+ body {
85
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
86
+ background: #0f0f11; color: #e2e2e5;
87
+ min-height: 100vh; display: flex; align-items: center; justify-content: center;
88
+ }
89
+ .card {
90
+ background: #1a1a1f; border: 1px solid #2a2a32; border-radius: 16px;
91
+ padding: 2.5rem; width: 100%; max-width: 360px;
92
+ box-shadow: 0 8px 40px rgba(0,0,0,0.4);
93
+ }
94
+ h1 { font-size: 1.2rem; font-weight: 600; color: #fff; margin-bottom: 0.3rem; }
95
+ .sub { font-size: 0.82rem; color: #555; margin-bottom: 2rem; }
96
+ label { display: block; font-size: 0.78rem; font-weight: 500; color: #888; margin-bottom: 0.4rem; text-transform: uppercase; letter-spacing: 0.05em; }
97
+ input[type="password"] {
98
+ width: 100%; background: #111115; border: 1px solid #2a2a32; border-radius: 10px;
99
+ padding: 0.7rem 1rem; font-size: 0.9rem; color: #e2e2e5; outline: none;
100
+ margin-bottom: 1rem; transition: border-color 0.15s;
101
+ }
102
+ input[type="password"]:focus { border-color: #5b5bf6; }
103
+ button {
104
+ width: 100%; background: #5b5bf6; color: #fff; border: none; border-radius: 10px;
105
+ padding: 0.75rem; font-size: 0.95rem; font-weight: 600; cursor: pointer;
106
+ transition: background 0.15s;
107
+ }
108
+ button:hover { background: #4a4ae0; }
109
+ .err { color: #f66; font-size: 0.82rem; margin-top: 0.75rem; display: none; }
110
+ .err.visible { display: block; }
111
+ </style>
112
+ </head>
113
+ <body>
114
+ <div class="card">
115
+ <h1>VOD Transcriber</h1>
116
+ <p class="sub">Enter password to continue</p>
117
+ <form method="POST" action="/login">
118
+ <label for="pw">Password</label>
119
+ <input type="password" id="pw" name="password" autofocus autocomplete="current-password">
120
+ <button type="submit">Sign in</button>
121
+ </form>
122
+ <p class="err {err_class}">{err_msg}</p>
123
+ </div>
124
+ </body>
125
+ </html>"""
126
+
127
+
128
+ @app.get("/login", response_class=HTMLResponse)
129
+ async def login_page():
130
+ return LOGIN_HTML.replace("{err_class}", "").replace("{err_msg}", "")
131
+
132
+
133
+ @app.post("/login")
134
+ async def login(password: str = Form(...)):
135
+ if secrets.compare_digest(password.encode(), PASSWORD.encode()):
136
+ resp = RedirectResponse("/", status_code=302)
137
+ resp.set_cookie(COOKIE_NAME, _make_token(PASSWORD), httponly=True, samesite="lax")
138
+ return resp
139
+ html = LOGIN_HTML.replace("{err_class}", "visible").replace("{err_msg}", "Incorrect password.")
140
+ return HTMLResponse(html, status_code=401)
141
 
142
 
143
  # ---------------------------------------------------------------------------
 
190
  # ---------------------------------------------------------------------------
191
  # Routes
192
  # ---------------------------------------------------------------------------
193
+ @app.get("/", response_class=HTMLResponse)
194
  async def index():
195
+ return HTML
 
196
 
197
 
198
  @app.post("/transcribe/file")
199
+ async def transcribe_upload(file: UploadFile = File(...)):
 
 
 
200
  # Size check via content-length header
201
  size = int(file.headers.get("content-length", 0))
202
  if size > MAX_UPLOAD_BYTES:
 
221
 
222
 
223
  @app.post("/transcribe/url")
224
+ async def transcribe_url(url: str = Form(...)):
 
 
 
225
  jid = new_job()
226
  push(jid, f"Fetching URL: {url}")
227
 
 
275
 
276
 
277
  @app.get("/progress/{jid}")
278
+ async def progress(jid: str):
279
  if jid not in jobs:
280
  raise HTTPException(404, "Job not found.")
281