Add authentification

This commit is contained in:
2026-02-17 00:54:36 +01:00
parent ab98ba81c8
commit a8c8c489da
31 changed files with 1118 additions and 451 deletions

View File

@@ -1,91 +1,140 @@
from fastapi import APIRouter, Security, HTTPException, Depends
from fastapi.responses import RedirectResponse
from fastapi import APIRouter, Security, HTTPException, Depends, Request
from fastapi.responses import RedirectResponse, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlmodel import Session
from sqlmodel import Session, select
import jwt
from jwt import PyJWKClient
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
from src.models import UserCreate, User, UserPublic
import secrets
import jwt
from jwt import PyJWKClient
import requests
from src.messages import tokenExpired, invalidToken
import src.messages as messages
router = APIRouter(prefix="/auth")
router = APIRouter(prefix='/auth')
jwk_client = PyJWKClient(JWKS_URL)
security = HTTPBearer()
@router.post('/logout')
def logout(response: Response):
response.delete_cookie('access_token')
response.delete_cookie('refresh_token')
return {'detail': messages.userloggedout}
@router.get('/login')
def login():
state = secrets.token_urlsafe(16)
params = {
"client_id": settings.keycloak_client_id,
"response_type": "code",
"scope": "openid",
"redirect_uri": settings.keycloak_redirect_uri,
"state": state,
'client_id': settings.keycloak_client_id,
'response_type': 'code',
'scope': 'openid',
'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")
@router.get('/callback')
def callback(code: str, session: Session = Depends(get_session)):
data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": settings.keycloak_redirect_uri,
"client_id": settings.keycloak_client_id,
"client_secret": settings.keycloak_client_secret,
'grant_type': 'authorization_code',
'code': code,
'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"
'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
raise HTTPException(
status_code=400,
detail=messages.failtogettoken
)
token_data = response.json()
id_token = token_data["id_token"]
decoded_token = jwt.decode(id_token, options={"verify_signature": False})
id_token = token_data['id_token']
decoded_token = jwt.decode(id_token, options={'verify_signature': False})
decoded_access_token = jwt.decode(token_data['access_token'], options={'verify_signature': False})
roles = decoded_access_token['resource_access'][settings.keycloak_client_id]
user_create = UserCreate(
email=decoded_token.get("email"),
name=decoded_token.get("preferred_username")
email=decoded_token.get('email'),
name=decoded_token.get('preferred_username'),
role_names=roles['roles']
)
user = service.get_or_create_user(session, user_create)
return {
"access_token": token_data["access_token"],
"id_token": token_data["id_token"],
"refresh_token": token_data["refresh_token"],
}
service.get_or_create_user(session, user_create)
response = RedirectResponse(settings.origins)
response.set_cookie(
key='access_token',
value=token_data['access_token'],
httponly=True,
secure=True if settings.debug == False else True,
samesite='strict',
max_age=settings.max_age
)
response.set_cookie(
key='refresh_token',
value=token_data['refresh_token'] or '',
httponly=True,
secure=True if settings.debug == False else True,
samesite='strict',
max_age=30 * 24 * settings.max_age
)
return response
def verify_token(token: str):
try:
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})
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
algorithms=['RS256'],
audience=settings.keycloak_client_id,
issuer=ISSUER,
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail=tokenExpired)
raise HTTPException(status_code=401, detail=messages.tokenexipired)
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail=invalidToken)
raise HTTPException(status_code=401, detail=messages.invalidtoken)
def get_current_user(
credentials: HTTPAuthorizationCredentials = Security(security)
):
return verify_token(credentials.credentials)
def get_current_user(request: Request, session: Session = Depends(get_session)):
access_token = request.cookies.get("access_token")
if not access_token:
raise HTTPException(status_code=401, detail=messages.notauthenticated)
payload = verify_token(access_token)
if not payload:
raise HTTPException(status_code=401, detail="aze")
email = payload.get('email')
if not email:
raise HTTPException(status_code=401, detail=messages.notauthenticated)
user = session.exec(select(User).where(User.email == email)).first()
if not user:
raise HTTPException(status_code=401, detail=messages.usernotfound)
return user
@router.get('/user/me')
def me(user: UserPublic = Depends(get_current_user)):
if not user:
return {"logged": False}
return {
"logged": True,
"user": {
"name": user.name,
"email": user.email,
"id": user.id,
"roles": [role.name for role in user.roles]
}
}

View File

@@ -3,12 +3,13 @@ from fastapi.responses import StreamingResponse
from src.database import get_session
from sqlmodel import Session
from src.contracts.generate_contract import generate_html_contract
from src.auth.auth import get_current_user
import src.models as models
from src.messages import PDFerrorOccured
import src.messages as messages
import src.contracts.service as service
import src.forms.service as form_service
import io
import zipfile
router = APIRouter(prefix='/contracts')
def compute_recurrent_prices(products_quantities: list[dict], nb_shipment: int):
@@ -71,7 +72,8 @@ def create_occasional_dict(contract_products: list[models.ContractProduct]):
@router.post('/')
async def create_contract(
contract: models.ContractCreate,
session: Session = Depends(get_session)
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
new_contract = service.create_one(session, contract)
occasional_contract_products = list(filter(lambda contract_product: contract_product.product.type == models.ProductType.OCCASIONAL, new_contract.products))
@@ -93,50 +95,77 @@ async def create_contract(
)
pdf_file = io.BytesIO(pdf_bytes)
contract_id = f'{new_contract.firstname}_{new_contract.lastname}_{new_contract.form.productor.type}_{new_contract.form.season}'
service.add_contract_file(session, id, pdf_bytes)
except:
raise HTTPException(status_code=400, detail=PDFerrorOccured)
service.add_contract_file(session, new_contract.id, pdf_bytes)
except Exception as e:
print(e)
raise HTTPException(status_code=400, detail=messages.pdferror)
return StreamingResponse(
pdf_file,
media_type='application/pdf',
headers={
'Content-Disposition': f'attachement; filename=contract_{contract_id}.pdf'
'Content-Disposition': f'attachment; filename=contract_{contract_id}.pdf'
}
)
@router.get('/', response_model=list[models.ContractPublic])
def get_contracts(
forms: list[str] = Query([]),
session: Session = Depends(get_session)
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
return service.get_all(session, forms)
@router.get('/{id}/file')
def get_contract_file(
id: int,
session: Session = Depends(get_session)
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
contract = service.get_one(session, id)
print(contract.file)
if contract is None:
raise HTTPException(status_code=404, detail=messages.notfound)
filename = f'{contract.form.name.replace(' ', '_')}_{contract.form.season}_{contract.firstname}-{contract.lastname}'
return StreamingResponse(
contract.file,
io.BytesIO(contract.file),
media_type='application/pdf',
headers={
'Content-Disposition': f'attachement; filename=contract_{contract.id}.pdf'
'Content-Disposition': f'attachment; filename={filename}.pdf'
}
)
@router.get('/{form_id}/files')
def get_contract_files(
form_id: int,
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
form = form_service.get_one(session, form_id=form_id)
contracts = service.get_all(session, [form.name])
zipped_contracts = io.BytesIO()
with zipfile.ZipFile(zipped_contracts, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
for contract in contracts:
contract_filename = f'{contract.form.name.replace(' ', '_')}_{contract.form.season}_{contract.firstname}-{contract.lastname}.pdf'
zip_file.writestr(contract_filename, contract.file)
filename = f'{form.name.replace(" ", "_")}_{form.season}'
return StreamingResponse(
io.BytesIO(zipped_contracts.getvalue()),
media_type='application/zip',
headers={
'Content-Disposition': f'attachment; filename={filename}.zip'
}
)
@router.get('/{id}', response_model=models.ContractPublic)
def get_contract(id: int, session: Session = Depends(get_session)):
def get_contract(id: int, session: Session = Depends(get_session), user: models.User = Depends(get_current_user)):
result = service.get_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result
@router.delete('/{id}', response_model=models.ContractPublic)
def delete_contract(id: int, session: Session = Depends(get_session)):
def delete_contract(id: int, session: Session = Depends(get_session), user: models.User = Depends(get_current_user)):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)

View File

@@ -3,9 +3,12 @@ import src.models as models
def get_all(
session: Session,
forms: list[str]
forms: list[str] = [],
form_id: int | None = None,
) -> list[models.ContractPublic]:
statement = select(models.Contract)
if form_id:
statement = statement.join(models.Form).where(models.Form.id == form_id)
if len(forms) > 0:
statement = statement.join(models.Form).where(models.Form.name.in_(forms))
return session.exec(statement.order_by(models.Contract.id)).all()

View File

@@ -4,6 +4,7 @@ import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.forms.service as service
from src.auth.auth import get_current_user
router = APIRouter(prefix='/forms')
@@ -23,18 +24,30 @@ async def get_form(id: int, session: Session = Depends(get_session)):
return result
@router.post('/', response_model=models.FormPublic)
async def create_form(form: models.FormCreate, session: Session = Depends(get_session)):
async def create_form(
form: models.FormCreate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.create_one(session, form)
@router.put('/{id}', response_model=models.FormPublic)
async def update_form(id: int, form: models.FormUpdate, session: Session = Depends(get_session)):
async def update_form(
id: int, form: models.FormUpdate,
user: models.User = Depends(get_current_user),
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)
async def delete_form(id: int, session: Session = Depends(get_session)):
async def delete_form(
id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)

View File

@@ -22,7 +22,7 @@ app.add_middleware(
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=['x-nbpage']
expose_headers=['x-nbpage', 'Content-Disposition']
)

View File

@@ -1,4 +1,8 @@
notfound = "Resource was not found."
PDFerrorOccured = "An error occured during PDF generation please contact administrator"
tokenExpired = "Token expired"
invalidToken = "Invalid token"
pdferror = "An error occured during PDF generation please contact administrator"
tokenexipired = "Token expired"
invalidtoken = "Invalid token"
notauthenticated = "Not authenticated"
usernotfound = "User not found"
userloggedout = "User logged out"
failtogettoken = "Failed to get token"

View File

@@ -3,22 +3,35 @@ from enum import StrEnum
from typing import Optional
import datetime
class ContractType(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
class UserContractTypeLink(SQLModel, table=True):
user_id: int = Field(foreign_key="user.id", primary_key=True)
contract_type_id: int = Field(foreign_key="contracttype.id", primary_key=True)
class UserBase(SQLModel):
name: str
email: str
class UserPublic(UserBase):
id: int
roles: list[ContractType]
class User(UserBase, table=True):
id: int | None = Field(default=None, primary_key=True)
roles: list[ContractType] = Relationship(
link_model=UserContractTypeLink
)
class UserUpdate(SQLModel):
name: str | None
email: str | None
role_names: list[str] | None
class UserCreate(UserBase):
pass
role_names: list[str] | None
class PaymentMethodBase(SQLModel):
name: str

View File

@@ -4,6 +4,7 @@ import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.productors.service as service
from src.auth.auth import get_current_user
router = APIRouter(prefix='/productors')
@@ -11,30 +12,47 @@ router = APIRouter(prefix='/productors')
def get_productors(
names: list[str] = Query([]),
types: list[str] = Query([]),
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.get_all(session, names, types)
@router.get('/{id}', response_model=models.ProductorPublic)
def get_productor(id: int, session: Session = Depends(get_session)):
def get_productor(
id: int,
user: models.User = Depends(get_current_user),
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)):
def create_productor(
productor: models.ProductorCreate,
user: models.User = Depends(get_current_user),
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)):
def update_productor(
id: int, productor: models.ProductorUpdate,
user: models.User = Depends(get_current_user),
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)):
def delete_productor(
id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)

View File

@@ -9,6 +9,7 @@ router = APIRouter(prefix='/products')
#user=Depends(get_current_user)
@router.get('/', response_model=list[models.ProductPublic], )
def get_products(
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session),
names: list[str] = Query([]),
types: list[str] = Query([]),
@@ -22,25 +23,41 @@ def get_products(
)
@router.get('/{id}', response_model=models.ProductPublic)
def get_product(id: int, session: Session = Depends(get_session)):
def get_product(
id: int,
user: models.User = Depends(get_current_user),
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)):
def create_product(
product: models.ProductCreate,
user: models.User = Depends(get_current_user),
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)):
def update_product(
id: int, product: models.ProductUpdate,
user: models.User = Depends(get_current_user),
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)):
def delete_product(
id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)

View File

@@ -13,6 +13,8 @@ class Settings(BaseSettings):
keycloak_client_secret: str
keycloak_redirect_uri: str
vite_api_url: str
max_age: int
debug: bool
class Config:
env_file = "../.env"

View File

@@ -23,25 +23,41 @@ def get_shipments(
)
@router.get('/{id}', response_model=models.ShipmentPublic)
def get_shipment(id: int, session: Session = Depends(get_session)):
def get_shipment(
id: int,
user: models.User = Depends(get_current_user),
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)):
def create_shipment(
shipment: models.ShipmentCreate,
user: models.User = Depends(get_current_user),
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)):
def update_shipment(
id: int, shipment: models.ShipmentUpdate,
user: models.User = Depends(get_current_user),
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)):
def delete_shipment(
id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)

View File

@@ -4,33 +4,53 @@ import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.templates.service as service
from src.auth.auth import get_current_user
router = APIRouter(prefix='/templates')
@router.get('/', response_model=list[models.TemplatePublic])
def get_templates(session: Session = Depends(get_session)):
def get_templates(
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.get_all(session)
@router.get('/{id}', response_model=models.TemplatePublic)
def get_template(id: int, session: Session = Depends(get_session)):
def get_template(
id: int,
user: models.User = Depends(get_current_user),
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.TemplatePublic)
def create_template(template: models.TemplateCreate, session: Session = Depends(get_session)):
def create_template(
template: models.TemplateCreate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.create_one(session, template)
@router.put('/{id}', response_model=models.TemplatePublic)
def update_template(id: int, template: models.TemplateUpdate, session: Session = Depends(get_session)):
def update_template(
id: int, template: models.TemplateUpdate,
user: models.User = Depends(get_current_user),
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)):
def delete_template(
id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)

View File

@@ -16,6 +16,23 @@ def get_all(
def get_one(session: Session, user_id: int) -> models.UserPublic:
return session.get(models.User, user_id)
def get_or_create_roles(session: Session, role_names) -> list[models.ContractType]:
statement = select(models.ContractType).where(models.ContractType.name.in_(role_names))
existing = session.exec(statement).all()
existing_roles = {role.name for role in existing}
missing_role = set(role_names) - existing_roles
new_roles = []
for role_name in missing_role:
role = models.ContractType(name=role_name)
session.add(role)
new_roles.append(role)
session.commit()
for role in new_roles:
session.refresh(role)
return existing + new_roles
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()
@@ -24,9 +41,20 @@ def get_or_create_user(session: Session, user_create: models.UserCreate):
user = create_one(session, user_create)
return user
def get_roles(session: Session):
statement = select(models.ContractType)
return session.exec(statement.order_by(models.ContractType.name)).all()
def create_one(session: Session, user: models.UserCreate) -> models.UserPublic:
user_create = user.model_dump(exclude_unset=True)
new_user = models.User(**user_create)
print("USER CREATE", user)
new_user = models.User(
name=user.name,
email=user.email
)
roles = get_or_create_roles(session, user.role_names)
new_user.roles = roles
session.add(new_user)
session.commit()
session.refresh(new_user)

View File

@@ -4,12 +4,14 @@ import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.users.service as service
from src.auth.auth import get_current_user
router = APIRouter(prefix='/users')
@router.get('/', response_model=list[models.UserPublic])
def get_users(
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user),
names: list[str] = Query([]),
emails: list[str] = Query([]),
):
@@ -19,26 +21,50 @@ def get_users(
emails,
)
@router.get('/roles', response_model=list[models.ContractType])
def get_roles(
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.get_roles(session)
@router.get('/{id}', response_model=models.UserPublic)
def get_users(id: int, session: Session = Depends(get_session)):
def get_users(
id: int,
user: models.User = Depends(get_current_user),
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.UserPublic)
def create_user(user: models.UserCreate, session: Session = Depends(get_session)):
def create_user(
user: models.UserCreate,
logged_user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.create_one(session, user)
@router.put('/{id}', response_model=models.UserPublic)
def update_user(id: int, user: models.UserUpdate, session: Session = Depends(get_session)):
def update_user(
id: int,
user: models.UserUpdate,
logged_user: models.User = Depends(get_current_user),
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)):
def delete_user(
id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)