diff --git a/.env.example b/.env.example index bb0bb5d..21eede3 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,12 @@ -POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres -POSTGRES_DB=amap -ROOT_FQDN=http://localhost \ No newline at end of file +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= \ No newline at end of file diff --git a/amapcontract/collection.bru b/amapcontract/collection.bru index f694f60..9aa1c92 100644 --- a/amapcontract/collection.bru +++ b/amapcontract/collection.bru @@ -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 { diff --git a/amapcontract/forms/Create.bru b/amapcontract/forms/Create.bru new file mode 100644 index 0000000..f0f8a06 --- /dev/null +++ b/amapcontract/forms/Create.bru @@ -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 +} diff --git a/amapcontract/forms/Delete one.bru b/amapcontract/forms/Delete one.bru new file mode 100644 index 0000000..789b008 --- /dev/null +++ b/amapcontract/forms/Delete one.bru @@ -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 +} diff --git a/amapcontract/forms/Get all.bru b/amapcontract/forms/Get all.bru new file mode 100644 index 0000000..fd3efdc --- /dev/null +++ b/amapcontract/forms/Get all.bru @@ -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 +} diff --git a/amapcontract/forms/Get one.bru b/amapcontract/forms/Get one.bru new file mode 100644 index 0000000..fac4008 --- /dev/null +++ b/amapcontract/forms/Get one.bru @@ -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 +} diff --git a/amapcontract/forms/Update one.bru b/amapcontract/forms/Update one.bru new file mode 100644 index 0000000..097d954 --- /dev/null +++ b/amapcontract/forms/Update one.bru @@ -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 +} diff --git a/amapcontract/forms/folder.bru b/amapcontract/forms/folder.bru new file mode 100644 index 0000000..eaa3f6c --- /dev/null +++ b/amapcontract/forms/folder.bru @@ -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" + } + ''' +} diff --git a/amapcontract/products/Delete one.bru b/amapcontract/products/Delete one.bru index 789b008..53d0a5a 100644 --- a/amapcontract/products/Delete one.bru +++ b/amapcontract/products/Delete one.bru @@ -5,7 +5,7 @@ meta { } delete { - url: {{Service}}/{{Route}}/2 + url: {{Service}}/{{Route}}/6 body: none auth: inherit } diff --git a/amapcontract/products/Update one.bru b/amapcontract/products/Update one.bru index 097d954..9576f6e 100644 --- a/amapcontract/products/Update one.bru +++ b/amapcontract/products/Update one.bru @@ -5,7 +5,7 @@ meta { } put { - url: {{Service}}/{{Route}}/1 + url: {{Service}}/{{Route}}/3 body: json auth: inherit } diff --git a/amapcontract/products/folder.bru b/amapcontract/products/folder.bru index 8678d7e..5a0279b 100644 --- a/amapcontract/products/folder.bru +++ b/amapcontract/products/folder.bru @@ -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] + } + ''' } diff --git a/amapcontract/shipments/Create.bru b/amapcontract/shipments/Create.bru new file mode 100644 index 0000000..f0f8a06 --- /dev/null +++ b/amapcontract/shipments/Create.bru @@ -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 +} diff --git a/amapcontract/shipments/Delete one.bru b/amapcontract/shipments/Delete one.bru new file mode 100644 index 0000000..789b008 --- /dev/null +++ b/amapcontract/shipments/Delete one.bru @@ -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 +} diff --git a/amapcontract/shipments/Get all.bru b/amapcontract/shipments/Get all.bru new file mode 100644 index 0000000..fd3efdc --- /dev/null +++ b/amapcontract/shipments/Get all.bru @@ -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 +} diff --git a/amapcontract/shipments/Get one.bru b/amapcontract/shipments/Get one.bru new file mode 100644 index 0000000..fac4008 --- /dev/null +++ b/amapcontract/shipments/Get one.bru @@ -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 +} diff --git a/amapcontract/shipments/Update one.bru b/amapcontract/shipments/Update one.bru new file mode 100644 index 0000000..097d954 --- /dev/null +++ b/amapcontract/shipments/Update one.bru @@ -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 +} diff --git a/amapcontract/shipments/folder.bru b/amapcontract/shipments/folder.bru new file mode 100644 index 0000000..ee04ee0 --- /dev/null +++ b/amapcontract/shipments/folder.bru @@ -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] + } + ''' +} diff --git a/amapcontract/users/Create.bru b/amapcontract/users/Create.bru new file mode 100644 index 0000000..f0f8a06 --- /dev/null +++ b/amapcontract/users/Create.bru @@ -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 +} diff --git a/amapcontract/users/Delete one.bru b/amapcontract/users/Delete one.bru new file mode 100644 index 0000000..789b008 --- /dev/null +++ b/amapcontract/users/Delete one.bru @@ -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 +} diff --git a/amapcontract/users/Get all.bru b/amapcontract/users/Get all.bru new file mode 100644 index 0000000..fd3efdc --- /dev/null +++ b/amapcontract/users/Get all.bru @@ -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 +} diff --git a/amapcontract/users/Get one.bru b/amapcontract/users/Get one.bru new file mode 100644 index 0000000..fac4008 --- /dev/null +++ b/amapcontract/users/Get one.bru @@ -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 +} diff --git a/amapcontract/users/Update one.bru b/amapcontract/users/Update one.bru new file mode 100644 index 0000000..097d954 --- /dev/null +++ b/amapcontract/users/Update one.bru @@ -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 +} diff --git a/amapcontract/users/folder.bru b/amapcontract/users/folder.bru new file mode 100644 index 0000000..b74703b --- /dev/null +++ b/amapcontract/users/folder.bru @@ -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"} +} diff --git a/backend/pyproject.toml b/backend/pyproject.toml index fcff9ab..30cb798 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -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"] \ No newline at end of file diff --git a/backend/src/auth/auth.py b/backend/src/auth/auth.py index b8fd8cc..f9732d8 100644 --- a/backend/src/auth/auth.py +++ b/backend/src/auth/auth.py @@ -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") diff --git a/backend/src/database.py b/backend/src/database.py index daf4db2..e536ccf 100644 --- a/backend/src/database.py +++ b/backend/src/database.py @@ -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 \ No newline at end of file + yield session + +def create_all_tables(): + SQLModel.metadata.create_all(engine) diff --git a/backend/src/forms/forms.py b/backend/src/forms/forms.py index d7c684d..8e6b82a 100644 --- a/backend/src/forms/forms.py +++ b/backend/src/forms/forms.py @@ -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 diff --git a/backend/src/forms/model.py b/backend/src/forms/model.py deleted file mode 100644 index eb719a7..0000000 --- a/backend/src/forms/model.py +++ /dev/null @@ -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 - \ No newline at end of file diff --git a/backend/src/forms/service.py b/backend/src/forms/service.py new file mode 100644 index 0000000..fe17c4e --- /dev/null +++ b/backend/src/forms/service.py @@ -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 diff --git a/backend/src/main.py b/backend/src/main.py index cb9d1d6..0266e97 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -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) \ No newline at end of file diff --git a/backend/src/models.py b/backend/src/models.py index 9e0b601..6445573 100644 --- a/backend/src/models.py +++ b/backend/src/models.py @@ -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 @@ -97,4 +18,150 @@ class UserUpdate(SQLModel): email: str | None class UserCreate(UserBase): - pass \ No newline at end of file + 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] \ No newline at end of file diff --git a/backend/src/productors/service.py b/backend/src/productors/service.py index 4d2712f..f753f2c 100644 --- a/backend/src/productors/service.py +++ b/backend/src/productors/service.py @@ -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 diff --git a/backend/src/products/products.py b/backend/src/products/products.py index 0783b61..dc61267 100644 --- a/backend/src/products/products.py +++ b/backend/src/products/products.py @@ -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) diff --git a/backend/src/products/service.py b/backend/src/products/service.py index 19e9e77..8eaa06b 100644 --- a/backend/src/products/service.py +++ b/backend/src/products/service.py @@ -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 diff --git a/backend/src/secrets.py b/backend/src/secrets.py deleted file mode 100644 index 925a18d..0000000 --- a/backend/src/secrets.py +++ /dev/null @@ -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" \ No newline at end of file diff --git a/backend/src/settings.py b/backend/src/settings.py new file mode 100644 index 0000000..97d5ea3 --- /dev/null +++ b/backend/src/settings.py @@ -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" diff --git a/backend/src/shipments/__init__.py b/backend/src/shipments/__init__.py new file mode 100644 index 0000000..10fe5b0 --- /dev/null +++ b/backend/src/shipments/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026-present Julien Aldon +# +# SPDX-License-Identifier: MIT \ No newline at end of file diff --git a/backend/src/shipments/service.py b/backend/src/shipments/service.py new file mode 100644 index 0000000..80b9e2c --- /dev/null +++ b/backend/src/shipments/service.py @@ -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 \ No newline at end of file diff --git a/backend/src/shipments/shipments.py b/backend/src/shipments/shipments.py new file mode 100644 index 0000000..7302e15 --- /dev/null +++ b/backend/src/shipments/shipments.py @@ -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 diff --git a/backend/src/templates/service.py b/backend/src/templates/service.py new file mode 100644 index 0000000..1d28256 --- /dev/null +++ b/backend/src/templates/service.py @@ -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 diff --git a/backend/src/templates/templates.py b/backend/src/templates/templates.py index e794ca2..c0305df 100644 --- a/backend/src/templates/templates.py +++ b/backend/src/templates/templates.py @@ -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 diff --git a/backend/src/users/__init__.py b/backend/src/users/__init__.py new file mode 100644 index 0000000..10fe5b0 --- /dev/null +++ b/backend/src/users/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026-present Julien Aldon +# +# SPDX-License-Identifier: MIT \ No newline at end of file diff --git a/backend/src/users/service.py b/backend/src/users/service.py new file mode 100644 index 0000000..3d9e7ac --- /dev/null +++ b/backend/src/users/service.py @@ -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 diff --git a/backend/src/users/users.py b/backend/src/users/users.py index da8a006..311e90f 100644 --- a/backend/src/users/users.py +++ b/backend/src/users/users.py @@ -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 diff --git a/docker-compose.yaml b/docker-compose.yaml index 9320c27..ea142cb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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"