add project base

This commit is contained in:
Julien Aldon
2026-02-09 17:39:09 +01:00
commit 145f3f632e
43 changed files with 1045 additions and 0 deletions

4
backend/src/__about__.py Normal file
View File

@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT
__version__ = "0.0.1"

3
backend/src/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT

80
backend/src/auth/auth.py Normal file
View File

@@ -0,0 +1,80 @@
from fastapi import APIRouter, Security, HTTPException
from fastapi.responses import RedirectResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from src.secrets import CLIENT_ID, REDIRECT_URI, AUTH_URL, CLIENT_SECRET, TOKEN_URL, JWKS_URL, ISSUER
import secrets
import jwt
from jwt import PyJWKClient
import requests
router = APIRouter(prefix="/auth")
jwk_client = PyJWKClient(JWKS_URL)
security = HTTPBearer()
@router.get('/login')
def login():
state = secrets.token_urlsafe(16)
params = {
"client_id": CLIENT_ID,
"response_type": "code",
"scope": "openid",
"redirect_uri": REDIRECT_URI,
"state": state,
}
request_url = requests.Request('GET', AUTH_URL, params=params).prepare().url
return RedirectResponse(request_url)
@router.get("/callback")
def callback(code: str):
data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
}
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
response = requests.post(TOKEN_URL, data=data, headers=headers)
if response.status_code != 200:
return JSONResponse(
{"error": "Failed to get token"},
status_code=400
)
token_data = response.json()
return {
"access_token": token_data["access_token"],
"id_token": token_data["id_token"],
"refresh_token": token_data["refresh_token"],
}
def verify_token(token: str):
try:
signing_key = jwk_client.get_signing_key_from_jwt(token)
decoded = jwt.decode(token, options={"verify_signature": False})
print(decoded, ISSUER)
print(decoded["exp"])
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=CLIENT_ID,
issuer=ISSUER,
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
def get_current_user(
credentials: HTTPAuthorizationCredentials = Security(security)
):
return verify_token(credentials.credentials)

View File

@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT

View File

@@ -0,0 +1,3 @@
from fastapi import APIRouter
router = APIRouter(prefix='/contracts')

9
backend/src/database.py Normal file
View File

@@ -0,0 +1,9 @@
from sqlmodel import create_engine, SQLModel, Session
from src.secrets import dbname, dbhost, dbuser, dbpass
engine = create_engine(f'postgresql://{dbuser}:{dbpass}@{dbhost}:54321/{dbname}')
# SQLModel.metadata.create_all(engine)
def get_session():
with Session(engine) as session:
yield session

View File

@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT

View File

@@ -0,0 +1,19 @@
from fastapi import APIRouter
router = APIRouter(prefix='/forms')
@router.get('/')
def get_forms():
return []
@router.post('/')
def create_form():
return {}
@router.put('/')
def update_form():
return {}
@router.delete('/')
def delete_form():
return {}

View File

@@ -0,0 +1,10 @@
from sqlmodel import Field, SQLModel
form src.productors.model import Productor
class Form(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
productor_id: int | None = Field(default=None, foreign_key="productor.id")
shipment_number: int
season: str

36
backend/src/main.py Normal file
View File

@@ -0,0 +1,36 @@
from sqlmodel import SQLModel
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from src.templates.templates import router as template_router
from src.contracts.contracts import router as contracts_router
from src.forms.forms import router as forms_router
from src.productors.productors import router as productors_router
from src.products.products import router as products_router
from src.users.users import router as users_router
from src.auth.auth import router as auth_router
from src.secrets import origins
from src.database import engine
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=['x-nbpage']
)
app.include_router(template_router)
app.include_router(contracts_router)
app.include_router(forms_router)
app.include_router(productors_router)
app.include_router(products_router)
app.include_router(users_router)
app.include_router(auth_router)
SQLModel.metadata.create_all(engine)

1
backend/src/messages.py Normal file
View File

@@ -0,0 +1 @@
notfound = "Resource was not found."

100
backend/src/models.py Normal file
View File

@@ -0,0 +1,100 @@
from sqlmodel import Field, SQLModel, Relationship
from enum import Enum
from typing import Optional
import datetime
class Unit(Enum):
GRAMS = 1
KILO = 2
class ProductBase(SQLModel):
name: str
unit: Unit
price: float
price_kg: float | None
weight: float
productor_id: int | None = Field(default=None, foreign_key="productor.id")
class ProductPublic(ProductBase):
id: int
class Product(ProductBase, table=True):
id: int | None = Field(default=None, primary_key=True)
productor: Optional['Productor'] = Relationship(back_populates="products")
class ProductUpdate(SQLModel):
name: str | None
unit: Unit | None
price: float | None
price_kg: float | None
weight: float | None
productor_id: int | None = Field(default=None, foreign_key="productor.id")
class ProductCreate(ProductBase):
pass
class ProductorBase(SQLModel):
name: str
address: str
payment: str
class ProductorPublic(ProductorBase):
id: int
products: list[Product] = []
class Productor(ProductorBase, table=True):
id: int | None = Field(default=None, primary_key=True)
products: list[Product] = Relationship(back_populates='productor')
class ProductorUpdate(SQLModel):
name: str | None
address: str | None
payment: str | None
class ProductorCreate(ProductorBase):
pass
class FormBase(SQLModel):
productor_id: int | None = Field(default=None, foreign_key="productor.id")
referer_id: int | None = Field(default=None, foreign_key="referer.id")
season: str
shipments: int
start: datetime.date
end: datetime.date
class FormPublic(FormBase):
id: int
class Form(FormBase, table=True):
id: int | None = Field(default=None, primary_key=True)
class FormUpdate(SQLModel):
productor_id: int | None = Field(default=None, foreign_key="productor.id")
referer_id: int | None = Field(default=None, foreign_key="user.id")
season: str | None
shipments: int | None
start: datetime.date | None
end: datetime.date | None
class FormCreate(FormBase):
pass
class UserBase(SQLModel):
name: str
email: str
class UserPublic(UserBase):
id: int
class User(UserBase, table=True):
id: int | None = Field(default=None, primary_key=True)
class UserUpdate(SQLModel):
name: str | None
email: str | None
class UserCreate(UserBase):
pass

View File

@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT

View File

@@ -0,0 +1,37 @@
from fastapi import APIRouter, HTTPException, Depends
import src.messages as messages
import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.productors.service as service
router = APIRouter(prefix='/productors')
@router.get('/', response_model=list[models.ProductorPublic])
def get_productors(session: Session = Depends(get_session)):
return service.get_all(session)
@router.get('/{id}', response_model=models.ProductorPublic)
def get_productors(id: int, session: Session = Depends(get_session)):
result = service.get_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result
@router.post('/', response_model=models.ProductorPublic)
def create_productor(productor: models.ProductorCreate, session: Session = Depends(get_session)):
return service.create_one(session, productor)
@router.put('/{id}', response_model=models.ProductorPublic)
def update_productor(id: int, productor: models.ProductorUpdate, session: Session = Depends(get_session)):
result = service.update_one(session, id, productor)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result
@router.delete('/{id}', response_model=models.ProductorPublic)
def delete_productor(id: int, session: Session = Depends(get_session)):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result

View File

@@ -0,0 +1,41 @@
from sqlmodel import Session, select
from src.models import Productor
def get_all(session: Session) -> list[Productor]:
statement = select(Productor)
return session.exec(statement).all()
def get_one(session: Session, productor_id: int) -> Productor:
return session.get(Productor, productor_id)
def create_one(session: Session, productor: Productor) -> Productor:
productor_create = productor.model_dump(exclude_unset=True)
new_productor = Productor(**productor_create)
session.add(new_productor)
session.commit()
session.refresh(new_productor)
return new_productor
def update_one(session: Session, id: int, productor: Productor) -> Productor:
statement = select(Productor).where(Productor.id == id)
result = session.exec(statement)
new_productor = result.first()
if not new_productor:
return None
productor_updates = productor.model_dump(exclude_unset=True)
for key, value in productor_updates.items():
setattr(new_productor, key, value)
session.add(new_productor)
session.commit()
session.refresh(new_productor)
return new_productor
def delete_one(session: Session, id: int) -> Productor:
statement = select(Productor).where(Productor.id == id)
result = session.exec(statement)
productor = result.first()
if not productor:
return None
session.delete(productor)
session.commit()
return productor

View File

@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT

View File

@@ -0,0 +1,37 @@
from fastapi import APIRouter, HTTPException, Depends
import src.messages as messages
import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.products.service as service
from src.auth.auth import get_current_user
router = APIRouter(prefix='/products')
@router.get('/', response_model=list[models.ProductPublic], )
def get_products(session: Session = Depends(get_session), user=Depends(get_current_user)):
return service.get_all(session)
@router.get('/{id}', response_model=models.ProductPublic)
def get_product(id: int, session: Session = Depends(get_session)):
result = service.get_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result
@router.post('/', response_model=models.ProductPublic)
def create_product(product: models.ProductCreate, session: Session = Depends(get_session)):
return service.create_one(session, product)
@router.put('/{id}', response_model=models.ProductPublic)
def update_product(id: int, product: models.ProductUpdate, session: Session = Depends(get_session)):
result = service.update_one(session, id, product)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result
@router.delete('/{id}', response_model=models.ProductPublic)
def delete_product(id: int, session: Session = Depends(get_session)):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result

View File

@@ -0,0 +1,41 @@
from sqlmodel import Session, select
from src.models import Product
def get_all(session: Session) -> list[Product]:
statement = select(Product)
return session.exec(statement).all()
def get_one(session: Session, product_id: int) -> Product:
return session.get(Product, product_id)
def create_one(session: Session, product: Product) -> Product:
product_create = product.model_dump(exclude_unset=True)
new_product = Product(**product_create)
session.add(new_product)
session.commit()
session.refresh(new_product)
return new_product
def update_one(session: Session, id: int, product: Product) -> Product:
statement = select(Product).where(Product.id == id)
result = session.exec(statement)
new_product = result.first()
if not new_product:
return None
product_updates = product.model_dump(exclude_unset=True)
for key, value in product_updates.items():
setattr(new_product, key, value)
session.add(new_product)
session.commit()
session.refresh(new_product)
return new_product
def delete_one(session: Session, id: int) -> Product:
statement = select(Product).where(Product.id == id)
result = session.exec(statement)
product = result.first()
if not product:
return None
session.delete(product)
session.commit()
return product

29
backend/src/secrets.py Normal file
View File

@@ -0,0 +1,29 @@
import os
origins = [
os.environ.get('SERVICE_ORIGIN') or 'http://localhost'
]
dbhost = os.environ.get('DB_HOST') or 'localhost'
dbuser = os.environ.get('PGSQL_USER') or 'postgres'
dbpass = os.environ.get('PGSQL_PASSWORD') or 'postgres'
dbname = os.environ.get('PGSQL_DATABASE') or 'amap'
# openssl rand -hex 32
SECRET_KEY = os.environ.get('SERVICE_SECRET_KEY') or 'test'
ALGORITHM = 'HS256'
ACCESS_TOKEN_EXPIRE_MINUTES = 600
KEYCLOAK_SERVER = ""
REALM = ""
CLIENT_ID = ""
CLIENT_SECRET = ""
REDIRECT_URI = "http://localhost:8000/auth/callback"
AUTH_URL = f"{KEYCLOAK_SERVER}/realms/{REALM}/protocol/openid-connect/auth"
TOKEN_URL = f"{KEYCLOAK_SERVER}/realms/{REALM}/protocol/openid-connect/token"
ISSUER = f"{KEYCLOAK_SERVER}/realms/{REALM}"
JWKS_URL = f"{ISSUER}/protocol/openid-connect/certs"

View File

@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT

View File

@@ -0,0 +1,19 @@
from fastapi import APIRouter
router = APIRouter(prefix='/templates')
@router.get('/')
def get_templates():
return []
@router.post('/')
def create_template():
return {}
@router.put('/')
def update_template():
return {}
@router.delete('/')
def delete_template():
return {}

View File

@@ -0,0 +1,19 @@
from fastapi import APIRouter
router = APIRouter(prefix='/users')
@router.get('/')
def get_users():
return []
@router.post('/')
def create_user():
return {}
@router.put('/')
def update_user():
return {}
@router.delete('/')
def delete_user():
return {}