add crud for forms, templates, shipment, users and auth with keycloak

This commit is contained in:
2026-02-09 23:38:22 +01:00
parent 145f3f632e
commit be7ca58513
45 changed files with 949 additions and 226 deletions

View File

@@ -1,4 +1,12 @@
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=amap
DB_USER=postgres
DB_PASS=postgres
DB_NAME=amap
DB_HOST=localhost
ORIGINS=http://localhost:8000
SECRET_KEY=
ROOT_FQDN=http://localhost
KEYCLOAK_SERVER=
KEYCLOAK_REALM=
KEYCLOAK_CLIENT_ID=
KEYCLOAK_CLIENT_SECRET=
KEYCLOAK_REDIRECT_URI=

View File

@@ -1,9 +1,5 @@
auth {
mode: bearer
}
auth:bearer {
token: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJtNUtSQkp1T3VqMnFiUElySlRldFVISGVWMWRTLTEzUG5saU1PSWRLcWFvIn0.eyJleHAiOjE3NzA2NTQwMzYsImlhdCI6MTc3MDY1MzczNiwiYXV0aF90aW1lIjoxNzcwNjUzMDU2LCJqdGkiOiJvbnJ0YWM6YmFiMDZiNGMtMjM5ZC00NzM3LTliYWEtNjE2MjBmMzVjM2NhIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5hbGRvbi5mci9yZWFsbXMvYWxkb24uZnIiLCJhdWQiOlsibmV4dGNsb3VkIiwiYWNjb3VudCJdLCJzdWIiOiJlM2VkN2NiNC1iMDYxLTRiZWQtYTY5YS1lMGQ5ZWJmNDJhZWIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhbWFwIiwic2lkIjoiMTkwY2I4YzAtMGYxZC00MWJiLTkwYWYtZjNmMGJkZWMzYjA3IiwiYWNyIjoiMCIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjgwMDAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtZ2lyYXNvbCIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJuZXh0Y2xvdWQiOnsicm9sZXMiOlsicGVsYXJnbyIsImZlZmFuIiwiYWRtaW4iLCJnaXJhc29sIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiSnVsaWVuIEFsZG9uIiwicHJlZmVycmVkX3VzZXJuYW1lIjoianVsaWVuIiwiZ2l2ZW5fbmFtZSI6Ikp1bGllbiIsImZhbWlseV9uYW1lIjoiQWxkb24iLCJlbWFpbCI6Imp1bGllbi5hbGRvbkB3YW5hZG9vLmZyIn0.bq-EUtK_UqsIOwI6KDHB8eELMirWPDfTMta904XNeffj_v_ptEnHbecCf1OG6zzwanrBUyl_On7z95zVvVuKX6fQM9iaqxDqm7VlAK1O6n97367evTjQTOggkl3eTgX3xkfbCjJyzP_8RhTPXBsL_Nao8h5kgCnDwUHKEZ547oeoPKVEzlc82SgPi2rsiTVyvznJxGyJkQOTcDDMqTUxj4OVqWD5FMEDCfLnisUNPADhq0Umyw8hU4YwtI1-3hn6aXbnVcDekk2oWVli_6MeJHyejI8_yPnnQMvcp9OqciXRMtCGml1vMHcb5kUh4U9OeAhewzBFb_Mk9KDOspktSQ
mode: none
}
vars:pre-request {

View File

@@ -0,0 +1,20 @@
meta {
name: Create
type: http
seq: 1
}
post {
url: {{Service}}/{{Route}}
body: json
auth: inherit
}
body:json {
{{ExamplePOSTBody}}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,16 @@
meta {
name: Delete one
type: http
seq: 2
}
delete {
url: {{Service}}/{{Route}}/2
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,16 @@
meta {
name: Get all
type: http
seq: 2
}
get {
url: {{Service}}/{{Route}}
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,16 @@
meta {
name: Get one
type: http
seq: 2
}
get {
url: {{Service}}/{{Route}}/1
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,20 @@
meta {
name: Update one
type: http
seq: 2
}
put {
url: {{Service}}/{{Route}}/1
body: json
auth: inherit
}
body:json {
{{ExamplePUTBody}}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,31 @@
meta {
name: forms
}
auth {
mode: inherit
}
vars:pre-request {
Route: forms
ExamplePOSTBody: '''
{
"productor_id": 1,
"referer_id": 1,
"season": "Hiver-2026",
"shipments": 5,
"start": "2026-01-10",
"end": "2026-05-10"
}
'''
ExamplePUTBody: '''
{
"productor_id": 1,
"referer_id": 1,
"season": "updatedHiver-2026",
"shipments": 7,
"start": "2026-01-10",
"end": "2026-05-10"
}
'''
}

View File

@@ -5,7 +5,7 @@ meta {
}
delete {
url: {{Service}}/{{Route}}/2
url: {{Service}}/{{Route}}/6
body: none
auth: inherit
}

View File

@@ -5,7 +5,7 @@ meta {
}
put {
url: {{Service}}/{{Route}}/1
url: {{Service}}/{{Route}}/3
body: json
auth: inherit
}

View File

@@ -8,6 +8,28 @@ auth {
vars:pre-request {
Route: products
ExamplePOSTBody: {"name": "test", "unit": "KILO", "price": 3.50, "price_kg": 3.50, "weight": "1.0", "productor_id": 1}
ExamplePUTBody: {"name": "updatetestt", "address": "updatetestt"}
ExamplePOSTBody: '''
{
"name": "test",
"unit": 1,
"price": 3.50,
"price_kg": 3.50,
"weight": "1.0",
"productor_id": 1,
"type": 2,
"shipment_ids": []
}
'''
ExamplePUTBody: '''
{
"name": "test",
"unit": 1,
"price": 3.50,
"price_kg": 3.50,
"weight": "1.0",
"productor_id": 1,
"type": 2,
"shipment_ids": [1]
}
'''
}

View File

@@ -0,0 +1,20 @@
meta {
name: Create
type: http
seq: 1
}
post {
url: {{Service}}/{{Route}}
body: json
auth: inherit
}
body:json {
{{ExamplePOSTBody}}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,16 @@
meta {
name: Delete one
type: http
seq: 2
}
delete {
url: {{Service}}/{{Route}}/2
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,16 @@
meta {
name: Get all
type: http
seq: 2
}
get {
url: {{Service}}/{{Route}}
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,16 @@
meta {
name: Get one
type: http
seq: 2
}
get {
url: {{Service}}/{{Route}}/1
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,20 @@
meta {
name: Update one
type: http
seq: 2
}
put {
url: {{Service}}/{{Route}}/1
body: json
auth: inherit
}
body:json {
{{ExamplePUTBody}}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,26 @@
meta {
name: shipments
}
auth {
mode: inherit
}
vars:pre-request {
Route: shipments
ExamplePOSTBody: '''
{
"name": "test",
"date": "2026-01-10",
"product_ids": [1],
"form_id": 3
}
'''
ExamplePUTBody: '''
{
"name": "updatedtestt",
"date": "2026-01-10",
"product_ids": [2]
}
'''
}

View File

@@ -0,0 +1,20 @@
meta {
name: Create
type: http
seq: 1
}
post {
url: {{Service}}/{{Route}}
body: json
auth: inherit
}
body:json {
{{ExamplePOSTBody}}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,16 @@
meta {
name: Delete one
type: http
seq: 2
}
delete {
url: {{Service}}/{{Route}}/2
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,16 @@
meta {
name: Get all
type: http
seq: 2
}
get {
url: {{Service}}/{{Route}}
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,16 @@
meta {
name: Get one
type: http
seq: 2
}
get {
url: {{Service}}/{{Route}}/1
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,20 @@
meta {
name: Update one
type: http
seq: 2
}
put {
url: {{Service}}/{{Route}}/1
body: json
auth: inherit
}
body:json {
{{ExamplePUTBody}}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,13 @@
meta {
name: users
}
auth {
mode: inherit
}
vars:pre-request {
Route: users
ExamplePOSTBody: {"name": "test", "email": "test@test.test"}
ExamplePUTBody: {"name": "updatedtest", "email": "updatedtest@test.test"}
}

View File

@@ -22,9 +22,9 @@ classifiers = [
"Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = [
"fastapi",
"fastapi[standard]",
"sqlmodel",
"psycopg2",
"psycopg2-binary",
"PyJWT",
"cryptography",
"requests"
@@ -43,7 +43,7 @@ extra-dependencies = [
"mypy>=1.0.0",
]
[tool.hatch.envs.types.scripts]
check = "mypy --install-types --non-interactive {args:src/backend tests}"
check = "mypy --install-types --non-interactive {args:src tests}"
[tool.coverage.run]
source_pkgs = ["backend", "tests"]
@@ -63,3 +63,7 @@ exclude_lines = [
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
[tool.hatch.build.targets.wheel]
packages = ["src"]
include = ["src/**/*.py"]

View File

@@ -1,7 +1,13 @@
from fastapi import APIRouter, Security, HTTPException
from fastapi import APIRouter, Security, HTTPException, Depends
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
from sqlmodel import Session
from src.settings import AUTH_URL, TOKEN_URL, JWKS_URL, ISSUER, settings
import src.users.service as service
from src.database import get_session
from src.models import UserCreate
import secrets
import jwt
from jwt import PyJWKClient
@@ -15,26 +21,24 @@ security = HTTPBearer()
@router.get('/login')
def login():
state = secrets.token_urlsafe(16)
params = {
"client_id": CLIENT_ID,
"client_id": settings.keycloak_client_id,
"response_type": "code",
"scope": "openid",
"redirect_uri": REDIRECT_URI,
"redirect_uri": settings.keycloak_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):
def callback(code: str, session: Session = Depends(get_session)):
data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"redirect_uri": settings.keycloak_redirect_uri,
"client_id": settings.keycloak_client_id,
"client_secret": settings.keycloak_client_secret,
}
headers = {
"Content-Type": "application/x-www-form-urlencoded"
@@ -45,7 +49,17 @@ def callback(code: str):
{"error": "Failed to get token"},
status_code=400
)
token_data = response.json()
id_token = token_data["id_token"]
decoded_token = jwt.decode(id_token, options={"verify_signature": False})
user_create = UserCreate(
email=decoded_token.get("email"),
name=decoded_token.get("preferred_username")
)
print(user_create)
user = service.get_or_create_user(session, user_create)
return {
"access_token": token_data["access_token"],
"id_token": token_data["id_token"],
@@ -56,20 +70,16 @@ 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,
audience=settings.keycloak_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")

View File

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

View File

@@ -1,19 +1,37 @@
from fastapi import APIRouter
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.forms.service as service
router = APIRouter(prefix='/forms')
@router.get('/')
def get_forms():
return []
@router.get('/', response_model=list[models.FormPublic])
def get_forms(session: Session = Depends(get_session)):
return service.get_all(session)
@router.post('/')
def create_form():
return {}
@router.get('/{id}', response_model=models.FormPublic)
def get_forms(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.put('/')
def update_form():
return {}
@router.post('/', response_model=models.FormPublic)
def create_form(form: models.FormCreate, session: Session = Depends(get_session)):
return service.create_one(session, form)
@router.delete('/')
def delete_form():
return {}
@router.put('/{id}', response_model=models.FormPublic)
def update_form(id: int, form: models.FormUpdate, session: Session = Depends(get_session)):
result = service.update_one(session, id, form)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result
@router.delete('/{id}', response_model=models.FormPublic)
def delete_form(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

@@ -1,10 +0,0 @@
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

View File

@@ -0,0 +1,42 @@
from sqlmodel import Session, select
import src.models as models
def get_all(session: Session) -> list[models.FormPublic]:
statement = select(models.Form)
return session.exec(statement).all()
def get_one(session: Session, form_id: int) -> models.FormPublic:
return session.get(models.Form, form_id)
def create_one(session: Session, form: models.FormCreate) -> models.FormPublic:
form_create = form.model_dump(exclude_unset=True)
new_form = models.Form(**form_create)
session.add(new_form)
session.commit()
session.refresh(new_form)
return new_form
def update_one(session: Session, id: int, form: models.FormUpdate) -> models.FormPublic:
statement = select(models.Form).where(models.Form.id == id)
result = session.exec(statement)
new_form = result.first()
if not new_form:
return None
form_updates = form.model_dump(exclude_unset=True)
for key, value in form_updates.items():
setattr(new_form, key, value)
session.add(new_form)
session.commit()
session.refresh(new_form)
return new_form
def delete_one(session: Session, id: int) -> models.FormPublic:
statement = select(models.Form).where(models.Form.id == id)
result = session.exec(statement)
form = result.first()
if not form:
return None
result = models.FormPublic.model_validate(form)
session.delete(form)
session.commit()
return result

View File

@@ -10,14 +10,15 @@ 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.shipments.shipments import router as shipment_router
from src.settings import settings
from src.database import engine
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_origins=[settings.origins],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -32,5 +33,6 @@ app.include_router(productors_router)
app.include_router(products_router)
app.include_router(users_router)
app.include_router(auth_router)
app.include_router(shipment_router)
SQLModel.metadata.create_all(engine)

View File

@@ -3,85 +3,6 @@ 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
@@ -98,3 +19,149 @@ class UserUpdate(SQLModel):
class UserCreate(UserBase):
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 Unit(Enum):
GRAMS = 1
KILO = 2
class ProductType(Enum):
PLANNED = 1
RECCURENT = 2
class ShipmentProductLink(SQLModel, table=True):
shipment_id: Optional[int] = Field(default=None, foreign_key="shipment.id", primary_key=True)
product_id: Optional[int] = Field(default=None, foreign_key="product.id", primary_key=True)
class ProductBase(SQLModel):
name: str
unit: Unit
price: float
price_kg: float | None
weight: float
type: ProductType
productor_id: int | None = Field(default=None, foreign_key="productor.id")
class ProductPublic(ProductBase):
id: int
productor: Productor | None
shipments: list["Shipment"] | None
class Product(ProductBase, table=True):
id: int | None = Field(default=None, primary_key=True)
shipments: list["Shipment"] = Relationship(back_populates="products", link_model=ShipmentProductLink)
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
shipment_ids: list[int] | None
class ProductCreate(ProductBase):
shipment_ids: list[int] | None
class FormBase(SQLModel):
name: str
productor_id: int | None = Field(default=None, foreign_key="productor.id")
referer_id: int | None = Field(default=None, foreign_key="user.id")
season: str
start: datetime.date
end: datetime.date
class FormPublic(FormBase):
id: int
productor: ProductorPublic | None
referer: User
shipments: list["Shipment"] = []
class Form(FormBase, table=True):
id: int | None = Field(default=None, primary_key=True)
productor: Optional['Productor'] = Relationship()
referer: Optional['User'] = Relationship()
shipments: list["Shipment"] = Relationship()
class FormUpdate(SQLModel):
productor_id: int | None
referer_id: int | None
season: str | None
start: datetime.date | None
end: datetime.date | None
class FormCreate(FormBase):
pass
class TemplateBase(SQLModel):
pass
class TemplatePublic(TemplateBase):
id: int
class Template(TemplateBase, table=True):
id: int | None = Field(default=None, primary_key=True)
class TemplateUpdate(SQLModel):
pass
class TemplateCreate(TemplateBase):
pass
class ContractBase(SQLModel):
pass
class ContractPublic(ContractBase):
id: int
class Contract(ContractBase, table=True):
id: int | None = Field(default=None, primary_key=True)
class ContractUpdate(SQLModel):
pass
class ContractCreate(ContractBase):
pass
class ShipmentBase(SQLModel):
name: str
date: datetime.date
form_id: int | None = Field(default=None, foreign_key="form.id")
class ShipmentPublic(ShipmentBase):
id: int
products: list[Product] = []
class Shipment(ShipmentBase, table=True):
id: int | None = Field(default=None, primary_key=True)
products: list[Product] = Relationship(back_populates="shipments", link_model=ShipmentProductLink)
class ShipmentUpdate(SQLModel):
name: str | None
date: str | None
product_ids: list[int]
class ShipmentCreate(ShipmentBase):
product_ids: list[int]

View File

@@ -1,23 +1,23 @@
from sqlmodel import Session, select
from src.models import Productor
import src.models as models
def get_all(session: Session) -> list[Productor]:
statement = select(Productor)
def get_all(session: Session) -> list[models.ProductorPublic]:
statement = select(models.Productor)
return session.exec(statement).all()
def get_one(session: Session, productor_id: int) -> Productor:
return session.get(Productor, productor_id)
def get_one(session: Session, productor_id: int) -> models.ProductorPublic:
return session.get(models.Productor, productor_id)
def create_one(session: Session, productor: Productor) -> Productor:
def create_one(session: Session, productor: models.ProductorCreate) -> models.ProductorPublic:
productor_create = productor.model_dump(exclude_unset=True)
new_productor = Productor(**productor_create)
new_productor = models.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)
def update_one(session: Session, id: int, productor: models.ProductorUpdate) -> models.ProductorPublic:
statement = select(models.Productor).where(models.Productor.id == id)
result = session.exec(statement)
new_productor = result.first()
if not new_productor:
@@ -30,12 +30,13 @@ def update_one(session: Session, id: int, productor: Productor) -> Productor:
session.refresh(new_productor)
return new_productor
def delete_one(session: Session, id: int) -> Productor:
statement = select(Productor).where(Productor.id == id)
def delete_one(session: Session, id: int) -> models.ProductorPublic:
statement = select(models.Productor).where(models.Productor.id == id)
result = session.exec(statement)
productor = result.first()
if not productor:
return None
result = models.ProductorPublic.model_validate(productor)
session.delete(productor)
session.commit()
return productor
return result

View File

@@ -6,9 +6,9 @@ from sqlmodel import Session
import src.products.service as service
from src.auth.auth import get_current_user
router = APIRouter(prefix='/products')
#user=Depends(get_current_user)
@router.get('/', response_model=list[models.ProductPublic], )
def get_products(session: Session = Depends(get_session), user=Depends(get_current_user)):
def get_products(session: Session = Depends(get_session)):
return service.get_all(session)
@router.get('/{id}', response_model=models.ProductPublic)

View File

@@ -1,41 +1,51 @@
from sqlmodel import Session, select
from src.models import Product
import src.models as models
def get_all(session: Session) -> list[Product]:
statement = select(Product)
def get_all(session: Session) -> list[models.ProductPublic]:
statement = select(models.Product)
return session.exec(statement).all()
def get_one(session: Session, product_id: int) -> Product:
return session.get(Product, product_id)
def get_one(session: Session, product_id: int) -> models.ProductPublic:
return session.get(models.Product, product_id)
def create_one(session: Session, product: Product) -> Product:
product_create = product.model_dump(exclude_unset=True)
new_product = Product(**product_create)
def create_one(session: Session, product: models.ProductCreate) -> models.ProductPublic:
shipments = session.exec(select(models.Shipment).where(models.Shipment.id.in_(product.shipment_ids))).all()
product_create = product.model_dump(exclude_unset=True, exclude={'shipment_ids'})
new_product = models.Product(**product_create, shipments=shipments)
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)
def update_one(session: Session, id: int, product: models.ProductUpdate) -> models.ProductPublic:
statement = select(models.Product).where(models.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)
shipments_to_add = session.exec(select(models.Shipment).where(models.Shipment.id.in_(product.shipment_ids))).all()
new_product.shipments.clear()
for add in shipments_to_add:
new_product.shipments.append(add)
product_updates = product.model_dump(exclude_unset=True, exclude={"shipment_ids"})
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)
def delete_one(session: Session, id: int) -> models.ProductPublic:
statement = select(models.Product).where(models.Product.id == id)
result = session.exec(statement)
product = result.first()
if not product:
return None
result = models.ProductPublic.model_validate(product)
session.delete(product)
session.commit()
return product
return result

View File

@@ -1,29 +0,0 @@
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"

25
backend/src/settings.py Normal file
View File

@@ -0,0 +1,25 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
origins: str
db_host: str
db_user: str
db_pass: str
db_name: str
secret_key: str
keycloak_server: str
keycloak_realm: str
keycloak_client_id: str
keycloak_client_secret: str
keycloak_redirect_uri: str
root_fqdn: str
class Config:
env_file = "../.env"
settings = Settings()
AUTH_URL = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}/protocol/openid-connect/auth"
TOKEN_URL = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}/protocol/openid-connect/token"
ISSUER = f"{settings.keycloak_server}/realms/{settings.keycloak_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,50 @@
from sqlmodel import Session, select
import src.models as models
def get_all(session: Session) -> list[models.ShipmentPublic]:
statement = select(models.Shipment)
return session.exec(statement).all()
def get_one(session: Session, shipment_id: int) -> models.ShipmentPublic:
return session.get(models.Shipment, shipment_id)
def create_one(session: Session, shipment: models.ShipmentCreate) -> models.ShipmentPublic:
products = session.exec(select(models.Product).where(models.Product.id.in_(shipment.product_ids))).all()
shipment_create = shipment.model_dump(exclude_unset=True, exclude={'product_ids'})
new_shipment = models.Shipment(**shipment_create, products=products)
session.add(new_shipment)
session.commit()
session.refresh(new_shipment)
return new_shipment
def update_one(session: Session, id: int, shipment: models.ShipmentUpdate) -> models.ShipmentPublic:
statement = select(models.Shipment).where(models.Shipment.id == id)
result = session.exec(statement)
new_shipment = result.first()
if not new_shipment:
return None
products_to_add = session.exec(select(models.Product).where(models.Product.id.in_(shipment.product_ids))).all()
new_shipment.products.clear()
for add in products_to_add:
new_shipment.products.append(add)
shipment_updates = shipment.model_dump(exclude_unset=True, exclude={"product_ids"})
for key, value in shipment_updates.items():
setattr(new_shipment, key, value)
session.add(new_shipment)
session.commit()
session.refresh(new_shipment)
return new_shipment
def delete_one(session: Session, id: int) -> models.ShipmentPublic:
statement = select(models.Shipment).where(models.Shipment.id == id)
result = session.exec(statement)
shipment = result.first()
if not shipment:
return None
result = models.ShipmentPublic.model_validate(shipment)
session.delete(shipment)
session.commit()
return result

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.shipments.service as service
from src.auth.auth import get_current_user
router = APIRouter(prefix='/shipments')
@router.get('/', response_model=list[models.ShipmentPublic], )
def get_shipments(session: Session = Depends(get_session)):
return service.get_all(session)
@router.get('/{id}', response_model=models.ShipmentPublic)
def get_shipment(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.ShipmentPublic)
def create_shipment(shipment: models.ShipmentCreate, session: Session = Depends(get_session)):
return service.create_one(session, shipment)
@router.put('/{id}', response_model=models.ShipmentPublic)
def update_shipment(id: int, shipment: models.ShipmentUpdate, session: Session = Depends(get_session)):
result = service.update_one(session, id, shipment)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result
@router.delete('/{id}', response_model=models.ShipmentPublic)
def delete_shipment(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,42 @@
from sqlmodel import Session, select
import src.models as models
def get_all(session: Session) -> list[models.TemplatePublic]:
statement = select(models.Template)
return session.exec(statement).all()
def get_one(session: Session, template_id: int) -> models.TemplatePublic:
return session.get(models.Template, template_id)
def create_one(session: Session, template: models.TemplateCreate) -> models.TemplatePublic:
template_create = template.model_dump(exclude_unset=True)
new_template = models.Template(**template_create)
session.add(new_template)
session.commit()
session.refresh(new_template)
return new_template
def update_one(session: Session, id: int, template: models.TemplateUpdate) -> models.TemplatePublic:
statement = select(models.Template).where(models.Template.id == id)
result = session.exec(statement)
new_template = result.first()
if not new_template:
return None
template_updates = template.model_dump(exclude_unset=True)
for key, value in template_updates.items():
setattr(new_template, key, value)
session.add(new_template)
session.commit()
session.refresh(new_template)
return new_template
def delete_one(session: Session, id: int) -> models.TemplatePublic:
statement = select(models.Template).where(models.Template.id == id)
result = session.exec(statement)
template = result.first()
if not template:
return None
result = models.TemplatePublic.model_validate(template)
session.delete(template)
session.commit()
return result

View File

@@ -1,19 +1,37 @@
from fastapi import APIRouter
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.templates.service as service
router = APIRouter(prefix='/templates')
@router.get('/')
def get_templates():
return []
@router.get('/', response_model=list[models.TemplatePublic])
def get_templates(session: Session = Depends(get_session)):
return service.get_all(session)
@router.post('/')
def create_template():
return {}
@router.get('/{id}', response_model=models.TemplatePublic)
def get_templates(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.put('/')
def update_template():
return {}
@router.post('/', response_model=models.TemplatePublic)
def create_template(template: models.TemplateCreate, session: Session = Depends(get_session)):
return service.create_one(session, template)
@router.delete('/')
def delete_template():
return {}
@router.put('/{id}', response_model=models.TemplatePublic)
def update_template(id: int, template: models.TemplateUpdate, session: Session = Depends(get_session)):
result = service.update_one(session, id, template)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result
@router.delete('/{id}', response_model=models.TemplatePublic)
def delete_template(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,3 @@
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT

View File

@@ -0,0 +1,50 @@
from sqlmodel import Session, select
import src.models as models
def get_all(session: Session) -> list[models.UserPublic]:
statement = select(models.User)
return session.exec(statement).all()
def get_one(session: Session, user_id: int) -> models.UserPublic:
return session.get(models.User, user_id)
def get_or_create_user(session: Session, user_create: models.UserCreate):
statement = select(models.User).where(models.User.email == user_create.email)
user = session.exec(statement).first()
if user:
return user
user = create_one(session, user_create)
return user
def create_one(session: Session, user: models.UserCreate) -> models.UserPublic:
user_create = user.model_dump(exclude_unset=True)
new_user = models.User(**user_create)
session.add(new_user)
session.commit()
session.refresh(new_user)
return new_user
def update_one(session: Session, id: int, user: models.UserUpdate) -> models.UserPublic:
statement = select(models.User).where(models.User.id == id)
result = session.exec(statement)
new_user = result.first()
if not new_user:
return None
user_updates = user.model_dump(exclude_unset=True)
for key, value in user_updates.items():
setattr(new_user, key, value)
session.add(new_user)
session.commit()
session.refresh(new_user)
return new_user
def delete_one(session: Session, id: int) -> models.UserPublic:
statement = select(models.User).where(models.User.id == id)
result = session.exec(statement)
user = result.first()
if not user:
return None
result = models.UserPublic.model_validate(user)
session.delete(user)
session.commit()
return result

View File

@@ -1,19 +1,37 @@
from fastapi import APIRouter
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.users.service as service
router = APIRouter(prefix='/users')
@router.get('/')
def get_users():
return []
@router.get('/', response_model=list[models.UserPublic])
def get_users(session: Session = Depends(get_session)):
return service.get_all(session)
@router.post('/')
def create_user():
return {}
@router.get('/{id}', response_model=models.UserPublic)
def get_users(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.put('/')
def update_user():
return {}
@router.post('/', response_model=models.UserPublic)
def create_user(user: models.UserCreate, session: Session = Depends(get_session)):
return service.create_one(session, user)
@router.delete('/')
def delete_user():
return {}
@router.put('/{id}', response_model=models.UserPublic)
def update_user(id: int, user: models.UserUpdate, session: Session = Depends(get_session)):
result = service.update_one(session, id, user)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result
@router.delete('/{id}', response_model=models.UserPublic)
def delete_user(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

@@ -32,9 +32,9 @@ services:
restart: always
shm_size: 128mb
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: ${DB_NAME}
ROOT_FQDN: ${ROOT_FQDN}
ports:
- "54321:5432"