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 DB_USER=postgres
POSTGRES_PASSWORD=postgres DB_PASS=postgres
POSTGRES_DB=amap DB_NAME=amap
ROOT_FQDN=http://localhost 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 { auth {
mode: bearer mode: none
}
auth:bearer {
token: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJtNUtSQkp1T3VqMnFiUElySlRldFVISGVWMWRTLTEzUG5saU1PSWRLcWFvIn0.eyJleHAiOjE3NzA2NTQwMzYsImlhdCI6MTc3MDY1MzczNiwiYXV0aF90aW1lIjoxNzcwNjUzMDU2LCJqdGkiOiJvbnJ0YWM6YmFiMDZiNGMtMjM5ZC00NzM3LTliYWEtNjE2MjBmMzVjM2NhIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5hbGRvbi5mci9yZWFsbXMvYWxkb24uZnIiLCJhdWQiOlsibmV4dGNsb3VkIiwiYWNjb3VudCJdLCJzdWIiOiJlM2VkN2NiNC1iMDYxLTRiZWQtYTY5YS1lMGQ5ZWJmNDJhZWIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhbWFwIiwic2lkIjoiMTkwY2I4YzAtMGYxZC00MWJiLTkwYWYtZjNmMGJkZWMzYjA3IiwiYWNyIjoiMCIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjgwMDAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtZ2lyYXNvbCIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJuZXh0Y2xvdWQiOnsicm9sZXMiOlsicGVsYXJnbyIsImZlZmFuIiwiYWRtaW4iLCJnaXJhc29sIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiSnVsaWVuIEFsZG9uIiwicHJlZmVycmVkX3VzZXJuYW1lIjoianVsaWVuIiwiZ2l2ZW5fbmFtZSI6Ikp1bGllbiIsImZhbWlseV9uYW1lIjoiQWxkb24iLCJlbWFpbCI6Imp1bGllbi5hbGRvbkB3YW5hZG9vLmZyIn0.bq-EUtK_UqsIOwI6KDHB8eELMirWPDfTMta904XNeffj_v_ptEnHbecCf1OG6zzwanrBUyl_On7z95zVvVuKX6fQM9iaqxDqm7VlAK1O6n97367evTjQTOggkl3eTgX3xkfbCjJyzP_8RhTPXBsL_Nao8h5kgCnDwUHKEZ547oeoPKVEzlc82SgPi2rsiTVyvznJxGyJkQOTcDDMqTUxj4OVqWD5FMEDCfLnisUNPADhq0Umyw8hU4YwtI1-3hn6aXbnVcDekk2oWVli_6MeJHyejI8_yPnnQMvcp9OqciXRMtCGml1vMHcb5kUh4U9OeAhewzBFb_Mk9KDOspktSQ
} }
vars:pre-request { 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 { delete {
url: {{Service}}/{{Route}}/2 url: {{Service}}/{{Route}}/6
body: none body: none
auth: inherit auth: inherit
} }

View File

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

View File

@@ -8,6 +8,28 @@ auth {
vars:pre-request { vars:pre-request {
Route: products Route: products
ExamplePOSTBody: {"name": "test", "unit": "KILO", "price": 3.50, "price_kg": 3.50, "weight": "1.0", "productor_id": 1} ExamplePOSTBody: '''
ExamplePUTBody: {"name": "updatetestt", "address": "updatetestt"} {
"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", "Programming Language :: Python :: Implementation :: PyPy",
] ]
dependencies = [ dependencies = [
"fastapi", "fastapi[standard]",
"sqlmodel", "sqlmodel",
"psycopg2", "psycopg2-binary",
"PyJWT", "PyJWT",
"cryptography", "cryptography",
"requests" "requests"
@@ -43,7 +43,7 @@ extra-dependencies = [
"mypy>=1.0.0", "mypy>=1.0.0",
] ]
[tool.hatch.envs.types.scripts] [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] [tool.coverage.run]
source_pkgs = ["backend", "tests"] source_pkgs = ["backend", "tests"]
@@ -63,3 +63,7 @@ exclude_lines = [
"if __name__ == .__main__.:", "if __name__ == .__main__.:",
"if TYPE_CHECKING:", "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.responses import RedirectResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials 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 secrets
import jwt import jwt
from jwt import PyJWKClient from jwt import PyJWKClient
@@ -15,26 +21,24 @@ security = HTTPBearer()
@router.get('/login') @router.get('/login')
def login(): def login():
state = secrets.token_urlsafe(16) state = secrets.token_urlsafe(16)
params = { params = {
"client_id": CLIENT_ID, "client_id": settings.keycloak_client_id,
"response_type": "code", "response_type": "code",
"scope": "openid", "scope": "openid",
"redirect_uri": REDIRECT_URI, "redirect_uri": settings.keycloak_redirect_uri,
"state": state, "state": state,
} }
request_url = requests.Request('GET', AUTH_URL, params=params).prepare().url request_url = requests.Request('GET', AUTH_URL, params=params).prepare().url
return RedirectResponse(request_url) return RedirectResponse(request_url)
@router.get("/callback") @router.get("/callback")
def callback(code: str): def callback(code: str, session: Session = Depends(get_session)):
data = { data = {
"grant_type": "authorization_code", "grant_type": "authorization_code",
"code": code, "code": code,
"redirect_uri": REDIRECT_URI, "redirect_uri": settings.keycloak_redirect_uri,
"client_id": CLIENT_ID, "client_id": settings.keycloak_client_id,
"client_secret": CLIENT_SECRET, "client_secret": settings.keycloak_client_secret,
} }
headers = { headers = {
"Content-Type": "application/x-www-form-urlencoded" "Content-Type": "application/x-www-form-urlencoded"
@@ -45,7 +49,17 @@ def callback(code: str):
{"error": "Failed to get token"}, {"error": "Failed to get token"},
status_code=400 status_code=400
) )
token_data = response.json() 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 { return {
"access_token": token_data["access_token"], "access_token": token_data["access_token"],
"id_token": token_data["id_token"], "id_token": token_data["id_token"],
@@ -56,20 +70,16 @@ def verify_token(token: str):
try: try:
signing_key = jwk_client.get_signing_key_from_jwt(token) signing_key = jwk_client.get_signing_key_from_jwt(token)
decoded = jwt.decode(token, options={"verify_signature": False}) decoded = jwt.decode(token, options={"verify_signature": False})
print(decoded, ISSUER)
print(decoded["exp"])
payload = jwt.decode( payload = jwt.decode(
token, token,
signing_key.key, signing_key.key,
algorithms=["RS256"], algorithms=["RS256"],
audience=CLIENT_ID, audience=settings.keycloak_client_id,
issuer=ISSUER, issuer=ISSUER,
) )
return payload return payload
except jwt.ExpiredSignatureError: except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired") raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token") raise HTTPException(status_code=401, detail="Invalid token")

View File

@@ -1,9 +1,11 @@
from sqlmodel import create_engine, SQLModel, Session 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}') engine = create_engine(f'postgresql://{settings.db_user}:{settings.db_pass}@{settings.db_host}:54321/{settings.db_name}')
# SQLModel.metadata.create_all(engine)
def get_session(): def get_session():
with Session(engine) as session: with Session(engine) as session:
yield 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 = APIRouter(prefix='/forms')
@router.get('/') @router.get('/', response_model=list[models.FormPublic])
def get_forms(): def get_forms(session: Session = Depends(get_session)):
return [] return service.get_all(session)
@router.post('/') @router.get('/{id}', response_model=models.FormPublic)
def create_form(): def get_forms(id: int, session: Session = Depends(get_session)):
return {} result = service.get_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result
@router.put('/') @router.post('/', response_model=models.FormPublic)
def update_form(): def create_form(form: models.FormCreate, session: Session = Depends(get_session)):
return {} return service.create_one(session, form)
@router.delete('/') @router.put('/{id}', response_model=models.FormPublic)
def delete_form(): def update_form(id: int, form: models.FormUpdate, session: Session = Depends(get_session)):
return {} 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.products.products import router as products_router
from src.users.users import router as users_router from src.users.users import router as users_router
from src.auth.auth import router as auth_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 from src.database import engine
app = FastAPI() app = FastAPI()
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=origins, allow_origins=[settings.origins],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
@@ -32,5 +33,6 @@ app.include_router(productors_router)
app.include_router(products_router) app.include_router(products_router)
app.include_router(users_router) app.include_router(users_router)
app.include_router(auth_router) app.include_router(auth_router)
app.include_router(shipment_router)
SQLModel.metadata.create_all(engine) SQLModel.metadata.create_all(engine)

View File

@@ -3,85 +3,6 @@ from enum import Enum
from typing import Optional from typing import Optional
import datetime 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): class UserBase(SQLModel):
name: str name: str
email: str email: str
@@ -97,4 +18,150 @@ class UserUpdate(SQLModel):
email: str | None email: str | None
class UserCreate(UserBase): class UserCreate(UserBase):
pass 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 sqlmodel import Session, select
from src.models import Productor import src.models as models
def get_all(session: Session) -> list[Productor]: def get_all(session: Session) -> list[models.ProductorPublic]:
statement = select(Productor) statement = select(models.Productor)
return session.exec(statement).all() return session.exec(statement).all()
def get_one(session: Session, productor_id: int) -> Productor: def get_one(session: Session, productor_id: int) -> models.ProductorPublic:
return session.get(Productor, productor_id) 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) productor_create = productor.model_dump(exclude_unset=True)
new_productor = Productor(**productor_create) new_productor = models.Productor(**productor_create)
session.add(new_productor) session.add(new_productor)
session.commit() session.commit()
session.refresh(new_productor) session.refresh(new_productor)
return new_productor return new_productor
def update_one(session: Session, id: int, productor: Productor) -> Productor: def update_one(session: Session, id: int, productor: models.ProductorUpdate) -> models.ProductorPublic:
statement = select(Productor).where(Productor.id == id) statement = select(models.Productor).where(models.Productor.id == id)
result = session.exec(statement) result = session.exec(statement)
new_productor = result.first() new_productor = result.first()
if not new_productor: if not new_productor:
@@ -30,12 +30,13 @@ def update_one(session: Session, id: int, productor: Productor) -> Productor:
session.refresh(new_productor) session.refresh(new_productor)
return new_productor return new_productor
def delete_one(session: Session, id: int) -> Productor: def delete_one(session: Session, id: int) -> models.ProductorPublic:
statement = select(Productor).where(Productor.id == id) statement = select(models.Productor).where(models.Productor.id == id)
result = session.exec(statement) result = session.exec(statement)
productor = result.first() productor = result.first()
if not productor: if not productor:
return None return None
result = models.ProductorPublic.model_validate(productor)
session.delete(productor) session.delete(productor)
session.commit() session.commit()
return productor return result

View File

@@ -6,9 +6,9 @@ from sqlmodel import Session
import src.products.service as service import src.products.service as service
from src.auth.auth import get_current_user from src.auth.auth import get_current_user
router = APIRouter(prefix='/products') router = APIRouter(prefix='/products')
#user=Depends(get_current_user)
@router.get('/', response_model=list[models.ProductPublic], ) @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) return service.get_all(session)
@router.get('/{id}', response_model=models.ProductPublic) @router.get('/{id}', response_model=models.ProductPublic)

View File

@@ -1,41 +1,51 @@
from sqlmodel import Session, select from sqlmodel import Session, select
from src.models import Product import src.models as models
def get_all(session: Session) -> list[Product]: def get_all(session: Session) -> list[models.ProductPublic]:
statement = select(Product) statement = select(models.Product)
return session.exec(statement).all() return session.exec(statement).all()
def get_one(session: Session, product_id: int) -> Product: def get_one(session: Session, product_id: int) -> models.ProductPublic:
return session.get(Product, product_id) return session.get(models.Product, product_id)
def create_one(session: Session, product: Product) -> Product: def create_one(session: Session, product: models.ProductCreate) -> models.ProductPublic:
product_create = product.model_dump(exclude_unset=True) shipments = session.exec(select(models.Shipment).where(models.Shipment.id.in_(product.shipment_ids))).all()
new_product = Product(**product_create)
product_create = product.model_dump(exclude_unset=True, exclude={'shipment_ids'})
new_product = models.Product(**product_create, shipments=shipments)
session.add(new_product) session.add(new_product)
session.commit() session.commit()
session.refresh(new_product) session.refresh(new_product)
return new_product return new_product
def update_one(session: Session, id: int, product: Product) -> Product: def update_one(session: Session, id: int, product: models.ProductUpdate) -> models.ProductPublic:
statement = select(Product).where(Product.id == id) statement = select(models.Product).where(models.Product.id == id)
result = session.exec(statement) result = session.exec(statement)
new_product = result.first() new_product = result.first()
if not new_product: if not new_product:
return None 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(): for key, value in product_updates.items():
setattr(new_product, key, value) setattr(new_product, key, value)
session.add(new_product) session.add(new_product)
session.commit() session.commit()
session.refresh(new_product) session.refresh(new_product)
return new_product return new_product
def delete_one(session: Session, id: int) -> Product: def delete_one(session: Session, id: int) -> models.ProductPublic:
statement = select(Product).where(Product.id == id) statement = select(models.Product).where(models.Product.id == id)
result = session.exec(statement) result = session.exec(statement)
product = result.first() product = result.first()
if not product: if not product:
return None return None
result = models.ProductPublic.model_validate(product)
session.delete(product) session.delete(product)
session.commit() 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 = APIRouter(prefix='/templates')
@router.get('/') @router.get('/', response_model=list[models.TemplatePublic])
def get_templates(): def get_templates(session: Session = Depends(get_session)):
return [] return service.get_all(session)
@router.post('/') @router.get('/{id}', response_model=models.TemplatePublic)
def create_template(): def get_templates(id: int, session: Session = Depends(get_session)):
return {} result = service.get_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result
@router.put('/') @router.post('/', response_model=models.TemplatePublic)
def update_template(): def create_template(template: models.TemplateCreate, session: Session = Depends(get_session)):
return {} return service.create_one(session, template)
@router.delete('/') @router.put('/{id}', response_model=models.TemplatePublic)
def delete_template(): def update_template(id: int, template: models.TemplateUpdate, session: Session = Depends(get_session)):
return {} 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 = APIRouter(prefix='/users')
@router.get('/') @router.get('/', response_model=list[models.UserPublic])
def get_users(): def get_users(session: Session = Depends(get_session)):
return [] return service.get_all(session)
@router.post('/') @router.get('/{id}', response_model=models.UserPublic)
def create_user(): def get_users(id: int, session: Session = Depends(get_session)):
return {} result = service.get_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result
@router.put('/') @router.post('/', response_model=models.UserPublic)
def update_user(): def create_user(user: models.UserCreate, session: Session = Depends(get_session)):
return {} return service.create_one(session, user)
@router.delete('/') @router.put('/{id}', response_model=models.UserPublic)
def delete_user(): def update_user(id: int, user: models.UserUpdate, session: Session = Depends(get_session)):
return {} 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 restart: always
shm_size: 128mb shm_size: 128mb
environment: environment:
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${DB_NAME}
ROOT_FQDN: ${ROOT_FQDN} ROOT_FQDN: ${ROOT_FQDN}
ports: ports:
- "54321:5432" - "54321:5432"