From b2b0b35a7055315702856e2bd9ac1962141ab589 Mon Sep 17 00:00:00 2001 From: lkrone Date: Tue, 3 Nov 2026 10:43:49 +0000 Subject: [PATCH 01/11] enforce login and roles for all routes --- backend/app/main.py | 19 +++++++++++++------ backend/app/routers/properties.py | 8 ++++---- backend/app/routers/propertyGroups.py | 8 ++++---- backend/app/routers/roles.py | 8 ++++---- backend/app/routers/standards.py | 8 ++++---- backend/app/routers/subUseCases.py | 8 ++++---- backend/app/routers/transactions.py | 8 ++++---- backend/app/routers/useCases.py | 8 ++++---- 8 files changed, 41 insertions(+), 34 deletions(-) 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 e17aa8d..513ef60 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 +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 218f763..59113d2 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 +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 1ba5c86..71278db 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 +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 a349091..21ac68a 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 +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 a93f292..aad3f80 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 +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 a9498b6..efa320b 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 +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 9ed345f..29fc705 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"]) @@ -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") From d2ae6f0538876bc9f2af0ac19f02ba97866aab96 Mon Sep 17 00:00:00 2001 From: lkrone Date: Tue, 3 Nov 2026 13:52:05 +0000 Subject: [PATCH 02/11] disable create, edit and save buttons if role is read --- frontend/src/components/CrudTable.tsx | 8 ++++++-- frontend/src/pages/ClassDetail.tsx | 7 +++++-- frontend/src/pages/PropertyDetail.tsx | 6 ++++-- frontend/src/pages/PropertyGroupDetail.tsx | 6 ++++-- frontend/src/pages/RoleDetail.tsx | 6 ++++-- frontend/src/pages/StandardDetail.tsx | 6 ++++-- frontend/src/pages/SubUseCaseDetail.tsx | 4 +++- frontend/src/pages/TransactionDetail.tsx | 6 ++++-- frontend/src/pages/UseCaseDetail.tsx | 6 ++++-- 9 files changed, 38 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/CrudTable.tsx b/frontend/src/components/CrudTable.tsx index f86b060..2c930c8 100644 --- a/frontend/src/components/CrudTable.tsx +++ b/frontend/src/components/CrudTable.tsx @@ -1,4 +1,5 @@ import React, {useState, useMemo} from "react"; +import { useAuth } from "../auth/useAuth.ts"; import { Link } from "react-router-dom"; import { ChevronUp, ChevronDown, Search, Trash2, Edit2, Plus, Inbox } from "lucide-react"; import type {MultiLangString} from "../types.ts"; @@ -63,6 +64,7 @@ export default function CrudTable({ isLoading = false, }: Props) { const [sortKey, setSortKey] = useState(null); + const { hasWriteAccess } = useAuth(); const [sortDirection, setSortDirection] = useState("asc"); const [filterText, setFilterText] = useState(""); @@ -147,7 +149,8 @@ export default function CrudTable({ diff --git a/frontend/src/pages/ClassDetail.tsx b/frontend/src/pages/ClassDetail.tsx index a82aa6b..7c63a30 100644 --- a/frontend/src/pages/ClassDetail.tsx +++ b/frontend/src/pages/ClassDetail.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useAuth } from "../auth/useAuth.ts"; import { useParams, useNavigate } from "react-router-dom"; import {api, ApiError} from "../api/api.ts"; import type {PropertyGroup, Property, VersionConflictDetail} from "../types.ts"; @@ -23,6 +24,7 @@ const emptyClass: Partial = { }; export default function ClassDetail() { + const { hasWriteAccess } = useAuth(); const { uuid } = useParams(); const navigate = useNavigate(); const [item, setItem] = useState>(emptyClass); @@ -380,10 +382,11 @@ export default function ClassDetail() {
+ {open && ( +
{children}
+ )} +
+ ); +} + +function Badge({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +function ObjectRow({ + o, + model, + path, +}: { + o: ObjectValue; + model: TtlModel; + path: ReadonlySet; +}) { + if (o.kind === "literal") { + return ( +
+ {o.value} + {o.lang && @{o.lang}} + {o.datatype && ( + ^^{model.shorten(o.datatype)} + )} +
+ ); + } + const target = model.entities.get(o.value); + const name = target?.label ?? model.shorten(o.value); + if (!target) return
{name}
; + if (path.has(o.value)) { + return ( +
+ ↺ {name} (Zyklus) +
+ ); + } + return ; +} + +function EntityNode({ + id, + model, + ancestors, + defaultOpen = false, +}: { + id: string; + model: TtlModel; + ancestors: ReadonlySet; + defaultOpen?: boolean; +}) { + const entity = model.entities.get(id)!; + const path = useMemo(() => new Set(ancestors).add(id), [ancestors, id]); + + return ( + + {entity.label ?? model.shorten(id)} + {entity.label && ( + {model.shorten(id)} + )} + {entity.types.map((t) => ( + {model.shorten(t)} + ))} + + } + > + {entity.predicates.length === 0 && ( +
(keine weiteren Eigenschaften)
+ )} + {entity.predicates.map((p) => ( +
+
+ {model.shorten(p.predicate)} +
+
+ {p.objects.map((o, i) => ( + + ))} +
+
+ ))} +
+ ); +} + +const EMPTY: ReadonlySet = new Set(); + +export default function TurtleViewer() { + const [model, setModel] = useState(null); + const [fileName, setFileName] = useState(""); + const [error, setError] = useState(null); + const [query, setQuery] = useState(""); + const [grouped, setGrouped] = useState(true); + + const onFile = async (e: ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + try { + setModel(await parseTurtle(await file.text())); + setFileName(file.name); + setQuery(""); + setError(null); + } catch (err) { + setModel(null); + setError(err instanceof Error ? err.message : "Parsing fehlgeschlagen"); + } + e.target.value = ""; // gleiche Datei erneut wählbar + }; + + const searching = query.trim() !== ""; + + const ids = useMemo(() => { + if (!model) return []; + if (!searching) return model.roots; + const q = query.trim().toLowerCase(); + return [...model.entities.values()] + .filter((e) => e.search.includes(q)) + .slice(0, 200) + .map((e) => e.id); + }, [model, query, searching]); + + const groups = useMemo(() => { + if (!model) return []; + if (!grouped || searching) return [{ key: "", ids }]; + const map = new Map(); + for (const id of ids) { + const key = model.entities.get(id)!.types[0] ?? ""; + map.set(key, [...(map.get(key) ?? []), id]); + } + return [...map.entries()] + .sort(([a], [b]) => (a === "" ? 1 : b === "" ? -1 : a.localeCompare(b))) + .map(([key, ids]) => ({ key, ids })); + }, [model, ids, grouped, searching]); + + const renderIds = (list: string[]) => + list.map((id) => ); + + return ( +
+
+ + {model && ( + <> + setQuery(e.target.value)} + /> + + + )} +
+ + {error &&

{error}

} + + {model && ( + <> +

+ {fileName} · {model.tripleCount} Tripel · {model.entities.size} Ressourcen + {searching && ` · ${ids.length} Treffer`} +

+ + {groups.map((g) => + g.key === "" && groups.length === 1 ? ( +
{renderIds(g.ids)}
+ ) : ( + + {g.key ? model.shorten(g.key) : "Ohne Typ"}{" "} + ({g.ids.length}) + + } + > + {renderIds(g.ids)} + + ) + )} + + )} +
+ ); +} diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 7c9361a..eb5c63e 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -20,6 +20,7 @@ import ClassList from "./pages/ClassList.tsx"; import ClassDetail from "./pages/ClassDetail.tsx"; import UserManagement from "./pages/UserManagement.tsx"; import OntologyGraph from "./pages/OntologyGraph.tsx"; +import TurtleViewer from "./pages/TurtleViewer"; export const router = createBrowserRouter([ @@ -165,10 +166,17 @@ export const router = createBrowserRouter([ ), }, + { + path: "turtle-viewer", + element: ( + + + + ), + }, - - {/* Admin Routes */}, + // Admin Routes { path: "users", element: ( From 991c29dc888cdd6fd74a19bf23e93cef43bbd614 Mon Sep 17 00:00:00 2001 From: lkrone Date: Mon, 9 Nov 2026 09:14:17 +0000 Subject: [PATCH 04/11] consider array columns in search (e.g. keywords for usecases) --- frontend/src/components/CrudTable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/CrudTable.tsx b/frontend/src/components/CrudTable.tsx index 2c930c8..d5e52c5 100644 --- a/frontend/src/components/CrudTable.tsx +++ b/frontend/src/components/CrudTable.tsx @@ -31,6 +31,7 @@ function resolveValue(value: unknown): string | number { if (value === null || value === undefined) return ""; if (typeof value === "number") return value; if (typeof value === "string") return value; + if (Array.isArray(value)) return value.join(' '); if (typeof value === "object") { const obj = value as Record; From 99fa6a3081818d6647d8b9ad4dda7b50ada0b379 Mon Sep 17 00:00:00 2001 From: lkrone Date: Mon, 9 Nov 2026 14:06:34 +0000 Subject: [PATCH 05/11] Update dev keycloak realm --- knowledge-hub-dev-realm.json | 67 ++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/knowledge-hub-dev-realm.json b/knowledge-hub-dev-realm.json index 976f854..c3e77dc 100644 --- a/knowledge-hub-dev-realm.json +++ b/knowledge-hub-dev-realm.json @@ -5,7 +5,7 @@ "defaultSignatureAlgorithm": "RS256", "revokeRefreshToken": false, "refreshTokenMaxReuse": 0, - "accessTokenLifespan": 300, + "accessTokenLifespan": 600, "accessTokenLifespanForImplicitFlow": 900, "ssoSessionIdleTimeout": 1800, "ssoSessionMaxLifespan": 36000, @@ -441,7 +441,17 @@ "attributes": {} } ], - "knowledge-hub-dev-client": [] + "knowledge-hub-dev-client": [ + { + "id": "112ea175-a28c-4ece-bc15-54ed9d43f0f3", + "name": "write", + "description": "", + "composite": false, + "clientRole": true, + "containerId": "e8e736dd-ae59-484f-9566-29e5329a9195", + "attributes": {} + } + ] } }, "groups": [], @@ -499,6 +509,29 @@ "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, "webAuthnPolicyPasswordlessAcceptableAaguids": [], "webAuthnPolicyPasswordlessExtraOrigins": [], + "users": [ + { + "id": "7bac59d5-0ce4-4aba-abde-fa4efbf273c9", + "username": "service-account-knowledge-hub-dev-swagger", + "emailVerified": false, + "enabled": true, + "createdTimestamp": 1791183551860, + "totp": false, + "serviceAccountClientId": "knowledge-hub-dev-swagger", + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-knowledge-hub-dev" + ], + "clientRoles": { + "knowledge-hub-dev-client": [ + "write" + ] + }, + "notBefore": 0, + "groups": [] + } + ], "scopeMappings": [ { "clientScope": "offline_access", @@ -781,8 +814,9 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", + "secret": "**********", "redirectUris": [ - "http://localhost:8001/docs/oauth2-redirect" + "http://localhost:8001/api/docs/oauth2-redirect" ], "webOrigins": [ "http://localhost:8001" @@ -792,22 +826,22 @@ "consentRequired": false, "standardFlowEnabled": true, "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "publicClient": false, "frontchannelLogout": true, "protocol": "openid-connect", "attributes": { "realm_client": "false", "logout.confirmation.enabled": "false", "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1791183546", "backchannel.logout.session.required": "true", "standard.token.exchange.enabled": "false", "oauth2.jwt.authorization.grant.enabled": "false", "frontchannel.logout.session.required": "true", "oauth2.device.authorization.grant.enabled": "false", "display.on.consent.screen": "false", - "pkce.code.challenge.method": "S256", "backchannel.logout.revoke.offline.tokens": "false", "dpop.bound.access.tokens": "false" }, @@ -815,6 +849,7 @@ "fullScopeAllowed": true, "nodeReRegistrationTimeout": -1, "defaultClientScopes": [ + "service_account", "web-origins", "acr", "roles", @@ -2685,13 +2720,23 @@ "firstBrokerLoginFlow": "first broker login", "attributes": { "cibaBackchannelTokenDeliveryMode": "poll", - "cibaExpiresIn": "120", "cibaAuthRequestedUserHint": "login_hint", - "oauth2DeviceCodeLifespan": "600", "oauth2DevicePollingInterval": "5", - "parRequestUriLifespan": "60", + "clientOfflineSessionMaxLifespan": "0", + "clientSessionIdleTimeout": "0", + "actionTokenGeneratedByUserLifespan.verify-email": "", + "actionTokenGeneratedByUserLifespan.idp-verify-account-via-email": "", + "clientOfflineSessionIdleTimeout": "0", + "actionTokenGeneratedByUserLifespan.execute-actions": "", "cibaInterval": "5", - "realmReusableOtpCode": "false" + "realmReusableOtpCode": "false", + "cibaExpiresIn": "120", + "oauth2DeviceCodeLifespan": "600", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "scimApiEnabled": "false", + "shortVerificationUri": "", + "actionTokenGeneratedByUserLifespan.reset-credentials": "" }, "keycloakVersion": "26.7.1", "userManagedAccessAllowed": false, From ad79d96bee52817ef7c0de0538b7a6dd2e1f8179 Mon Sep 17 00:00:00 2001 From: lkrone Date: Mon, 9 Nov 2026 19:56:39 +0000 Subject: [PATCH 06/11] add dummy download aasx button --- frontend/src/pages/ClassDetail.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/ClassDetail.tsx b/frontend/src/pages/ClassDetail.tsx index 7c63a30..3e02f74 100644 --- a/frontend/src/pages/ClassDetail.tsx +++ b/frontend/src/pages/ClassDetail.tsx @@ -133,6 +133,16 @@ export default function ClassDetail() {
Klasse
+ {/* Download AASX button */} + {uuid !== "new" && ( + + )} ); -} \ No newline at end of file +} From b715d03662c868b739a890e77d78dd30f546e691 Mon Sep 17 00:00:00 2001 From: lkrone Date: Mon, 9 Nov 2026 19:59:21 +0000 Subject: [PATCH 07/11] change text in login button --- frontend/src/pages/Login.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index aff2644..1649387 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -47,7 +47,7 @@ const Login: React.FC = () => { Willkommen zurück

- Melden Sie sich an, um fortzufahren + Melden Sie sich an, um fortzufahren.

@@ -61,7 +61,7 @@ const Login: React.FC = () => { style={{ width: '100%' }} > - Mit Keycloak anmelden + Über Construct-X Portal anmelden From 0cbe68663bd99f576c44bdb744214e8f214b2293 Mon Sep 17 00:00:00 2001 From: lkrone Date: Tue, 10 Nov 2026 06:55:20 +0000 Subject: [PATCH 08/11] Merge main into keycloak --- backend/app/routers/roles.py | 2 +- backend/app/routers/standards.py | 2 +- backend/app/routers/subUseCases.py | 2 +- backend/app/routers/transactions.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/app/routers/roles.py b/backend/app/routers/roles.py index 71278db..ad84dfc 100644 --- a/backend/app/routers/roles.py +++ b/backend/app/routers/roles.py @@ -7,7 +7,7 @@ from ..schemas.roles import * from ..schemas.logs import VersionInfo -router = APIRouter(prefix="/roles", tags=["Roles"]) +router = APIRouter(prefix="/roles", tags=["Roles"], dependencies=[Depends(require_role(RoleEnum.READ))]) # ---------LIST-------- @router.get("/", response_model=list[Role]) diff --git a/backend/app/routers/standards.py b/backend/app/routers/standards.py index 21ac68a..bba7aa9 100644 --- a/backend/app/routers/standards.py +++ b/backend/app/routers/standards.py @@ -7,7 +7,7 @@ from ..schemas.standards import * from ..schemas.logs import VersionInfo -router = APIRouter(prefix="/standards", tags=["Standards"]) +router = APIRouter(prefix="/standards", tags=["Standards"], dependencies=[Depends(require_role(RoleEnum.READ))]) # ---------LIST-------- @router.get("/", response_model=list[Standard]) diff --git a/backend/app/routers/subUseCases.py b/backend/app/routers/subUseCases.py index aad3f80..605093f 100644 --- a/backend/app/routers/subUseCases.py +++ b/backend/app/routers/subUseCases.py @@ -12,7 +12,7 @@ from ..schemas.logs import VersionInfo from ..config import IMAGE_DIR -router = APIRouter(prefix="/subusecases", tags=["Sub Use Cases"]) +router = APIRouter(prefix="/subusecases", tags=["Sub Use Cases"], dependencies=[Depends(require_role(RoleEnum.READ))]) # -------LIST-------- @router.get("/", response_model=list[SubUseCase]) diff --git a/backend/app/routers/transactions.py b/backend/app/routers/transactions.py index efa320b..54b0566 100644 --- a/backend/app/routers/transactions.py +++ b/backend/app/routers/transactions.py @@ -7,7 +7,7 @@ from ..schemas.transactions import * from ..schemas.logs import VersionInfo -router = APIRouter(prefix="/transactions", tags=["Transactions"]) +router = APIRouter(prefix="/transactions", tags=["Transactions"], dependencies=[Depends(require_role(RoleEnum.READ))]) # --------LIST-------- @router.get("/", response_model=list[Transaction]) From cec947eec7109cf9f92939846dc2fe30e4fe0ac2 Mon Sep 17 00:00:00 2001 From: lkrone Date: Tue, 10 Nov 2026 07:18:37 +0000 Subject: [PATCH 09/11] Update package.json --- frontend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/package.json b/frontend/package.json index ea5a16a..91b4c36 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,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", From cdd09045c88fd48eb8e2819a47821dc53b06571c Mon Sep 17 00:00:00 2001 From: lkrone Date: Tue, 10 Nov 2026 07:27:10 +0000 Subject: [PATCH 10/11] Update package-lock.json --- frontend/package-lock.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 82975ab..8dbe129 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,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", @@ -1144,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", @@ -1839,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", From e283e0f113061a5fc1fcebddf6dc613515e87349 Mon Sep 17 00:00:00 2001 From: lkrone Date: Tue, 10 Nov 2026 07:46:50 +0000 Subject: [PATCH 11/11] Update ttlModel.ts --- frontend/src/components/ttlModel.ts | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/ttlModel.ts b/frontend/src/components/ttlModel.ts index a4c60ce..54f2ca7 100644 --- a/frontend/src/components/ttlModel.ts +++ b/frontend/src/components/ttlModel.ts @@ -36,26 +36,12 @@ const idOf = (t: Term) => (t.termType === "BlankNode" ? `_:${t.value}` : t.value const isResource = (t: Term) => t.termType === "NamedNode" || t.termType === "BlankNode"; export function parseTurtle(text: string): TtlModel { - return new Promise((resolve, reject) => { - const quads: Quad[] = []; - - new Parser({ format: "text/turtle" }).parse(text, (err, quad, pfx) => { - if (err) { - reject(err); - return; - } - if (quad) { - quads.push(quad); - return; - } - // Ende der Eingabe: quad ist null, pfx enthält die Präfixe - try { - resolve(buildModel(quads, (pfx ?? {}) as Record)); - } catch (e) { - reject(e); - } - }); + const prefixes: Record = {}; + const parser = new Parser({ format: "text/turtle" }); + const quads: Quad[] = parser.parse(text, null, (prefix: string, node) => { + prefixes[prefix] = node.value; }); + return buildModel(quads, prefixes); }