RegWatch backend : FastAPI + Postgres partagé + Docker/CI homelab
Build & Deploy / build (push) Successful in 23s

- SECRET_KEY et DATABASE_URL externalisés (Vault -> env), plus de secret en dur
- SQLite -> postgres-shared (psycopg2), schéma créé au boot (entrypoint.sh)
- Dockerfile + docker-compose (Watchtower) + .gitea/workflows/build.yml

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-24 18:43:17 +02:00
co-authored by Claude Opus 4.8
commit 3d007c7013
18 changed files with 924 additions and 0 deletions
View File
+61
View File
@@ -0,0 +1,61 @@
from datetime import datetime, timedelta, timezone
from typing import Optional
import os
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
# Secret JWT injecté par l'env (Vault -> .env). Repli dev uniquement ; en prod SECRET_KEY est obligatoire.
SECRET_KEY = os.getenv("SECRET_KEY", "dev-only-insecure-secret-change-me")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
email: Optional[str] = None
class UserLogin(BaseModel):
email: str
password: str
class UserCreate(BaseModel):
email: str
password: str
full_name: Optional[str] = None
class UserResponse(BaseModel):
id: int
email: str
full_name: Optional[str] = None
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def decode_token(token: str) -> Optional[str]:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
email: str = payload.get("sub")
if email is None:
return None
return email
except JWTError:
return None
+141
View File
@@ -0,0 +1,141 @@
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from datetime import timedelta
from database.database import SessionLocal
from database.models import Document, User
from api.auth import (
UserLogin, UserCreate, Token, UserResponse, get_password_hash, verify_password,
create_access_token, decode_token, ACCESS_TOKEN_EXPIRE_MINUTES
)
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
def root():
return {"message": "RegWatch API running"}
@app.post("/test-login")
def test_login(credentials: UserLogin):
return {"received": credentials.dict()}
@app.post("/signup", response_model=UserResponse)
def signup(user: UserCreate):
db = SessionLocal()
existing_user = db.query(User).filter(User.email == user.email).first()
if existing_user:
db.close()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
hashed_password = get_password_hash(user.password)
db_user = User(
email=user.email,
password=hashed_password,
full_name=user.full_name
)
db.add(db_user)
db.commit()
db.refresh(db_user)
db.close()
return UserResponse(
id=db_user.id,
email=db_user.email,
full_name=db_user.full_name
)
@app.post("/login", response_model=Token)
def login(credentials: UserLogin):
try:
db = SessionLocal()
user = db.query(User).filter(User.email == credentials.email).first()
db.close()
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found"
)
if not verify_password(credentials.password, user.password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid password"
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.email}, expires_delta=access_token_expires
)
return Token(access_token=access_token, token_type="bearer")
except HTTPException:
raise
except Exception as e:
print(f"ERROR in login: {str(e)}")
import traceback
traceback.print_exc()
raise HTTPException(
status_code=500,
detail=f"Server error: {str(e)}"
)
@app.get("/me", response_model=UserResponse)
def get_current_user(token: str):
email = decode_token(token)
if not email:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
db = SessionLocal()
user = db.query(User).filter(User.email == email).first()
db.close()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return UserResponse(
id=user.id,
email=user.email,
full_name=user.full_name
)
@app.get("/documents")
def get_documents():
db = SessionLocal()
documents = db.query(Document).all()
result = []
for doc in documents:
result.append({
"id": doc.id,
"title": doc.title,
"ingredient": doc.ingredient,
"source": doc.source,
"type": doc.document_type,
"date": doc.meeting_date,
"pdf_url": doc.pdf_url
})
db.close()
return result