Skip to content
Merged
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
28 changes: 28 additions & 0 deletions .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Pylint

on: [push]

jobs:
lint:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.14"

- name: Install dependencies
working-directory: backend
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pylint

- name: Run Pylint
working-directory: backend
run: |
pylint $(git ls-files '*.py')
48 changes: 0 additions & 48 deletions Backend/app/core/security.py

This file was deleted.

21 changes: 0 additions & 21 deletions Backend/app/schemas/auth_dto.py

This file was deleted.

8 changes: 0 additions & 8 deletions Backend/app/schemas/user_dto.py

This file was deleted.

File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 3 additions & 0 deletions Backend/app/core/config.py → backend/app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""Configuration objects and helpers for the FastAPI application."""

import os

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
"""Application settings loaded from environment variables or ``.env`` files."""
# --- App Settings ---
APP_NAME: str = "Inquiro API"
ENVIRONMENT: str = "dev"
Expand Down
17 changes: 10 additions & 7 deletions Backend/app/core/database.py → backend/app/core/database.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""Database configuration and session utilities for the API."""

import logging
from importlib import import_module

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy.orm import declarative_base, sessionmaker

from app.core.config import settings

Expand All @@ -14,20 +17,20 @@
echo=(settings.ENVIRONMENT == "dev"),
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
SESSION_LOCAL = sessionmaker(autocommit=False, autoflush=False, bind=engine)


def init_db():
"""Automatically create or update tables based on models."""
import app.models # noqa: F401 (ensures models are imported)
def init_db() -> None:
"""Automatically create or update tables based on SQLAlchemy models."""
import_module("app.models.user")
logger.info("🔄 Creating / updating database schema...")
Base.metadata.create_all(bind=engine)
logger.info("✅ Database schema up to date.")


def get_db():
"""Provide a database session for FastAPI routes."""
db = SessionLocal()
"""Yield a database session for FastAPI routes."""
db = SESSION_LOCAL()
try:
yield db
finally:
Expand Down
66 changes: 66 additions & 0 deletions backend/app/core/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Security utilities for creating and validating JWT tokens."""

from datetime import datetime, timedelta, timezone
from typing import Any, Dict, Optional

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt

from app.core.config import settings

ALGORITHM = "HS256"

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


def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
"""Create a signed JWT access token."""

to_encode = data.copy()
expire = datetime.now(timezone.utc) + (
expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=ALGORITHM)


def create_refresh_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
"""Create a signed JWT refresh token."""

to_encode = data.copy()
expire = datetime.now(timezone.utc) + (
expires_delta or timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
)
to_encode.update({"exp": expire, "type": "refresh"})
return jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=ALGORITHM)


def verify_token(token: str) -> Optional[Dict[str, Any]]:
"""Decode a JWT token and return its payload, if valid."""

try:
return jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[ALGORITHM])
except JWTError:
return None


def get_current_user(token: str = Depends(oauth2_scheme)) -> str:
"""Return the username encoded in the access token."""

payload = verify_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token.",
headers={"WWW-Authenticate": "Bearer"},
)

username = payload.get("sub")
if not username:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token payload.",
)

return username
12 changes: 8 additions & 4 deletions Backend/app/main.py → backend/app/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""Application entry point for the Inquiro FastAPI service."""

import logging
from contextlib import asynccontextmanager

from fastapi import FastAPI

from app.core.config import settings
from app.core.database import init_db
from app.routes import user_routes, auth_routes
from app.routes import auth_routes, user_routes

# ---------------------------------------------------------
# Configure Logging
Expand All @@ -21,8 +23,10 @@
# Lifespan Event Handlers (Modern FastAPI)
# ---------------------------------------------------------
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info(f"🚀 Starting Inquiro API in '{settings.ENVIRONMENT}' mode...")
async def lifespan(_app: FastAPI):
"""Initialize and tear down application resources."""

logger.info("🚀 Starting Inquiro API in '%s' mode...", settings.ENVIRONMENT)
if settings.ENVIRONMENT == "dev":
init_db() # Auto-create tables only in dev
logger.info("✅ Startup complete.")
Expand All @@ -40,7 +44,7 @@ async def lifespan(app: FastAPI):
title="Inquiro API",
description="AI-powered research discovery backend for Inquiro.",
version="0.1.0",
lifespan=lifespan
lifespan=lifespan,
)


Expand Down
File renamed without changes.
10 changes: 9 additions & 1 deletion Backend/app/models/user.py → backend/app/models/user.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
from sqlalchemy import String, Integer
"""SQLAlchemy models used by the application."""

# pylint: disable=too-few-public-methods

from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from app.core.database import Base


class User(Base):
"""Database representation of an application user."""

__tablename__ = "users"

id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Routes related to authentication and JWT management."""

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

Expand All @@ -22,6 +24,8 @@
summary="Authenticate a user and return JWT tokens",
)
def login(request: LoginRequest, db: Session = Depends(get_db)) -> LoginResponse:
"""Authenticate a user and return access plus refresh tokens."""

user = db.query(User).filter(User.username == request.username).first()
if not user:
raise HTTPException(
Expand Down Expand Up @@ -49,6 +53,8 @@ def login(request: LoginRequest, db: Session = Depends(get_db)) -> LoginResponse
summary="Generate a new access token using a refresh token",
)
def refresh_access_token(request: RefreshRequest) -> RefreshResponse:
"""Validate a refresh token and return a new access token."""

payload = verify_token(request.refresh_token)
if not payload or payload.get("type") != "refresh":
raise HTTPException(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Routes for managing and retrieving user information."""

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session
Expand All @@ -12,26 +14,36 @@


class UserCreate(BaseModel):
"""Payload used when creating a new user."""

username: str


@router.post("/", response_model=UserResponse, status_code=201)
def create_user(request: UserCreate, db: Session = Depends(get_db)):
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
def create_user(request: UserCreate, db: Session = Depends(get_db)) -> User:
"""Create a new user record if the username is available."""

if db.query(User).filter(User.username == request.username).first():
raise HTTPException(status_code=400, detail="Username already exists")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already exists",
)
user = User(username=request.username)
db.add(user)
db.commit()
db.refresh(user)
return user


@router.get("/me", response_model=UserResponse, status_code=status.HTTP_200_OK)
def get_current_user_profile(
current_username: str = Depends(get_current_user),
db: Session = Depends(get_db),
):
) -> User:
"""Return the user information for the authenticated principal."""

user = db.query(User).filter(User.username == current_username).first()
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found.")

return user
return user
File renamed without changes.
33 changes: 33 additions & 0 deletions backend/app/schemas/auth_dto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Pydantic models for authentication requests and responses."""

from typing import Any, Dict

from pydantic import BaseModel


class LoginRequest(BaseModel):
"""Payload for authenticating a user."""

username: str


class LoginResponse(BaseModel):
"""Response returned after a successful login."""

access_token: str
refresh_token: str
token_type: str
user: Dict[str, Any]


class RefreshRequest(BaseModel):
"""Payload containing the refresh token."""

refresh_token: str


class RefreshResponse(BaseModel):
"""Response containing a fresh access token."""

access_token: str
token_type: str
Loading