add project base
This commit is contained in:
4
backend/src/__about__.py
Normal file
4
backend/src/__about__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
__version__ = "0.0.1"
|
||||
3
backend/src/__init__.py
Normal file
3
backend/src/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
80
backend/src/auth/auth.py
Normal file
80
backend/src/auth/auth.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from fastapi import APIRouter, Security, HTTPException
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from src.secrets import CLIENT_ID, REDIRECT_URI, AUTH_URL, CLIENT_SECRET, TOKEN_URL, JWKS_URL, ISSUER
|
||||
import secrets
|
||||
import jwt
|
||||
from jwt import PyJWKClient
|
||||
import requests
|
||||
|
||||
router = APIRouter(prefix="/auth")
|
||||
|
||||
jwk_client = PyJWKClient(JWKS_URL)
|
||||
security = HTTPBearer()
|
||||
|
||||
@router.get('/login')
|
||||
def login():
|
||||
state = secrets.token_urlsafe(16)
|
||||
|
||||
params = {
|
||||
"client_id": CLIENT_ID,
|
||||
"response_type": "code",
|
||||
"scope": "openid",
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
"state": state,
|
||||
}
|
||||
|
||||
request_url = requests.Request('GET', AUTH_URL, params=params).prepare().url
|
||||
return RedirectResponse(request_url)
|
||||
|
||||
@router.get("/callback")
|
||||
def callback(code: str):
|
||||
data = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
"client_id": CLIENT_ID,
|
||||
"client_secret": CLIENT_SECRET,
|
||||
}
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
response = requests.post(TOKEN_URL, data=data, headers=headers)
|
||||
if response.status_code != 200:
|
||||
return JSONResponse(
|
||||
{"error": "Failed to get token"},
|
||||
status_code=400
|
||||
)
|
||||
token_data = response.json()
|
||||
return {
|
||||
"access_token": token_data["access_token"],
|
||||
"id_token": token_data["id_token"],
|
||||
"refresh_token": token_data["refresh_token"],
|
||||
}
|
||||
|
||||
def verify_token(token: str):
|
||||
try:
|
||||
signing_key = jwk_client.get_signing_key_from_jwt(token)
|
||||
decoded = jwt.decode(token, options={"verify_signature": False})
|
||||
print(decoded, ISSUER)
|
||||
print(decoded["exp"])
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
signing_key.key,
|
||||
algorithms=["RS256"],
|
||||
audience=CLIENT_ID,
|
||||
issuer=ISSUER,
|
||||
)
|
||||
return payload
|
||||
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(status_code=401, detail="Token expired")
|
||||
|
||||
except jwt.InvalidTokenError:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
|
||||
|
||||
def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Security(security)
|
||||
):
|
||||
return verify_token(credentials.credentials)
|
||||
3
backend/src/contracts/__init__.py
Normal file
3
backend/src/contracts/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
3
backend/src/contracts/contracts.py
Normal file
3
backend/src/contracts/contracts.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(prefix='/contracts')
|
||||
9
backend/src/database.py
Normal file
9
backend/src/database.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from sqlmodel import create_engine, SQLModel, Session
|
||||
from src.secrets import dbname, dbhost, dbuser, dbpass
|
||||
|
||||
engine = create_engine(f'postgresql://{dbuser}:{dbpass}@{dbhost}:54321/{dbname}')
|
||||
# SQLModel.metadata.create_all(engine)
|
||||
|
||||
def get_session():
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
3
backend/src/forms/__init__.py
Normal file
3
backend/src/forms/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
19
backend/src/forms/forms.py
Normal file
19
backend/src/forms/forms.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(prefix='/forms')
|
||||
|
||||
@router.get('/')
|
||||
def get_forms():
|
||||
return []
|
||||
|
||||
@router.post('/')
|
||||
def create_form():
|
||||
return {}
|
||||
|
||||
@router.put('/')
|
||||
def update_form():
|
||||
return {}
|
||||
|
||||
@router.delete('/')
|
||||
def delete_form():
|
||||
return {}
|
||||
10
backend/src/forms/model.py
Normal file
10
backend/src/forms/model.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from sqlmodel import Field, SQLModel
|
||||
form src.productors.model import Productor
|
||||
|
||||
class Form(SQLModel, table=True):
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
productor_id: int | None = Field(default=None, foreign_key="productor.id")
|
||||
shipment_number: int
|
||||
season: str
|
||||
|
||||
36
backend/src/main.py
Normal file
36
backend/src/main.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from src.templates.templates import router as template_router
|
||||
from src.contracts.contracts import router as contracts_router
|
||||
from src.forms.forms import router as forms_router
|
||||
from src.productors.productors import router as productors_router
|
||||
from src.products.products import router as products_router
|
||||
from src.users.users import router as users_router
|
||||
from src.auth.auth import router as auth_router
|
||||
from src.secrets import origins
|
||||
from src.database import engine
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
expose_headers=['x-nbpage']
|
||||
)
|
||||
|
||||
|
||||
app.include_router(template_router)
|
||||
app.include_router(contracts_router)
|
||||
app.include_router(forms_router)
|
||||
app.include_router(productors_router)
|
||||
app.include_router(products_router)
|
||||
app.include_router(users_router)
|
||||
app.include_router(auth_router)
|
||||
|
||||
SQLModel.metadata.create_all(engine)
|
||||
1
backend/src/messages.py
Normal file
1
backend/src/messages.py
Normal file
@@ -0,0 +1 @@
|
||||
notfound = "Resource was not found."
|
||||
100
backend/src/models.py
Normal file
100
backend/src/models.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from sqlmodel import Field, SQLModel, Relationship
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
import datetime
|
||||
|
||||
class Unit(Enum):
|
||||
GRAMS = 1
|
||||
KILO = 2
|
||||
|
||||
class ProductBase(SQLModel):
|
||||
name: str
|
||||
unit: Unit
|
||||
price: float
|
||||
price_kg: float | None
|
||||
weight: float
|
||||
productor_id: int | None = Field(default=None, foreign_key="productor.id")
|
||||
|
||||
class ProductPublic(ProductBase):
|
||||
id: int
|
||||
|
||||
class Product(ProductBase, table=True):
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
|
||||
productor: Optional['Productor'] = Relationship(back_populates="products")
|
||||
|
||||
class ProductUpdate(SQLModel):
|
||||
name: str | None
|
||||
unit: Unit | None
|
||||
price: float | None
|
||||
price_kg: float | None
|
||||
weight: float | None
|
||||
productor_id: int | None = Field(default=None, foreign_key="productor.id")
|
||||
|
||||
|
||||
class ProductCreate(ProductBase):
|
||||
pass
|
||||
|
||||
class ProductorBase(SQLModel):
|
||||
name: str
|
||||
address: str
|
||||
payment: str
|
||||
|
||||
class ProductorPublic(ProductorBase):
|
||||
id: int
|
||||
products: list[Product] = []
|
||||
|
||||
class Productor(ProductorBase, table=True):
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
|
||||
products: list[Product] = Relationship(back_populates='productor')
|
||||
|
||||
class ProductorUpdate(SQLModel):
|
||||
name: str | None
|
||||
address: str | None
|
||||
payment: str | None
|
||||
|
||||
class ProductorCreate(ProductorBase):
|
||||
pass
|
||||
|
||||
class FormBase(SQLModel):
|
||||
productor_id: int | None = Field(default=None, foreign_key="productor.id")
|
||||
referer_id: int | None = Field(default=None, foreign_key="referer.id")
|
||||
season: str
|
||||
shipments: int
|
||||
start: datetime.date
|
||||
end: datetime.date
|
||||
|
||||
class FormPublic(FormBase):
|
||||
id: int
|
||||
|
||||
class Form(FormBase, table=True):
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
|
||||
class FormUpdate(SQLModel):
|
||||
productor_id: int | None = Field(default=None, foreign_key="productor.id")
|
||||
referer_id: int | None = Field(default=None, foreign_key="user.id")
|
||||
season: str | None
|
||||
shipments: int | None
|
||||
start: datetime.date | None
|
||||
end: datetime.date | None
|
||||
|
||||
class FormCreate(FormBase):
|
||||
pass
|
||||
|
||||
class UserBase(SQLModel):
|
||||
name: str
|
||||
email: str
|
||||
|
||||
class UserPublic(UserBase):
|
||||
id: int
|
||||
|
||||
class User(UserBase, table=True):
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
|
||||
class UserUpdate(SQLModel):
|
||||
name: str | None
|
||||
email: str | None
|
||||
|
||||
class UserCreate(UserBase):
|
||||
pass
|
||||
3
backend/src/productors/__init__.py
Normal file
3
backend/src/productors/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
37
backend/src/productors/productors.py
Normal file
37
backend/src/productors/productors.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
import src.messages as messages
|
||||
import src.models as models
|
||||
from src.database import get_session
|
||||
from sqlmodel import Session
|
||||
import src.productors.service as service
|
||||
|
||||
router = APIRouter(prefix='/productors')
|
||||
|
||||
@router.get('/', response_model=list[models.ProductorPublic])
|
||||
def get_productors(session: Session = Depends(get_session)):
|
||||
return service.get_all(session)
|
||||
|
||||
@router.get('/{id}', response_model=models.ProductorPublic)
|
||||
def get_productors(id: int, session: Session = Depends(get_session)):
|
||||
result = service.get_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.post('/', response_model=models.ProductorPublic)
|
||||
def create_productor(productor: models.ProductorCreate, session: Session = Depends(get_session)):
|
||||
return service.create_one(session, productor)
|
||||
|
||||
@router.put('/{id}', response_model=models.ProductorPublic)
|
||||
def update_productor(id: int, productor: models.ProductorUpdate, session: Session = Depends(get_session)):
|
||||
result = service.update_one(session, id, productor)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.delete('/{id}', response_model=models.ProductorPublic)
|
||||
def delete_productor(id: int, session: Session = Depends(get_session)):
|
||||
result = service.delete_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
41
backend/src/productors/service.py
Normal file
41
backend/src/productors/service.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from sqlmodel import Session, select
|
||||
from src.models import Productor
|
||||
|
||||
def get_all(session: Session) -> list[Productor]:
|
||||
statement = select(Productor)
|
||||
return session.exec(statement).all()
|
||||
|
||||
def get_one(session: Session, productor_id: int) -> Productor:
|
||||
return session.get(Productor, productor_id)
|
||||
|
||||
def create_one(session: Session, productor: Productor) -> Productor:
|
||||
productor_create = productor.model_dump(exclude_unset=True)
|
||||
new_productor = Productor(**productor_create)
|
||||
session.add(new_productor)
|
||||
session.commit()
|
||||
session.refresh(new_productor)
|
||||
return new_productor
|
||||
|
||||
def update_one(session: Session, id: int, productor: Productor) -> Productor:
|
||||
statement = select(Productor).where(Productor.id == id)
|
||||
result = session.exec(statement)
|
||||
new_productor = result.first()
|
||||
if not new_productor:
|
||||
return None
|
||||
productor_updates = productor.model_dump(exclude_unset=True)
|
||||
for key, value in productor_updates.items():
|
||||
setattr(new_productor, key, value)
|
||||
session.add(new_productor)
|
||||
session.commit()
|
||||
session.refresh(new_productor)
|
||||
return new_productor
|
||||
|
||||
def delete_one(session: Session, id: int) -> Productor:
|
||||
statement = select(Productor).where(Productor.id == id)
|
||||
result = session.exec(statement)
|
||||
productor = result.first()
|
||||
if not productor:
|
||||
return None
|
||||
session.delete(productor)
|
||||
session.commit()
|
||||
return productor
|
||||
3
backend/src/products/__init__.py
Normal file
3
backend/src/products/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
37
backend/src/products/products.py
Normal file
37
backend/src/products/products.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
import src.messages as messages
|
||||
import src.models as models
|
||||
from src.database import get_session
|
||||
from sqlmodel import Session
|
||||
import src.products.service as service
|
||||
from src.auth.auth import get_current_user
|
||||
router = APIRouter(prefix='/products')
|
||||
|
||||
@router.get('/', response_model=list[models.ProductPublic], )
|
||||
def get_products(session: Session = Depends(get_session), user=Depends(get_current_user)):
|
||||
return service.get_all(session)
|
||||
|
||||
@router.get('/{id}', response_model=models.ProductPublic)
|
||||
def get_product(id: int, session: Session = Depends(get_session)):
|
||||
result = service.get_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.post('/', response_model=models.ProductPublic)
|
||||
def create_product(product: models.ProductCreate, session: Session = Depends(get_session)):
|
||||
return service.create_one(session, product)
|
||||
|
||||
@router.put('/{id}', response_model=models.ProductPublic)
|
||||
def update_product(id: int, product: models.ProductUpdate, session: Session = Depends(get_session)):
|
||||
result = service.update_one(session, id, product)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.delete('/{id}', response_model=models.ProductPublic)
|
||||
def delete_product(id: int, session: Session = Depends(get_session)):
|
||||
result = service.delete_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
41
backend/src/products/service.py
Normal file
41
backend/src/products/service.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from sqlmodel import Session, select
|
||||
from src.models import Product
|
||||
|
||||
def get_all(session: Session) -> list[Product]:
|
||||
statement = select(Product)
|
||||
return session.exec(statement).all()
|
||||
|
||||
def get_one(session: Session, product_id: int) -> Product:
|
||||
return session.get(Product, product_id)
|
||||
|
||||
def create_one(session: Session, product: Product) -> Product:
|
||||
product_create = product.model_dump(exclude_unset=True)
|
||||
new_product = Product(**product_create)
|
||||
session.add(new_product)
|
||||
session.commit()
|
||||
session.refresh(new_product)
|
||||
return new_product
|
||||
|
||||
def update_one(session: Session, id: int, product: Product) -> Product:
|
||||
statement = select(Product).where(Product.id == id)
|
||||
result = session.exec(statement)
|
||||
new_product = result.first()
|
||||
if not new_product:
|
||||
return None
|
||||
product_updates = product.model_dump(exclude_unset=True)
|
||||
for key, value in product_updates.items():
|
||||
setattr(new_product, key, value)
|
||||
session.add(new_product)
|
||||
session.commit()
|
||||
session.refresh(new_product)
|
||||
return new_product
|
||||
|
||||
def delete_one(session: Session, id: int) -> Product:
|
||||
statement = select(Product).where(Product.id == id)
|
||||
result = session.exec(statement)
|
||||
product = result.first()
|
||||
if not product:
|
||||
return None
|
||||
session.delete(product)
|
||||
session.commit()
|
||||
return product
|
||||
29
backend/src/secrets.py
Normal file
29
backend/src/secrets.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import os
|
||||
|
||||
origins = [
|
||||
os.environ.get('SERVICE_ORIGIN') or 'http://localhost'
|
||||
]
|
||||
|
||||
dbhost = os.environ.get('DB_HOST') or 'localhost'
|
||||
dbuser = os.environ.get('PGSQL_USER') or 'postgres'
|
||||
dbpass = os.environ.get('PGSQL_PASSWORD') or 'postgres'
|
||||
dbname = os.environ.get('PGSQL_DATABASE') or 'amap'
|
||||
|
||||
# openssl rand -hex 32
|
||||
SECRET_KEY = os.environ.get('SERVICE_SECRET_KEY') or 'test'
|
||||
ALGORITHM = 'HS256'
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 600
|
||||
|
||||
|
||||
KEYCLOAK_SERVER = ""
|
||||
REALM = ""
|
||||
CLIENT_ID = ""
|
||||
CLIENT_SECRET = ""
|
||||
|
||||
REDIRECT_URI = "http://localhost:8000/auth/callback"
|
||||
|
||||
AUTH_URL = f"{KEYCLOAK_SERVER}/realms/{REALM}/protocol/openid-connect/auth"
|
||||
TOKEN_URL = f"{KEYCLOAK_SERVER}/realms/{REALM}/protocol/openid-connect/token"
|
||||
|
||||
ISSUER = f"{KEYCLOAK_SERVER}/realms/{REALM}"
|
||||
JWKS_URL = f"{ISSUER}/protocol/openid-connect/certs"
|
||||
3
backend/src/templates/__init__.py
Normal file
3
backend/src/templates/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
19
backend/src/templates/templates.py
Normal file
19
backend/src/templates/templates.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(prefix='/templates')
|
||||
|
||||
@router.get('/')
|
||||
def get_templates():
|
||||
return []
|
||||
|
||||
@router.post('/')
|
||||
def create_template():
|
||||
return {}
|
||||
|
||||
@router.put('/')
|
||||
def update_template():
|
||||
return {}
|
||||
|
||||
@router.delete('/')
|
||||
def delete_template():
|
||||
return {}
|
||||
19
backend/src/users/users.py
Normal file
19
backend/src/users/users.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(prefix='/users')
|
||||
|
||||
@router.get('/')
|
||||
def get_users():
|
||||
return []
|
||||
|
||||
@router.post('/')
|
||||
def create_user():
|
||||
return {}
|
||||
|
||||
@router.put('/')
|
||||
def update_user():
|
||||
return {}
|
||||
|
||||
@router.delete('/')
|
||||
def delete_user():
|
||||
return {}
|
||||
Reference in New Issue
Block a user