From 094b932e03903c3353904361d8247644ddd0a911 Mon Sep 17 00:00:00 2001 From: GabrielFcGoncalves Date: Mon, 18 May 2026 18:33:44 +0000 Subject: [PATCH 1/8] OTP SMTP update --- api/src/services/keycloak_admin/user_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/services/keycloak_admin/user_handler.py b/api/src/services/keycloak_admin/user_handler.py index 7236300..e0c1296 100644 --- a/api/src/services/keycloak_admin/user_handler.py +++ b/api/src/services/keycloak_admin/user_handler.py @@ -105,7 +105,7 @@ def add_user( user_id = location.rstrip("/").split("/")[-1] # Trigger "Update Password" email - kc.execute_actions_email(realm_name, token, user_id, ["UPDATE_PASSWORD"]) + # kc.execute_actions_email(realm_name, token, user_id, ["UPDATE_PASSWORD"]) # Determine org manager flag from requested role is_org_manager = (role or "").strip().upper() == "ORG_MANAGER" From fcc8eb1dc623a489fe33f4069e7addba0084563e Mon Sep 17 00:00:00 2001 From: GabrielFcGoncalves Date: Mon, 18 May 2026 18:38:11 +0000 Subject: [PATCH 2/8] OTP SMTP update --- .../services/test_keycloak_admin_coverage.py | 38 +++++++++---------- api/test/services/test_user_handler.py | 6 +-- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/api/test/services/test_keycloak_admin_coverage.py b/api/test/services/test_keycloak_admin_coverage.py index e5f8a4e..9ed5ae3 100644 --- a/api/test/services/test_keycloak_admin_coverage.py +++ b/api/test/services/test_keycloak_admin_coverage.py @@ -77,25 +77,25 @@ def test_create_realm_with_smtp_and_required_actions(mock_kc): assert payload["clients"] == template["clients"] assert payload["users"] == template["users"] -def test_add_user_triggers_email(mock_kc): - # Arrange - uh = user_handler() - session = MagicMock() - - mock_response = MagicMock( - status_code=201, - headers={"Location": "http://keycloak/u/123"} - ) - mock_kc.create_user.return_value = mock_response - mock_kc.get_realm_role.return_value = {"id": "r1"} - - # Act - uh.add_user(session, "realm", "user", "pass", "Full Name", "user@test.com", "ORG_MANAGER") - - # Assert - mock_kc.execute_actions_email.assert_called_once_with( - "realm", ANY, "123", ["UPDATE_PASSWORD"] - ) +# def test_add_user_triggers_email(mock_kc): +# # Arrange +# uh = user_handler() +# session = MagicMock() +# +# mock_response = MagicMock( +# status_code=201, +# headers={"Location": "http://keycloak/u/123"} +# ) +# mock_kc.create_user.return_value = mock_response +# mock_kc.get_realm_role.return_value = {"id": "r1"} +# +# # Act +# uh.add_user(session, "realm", "user", "pass", "Full Name", "user@test.com", "ORG_MANAGER") +# +# # Assert +# mock_kc.execute_actions_email.assert_called_once_with( +# "realm", ANY, "123", ["UPDATE_PASSWORD"] +# ) def test_add_user_location_missing(mock_kc): uh = user_handler() diff --git a/api/test/services/test_user_handler.py b/api/test/services/test_user_handler.py index eb769a9..d814546 100644 --- a/api/test/services/test_user_handler.py +++ b/api/test/services/test_user_handler.py @@ -131,9 +131,9 @@ def test_create_user_success(service, mock_kc, session: Session): # Assert assert res["status"] == "created" assert res["username"] == "john.doe" - mock_kc.execute_actions_email.assert_called_once_with( - realm_name, "token", "123", ["UPDATE_PASSWORD"] - ) + # mock_kc.execute_actions_email.assert_called_once_with( + # realm_name, "token", "123", ["UPDATE_PASSWORD"] + # ) # Verify DB db_user = session.get(User, "123") From 378e1685ddb3caca3375103886cf7695f3f9ca0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sim=C3=A3o=20Santos?= <145043065+sle3pyy@users.noreply.github.com> Date: Mon, 18 May 2026 22:53:29 +0100 Subject: [PATCH 3/8] Fix: Added higher sqlalchemy rates (#158) --- api/src/core/db.py | 6 +++++- api/src/core/settings.py | 2 ++ performance/k6/base-user.js | 18 +++++++++--------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/api/src/core/db.py b/api/src/core/db.py index da9c415..61aca8a 100644 --- a/api/src/core/db.py +++ b/api/src/core/db.py @@ -2,7 +2,11 @@ from src.core.settings import settings import src.models -engine = create_engine(str(settings.PGSQL_DATABASE_URI)) +engine = create_engine( + str(settings.PGSQL_DATABASE_URI), + pool_size=settings.POSTGRES_POOL_SIZE, + max_overflow=settings.POSTGRES_MAX_OVERFLOW, +) async def init_db(): diff --git a/api/src/core/settings.py b/api/src/core/settings.py index 5b7942a..07c1820 100644 --- a/api/src/core/settings.py +++ b/api/src/core/settings.py @@ -18,6 +18,8 @@ class Settings(BaseSettings): POSTGRES_USER: str POSTGRES_PASSWORD: str = "" POSTGRES_DB: str = "" + POSTGRES_POOL_SIZE: int = 15 + POSTGRES_MAX_OVERFLOW: int = 20 # Keycloak KEYCLOAK_URL: str = "" diff --git a/performance/k6/base-user.js b/performance/k6/base-user.js index aff0b27..a968b27 100644 --- a/performance/k6/base-user.js +++ b/performance/k6/base-user.js @@ -28,7 +28,7 @@ export const options = { const scenarios = { current_user: { executor: "constant-arrival-rate", - rate: Number(__ENV.K6_PROFILE_RATE || 5), + rate: Number(__ENV.K6_PROFILE_RATE || 15), timeUnit: "1s", duration: __ENV.K6_DURATION || "1m", preAllocatedVUs: Number(__ENV.K6_PREALLOCATED_VUS || 5), @@ -38,7 +38,7 @@ export const options = { }, enrolled_courses: { executor: "constant-arrival-rate", - rate: Number(__ENV.K6_ENROLLED_RATE || 5), + rate: Number(__ENV.K6_ENROLLED_RATE || 15), timeUnit: "1s", duration: __ENV.K6_DURATION || "1m", preAllocatedVUs: Number(__ENV.K6_PREALLOCATED_VUS || 5), @@ -48,7 +48,7 @@ export const options = { }, progress_list: { executor: "constant-arrival-rate", - rate: Number(__ENV.K6_PROGRESS_RATE || 5), + rate: Number(__ENV.K6_PROGRESS_RATE || 15), timeUnit: "1s", duration: __ENV.K6_DURATION || "1m", preAllocatedVUs: Number(__ENV.K6_PREALLOCATED_VUS || 5), @@ -58,7 +58,7 @@ export const options = { }, certificates: { executor: "constant-arrival-rate", - rate: Number(__ENV.K6_CERTIFICATES_RATE || 3), + rate: Number(__ENV.K6_CERTIFICATES_RATE || 15), timeUnit: "1s", duration: __ENV.K6_DURATION || "1m", preAllocatedVUs: Number(__ENV.K6_PREALLOCATED_VUS || 5), @@ -68,7 +68,7 @@ export const options = { }, my_stats: { executor: "constant-arrival-rate", - rate: Number(__ENV.K6_ME_STATS_RATE || 3), + rate: Number(__ENV.K6_ME_STATS_RATE || 15), timeUnit: "1s", duration: __ENV.K6_DURATION || "1m", preAllocatedVUs: Number(__ENV.K6_PREALLOCATED_VUS || 5), @@ -78,7 +78,7 @@ export const options = { }, compliance_status: { executor: "constant-arrival-rate", - rate: Number(__ENV.K6_COMPLIANCE_RATE || 3), + rate: Number(__ENV.K6_COMPLIANCE_RATE || 15), timeUnit: "1s", duration: __ENV.K6_DURATION || "1m", preAllocatedVUs: Number(__ENV.K6_PREALLOCATED_VUS || 5), @@ -88,7 +88,7 @@ export const options = { }, compliance_doc: { executor: "constant-arrival-rate", - rate: Number(__ENV.K6_COMPLIANCE_DOC_RATE || 2), + rate: Number(__ENV.K6_COMPLIANCE_DOC_RATE || 15), timeUnit: "1s", duration: __ENV.K6_DURATION || "1m", preAllocatedVUs: Number(__ENV.K6_PREALLOCATED_VUS || 5), @@ -98,7 +98,7 @@ export const options = { }, compliance_quiz: { executor: "constant-arrival-rate", - rate: Number(__ENV.K6_COMPLIANCE_QUIZ_RATE || 2), + rate: Number(__ENV.K6_COMPLIANCE_QUIZ_RATE || 15), timeUnit: "1s", duration: __ENV.K6_DURATION || "1m", preAllocatedVUs: Number(__ENV.K6_PREALLOCATED_VUS || 5), @@ -111,7 +111,7 @@ export const options = { if (courseId) { scenarios.course_progress = { executor: "constant-arrival-rate", - rate: Number(__ENV.K6_COURSE_PROGRESS_RATE || 3), + rate: Number(__ENV.K6_COURSE_PROGRESS_RATE || 15), timeUnit: "1s", duration: __ENV.K6_DURATION || "1m", preAllocatedVUs: Number(__ENV.K6_PREALLOCATED_VUS || 5), From a6e9357785c0b9f2bd95214287281006fcf32534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sim=C3=A3o=20Santos?= <145043065+sle3pyy@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:46:17 +0100 Subject: [PATCH 4/8] Fix: Moved keycloak provider into @providers.tsx (#152) From 8875af997129330c97e4ca4dc665900b60b00817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sim=C3=A3o=20Santos?= <145043065+sle3pyy@users.noreply.github.com> Date: Mon, 1 Jun 2026 19:30:06 +0100 Subject: [PATCH 5/8] Hotfix garage (#161) * Fix(deploy): Added garage nginx support and changed env vars that were used wrong in production --- deployment/.env.prod.example | 2 +- deployment/nginx.mednat.conf | 22 +++++++++++++++++ deployment/nginx.securelearning.conf | 35 ++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/deployment/.env.prod.example b/deployment/.env.prod.example index 3c19a7b..b432f2d 100644 --- a/deployment/.env.prod.example +++ b/deployment/.env.prod.example @@ -15,7 +15,7 @@ MONGO_PASSWORD=template_pass # Garage object storage (S3-compatible API) FILE_STORAGE_BACKEND=garage GARAGE_S3_ENDPOINT=http://garage:3900 -GARAGE_S3_PUBLIC_ENDPOINT=http://localhost:3900 +GARAGE_S3_PUBLIC_ENDPOINT=https://mednat.ieeta.pt:9071/garage GARAGE_S3_REGION=garage GARAGE_ACCESS_KEY_ID=garage-access-key GARAGE_SECRET_ACCESS_KEY=garage-secret-key diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index b748efc..f7c27dc 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -37,6 +37,11 @@ http { server keycloak:8080 resolve max_fails=3 fail_timeout=30s; } + upstream garages { + zone garages 64k; + server garage:3900 resolve max_fails=3 fail_timeout=30s; + } + upstream grafanas { zone grafanas 64k; server grafana:3000 resolve max_fails=3 fail_timeout=30s; @@ -97,6 +102,23 @@ http { proxy_set_header X-Forwarded-Port $forwarded_port; } + # Garage S3 API on same host via /garage for presigned object URLs. + location = /garage { + return 301 /garage/; + } + location /garage/ { + rewrite ^/garage/(.*)$ /$1 break; + proxy_pass http://garages; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $forwarded_port; + } + # Frontend entrypoint is /app. location = / { diff --git a/deployment/nginx.securelearning.conf b/deployment/nginx.securelearning.conf index dc8fedc..1bb85c7 100644 --- a/deployment/nginx.securelearning.conf +++ b/deployment/nginx.securelearning.conf @@ -31,6 +31,11 @@ http { server keycloak:8080 resolve max_fails=3 fail_timeout=30s; } + upstream garages { + zone garages 64k; + server garage:3900 resolve max_fails=3 fail_timeout=30s; + } + upstream grafanas { zone grafanas 64k; server grafana:3000 resolve max_fails=3 fail_timeout=30s; @@ -102,6 +107,36 @@ http { } } + # Garage S3 - s3.localhost / s3.securelearning.pt + server { + listen 8081 ssl; + listen [::]:8081 ssl; + server_name s3.localhost s3.securelearning.pt s3.mednat.ieeta.pt; + + ssl_certificate /etc/nginx/certs/tls.crt; + ssl_certificate_key /etc/nginx/certs/tls.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + large_client_header_buffers 4 32k; + + location / { + proxy_pass http://garages/; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + } + } + # Keycloak - kc.localhost / kc.securelearning.pt server { listen 8081 ssl; From 41e6975b55bd3f8e5e3d509914136caf1e226d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sim=C3=A3o=20Santos?= <145043065+sle3pyy@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:18:56 +0100 Subject: [PATCH 6/8] Hotfix me and garage (#164) * Fix: Added higher sqlalchemy rates * Fix(deploy): Added garage nginx support and changed env vars that were used wrong in production * Fix(garage): maybe deploy fix * feat: add public content URL generation and streaming functionality --- api/src/core/settings.py | 1 + api/src/routers/content.py | 12 ++++- api/src/services/content.py | 91 ++++++++++++++++++++++++++++++++--- deployment/.env.prod.example | 3 +- deployment/docker-compose.yml | 1 + 5 files changed, 98 insertions(+), 10 deletions(-) diff --git a/api/src/core/settings.py b/api/src/core/settings.py index 07c1820..29270f1 100644 --- a/api/src/core/settings.py +++ b/api/src/core/settings.py @@ -63,6 +63,7 @@ class Settings(BaseSettings): GARAGE_CONTENT_PREFIX: str = "content" GARAGE_LOGOS_PREFIX: str = "logos" GARAGE_PRESIGNED_URL_TTL_SECONDS: int = 900 + CONTENT_PUBLIC_URL_SECRET: str = "" # RabbitMQ RABBITMQ_HOST: str diff --git a/api/src/routers/content.py b/api/src/routers/content.py index 8922b1d..3bb177d 100644 --- a/api/src/routers/content.py +++ b/api/src/routers/content.py @@ -1,6 +1,6 @@ from typing import Annotated -from fastapi import APIRouter, File, Form, UploadFile, status +from fastapi import APIRouter, File, Form, Query, UploadFile, status from src.core.dependencies import CurrentRealm from src.services import content as content_service @@ -91,6 +91,16 @@ async def get_content_file_url(content_piece_id: str, _: CurrentRealm) -> dict[s return {"url": await content_service.get_content_file_url(content_piece_id)} +@router.get("/content-public/{content_piece_id}/file") +async def get_public_content_file( + content_piece_id: str, + expires: Annotated[int, Query(...)], + sig: Annotated[str, Query(min_length=1)], +): + content_service.verify_public_content_signature(content_piece_id, expires, sig) + return await content_service.stream_content_file(content_piece_id) + + @router.delete("/content/{content_piece_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_content(content_piece_id: str, _: CurrentRealm) -> None: await content_service.delete_content_piece(content_piece_id) diff --git a/api/src/services/content.py b/api/src/services/content.py index 3a7d69a..861737a 100644 --- a/api/src/services/content.py +++ b/api/src/services/content.py @@ -1,9 +1,12 @@ +import hashlib +import hmac from datetime import datetime, timezone from typing import Any, Literal +from urllib.parse import urlencode from uuid import uuid4 from fastapi import HTTPException, UploadFile, status -from fastapi.responses import RedirectResponse +from fastapi.responses import StreamingResponse from pydantic import BaseModel, Field from src.core.mongo import ( @@ -18,7 +21,7 @@ delete_object, ensure_bucket, garage_enabled, - generate_presigned_get_url, + get_object, put_bytes, ) from src.core.settings import settings @@ -32,6 +35,7 @@ CONTENT_NOT_FOUND = "Content not found" FILE_NOT_FOUND = "File not found" FOLDER_NOT_FOUND = "Folder not found" +PUBLIC_CONTENT_URL_TTL_SECONDS = 900 class ContentPieceCreate(BaseModel): @@ -176,7 +180,43 @@ def _content_file_url(file_meta: dict[str, Any] | None) -> str | None: if not isinstance(object_key, str) or not object_key: return None - return generate_presigned_get_url(bucket=settings.GARAGE_BUCKET_CONTENT, key=object_key) + return generate_public_content_url(file_meta.get("content_piece_id")) + + +def _content_public_secret() -> str: + secret = settings.CONTENT_PUBLIC_URL_SECRET.strip() or settings.CLIENT_SECRET.strip() + if not secret: + raise ObjectStorageError("Content public URL secret is not configured") + return secret + + +def _sign_public_content_url(content_piece_id: str, expires: int) -> str: + payload = f"{content_piece_id}:{expires}".encode() + return hmac.new( + _content_public_secret().encode(), + payload, + hashlib.sha256, + ).hexdigest() + + +def generate_public_content_url(content_piece_id: str | None, expires_in: int = PUBLIC_CONTENT_URL_TTL_SECONDS) -> str | None: + if not isinstance(content_piece_id, str) or not content_piece_id: + return None + + expires = int(datetime.now(timezone.utc).timestamp()) + expires_in + sig = _sign_public_content_url(content_piece_id, expires) + query = urlencode({"expires": expires, "sig": sig}) + return f"/garage/{content_piece_id}?{query}" + + +def verify_public_content_signature(content_piece_id: str, expires: int, sig: str) -> None: + now_ts = int(datetime.now(timezone.utc).timestamp()) + if expires < now_ts: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Content URL expired") + + expected = _sign_public_content_url(content_piece_id, expires) + if not hmac.compare_digest(sig, expected): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid content URL signature") async def _load_folders_by_id() -> dict[str, dict[str, Any]]: @@ -217,6 +257,7 @@ def _to_content_out( payload["path"] = _build_folder_path(payload.get("folder_id"), folders_by_id or {}) file_meta = payload.get("file") if isinstance(file_meta, dict): + file_meta["content_piece_id"] = payload.get("content_piece_id") file_meta["file_url"] = _content_file_url(file_meta) if include_file_url else None return ContentPieceOut.model_validate(payload) @@ -559,11 +600,8 @@ async def upload_content_piece( raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(exc)) from exc -async def download_content_file(content_piece_id: str) -> RedirectResponse: - url = await get_content_file_url(content_piece_id) - if not url: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=FILE_NOT_FOUND) - return RedirectResponse(url=url, status_code=status.HTTP_307_TEMPORARY_REDIRECT) +async def download_content_file(content_piece_id: str) -> StreamingResponse: + return await stream_content_file(content_piece_id) async def get_content_file_url(content_piece_id: str) -> str | None: @@ -580,11 +618,48 @@ async def get_content_file_url(content_piece_id: str) -> str | None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=FILE_NOT_FOUND) try: + file_meta["content_piece_id"] = content_piece_id return _content_file_url(file_meta) except ObjectStorageError as exc: raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(exc)) from exc +async def stream_content_file(content_piece_id: str) -> StreamingResponse: + collection = get_content_collection() + doc = await collection.find_one( + {"kind": "content_piece", "content_piece_id": content_piece_id}, + {"file": 1}, + ) + if not doc: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=CONTENT_NOT_FOUND) + + file_meta = doc.get("file") + if not isinstance(file_meta, dict): + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=FILE_NOT_FOUND) + + object_key = file_meta.get("object_key") + if not isinstance(object_key, str) or not object_key: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=FILE_NOT_FOUND) + + content_type = file_meta.get("content_type") or "application/octet-stream" + filename = file_meta.get("filename") or "content" + size = file_meta.get("size") + etag = file_meta.get("etag") + + try: + stored = await get_object(bucket=settings.GARAGE_BUCKET_CONTENT, key=object_key) + except ObjectStorageError as exc: + raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(exc)) from exc + + headers = {"Content-Disposition": f'inline; filename="{filename}"'} + if isinstance(size, int) and size >= 0: + headers["Content-Length"] = str(size) + if isinstance(etag, str) and etag: + headers["ETag"] = etag + + return StreamingResponse(stored.stream, media_type=content_type, headers=headers) + + async def delete_content_piece(content_piece_id: str) -> None: collection = get_content_collection() doc = await collection.find_one({"kind": "content_piece", "content_piece_id": content_piece_id}) diff --git a/deployment/.env.prod.example b/deployment/.env.prod.example index b432f2d..52e2c5d 100644 --- a/deployment/.env.prod.example +++ b/deployment/.env.prod.example @@ -15,7 +15,7 @@ MONGO_PASSWORD=template_pass # Garage object storage (S3-compatible API) FILE_STORAGE_BACKEND=garage GARAGE_S3_ENDPOINT=http://garage:3900 -GARAGE_S3_PUBLIC_ENDPOINT=https://mednat.ieeta.pt:9071/garage +GARAGE_S3_PUBLIC_ENDPOINT=https://mednat.ieeta.pt:9072 GARAGE_S3_REGION=garage GARAGE_ACCESS_KEY_ID=garage-access-key GARAGE_SECRET_ACCESS_KEY=garage-secret-key @@ -55,6 +55,7 @@ KEYCLOAK_ISSUER_URL=https://mednat.ieeta.pt:9071/kc/realms/platform # Port Configuration NGINX_PORT=8081 +GARAGE_PUBLIC_PORT=9072 SERVER_PORT=9071 # TLS certificate file names inside deployment/certs diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 28d6221..fef184e 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -7,6 +7,7 @@ services: restart: unless-stopped ports: - "${NGINX_PORT}:8081" + - "${GARAGE_PUBLIC_PORT}:8082" expose: - "8080" volumes: From 5b2fa3986b22bb58a12a4be4d25d1e6432d944a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sim=C3=A3o=20Santos?= <145043065+sle3pyy@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:38:37 +0100 Subject: [PATCH 7/8] Hotfix me and garage (#165) * Fix(deploy): Added garage nginx support and changed env vars that were used wrong in production * Fix(garage): maybe deploy fix * feat: add public content URL generation and streaming functionality * Fix(nginx): update garage route to proxy to APIs for public content access --- deployment/nginx.mednat.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index f7c27dc..eb40cd9 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -107,8 +107,8 @@ http { return 301 /garage/; } location /garage/ { - rewrite ^/garage/(.*)$ /$1 break; - proxy_pass http://garages; + rewrite ^/garage/(.*)$ /api/content-public/$1/file break; + proxy_pass http://apis; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Host $http_host; From fad5882b7c74ef1b0f42dc93d10f6b80451b6071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sim=C3=A3o=20Santos?= <145043065+sle3pyy@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:48:07 +0100 Subject: [PATCH 8/8] Hotfix me and garage (#166) * Fix: Added higher sqlalchemy rates * Fix(deploy): Added garage nginx support and changed env vars that were used wrong in production * Fix(garage): maybe deploy fix * feat: add public content URL generation and streaming functionality * Fix(nginx): update garage route to proxy to APIs for public content access * Fix(user_handler): improve current user profile resolution with admin user data --- api/src/services/platform_admin/user_handler.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/api/src/services/platform_admin/user_handler.py b/api/src/services/platform_admin/user_handler.py index 77e0a64..d907453 100644 --- a/api/src/services/platform_admin/user_handler.py +++ b/api/src/services/platform_admin/user_handler.py @@ -160,13 +160,19 @@ def get_current_user_profile(self, token: str) -> CurrentUserProfileDTO: """Resolve current user from bearer token and return base profile info.""" claims = decode_token_verified(token) realm = get_realm_from_iss(claims.get("iss")) - profile = self.admin.keycloak_client.get_userinfo(realm, token) - user_id = profile.get("sub") or claims.get("sub") + user_id = claims.get("sub") if not user_id: raise HTTPException(status_code=401, detail="Unable to resolve current user") - return self._build_current_user_profile(realm, user_id, claims, profile, {}) + admin_user: dict = {} + try: + admin_token = self.admin._get_admin_token() + admin_user = self.admin.keycloak_client.get_user(realm, admin_token, user_id) or {} + except HTTPException: + admin_user = {} + + return self._build_current_user_profile(realm, user_id, claims, {}, admin_user) def delete_user_in_realm( self, realm: str, user_id: str, session: Session, token: str