add postgres

This commit is contained in:
2026-04-07 23:48:47 +03:00
parent 4fff4bebaf
commit 3d29e1f84e
8 changed files with 326 additions and 12 deletions

67
backend/auth.py Normal file
View File

@@ -0,0 +1,67 @@
from __future__ import annotations
import os
from datetime import datetime, timedelta, timezone
from fastapi import Depends, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from backend.db import get_db
from backend.models import User
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
bearer = HTTPBearer(auto_error=False)
def _env(name: str, default: str) -> str:
value = os.getenv(name)
return value if value else default
JWT_SECRET = _env("JWT_SECRET", "change-me")
JWT_ALG = _env("JWT_ALG", "HS256")
JWT_EXPIRES_MINUTES = int(_env("JWT_EXPIRES_MINUTES", "480")) # 8h
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(password: str, password_hash: str) -> bool:
return pwd_context.verify(password, password_hash)
def create_access_token(*, user_id: str) -> str:
now = datetime.now(timezone.utc)
payload = {
"sub": user_id,
"iat": int(now.timestamp()),
"exp": int((now + timedelta(minutes=JWT_EXPIRES_MINUTES)).timestamp()),
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALG)
def get_current_user(
creds: HTTPAuthorizationCredentials | None = Depends(bearer),
db: Session = Depends(get_db),
) -> User:
if creds is None or not creds.credentials:
raise HTTPException(status_code=401, detail="Not authenticated")
token = creds.credentials
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALG])
user_id = payload.get("sub")
if not user_id:
raise HTTPException(status_code=401, detail="Invalid token")
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
user = db.get(User, user_id)
if not user or not user.is_active:
raise HTTPException(status_code=401, detail="User inactive or not found")
return user

35
backend/db.py Normal file
View File

@@ -0,0 +1,35 @@
from __future__ import annotations
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
def _env(name: str, default: str) -> str:
value = os.getenv(name)
return value if value else default
def build_database_url() -> str:
host = _env("POSTGRES_HOST", "10.10.10.167")
port = _env("POSTGRES_PORT", "5432")
db = _env("POSTGRES_DB", "devops_portal")
user = _env("POSTGRES_USER", "devops_portal")
password = _env("POSTGRES_PASSWORD", "change-me")
return f"postgresql+psycopg://{user}:{password}@{host}:{port}/{db}"
DATABASE_URL = _env("DATABASE_URL", build_database_url())
engine = create_engine(DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

View File

@@ -4,8 +4,10 @@ import os
import requests
import yaml
from fastapi import FastAPI, HTTPException
from fastapi import Depends, FastAPI, HTTPException
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.orm import Session
app = FastAPI(title="AI CI/CD Assistant")
@@ -17,6 +19,11 @@ def _env(name: str, default: str) -> str:
# Example for local Ollama: http://localhost:11434/api/generate
OLLAMA_API_URL = _env("OLLAMA_API_URL", "http://host.docker.internal:11434/api/generate")
REQUIRE_AUTH_FOR_GENERATE = _env("REQUIRE_AUTH_FOR_GENERATE", "true").lower() in ("1", "true", "yes", "on")
from backend.auth import create_access_token, get_current_user, hash_password, verify_password # noqa: E402
from backend.db import engine, get_db # noqa: E402
from backend.models import Base, User # noqa: E402
class GenerationRequest(BaseModel):
@@ -38,8 +45,69 @@ async def health():
return {"status": "ok"}
@app.on_event("startup")
def _startup():
Base.metadata.create_all(bind=engine)
class RegisterRequest(BaseModel):
email: str
password: str
full_name: str | None = None
class LoginRequest(BaseModel):
email: str
password: str
@app.post("/auth/register")
def register(req: RegisterRequest, db: Session = Depends(get_db)):
email = req.email.strip().lower()
if not email or not req.password:
raise HTTPException(status_code=400, detail="Email and password are required")
exists = db.execute(select(User).where(User.email == email)).scalar_one_or_none()
if exists:
raise HTTPException(status_code=409, detail="Email already registered")
user = User(
email=email,
full_name=req.full_name.strip() if req.full_name else None,
password_hash=hash_password(req.password),
is_active=True,
is_admin=False,
)
db.add(user)
db.commit()
db.refresh(user)
return {"id": str(user.id), "email": user.email, "full_name": user.full_name, "is_admin": user.is_admin}
@app.post("/auth/login")
def login(req: LoginRequest, db: Session = Depends(get_db)):
email = req.email.strip().lower()
user = db.execute(select(User).where(User.email == email)).scalar_one_or_none()
if not user or not verify_password(req.password, user.password_hash):
raise HTTPException(status_code=401, detail="Invalid credentials")
if not user.is_active:
raise HTTPException(status_code=403, detail="User is inactive")
token = create_access_token(user_id=str(user.id))
return {"access_token": token, "token_type": "bearer"}
@app.get("/auth/me")
def me(user: User = Depends(get_current_user)):
return {"id": str(user.id), "email": user.email, "full_name": user.full_name, "is_admin": user.is_admin}
@app.post("/generate")
async def generate_pipeline(req: GenerationRequest):
async def generate_pipeline(
req: GenerationRequest,
user: User | None = Depends(get_current_user) if REQUIRE_AUTH_FOR_GENERATE else None,
):
prompt = (
f"Write only a valid {req.platform} YAML configuration for: {req.description}. "
"No explanations, just code."

29
backend/models.py Normal file
View File

@@ -0,0 +1,29 @@
from __future__ import annotations
import uuid
from datetime import datetime, timezone
from sqlalchemy import Boolean, DateTime, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
def _now():
return datetime.now(timezone.utc)
class User(Base):
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email: Mapped[str] = mapped_column(String(320), unique=True, index=True, nullable=False)
full_name: Mapped[str | None] = mapped_column(String(200), nullable=True)
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
is_admin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now, nullable=False)

View File

@@ -2,3 +2,12 @@ fastapi
uvicorn[standard]
requests
pyyaml
sqlalchemy
psycopg[binary]
passlib[bcrypt]
python-jose[cryptography]
pydantic[email]
fastapi
uvicorn[standard]
requests
pyyaml