Add API server
This commit is contained in:
parent
9c7588c396
commit
8c70863c4b
|
|
@ -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")
|
||||||
Loading…
Reference in New Issue