fix(repairs): normalize public status tokens
This commit is contained in:
parent
09cce1f2b6
commit
3ed6596bf0
11 changed files with 35 additions and 6 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
|
|
@ -28,3 +28,7 @@ KNOWLEDGE_MAX_UPLOAD_MB=50
|
||||||
# Server-zu-Server Token fuer spaetere Website-Reparaturannahme.
|
# Server-zu-Server Token fuer spaetere Website-Reparaturannahme.
|
||||||
# Produktiv als langen Zufallswert setzen. Nicht im Frontend verwenden.
|
# Produktiv als langen Zufallswert setzen. Nicht im Frontend verwenden.
|
||||||
OLYMPUS_REPAIR_INTAKE_TOKEN=
|
OLYMPUS_REPAIR_INTAKE_TOKEN=
|
||||||
|
|
||||||
|
# Optionale Website-Basis-URL fuer oeffentliche Reparaturstatuslinks.
|
||||||
|
# Beispiel: https://test.funktechnik-schubert.de/status
|
||||||
|
PUBLIC_REPAIR_STATUS_BASE_URL=
|
||||||
|
|
|
||||||
|
|
@ -893,6 +893,9 @@ docker-compose.yml
|
||||||
`OLYMPUS_REPAIR_INTAKE_TOKEN`
|
`OLYMPUS_REPAIR_INTAKE_TOKEN`
|
||||||
: Server-zu-Server Token fuer `POST /public/repair-intake`. Dieser Wert darf nicht im Frontend oder in Logs erscheinen.
|
: Server-zu-Server Token fuer `POST /public/repair-intake`. Dieser Wert darf nicht im Frontend oder in Logs erscheinen.
|
||||||
|
|
||||||
|
`PUBLIC_REPAIR_STATUS_BASE_URL`
|
||||||
|
: Optionale Basis-URL fuer von Olympus erzeugte oeffentliche Reparaturstatuslinks, z. B. `https://test.funktechnik-schubert.de/status`. Wenn leer, gibt Hermes weiterhin relative Links im Format `/status/<token>` zurueck.
|
||||||
|
|
||||||
### Athena
|
### Athena
|
||||||
|
|
||||||
`HERMES_INTERNAL_URL`
|
`HERMES_INTERNAL_URL`
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ STORAGE_BASE_PATH=/data/storage
|
||||||
STORAGE_MAX_UPLOAD_MB=50
|
STORAGE_MAX_UPLOAD_MB=50
|
||||||
STORAGE_HOST_PATH=./storage
|
STORAGE_HOST_PATH=./storage
|
||||||
OLYMPUS_REPAIR_INTAKE_TOKEN=
|
OLYMPUS_REPAIR_INTAKE_TOKEN=
|
||||||
|
PUBLIC_REPAIR_STATUS_BASE_URL=
|
||||||
```
|
```
|
||||||
|
|
||||||
Wenn `SECRET_KEY` Sonderzeichen wie `$` enthaelt, den Wert in der Shell oder Compose-Umgebung korrekt quoten. Secrets gehoeren nicht ins Git.
|
Wenn `SECRET_KEY` Sonderzeichen wie `$` enthaelt, den Wert in der Shell oder Compose-Umgebung korrekt quoten. Secrets gehoeren nicht ins Git.
|
||||||
|
|
@ -159,6 +160,7 @@ Statuslink-Konzept:
|
||||||
- RBAC-Permission: `repairs.public_link.manage`.
|
- RBAC-Permission: `repairs.public_link.manage`.
|
||||||
- Tokens sind lang, zufaellig und werden nur gehasht gespeichert.
|
- Tokens sind lang, zufaellig und werden nur gehasht gespeichert.
|
||||||
- Der Klartexttoken wird nur einmal bei Erstellung zurueckgegeben.
|
- Der Klartexttoken wird nur einmal bei Erstellung zurueckgegeben.
|
||||||
|
- `PUBLIC_REPAIR_STATUS_BASE_URL` kann auf die Website-Route zeigen, z. B. `https://test.funktechnik-schubert.de/status`.
|
||||||
- Oeffentliche Statusdaten kommen spaeter ueber `GET /public/repairs/status/{token}`.
|
- Oeffentliche Statusdaten kommen spaeter ueber `GET /public/repairs/status/{token}`.
|
||||||
- Die Antwort enthaelt keine Kundendaten, keine internen Notizen und keine nicht freigegebenen Diagnosen.
|
- Die Antwort enthaelt keine Kundendaten, keine internen Notizen und keine nicht freigegebenen Diagnosen.
|
||||||
|
|
||||||
|
|
|
||||||
BIN
backend/.DS_Store
vendored
BIN
backend/.DS_Store
vendored
Binary file not shown.
|
|
@ -19,3 +19,4 @@ STORAGE_MAX_UPLOAD_MB=50
|
||||||
KNOWLEDGE_STORAGE_PATH=/data/knowledge
|
KNOWLEDGE_STORAGE_PATH=/data/knowledge
|
||||||
KNOWLEDGE_MAX_UPLOAD_MB=50
|
KNOWLEDGE_MAX_UPLOAD_MB=50
|
||||||
OLYMPUS_REPAIR_INTAKE_TOKEN=
|
OLYMPUS_REPAIR_INTAKE_TOKEN=
|
||||||
|
PUBLIC_REPAIR_STATUS_BASE_URL=
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ class Settings(BaseSettings):
|
||||||
knowledge_storage_path: str = "/data/knowledge"
|
knowledge_storage_path: str = "/data/knowledge"
|
||||||
knowledge_max_upload_mb: int = 50
|
knowledge_max_upload_mb: int = 50
|
||||||
olympus_repair_intake_token: str | None = None
|
olympus_repair_intake_token: str | None = None
|
||||||
|
public_repair_status_base_url: str | None = None
|
||||||
|
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
env_file=".env",
|
env_file=".env",
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,7 @@ class RepairRepository:
|
||||||
select(RepairPublicAccessToken)
|
select(RepairPublicAccessToken)
|
||||||
.where(RepairPublicAccessToken.repair_id == repair_id)
|
.where(RepairPublicAccessToken.repair_id == repair_id)
|
||||||
.where(RepairPublicAccessToken.is_active.is_(True))
|
.where(RepairPublicAccessToken.is_active.is_(True))
|
||||||
|
.where(RepairPublicAccessToken.revoked_at.is_(None))
|
||||||
.where(or_(RepairPublicAccessToken.expires_at.is_(None), RepairPublicAccessToken.expires_at > now))
|
.where(or_(RepairPublicAccessToken.expires_at.is_(None), RepairPublicAccessToken.expires_at > now))
|
||||||
.order_by(RepairPublicAccessToken.created_at.desc(), RepairPublicAccessToken.id.desc())
|
.order_by(RepairPublicAccessToken.created_at.desc(), RepairPublicAccessToken.id.desc())
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
@ -234,6 +235,7 @@ class RepairRepository:
|
||||||
.options(selectinload(RepairPublicAccessToken.repair))
|
.options(selectinload(RepairPublicAccessToken.repair))
|
||||||
.where(RepairPublicAccessToken.token_hash == token_hash)
|
.where(RepairPublicAccessToken.token_hash == token_hash)
|
||||||
.where(RepairPublicAccessToken.is_active.is_(True))
|
.where(RepairPublicAccessToken.is_active.is_(True))
|
||||||
|
.where(RepairPublicAccessToken.revoked_at.is_(None))
|
||||||
.where(or_(RepairPublicAccessToken.expires_at.is_(None), RepairPublicAccessToken.expires_at > now))
|
.where(or_(RepairPublicAccessToken.expires_at.is_(None), RepairPublicAccessToken.expires_at > now))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,24 @@ from app.services.repair_notification_service import STATUS_LABELS
|
||||||
from app.services.repair_service import repair_label
|
from app.services.repair_service import repair_label
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_token(token: str) -> str:
|
||||||
|
return token.strip()
|
||||||
|
|
||||||
|
|
||||||
def public_status_path(token: str) -> str:
|
def public_status_path(token: str) -> str:
|
||||||
return f"/status/{token}"
|
normalized_token = normalize_token(token)
|
||||||
|
path = f"/status/{normalized_token}"
|
||||||
|
base_url = (settings.public_repair_status_base_url or "").strip().rstrip("/")
|
||||||
|
if not base_url:
|
||||||
|
return path
|
||||||
|
return f"{base_url}/{normalized_token}"
|
||||||
|
|
||||||
|
|
||||||
class RepairPublicLinkService:
|
class RepairPublicLinkService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def hash_token(token: str) -> str:
|
def hash_token(token: str) -> str:
|
||||||
return hmac.new(settings.secret_key.encode("utf-8"), token.encode("utf-8"), hashlib.sha256).hexdigest()
|
normalized_token = normalize_token(token)
|
||||||
|
return hmac.new(settings.secret_key.encode("utf-8"), normalized_token.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_token() -> str:
|
def create_token() -> str:
|
||||||
|
|
@ -100,14 +110,17 @@ class RepairPublicLinkService:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def public_status(db: Session, token: str) -> RepairPublicStatusResponse:
|
def public_status(db: Session, token: str) -> RepairPublicStatusResponse:
|
||||||
public_link = RepairRepository.get_public_link_by_hash(db, RepairPublicLinkService.hash_token(token))
|
normalized_token = normalize_token(token)
|
||||||
|
if not normalized_token:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Reparaturstatus nicht gefunden")
|
||||||
|
|
||||||
|
public_link = RepairRepository.get_public_link_by_hash(db, RepairPublicLinkService.hash_token(normalized_token))
|
||||||
if public_link is None or public_link.repair is None:
|
if public_link is None or public_link.repair is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Reparaturstatus nicht gefunden")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Reparaturstatus nicht gefunden")
|
||||||
|
|
||||||
RepairRepository.mark_public_link_used(db, public_link)
|
|
||||||
repair = public_link.repair
|
repair = public_link.repair
|
||||||
history = RepairRepository.get_history_public(db, repair.id)
|
history = RepairRepository.get_history_public(db, repair.id)
|
||||||
return RepairPublicStatusResponse(
|
response = RepairPublicStatusResponse(
|
||||||
repair_number=repair.repair_number,
|
repair_number=repair.repair_number,
|
||||||
public_status_label=STATUS_LABELS.get(repair.status, repair.status),
|
public_status_label=STATUS_LABELS.get(repair.status, repair.status),
|
||||||
device_manufacturer=repair.device_manufacturer,
|
device_manufacturer=repair.device_manufacturer,
|
||||||
|
|
@ -122,3 +135,5 @@ class RepairPublicLinkService:
|
||||||
],
|
],
|
||||||
updated_at=repair.updated_at,
|
updated_at=repair.updated_at,
|
||||||
)
|
)
|
||||||
|
RepairRepository.mark_public_link_used(db, public_link)
|
||||||
|
return response
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ services:
|
||||||
KNOWLEDGE_STORAGE_PATH: ${KNOWLEDGE_STORAGE_PATH:-/data/knowledge}
|
KNOWLEDGE_STORAGE_PATH: ${KNOWLEDGE_STORAGE_PATH:-/data/knowledge}
|
||||||
KNOWLEDGE_MAX_UPLOAD_MB: ${KNOWLEDGE_MAX_UPLOAD_MB:-50}
|
KNOWLEDGE_MAX_UPLOAD_MB: ${KNOWLEDGE_MAX_UPLOAD_MB:-50}
|
||||||
OLYMPUS_REPAIR_INTAKE_TOKEN: ${OLYMPUS_REPAIR_INTAKE_TOKEN:-}
|
OLYMPUS_REPAIR_INTAKE_TOKEN: ${OLYMPUS_REPAIR_INTAKE_TOKEN:-}
|
||||||
|
PUBLIC_REPAIR_STATUS_BASE_URL: ${PUBLIC_REPAIR_STATUS_BASE_URL:-}
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${STORAGE_HOST_PATH:-./storage}:/data/storage
|
- ${STORAGE_HOST_PATH:-./storage}:/data/storage
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit a284bbc1886b26091bea7ea7a5aacee71b4507fe
|
Subproject commit efdc4d5be10e921f15eaa6a8de7547e0d8893dc4
|
||||||
Loading…
Add table
Add a link
Reference in a new issue