Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.security import OAuth2PasswordBearer
from app.auth.jwt import decode_access_token

http_bearer = HTTPBearer()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")

def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(http_bearer)) -> dict:
token = credentials.credentials
def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
payload = decode_access_token(token)
if payload is None:
raise HTTPException(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,16 @@
from datetime import datetime, timedelta, timezone
from jose import JWTError, jwt
from app.config import JWT_SECRET_KEY, JWT_ALGORITHM, JWT_EXPIRE_MINUTES, REFRESH_TOKEN_EXPIRE_DAYS

from app.config import JWT_SECRET_KEY, JWT_ALGORITHM, JWT_EXPIRE_MINUTES

def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(minutes=JWT_EXPIRE_MINUTES)
to_encode.update({"exp": expire, "type": "access"})
to_encode.update({"exp": expire})
return jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)


def create_refresh_token(data: dict) -> tuple[str, datetime]:
"""Returns (token, expires_at)"""
to_encode = data.copy()
expires_at = datetime.now(timezone.utc) + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
to_encode.update({"exp": expires_at, "type": "refresh"})
token = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)
return token, expires_at


def decode_access_token(token: str) -> dict:
try:
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM])
if payload.get("type") != "access":
return None
return payload
except JWTError:
return None


def decode_refresh_token(token: str) -> dict:
try:
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM])
if payload.get("type") != "refresh":
return None
return payload
except JWTError:
return None
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@

USE_MOCK_SERVICES = os.getenv("USE_MOCK_SERVICES", "true").lower() == "true"

DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://user:password@localhost:5432/orion_db")
# Have just added async driver ('+asyncpg') to URL to match app - Lucas
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://user:password@localhost:5432/orion_db")

JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-here")
JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
JWT_EXPIRE_MINUTES = int(os.getenv("JWT_EXPIRE_MINUTES", 60))
REFRESH_TOKEN_EXPIRE_DAYS = int(os.getenv("REFRESH_TOKEN_EXPIRE_DAYS", 7))

DEBUG = os.getenv("DEBUG", "True").lower() == "true"
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")


Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, declarative_base
from app.config import DATABASE_URL

engine = create_engine(DATABASE_URL)
engine = create_async_engine(
DATABASE_URL,
echo=True
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
SessionLocal = sessionmaker(
bind=engine,
class_=AsyncSession,
expire_on_commit=False
)

Base = declarative_base()


def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
async def get_db():
async with SessionLocal() as session:
yield session
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
)
logger = logging.getLogger(__name__)

Base.metadata.create_all(bind=engine)
# Base.metadata.create_all(bind=engine.sync_engine)

app = FastAPI(
title="Project Orion Backend API",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, String, DateTime, ForeignKey, Boolean
from sqlalchemy import Column, String, DateTime, ForeignKey
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship # ✅ ADDED
from app.database import Base
Expand All @@ -19,17 +19,6 @@ class User(Base):
jobs = relationship("Job", back_populates="user")


class RefreshToken(Base):
__tablename__ = "refresh_tokens"

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
token = Column(String, unique=True, nullable=False)
is_active = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
expires_at = Column(DateTime, nullable=False)


class Job(Base):
__tablename__ = "jobs"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import User, RefreshToken
Expand All @@ -13,7 +13,6 @@


def _issue_tokens(user: User, db: Session) -> dict:
"""Create access + refresh tokens, store refresh token in DB."""
token_data = {
"sub": str(user.user_id),
"email": user.email,
Expand Down Expand Up @@ -41,30 +40,44 @@ def _issue_tokens(user: User, db: Session) -> dict:

@router.post("/register", response_model=AuthResponse)
def register(user: RegisterRequest, db: Session = Depends(get_db)):
if db.query(User).filter(User.email == user.email).first():
raise HTTPException(status_code=400, detail="Email already registered")
if db.query(User).filter(User.username == user.username).first():
raise HTTPException(status_code=400, detail="Username already taken")

new_user = User(
email=user.email,
username=user.username,
password=hash_password(user.password)
)
db.add(new_user)
db.commit()
db.refresh(new_user)
try:
if db.query(User).filter(User.email == user.email).first():
raise HTTPException(status_code=409, detail="Email already registered")
if db.query(User).filter(User.username == user.username).first():
raise HTTPException(status_code=409, detail="Username already taken")

new_user = User(
email=user.email,
username=user.username,
password=hash_password(user.password)
)
db.add(new_user)
db.commit()
db.refresh(new_user)

return _issue_tokens(new_user, db)
return _issue_tokens(new_user, db)

except HTTPException:
raise

except Exception:
raise HTTPException(status_code=500, detail="Internal server error during registration")


@router.post("/login", response_model=AuthResponse)
def login(user: LoginRequest, db: Session = Depends(get_db)):
db_user = db.query(User).filter(User.email == user.email).first()
if not db_user or not verify_password(user.password, db_user.password):
raise HTTPException(status_code=401, detail="Invalid email or password")
try:
db_user = db.query(User).filter(User.email == user.email).first()
if not db_user or not verify_password(user.password, db_user.password):
raise HTTPException(status_code=401, detail="Invalid email or password")

return _issue_tokens(db_user, db)

return _issue_tokens(db_user, db)
except HTTPException:
raise

except Exception:
raise HTTPException(status_code=500, detail="Internal server error during login")


@router.post("/refresh", response_model=AuthResponse)
Expand All @@ -90,7 +103,6 @@ def refresh(body: RefreshRequest, db: Session = Depends(get_db)):
if not user:
raise HTTPException(status_code=404, detail="User not found")

# Revoke old refresh token and issue a new pair
db_token.is_active = False
db.commit()

Expand All @@ -112,7 +124,14 @@ def logout(body: LogoutRequest, db: Session = Depends(get_db)):

@router.get("/me", response_model=UserResponse)
def get_me(current_user: dict = Depends(get_current_user), db: Session = Depends(get_db)):
user = db.query(User).filter(User.user_id == current_user["sub"]).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
try:
user = db.query(User).filter(User.user_id == current_user["sub"]).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user

except HTTPException:
raise

except Exception:
raise HTTPException(status_code=500, detail="Internal server error retrieving user")
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from app.services.crowd_client import get_crowd_data


router = APIRouter(prefix="/api", tags=["Crowd"])


@router.get("/crowd")
async def get_crowd():
data = await get_crowd_data()
return {
"status": "success",
"message": "Crowd data retrieved successfully",
"data": data
}
try:
data = await get_crowd_data()

if not data:
raise HTTPException(status_code=404, detail="Crowd data not found")

return {
"status": "success",
"message": "Crowd data retrieved successfully",
"data": data
}

except HTTPException:
raise

except Exception:
raise HTTPException(status_code=500, detail="Internal server error while retrieving crowd data")
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import asyncio
import httpx
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Job
from app.schemas.jobs import JobDetail, JobListResponse, JobResults, JobErrors
from app.auth.dependencies import get_current_user
from app.services.player_client import get_player_data
from app.services.crowd_client import get_crowd_data
from app.config import CROWD_SERVICE_URL

router = APIRouter()

Expand Down Expand Up @@ -129,6 +132,32 @@ async def retry_job(
return {"job_id": str(job.job_id), "status": job.status}


@router.get("/jobs/{job_id}/heatmap")
async def get_heatmap(
job_id: str,
current_user: dict = Depends(get_current_user),
db: Session = Depends(get_db)
):
job = db.query(Job).filter(Job.job_id == job_id).first()
if not job:
raise HTTPException(status_code=404, detail="Job not found")
check_job_access(job, current_user)

crowd = job.crowd_result
if not crowd or not crowd.get("heatmap") or not crowd["heatmap"].get("image_path"):
raise HTTPException(status_code=404, detail="Heatmap not available for this job")

image_path = crowd["heatmap"]["image_path"].replace("\\", "/")
url = f"{CROWD_SERVICE_URL}/artifacts/{image_path}"

async with httpx.AsyncClient(timeout=10.0) as client:
r = await client.get(url)
if r.status_code != 200:
raise HTTPException(status_code=502, detail="Could not fetch heatmap from crowd service")

return StreamingResponse(iter([r.content]), media_type="image/png")


@router.delete("/jobs/{job_id}")
def delete_job(
job_id: str,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from app.services.player_client import get_player_data

router = APIRouter(prefix="/api", tags=["Players"])


@router.get("/players")
async def get_players():
data = await get_player_data()
return {
"status": "success",
"message": "Players data retrieved successfully",
"data": data
}
try:
data = await get_player_data()

if not data:
raise HTTPException(status_code=404, detail="Player data not found")

return {
"status": "success",
"message": "Players data retrieved successfully",
"data": data
}
except HTTPException:
raise

except Exception:
raise HTTPException(status_code=500, detail="Internal server error while retrieving player data")
Loading
Loading