fix(auth): correct local cookie security configuration
This commit is contained in:
parent
b584e60273
commit
155fdbb16a
67 changed files with 1003 additions and 62 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -5,7 +5,9 @@ from datetime import UTC, datetime
|
|||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.security import create_access_token, verify_password
|
||||
from app.models.user import User
|
||||
from app.repositories.domain import UserRepository
|
||||
|
||||
|
||||
|
|
@ -13,14 +15,16 @@ class AuthService:
|
|||
def __init__(self, session: Session) -> None:
|
||||
self.users = UserRepository(session)
|
||||
|
||||
def login(self, email: str, password: str) -> str:
|
||||
def login(self, email: str, password: str) -> tuple[str, User]:
|
||||
user = self.users.by_email(email)
|
||||
if user is None or not user.is_active or not verify_password(password, user.password_hash):
|
||||
if user is None or not verify_password(password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user")
|
||||
user.last_login_at = datetime.now(UTC)
|
||||
self.users.session.commit()
|
||||
return create_access_token(user.id, user.role)
|
||||
return create_access_token(user.id, user.role), user
|
||||
|
|
|
|||
|
|
@ -0,0 +1,380 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.contact import Contact
|
||||
from app.models.customer import Customer, CustomerType
|
||||
from app.models.device import Device
|
||||
from app.models.equipment import Equipment, EquipmentKind, EquipmentStatus
|
||||
from app.models.location import Location
|
||||
from app.models.validation import Validation, ValidationStatus
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImportResult:
|
||||
created: list[str] = field(default_factory=list)
|
||||
updated: list[str] = field(default_factory=list)
|
||||
unchanged: list[str] = field(default_factory=list)
|
||||
conflicts: list[str] = field(default_factory=list)
|
||||
errors: list[str] = field(default_factory=list)
|
||||
validation_id: str | None = None
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"created": self.created,
|
||||
"updated": self.updated,
|
||||
"unchanged": self.unchanged,
|
||||
"conflicts": self.conflicts,
|
||||
"errors": self.errors,
|
||||
"validation_id": self.validation_id,
|
||||
}
|
||||
|
||||
|
||||
class ReferenceMasterdataImportService:
|
||||
def __init__(self, session: Session) -> None:
|
||||
self.session = session
|
||||
|
||||
def import_reference_docx(
|
||||
self,
|
||||
reference_path: Path,
|
||||
*,
|
||||
dry_run: bool = False,
|
||||
update_existing: bool = False,
|
||||
create_validation: bool = False,
|
||||
) -> ImportResult:
|
||||
if not reference_path.exists():
|
||||
raise FileNotFoundError(reference_path)
|
||||
|
||||
result = ImportResult()
|
||||
payload = self._payload()
|
||||
|
||||
customer = self._upsert_customer(payload["customer"], result, dry_run, update_existing)
|
||||
location = self._upsert_location(customer, payload["location"], result, dry_run, update_existing)
|
||||
self._upsert_contacts(customer, payload["contacts"], result, dry_run, update_existing)
|
||||
device = self._upsert_device(customer, location, payload["device"], result, dry_run, update_existing)
|
||||
self._upsert_equipment(payload["equipment"], result, dry_run, update_existing)
|
||||
|
||||
if create_validation:
|
||||
validation = self._create_validation(customer, location, device, result, dry_run)
|
||||
result.validation_id = validation.id if validation is not None else None
|
||||
|
||||
if not dry_run:
|
||||
self.session.commit()
|
||||
|
||||
return result
|
||||
|
||||
def _payload(self) -> dict[str, Any]:
|
||||
return {
|
||||
"customer": {
|
||||
"name": "Urologische Praxis Dr. Durmaz",
|
||||
"customer_type": CustomerType.practice,
|
||||
"specialty": "Urologie",
|
||||
"display_name": "Urologie Dr. Durmaz",
|
||||
},
|
||||
"location": {
|
||||
"name": "Praxis Nürnberg",
|
||||
"street": "Wölckernstr. 5",
|
||||
"postal_code": "90459",
|
||||
"city": "Nürnberg",
|
||||
"country": "Deutschland",
|
||||
},
|
||||
"contacts": [
|
||||
{
|
||||
"full_name": "Dr. Durmaz",
|
||||
"function": "Verantwortlicher Betreiber",
|
||||
"notes": "Arzt; QM-Mitverantwortlicher",
|
||||
},
|
||||
{
|
||||
"full_name": "Frau Bal",
|
||||
"function": "QM-Beauftragte",
|
||||
"notes": "Hygienebeauftragte; Sachkunde A / Fachkenntnisse Aufbereitung und Freigabe von Medizinprodukten",
|
||||
},
|
||||
],
|
||||
"device": {
|
||||
"manufacturer": "Euronda SpA",
|
||||
"model": "E10.7",
|
||||
"device_type": "Dampf-Kleinsterilisator Klasse B",
|
||||
"serial_number": "EXN250688",
|
||||
"year_built": 2025,
|
||||
"commissioned_on": date(2025, 12, 5),
|
||||
"chamber_volume_liters": 23,
|
||||
"steam_generation": "Eigendampferzeugung",
|
||||
"water_treatment": "Wasserversorgung über Aquafilter Euronda",
|
||||
"documentation": "interne CF-Card; Protokollausgabe am PC/Rechner möglich",
|
||||
"supplier": "schubamed-Medizintechnik, 92421 Schwandorf",
|
||||
"notes": "Sterilisationsverfahren: Konditionierung mit Wasserdampf; Konditionierung teilweise oberhalb und unterhalb des Umgebungsdruckes; Chargendokumentation über interne CF-Card.",
|
||||
},
|
||||
"equipment": [
|
||||
{
|
||||
"kind": EquipmentKind.temperature_logger,
|
||||
"manufacturer": "Ebro",
|
||||
"model": "EBI 11",
|
||||
"serial_number": "15102807",
|
||||
"calibrated_on": date(2025, 1, 17),
|
||||
"status": EquipmentStatus.green,
|
||||
"notes": "Bezeichnung T235; Messbereich 0 °C bis +150 °C",
|
||||
},
|
||||
{
|
||||
"kind": EquipmentKind.temperature_logger,
|
||||
"manufacturer": "Ebro",
|
||||
"model": "EBI 11",
|
||||
"serial_number": "15211066",
|
||||
"calibrated_on": date(2025, 1, 17),
|
||||
"status": EquipmentStatus.green,
|
||||
"notes": "Bezeichnung T240; Messbereich 0 °C bis +150 °C",
|
||||
},
|
||||
{
|
||||
"kind": EquipmentKind.temperature_logger,
|
||||
"manufacturer": "Ebro",
|
||||
"model": "EBI 11",
|
||||
"serial_number": "15102538",
|
||||
"calibrated_on": date(2025, 1, 17),
|
||||
"status": EquipmentStatus.green,
|
||||
"notes": "Bezeichnung T240; Messbereich 0 °C bis +150 °C",
|
||||
},
|
||||
{
|
||||
"kind": EquipmentKind.temperature_logger,
|
||||
"manufacturer": "Ebro",
|
||||
"model": "EBI 11",
|
||||
"serial_number": "15211067",
|
||||
"calibrated_on": date(2025, 1, 17),
|
||||
"status": EquipmentStatus.green,
|
||||
"notes": "Bezeichnung T240; Messbereich 0 °C bis +150 °C",
|
||||
},
|
||||
{
|
||||
"kind": EquipmentKind.temperature_logger,
|
||||
"manufacturer": "Ebro",
|
||||
"model": "EBI 11",
|
||||
"serial_number": "15125738",
|
||||
"calibrated_on": date(2025, 1, 17),
|
||||
"status": EquipmentStatus.green,
|
||||
"notes": "Bezeichnung T240; Quellenkonflikt: Referenz nennt auch 1525738, mehrfach belegt ist 15125738.",
|
||||
},
|
||||
{
|
||||
"kind": EquipmentKind.pressure_logger,
|
||||
"manufacturer": "Ebro",
|
||||
"model": "EBI 11",
|
||||
"serial_number": "P111",
|
||||
"calibrated_on": date(2025, 1, 17),
|
||||
"status": EquipmentStatus.green,
|
||||
"notes": "Drucklogger",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def _normalize(self, value: str) -> str:
|
||||
return re.sub(r"[^a-z0-9]+", "", value.lower())
|
||||
|
||||
def _upsert_customer(self, payload: dict[str, Any], result: ImportResult, dry_run: bool, update_existing: bool) -> Customer:
|
||||
target_name = self._normalize(payload["name"])
|
||||
customer = next(
|
||||
(item for item in self.session.scalars(select(Customer)) if self._normalize(item.name) == target_name),
|
||||
None,
|
||||
)
|
||||
if customer is None:
|
||||
customer = Customer(customer_type=payload["customer_type"], name=payload["name"])
|
||||
customer.notes = f"Fachrichtung: {payload['specialty']}\nAnzeigename: {payload['display_name']}"
|
||||
if not dry_run:
|
||||
self.session.add(customer)
|
||||
self.session.flush()
|
||||
result.created.append("customer")
|
||||
return customer
|
||||
if update_existing:
|
||||
changed = False
|
||||
if customer.notes != f"Fachrichtung: {payload['specialty']}\nAnzeigename: {payload['display_name']}":
|
||||
customer.notes = f"Fachrichtung: {payload['specialty']}\nAnzeigename: {payload['display_name']}"
|
||||
changed = True
|
||||
if customer.customer_type != payload["customer_type"]:
|
||||
customer.customer_type = payload["customer_type"]
|
||||
changed = True
|
||||
if changed:
|
||||
result.updated.append("customer")
|
||||
else:
|
||||
result.unchanged.append("customer")
|
||||
else:
|
||||
result.unchanged.append("customer")
|
||||
return customer
|
||||
|
||||
def _upsert_location(self, customer: Customer, payload: dict[str, Any], result: ImportResult, dry_run: bool, update_existing: bool) -> Location:
|
||||
location = next(
|
||||
(
|
||||
item
|
||||
for item in self.session.scalars(select(Location).where(Location.customer_id == customer.id))
|
||||
if item.street == payload["street"]
|
||||
and item.postal_code == payload["postal_code"]
|
||||
and item.city == payload["city"]
|
||||
),
|
||||
None,
|
||||
)
|
||||
if location is None:
|
||||
location = Location(
|
||||
customer_id=customer.id,
|
||||
name=payload["name"],
|
||||
street=payload["street"],
|
||||
postal_code=payload["postal_code"],
|
||||
city=payload["city"],
|
||||
)
|
||||
if not dry_run:
|
||||
self.session.add(location)
|
||||
self.session.flush()
|
||||
result.created.append("location")
|
||||
return location
|
||||
if update_existing:
|
||||
location.name = payload["name"]
|
||||
result.updated.append("location")
|
||||
else:
|
||||
result.unchanged.append("location")
|
||||
return location
|
||||
|
||||
def _upsert_contacts(
|
||||
self,
|
||||
customer: Customer,
|
||||
contacts: list[dict[str, Any]],
|
||||
result: ImportResult,
|
||||
dry_run: bool,
|
||||
update_existing: bool,
|
||||
) -> None:
|
||||
for payload in contacts:
|
||||
contact = self.session.scalar(
|
||||
select(Contact).where(
|
||||
Contact.customer_id == customer.id,
|
||||
Contact.full_name == payload["full_name"],
|
||||
)
|
||||
)
|
||||
notes = payload["notes"]
|
||||
if contact is None:
|
||||
contact = Contact(
|
||||
customer_id=customer.id,
|
||||
full_name=payload["full_name"],
|
||||
function=payload["function"],
|
||||
notes=notes,
|
||||
)
|
||||
if not dry_run:
|
||||
self.session.add(contact)
|
||||
self.session.flush()
|
||||
result.created.append(f"contact:{payload['full_name']}")
|
||||
continue
|
||||
if update_existing:
|
||||
contact.function = payload["function"]
|
||||
contact.notes = notes
|
||||
result.updated.append(f"contact:{payload['full_name']}")
|
||||
else:
|
||||
result.unchanged.append(f"contact:{payload['full_name']}")
|
||||
|
||||
def _upsert_device(
|
||||
self,
|
||||
customer: Customer,
|
||||
location: Location,
|
||||
payload: dict[str, Any],
|
||||
result: ImportResult,
|
||||
dry_run: bool,
|
||||
update_existing: bool,
|
||||
) -> Device:
|
||||
device = self.session.scalar(select(Device).where(Device.serial_number == payload["serial_number"]))
|
||||
if device is None:
|
||||
device = Device(
|
||||
customer_id=customer.id,
|
||||
location_id=location.id,
|
||||
manufacturer=payload["manufacturer"],
|
||||
model=payload["model"],
|
||||
device_type=payload["device_type"],
|
||||
serial_number=payload["serial_number"],
|
||||
year_built=payload["year_built"],
|
||||
commissioned_on=payload["commissioned_on"],
|
||||
chamber_volume_liters=payload["chamber_volume_liters"],
|
||||
steam_generation=payload["steam_generation"],
|
||||
water_treatment=payload["water_treatment"],
|
||||
documentation=payload["documentation"],
|
||||
supplier=payload["supplier"],
|
||||
notes=payload["notes"],
|
||||
)
|
||||
if not dry_run:
|
||||
self.session.add(device)
|
||||
self.session.flush()
|
||||
result.created.append("device")
|
||||
return device
|
||||
if update_existing:
|
||||
for key, value in payload.items():
|
||||
if hasattr(device, key):
|
||||
setattr(device, key, value)
|
||||
result.updated.append("device")
|
||||
else:
|
||||
result.unchanged.append("device")
|
||||
return device
|
||||
|
||||
def _upsert_equipment(
|
||||
self,
|
||||
items: list[dict[str, Any]],
|
||||
result: ImportResult,
|
||||
dry_run: bool,
|
||||
update_existing: bool,
|
||||
) -> None:
|
||||
for payload in items:
|
||||
equipment = self.session.scalar(
|
||||
select(Equipment).where(Equipment.serial_number == payload["serial_number"])
|
||||
)
|
||||
if equipment is None:
|
||||
equipment = Equipment(
|
||||
kind=payload["kind"],
|
||||
manufacturer=payload["manufacturer"],
|
||||
model=payload["model"],
|
||||
serial_number=payload["serial_number"],
|
||||
calibrated_on=payload["calibrated_on"],
|
||||
status=payload["status"],
|
||||
notes=payload["notes"],
|
||||
)
|
||||
if not dry_run:
|
||||
self.session.add(equipment)
|
||||
self.session.flush()
|
||||
result.created.append(f"equipment:{payload['serial_number']}")
|
||||
continue
|
||||
if update_existing:
|
||||
equipment.kind = payload["kind"]
|
||||
equipment.manufacturer = payload["manufacturer"]
|
||||
equipment.model = payload["model"]
|
||||
equipment.calibrated_on = payload["calibrated_on"]
|
||||
equipment.status = payload["status"]
|
||||
equipment.notes = payload["notes"]
|
||||
result.updated.append(f"equipment:{payload['serial_number']}")
|
||||
else:
|
||||
result.unchanged.append(f"equipment:{payload['serial_number']}")
|
||||
|
||||
def _create_validation(
|
||||
self,
|
||||
customer: Customer,
|
||||
location: Location,
|
||||
device: Device,
|
||||
result: ImportResult,
|
||||
dry_run: bool,
|
||||
) -> Validation | None:
|
||||
existing = self.session.scalar(
|
||||
select(Validation).where(
|
||||
Validation.customer_id == customer.id,
|
||||
Validation.device_id == device.id,
|
||||
Validation.validation_type == "Erstvalidierung",
|
||||
)
|
||||
)
|
||||
if existing is not None:
|
||||
result.unchanged.append("validation")
|
||||
return existing
|
||||
validation = Validation(
|
||||
report_number="REF-IMPORT-1",
|
||||
customer_id=customer.id,
|
||||
location_id=location.id,
|
||||
device_id=device.id,
|
||||
validation_type="Erstvalidierung",
|
||||
status=ValidationStatus.draft.value,
|
||||
examiner_name="nicht erfasst",
|
||||
)
|
||||
if not dry_run:
|
||||
self.session.add(validation)
|
||||
self.session.flush()
|
||||
result.created.append("validation")
|
||||
return validation
|
||||
Loading…
Add table
Add a link
Reference in a new issue