AI Bot commited on
Commit
c21f8a5
·
1 Parent(s): 81d8858

fix: enforce X-AutoAlt-Key validation on scanstart, progress, result, and report endpoints

Browse files
Files changed (3) hide show
  1. api.py +34 -21
  2. app.py +18 -14
  3. static/index.html +46 -5
api.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import FastAPI, BackgroundTasks, Query, Header, HTTPException
2
  from fastapi.middleware.cors import CORSMiddleware as CORS
3
  from fastapi.responses import JSONResponse, FileResponse
4
  from fastapi.staticfiles import StaticFiles
@@ -23,6 +23,22 @@ logger = logging.getLogger("API")
23
  # Securely load the key from Hugging Face environment variables
24
  SECRET_KEY = os.getenv("AUTOALT_SECRET_KEY")
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  app = FastAPI(title="Antigravity SEO Scaler API")
27
 
28
  app.add_middleware(
@@ -193,15 +209,8 @@ def run_scan_job(job_id: str, domain: str, limit: int, is_auto: bool = False):
193
  # --- Endpoints ---
194
 
195
  @app.get("/scrape")
196
- def run_scraper(x_autoalt_key: str = Header(None, alias="X-AutoAlt-Key")):
197
- # Block if the header is missing
198
- if not x_autoalt_key:
199
- raise HTTPException(status_code=401, detail="Unauthorized: X-AutoAlt-Key header is missing")
200
-
201
- # Block if the header value does not match your secret hash
202
- if x_autoalt_key != SECRET_KEY:
203
- raise HTTPException(status_code=403, detail="Forbidden: Invalid X-AutoAlt-Key")
204
-
205
  # If successful, return the scraped data
206
  return {"status": "success", "data": "Scraped data goes here"}
207
 
@@ -217,17 +226,20 @@ def health_check():
217
  return {"status": "alive"}
218
 
219
  @app.get("/api/status")
 
220
  def get_system_status(domain: Optional[str] = None):
221
  # If a domain is provided, we check its connectivity specifically
222
  stats = SystemMonitor.get_system_stats(target_url=domain)
223
  return stats
224
 
225
  @app.post("/api/scanstart")
 
226
  def start_scan(
227
  bg: BackgroundTasks,
228
  payload: Optional[StartReq] = None,
229
  domain: Optional[str] = Query(None),
230
- limit: Optional[int] = Query(25)
 
231
  ):
232
  # Support both JSON body and query parameters
233
  if payload:
@@ -263,7 +275,9 @@ def start_scan(
263
 
264
  @app.get("/api/progress/{job_id}")
265
  @app.get("/api/progress")
266
- def scan_progress(job_id: Optional[str] = None):
 
 
267
  if not job_id:
268
  return JSONResponse(status_code=400, content={"error": "job_id is required"})
269
 
@@ -299,7 +313,9 @@ def scan_progress(job_id: Optional[str] = None):
299
 
300
  @app.get("/api/result/{job_id}")
301
  @app.get("/api/result")
302
- def scan_result(job_id: Optional[str] = None):
 
 
303
  if not job_id:
304
  return JSONResponse(status_code=400, content={"error": "job_id is required"})
305
 
@@ -313,21 +329,18 @@ def scan_result(job_id: Optional[str] = None):
313
  return job["result"]
314
 
315
  # Legacy Endpoint Support (Optional - redirects to a sync wait or error?)
316
- # For now, let's keep it but make it use the new logic synchronously if possible,
317
- # OR just deprecate it. Given the request, we should probably stick to the new API.
318
- # But for backward compatibility with existing frontend using /api/seo-report?
319
- # The user didn't ask to remove it, but the new code replaces the structure.
320
- # I'll enable a blocking legacy endpoint for safety.
321
-
322
  @app.get("/api/seo-report")
 
323
  def get_seo_report_legacy_get(
324
  domain: Optional[str] = Query(None),
325
- limit: Optional[int] = Query(25)
 
326
  ):
327
  return handle_legacy_request(domain, limit)
328
 
329
  @app.post("/api/seo-report")
330
- def get_seo_report_legacy_post(payload: StartReq):
 
331
  return handle_legacy_request(payload.domain, payload.limit)
332
 
333
  def handle_legacy_request(domain: Optional[str], limit: int):
 
1
+ from fastapi import FastAPI, BackgroundTasks, Query, Header, HTTPException, Depends
2
  from fastapi.middleware.cors import CORSMiddleware as CORS
3
  from fastapi.responses import JSONResponse, FileResponse
4
  from fastapi.staticfiles import StaticFiles
 
23
  # Securely load the key from Hugging Face environment variables
24
  SECRET_KEY = os.getenv("AUTOALT_SECRET_KEY")
25
 
26
+ def verify_key(
27
+ x_autoalt_key: Optional[str] = Header(None, alias="X-AutoAlt-Key"),
28
+ key: Optional[str] = Query(None),
29
+ x_key: Optional[str] = Query(None, alias="x_autoalt_key")
30
+ ):
31
+ token = x_autoalt_key or key or x_key
32
+ # Block if the header/key is missing
33
+ if not token:
34
+ raise HTTPException(status_code=401, detail="Unauthorized: X-AutoAlt-Key header is missing")
35
+
36
+ # Block if the header value does not match your secret hash
37
+ if token != SECRET_KEY:
38
+ raise HTTPException(status_code=403, detail="Forbidden: Invalid X-AutoAlt-Key")
39
+
40
+ return token
41
+
42
  app = FastAPI(title="Antigravity SEO Scaler API")
43
 
44
  app.add_middleware(
 
209
  # --- Endpoints ---
210
 
211
  @app.get("/scrape")
212
+ @app.get("/api/scrape")
213
+ def run_scraper(auth: str = Depends(verify_key)):
 
 
 
 
 
 
 
214
  # If successful, return the scraped data
215
  return {"status": "success", "data": "Scraped data goes here"}
216
 
 
226
  return {"status": "alive"}
227
 
228
  @app.get("/api/status")
229
+ @app.get("/status")
230
  def get_system_status(domain: Optional[str] = None):
231
  # If a domain is provided, we check its connectivity specifically
232
  stats = SystemMonitor.get_system_stats(target_url=domain)
233
  return stats
234
 
235
  @app.post("/api/scanstart")
236
+ @app.post("/scanstart")
237
  def start_scan(
238
  bg: BackgroundTasks,
239
  payload: Optional[StartReq] = None,
240
  domain: Optional[str] = Query(None),
241
+ limit: Optional[int] = Query(25),
242
+ auth: str = Depends(verify_key)
243
  ):
244
  # Support both JSON body and query parameters
245
  if payload:
 
275
 
276
  @app.get("/api/progress/{job_id}")
277
  @app.get("/api/progress")
278
+ @app.get("/progress/{job_id}")
279
+ @app.get("/progress")
280
+ def scan_progress(job_id: Optional[str] = None, auth: str = Depends(verify_key)):
281
  if not job_id:
282
  return JSONResponse(status_code=400, content={"error": "job_id is required"})
283
 
 
313
 
314
  @app.get("/api/result/{job_id}")
315
  @app.get("/api/result")
316
+ @app.get("/result/{job_id}")
317
+ @app.get("/result")
318
+ def scan_result(job_id: Optional[str] = None, auth: str = Depends(verify_key)):
319
  if not job_id:
320
  return JSONResponse(status_code=400, content={"error": "job_id is required"})
321
 
 
329
  return job["result"]
330
 
331
  # Legacy Endpoint Support (Optional - redirects to a sync wait or error?)
 
 
 
 
 
 
332
  @app.get("/api/seo-report")
333
+ @app.get("/seo-report")
334
  def get_seo_report_legacy_get(
335
  domain: Optional[str] = Query(None),
336
+ limit: Optional[int] = Query(25),
337
+ auth: str = Depends(verify_key)
338
  ):
339
  return handle_legacy_request(domain, limit)
340
 
341
  @app.post("/api/seo-report")
342
+ @app.post("/seo-report")
343
+ def get_seo_report_legacy_post(payload: StartReq, auth: str = Depends(verify_key)):
344
  return handle_legacy_request(payload.domain, payload.limit)
345
 
346
  def handle_legacy_request(domain: Optional[str], limit: int):
app.py CHANGED
@@ -1,23 +1,27 @@
1
  import os
2
  from fastapi import FastAPI, Header, HTTPException
3
 
4
- app = FastAPI()
5
-
6
  # Securely load the key from Hugging Face environment variables
7
  SECRET_KEY = os.getenv("AUTOALT_SECRET_KEY")
8
 
9
- @app.get("/scrape")
10
- def run_scraper(x_autoalt_key: str = Header(None, alias="X-AutoAlt-Key")):
11
- # Block if the header is missing
12
- if not x_autoalt_key:
13
- raise HTTPException(status_code=401, detail="Unauthorized: X-AutoAlt-Key header is missing")
14
-
15
- # Block if the header value does not match your secret hash
16
- if x_autoalt_key != SECRET_KEY:
17
- raise HTTPException(status_code=403, detail="Forbidden: Invalid X-AutoAlt-Key")
18
-
19
- # If successful, return the scraped data
20
- return {"status": "success", "data": "Scraped data goes here"}
 
 
 
 
 
 
21
 
22
  if __name__ == "__main__":
23
  import uvicorn
 
1
  import os
2
  from fastapi import FastAPI, Header, HTTPException
3
 
 
 
4
  # Securely load the key from Hugging Face environment variables
5
  SECRET_KEY = os.getenv("AUTOALT_SECRET_KEY")
6
 
7
+ try:
8
+ from api import app, verify_key, run_scraper
9
+ except ImportError:
10
+ app = FastAPI()
11
+
12
+ @app.get("/scrape")
13
+ @app.get("/api/scrape")
14
+ def run_scraper(x_autoalt_key: str = Header(None, alias="X-AutoAlt-Key")):
15
+ # Block if the header is missing
16
+ if not x_autoalt_key:
17
+ raise HTTPException(status_code=401, detail="Unauthorized: X-AutoAlt-Key header is missing")
18
+
19
+ # Block if the header value does not match your secret hash
20
+ if x_autoalt_key != SECRET_KEY:
21
+ raise HTTPException(status_code=403, detail="Forbidden: Invalid X-AutoAlt-Key")
22
+
23
+ # If successful, return the scraped data
24
+ return {"status": "success", "data": "Scraped data goes here"}
25
 
26
  if __name__ == "__main__":
27
  import uvicorn
static/index.html CHANGED
@@ -216,6 +216,9 @@
216
  </header>
217
 
218
  <div class="card">
 
 
 
219
  <div class="input-group">
220
  <input type="text" id="domainInput" placeholder="Enter Domain (e.g. https://example.com)"
221
  value="https://example.com">
@@ -265,6 +268,17 @@
265
  let jobId = null;
266
  let pollInterval = null;
267
 
 
 
 
 
 
 
 
 
 
 
 
268
  async function startScan() {
269
  const domain = document.getElementById('domainInput').value;
270
  const limit = parseInt(document.getElementById('limitInput').value);
@@ -280,9 +294,19 @@
280
  // 1. Start Job
281
  const res = await fetch('/api/scanstart', {
282
  method: 'POST',
283
- headers: { 'Content-Type': 'application/json' },
284
  body: JSON.stringify({ domain, limit })
285
  });
 
 
 
 
 
 
 
 
 
 
286
  const data = await res.json();
287
  jobId = data.job_id;
288
 
@@ -292,6 +316,7 @@
292
  } catch (e) {
293
  alert("Error starting scan: " + e);
294
  document.getElementById('scanBtn').disabled = false;
 
295
  }
296
  }
297
 
@@ -299,7 +324,9 @@
299
  if (!jobId) return;
300
 
301
  try {
302
- const res = await fetch(`/api/progress/${jobId}`);
 
 
303
  const data = await res.json();
304
 
305
  // Update UI
@@ -329,16 +356,30 @@
329
  }
330
 
331
  async function fetchResult() {
332
- const res = await fetch(`/api/result/${jobId}`);
 
 
333
  const data = await res.json();
334
 
335
  document.getElementById('result-section').style.display = 'block';
336
  document.getElementById('json-output').innerText = JSON.stringify(data, null, 2);
337
  }
338
 
339
- // Auto-fill from query params
340
  window.onload = () => {
 
 
 
 
 
 
 
341
  const urlParams = new URLSearchParams(window.location.search);
 
 
 
 
 
342
  const domain = urlParams.get('domain');
343
  const limit = urlParams.get('limit');
344
 
@@ -346,7 +387,7 @@
346
  document.getElementById('domainInput').value = domain;
347
  if (limit) document.getElementById('limitInput').value = limit;
348
 
349
- // Auto start
350
  startScan();
351
  }
352
  }
 
216
  </header>
217
 
218
  <div class="card">
219
+ <div class="input-group" style="margin-bottom: 0.75rem;">
220
+ <input type="password" id="apiKeyInput" placeholder="Enter X-AutoAlt-Key (API Key)" style="width: 100%;">
221
+ </div>
222
  <div class="input-group">
223
  <input type="text" id="domainInput" placeholder="Enter Domain (e.g. https://example.com)"
224
  value="https://example.com">
 
268
  let jobId = null;
269
  let pollInterval = null;
270
 
271
+ function getAuthHeaders(extraHeaders = {}) {
272
+ const headers = { ...extraHeaders };
273
+ const apiKeyInput = document.getElementById('apiKeyInput');
274
+ const apiKey = apiKeyInput ? apiKeyInput.value.trim() : '';
275
+ if (apiKey) {
276
+ headers['X-AutoAlt-Key'] = apiKey;
277
+ try { localStorage.setItem('autoalt_key', apiKey); } catch (e) {}
278
+ }
279
+ return headers;
280
+ }
281
+
282
  async function startScan() {
283
  const domain = document.getElementById('domainInput').value;
284
  const limit = parseInt(document.getElementById('limitInput').value);
 
294
  // 1. Start Job
295
  const res = await fetch('/api/scanstart', {
296
  method: 'POST',
297
+ headers: getAuthHeaders({ 'Content-Type': 'application/json' }),
298
  body: JSON.stringify({ domain, limit })
299
  });
300
+
301
+ if (!res.ok) {
302
+ const err = await res.json().catch(() => ({}));
303
+ const msg = err.detail || err.error || ('HTTP ' + res.status);
304
+ alert("Scan start failed: " + msg);
305
+ document.getElementById('scanBtn').disabled = false;
306
+ document.getElementById('progress-section').style.display = 'none';
307
+ return;
308
+ }
309
+
310
  const data = await res.json();
311
  jobId = data.job_id;
312
 
 
316
  } catch (e) {
317
  alert("Error starting scan: " + e);
318
  document.getElementById('scanBtn').disabled = false;
319
+ document.getElementById('progress-section').style.display = 'none';
320
  }
321
  }
322
 
 
324
  if (!jobId) return;
325
 
326
  try {
327
+ const res = await fetch(`/api/progress/${jobId}`, {
328
+ headers: getAuthHeaders()
329
+ });
330
  const data = await res.json();
331
 
332
  // Update UI
 
356
  }
357
 
358
  async function fetchResult() {
359
+ const res = await fetch(`/api/result/${jobId}`, {
360
+ headers: getAuthHeaders()
361
+ });
362
  const data = await res.json();
363
 
364
  document.getElementById('result-section').style.display = 'block';
365
  document.getElementById('json-output').innerText = JSON.stringify(data, null, 2);
366
  }
367
 
368
+ // Auto-fill from query params or localStorage
369
  window.onload = () => {
370
+ try {
371
+ const savedKey = localStorage.getItem('autoalt_key');
372
+ if (savedKey && document.getElementById('apiKeyInput')) {
373
+ document.getElementById('apiKeyInput').value = savedKey;
374
+ }
375
+ } catch (e) {}
376
+
377
  const urlParams = new URLSearchParams(window.location.search);
378
+ const keyParam = urlParams.get('key') || urlParams.get('x_autoalt_key');
379
+ if (keyParam && document.getElementById('apiKeyInput')) {
380
+ document.getElementById('apiKeyInput').value = keyParam;
381
+ }
382
+
383
  const domain = urlParams.get('domain');
384
  const limit = urlParams.get('limit');
385
 
 
387
  document.getElementById('domainInput').value = domain;
388
  if (limit) document.getElementById('limitInput').value = limit;
389
 
390
+ // Auto start if key is also available
391
  startScan();
392
  }
393
  }