Skip to content
Open
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
2 changes: 1 addition & 1 deletion .env.docker
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ DB_PORT=1433
DB_NAME=open_dateaubase
DB_USER=SA
DB_PASSWORD=StrongPwd123!

DB_DRIVER=ODBC Driver 18 for SQL Server
2 changes: 1 addition & 1 deletion Dockerfile.api
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ RUN curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
RUN pip install --no-cache-dir uv

# Copy dependency files first for layer caching
COPY pyproject.toml uv.lock ./
COPY pyproject.toml ./
COPY src/ ./src/

# Install API dependencies (fastapi, uvicorn, pyodbc, python-dotenv, local package)
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.app
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN pip install --no-cache-dir uv

# Copy dependency files first for layer caching
COPY pyproject.toml uv.lock ./
COPY pyproject.toml ./
COPY src/ ./src/

# Install app dependencies (streamlit, httpx, plotly — no pyodbc)
Expand Down
68 changes: 68 additions & 0 deletions api/v1/endpoints/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Authentication API endpoints."""

from __future__ import annotations

from fastapi import APIRouter, Depends, Header, HTTPException, status

from api.database import get_db
from ..repositories.auth_repository import AuthRepository
from ..schemas.auth import AuthResponse, LoginRequest, SignupRequest, UserOut
from ..services.auth_service import AuthService

router = APIRouter()


def get_auth_repo(conn=Depends(get_db)):
"""Dependency to get auth repository."""
return AuthRepository(conn)


def get_auth_service(repo=Depends(get_auth_repo)):
"""Dependency to get auth service."""
return AuthService(repo)


def get_current_user(
authorization: str | None = Header(default=None),
service: AuthService = Depends(get_auth_service),
):
"""Extract and validate bearer token from Authorization header."""
if authorization is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing Authorization header.",
)

scheme, _, token = authorization.partition(" ")
if scheme.lower() != "bearer" or not token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid Authorization header.",
)

return service.get_current_user_from_token(token)


@router.post("/signup", response_model=AuthResponse)
def signup(payload: SignupRequest, service=Depends(get_auth_service)):
"""Create a new user account and return a bearer token."""
return service.signup(
email=payload.email,
full_name=payload.full_name,
password=payload.password,
)


@router.post("/login", response_model=AuthResponse)
def login(payload: LoginRequest, service=Depends(get_auth_service)):
"""Authenticate a user and return a bearer token."""
return service.login(
email=payload.email,
password=payload.password,
)


@router.get("/me", response_model=UserOut)
def me(current_user=Depends(get_current_user)):
"""Return the currently authenticated user."""
return current_user
123 changes: 123 additions & 0 deletions api/v1/repositories/auth_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""Repository for authentication queries."""

from __future__ import annotations

from typing import Optional

import pyodbc


class AuthRepository:
"""Repository for authentication-related database access."""

def __init__(self, conn: pyodbc.Connection):
self.conn = conn

def get_user_by_email(self, email: str) -> Optional[dict]:
cursor = self.conn.cursor()
cursor.execute(
"""
SELECT
[UserAccount_ID],
[Email],
[FullName],
[PasswordHash],
[IsActive],
[IsVerified],
[CreatedAt],
[UpdatedAt]
FROM dbo.[UserAccount]
WHERE [Email] = ?
""",
email,
)
row = cursor.fetchone()
cursor.close()

if not row:
return None

return {
"user_id": row[0],
"email": row[1],
"full_name": row[2],
"password_hash": row[3],
"is_active": bool(row[4]),
"is_verified": bool(row[5]),
"created_at": row[6],
"updated_at": row[7],
}

def get_user_by_id(self, user_id: int) -> Optional[dict]:
cursor = self.conn.cursor()
cursor.execute(
"""
SELECT
[UserAccount_ID],
[Email],
[FullName],
[PasswordHash],
[IsActive],
[IsVerified],
[CreatedAt],
[UpdatedAt]
FROM dbo.[UserAccount]
WHERE [UserAccount_ID] = ?
""",
user_id,
)
row = cursor.fetchone()
cursor.close()

if not row:
return None

return {
"user_id": row[0],
"email": row[1],
"full_name": row[2],
"password_hash": row[3],
"is_active": bool(row[4]),
"is_verified": bool(row[5]),
"created_at": row[6],
"updated_at": row[7],
}

def create_user(self, email: str, full_name: str, password_hash: str) -> dict:
cursor = self.conn.cursor()
cursor.execute(
"""
INSERT INTO dbo.[UserAccount] (
[Email],
[FullName],
[PasswordHash],
[IsActive],
[IsVerified]
)
OUTPUT
INSERTED.[UserAccount_ID],
INSERTED.[Email],
INSERTED.[FullName],
INSERTED.[IsActive],
INSERTED.[IsVerified],
INSERTED.[CreatedAt],
INSERTED.[UpdatedAt]
VALUES (?, ?, ?, 1, 1)
""",
email,
full_name,
password_hash,
)
row = cursor.fetchone()
self.conn.commit()
cursor.close()

return {
"user_id": row[0],
"email": row[1],
"full_name": row[2],
"is_active": bool(row[3]),
"is_verified": bool(row[4]),
"created_at": row[5],
"updated_at": row[6],
}
2 changes: 2 additions & 0 deletions api/v1/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from fastapi import APIRouter
from .endpoints.auth import router as auth_router

from .endpoints.health import router as health_router
from .endpoints.sites import router as sites_router
Expand All @@ -23,6 +24,7 @@

router = APIRouter()

router.include_router(auth_router, prefix="/auth", tags=["auth"])
router.include_router(health_router, tags=["health"])
router.include_router(sites_router, prefix="/sites", tags=["sites"])
router.include_router(channels_router, prefix="/channels", tags=["channels"])
Expand Down
34 changes: 34 additions & 0 deletions api/v1/schemas/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Pydantic schemas for authentication."""

from __future__ import annotations

from datetime import datetime

from pydantic import BaseModel, Field


class SignupRequest(BaseModel):
email: str
full_name: str = Field(min_length=1, max_length=255)
password: str = Field(min_length=8, max_length=255)


class LoginRequest(BaseModel):
email: str
password: str = Field(min_length=1, max_length=255)


class UserOut(BaseModel):
user_id: int
email: str
full_name: str
is_active: bool
is_verified: bool
created_at: datetime
updated_at: datetime


class AuthResponse(BaseModel):
access_token: str
token_type: str = "bearer"
user: UserOut
Loading
Loading