From 8c70863c4b0c29a3fbd48307abfc01149960c242 Mon Sep 17 00:00:00 2001 From: Nolan Kovacs Date: Wed, 25 Mar 2026 19:35:35 -0400 Subject: [PATCH] Add API server --- api/main.py | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 api/main.py diff --git a/api/main.py b/api/main.py new file mode 100644 index 0000000..29c137f --- /dev/null +++ b/api/main.py @@ -0,0 +1,138 @@ +import os +import hmac +import hashlib +import time +import psycopg2 +import psycopg2.extras +from fastapi import FastAPI, HTTPException, Depends, Header +from fastapi.responses import FileResponse +from pydantic import BaseModel +from jose import jwt, JWTError +from datetime import datetime, timedelta, timezone +from typing import Optional + +app = FastAPI() + +DB_HOST = "127.0.0.1" +DB_NAME = "verimund" +DB_USER = "verimund_user" +DB_PASS = "Shlevison2k17" +JWT_SECRET = "CHANGE_THIS_TO_A_LONG_RANDOM_SECRET" +HMAC_SECRET = "CHANGE_THIS_TO_ANOTHER_LONG_RANDOM_SECRET" +JWT_ALGORITHM = "HS256" +JWT_EXPIRE_HOURS = 24 + + +def get_db(): + conn = psycopg2.connect(host=DB_HOST, dbname=DB_NAME, user=DB_USER, password=DB_PASS) + try: + yield conn + finally: + conn.close() + + +def verify_hmac(license_key: str, timestamp: str, signature: str) -> bool: + expected = hmac.new( + HMAC_SECRET.encode(), + f"{license_key}{timestamp}".encode(), + hashlib.sha256 + ).hexdigest() + return hmac.compare_digest(expected, signature) + + +def verify_jwt(authorization: Optional[str] = Header(None)): + if not authorization or not authorization.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Missing token") + token = authorization.split(" ")[1] + try: + payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) + return payload + except JWTError: + raise HTTPException(status_code=401, detail="Invalid token") + + +class AuthRequest(BaseModel): + license_key: str + hwid: str + timestamp: str + signature: str + + +@app.post("/auth") +def authenticate(req: AuthRequest, db=Depends(get_db)): + try: + req_time = int(req.timestamp) + if abs(time.time() - req_time) > 30: + raise HTTPException(status_code=401, detail="Request expired") + except ValueError: + raise HTTPException(status_code=400, detail="Invalid timestamp") + + if not verify_hmac(req.license_key, req.timestamp, req.signature): + raise HTTPException(status_code=401, detail="Invalid signature") + + cur = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute("SELECT * FROM licenses WHERE license_key = %s", (req.license_key,)) + license = cur.fetchone() + + if not license: + raise HTTPException(status_code=401, detail="Invalid license") + if not license["active"]: + raise HTTPException(status_code=401, detail="License inactive") + if license["expiry_date"] and license["expiry_date"] < datetime.now(timezone.utc): + raise HTTPException(status_code=401, detail="License expired") + if license["hwid"] and license["hwid"] != req.hwid: + raise HTTPException(status_code=401, detail="HWID mismatch") + + if not license["hwid"]: + cur.execute("UPDATE licenses SET hwid = %s WHERE license_key = %s", (req.hwid, req.license_key)) + db.commit() + + payload = { + "license_key": req.license_key, + "hwid": req.hwid, + "channel": license["channel"], + "exp": datetime.now(timezone.utc) + timedelta(hours=JWT_EXPIRE_HOURS) + } + token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) + return {"token": token, "channel": license["channel"]} + + +@app.get("/cache/analyst") +def get_analyst_cache(payload=Depends(verify_jwt), db=Depends(get_db)): + cur = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + table = "beta_analyst_cache" if payload.get("channel") == "beta" else "analyst_cache" + cur.execute(f"SELECT * FROM {table}") + return {"data": cur.fetchall()} + + +@app.get("/cache/fundamentals") +def get_fundamentals_cache(payload=Depends(verify_jwt), db=Depends(get_db)): + cur = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + table = "beta_fundamentals_cache" if payload.get("channel") == "beta" else "fundamentals_cache" + cur.execute(f"SELECT * FROM {table}") + return {"data": cur.fetchall()} + + +@app.get("/update") +def check_update(payload=Depends(verify_jwt), db=Depends(get_db)): + cur = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + channel = payload.get("channel", "stable") + cur.execute("SELECT * FROM app_versions WHERE channel = %s ORDER BY created_at DESC LIMIT 1", (channel,)) + version = cur.fetchone() + if not version: + raise HTTPException(status_code=404, detail="No version found") + return {"version": version["version"], "channel": channel, "release_notes": version.get("release_notes", "")} + + +@app.post("/download") +def download_exe(payload=Depends(verify_jwt), db=Depends(get_db)): + cur = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + channel = payload.get("channel", "stable") + cur.execute("SELECT * FROM app_versions WHERE channel = %s ORDER BY created_at DESC LIMIT 1", (channel,)) + version = cur.fetchone() + if not version: + raise HTTPException(status_code=404, detail="No version found") + path = f"/srv/releases/{channel}/StockScreener-v{version['version']}.exe" + if not os.path.exists(path): + raise HTTPException(status_code=404, detail="File not found") + return FileResponse(path, filename=f"StockScreener-v{version['version']}.exe")