diff --git a/backend/app/main.py b/backend/app/main.py
index 644d60e..ccec2e6 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -1,4 +1,4 @@
-from fastapi import FastAPI, Request, APIRouter
+from fastapi import FastAPI, Request, APIRouter, Depends
from fastapi.responses import JSONResponse, Response
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
@@ -6,6 +6,7 @@
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from .routers import auth, useCases, subUseCases, roles, transactions, properties, propertyGroups, users, ontology, standards
+from .crud.auth import get_current_user_from_token
from .config import IMAGE_DIR, get_settings
from .exceptions import VersionConflictError
import logging
@@ -76,15 +77,16 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
secret_key=get_settings().session_secret_key,
)
-api = APIRouter(prefix="/api")
+api = APIRouter(prefix="/api", dependencies=[Depends(get_current_user_from_token)])
+
-@api.get("/health")
-def health() -> Response:
- return Response(content="ok\n", media_type="text/plain")
app.mount("/static/images", StaticFiles(directory=IMAGE_DIR), name="images")
-api.include_router(auth.auth_router)
+# Public auth endpoints (no token required)
+app.include_router(auth.auth_router, prefix="/api")
+
+# Authenticated API routes
api.include_router(users.user_router)
api.include_router(useCases.router)
api.include_router(subUseCases.router)
@@ -95,3 +97,8 @@ def health() -> Response:
api.include_router(ontology.router)
api.include_router(standards.router)
app.include_router(api)
+
+# Public health endpoint (no auth required)
+@app.get("/api/health")
+def health() -> Response:
+ return Response(content="ok\n", media_type="text/plain")
diff --git a/backend/app/routers/properties.py b/backend/app/routers/properties.py
index a514371..af94d46 100644
--- a/backend/app/routers/properties.py
+++ b/backend/app/routers/properties.py
@@ -2,7 +2,7 @@
from sqlalchemy.orm import Session
from ..schemas.logs import VersionInfo
from ..models.users import User
-from ..crud.auth import get_current_user_from_token, require_role, RoleEnum
+from ..crud.auth import require_role, RoleEnum
from ..db import get_db
from ..crud import properties as crud
from ..schemas.properties import *
@@ -37,13 +37,13 @@ def get_property_version(uuid: UUID, db: Session = Depends(get_db)):
# --------CREATE-------
@router.post("/", response_model=PropertyResponse, status_code=201)
-def create_property(prop: PropertyCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def create_property(prop: PropertyCreate, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
return crud.create_property(db, prop, current_user.username)
# --------UPDATE-------
@router.put("/{uuid}", response_model=PropertyResponse)
-def update_property(uuid: UUID, prop: PropertyUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def update_property(uuid: UUID, prop: PropertyUpdate, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
updated = crud.update_property(db, uuid, prop, current_user.username)
if not updated:
raise HTTPException(status_code=404, detail="Property not found")
@@ -52,7 +52,7 @@ def update_property(uuid: UUID, prop: PropertyUpdate, db: Session = Depends(get_
# --------DELETE-------
@router.delete("/{uuid}", status_code=204)
-def delete_property(uuid: UUID, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def delete_property(uuid: UUID, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
success = crud.delete_property(db, uuid, current_user.username)
if not success:
raise HTTPException(status_code=404, detail="Property not found")
diff --git a/backend/app/routers/propertyGroups.py b/backend/app/routers/propertyGroups.py
index b5b53af..0946825 100644
--- a/backend/app/routers/propertyGroups.py
+++ b/backend/app/routers/propertyGroups.py
@@ -2,7 +2,7 @@
from sqlalchemy.orm import Session
from ..db import get_db
from ..models.users import User
-from ..crud.auth import get_current_user_from_token, require_role, RoleEnum
+from ..crud.auth import require_role, RoleEnum
from ..crud import propertyGroups as crud
from ..schemas.propertyGroups import *
from ..schemas.logs import VersionInfo
@@ -43,12 +43,12 @@ def get_propertyGroup_version(uuid: UUID, db: Session = Depends(get_db)):
# --------CREATE-------
@router.post("/", response_model=PropertyGroupResponse, status_code=201)
-def create_propertyGroup(property_group: PropertyGroupCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def create_propertyGroup(property_group: PropertyGroupCreate, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
return crud.create_propertyGroup(db, property_group, current_user.username)
# --------UPDATE-------
@router.put("/{uuid}", response_model=PropertyGroupResponse)
-def update_propertyGroup(uuid: UUID, property_group: PropertyGroupUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def update_propertyGroup(uuid: UUID, property_group: PropertyGroupUpdate, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
updated = crud.update_propertyGroup(db, uuid, property_group, current_user.username)
if not updated:
raise HTTPException(status_code=404, detail="Property Group not found")
@@ -56,7 +56,7 @@ def update_propertyGroup(uuid: UUID, property_group: PropertyGroupUpdate, db: Se
# --------DELETE-------
@router.delete("/{uuid}", status_code=204)
-def delete_propertyGroup(uuid: UUID, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def delete_propertyGroup(uuid: UUID, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
success = crud.delete_propertyGroup(db, uuid, current_user.username)
if not success:
raise HTTPException(status_code=404, detail="Property Group not found")
diff --git a/backend/app/routers/roles.py b/backend/app/routers/roles.py
index c654412..ad84dfc 100644
--- a/backend/app/routers/roles.py
+++ b/backend/app/routers/roles.py
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from ..models.users import User
-from ..crud.auth import get_current_user_from_token, require_role, RoleEnum
+from ..crud.auth import require_role, RoleEnum
from ..crud import roles as crud
from ..db import get_db
from ..schemas.roles import *
@@ -31,12 +31,12 @@ def get_role_version(id: int, db: Session = Depends(get_db)):
# ---------CREATE--------
@router.post("/", response_model=Role)
-def create_role(data: RoleMutate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def create_role(data: RoleMutate, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
return crud.create_role(db, data, current_user.username)
# ---------UPDATE--------
@router.put("/{id}", response_model=Role)
-def update_role(id: int, data: RoleMutate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def update_role(id: int, data: RoleMutate, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
role = crud.get_role_by_id(db, id)
if not role:
raise HTTPException(status_code=404, detail="Rolle nicht gefunden")
@@ -44,7 +44,7 @@ def update_role(id: int, data: RoleMutate, db: Session = Depends(get_db), curren
# ---------DELETE--------
@router.delete("/{id}", status_code=204)
-def delete_role(id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def delete_role(id: int, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
role = crud.get_role_by_id(db, id)
if not role:
raise HTTPException(status_code=404, detail="Rolle nicht gefunden")
diff --git a/backend/app/routers/standards.py b/backend/app/routers/standards.py
index 530f5ec..bba7aa9 100644
--- a/backend/app/routers/standards.py
+++ b/backend/app/routers/standards.py
@@ -3,7 +3,7 @@
from ..db import get_db
from ..crud import standards as crud
from ..models.users import User
-from ..crud.auth import get_current_user_from_token, require_role, RoleEnum
+from ..crud.auth import require_role, RoleEnum
from ..schemas.standards import *
from ..schemas.logs import VersionInfo
@@ -31,12 +31,12 @@ def get_standard_version(id: int, db: Session = Depends(get_db)):
# ---------CREATE--------
@router.post("/", response_model=Standard)
-def create_standard(data: StandardMutate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def create_standard(data: StandardMutate, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
return crud.create_standard(db, data, current_user.username)
# ---------UPDATE--------
@router.put("/{id}", response_model=Standard)
-def update_standard(id: int, data: StandardMutate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def update_standard(id: int, data: StandardMutate, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
standard = crud.get_standard_by_id(db, id)
if not standard:
raise HTTPException(status_code=404, detail="Standard nicht gefunden")
@@ -45,7 +45,7 @@ def update_standard(id: int, data: StandardMutate, db: Session = Depends(get_db)
# ---------DELETE--------
@router.delete("/{id}", status_code=204)
-def delete_standard(id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def delete_standard(id: int, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
standard = crud.get_standard_by_id(db, id)
if not standard:
raise HTTPException(status_code=404, detail="Standard nicht gefunden")
diff --git a/backend/app/routers/subUseCases.py b/backend/app/routers/subUseCases.py
index db9adae..605093f 100644
--- a/backend/app/routers/subUseCases.py
+++ b/backend/app/routers/subUseCases.py
@@ -5,7 +5,7 @@
from sqlalchemy.orm import Session
from xml.etree import ElementTree as ET
from ..models.users import User
-from ..crud.auth import get_current_user_from_token, require_role, RoleEnum
+from ..crud.auth import require_role, RoleEnum
from ..db import get_db
from ..crud import subUseCases as crud
from ..schemas.subUseCases import *
@@ -40,12 +40,12 @@ def list_subUseCases_by_role(role_id: int, db: Session = Depends(get_db)):
# --------CREATE--------
@router.post("/", response_model=SubUseCase)
-def create_subUseCase(data: SubUseCaseCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def create_subUseCase(data: SubUseCaseCreate, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
return crud.create_subUseCase(db, data, current_user.username)
# --------UPDATE--------
@router.put("/{id}", response_model=SubUseCase)
-def update_subUseCase(id: int, data: SubUseCaseUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def update_subUseCase(id: int, data: SubUseCaseUpdate, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
subUseCase = crud.get_subUseCase_by_id(db, id)
if not subUseCase:
raise HTTPException(status_code=404, detail="Sub Use Case nicht gefunden")
@@ -53,7 +53,7 @@ def update_subUseCase(id: int, data: SubUseCaseUpdate, db: Session = Depends(get
# ---------DELETE--------
@router.delete("/{id}", status_code=204)
-def delete_subUseCase(id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def delete_subUseCase(id: int, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
subUseCase = crud.get_subUseCase_by_id(db, id)
if not subUseCase:
raise HTTPException(status_code=404, detail="Sub Use Case nicht gefunden")
diff --git a/backend/app/routers/transactions.py b/backend/app/routers/transactions.py
index 731f264..54b0566 100644
--- a/backend/app/routers/transactions.py
+++ b/backend/app/routers/transactions.py
@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
-from ..crud.auth import get_current_user_from_token, require_role, RoleEnum
+from ..crud.auth import require_role, RoleEnum
from ..db import get_db
from ..crud import transactions as crud
from ..models.users import User
@@ -35,12 +35,12 @@ def list_transactions_by_subusecase(sub_id: int, db: Session = Depends(get_db)):
# --------CREATE-------
@router.post("/", response_model=Transaction)
-def create_transaction(data: TransactionMutate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def create_transaction(data: TransactionMutate, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
return crud.create_transaction(db, data, current_user.username)
# --------UPDATE-------
@router.put("/{id}", response_model=Transaction)
-def update_transaction(id: int, data: TransactionMutate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def update_transaction(id: int, data: TransactionMutate, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
transaction = crud.get_transaction_by_id(db, id)
if not transaction:
raise HTTPException(status_code=404, detail="Transaktion nicht gefunden")
@@ -48,7 +48,7 @@ def update_transaction(id: int, data: TransactionMutate, db: Session = Depends(g
# --------DELETE-------
@router.delete("/{id}", status_code=204)
-def delete_transaction(id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def delete_transaction(id: int, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
transaction = crud.get_transaction_by_id(db, id)
if not transaction:
raise HTTPException(status_code=404, detail="Transaktion nicht gefunden")
diff --git a/backend/app/routers/useCases.py b/backend/app/routers/useCases.py
index 54180c2..bfc3039 100644
--- a/backend/app/routers/useCases.py
+++ b/backend/app/routers/useCases.py
@@ -6,7 +6,7 @@
from ..models.users import User
from ..schemas.useCases import *
from ..schemas.logs import *
-from ..crud.auth import require_role, RoleEnum, get_current_user_from_token
+from ..crud.auth import require_role, RoleEnum
router = APIRouter(prefix="/usecases", tags=["Use Cases"], dependencies=[Depends(require_role(RoleEnum.READ))])
@@ -32,7 +32,7 @@ def get_useCase_version(id: int, db: Session = Depends(get_db)):
# ---------CREATE--------
@router.post("/", response_model=UseCase)
-def create_useCase(data: UseCaseCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def create_useCase(data: UseCaseCreate, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
roles = db.query(models.Role).filter(models.Role.id.in_(data.roles)).all()
if len(roles) != len(data.roles):
raise HTTPException(status_code=400, detail="Eine oder mehrere Rollen nicht gefunden")
@@ -40,7 +40,7 @@ def create_useCase(data: UseCaseCreate, db: Session = Depends(get_db), current_u
# ---------UPDATE--------
@router.put("/{id}", response_model=UseCase)
-def update_useCase(id: int, data: UseCaseUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def update_useCase(id: int, data: UseCaseUpdate, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
roles = db.query(models.Role).filter(models.Role.id.in_(data.roles)).all()
if len(roles) != len(data.roles):
raise HTTPException(status_code=400, detail="Eine oder mehrere Rollen nicht gefunden")
@@ -54,7 +54,7 @@ def update_useCase(id: int, data: UseCaseUpdate, db: Session = Depends(get_db),
# ---------DELETE--------
@router.delete("/{id}", status_code=204)
-def delete_useCase(id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user_from_token)):
+def delete_useCase(id: int, db: Session = Depends(get_db), current_user: User = Depends(require_role(RoleEnum.WRITE))):
useCase = crud.get_useCase_by_id(db, id)
if not useCase:
raise HTTPException(status_code=404, detail="Use Case nicht gefunden")
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 0848509..8dbe129 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -14,6 +14,7 @@
"bpmn-js": "^18.10.1",
"dagre": "^0.8.5",
"lucide-react": "^0.562.0",
+ "n3": "^1.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.3",
"react-router-dom": "^7.9.6",
@@ -21,6 +22,7 @@
},
"devDependencies": {
"@eslint/js": "^9.39.1",
+ "@types/n3": "^1.16.4",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
@@ -1143,6 +1145,15 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@rdfjs/types": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-2.0.1.tgz",
+ "integrity": "sha512-uyAzpugX7KekAXAHq26m3JlUIZJOC0uSBhpnefGV5i15bevDyyejoB7I+9MKeUrzXD8OOUI3+4FeV1wwQr5ihA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.53",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
@@ -1838,6 +1849,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/n3": {
+ "version": "1.26.6",
+ "resolved": "https://registry.npmjs.org/@types/n3/-/n3-1.26.6.tgz",
+ "integrity": "sha512-BLXT2zu5wcbIotEVJsCI/eUjV5QWbwNBPNwnDrEf3qFv844UiCGe8Pg/PKSBR572V2BtBFlu46pDLtvhsOeWqg==",
+ "dev": true,
+ "dependencies": {
+ "@rdfjs/types": "*",
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "24.10.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
@@ -2202,6 +2223,17 @@
"d3-zoom": "^3.0.0"
}
},
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -2488,6 +2520,25 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
"node_modules/baseline-browser-mapping": {
"version": "2.9.11",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
@@ -2574,6 +2625,29 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
"node_modules/call-bind": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
@@ -3630,6 +3704,22 @@
"node": ">=0.10.0"
}
},
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -4088,6 +4178,25 @@
"resolved": "https://registry.npmjs.org/ids/-/ids-1.0.5.tgz",
"integrity": "sha512-XQ0yom/4KWTL29sLG+tyuycy7UmeaM/79GRtSJq6IG9cJGIPeBz5kwDCguie3TwxaMNIc3WtPi0cTa1XYHicpw=="
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -5104,6 +5213,18 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
+ "node_modules/n3": {
+ "version": "1.26.0",
+ "resolved": "https://registry.npmjs.org/n3/-/n3-1.26.0.tgz",
+ "integrity": "sha512-SQknS0ua90rN+3RHuk8BeIqeYyqIH/+ecViZxX08jR4j6MugqWRjtONl3uANG/crWXnOM2WIqBJtjIhVYFha+w==",
+ "dependencies": {
+ "buffer": "^6.0.3",
+ "readable-stream": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12.0"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
@@ -5494,6 +5615,14 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -5627,6 +5756,21 @@
"react-dom": ">=16.6.0"
}
},
+ "node_modules/readable-stream": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.8.0.tgz",
+ "integrity": "sha512-2jhTTU3Eilp/2DMT71gbcoqC35eQHasMJH0RHkYxwlxIv3+FR6Y5xAFZIwEGLxKkjZYEFlsiMNIdwUqjK2lLHQ==",
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -5764,6 +5908,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
"node_modules/safe-push-apply": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@@ -6009,6 +6172,14 @@
"node": ">= 0.4"
}
},
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
"node_modules/string.prototype.matchall": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 4fa0e54..91b4c36 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -18,6 +18,7 @@
"bpmn-js": "^18.10.1",
"dagre": "^0.8.5",
"lucide-react": "^0.562.0",
+ "n3": "^1.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.3",
"react-router-dom": "^7.9.6",
@@ -27,6 +28,7 @@
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
+ "@types/n3": "^1.16.4",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.22",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index bc196a1..bc1bf80 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -109,6 +109,7 @@ function AppContent() {