From 3a6c32289c4f2c5e53b4f0cd01542b4eb6fe9710 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 26 Feb 2026 12:35:09 +0000 Subject: [PATCH 01/87] done --- .github/workflows/CD-workflow.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CD-workflow.yml b/.github/workflows/CD-workflow.yml index f6ffedc4..19f6c5ff 100644 --- a/.github/workflows/CD-workflow.yml +++ b/.github/workflows/CD-workflow.yml @@ -23,5 +23,5 @@ jobs: env: DOCKER_BUILDKIT: 1 run: | - sudo docker compose --env-file deployment/.env -f deployment/docker-compose.yml down --remove-orphans - sudo docker compose --env-file deployment/.env -f deployment/docker-compose.yml up -d --build + docker compose --env-file deployment/.env -f deployment/docker-compose.yml down --remove-orphans + docker compose --env-file deployment/.env -f deployment/docker-compose.yml up -d --build From 6bb6d62b76e6670a973b7c1e43eaa3b1c03d42ab Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 26 Feb 2026 12:43:39 +0000 Subject: [PATCH 02/87] test fix --- web/Dockerfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web/Dockerfile b/web/Dockerfile index 437bf054..d0a70ca3 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -11,7 +11,12 @@ ENV VITE_API_URL=$VITE_API_URL/api ENV VITE_KEYCLOAK_URL=$VITE_KEYCLOAK_URL COPY package*.json ./ -RUN npm install +RUN npm config set registry https://registry.npmjs.org/ \ + && npm config set fetch-retries 5 \ + && npm config set fetch-retry-factor 2 \ + && npm config set fetch-retry-mintimeout 20000 \ + && npm config set fetch-retry-maxtimeout 120000 \ + && npm ci --no-audit --no-fund COPY . . RUN npm run build @@ -28,4 +33,4 @@ COPY --from=builder /app/dist ./dist EXPOSE 80 # serve with SPA fallback (-s flag handles client-side routing) -CMD ["serve", "-s", "dist", "-l", "80"] \ No newline at end of file +CMD ["serve", "-s", "dist", "-l", "80"] From a5696d421a83f50fa9202658508e640098e20544 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 26 Feb 2026 16:34:31 +0000 Subject: [PATCH 03/87] test fix --- deployment/.env | 10 +++++----- deployment/.env.example | 10 +++++----- deployment/docker-compose.yml | 2 +- deployment/nginx.conf | 18 +++++++++--------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/deployment/.env b/deployment/.env index a1259f83..14dd92be 100644 --- a/deployment/.env +++ b/deployment/.env @@ -59,10 +59,10 @@ API_INTERNAL_URL=http://api:80 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml -KEYCLOAK_HOSTNAME=kc.localhost -KEYCLOAK_URL=http://kc.localhost -API_URL=http://api.localhost -WEB_URL=http://app.localhost +KEYCLOAK_HOSTNAME=kc.mednat.ieeta.pt +KEYCLOAK_URL=http://kc.mednat.ieeta.pt:9071 +API_URL=http://api.mednat.ieeta.pt:9071 +WEB_URL=http://mednat.ieeta.pt:9071 # Port Configuration (Production only - nginx exposes this port) -NGINX_PORT=80 +NGINX_PORT=9071 diff --git a/deployment/.env.example b/deployment/.env.example index a1259f83..14dd92be 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -59,10 +59,10 @@ API_INTERNAL_URL=http://api:80 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml -KEYCLOAK_HOSTNAME=kc.localhost -KEYCLOAK_URL=http://kc.localhost -API_URL=http://api.localhost -WEB_URL=http://app.localhost +KEYCLOAK_HOSTNAME=kc.mednat.ieeta.pt +KEYCLOAK_URL=http://kc.mednat.ieeta.pt:9071 +API_URL=http://api.mednat.ieeta.pt:9071 +WEB_URL=http://mednat.ieeta.pt:9071 # Port Configuration (Production only - nginx exposes this port) -NGINX_PORT=80 +NGINX_PORT=9071 diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 3487c8f8..b7783ec6 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -5,7 +5,7 @@ services: image: nginx:alpine restart: unless-stopped ports: - - "${NGINX_PORT}:80" + - "${NGINX_PORT}:8081" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro depends_on: diff --git a/deployment/nginx.conf b/deployment/nginx.conf index a8308bab..aa62e16e 100644 --- a/deployment/nginx.conf +++ b/deployment/nginx.conf @@ -33,9 +33,9 @@ http { # Frontend - app.localhost / app.securelearning.pt server { - listen 80; - listen [::]:80; - server_name app.localhost app.securelearning.pt; + listen 8081; + listen [::]:8081; + server_name app.localhost app.securelearning.pt mednat.ieeta.pt app.mednat.ieeta.pt; location / { proxy_pass http://frontends/; @@ -54,9 +54,9 @@ http { # API - api.localhost / api.securelearning.pt server { - listen 80; - listen [::]:80; - server_name api.localhost api.securelearning.pt; + listen 8081; + listen [::]:8081; + server_name api.localhost api.securelearning.pt api.mednat.ieeta.pt; proxy_buffer_size 128k; proxy_buffers 4 256k; @@ -80,9 +80,9 @@ http { # Keycloak - kc.localhost / kc.securelearning.pt server { - listen 80; - listen [::]:80; - server_name kc.localhost kc.securelearning.pt; + listen 8081; + listen [::]:8081; + server_name kc.localhost kc.securelearning.pt kc.mednat.ieeta.pt; proxy_buffer_size 128k; proxy_buffers 4 256k; From 108c691097af3746ef18f0c35f2a880882facc90 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 26 Feb 2026 17:47:15 +0000 Subject: [PATCH 04/87] DNS fixes --- deployment/.env | 8 ++--- deployment/.env.example | 8 ++--- deployment/docker-compose.yml | 3 +- deployment/nginx.conf | 61 +++++++++++++---------------------- 4 files changed, 32 insertions(+), 48 deletions(-) diff --git a/deployment/.env b/deployment/.env index 14dd92be..d87039a6 100644 --- a/deployment/.env +++ b/deployment/.env @@ -59,10 +59,10 @@ API_INTERNAL_URL=http://api:80 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml -KEYCLOAK_HOSTNAME=kc.mednat.ieeta.pt -KEYCLOAK_URL=http://kc.mednat.ieeta.pt:9071 -API_URL=http://api.mednat.ieeta.pt:9071 +KEYCLOAK_HOSTNAME=mednat.ieeta.pt:9071 +KEYCLOAK_URL=http://mednat.ieeta.pt:9071/auth +API_URL=http://mednat.ieeta.pt:9071 WEB_URL=http://mednat.ieeta.pt:9071 # Port Configuration (Production only - nginx exposes this port) -NGINX_PORT=9071 +NGINX_PORT=8071 diff --git a/deployment/.env.example b/deployment/.env.example index 14dd92be..d87039a6 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -59,10 +59,10 @@ API_INTERNAL_URL=http://api:80 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml -KEYCLOAK_HOSTNAME=kc.mednat.ieeta.pt -KEYCLOAK_URL=http://kc.mednat.ieeta.pt:9071 -API_URL=http://api.mednat.ieeta.pt:9071 +KEYCLOAK_HOSTNAME=mednat.ieeta.pt:9071 +KEYCLOAK_URL=http://mednat.ieeta.pt:9071/auth +API_URL=http://mednat.ieeta.pt:9071 WEB_URL=http://mednat.ieeta.pt:9071 # Port Configuration (Production only - nginx exposes this port) -NGINX_PORT=9071 +NGINX_PORT=8071 diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index b7783ec6..076cb967 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -126,8 +126,9 @@ services: KC_HTTP_ENABLED: "true" KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true" - KC_HOSTNAME: ${KEYCLOAK_URL} + KC_HOSTNAME: ${KEYCLOAK_HOSTNAME} KC_HOSTNAME_STRICT: "false" + KC_HTTP_RELATIVE_PATH: /auth KC_HEALTH_ENABLED: "true" diff --git a/deployment/nginx.conf b/deployment/nginx.conf index aa62e16e..f03b72b4 100644 --- a/deployment/nginx.conf +++ b/deployment/nginx.conf @@ -31,40 +31,22 @@ http { server keycloak:8080 resolve max_fails=3 fail_timeout=30s; } - # Frontend - app.localhost / app.securelearning.pt + # Single host routing (path-based): + # - / -> frontend + # - /api -> api + # - /auth -> keycloak server { listen 8081; listen [::]:8081; - server_name app.localhost app.securelearning.pt mednat.ieeta.pt app.mednat.ieeta.pt; - - location / { - proxy_pass http://frontends/; - proxy_redirect off; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - 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_cache_bypass $http_upgrade; - } - } - - # API - api.localhost / api.securelearning.pt - server { - listen 8081; - listen [::]:8081; - server_name api.localhost api.securelearning.pt api.mednat.ieeta.pt; + server_name mednat.ieeta.pt app.localhost app.securelearning.pt app.mednat.ieeta.pt; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; large_client_header_buffers 4 32k; - location / { - proxy_pass http://apis/; + location /api/ { + proxy_pass http://apis; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; @@ -76,20 +58,8 @@ http { proxy_set_header X-Forwarded-Host $http_host; proxy_cache_bypass $http_upgrade; } - } - - # Keycloak - kc.localhost / kc.securelearning.pt - server { - listen 8081; - listen [::]:8081; - server_name kc.localhost kc.securelearning.pt kc.mednat.ieeta.pt; - - proxy_buffer_size 128k; - proxy_buffers 4 256k; - proxy_busy_buffers_size 256k; - large_client_header_buffers 4 32k; - location / { + location /auth/ { proxy_pass http://keycloaks; proxy_redirect off; proxy_intercept_errors off; @@ -98,7 +68,6 @@ http { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -106,5 +75,19 @@ http { proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; } + + location / { + proxy_pass http://frontends/; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + 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_cache_bypass $http_upgrade; + } } } From 8a7bf61c7388d60a79157d0ba377a8a9919bbf6e Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 26 Feb 2026 18:31:36 +0000 Subject: [PATCH 05/87] Revert "Merge pull request #78 from PEI-SecureLearning/HOTFIX-port-mapping" This reverts commit 1e386beb6f8bb21e3ee04bd613a39a61c44538ac, reversing changes made to 2bd17f1e7d7cd7532ec6b236f3a85f065d5f38ad. --- deployment/.env | 8 ++--- deployment/.env.example | 8 ++--- deployment/docker-compose.yml | 3 +- deployment/nginx.conf | 61 ++++++++++++++++++++++------------- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/deployment/.env b/deployment/.env index d87039a6..14dd92be 100644 --- a/deployment/.env +++ b/deployment/.env @@ -59,10 +59,10 @@ API_INTERNAL_URL=http://api:80 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml -KEYCLOAK_HOSTNAME=mednat.ieeta.pt:9071 -KEYCLOAK_URL=http://mednat.ieeta.pt:9071/auth -API_URL=http://mednat.ieeta.pt:9071 +KEYCLOAK_HOSTNAME=kc.mednat.ieeta.pt +KEYCLOAK_URL=http://kc.mednat.ieeta.pt:9071 +API_URL=http://api.mednat.ieeta.pt:9071 WEB_URL=http://mednat.ieeta.pt:9071 # Port Configuration (Production only - nginx exposes this port) -NGINX_PORT=8071 +NGINX_PORT=9071 diff --git a/deployment/.env.example b/deployment/.env.example index d87039a6..14dd92be 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -59,10 +59,10 @@ API_INTERNAL_URL=http://api:80 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml -KEYCLOAK_HOSTNAME=mednat.ieeta.pt:9071 -KEYCLOAK_URL=http://mednat.ieeta.pt:9071/auth -API_URL=http://mednat.ieeta.pt:9071 +KEYCLOAK_HOSTNAME=kc.mednat.ieeta.pt +KEYCLOAK_URL=http://kc.mednat.ieeta.pt:9071 +API_URL=http://api.mednat.ieeta.pt:9071 WEB_URL=http://mednat.ieeta.pt:9071 # Port Configuration (Production only - nginx exposes this port) -NGINX_PORT=8071 +NGINX_PORT=9071 diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 076cb967..b7783ec6 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -126,9 +126,8 @@ services: KC_HTTP_ENABLED: "true" KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true" - KC_HOSTNAME: ${KEYCLOAK_HOSTNAME} + KC_HOSTNAME: ${KEYCLOAK_URL} KC_HOSTNAME_STRICT: "false" - KC_HTTP_RELATIVE_PATH: /auth KC_HEALTH_ENABLED: "true" diff --git a/deployment/nginx.conf b/deployment/nginx.conf index f03b72b4..aa62e16e 100644 --- a/deployment/nginx.conf +++ b/deployment/nginx.conf @@ -31,22 +31,40 @@ http { server keycloak:8080 resolve max_fails=3 fail_timeout=30s; } - # Single host routing (path-based): - # - / -> frontend - # - /api -> api - # - /auth -> keycloak + # Frontend - app.localhost / app.securelearning.pt server { listen 8081; listen [::]:8081; - server_name mednat.ieeta.pt app.localhost app.securelearning.pt app.mednat.ieeta.pt; + server_name app.localhost app.securelearning.pt mednat.ieeta.pt app.mednat.ieeta.pt; + + location / { + proxy_pass http://frontends/; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + 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_cache_bypass $http_upgrade; + } + } + + # API - api.localhost / api.securelearning.pt + server { + listen 8081; + listen [::]:8081; + server_name api.localhost api.securelearning.pt api.mednat.ieeta.pt; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; large_client_header_buffers 4 32k; - location /api/ { - proxy_pass http://apis; + location / { + proxy_pass http://apis/; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; @@ -58,8 +76,20 @@ http { proxy_set_header X-Forwarded-Host $http_host; proxy_cache_bypass $http_upgrade; } + } + + # Keycloak - kc.localhost / kc.securelearning.pt + server { + listen 8081; + listen [::]:8081; + server_name kc.localhost kc.securelearning.pt kc.mednat.ieeta.pt; + + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + large_client_header_buffers 4 32k; - location /auth/ { + location / { proxy_pass http://keycloaks; proxy_redirect off; proxy_intercept_errors off; @@ -68,6 +98,7 @@ http { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -75,19 +106,5 @@ http { proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; } - - location / { - proxy_pass http://frontends/; - proxy_redirect off; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - 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_cache_bypass $http_upgrade; - } } } From 070006f82c27ff609a586b9d6356899ccd6c2b29 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Mon, 2 Mar 2026 15:30:07 +0000 Subject: [PATCH 06/87] new nginx --- deployment/.env | 8 +- deployment/.env.example | 8 +- deployment/docker-compose.yml | 3 +- deployment/nginx.mednat.conf | 94 +++++++++++++++++++ ...nx.conf => nginx.securelearning copy.conf} | 0 5 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 deployment/nginx.mednat.conf rename deployment/{nginx.conf => nginx.securelearning copy.conf} (100%) diff --git a/deployment/.env b/deployment/.env index 14dd92be..1fc536bf 100644 --- a/deployment/.env +++ b/deployment/.env @@ -59,10 +59,10 @@ API_INTERNAL_URL=http://api:80 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml -KEYCLOAK_HOSTNAME=kc.mednat.ieeta.pt -KEYCLOAK_URL=http://kc.mednat.ieeta.pt:9071 -API_URL=http://api.mednat.ieeta.pt:9071 +KEYCLOAK_HOSTNAME=http://mednat.ieeta.pt:9071 +KEYCLOAK_URL=http://mednat.ieeta.pt:9071/kc +API_URL=http://mednat.ieeta.pt:9071/api WEB_URL=http://mednat.ieeta.pt:9071 # Port Configuration (Production only - nginx exposes this port) -NGINX_PORT=9071 +NGINX_PORT=8081 diff --git a/deployment/.env.example b/deployment/.env.example index 14dd92be..ce13b88e 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -59,10 +59,10 @@ API_INTERNAL_URL=http://api:80 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml -KEYCLOAK_HOSTNAME=kc.mednat.ieeta.pt -KEYCLOAK_URL=http://kc.mednat.ieeta.pt:9071 -API_URL=http://api.mednat.ieeta.pt:9071 +KEYCLOAK_HOSTNAME=http://mednat.ieeta.pt:9071 +KEYCLOAK_URL=http://mednat.ieeta.pt:9071/kc +API_URL=http://mednat.ieeta.pt:9071 WEB_URL=http://mednat.ieeta.pt:9071 # Port Configuration (Production only - nginx exposes this port) -NGINX_PORT=9071 +NGINX_PORT=8081 diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index b7783ec6..0c828919 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -126,8 +126,9 @@ services: KC_HTTP_ENABLED: "true" KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true" - KC_HOSTNAME: ${KEYCLOAK_URL} + KC_HOSTNAME: ${KEYCLOAK_HOSTNAME} KC_HOSTNAME_STRICT: "false" + KC_HTTP_RELATIVE_PATH: /kc KC_HEALTH_ENABLED: "true" diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf new file mode 100644 index 00000000..7164b572 --- /dev/null +++ b/deployment/nginx.mednat.conf @@ -0,0 +1,94 @@ +worker_processes auto; +worker_rlimit_nofile 65535; + +events { + multi_accept on; + worker_connections 65535; +} + +http { + client_max_body_size 100M; + resolver 127.0.0.11 ipv6=off valid=10s; + + # Necessário para proxy_set_header Connection $connection_upgrade; + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + upstream frontends { + zone frontends 64k; + server frontend:80 resolve max_fails=3 fail_timeout=30s; + } + + upstream apis { + zone apis 64k; + server api:80 resolve max_fails=3 fail_timeout=30s; + } + + upstream keycloaks { + zone keycloaks 64k; + server keycloak:8080 resolve max_fails=3 fail_timeout=30s; + } + + # Single host routing for mednat.ieeta.pt + server { + listen 8081; + listen [::]:8081; + server_name mednat.ieeta.pt; + + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + large_client_header_buffers 4 32k; + + # API on same host via /api + location /api/ { + proxy_pass http://apis; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + 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_cache_bypass $http_upgrade; + } + + # Keycloak on same host via /kc + location = /kc { + return 301 /kc/; + } + location /kc/ { + proxy_pass http://keycloaks; + proxy_redirect off; + proxy_intercept_errors off; + proxy_pass_request_headers on; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $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 $host; + proxy_set_header X-Forwarded-Port $server_port; + } + + location / { + proxy_pass http://frontends/; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + 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_cache_bypass $http_upgrade; + } + } +} diff --git a/deployment/nginx.conf b/deployment/nginx.securelearning copy.conf similarity index 100% rename from deployment/nginx.conf rename to deployment/nginx.securelearning copy.conf From 359d8163a903f420313da56a792abbf43f5a0c46 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Mon, 2 Mar 2026 15:35:10 +0000 Subject: [PATCH 07/87] changed deploy branch --- .github/workflows/CD-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CD-workflow.yml b/.github/workflows/CD-workflow.yml index f6ffedc4..1d2ff1f7 100644 --- a/.github/workflows/CD-workflow.yml +++ b/.github/workflows/CD-workflow.yml @@ -3,7 +3,7 @@ name: "CD-workflow" on: push: branches: - - main + - deploy jobs: build: From d544c8b19c6652072a4fc17b8b01660cf928b847 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Mon, 2 Mar 2026 15:43:11 +0000 Subject: [PATCH 08/87] forgor --- deployment/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 0c828919..55138604 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -7,7 +7,7 @@ services: ports: - "${NGINX_PORT}:8081" volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx.mednat.conf:/etc/nginx/nginx.conf:ro depends_on: frontend: condition: service_healthy From 62477dd064e7f29c32555ad3d552b51a5e23c586 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Mon, 2 Mar 2026 15:56:50 +0000 Subject: [PATCH 09/87] test --- deployment/docker-compose.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 55138604..1080acda 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -136,11 +136,10 @@ services: WEB_URL: ${WEB_URL} API_URL: ${API_URL} healthcheck: - test: [ "CMD-SHELL", "exec 3<>/dev/tcp/localhost/9000 && echo -e 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && timeout 1 cat <&3 | grep -q UP" ] + test: ["CMD", "true"] interval: 30s timeout: 10s - retries: 30 - start_period: 120s + retries: 3 expose: - "8080" - "9000" From da5fbc871fd9e8aed1ccce5b7519db151ed17903 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Mon, 2 Mar 2026 16:11:09 +0000 Subject: [PATCH 10/87] small keycloak fix --- deployment/nginx.mednat.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 7164b572..ca3ee7b2 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -64,6 +64,7 @@ http { location /kc/ { proxy_pass http://keycloaks; proxy_redirect off; + proxy_redirect ~^/admin(/.*)?$ /kc/admin$1; proxy_intercept_errors off; proxy_pass_request_headers on; proxy_http_version 1.1; From 6a372cd73d08c632da31cb901c84735edf1d41a8 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Mon, 2 Mar 2026 16:21:39 +0000 Subject: [PATCH 11/87] testing fr --- deployment/.env | 2 +- deployment/nginx.mednat.conf | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/deployment/.env b/deployment/.env index 1fc536bf..bb7d4a02 100644 --- a/deployment/.env +++ b/deployment/.env @@ -62,7 +62,7 @@ API_INTERNAL_URL=http://api:80 KEYCLOAK_HOSTNAME=http://mednat.ieeta.pt:9071 KEYCLOAK_URL=http://mednat.ieeta.pt:9071/kc API_URL=http://mednat.ieeta.pt:9071/api -WEB_URL=http://mednat.ieeta.pt:9071 +WEB_URL=http://mednat.ieeta.pt:9071/app # Port Configuration (Production only - nginx exposes this port) NGINX_PORT=8081 diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index ca3ee7b2..69d9b196 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -64,7 +64,6 @@ http { location /kc/ { proxy_pass http://keycloaks; proxy_redirect off; - proxy_redirect ~^/admin(/.*)?$ /kc/admin$1; proxy_intercept_errors off; proxy_pass_request_headers on; proxy_http_version 1.1; @@ -78,7 +77,7 @@ http { proxy_set_header X-Forwarded-Port $server_port; } - location / { + location /app/ { proxy_pass http://frontends/; proxy_redirect off; proxy_http_version 1.1; From ea4b5299a3dc5995f335d23bb7485931aa847f59 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Mon, 2 Mar 2026 16:36:47 +0000 Subject: [PATCH 12/87] caching --- .github/workflows/CD-workflow.yml | 15 ++++++++++++++- deployment/.env.example | 4 ++-- deployment/nginx.mednat.conf | 1 + 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CD-workflow.yml b/.github/workflows/CD-workflow.yml index ca2db608..52c6b373 100644 --- a/.github/workflows/CD-workflow.yml +++ b/.github/workflows/CD-workflow.yml @@ -19,9 +19,22 @@ jobs: run: | bash scripts/copy-env-examples.sh + - name: Build images with local Buildx cache + env: + DOCKER_BUILDKIT: 1 + run: | + mkdir -p .buildx-cache .buildx-cache-new + docker buildx bake \ + --file deployment/docker-compose.yml \ + --set '*.cache-from=type=local,src=.buildx-cache' \ + --set '*.cache-to=type=local,dest=.buildx-cache-new,mode=max' \ + --load + rm -rf .buildx-cache + mv .buildx-cache-new .buildx-cache + - name: Rebuild and redeploy stack env: DOCKER_BUILDKIT: 1 run: | docker compose --env-file deployment/.env -f deployment/docker-compose.yml down --remove-orphans - docker compose --env-file deployment/.env -f deployment/docker-compose.yml up -d --build + docker compose --env-file deployment/.env -f deployment/docker-compose.yml up -d diff --git a/deployment/.env.example b/deployment/.env.example index ce13b88e..bb7d4a02 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -61,8 +61,8 @@ API_INTERNAL_URL=http://api:80 # Use these settings when running with docker-compose.yml KEYCLOAK_HOSTNAME=http://mednat.ieeta.pt:9071 KEYCLOAK_URL=http://mednat.ieeta.pt:9071/kc -API_URL=http://mednat.ieeta.pt:9071 -WEB_URL=http://mednat.ieeta.pt:9071 +API_URL=http://mednat.ieeta.pt:9071/api +WEB_URL=http://mednat.ieeta.pt:9071/app # Port Configuration (Production only - nginx exposes this port) NGINX_PORT=8081 diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 69d9b196..92a4c34b 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -64,6 +64,7 @@ http { location /kc/ { proxy_pass http://keycloaks; proxy_redirect off; + proxy_redirect ~^/admin(/.*)?$ /kc/admin$1; proxy_intercept_errors off; proxy_pass_request_headers on; proxy_http_version 1.1; From 447c316d93e5fdb25580d1f425c94670d8205216 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Mon, 2 Mar 2026 16:39:04 +0000 Subject: [PATCH 13/87] small fix --- .github/workflows/CD-workflow.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/CD-workflow.yml b/.github/workflows/CD-workflow.yml index 52c6b373..70752c6a 100644 --- a/.github/workflows/CD-workflow.yml +++ b/.github/workflows/CD-workflow.yml @@ -23,8 +23,12 @@ jobs: env: DOCKER_BUILDKIT: 1 run: | + set -a + . deployment/.env + set +a mkdir -p .buildx-cache .buildx-cache-new docker buildx bake \ + --allow=fs.read=.. \ --file deployment/docker-compose.yml \ --set '*.cache-from=type=local,src=.buildx-cache' \ --set '*.cache-to=type=local,dest=.buildx-cache-new,mode=max' \ From b98f6552f39531c8ab755dc7dbb339e10a81a134 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Mon, 2 Mar 2026 16:40:10 +0000 Subject: [PATCH 14/87] rev --- .github/workflows/CD-workflow.yml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/.github/workflows/CD-workflow.yml b/.github/workflows/CD-workflow.yml index 70752c6a..695f1dbc 100644 --- a/.github/workflows/CD-workflow.yml +++ b/.github/workflows/CD-workflow.yml @@ -19,23 +19,6 @@ jobs: run: | bash scripts/copy-env-examples.sh - - name: Build images with local Buildx cache - env: - DOCKER_BUILDKIT: 1 - run: | - set -a - . deployment/.env - set +a - mkdir -p .buildx-cache .buildx-cache-new - docker buildx bake \ - --allow=fs.read=.. \ - --file deployment/docker-compose.yml \ - --set '*.cache-from=type=local,src=.buildx-cache' \ - --set '*.cache-to=type=local,dest=.buildx-cache-new,mode=max' \ - --load - rm -rf .buildx-cache - mv .buildx-cache-new .buildx-cache - - name: Rebuild and redeploy stack env: DOCKER_BUILDKIT: 1 From 9853932c3f5aefbb8dfbc2ae6003ed36968298c8 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Mon, 2 Mar 2026 16:43:53 +0000 Subject: [PATCH 15/87] fix --- deployment/nginx.mednat.conf | 1 - 1 file changed, 1 deletion(-) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 92a4c34b..6c061259 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -63,7 +63,6 @@ http { } location /kc/ { proxy_pass http://keycloaks; - proxy_redirect off; proxy_redirect ~^/admin(/.*)?$ /kc/admin$1; proxy_intercept_errors off; proxy_pass_request_headers on; From 30f2e6dc12b406abbc986445bf202d1cbb92bbc2 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Mon, 2 Mar 2026 16:49:43 +0000 Subject: [PATCH 16/87] please just work --- deployment/nginx.mednat.conf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 6c061259..6d4cac4f 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -64,12 +64,15 @@ http { location /kc/ { proxy_pass http://keycloaks; proxy_redirect ~^/admin(/.*)?$ /kc/admin$1; + proxy_redirect ~^/realms(/.*)?$ /kc/realms$1; + proxy_redirect ~^/resources(/.*)?$ /kc/resources$1; proxy_intercept_errors off; proxy_pass_request_headers on; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; + proxy_set_header X-Forwarded-Prefix /kc; 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; From 948f209e08130c7125f3bf11be96d0a4695ebf9e Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 3 Mar 2026 13:54:44 +0000 Subject: [PATCH 17/87] maybe? --- deployment/.env | 1 + deployment/docker-compose.yml | 5 ++--- deployment/nginx.mednat.conf | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/deployment/.env b/deployment/.env index a0991a49..cb67224f 100644 --- a/deployment/.env +++ b/deployment/.env @@ -59,6 +59,7 @@ WEB_URL=http://localhost:5173 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml +KC_HTTP_RELATIVE_PATH=/kc KEYCLOAK_HOSTNAME=http://mednat.ieeta.pt:9071 KEYCLOAK_URL=http://mednat.ieeta.pt:9071/kc API_URL=http://mednat.ieeta.pt:9071/api diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 1080acda..9a0dc2f8 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -127,9 +127,8 @@ services: KC_HTTP_ENABLED: "true" KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true" KC_HOSTNAME: ${KEYCLOAK_HOSTNAME} - KC_HOSTNAME_STRICT: "false" - KC_HTTP_RELATIVE_PATH: /kc - + KC_HTTP_RELATIVE_PATH: ${KC_HTTP_RELATIVE_PATH} + KC_HOSTNAME_STRICT: "true" KC_HEALTH_ENABLED: "true" CLIENT_SECRET: ${CLIENT_SECRET} diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 6d4cac4f..506d443a 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -63,9 +63,7 @@ http { } location /kc/ { proxy_pass http://keycloaks; - proxy_redirect ~^/admin(/.*)?$ /kc/admin$1; - proxy_redirect ~^/realms(/.*)?$ /kc/realms$1; - proxy_redirect ~^/resources(/.*)?$ /kc/resources$1; + proxy_redirect off; proxy_intercept_errors off; proxy_pass_request_headers on; proxy_http_version 1.1; From 91c0c0cb7491cea213d43daf46f85d965d2e71d6 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 3 Mar 2026 13:55:59 +0000 Subject: [PATCH 18/87] forgot --- deployment/.env.example | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/deployment/.env.example b/deployment/.env.example index bb7d4a02..cb67224f 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -44,10 +44,10 @@ API_INTERNAL_URL=http://api:80 # --- DEVELOPMENT (docker-compose.dev.yml) --- # Use these settings when running with docker-compose.dev.yml -#KEYCLOAK_HOSTNAME=localhost -#KEYCLOAK_URL=http://localhost:8080 -#API_URL=http://localhost:8000 -#WEB_URL=http://localhost:5173 +KEYCLOAK_HOSTNAME=localhost +KEYCLOAK_URL=http://localhost:8080 +API_URL=http://localhost:8000 +WEB_URL=http://localhost:5173 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml @@ -59,10 +59,16 @@ API_INTERNAL_URL=http://api:80 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml +KC_HTTP_RELATIVE_PATH=/kc KEYCLOAK_HOSTNAME=http://mednat.ieeta.pt:9071 KEYCLOAK_URL=http://mednat.ieeta.pt:9071/kc API_URL=http://mednat.ieeta.pt:9071/api WEB_URL=http://mednat.ieeta.pt:9071/app +# # Use these settings when running with docker-compose.yml +# KEYCLOAK_HOSTNAME=kc.localhost +# KEYCLOAK_URL=http://kc.localhost +# API_URL=http://api.localhost +# WEB_URL=http://app.localhost # Port Configuration (Production only - nginx exposes this port) NGINX_PORT=8081 From 6ebd986b94c32782d5f68f44f8ed3977668df5fc Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 3 Mar 2026 14:08:39 +0000 Subject: [PATCH 19/87] some issues fix --- web/src/components/AdminPanel.tsx | 22 ++----------------- web/src/components/EmailEntry.tsx | 6 ++++- .../compliance/ComplianceQuizStep.tsx | 2 +- .../compliance/ComplianceReadStep.tsx | 1 - web/src/components/courses/CourseCard.tsx | 6 +++++ web/src/components/sidebar.tsx | 1 - 6 files changed, 14 insertions(+), 24 deletions(-) diff --git a/web/src/components/AdminPanel.tsx b/web/src/components/AdminPanel.tsx index 613e5157..79432123 100644 --- a/web/src/components/AdminPanel.tsx +++ b/web/src/components/AdminPanel.tsx @@ -1,12 +1,11 @@ import { useState, useEffect } from 'react' -import { useKeycloak } from '@react-keycloak/web' import { useNavigate } from '@tanstack/react-router' import { TenantForm } from './admin/TenantForm' import { PreviewPanel } from './admin/PreviewPanel' import { toast } from 'sonner' +import { apiClient } from '../lib/api-client' export function CreateTenantPage() { - const { keycloak } = useKeycloak() const navigate = useNavigate() const [realmName, setRealmName] = useState('') const [domain, setDomain] = useState('') @@ -41,24 +40,7 @@ export function CreateTenantPage() { setLogoPreviewUrl(previewUrl) } - const extractDetailText = (detail: unknown): string => { - if (typeof detail === 'string') return detail.trim() - if (!Array.isArray(detail) || detail.length === 0) return '' - const first = detail[0] as { msg?: unknown } | string - if (typeof first === 'string') return first - if (typeof first?.msg === 'string') return first.msg - return '' - } - - const getStatusErrorMessage = (status?: number): string | null => { - if (status === 401) return 'Your session expired. Please log in again and retry.' - if (status === 403) return 'You do not have permission to create tenants.' - if (status === 400 || status === 422) return 'Some tenant details are invalid. Please review the form.' - if (status && status >= 500) return 'Server error while creating tenant. Please try again in a moment.' - return null - } - - const getCreateTenantErrorMessage = (error: unknown): string => { + const getCreateTenantErrorMessage = (_error: unknown): string => { // The fetch-based client throws plain Error with message including statusText. // We can't reliably parse JSON body here, so return generic message. return 'Failed to create tenant. Please try again.' diff --git a/web/src/components/EmailEntry.tsx b/web/src/components/EmailEntry.tsx index 287e6b6c..ae4553a9 100644 --- a/web/src/components/EmailEntry.tsx +++ b/web/src/components/EmailEntry.tsx @@ -3,6 +3,10 @@ import { motion } from 'motion/react'; import { ArrowRight, Mail, Sparkles, Loader2 } from 'lucide-react'; import {apiClient} from '../lib/api-client' +type RealmLookupResponse = { + realm?: string; +}; + export const EmailEntry = () => { const [email, setEmail] = useState(''); const [loading, setLoading] = useState(false); @@ -19,7 +23,7 @@ export const EmailEntry = () => { throw new Error("Invalid email address"); } - const resp = await apiClient.get(`/realms?domain=${encodeURIComponent(domain)}`); + const resp = await apiClient.get(`/realms?domain=${encodeURIComponent(domain)}`); const realm = resp.realm; if (realm) { diff --git a/web/src/components/compliance/ComplianceQuizStep.tsx b/web/src/components/compliance/ComplianceQuizStep.tsx index 7f416144..dc25dc91 100644 --- a/web/src/components/compliance/ComplianceQuizStep.tsx +++ b/web/src/components/compliance/ComplianceQuizStep.tsx @@ -1,4 +1,4 @@ -import { ArrowUpRight, CheckCircle2, RotateCcw, ShieldCheck, Timer } from "lucide-react"; +import { ArrowUpRight, CheckCircle2, RotateCcw, Timer } from "lucide-react"; import type { QuizPayload, SubmitResponse } from "./types"; import { apiClient } from "../../lib/api-client"; diff --git a/web/src/components/compliance/ComplianceReadStep.tsx b/web/src/components/compliance/ComplianceReadStep.tsx index c9cde222..2b043e0e 100644 --- a/web/src/components/compliance/ComplianceReadStep.tsx +++ b/web/src/components/compliance/ComplianceReadStep.tsx @@ -1,5 +1,4 @@ import { useMemo } from "react"; -import { BookOpen } from "lucide-react"; import ReactMarkdown from "react-markdown"; import type { ComplianceDoc } from "./types"; import ExpandButton from "./ExpandButton"; diff --git a/web/src/components/courses/CourseCard.tsx b/web/src/components/courses/CourseCard.tsx index 50971611..86e44921 100644 --- a/web/src/components/courses/CourseCard.tsx +++ b/web/src/components/courses/CourseCard.tsx @@ -9,6 +9,12 @@ type CourseCardProps = { cols: GridCols } +const difficultyColor: Record = { + Beginner: 'bg-emerald-100 text-emerald-700', + Intermediate: 'bg-amber-100 text-amber-700', + Advanced: 'bg-rose-100 text-rose-700', +} + // ─── shared progress badge helpers ─────────────────────────────────────────── function ProgressBadge({ progress }: { progress: number }) { diff --git a/web/src/components/sidebar.tsx b/web/src/components/sidebar.tsx index daf40152..0a77a093 100644 --- a/web/src/components/sidebar.tsx +++ b/web/src/components/sidebar.tsx @@ -200,7 +200,6 @@ export function Sidebar() { if (isContentManagerRoute) return contentManagerLinks; return userLinks.filter((link) => { if (!link.roles) return true; - let hasAcess = false; if (link.feature && !realmFeatures[link.feature]) { return false; From 31d092f0634b26e34409e8daade25ec05bbd40f3 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 3 Mar 2026 14:18:22 +0000 Subject: [PATCH 20/87] small test --- deployment/nginx.mednat.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 506d443a..4035d4a1 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -37,6 +37,8 @@ http { listen [::]:8081; server_name mednat.ieeta.pt; + add_header X-Content-Type-Options "nosniff" always; + proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; From 1cd5b3c9945264ed2e921262e1c52235ec0596ca Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 3 Mar 2026 14:25:02 +0000 Subject: [PATCH 21/87] maybe this works? --- deployment/nginx.mednat.conf | 55 ++++++++++++++++++++++++++++++++---- web/Dockerfile | 4 +-- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 4035d4a1..a75a2cac 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -65,24 +65,55 @@ http { } location /kc/ { proxy_pass http://keycloaks; - proxy_redirect off; + proxy_redirect ~^/admin(/.*)?$ /kc/admin$1; + proxy_redirect ~^/realms(/.*)?$ /kc/realms$1; + proxy_redirect ~^/resources(/.*)?$ /kc/resources$1; + proxy_redirect ~^https?://[^/]+/admin(/.*)?$ /kc/admin$1; + proxy_redirect ~^https?://[^/]+/realms(/.*)?$ /kc/realms$1; + proxy_redirect ~^https?://[^/]+/resources(/.*)?$ /kc/resources$1; proxy_intercept_errors off; proxy_pass_request_headers on; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header X-Forwarded-Prefix /kc; 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 $host; + proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port; } - location /app/ { - proxy_pass http://frontends/; + location = /app { + return 301 /; + } + + location ~ ^/app/(.*)$ { + return 301 /$1; + } + + # Asset files must never fallback to index.html. + location /assets/ { + proxy_pass http://frontends; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + 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_cache_bypass $http_upgrade; + } + + # App routes fallback to index.html. + location / { + proxy_pass http://frontends; proxy_redirect off; + proxy_intercept_errors on; + error_page 404 = @frontend_index; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; @@ -93,5 +124,19 @@ http { proxy_set_header X-Forwarded-Host $http_host; proxy_cache_bypass $http_upgrade; } + + location @frontend_index { + proxy_pass http://frontends/index.html; + 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; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } } } diff --git a/web/Dockerfile b/web/Dockerfile index d0a70ca3..88299695 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -32,5 +32,5 @@ COPY --from=builder /app/dist ./dist EXPOSE 80 -# serve with SPA fallback (-s flag handles client-side routing) -CMD ["serve", "-s", "dist", "-l", "80"] +# Serve static files only. Nginx handles SPA route fallback. +CMD ["serve", "dist", "-l", "80"] From a24fb066f14ea758647046c96b06a995d6423319 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 3 Mar 2026 15:22:26 +0000 Subject: [PATCH 22/87] oopsie --- deployment/nginx.mednat.conf | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index a75a2cac..7f531b2b 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -65,12 +65,7 @@ http { } location /kc/ { proxy_pass http://keycloaks; - proxy_redirect ~^/admin(/.*)?$ /kc/admin$1; - proxy_redirect ~^/realms(/.*)?$ /kc/realms$1; - proxy_redirect ~^/resources(/.*)?$ /kc/resources$1; - proxy_redirect ~^https?://[^/]+/admin(/.*)?$ /kc/admin$1; - proxy_redirect ~^https?://[^/]+/realms(/.*)?$ /kc/realms$1; - proxy_redirect ~^https?://[^/]+/resources(/.*)?$ /kc/resources$1; + proxy_redirect off; proxy_intercept_errors off; proxy_pass_request_headers on; proxy_http_version 1.1; From 30b64c7bd4f93c796ec42d53ecd404d4583376e0 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 3 Mar 2026 15:26:53 +0000 Subject: [PATCH 23/87] oopsie v2 --- deployment/nginx.mednat.conf | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 7f531b2b..26c5e580 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -108,7 +108,6 @@ http { proxy_pass http://frontends; proxy_redirect off; proxy_intercept_errors on; - error_page 404 = @frontend_index; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; @@ -119,19 +118,5 @@ http { proxy_set_header X-Forwarded-Host $http_host; proxy_cache_bypass $http_upgrade; } - - location @frontend_index { - proxy_pass http://frontends/index.html; - 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; - add_header Cache-Control "no-store, no-cache, must-revalidate"; - add_header Pragma "no-cache"; - add_header Expires "0"; - } } } From 4d1b50731c1e670da95c2fefd36647afae087fc9 Mon Sep 17 00:00:00 2001 From: GabrielFcGoncalves Date: Wed, 4 Mar 2026 16:32:43 +0000 Subject: [PATCH 24/87] new flag --- deployment/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 9a0dc2f8..cc7a18bd 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -108,7 +108,7 @@ services: build: context: ../keycloak dockerfile: Dockerfile - command: start-dev --import-realm --proxy-headers=xforwarded + command: start-dev --import-realm --proxy-headers=xforwarded --http-relative-path=/kc container_name: keycloak restart: unless-stopped environment: From 30f86c294b45934e3a38ae707ee000e20e4f6347 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 5 Mar 2026 10:35:43 +0000 Subject: [PATCH 25/87] maybe --- deployment/.env.example | 13 +++++++------ deployment/docker-compose.yml | 3 ++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/deployment/.env.example b/deployment/.env.example index cb67224f..57cc39d7 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -44,10 +44,10 @@ API_INTERNAL_URL=http://api:80 # --- DEVELOPMENT (docker-compose.dev.yml) --- # Use these settings when running with docker-compose.dev.yml -KEYCLOAK_HOSTNAME=localhost -KEYCLOAK_URL=http://localhost:8080 -API_URL=http://localhost:8000 -WEB_URL=http://localhost:5173 +#KEYCLOAK_HOSTNAME=localhost +#KEYCLOAK_URL=http://localhost:8080 +#API_URL=http://localhost:8000 +#WEB_URL=http://localhost:5173 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml @@ -60,8 +60,9 @@ WEB_URL=http://localhost:5173 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml KC_HTTP_RELATIVE_PATH=/kc -KEYCLOAK_HOSTNAME=http://mednat.ieeta.pt:9071 -KEYCLOAK_URL=http://mednat.ieeta.pt:9071/kc +KC_HOSTNAME=mednat.ieeta.pt +KC_HOSTNAME_URL=http://mednat.ieeta.pt:9071/kc +KC_PROXY=edge API_URL=http://mednat.ieeta.pt:9071/api WEB_URL=http://mednat.ieeta.pt:9071/app # # Use these settings when running with docker-compose.yml diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 9a0dc2f8..1eed7f6c 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -128,8 +128,9 @@ services: KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true" KC_HOSTNAME: ${KEYCLOAK_HOSTNAME} KC_HTTP_RELATIVE_PATH: ${KC_HTTP_RELATIVE_PATH} - KC_HOSTNAME_STRICT: "true" + KC_HOSTNAME_STRICT: "false" KC_HEALTH_ENABLED: "true" + KC_PROXY: ${KC_PROXY} CLIENT_SECRET: ${CLIENT_SECRET} WEB_URL: ${WEB_URL} From 72e648f1bdfd6ac724371969118e962bc4c6d025 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 5 Mar 2026 10:40:11 +0000 Subject: [PATCH 26/87] a --- deployment/docker-compose.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 0b1c402c..c6407f01 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -125,12 +125,11 @@ services: KC_DB_PASSWORD: ${POSTGRES_PASSWORD} KC_HTTP_ENABLED: "true" - KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true" - KC_HOSTNAME: ${KEYCLOAK_HOSTNAME} - KC_HTTP_RELATIVE_PATH: ${KC_HTTP_RELATIVE_PATH} + KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "false" + KC_HOSTNAME: ${KC_HOSTNAME} + KC_HOSTNAME_URL: ${KC_HOSTNAME_URL} KC_HOSTNAME_STRICT: "false" KC_HEALTH_ENABLED: "true" - KC_PROXY: ${KC_PROXY} CLIENT_SECRET: ${CLIENT_SECRET} WEB_URL: ${WEB_URL} From 7bf0c064ac072d387736e6831873985256e19342 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 5 Mar 2026 10:45:51 +0000 Subject: [PATCH 27/87] MAYBE???? --- deployment/.env | 5 +++-- deployment/.env.example | 2 +- deployment/nginx.mednat.conf | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/deployment/.env b/deployment/.env index cb67224f..94fc586e 100644 --- a/deployment/.env +++ b/deployment/.env @@ -60,8 +60,8 @@ WEB_URL=http://localhost:5173 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml KC_HTTP_RELATIVE_PATH=/kc -KEYCLOAK_HOSTNAME=http://mednat.ieeta.pt:9071 -KEYCLOAK_URL=http://mednat.ieeta.pt:9071/kc +KC_HOSTNAME=mednat.ieeta.pt +KC_HOSTNAME_URL=http://mednat.ieeta.pt:9071/kc API_URL=http://mednat.ieeta.pt:9071/api WEB_URL=http://mednat.ieeta.pt:9071/app # # Use these settings when running with docker-compose.yml @@ -72,3 +72,4 @@ WEB_URL=http://mednat.ieeta.pt:9071/app # Port Configuration (Production only - nginx exposes this port) NGINX_PORT=8081 +SERVER_PORT=9071 \ No newline at end of file diff --git a/deployment/.env.example b/deployment/.env.example index 57cc39d7..b92f55f4 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -62,7 +62,6 @@ API_INTERNAL_URL=http://api:80 KC_HTTP_RELATIVE_PATH=/kc KC_HOSTNAME=mednat.ieeta.pt KC_HOSTNAME_URL=http://mednat.ieeta.pt:9071/kc -KC_PROXY=edge API_URL=http://mednat.ieeta.pt:9071/api WEB_URL=http://mednat.ieeta.pt:9071/app # # Use these settings when running with docker-compose.yml @@ -73,3 +72,4 @@ WEB_URL=http://mednat.ieeta.pt:9071/app # Port Configuration (Production only - nginx exposes this port) NGINX_PORT=8081 +SERVER_PORT=9071 \ No newline at end of file diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 26c5e580..a2f8662d 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -77,7 +77,7 @@ http { 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 $server_port; + proxy_set_header X-Forwarded-Port 9071; } location = /app { From 65e9dabc4ce06f84106947a5700cc4fdb0ff875c Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 5 Mar 2026 10:50:54 +0000 Subject: [PATCH 28/87] test --- deployment/docker-compose.dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/docker-compose.dev.yml b/deployment/docker-compose.dev.yml index ddf90f19..86485fca 100644 --- a/deployment/docker-compose.dev.yml +++ b/deployment/docker-compose.dev.yml @@ -188,7 +188,7 @@ services: container_name: frontend environment: VITE_API_URL: ${API_URL}/api - VITE_KEYCLOAK_URL: ${KEYCLOAK_URL} + VITE_KEYCLOAK_URL: ${KC_HOSTNAME_URL} ports: - "5173:5173" volumes: From 5b13b2da0c31e0e05bce1812a4ae54ea823cce5d Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 11 Mar 2026 17:51:31 +0000 Subject: [PATCH 29/87] test self signed cert for web crypto api --- .github/workflows/CD-workflow.yml | 15 ++- deployment/.env | 23 ++-- deployment/.env.example | 15 ++- deployment/docker-compose.yml | 1 + deployment/nginx.mednat.conf | 11 +- deployment/scripts/generate-tls-certs.sh | 145 +++++++++++++++++++++++ 6 files changed, 193 insertions(+), 17 deletions(-) create mode 100755 deployment/scripts/generate-tls-certs.sh diff --git a/.github/workflows/CD-workflow.yml b/.github/workflows/CD-workflow.yml index 695f1dbc..e71e9631 100644 --- a/.github/workflows/CD-workflow.yml +++ b/.github/workflows/CD-workflow.yml @@ -19,9 +19,22 @@ jobs: run: | bash scripts/copy-env-examples.sh + - name: Generate TLS certificates + run: | + set -euo pipefail + source deployment/.env + mkdir -p deployment/certs + ./deployment/scripts/generate-tls-certs.sh \ + --domain "${KC_HOSTNAME}" \ + --alt "${KC_HOSTNAME},localhost,127.0.0.1" \ + --out-dir deployment/certs \ + --cert-file "${TLS_CERT_FILE:-tls.crt}" \ + --key-file "${TLS_KEY_FILE:-tls.key}" \ + --days 3650 + - name: Rebuild and redeploy stack env: DOCKER_BUILDKIT: 1 run: | docker compose --env-file deployment/.env -f deployment/docker-compose.yml down --remove-orphans - docker compose --env-file deployment/.env -f deployment/docker-compose.yml up -d + docker compose --env-file deployment/.env -f deployment/docker-compose.yml up -d --build diff --git a/deployment/.env b/deployment/.env index 94fc586e..2d0632aa 100644 --- a/deployment/.env +++ b/deployment/.env @@ -44,10 +44,10 @@ API_INTERNAL_URL=http://api:80 # --- DEVELOPMENT (docker-compose.dev.yml) --- # Use these settings when running with docker-compose.dev.yml -KEYCLOAK_HOSTNAME=localhost -KEYCLOAK_URL=http://localhost:8080 -API_URL=http://localhost:8000 -WEB_URL=http://localhost:5173 +# KEYCLOAK_HOSTNAME=localhost +# KEYCLOAK_URL=http://localhost:8080 +# API_URL=http://localhost:8000 +# WEB_URL=http://localhost:5173 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml @@ -61,9 +61,10 @@ WEB_URL=http://localhost:5173 # Use these settings when running with docker-compose.yml KC_HTTP_RELATIVE_PATH=/kc KC_HOSTNAME=mednat.ieeta.pt -KC_HOSTNAME_URL=http://mednat.ieeta.pt:9071/kc -API_URL=http://mednat.ieeta.pt:9071/api -WEB_URL=http://mednat.ieeta.pt:9071/app +KC_HOSTNAME_URL=https://mednat.ieeta.pt:9071/kc +KEYCLOAK_URL=https://mednat.ieeta.pt:9071/kc +API_URL=https://mednat.ieeta.pt:9071/api +WEB_URL=https://mednat.ieeta.pt:9071 # # Use these settings when running with docker-compose.yml # KEYCLOAK_HOSTNAME=kc.localhost # KEYCLOAK_URL=http://kc.localhost @@ -71,5 +72,9 @@ WEB_URL=http://mednat.ieeta.pt:9071/app # WEB_URL=http://app.localhost # Port Configuration (Production only - nginx exposes this port) -NGINX_PORT=8081 -SERVER_PORT=9071 \ No newline at end of file +NGINX_PORT=9071 +SERVER_PORT=9071 + +# TLS certificate file names inside deployment/certs +TLS_CERT_FILE=tls.crt +TLS_KEY_FILE=tls.key diff --git a/deployment/.env.example b/deployment/.env.example index b92f55f4..2736b513 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -61,9 +61,10 @@ API_INTERNAL_URL=http://api:80 # Use these settings when running with docker-compose.yml KC_HTTP_RELATIVE_PATH=/kc KC_HOSTNAME=mednat.ieeta.pt -KC_HOSTNAME_URL=http://mednat.ieeta.pt:9071/kc -API_URL=http://mednat.ieeta.pt:9071/api -WEB_URL=http://mednat.ieeta.pt:9071/app +KC_HOSTNAME_URL=https://mednat.ieeta.pt:9071/kc +KEYCLOAK_URL=https://mednat.ieeta.pt:9071/kc +API_URL=https://mednat.ieeta.pt:9071/api +WEB_URL=https://mednat.ieeta.pt:9071 # # Use these settings when running with docker-compose.yml # KEYCLOAK_HOSTNAME=kc.localhost # KEYCLOAK_URL=http://kc.localhost @@ -71,5 +72,9 @@ WEB_URL=http://mednat.ieeta.pt:9071/app # WEB_URL=http://app.localhost # Port Configuration (Production only - nginx exposes this port) -NGINX_PORT=8081 -SERVER_PORT=9071 \ No newline at end of file +NGINX_PORT=9071 +SERVER_PORT=9071 + +# TLS certificate file names inside deployment/certs +TLS_CERT_FILE=tls.crt +TLS_KEY_FILE=tls.key diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index c6407f01..a789c902 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -8,6 +8,7 @@ services: - "${NGINX_PORT}:8081" volumes: - ./nginx.mednat.conf:/etc/nginx/nginx.conf:ro + - ./certs:/etc/nginx/certs:ro depends_on: frontend: condition: service_healthy diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index a2f8662d..2d25afa6 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -33,10 +33,17 @@ http { # Single host routing for mednat.ieeta.pt server { - listen 8081; - listen [::]:8081; + listen 8081 ssl; + listen [::]:8081 ssl; server_name 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; + add_header X-Content-Type-Options "nosniff" always; proxy_buffer_size 128k; diff --git a/deployment/scripts/generate-tls-certs.sh b/deployment/scripts/generate-tls-certs.sh new file mode 100755 index 00000000..3648fe20 --- /dev/null +++ b/deployment/scripts/generate-tls-certs.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generate self-signed TLS certs for local/staging use. +# Usage: +# ./deployment/scripts/generate-tls-certs.sh \ +# --domain mednat.ieeta.pt \ +# --alt mednat.ieeta.pt,localhost,127.0.0.1 \ +# --days 365 + +DOMAIN="" +ALT_NAMES="" +DAYS=365 +OUT_DIR="deployment/certs" +KEY_FILE="" +CERT_FILE="" + +usage() { + cat < [options] + +Options: + --domain Primary certificate CN (required) + --alt Subject Alternative Names CSV (default: domain) + --days Validity in days (default: 365) + --out-dir Output directory (default: deployment/certs) + --key-file Private key filename (default: .key) + --cert-file Certificate filename (default: .crt) + -h, --help Show this help + +Example: + $0 --domain mednat.ieeta.pt --alt mednat.ieeta.pt,localhost,127.0.0.1 +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --domain) + DOMAIN="${2:-}" + shift 2 + ;; + --alt) + ALT_NAMES="${2:-}" + shift 2 + ;; + --days) + DAYS="${2:-}" + shift 2 + ;; + --out-dir) + OUT_DIR="${2:-}" + shift 2 + ;; + --key-file) + KEY_FILE="${2:-}" + shift 2 + ;; + --cert-file) + CERT_FILE="${2:-}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + esac +done + +if [[ -z "$DOMAIN" ]]; then + echo "Error: --domain is required" >&2 + usage + exit 1 +fi + +if [[ -z "$ALT_NAMES" ]]; then + ALT_NAMES="$DOMAIN" +fi + +if ! command -v openssl >/dev/null 2>&1; then + echo "Error: openssl is required but not installed." >&2 + exit 1 +fi + +mkdir -p "$OUT_DIR" + +if [[ -z "$KEY_FILE" ]]; then + KEY_FILE="$DOMAIN.key" +fi +if [[ -z "$CERT_FILE" ]]; then + CERT_FILE="$DOMAIN.crt" +fi + +KEY_PATH="$OUT_DIR/$KEY_FILE" +CERT_PATH="$OUT_DIR/$CERT_FILE" +EXT_PATH="$OUT_DIR/$DOMAIN.ext" + +IFS=',' read -r -a SAN_ARRAY <<< "$ALT_NAMES" +SAN_LINE="" +idx=1 +for san in "${SAN_ARRAY[@]}"; do + san_trimmed="$(echo "$san" | xargs)" + if [[ -z "$san_trimmed" ]]; then + continue + fi + if [[ "$san_trimmed" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + SAN_LINE+="IP.${idx} = ${san_trimmed}"$'\n' + else + SAN_LINE+="DNS.${idx} = ${san_trimmed}"$'\n' + fi + idx=$((idx + 1)) +done + +cat > "$EXT_PATH" < Date: Wed, 11 Mar 2026 17:59:39 +0000 Subject: [PATCH 30/87] forgor --- deployment/nginx.mednat.conf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 2d25afa6..3f4c7e95 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -53,7 +53,7 @@ http { # API on same host via /api location /api/ { - proxy_pass http://apis; + proxy_pass https://apis; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; @@ -71,7 +71,7 @@ http { return 301 /kc/; } location /kc/ { - proxy_pass http://keycloaks; + proxy_pass https://keycloaks; proxy_redirect off; proxy_intercept_errors off; proxy_pass_request_headers on; @@ -97,7 +97,7 @@ http { # Asset files must never fallback to index.html. location /assets/ { - proxy_pass http://frontends; + proxy_pass https://frontends; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; @@ -112,7 +112,7 @@ http { # App routes fallback to index.html. location / { - proxy_pass http://frontends; + proxy_pass https://frontends; proxy_redirect off; proxy_intercept_errors on; proxy_http_version 1.1; From 3b9897733089f829a49d73efffc58ad4ef942daa Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 11 Mar 2026 18:23:37 +0000 Subject: [PATCH 31/87] test fix --- .github/workflows/CD-workflow.yml | 2 +- deployment/docker-compose.yml | 1 + deployment/nginx.mednat.conf | 21 ++++++++++++++------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/CD-workflow.yml b/.github/workflows/CD-workflow.yml index e71e9631..97cfc7cf 100644 --- a/.github/workflows/CD-workflow.yml +++ b/.github/workflows/CD-workflow.yml @@ -37,4 +37,4 @@ jobs: DOCKER_BUILDKIT: 1 run: | docker compose --env-file deployment/.env -f deployment/docker-compose.yml down --remove-orphans - docker compose --env-file deployment/.env -f deployment/docker-compose.yml up -d --build + docker compose --env-file deployment/.env -f deployment/docker-compose.yml up -d diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index a789c902..64d0d0ea 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -5,6 +5,7 @@ services: image: nginx:alpine restart: unless-stopped ports: + - "8081:8081" - "${NGINX_PORT}:8081" volumes: - ./nginx.mednat.conf:/etc/nginx/nginx.conf:ro diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 3f4c7e95..27170165 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -10,6 +10,15 @@ http { client_max_body_size 100M; resolver 127.0.0.11 ipv6=off valid=10s; + # Use the incoming host port for upstream forwarded-port. + # Examples: + # - Host: mednat.ieeta.pt:9071 -> 9071 + # - Host: 127.0.0.1:8081 -> 8081 + map $http_host $forwarded_port { + ~:(\d+)$ $1; + default 443; + } + # Necessário para proxy_set_header Connection $connection_upgrade; map $http_upgrade $connection_upgrade { default upgrade; @@ -44,8 +53,6 @@ http { ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; - add_header X-Content-Type-Options "nosniff" always; - proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; @@ -53,7 +60,7 @@ http { # API on same host via /api location /api/ { - proxy_pass https://apis; + proxy_pass http://apis; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; @@ -71,7 +78,7 @@ http { return 301 /kc/; } location /kc/ { - proxy_pass https://keycloaks; + proxy_pass http://keycloaks; proxy_redirect off; proxy_intercept_errors off; proxy_pass_request_headers on; @@ -84,7 +91,7 @@ http { 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 9071; + proxy_set_header X-Forwarded-Port $forwarded_port; } location = /app { @@ -97,7 +104,7 @@ http { # Asset files must never fallback to index.html. location /assets/ { - proxy_pass https://frontends; + proxy_pass http://frontends; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; @@ -112,7 +119,7 @@ http { # App routes fallback to index.html. location / { - proxy_pass https://frontends; + proxy_pass http://frontends; proxy_redirect off; proxy_intercept_errors on; proxy_http_version 1.1; From a752fc772f0ac0555c3390fd637c164941d2924d Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 11 Mar 2026 19:00:05 +0000 Subject: [PATCH 32/87] frontend currently not working for paths --- deployment/nginx.mednat.conf | 1 - web/Dockerfile | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 27170165..e33f4510 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -121,7 +121,6 @@ http { location / { proxy_pass http://frontends; proxy_redirect off; - proxy_intercept_errors on; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; diff --git a/web/Dockerfile b/web/Dockerfile index 88299695..371652f7 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -32,5 +32,5 @@ COPY --from=builder /app/dist ./dist EXPOSE 80 -# Serve static files only. Nginx handles SPA route fallback. -CMD ["serve", "dist", "-l", "80"] +# Serve with SPA fallback (same behavior as previous working deployments). +CMD ["serve", "-s", "dist", "-l", "80"] From 0c849616d1dde24692e67e0c09a80a06aa4547bb Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 11 Mar 2026 19:13:08 +0000 Subject: [PATCH 33/87] test /app --- deployment/.env | 2 +- deployment/.env.example | 2 +- deployment/nginx.mednat.conf | 14 ++++++-------- web/Dockerfile | 4 ++-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/deployment/.env b/deployment/.env index 2d0632aa..ecebe5b4 100644 --- a/deployment/.env +++ b/deployment/.env @@ -64,7 +64,7 @@ KC_HOSTNAME=mednat.ieeta.pt KC_HOSTNAME_URL=https://mednat.ieeta.pt:9071/kc KEYCLOAK_URL=https://mednat.ieeta.pt:9071/kc API_URL=https://mednat.ieeta.pt:9071/api -WEB_URL=https://mednat.ieeta.pt:9071 +WEB_URL=https://mednat.ieeta.pt:9071/app # # Use these settings when running with docker-compose.yml # KEYCLOAK_HOSTNAME=kc.localhost # KEYCLOAK_URL=http://kc.localhost diff --git a/deployment/.env.example b/deployment/.env.example index 2736b513..45ee3797 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -64,7 +64,7 @@ KC_HOSTNAME=mednat.ieeta.pt KC_HOSTNAME_URL=https://mednat.ieeta.pt:9071/kc KEYCLOAK_URL=https://mednat.ieeta.pt:9071/kc API_URL=https://mednat.ieeta.pt:9071/api -WEB_URL=https://mednat.ieeta.pt:9071 +WEB_URL=https://mednat.ieeta.pt:9071/app # # Use these settings when running with docker-compose.yml # KEYCLOAK_HOSTNAME=kc.localhost # KEYCLOAK_URL=http://kc.localhost diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index e33f4510..4b1f60fe 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -94,13 +94,6 @@ http { proxy_set_header X-Forwarded-Port $forwarded_port; } - location = /app { - return 301 /; - } - - location ~ ^/app/(.*)$ { - return 301 /$1; - } # Asset files must never fallback to index.html. location /assets/ { @@ -117,8 +110,13 @@ http { proxy_cache_bypass $http_upgrade; } + # Keycloak on same host via /kc + location = /app { + return 301 /app/; + } + # App routes fallback to index.html. - location / { + location /app/ { proxy_pass http://frontends; proxy_redirect off; proxy_http_version 1.1; diff --git a/web/Dockerfile b/web/Dockerfile index 371652f7..88299695 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -32,5 +32,5 @@ COPY --from=builder /app/dist ./dist EXPOSE 80 -# Serve with SPA fallback (same behavior as previous working deployments). -CMD ["serve", "-s", "dist", "-l", "80"] +# Serve static files only. Nginx handles SPA route fallback. +CMD ["serve", "dist", "-l", "80"] From b1edbad5c84bc4768f360489123400772034e82f Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 11 Mar 2026 19:22:52 +0000 Subject: [PATCH 34/87] maybe now? --- deployment/.env | 1 + deployment/.env.example | 1 + deployment/docker-compose.yml | 1 + web/Dockerfile | 4 ++- web/src/keycloak.ts | 5 ++-- web/src/main.tsx | 8 ++++-- web/vite.config.ts | 53 +++++++++++++++++++---------------- 7 files changed, 44 insertions(+), 29 deletions(-) diff --git a/deployment/.env b/deployment/.env index ecebe5b4..f1343b51 100644 --- a/deployment/.env +++ b/deployment/.env @@ -60,6 +60,7 @@ API_INTERNAL_URL=http://api:80 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml KC_HTTP_RELATIVE_PATH=/kc +VITE_BASE_PATH=/app/ KC_HOSTNAME=mednat.ieeta.pt KC_HOSTNAME_URL=https://mednat.ieeta.pt:9071/kc KEYCLOAK_URL=https://mednat.ieeta.pt:9071/kc diff --git a/deployment/.env.example b/deployment/.env.example index 45ee3797..2a56ac47 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -60,6 +60,7 @@ API_INTERNAL_URL=http://api:80 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml KC_HTTP_RELATIVE_PATH=/kc +VITE_BASE_PATH=/app/ KC_HOSTNAME=mednat.ieeta.pt KC_HOSTNAME_URL=https://mednat.ieeta.pt:9071/kc KEYCLOAK_URL=https://mednat.ieeta.pt:9071/kc diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 64d0d0ea..0309a227 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -196,6 +196,7 @@ services: args: VITE_API_URL: ${API_URL} VITE_KEYCLOAK_URL: ${KEYCLOAK_URL} + VITE_BASE_PATH: ${VITE_BASE_PATH} container_name: frontend expose: - "80" diff --git a/web/Dockerfile b/web/Dockerfile index 88299695..29b72c8e 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -5,10 +5,12 @@ WORKDIR /app # Accept build arguments for Vite environment variables ARG VITE_API_URL ARG VITE_KEYCLOAK_URL +ARG VITE_BASE_PATH # Set them as environment variables for the build process -ENV VITE_API_URL=$VITE_API_URL/api +ENV VITE_API_URL=$VITE_API_URL ENV VITE_KEYCLOAK_URL=$VITE_KEYCLOAK_URL +ENV VITE_BASE_PATH=$VITE_BASE_PATH COPY package*.json ./ RUN npm config set registry https://registry.npmjs.org/ \ diff --git a/web/src/keycloak.ts b/web/src/keycloak.ts index 4c11da76..0c3b4e9e 100644 --- a/web/src/keycloak.ts +++ b/web/src/keycloak.ts @@ -1,6 +1,7 @@ import Keycloak from 'keycloak-js' -const isAdminRoute = window.location.pathname.startsWith("/admin") || window.location.pathname.startsWith("/content-manager"); +const appPath = window.location.pathname.replace(/^\/app(?=\/|$)/, '') || '/' +const isAdminRoute = appPath.startsWith("/admin") || appPath.startsWith("/content-manager"); const realm = isAdminRoute ? 'platform' : await resolveRealm(); const clientId = isAdminRoute ? 'react-admin' : 'react-app'; @@ -29,4 +30,4 @@ const keycloak = new Keycloak({ }); -export default keycloak \ No newline at end of file +export default keycloak diff --git a/web/src/main.tsx b/web/src/main.tsx index fca12dc4..78ada9c0 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -9,6 +9,9 @@ import { Providers } from './lib/providers' import keycloak from "./keycloak" import { EmailEntry } from './components/EmailEntry' +const stripBasePath = (pathname: string) => + pathname.replace(/^\/app(?=\/|$)/, '') || '/' + const isSecureContext = window.isSecureContext || window.location.hostname === 'localhost'; const initOptions: Keycloak.KeycloakInitOptions = { @@ -19,7 +22,7 @@ const initOptions: Keycloak.KeycloakInitOptions = { }; // Create a new router instance -const router = createRouter({ routeTree }) +const router = createRouter({ routeTree, basepath: '/app' }) // Register the router instance for type safety declare module '@tanstack/react-router' { @@ -98,7 +101,8 @@ const App = () => { const rootElement = document.getElementById('root')! if (!rootElement.innerHTML) { - const isAdminRoute = window.location.pathname.startsWith("/admin") || window.location.pathname.startsWith("/content-manager"); + const appPath = stripBasePath(window.location.pathname) + const isAdminRoute = appPath.startsWith("/admin") || appPath.startsWith("/content-manager"); const userRealm = localStorage.getItem('user_realm'); const hasValidRealm = isAdminRoute || !!userRealm; diff --git a/web/vite.config.ts b/web/vite.config.ts index c6a5d6e3..0e11d5d3 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -4,30 +4,35 @@ import { tanstackRouter } from '@tanstack/router-plugin/vite' import tailwindcss from '@tailwindcss/vite' import path from 'path' -export default defineConfig({ - plugins: [ - tanstackRouter({ - target: 'react', - autoCodeSplitting: true, - }), - react(), - tailwindcss(), - ], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), +export default defineConfig(() => { + const basePath = process.env.VITE_BASE_PATH || '/' + + return { + base: basePath, + plugins: [ + tanstackRouter({ + target: 'react', + autoCodeSplitting: true, + }), + react(), + tailwindcss(), + ], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + optimizeDeps: { + exclude: ['@tanstack/react-query-devtools'], + include: ['@tanstack/react-query'] }, - }, - optimizeDeps: { - exclude: ['@tanstack/react-query-devtools'], - include: ['@tanstack/react-query'] - }, - server: { - proxy: { - '/api': { - target: 'http://localhost:8000', - changeOrigin: true, + server: { + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, }, }, - }, -}) \ No newline at end of file + } +}) From acf6eb776d2549031f72f3793ab03141e636b5de Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 11 Mar 2026 19:31:58 +0000 Subject: [PATCH 35/87] idk anymore --- deployment/nginx.mednat.conf | 17 +++-------------- web/Dockerfile | 4 ++-- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 4b1f60fe..3a1e70b9 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -95,22 +95,11 @@ http { } - # Asset files must never fallback to index.html. - location /assets/ { - proxy_pass http://frontends; - proxy_redirect off; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - 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_cache_bypass $http_upgrade; + # Frontend entrypoint is /app. + location = / { + return 301 /app/; } - # Keycloak on same host via /kc location = /app { return 301 /app/; } diff --git a/web/Dockerfile b/web/Dockerfile index 29b72c8e..df309063 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -34,5 +34,5 @@ COPY --from=builder /app/dist ./dist EXPOSE 80 -# Serve static files only. Nginx handles SPA route fallback. -CMD ["serve", "dist", "-l", "80"] +# Serve with SPA fallback. +CMD ["serve", "-s", "dist", "-l", "80"] From 2f94bdc4245b27b9e15b15d488eb88544b1766df Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 11 Mar 2026 19:41:12 +0000 Subject: [PATCH 36/87] redirects --- deployment/docker-compose.yml | 2 +- deployment/nginx.mednat.conf | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 0309a227..4b622723 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -5,7 +5,7 @@ services: image: nginx:alpine restart: unless-stopped ports: - - "8081:8081" + - "127.0.0.1:8081:8081" - "${NGINX_PORT}:8081" volumes: - ./nginx.mednat.conf:/etc/nginx/nginx.conf:ro diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 3a1e70b9..56e9a15f 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -10,13 +10,12 @@ http { client_max_body_size 100M; resolver 127.0.0.11 ipv6=off valid=10s; - # Use the incoming host port for upstream forwarded-port. - # Examples: - # - Host: mednat.ieeta.pt:9071 -> 9071 - # - Host: 127.0.0.1:8081 -> 8081 - map $http_host $forwarded_port { - ~:(\d+)$ $1; - default 443; + # Canonical forwarded port by host. + # Public traffic uses 9071; only local loopback uses 8081. + map $host $forwarded_port { + default 9071; + localhost 8081; + 127.0.0.1 8081; } # Necessário para proxy_set_header Connection $connection_upgrade; From 11f7ef787234c403d54282d8c7c0e4459f4da4fd Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 11 Mar 2026 19:44:40 +0000 Subject: [PATCH 37/87] i dont understand --- deployment/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 4b622723..0309a227 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -5,7 +5,7 @@ services: image: nginx:alpine restart: unless-stopped ports: - - "127.0.0.1:8081:8081" + - "8081:8081" - "${NGINX_PORT}:8081" volumes: - ./nginx.mednat.conf:/etc/nginx/nginx.conf:ro From 83eebcaa67ad6c6263776d73abb2aff40534d2da Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 11 Mar 2026 19:51:50 +0000 Subject: [PATCH 38/87] die --- deployment/nginx.mednat.conf | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 56e9a15f..729d7ae3 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -107,15 +107,18 @@ http { location /app/ { proxy_pass http://frontends; proxy_redirect off; + proxy_intercept_errors off; + proxy_pass_request_headers on; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Prefix /app; 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_cache_bypass $http_upgrade; + proxy_set_header X-Forwarded-Port $forwarded_port; } } } From 59edda57a4c16d5dd0b8a5ca04938d245f36eeb2 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 11 Mar 2026 19:56:20 +0000 Subject: [PATCH 39/87] test --- deployment/nginx.mednat.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 729d7ae3..9b2ac4ab 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -105,6 +105,7 @@ http { # App routes fallback to index.html. location /app/ { + rewrite ^/app/(.*)$ /$1 break; proxy_pass http://frontends; proxy_redirect off; proxy_intercept_errors off; From 0614f7b0aadfa8a36db5312d9943a6b6af3d2d1e Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 11 Mar 2026 19:59:54 +0000 Subject: [PATCH 40/87] maybe now --- deployment/nginx.mednat.conf | 41 ++++++++++++++++++++++++++++++++++-- web/Dockerfile | 4 ++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 9b2ac4ab..4d25711a 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -103,12 +103,33 @@ http { return 301 /app/; } - # App routes fallback to index.html. + # Backward compatibility for stale absolute asset URLs. + location /assets/ { + return 301 /app$uri; + } + + # Static assets under /app/assets/* (never SPA-fallback to HTML). + location /app/assets/ { + rewrite ^/app/(.*)$ /$1 break; + proxy_pass http://frontends; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + 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_cache_bypass $http_upgrade; + } + + # SPA routes under /app/* fallback to index.html on 404. location /app/ { rewrite ^/app/(.*)$ /$1 break; proxy_pass http://frontends; proxy_redirect off; - proxy_intercept_errors off; + proxy_intercept_errors on; proxy_pass_request_headers on; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; @@ -120,6 +141,22 @@ http { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $forwarded_port; + error_page 404 = @app_index; + } + + location @app_index { + rewrite ^ /index.html break; + proxy_pass http://frontends; + 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; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; } } } diff --git a/web/Dockerfile b/web/Dockerfile index df309063..b409f311 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -34,5 +34,5 @@ COPY --from=builder /app/dist ./dist EXPOSE 80 -# Serve with SPA fallback. -CMD ["serve", "-s", "dist", "-l", "80"] +# Serve static files only; Nginx handles SPA fallback. +CMD ["serve", "dist", "-l", "80"] From 09cdecb7f9dc1e8813939e4f0d14179dcde76a37 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 11 Mar 2026 20:03:06 +0000 Subject: [PATCH 41/87] aaaaaaaaaaaaa --- deployment/nginx.mednat.conf | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 4d25711a..98b6e8bc 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -124,12 +124,12 @@ http { proxy_cache_bypass $http_upgrade; } - # SPA routes under /app/* fallback to index.html on 404. + # SPA routes under /app/*. location /app/ { rewrite ^/app/(.*)$ /$1 break; proxy_pass http://frontends; proxy_redirect off; - proxy_intercept_errors on; + proxy_intercept_errors off; proxy_pass_request_headers on; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; @@ -141,22 +141,6 @@ http { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $forwarded_port; - error_page 404 = @app_index; - } - - location @app_index { - rewrite ^ /index.html break; - proxy_pass http://frontends; - 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; - add_header Cache-Control "no-store, no-cache, must-revalidate"; - add_header Pragma "no-cache"; - add_header Expires "0"; } } } From 9704a63bffd6c53806bd2bf3d6b36e684fd6df8c Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 12 Mar 2026 09:48:17 +0000 Subject: [PATCH 42/87] forg --- deployment/.env | 2 +- deployment/.env.example | 2 +- deployment/nginx.mednat.conf | 8 +++----- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/deployment/.env b/deployment/.env index f1343b51..fafc380f 100644 --- a/deployment/.env +++ b/deployment/.env @@ -73,7 +73,7 @@ WEB_URL=https://mednat.ieeta.pt:9071/app # WEB_URL=http://app.localhost # Port Configuration (Production only - nginx exposes this port) -NGINX_PORT=9071 +NGINX_PORT=8081 SERVER_PORT=9071 # TLS certificate file names inside deployment/certs diff --git a/deployment/.env.example b/deployment/.env.example index 2a56ac47..d4d87e9a 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -73,7 +73,7 @@ WEB_URL=https://mednat.ieeta.pt:9071/app # WEB_URL=http://app.localhost # Port Configuration (Production only - nginx exposes this port) -NGINX_PORT=9071 +NGINX_PORT=8081 SERVER_PORT=9071 # TLS certificate file names inside deployment/certs diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index 98b6e8bc..e4df902c 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -12,12 +12,10 @@ http { # Canonical forwarded port by host. # Public traffic uses 9071; only local loopback uses 8081. - map $host $forwarded_port { - default 9071; - localhost 8081; - 127.0.0.1 8081; + map $http_host $forwarded_port { + ~:(\d+)$ $1; + default 443; } - # Necessário para proxy_set_header Connection $connection_upgrade; map $http_upgrade $connection_upgrade { default upgrade; From 6df59e00e38c50e8cd493ddffece24b74ad21000 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 12 Mar 2026 10:46:59 +0000 Subject: [PATCH 43/87] ???? --- deployment/nginx.mednat.conf | 1 + web/src/keycloak.ts | 4 ++-- web/src/main.tsx | 7 ++----- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/deployment/nginx.mednat.conf b/deployment/nginx.mednat.conf index e4df902c..d59497e6 100644 --- a/deployment/nginx.mednat.conf +++ b/deployment/nginx.mednat.conf @@ -42,6 +42,7 @@ http { listen 8081 ssl; listen [::]:8081 ssl; server_name mednat.ieeta.pt; + absolute_redirect off; ssl_certificate /etc/nginx/certs/tls.crt; ssl_certificate_key /etc/nginx/certs/tls.key; diff --git a/web/src/keycloak.ts b/web/src/keycloak.ts index 0c3b4e9e..bf1ee232 100644 --- a/web/src/keycloak.ts +++ b/web/src/keycloak.ts @@ -1,7 +1,7 @@ import Keycloak from 'keycloak-js' -const appPath = window.location.pathname.replace(/^\/app(?=\/|$)/, '') || '/' -const isAdminRoute = appPath.startsWith("/admin") || appPath.startsWith("/content-manager"); +const path = window.location.pathname || '/' +const isAdminRoute = path.startsWith("/admin") || path.startsWith("/content-manager"); const realm = isAdminRoute ? 'platform' : await resolveRealm(); const clientId = isAdminRoute ? 'react-admin' : 'react-app'; diff --git a/web/src/main.tsx b/web/src/main.tsx index 78ada9c0..9bc80f89 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -9,9 +9,6 @@ import { Providers } from './lib/providers' import keycloak from "./keycloak" import { EmailEntry } from './components/EmailEntry' -const stripBasePath = (pathname: string) => - pathname.replace(/^\/app(?=\/|$)/, '') || '/' - const isSecureContext = window.isSecureContext || window.location.hostname === 'localhost'; const initOptions: Keycloak.KeycloakInitOptions = { @@ -101,8 +98,8 @@ const App = () => { const rootElement = document.getElementById('root')! if (!rootElement.innerHTML) { - const appPath = stripBasePath(window.location.pathname) - const isAdminRoute = appPath.startsWith("/admin") || appPath.startsWith("/content-manager"); + const path = window.location.pathname || '/' + const isAdminRoute = path.startsWith("/admin") || path.startsWith("/content-manager"); const userRealm = localStorage.getItem('user_realm'); const hasValidRealm = isAdminRoute || !!userRealm; From b67e49c830ecb90322467470c2cc5d012340bfdc Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 12 Mar 2026 11:02:35 +0000 Subject: [PATCH 44/87] frfr? --- deployment/.env | 2 +- deployment/.env.example | 2 +- web/src/main.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deployment/.env b/deployment/.env index fafc380f..8684339e 100644 --- a/deployment/.env +++ b/deployment/.env @@ -60,7 +60,7 @@ API_INTERNAL_URL=http://api:80 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml KC_HTTP_RELATIVE_PATH=/kc -VITE_BASE_PATH=/app/ +VITE_BASE_PATH=/ KC_HOSTNAME=mednat.ieeta.pt KC_HOSTNAME_URL=https://mednat.ieeta.pt:9071/kc KEYCLOAK_URL=https://mednat.ieeta.pt:9071/kc diff --git a/deployment/.env.example b/deployment/.env.example index d4d87e9a..18541b93 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -60,7 +60,7 @@ API_INTERNAL_URL=http://api:80 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml KC_HTTP_RELATIVE_PATH=/kc -VITE_BASE_PATH=/app/ +VITE_BASE_PATH=/ KC_HOSTNAME=mednat.ieeta.pt KC_HOSTNAME_URL=https://mednat.ieeta.pt:9071/kc KEYCLOAK_URL=https://mednat.ieeta.pt:9071/kc diff --git a/web/src/main.tsx b/web/src/main.tsx index 9bc80f89..5811b904 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -19,7 +19,7 @@ const initOptions: Keycloak.KeycloakInitOptions = { }; // Create a new router instance -const router = createRouter({ routeTree, basepath: '/app' }) +const router = createRouter({ routeTree }) // Register the router instance for type safety declare module '@tanstack/react-router' { From c1bb952151ca0f0f329f05e5b7a8204e5427cbe2 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 12 Mar 2026 17:45:04 +0000 Subject: [PATCH 45/87] man --- deployment/.env | 2 +- deployment/.env.example | 2 +- deployment/docker-compose.dev.yml | 2 ++ deployment/docker-compose.yml | 1 + web/Dockerfile | 2 ++ web/src/components/compliance/index.tsx | 3 ++- web/src/components/navbar.tsx | 5 +++-- web/src/components/sidebar.tsx | 5 +++-- web/src/keycloak.ts | 4 +++- web/src/main.tsx | 9 +++++++-- 10 files changed, 25 insertions(+), 10 deletions(-) diff --git a/deployment/.env b/deployment/.env index 8684339e..fafc380f 100644 --- a/deployment/.env +++ b/deployment/.env @@ -60,7 +60,7 @@ API_INTERNAL_URL=http://api:80 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml KC_HTTP_RELATIVE_PATH=/kc -VITE_BASE_PATH=/ +VITE_BASE_PATH=/app/ KC_HOSTNAME=mednat.ieeta.pt KC_HOSTNAME_URL=https://mednat.ieeta.pt:9071/kc KEYCLOAK_URL=https://mednat.ieeta.pt:9071/kc diff --git a/deployment/.env.example b/deployment/.env.example index 18541b93..d4d87e9a 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -60,7 +60,7 @@ API_INTERNAL_URL=http://api:80 # --- PRODUCTION (docker-compose.yml) --- # Use these settings when running with docker-compose.yml KC_HTTP_RELATIVE_PATH=/kc -VITE_BASE_PATH=/ +VITE_BASE_PATH=/app/ KC_HOSTNAME=mednat.ieeta.pt KC_HOSTNAME_URL=https://mednat.ieeta.pt:9071/kc KEYCLOAK_URL=https://mednat.ieeta.pt:9071/kc diff --git a/deployment/docker-compose.dev.yml b/deployment/docker-compose.dev.yml index 86485fca..c2d839ad 100644 --- a/deployment/docker-compose.dev.yml +++ b/deployment/docker-compose.dev.yml @@ -189,6 +189,8 @@ services: environment: VITE_API_URL: ${API_URL}/api VITE_KEYCLOAK_URL: ${KC_HOSTNAME_URL} + VITE_WEB_URL: ${WEB_URL} + VITE_BASE_PATH: ${VITE_BASE_PATH} ports: - "5173:5173" volumes: diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 0309a227..72df2bd3 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -197,6 +197,7 @@ services: VITE_API_URL: ${API_URL} VITE_KEYCLOAK_URL: ${KEYCLOAK_URL} VITE_BASE_PATH: ${VITE_BASE_PATH} + VITE_WEB_URL: ${WEB_URL} container_name: frontend expose: - "80" diff --git a/web/Dockerfile b/web/Dockerfile index b409f311..8475aaf8 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -6,11 +6,13 @@ WORKDIR /app ARG VITE_API_URL ARG VITE_KEYCLOAK_URL ARG VITE_BASE_PATH +ARG VITE_WEB_URL # Set them as environment variables for the build process ENV VITE_API_URL=$VITE_API_URL ENV VITE_KEYCLOAK_URL=$VITE_KEYCLOAK_URL ENV VITE_BASE_PATH=$VITE_BASE_PATH +ENV VITE_WEB_URL=$VITE_WEB_URL COPY package*.json ./ RUN npm config set registry https://registry.npmjs.org/ \ diff --git a/web/src/components/compliance/index.tsx b/web/src/components/compliance/index.tsx index 7200a4ae..8cccb872 100644 --- a/web/src/components/compliance/index.tsx +++ b/web/src/components/compliance/index.tsx @@ -3,6 +3,7 @@ import { createPortal } from "react-dom"; import { AlertTriangle, Loader2 } from "lucide-react"; import { useKeycloak } from "@react-keycloak/web"; import { apiClient } from "../../lib/api-client"; +import { isAppRoute } from "../../lib/app-path"; import type { ComplianceDoc, @@ -106,7 +107,7 @@ export default function ComplianceFlow() { const isAdminContext = typeof window !== "undefined" && - (location.pathname.startsWith("/admin") || + (isAppRoute(location.pathname, "/admin") || keycloak.tokenParsed?.iss?.includes("/realms/master") || realmRoles.has("admin")); const isOrgManager = realmRoles.has("org_manager"); diff --git a/web/src/components/navbar.tsx b/web/src/components/navbar.tsx index 847a98a0..6250198b 100644 --- a/web/src/components/navbar.tsx +++ b/web/src/components/navbar.tsx @@ -2,6 +2,7 @@ import { ChevronRight, User, LogOut } from "lucide-react"; import { useKeycloak } from "@react-keycloak/web"; import { useRouterState, Link } from "@tanstack/react-router"; import "../css/navbar.css"; +import { appBasePath } from "@/lib/app-path"; export function Logo() { @@ -27,14 +28,14 @@ export function Navbar() { const routerState = useRouterState(); const { keycloak } = useKeycloak(); const currentPath = routerState.location.pathname; - const BASE_URL = import.meta.env.VITE_WEB_URL; + const baseUrl = import.meta.env.VITE_WEB_URL || window.location.origin + appBasePath; const parts = currentPath.split("/").filter(Boolean); const handleLogout = async () => { try { await keycloak.logout({ - redirectUri: BASE_URL, + redirectUri: baseUrl, }); console.log('User has been successfully logged out and redirected.'); diff --git a/web/src/components/sidebar.tsx b/web/src/components/sidebar.tsx index 0a77a093..68a0b582 100644 --- a/web/src/components/sidebar.tsx +++ b/web/src/components/sidebar.tsx @@ -2,6 +2,7 @@ import { Link } from "@tanstack/react-router"; import { useState } from "react"; import { useKeycloak } from "@react-keycloak/web"; import { useLocation } from "@tanstack/react-router"; +import { isAppRoute } from "@/lib/app-path"; import { LayoutDashboard, Megaphone, @@ -176,8 +177,8 @@ export function Sidebar() { const { keycloak } = useKeycloak(); const location = useLocation(); const shouldShowContent = !isCollapsed || isHovered; - const isAdminRoute = location.pathname.startsWith("/admin"); - const isContentManagerRoute = location.pathname.startsWith("/content-manager"); + const isAdminRoute = isAppRoute(location.pathname, "/admin"); + const isContentManagerRoute = isAppRoute(location.pathname, "/content-manager"); const getUserRoles = () => { if (!keycloak.tokenParsed) return []; diff --git a/web/src/keycloak.ts b/web/src/keycloak.ts index bf1ee232..32314998 100644 --- a/web/src/keycloak.ts +++ b/web/src/keycloak.ts @@ -1,7 +1,9 @@ import Keycloak from 'keycloak-js' +import { isAppRoute } from './lib/app-path' const path = window.location.pathname || '/' -const isAdminRoute = path.startsWith("/admin") || path.startsWith("/content-manager"); +const isAdminRoute = + isAppRoute(path, "/admin") || isAppRoute(path, "/content-manager"); const realm = isAdminRoute ? 'platform' : await resolveRealm(); const clientId = isAdminRoute ? 'react-admin' : 'react-app'; diff --git a/web/src/main.tsx b/web/src/main.tsx index 5811b904..07478c08 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -8,6 +8,7 @@ import { routeTree } from './routeTree.gen' import { Providers } from './lib/providers' import keycloak from "./keycloak" import { EmailEntry } from './components/EmailEntry' +import { appBasePath, isAppRoute } from './lib/app-path' const isSecureContext = window.isSecureContext || window.location.hostname === 'localhost'; @@ -19,7 +20,10 @@ const initOptions: Keycloak.KeycloakInitOptions = { }; // Create a new router instance -const router = createRouter({ routeTree }) +const router = createRouter({ + routeTree, + basepath: appBasePath, +}) // Register the router instance for type safety declare module '@tanstack/react-router' { @@ -99,7 +103,8 @@ const App = () => { const rootElement = document.getElementById('root')! if (!rootElement.innerHTML) { const path = window.location.pathname || '/' - const isAdminRoute = path.startsWith("/admin") || path.startsWith("/content-manager"); + const isAdminRoute = + isAppRoute(path, "/admin") || isAppRoute(path, "/content-manager"); const userRealm = localStorage.getItem('user_realm'); const hasValidRealm = isAdminRoute || !!userRealm; From b118869c3221fce095751c9989944b78f6ccd42b Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 12 Mar 2026 18:03:03 +0000 Subject: [PATCH 46/87] no way --- .gitignore | 4 +++- web/src/lib/app-path.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 web/src/lib/app-path.ts diff --git a/.gitignore b/.gitignore index 90f8fbb3..fe320b3e 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,8 @@ downloads/ eggs/ .eggs/ lib/ +!web/src/lib/ +!web/src/lib/** lib64/ parts/ sdist/ @@ -173,4 +175,4 @@ poetry.toml # LSP config files pyrightconfig.json -# End of https://www.toptal.com/developers/gitignore/api/python \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/web/src/lib/app-path.ts b/web/src/lib/app-path.ts new file mode 100644 index 00000000..900f92bd --- /dev/null +++ b/web/src/lib/app-path.ts @@ -0,0 +1,26 @@ +const rawBaseUrl = import.meta.env.BASE_URL || "/"; + +export const appBasePath = + rawBaseUrl !== "/" && rawBaseUrl.endsWith("/") + ? rawBaseUrl.slice(0, -1) + : rawBaseUrl; + +export function stripAppBasePath(pathname: string) { + if (!pathname) return "/"; + if (appBasePath === "/") return pathname; + + if (pathname === appBasePath) return "/"; + if (pathname.startsWith(`${appBasePath}/`)) { + return pathname.slice(appBasePath.length) || "/"; + } + + return pathname; +} + +export function isAppRoute(pathname: string, routePrefix: string) { + const normalizedPath = stripAppBasePath(pathname); + return ( + normalizedPath === routePrefix || + normalizedPath.startsWith(`${routePrefix}/`) + ); +} From f987edb20b89fadc48b4095a798b5fd9156b6068 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 12 Mar 2026 18:36:34 +0000 Subject: [PATCH 47/87] could it be? --- deployment/.env | 2 +- deployment/.env.example | 2 +- web/Dockerfile | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deployment/.env b/deployment/.env index fafc380f..4cd7fd85 100644 --- a/deployment/.env +++ b/deployment/.env @@ -34,7 +34,7 @@ CLIENT_SECRET=your_very_secure_key_here # Internal URLs (Docker network communication) -KEYCLOAK_INTERNAL_URL=http://keycloak:8080 +KEYCLOAK_INTERNAL_URL=http://keycloak:8080/kc API_INTERNAL_URL=http://api:80 # ============================================================ diff --git a/deployment/.env.example b/deployment/.env.example index d4d87e9a..004929c5 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -34,7 +34,7 @@ CLIENT_SECRET=your_very_secure_key_here # Internal URLs (Docker network communication) -KEYCLOAK_INTERNAL_URL=http://keycloak:8080 +KEYCLOAK_INTERNAL_URL=http://keycloak:8080/kc API_INTERNAL_URL=http://api:80 # ============================================================ diff --git a/web/Dockerfile b/web/Dockerfile index 8475aaf8..9e158e5d 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -36,5 +36,5 @@ COPY --from=builder /app/dist ./dist EXPOSE 80 -# Serve static files only; Nginx handles SPA fallback. -CMD ["serve", "dist", "-l", "80"] +# Serve the app in SPA mode so client-side routes resolve to index.html. +CMD ["serve", "-s", "dist", "-l", "80"] From 58d3601f88c3002de9b872e1370404e433b86918 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 12 Mar 2026 18:49:51 +0000 Subject: [PATCH 48/87] assets issue --- web/src/components/AppLoader.tsx | 3 ++- web/src/components/navbar.tsx | 4 ++-- web/src/lib/app-path.ts | 5 +++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/web/src/components/AppLoader.tsx b/web/src/components/AppLoader.tsx index 491fb260..e6d93efe 100644 --- a/web/src/components/AppLoader.tsx +++ b/web/src/components/AppLoader.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { appAssetUrl } from '@/lib/app-path' /** * Full-screen branded loading screen. @@ -66,7 +67,7 @@ export function AppLoader({
SecureLearning - Logo + Logo
diff --git a/web/src/lib/app-path.ts b/web/src/lib/app-path.ts index 900f92bd..c0bcae4c 100644 --- a/web/src/lib/app-path.ts +++ b/web/src/lib/app-path.ts @@ -24,3 +24,8 @@ export function isAppRoute(pathname: string, routePrefix: string) { normalizedPath.startsWith(`${routePrefix}/`) ); } + +export function appAssetUrl(path: string) { + const normalizedPath = path.startsWith("/") ? path.slice(1) : path; + return `${appBasePath}/${normalizedPath}`.replace(/\/{2,}/g, "/"); +} From 64cb28705103afead2863ecb8451f94a6108a7f4 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 17 Mar 2026 15:50:20 +0000 Subject: [PATCH 49/87] test --- web/src/Pages/templates.tsx | 4 +- web/src/components/AdminPanel.tsx | 1 - .../components/admin/PreviewPanelHeader.tsx | 2 - .../UserManagementHeader.tsx | 2 +- .../content-manager/modules/ModuleDisplay.tsx | 2 +- web/src/components/courses/CourseCard.tsx | 6 --- .../phishing-kits/PhishingKitForm.tsx | 3 +- .../phishing-kits/PhishingKitsPage.tsx | 4 -- .../sending-profiles/SendingProfilesTable.tsx | 2 +- .../multi-picker/SearchableMultiPicker.tsx | 7 +-- web/src/components/ui/combobox.tsx | 41 ++++++++-------- web/src/routeTree.gen.ts | 48 +++++++++---------- web/src/routes/campaigns/index.tsx | 2 + .../content.$contentPieceId.tsx | 2 +- web/src/types/vendor-shims.d.ts | 18 +++++++ 15 files changed, 76 insertions(+), 68 deletions(-) create mode 100644 web/src/types/vendor-shims.d.ts diff --git a/web/src/Pages/templates.tsx b/web/src/Pages/templates.tsx index 3568648d..721f2b50 100644 --- a/web/src/Pages/templates.tsx +++ b/web/src/Pages/templates.tsx @@ -271,7 +271,7 @@ export default function TemplatesPage() { {templates.length === 0 ? ( ) : ( - setActiveTab(value as (typeof templatePaths)[number])}> + setActiveTab(value as (typeof templatePaths)[number])}> ); -} \ No newline at end of file +} diff --git a/web/src/components/AdminPanel.tsx b/web/src/components/AdminPanel.tsx index 8dc773db..327d4721 100644 --- a/web/src/components/AdminPanel.tsx +++ b/web/src/components/AdminPanel.tsx @@ -4,7 +4,6 @@ import { TenantForm } from './admin/TenantForm' import { PreviewPanel } from './admin/PreviewPanel' import { apiClient } from '../lib/api-client' import { toast } from 'sonner' -import { apiClient } from '../lib/api-client' export function CreateTenantPage() { const navigate = useNavigate() diff --git a/web/src/components/admin/PreviewPanelHeader.tsx b/web/src/components/admin/PreviewPanelHeader.tsx index 8e2a742c..413a41ff 100644 --- a/web/src/components/admin/PreviewPanelHeader.tsx +++ b/web/src/components/admin/PreviewPanelHeader.tsx @@ -1,5 +1,3 @@ -import { Eye } from 'lucide-react' - export function PreviewPanelHeader() { return (
diff --git a/web/src/components/admin/tenant-org-manager/UserManagementHeader.tsx b/web/src/components/admin/tenant-org-manager/UserManagementHeader.tsx index 0260af33..6a790fd3 100644 --- a/web/src/components/admin/tenant-org-manager/UserManagementHeader.tsx +++ b/web/src/components/admin/tenant-org-manager/UserManagementHeader.tsx @@ -1,5 +1,5 @@ import { Upload, Plus } from "lucide-react"; -import { RefObject } from "react"; +import type { RefObject } from "react"; import DisplayModeToggle from "@/components/shared/DisplayModeToggle"; import SearchBar from "@/components/shared/SearchBar"; diff --git a/web/src/components/content-manager/modules/ModuleDisplay.tsx b/web/src/components/content-manager/modules/ModuleDisplay.tsx index d2198120..0d1fa79a 100644 --- a/web/src/components/content-manager/modules/ModuleDisplay.tsx +++ b/web/src/components/content-manager/modules/ModuleDisplay.tsx @@ -3,13 +3,13 @@ import { motion, LayoutGroup, AnimatePresence } from 'motion/react' import { BookOpen, AlertCircle } from 'lucide-react' import { useKeycloak } from '@react-keycloak/web' import { fetchModules, type Module } from '@/services/modulesApi' -import type { ModuleSortValue } from './ToolBarModules' import { useDebounce } from '@/lib/useDebounce' import { DIFFICULTY_COLORS } from './module-creation/constants' import type { GridCols } from '@/components/courses/UniversalFilters' import CourseCard, { type CardItem } from '@/components/courses/CourseCard' const API_BASE = import.meta.env.VITE_API_URL as string +type ModuleSortValue = 'newest' | 'oldest' | 'title_asc' | 'title_desc' const gridClass: Record = { 1: 'grid-cols-1', diff --git a/web/src/components/courses/CourseCard.tsx b/web/src/components/courses/CourseCard.tsx index 4ff7e3eb..1541cd06 100644 --- a/web/src/components/courses/CourseCard.tsx +++ b/web/src/components/courses/CourseCard.tsx @@ -47,12 +47,6 @@ type CourseCardProps = { paramKey?: string } -const difficultyColor: Record = { - Beginner: 'bg-emerald-100 text-emerald-700', - Intermediate: 'bg-amber-100 text-amber-700', - Advanced: 'bg-rose-100 text-rose-700', -} - // ─── shared progress badge helpers ─────────────────────────────────────────── function ProgressBadge({ progress }: { progress: number }) { diff --git a/web/src/components/phishing-kits/PhishingKitForm.tsx b/web/src/components/phishing-kits/PhishingKitForm.tsx index 435d411a..e6dcbfaf 100644 --- a/web/src/components/phishing-kits/PhishingKitForm.tsx +++ b/web/src/components/phishing-kits/PhishingKitForm.tsx @@ -52,7 +52,6 @@ export default function PhishingKitForm({ function PhishingKitFormInner({ editId }: { readonly editId?: number }) { const navigate = useNavigate(); const { data, getValidationErrors, isValid } = usePhishingKit(); - const [submitError, setSubmitError] = useState(null); const [isCompleted, setIsCompleted] = useState(false); const stepIcons = [BookOpen, Mail, File, Send] as const; const stepCompletedIcons = [ @@ -121,7 +120,7 @@ function PhishingKitFormInner({ editId }: { readonly editId?: number }) { } catch (err) { const message = err instanceof Error ? err.message : "Failed to save phishing kit"; - setSubmitError(message); + toast.error(message); return false; } }; diff --git a/web/src/components/phishing-kits/PhishingKitsPage.tsx b/web/src/components/phishing-kits/PhishingKitsPage.tsx index e5ab6812..4f17849b 100644 --- a/web/src/components/phishing-kits/PhishingKitsPage.tsx +++ b/web/src/components/phishing-kits/PhishingKitsPage.tsx @@ -13,12 +13,10 @@ export default function PhishingKitsPage() { kits, filteredKits, isLoading, - isFetching, searchQuery, setSearchQuery, viewMode, setViewMode, - refetch, handleDelete, isDeleting, } = usePhishingKits(); @@ -29,8 +27,6 @@ export default function PhishingKitsPage() { viewMode={viewMode} setViewMode={setViewMode} onNewKit={() => navigate({ to: "/phishing-kits/new" })} - refetch={refetch} - isFetching={isFetching} searchQuery={searchQuery} setSearchQuery={setSearchQuery} /> diff --git a/web/src/components/sending-profiles/SendingProfilesTable.tsx b/web/src/components/sending-profiles/SendingProfilesTable.tsx index cb9885ab..cc582246 100644 --- a/web/src/components/sending-profiles/SendingProfilesTable.tsx +++ b/web/src/components/sending-profiles/SendingProfilesTable.tsx @@ -1,4 +1,4 @@ -import { Send, Mail, Trash2, Edit, ChevronRight, Server } from "lucide-react"; +import { Send, Mail, Trash2, Edit, Server } from "lucide-react"; import { Link } from "@tanstack/react-router"; import { type SendingProfileDisplayInfo } from "@/types/sendingProfile"; diff --git a/web/src/components/shared/multi-picker/SearchableMultiPicker.tsx b/web/src/components/shared/multi-picker/SearchableMultiPicker.tsx index d3c3dfbc..9a0b1bdf 100644 --- a/web/src/components/shared/multi-picker/SearchableMultiPicker.tsx +++ b/web/src/components/shared/multi-picker/SearchableMultiPicker.tsx @@ -1,4 +1,5 @@ import { useCallback, useMemo, useRef, useState, type ReactNode } from "react"; +import type { FocusEvent } from "react"; import { CircleQuestionMark, Loader2, Search } from "lucide-react"; import { Input } from "@/components/ui/input"; @@ -196,7 +197,7 @@ export default function SearchableMultiPicker< { + onOpenChange={(nextOpen: boolean) => { if (nextOpen) setOpen(true); }} modal={false} @@ -229,8 +230,8 @@ export default function SearchableMultiPicker< e.preventDefault()} - onFocusOutside={(e) => e.preventDefault()} + onOpenAutoFocus={(e: Event) => e.preventDefault()} + onFocusOutside={(e: FocusEvent | Event) => e.preventDefault()} style={{ width: anchorWidth ?? "100%" }} className="p-0 shadow-lg border-border bg-popover" > diff --git a/web/src/components/ui/combobox.tsx b/web/src/components/ui/combobox.tsx index 2415bd80..771b75cc 100644 --- a/web/src/components/ui/combobox.tsx +++ b/web/src/components/ui/combobox.tsx @@ -15,7 +15,7 @@ import { const Combobox = ComboboxPrimitive.Root -function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { +function ComboboxValue({ ...props }: React.ComponentProps) { return } @@ -23,7 +23,7 @@ function ComboboxTrigger({ className, children, ...props -}: ComboboxPrimitive.Trigger.Props) { +}: React.ComponentProps) { return ( ) { return ( & { showTrigger?: boolean showClear?: boolean }) { @@ -97,11 +97,13 @@ function ComboboxContent({ alignOffset = 0, anchor, ...props -}: ComboboxPrimitive.Popup.Props & - Pick< - ComboboxPrimitive.Positioner.Props, - "side" | "align" | "sideOffset" | "alignOffset" | "anchor" - >) { +}: React.ComponentProps & { + side?: string + sideOffset?: number + align?: string + alignOffset?: number + anchor?: unknown +}) { return ( ) { return ( ) { return ( ) { return ( ) { return ( ) { return ( ) } -function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { +function ComboboxEmpty({ className, ...props }: React.ComponentProps) { return ( ) { return ( & - ComboboxPrimitive.Chips.Props) { +}: React.ComponentPropsWithRef) { return ( & { showRemove?: boolean }) { return ( @@ -276,7 +277,7 @@ function ComboboxChipsInput({ className, children, ...props -}: ComboboxPrimitive.Input.Props) { +}: React.ComponentProps) { return ( { - throw redirect({ to: "/content-manager/content" }); + throw redirect({ to: "/content-manager/content", search: { addFile: false } }); }, component: RouteComponent, }); diff --git a/web/src/types/vendor-shims.d.ts b/web/src/types/vendor-shims.d.ts new file mode 100644 index 00000000..dd16513e --- /dev/null +++ b/web/src/types/vendor-shims.d.ts @@ -0,0 +1,18 @@ +declare module "@base-ui/react" { + export const Combobox: any; +} + +declare module "cmdk" { + export const Command: any; +} + +declare module "radix-ui" { + export const Dialog: any; + export const Popover: any; + export const ScrollArea: any; + export const Slot: any; + export const Tabs: any; + export const Toggle: any; + export const ToggleGroup: any; + export const Tooltip: any; +} From 2f69e461cf7480983c7219c68a48425fc401e507 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 17 Mar 2026 16:07:10 +0000 Subject: [PATCH 50/87] small fix --- api/uv.lock | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/api/uv.lock b/api/uv.lock index efc9bb6a..68e208e2 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -40,6 +40,7 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "apscheduler" }, + { name = "boto3" }, { name = "cryptography" }, { name = "fastapi", extra = ["all"] }, { name = "motor" }, @@ -60,6 +61,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "apscheduler", specifier = ">=3.10.0" }, + { name = "boto3", specifier = ">=1.39.0" }, { name = "cryptography", specifier = ">=46.0.3" }, { name = "fastapi", extras = ["all"], specifier = ">=0.119.0" }, { name = "motor", specifier = ">=3.6.0" }, @@ -89,6 +91,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/9f/d3c76f76c73fcc959d28e9def45b8b1cc3d7722660c5003b19c1022fd7f4/apscheduler-3.11.1-py3-none-any.whl", hash = "sha256:6162cb5683cb09923654fa9bdd3130c4be4bfda6ad8990971c9597ecd52965d2", size = 64278, upload-time = "2025-10-31T18:55:41.186Z" }, ] +[[package]] +name = "boto3" +version = "1.42.69" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/f3/26d800e4efe85e7d59c63ac11d02ab2fafed371bede567af7258eb7e4c1c/boto3-1.42.69.tar.gz", hash = "sha256:e59846f4ff467b23bae4751948298db554dbdda0d72b09028d2cacbeff27e1ad", size = 112777, upload-time = "2026-03-16T20:35:30.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/39/54ad87436c637de9f7bf83ba2a28cf3b15409cbb849401837fcc37fbd794/boto3-1.42.69-py3-none-any.whl", hash = "sha256:6823a4b59aa578c7d98124280a9b6d83cea04bdb02525cbaa79370e5b6f7f631", size = 140556, upload-time = "2026-03-16T20:35:28.754Z" }, +] + +[[package]] +name = "botocore" +version = "1.42.69" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/d1/81a6e39c7d5419ba34bad8a1ac2c5360c26f21af698a481a8397d79134d1/botocore-1.42.69.tar.gz", hash = "sha256:0934f2d90403c5c8c2cba83e754a39d77edcad5885d04a79363edff3e814f55e", size = 14997632, upload-time = "2026-03-16T20:35:18.533Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/13/779f3427e17f9989fd0fa6651817c5f13b63e574f3541e460b8238883290/botocore-1.42.69-py3-none-any.whl", hash = "sha256:ef0e3d860a5d7bffc0ccb4911781c4c27d538557ed9a616ba1926c762d72e5f6", size = 14670334, upload-time = "2026-03-16T20:35:14.543Z" }, +] + [[package]] name = "certifi" version = "2025.10.5" @@ -463,6 +493,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, @@ -473,6 +504,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, @@ -483,6 +515,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, @@ -595,6 +628,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1073,6 +1115,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.1" @@ -1247,6 +1301,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, ] +[[package]] +name = "s3transfer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, +] + [[package]] name = "sentry-sdk" version = "2.43.0" @@ -1269,6 +1335,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" From 1ddb72b6bf926d9cac516dfe843da5240bbf057d Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 17 Mar 2026 16:14:16 +0000 Subject: [PATCH 51/87] aint no way this was forgotten --- deployment/docker-compose.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 18e4733c..573ac72c 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -63,6 +63,28 @@ services: timeout: 5s retries: 10 + garage: + image: dxflrs/garage:76592723deb9285a071320c40842f6be61e924fd + container_name: garage + restart: unless-stopped + command: /garage server --single-node --default-bucket + environment: + GARAGE_RPC_SECRET: ${GARAGE_RPC_SECRET} + GARAGE_DEFAULT_ACCESS_KEY: ${GARAGE_ACCESS_KEY_ID} + GARAGE_DEFAULT_SECRET_KEY: ${GARAGE_SECRET_ACCESS_KEY} + GARAGE_DEFAULT_BUCKET: ${GARAGE_BUCKET_CONTENT} + ports: + - "3900:3900" + - "3901:3901" + volumes: + - ./garage.toml:/etc/garage.toml:ro + - garage_data:/var/lib/garage + healthcheck: + test: [ "CMD", "/garage", "-c", "/etc/garage.toml", "status" ] + interval: 15s + timeout: 10s + retries: 10 + rabbitmq: build: context: ../rabbitmq @@ -195,6 +217,8 @@ services: condition: service_healthy keycloak: condition: service_healthy + garage: + condition: service_healthy mongo: condition: service_healthy rabbitmq: @@ -229,5 +253,6 @@ networks: volumes: db_data: + garage_data: rabbitmq_data: mongo_data: From 58431f2cb5eed3e7114d42ac00aa21c33d6dfa8b Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 17 Mar 2026 16:44:48 +0000 Subject: [PATCH 52/87] debug --- api/src/core/security.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/core/security.py b/api/src/core/security.py index 0bc877e9..dcd3b2cd 100644 --- a/api/src/core/security.py +++ b/api/src/core/security.py @@ -71,6 +71,7 @@ async def __call__(self, request: Request, access_token: Annotated[str, Depends( access_token, options={"verify_signature": False, "verify_aud": False}, ) + print(decoded) except Exception as e: logging.error(f"Failed to decode token: {e}") raise HTTPException(status_code=401, detail="Invalid token") From a222dff293049337a24977b0d0d0f14785b85514 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 17 Mar 2026 18:10:38 +0000 Subject: [PATCH 53/87] removed dev keycloak --- deployment/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 573ac72c..b4920a30 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -132,7 +132,7 @@ services: build: context: ../keycloak dockerfile: Dockerfile - command: start-dev --import-realm --proxy-headers=xforwarded --http-relative-path=/kc + command: start --import-realm --proxy-headers=xforwarded --http-relative-path=/kc container_name: keycloak restart: unless-stopped environment: From 048ad46515d9cf0cd0f697ab94cd319e8b43eb91 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 17 Mar 2026 18:22:02 +0000 Subject: [PATCH 54/87] test config --- api/src/core/security.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/api/src/core/security.py b/api/src/core/security.py index dcd3b2cd..f23694fa 100644 --- a/api/src/core/security.py +++ b/api/src/core/security.py @@ -71,12 +71,12 @@ async def __call__(self, request: Request, access_token: Annotated[str, Depends( access_token, options={"verify_signature": False, "verify_aud": False}, ) - print(decoded) except Exception as e: logging.error(f"Failed to decode token: {e}") raise HTTPException(status_code=401, detail="Invalid token") realm_name = self._extract_realm_name(decoded) + client_id = decoded.get("azp") self._verify_token(access_token, realm_name) @@ -85,7 +85,7 @@ async def __call__(self, request: Request, access_token: Annotated[str, Depends( for resource in self.resources: permission = f"{resource}#{self.scope}" last_permission = permission - if await self.check_keycloak_permission(access_token, permission, realm_name): + if await self.check_keycloak_permission(access_token, permission, realm_name, client_id): authorized = True break @@ -98,7 +98,13 @@ async def __call__(self, request: Request, access_token: Annotated[str, Depends( return {"authorized": True} - async def check_keycloak_permission(self, access_token: str, permission: str, realm_name: str) -> bool: + async def check_keycloak_permission( + self, + access_token: str, + permission: str, + realm_name: str, + client_id: str | None = None, + ) -> bool: """Verify user permissions""" if not AUTH_SERVER_URL: @@ -117,6 +123,8 @@ async def check_keycloak_permission(self, access_token: str, permission: str, re "permission": permission, "audience": RESOURCE_SERVER_ID, } + if client_id: + data["client_id"] = client_id try: async with httpx.AsyncClient() as client: @@ -126,7 +134,15 @@ async def check_keycloak_permission(self, access_token: str, permission: str, re payload = response.json() return "access_token" in payload - if response.status_code in [400, 403]: + if response.status_code in [400, 401, 403]: + logging.warning( + "Keycloak UMA denied permission=%s realm=%s client_id=%s status=%s body=%s", + permission, + realm_name, + client_id, + response.status_code, + response.text, + ) return False logging.error(f"Keycloak error {response.status_code}: {response.text}") @@ -145,4 +161,3 @@ def _extract_realm_name(self, token_data: dict) -> str: raise HTTPException(status_code=401, detail="Invalid token: missing issuer") return iss.split("/realms/")[-1] - From b2cb96799de83ef227d73ee7719b1cc4ec5a3cb7 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 17 Mar 2026 18:27:03 +0000 Subject: [PATCH 55/87] maybe?? --- api/src/core/security.py | 25 +++++-------------------- deployment/docker-compose.yml | 2 +- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/api/src/core/security.py b/api/src/core/security.py index f23694fa..dcd3b2cd 100644 --- a/api/src/core/security.py +++ b/api/src/core/security.py @@ -71,12 +71,12 @@ async def __call__(self, request: Request, access_token: Annotated[str, Depends( access_token, options={"verify_signature": False, "verify_aud": False}, ) + print(decoded) except Exception as e: logging.error(f"Failed to decode token: {e}") raise HTTPException(status_code=401, detail="Invalid token") realm_name = self._extract_realm_name(decoded) - client_id = decoded.get("azp") self._verify_token(access_token, realm_name) @@ -85,7 +85,7 @@ async def __call__(self, request: Request, access_token: Annotated[str, Depends( for resource in self.resources: permission = f"{resource}#{self.scope}" last_permission = permission - if await self.check_keycloak_permission(access_token, permission, realm_name, client_id): + if await self.check_keycloak_permission(access_token, permission, realm_name): authorized = True break @@ -98,13 +98,7 @@ async def __call__(self, request: Request, access_token: Annotated[str, Depends( return {"authorized": True} - async def check_keycloak_permission( - self, - access_token: str, - permission: str, - realm_name: str, - client_id: str | None = None, - ) -> bool: + async def check_keycloak_permission(self, access_token: str, permission: str, realm_name: str) -> bool: """Verify user permissions""" if not AUTH_SERVER_URL: @@ -123,8 +117,6 @@ async def check_keycloak_permission( "permission": permission, "audience": RESOURCE_SERVER_ID, } - if client_id: - data["client_id"] = client_id try: async with httpx.AsyncClient() as client: @@ -134,15 +126,7 @@ async def check_keycloak_permission( payload = response.json() return "access_token" in payload - if response.status_code in [400, 401, 403]: - logging.warning( - "Keycloak UMA denied permission=%s realm=%s client_id=%s status=%s body=%s", - permission, - realm_name, - client_id, - response.status_code, - response.text, - ) + if response.status_code in [400, 403]: return False logging.error(f"Keycloak error {response.status_code}: {response.text}") @@ -161,3 +145,4 @@ def _extract_realm_name(self, token_data: dict) -> str: raise HTTPException(status_code=401, detail="Invalid token: missing issuer") return iss.split("/realms/")[-1] + diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index b4920a30..77c75eb2 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -182,7 +182,7 @@ services: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} - KEYCLOAK_URL: ${KEYCLOAK_INTERNAL_URL} + KEYCLOAK_URL: ${KEYCLOAK_URL} CLIENT_SECRET: ${CLIENT_SECRET} MONGODB_URI: mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/${MONGO_DB}?authSource=${MONGO_DB} MONGODB_DB: securelearning From de8d4affa2d5c208cf0ae7ce3b3e36c6fd4fa57a Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 17 Mar 2026 18:35:02 +0000 Subject: [PATCH 56/87] test xonfigs in keycloak --- deployment/docker-compose.yml | 2 +- keycloak/imports/realm-export.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 77c75eb2..b4920a30 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -182,7 +182,7 @@ services: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} - KEYCLOAK_URL: ${KEYCLOAK_URL} + KEYCLOAK_URL: ${KEYCLOAK_INTERNAL_URL} CLIENT_SECRET: ${CLIENT_SECRET} MONGODB_URI: mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/${MONGO_DB}?authSource=${MONGO_DB} MONGODB_DB: securelearning diff --git a/keycloak/imports/realm-export.json b/keycloak/imports/realm-export.json index dde12329..603ac179 100644 --- a/keycloak/imports/realm-export.json +++ b/keycloak/imports/realm-export.json @@ -101,7 +101,7 @@ { "clientId": "api", "enabled": true, - "serviceAccountsEnabled": true, + "serviceAccountsEnabled": false, "authorizationServicesEnabled": true, "authorizationSettings": { "allowRemoteResourceManagement": true, @@ -193,4 +193,4 @@ } ] } -] \ No newline at end of file +] From bd95ab2eebc94dd40db49fe67b8c85a01d2d5d2b Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 17 Mar 2026 19:39:49 +0000 Subject: [PATCH 57/87] maybe??? --- api/.env.example | 1 + api/src/core/security.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/api/.env.example b/api/.env.example index ffb8207f..53e6c22f 100644 --- a/api/.env.example +++ b/api/.env.example @@ -5,6 +5,7 @@ POSTGRES_PORT=5432 POSTGRES_DB=mydatabase CLIENT_SECRET="MbV2B0dwJfvdfvYyeYjcay7EuT5kid2C" KEYCLOAK_URL="http://localhost:8080" +KEYCLOAK_ISSUER_URL="https://mednat.ieeta.pt:9071/kc/realms/platform" MONGODB_URI="mongodb://template_user:template_pass@mongo:27017/securelearning?authSource=securelearning" MONGODB_DB="securelearning" MONGODB_COLLECTION_TEMPLATES="templates" diff --git a/api/src/core/security.py b/api/src/core/security.py index dcd3b2cd..8f17d193 100644 --- a/api/src/core/security.py +++ b/api/src/core/security.py @@ -15,6 +15,7 @@ oauth_2_scheme = OAuth2PasswordBearer(tokenUrl="token") AUTH_SERVER_URL = os.getenv("KEYCLOAK_URL") +KEYCLOAK_ISSUER_URL = os.getenv("KEYCLOAK_ISSUER_URL", AUTH_SERVER_URL) RESOURCE_SERVER_ID = "api" @@ -101,11 +102,11 @@ async def __call__(self, request: Request, access_token: Annotated[str, Depends( async def check_keycloak_permission(self, access_token: str, permission: str, realm_name: str) -> bool: """Verify user permissions""" - if not AUTH_SERVER_URL: - logging.error("KEYCLOAK_INTERNAL_URL is not set") + if not KEYCLOAK_ISSUER_URL: + logging.error("KEYCLOAK_ISSUER_URL is not set") return False - url = f"{AUTH_SERVER_URL}/realms/{realm_name}/protocol/openid-connect/token" + url = f"{KEYCLOAK_ISSUER_URL}/realms/{realm_name}/protocol/openid-connect/token" headers = { "Authorization": f"Bearer {access_token}", @@ -145,4 +146,3 @@ def _extract_realm_name(self, token_data: dict) -> str: raise HTTPException(status_code=401, detail="Invalid token: missing issuer") return iss.split("/realms/")[-1] - From f6b8eb551696b9838989b5fb18b6d94f9c36e07a Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 17 Mar 2026 19:57:20 +0000 Subject: [PATCH 58/87] crazy --- api/src/core/security.py | 172 ++++++++++++++++++++------------------- 1 file changed, 88 insertions(+), 84 deletions(-) diff --git a/api/src/core/security.py b/api/src/core/security.py index 8f17d193..86de9edf 100644 --- a/api/src/core/security.py +++ b/api/src/core/security.py @@ -1,15 +1,12 @@ import logging import os -import traceback from enum import StrEnum +from typing import Annotated import jwt from jwt import PyJWKClient -import requests -from fastapi.security import OAuth2PasswordBearer from fastapi import Depends, HTTPException, Request -from typing import Annotated -import httpx +from fastapi.security import OAuth2PasswordBearer oauth_2_scheme = OAuth2PasswordBearer(tokenUrl="token") @@ -17,26 +14,23 @@ AUTH_SERVER_URL = os.getenv("KEYCLOAK_URL") KEYCLOAK_ISSUER_URL = os.getenv("KEYCLOAK_ISSUER_URL", AUTH_SERVER_URL) RESOURCE_SERVER_ID = "api" - +_JWKS_CLIENTS: dict[str, PyJWKClient] = {} class Resource(StrEnum): - """Keycloak UMA resource names used in permission checks.""" - ADMIN = "admin" - ORG_MANAGER = "org_manager" + ADMIN = "admin" + ORG_MANAGER = "org_manager" CONTENT_MANAGER = "content-manager" class Scope(StrEnum): - """Keycloak UMA scope names used in permission checks.""" - VIEW = "view" + VIEW = "view" MANAGE = "manage" class Roles: - """ - Uses UMA (uma-ticket) to validate permissions in Keycloak. + Validates the JWT locally and checks roles from the token payload. """ def __init__(self, resource: str | list[str], scope: str): @@ -44,105 +38,115 @@ def __init__(self, resource: str | list[str], scope: str): self.scope = scope def _get_jwks_client(self, realm_name: str) -> PyJWKClient: + if not AUTH_SERVER_URL: + raise HTTPException( + status_code=500, + detail="Authentication server not configured", + ) + + if realm_name in _JWKS_CLIENTS: + return _JWKS_CLIENTS[realm_name] + url = f"{AUTH_SERVER_URL}/realms/{realm_name}/protocol/openid-connect/certs" - return PyJWKClient(url) - + jwks_client = PyJWKClient(url) + _JWKS_CLIENTS[realm_name] = jwks_client + return jwks_client + + def _extract_realm_name(self, token_data: dict) -> str: + iss = token_data.get("iss") + + if not iss or "/realms/" not in iss: + raise HTTPException(status_code=401, detail="Invalid token: missing issuer") + + return iss.split("/realms/")[-1].split("/")[0] + + def _get_expected_issuer(self, realm_name: str) -> str: + if not KEYCLOAK_ISSUER_URL: + raise HTTPException( + status_code=500, + detail="Authentication server not configured", + ) + + if "/realms/" in KEYCLOAK_ISSUER_URL: + return KEYCLOAK_ISSUER_URL + + return f"{KEYCLOAK_ISSUER_URL}/realms/{realm_name}" def _verify_token(self, access_token: str, realm_name: str) -> dict: jwks_client = self._get_jwks_client(realm_name) - signing_key = jwks_client.get_signing_key_from_jwt(access_token) - + issuer = self._get_expected_issuer(realm_name) + try: - jwt.decode( + signing_key = jwks_client.get_signing_key_from_jwt(access_token) + return jwt.decode( access_token, signing_key.key, algorithms=["RS256"], - options={"verify_aud": False}, + issuer=issuer, + audience=RESOURCE_SERVER_ID, ) except jwt.ExpiredSignatureError: raise HTTPException(status_code=401, detail="Token has expired") + except jwt.InvalidAudienceError as e: + logging.error(f"Invalid audience: {e}") + raise HTTPException(status_code=401, detail="Invalid token") + except jwt.InvalidIssuerError as e: + logging.error(f"Invalid issuer: {e}") + raise HTTPException(status_code=401, detail="Invalid token") except jwt.InvalidTokenError as e: logging.error(f"Token verification failed: {e}") raise HTTPException(status_code=401, detail="Invalid token") + except Exception as e: + logging.error(f"Unexpected token verification error: {e}") + raise HTTPException(status_code=401, detail="Invalid token") + def _get_required_roles(self) -> set[str]: + permission_map = { + (Resource.ADMIN.value, Scope.VIEW.value): {"admin"}, + (Resource.ADMIN.value, Scope.MANAGE.value): {"admin"}, + (Resource.ORG_MANAGER.value, Scope.VIEW.value): {"org_manager"}, + (Resource.ORG_MANAGER.value, Scope.MANAGE.value): {"org_manager"}, + (Resource.CONTENT_MANAGER.value, Scope.VIEW.value): {"content-manager"}, + (Resource.CONTENT_MANAGER.value, Scope.MANAGE.value): {"content-manager"}, + } - async def __call__(self, request: Request, access_token: Annotated[str, Depends(oauth_2_scheme)]): + required_roles: set[str] = set() + for resource in self.resources: + required_roles.update( + {role.lower() for role in permission_map.get((resource, self.scope), set())} + ) + return required_roles + + def _extract_roles(self, payload: dict) -> set[str]: + realm_access = payload.get("realm_access") or {} + token_roles = realm_access.get("roles") or [] + return {str(role).strip().lower() for role in token_roles if str(role).strip()} + + async def __call__( + self, + request: Request, + access_token: Annotated[str, Depends(oauth_2_scheme)], + ): try: - decoded = jwt.decode( + unverified_payload = jwt.decode( access_token, options={"verify_signature": False, "verify_aud": False}, ) - print(decoded) except Exception as e: logging.error(f"Failed to decode token: {e}") raise HTTPException(status_code=401, detail="Invalid token") - realm_name = self._extract_realm_name(decoded) + realm_name = self._extract_realm_name(unverified_payload) + payload = self._verify_token(access_token, realm_name) - self._verify_token(access_token, realm_name) + token_roles = self._extract_roles(payload) + required_roles = self._get_required_roles() - authorized = False - last_permission = "" - for resource in self.resources: - permission = f"{resource}#{self.scope}" - last_permission = permission - if await self.check_keycloak_permission(access_token, permission, realm_name): - authorized = True - break - - if not authorized: + if required_roles.isdisjoint(token_roles): + permission_label = f"{' or '.join(self.resources)}#{self.scope}" raise HTTPException( status_code=403, - detail=f"Permission denied for '{' or '.join(self.resources)}#{self.scope}'" if len(self.resources) > 1 else f"Permission denied for '{last_permission}'", + detail=f"Permission denied for '{permission_label}'", ) return {"authorized": True} - - - async def check_keycloak_permission(self, access_token: str, permission: str, realm_name: str) -> bool: - """Verify user permissions""" - - if not KEYCLOAK_ISSUER_URL: - logging.error("KEYCLOAK_ISSUER_URL is not set") - return False - - url = f"{KEYCLOAK_ISSUER_URL}/realms/{realm_name}/protocol/openid-connect/token" - - headers = { - "Authorization": f"Bearer {access_token}", - "Content-Type": "application/x-www-form-urlencoded", - } - - data = { - "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", - "permission": permission, - "audience": RESOURCE_SERVER_ID, - } - - try: - async with httpx.AsyncClient() as client: - response = await client.post(url, headers=headers, data=data, timeout=10) - - if response.status_code == 200: - payload = response.json() - return "access_token" in payload - - if response.status_code in [400, 403]: - return False - - logging.error(f"Keycloak error {response.status_code}: {response.text}") - raise HTTPException(status_code=response.status_code, detail="Authorization server error") - - except httpx.RequestError as e: - logging.error(f"Authorization request error: {e}") - raise HTTPException(status_code=503, detail="Authorization server unavailable") - - - def _extract_realm_name(self, token_data: dict) -> str: - - iss = token_data.get("iss") - - if not iss or "/realms/" not in iss: - raise HTTPException(status_code=401, detail="Invalid token: missing issuer") - - return iss.split("/realms/")[-1] From a03750329e03f0c5a72cde4e1fbcf0565836e6f4 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 17 Mar 2026 20:07:35 +0000 Subject: [PATCH 59/87] man wtf --- api/src/core/security.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/api/src/core/security.py b/api/src/core/security.py index 86de9edf..b915909f 100644 --- a/api/src/core/security.py +++ b/api/src/core/security.py @@ -12,7 +12,6 @@ oauth_2_scheme = OAuth2PasswordBearer(tokenUrl="token") AUTH_SERVER_URL = os.getenv("KEYCLOAK_URL") -KEYCLOAK_ISSUER_URL = os.getenv("KEYCLOAK_ISSUER_URL", AUTH_SERVER_URL) RESOURCE_SERVER_ID = "api" _JWKS_CLIENTS: dict[str, PyJWKClient] = {} @@ -60,21 +59,9 @@ def _extract_realm_name(self, token_data: dict) -> str: return iss.split("/realms/")[-1].split("/")[0] - def _get_expected_issuer(self, realm_name: str) -> str: - if not KEYCLOAK_ISSUER_URL: - raise HTTPException( - status_code=500, - detail="Authentication server not configured", - ) - - if "/realms/" in KEYCLOAK_ISSUER_URL: - return KEYCLOAK_ISSUER_URL - - return f"{KEYCLOAK_ISSUER_URL}/realms/{realm_name}" - def _verify_token(self, access_token: str, realm_name: str) -> dict: jwks_client = self._get_jwks_client(realm_name) - issuer = self._get_expected_issuer(realm_name) + issuer = f"{AUTH_SERVER_URL}/realms/{realm_name}" try: signing_key = jwks_client.get_signing_key_from_jwt(access_token) @@ -149,4 +136,4 @@ async def __call__( detail=f"Permission denied for '{permission_label}'", ) - return {"authorized": True} + return {"authorized": True} \ No newline at end of file From fa354298408caf3fd4d2406cd65ad6ec0ebea04e Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 17 Mar 2026 20:13:27 +0000 Subject: [PATCH 60/87] die --- deployment/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index b4920a30..b4154470 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -183,6 +183,7 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} KEYCLOAK_URL: ${KEYCLOAK_INTERNAL_URL} + KEYCLOAK_ISSUER_URL: ${KEYCLOAK_URL} CLIENT_SECRET: ${CLIENT_SECRET} MONGODB_URI: mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/${MONGO_DB}?authSource=${MONGO_DB} MONGODB_DB: securelearning From 1bdb19178adf83899be604072827a2e30d6d933d Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Tue, 17 Mar 2026 20:30:54 +0000 Subject: [PATCH 61/87] bingus --- api/src/routers/realm/user_routes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/routers/realm/user_routes.py b/api/src/routers/realm/user_routes.py index 80dfb0d8..e9481e93 100644 --- a/api/src/routers/realm/user_routes.py +++ b/api/src/routers/realm/user_routes.py @@ -29,14 +29,14 @@ def create_user_in_realm( ) -@router.get("/realms/{realm}/users", dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.VIEW))]) +@router.get("/realms/{realm}/users", dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))]) def list_users_in_realm(session: SessionDep, realm: str, token: OAuth2Scheme): """List users inside the specified Keycloak realm/tenant.""" realm_service.validate_realm_access(token, realm) return realm_service.list_users_in_realm(session, realm) -@router.get("/realms/{realm}/users/{user_id}", dependencies=[Depends(Roles(Resource.ADMIN, Scope.VIEW))]) +@router.get("/realms/{realm}/users/{user_id}", dependencies=[Depends(Roles(Resource.ADMIN, Scope.MANAGE))]) def get_user_in_realm(realm: str, user_id: str, token: OAuth2Scheme): realm_service.validate_realm_access(token, realm) return realm_service.get_user_in_realm(realm, user_id) From 99a8c96a6549564b92048a8976f0e504f8cb05fd Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 18 Mar 2026 17:20:26 +0000 Subject: [PATCH 62/87] was it this? --- api/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/.env.example b/api/.env.example index 53e6c22f..f41c61e7 100644 --- a/api/.env.example +++ b/api/.env.example @@ -5,7 +5,7 @@ POSTGRES_PORT=5432 POSTGRES_DB=mydatabase CLIENT_SECRET="MbV2B0dwJfvdfvYyeYjcay7EuT5kid2C" KEYCLOAK_URL="http://localhost:8080" -KEYCLOAK_ISSUER_URL="https://mednat.ieeta.pt:9071/kc/realms/platform" +KEYCLOAK_ISSUER_URL="https://mednat.ieeta.pt:9071/kc" MONGODB_URI="mongodb://template_user:template_pass@mongo:27017/securelearning?authSource=securelearning" MONGODB_DB="securelearning" MONGODB_COLLECTION_TEMPLATES="templates" From dc13a8d2ba9eb0c8fc31013aa189e64396676442 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 18 Mar 2026 17:25:12 +0000 Subject: [PATCH 63/87] forgor --- api/src/core/security.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/src/core/security.py b/api/src/core/security.py index b915909f..9e6947da 100644 --- a/api/src/core/security.py +++ b/api/src/core/security.py @@ -12,6 +12,7 @@ oauth_2_scheme = OAuth2PasswordBearer(tokenUrl="token") AUTH_SERVER_URL = os.getenv("KEYCLOAK_URL") +KEYCLOAK_ISSUER_URL = os.getenv("KEYCLOAK_ISSUER_URL", AUTH_SERVER_URL) RESOURCE_SERVER_ID = "api" _JWKS_CLIENTS: dict[str, PyJWKClient] = {} @@ -61,7 +62,7 @@ def _extract_realm_name(self, token_data: dict) -> str: def _verify_token(self, access_token: str, realm_name: str) -> dict: jwks_client = self._get_jwks_client(realm_name) - issuer = f"{AUTH_SERVER_URL}/realms/{realm_name}" + issuer = f"{KEYCLOAK_ISSUER_URL}/realms/{realm_name}" try: signing_key = jwks_client.get_signing_key_from_jwt(access_token) @@ -136,4 +137,4 @@ async def __call__( detail=f"Permission denied for '{permission_label}'", ) - return {"authorized": True} \ No newline at end of file + return {"authorized": True} From 9b6a47ab2c48408350a3fee32d1043cc892da1a0 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 18 Mar 2026 17:28:56 +0000 Subject: [PATCH 64/87] a --- api/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/.env.example b/api/.env.example index f41c61e7..53e6c22f 100644 --- a/api/.env.example +++ b/api/.env.example @@ -5,7 +5,7 @@ POSTGRES_PORT=5432 POSTGRES_DB=mydatabase CLIENT_SECRET="MbV2B0dwJfvdfvYyeYjcay7EuT5kid2C" KEYCLOAK_URL="http://localhost:8080" -KEYCLOAK_ISSUER_URL="https://mednat.ieeta.pt:9071/kc" +KEYCLOAK_ISSUER_URL="https://mednat.ieeta.pt:9071/kc/realms/platform" MONGODB_URI="mongodb://template_user:template_pass@mongo:27017/securelearning?authSource=securelearning" MONGODB_DB="securelearning" MONGODB_COLLECTION_TEMPLATES="templates" From dd6697666cfc7aeb447db852793a3af1b7eb4124 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 18 Mar 2026 17:50:10 +0000 Subject: [PATCH 65/87] small changes --- .../admin/tenant-org-manager/BulkImportModal.tsx | 11 ++++++----- .../admin/tenant-org-manager/NewUserModal.tsx | 3 ++- web/src/routes/tenants-org-manager.tsx | 6 +++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx b/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx index fc55d2be..b3d83dfa 100644 --- a/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx +++ b/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx @@ -38,7 +38,7 @@ export function BulkImportModal({ const groupIdMap: Record = {}; const fetchGroupsIds = async () => { try { - const res = await fetch(`${API_BASE}/org-manager/${encodeURIComponent(realm)}/groups`, { + const res = await fetch(`${API_BASE}/realms/${encodeURIComponent(realm)}/groups`, { headers: { Authorization: keycloak.token ? `Bearer ${keycloak.token}` : "" }, }); if (!res.ok) return; @@ -54,7 +54,7 @@ export function BulkImportModal({ for (const name of groupNames) { if (groupIdMap[name]) continue; try { - const res = await fetch(`${API_BASE}/org-manager/${encodeURIComponent(realm)}/groups`, { + const res = await fetch(`${API_BASE}/realms/${encodeURIComponent(realm)}/groups`, { method: "POST", headers: { Authorization: keycloak.token ? `Bearer ${keycloak.token}` : "", @@ -76,13 +76,14 @@ export function BulkImportModal({ continue; } try { - const res = await fetch(`${API_BASE}/org-manager/${encodeURIComponent(realm)}/users`, { + const res = await fetch(`${API_BASE}/realms/users`, { method: "POST", headers: { Authorization: keycloak.token ? `Bearer ${keycloak.token}` : "", "Content-Type": "application/json", }, body: JSON.stringify({ + realm, username: u.username, name: u.name, email: u.email, @@ -103,7 +104,7 @@ export function BulkImportModal({ // Assign groups try { - const res = await fetch(`${API_BASE}/org-manager/${encodeURIComponent(realm)}/users`, { + const res = await fetch(`${API_BASE}/realms/${encodeURIComponent(realm)}/users`, { headers: { Authorization: keycloak.token ? `Bearer ${keycloak.token}` : "" }, }); if (res.ok) { @@ -119,7 +120,7 @@ export function BulkImportModal({ const gid = groupIdMap[gName]; if (!gid) continue; try { - await fetch(`${API_BASE}/org-manager/${encodeURIComponent(realm)}/groups/${encodeURIComponent(gid)}/members/${encodeURIComponent(uid)}`, { + await fetch(`${API_BASE}/realms/${encodeURIComponent(realm)}/groups/${encodeURIComponent(gid)}/members/${encodeURIComponent(uid)}`, { method: "POST", headers: { Authorization: keycloak.token ? `Bearer ${keycloak.token}` : "" }, }); diff --git a/web/src/components/admin/tenant-org-manager/NewUserModal.tsx b/web/src/components/admin/tenant-org-manager/NewUserModal.tsx index 854e8aeb..4d9ce410 100644 --- a/web/src/components/admin/tenant-org-manager/NewUserModal.tsx +++ b/web/src/components/admin/tenant-org-manager/NewUserModal.tsx @@ -74,7 +74,7 @@ export function NewUserModal({ realm, groups, onClose, onUserCreated }: NewUserM try { const res = await fetch( - `${API_BASE}/org-manager/${encodeURIComponent(realm)}/users`, + `${API_BASE}/realms/users`, { method: "POST", headers: { @@ -82,6 +82,7 @@ export function NewUserModal({ realm, groups, onClose, onUserCreated }: NewUserM "Content-Type": "application/json", }, body: JSON.stringify({ + realm, username: newUserUsername || newUserEmail.split("@")[0], name: newUserName, email: newUserEmail, diff --git a/web/src/routes/tenants-org-manager.tsx b/web/src/routes/tenants-org-manager.tsx index 1371e1b2..befa2088 100644 --- a/web/src/routes/tenants-org-manager.tsx +++ b/web/src/routes/tenants-org-manager.tsx @@ -44,7 +44,7 @@ function UsersManagement() { setIsLoading(true); try { const res = await fetch( - `${API_BASE}/org-manager/${encodeURIComponent(targetRealm)}/users`, + `${API_BASE}/realms/${encodeURIComponent(targetRealm)}/users`, { headers: { Authorization: keycloak.token ? `Bearer ${keycloak.token}` : "", @@ -65,7 +65,7 @@ function UsersManagement() { const fetchGroups = async (targetRealm: string) => { try { const res = await fetch( - `${API_BASE}/org-manager/${encodeURIComponent(targetRealm)}/groups`, + `${API_BASE}/realms/${encodeURIComponent(targetRealm)}/groups`, { headers: { Authorization: keycloak.token ? `Bearer ${keycloak.token}` : "", @@ -96,7 +96,7 @@ function UsersManagement() { setDeletingIds((prev) => ({ ...prev, [id]: true })); try { const res = await fetch( - `${API_BASE}/org-manager/${encodeURIComponent(realm)}/users/${encodeURIComponent(id)}`, + `${API_BASE}/realms/admin/${encodeURIComponent(realm)}/users/${encodeURIComponent(id)}`, { method: "DELETE", headers: { From 7266700d111cf595de3e3bc78bdc61da1e45d98e Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 18 Mar 2026 18:16:49 +0000 Subject: [PATCH 66/87] test fixes --- .github/workflows/CD-workflow.yml | 4 ++-- api/src/routers/org_manager/user_routes.py | 24 ++++++++++++++++++- .../tenant-org-manager/BulkImportModal.tsx | 3 +-- .../admin/tenant-org-manager/NewUserModal.tsx | 3 +-- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.github/workflows/CD-workflow.yml b/.github/workflows/CD-workflow.yml index 97cfc7cf..656f6216 100644 --- a/.github/workflows/CD-workflow.yml +++ b/.github/workflows/CD-workflow.yml @@ -36,5 +36,5 @@ jobs: env: DOCKER_BUILDKIT: 1 run: | - docker compose --env-file deployment/.env -f deployment/docker-compose.yml down --remove-orphans - docker compose --env-file deployment/.env -f deployment/docker-compose.yml up -d + docker compose --env-file deployment/.env -f deployment/docker-compose.yml down + docker compose --env-file deployment/.env -f deployment/docker-compose.yml up -d --build diff --git a/api/src/routers/org_manager/user_routes.py b/api/src/routers/org_manager/user_routes.py index dffd5b2a..720f3b04 100644 --- a/api/src/routers/org_manager/user_routes.py +++ b/api/src/routers/org_manager/user_routes.py @@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, File, UploadFile, status -from src.core.dependencies import SessionDep, OAuth2Scheme +from src.core.dependencies import CurrentRealm, SessionDep, OAuth2Scheme from src.models import OrgUserCreate from src.core.security import Roles, Resource, Scope from src.services.org_manager import get_org_manager_service @@ -26,6 +26,28 @@ def list_users(realm: str, token: OAuth2Scheme): return org_manager_service.list_users(realm, token) +@router.post( + "/users", dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))] +) +def create_user_in_own_realm( + user: OrgUserCreate, + session: SessionDep, + token: OAuth2Scheme, + current_realm: CurrentRealm, +): + """Create a user in the org manager's own realm.""" + return org_manager_service.create_user( + session, + realm=current_realm, + token=token, + username=user.username, + name=user.name, + email=user.email, + role=user.role, + group_id=user.group_id, + ) + + @router.post( "/{realm}/users", dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))] ) diff --git a/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx b/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx index b3d83dfa..5b95a363 100644 --- a/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx +++ b/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx @@ -76,14 +76,13 @@ export function BulkImportModal({ continue; } try { - const res = await fetch(`${API_BASE}/realms/users`, { + const res = await fetch(`${API_BASE}/org-manager/users`, { method: "POST", headers: { Authorization: keycloak.token ? `Bearer ${keycloak.token}` : "", "Content-Type": "application/json", }, body: JSON.stringify({ - realm, username: u.username, name: u.name, email: u.email, diff --git a/web/src/components/admin/tenant-org-manager/NewUserModal.tsx b/web/src/components/admin/tenant-org-manager/NewUserModal.tsx index 4d9ce410..1724666b 100644 --- a/web/src/components/admin/tenant-org-manager/NewUserModal.tsx +++ b/web/src/components/admin/tenant-org-manager/NewUserModal.tsx @@ -74,7 +74,7 @@ export function NewUserModal({ realm, groups, onClose, onUserCreated }: NewUserM try { const res = await fetch( - `${API_BASE}/realms/users`, + `${API_BASE}/org-manager/users`, { method: "POST", headers: { @@ -82,7 +82,6 @@ export function NewUserModal({ realm, groups, onClose, onUserCreated }: NewUserM "Content-Type": "application/json", }, body: JSON.stringify({ - realm, username: newUserUsername || newUserEmail.split("@")[0], name: newUserName, email: newUserEmail, From e3816453d79870f0927a9caba978e4d19c08049a Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 18 Mar 2026 18:32:40 +0000 Subject: [PATCH 67/87] reverted --- api/src/routers/org_manager/user_routes.py | 24 +------------------ .../tenant-org-manager/BulkImportModal.tsx | 3 ++- .../admin/tenant-org-manager/NewUserModal.tsx | 3 ++- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/api/src/routers/org_manager/user_routes.py b/api/src/routers/org_manager/user_routes.py index 720f3b04..dffd5b2a 100644 --- a/api/src/routers/org_manager/user_routes.py +++ b/api/src/routers/org_manager/user_routes.py @@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, File, UploadFile, status -from src.core.dependencies import CurrentRealm, SessionDep, OAuth2Scheme +from src.core.dependencies import SessionDep, OAuth2Scheme from src.models import OrgUserCreate from src.core.security import Roles, Resource, Scope from src.services.org_manager import get_org_manager_service @@ -26,28 +26,6 @@ def list_users(realm: str, token: OAuth2Scheme): return org_manager_service.list_users(realm, token) -@router.post( - "/users", dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))] -) -def create_user_in_own_realm( - user: OrgUserCreate, - session: SessionDep, - token: OAuth2Scheme, - current_realm: CurrentRealm, -): - """Create a user in the org manager's own realm.""" - return org_manager_service.create_user( - session, - realm=current_realm, - token=token, - username=user.username, - name=user.name, - email=user.email, - role=user.role, - group_id=user.group_id, - ) - - @router.post( "/{realm}/users", dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))] ) diff --git a/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx b/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx index 5b95a363..38bd3448 100644 --- a/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx +++ b/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx @@ -76,13 +76,14 @@ export function BulkImportModal({ continue; } try { - const res = await fetch(`${API_BASE}/org-manager/users`, { + const res = await fetch(`${API_BASE}/org-manager/${encodeURIComponent(realm)}/users`, { method: "POST", headers: { Authorization: keycloak.token ? `Bearer ${keycloak.token}` : "", "Content-Type": "application/json", }, body: JSON.stringify({ + realm, username: u.username, name: u.name, email: u.email, diff --git a/web/src/components/admin/tenant-org-manager/NewUserModal.tsx b/web/src/components/admin/tenant-org-manager/NewUserModal.tsx index 1724666b..62bf01d0 100644 --- a/web/src/components/admin/tenant-org-manager/NewUserModal.tsx +++ b/web/src/components/admin/tenant-org-manager/NewUserModal.tsx @@ -74,7 +74,7 @@ export function NewUserModal({ realm, groups, onClose, onUserCreated }: NewUserM try { const res = await fetch( - `${API_BASE}/org-manager/users`, + `${API_BASE}/org-manager/${encodeURIComponent(realm)}/users`, { method: "POST", headers: { @@ -82,6 +82,7 @@ export function NewUserModal({ realm, groups, onClose, onUserCreated }: NewUserM "Content-Type": "application/json", }, body: JSON.stringify({ + realm, username: newUserUsername || newUserEmail.split("@")[0], name: newUserName, email: newUserEmail, From 46bc95f20672c0f2b1a9fbf42861747bd00ff6ac Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 18 Mar 2026 18:51:36 +0000 Subject: [PATCH 68/87] test fix --- api/src/routers/realm/user_routes.py | 26 ++++++++++++++++++- .../tenant-org-manager/BulkImportModal.tsx | 3 +-- .../admin/tenant-org-manager/NewUserModal.tsx | 3 +-- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/api/src/routers/realm/user_routes.py b/api/src/routers/realm/user_routes.py index e9481e93..057950dc 100644 --- a/api/src/routers/realm/user_routes.py +++ b/api/src/routers/realm/user_routes.py @@ -4,7 +4,7 @@ from src.core.security import Roles, Resource, Scope from src.core.dependencies import SessionDep, OAuth2Scheme -from src.models import RealmUserCreate +from src.models import OrgUserCreate, RealmUserCreate from src.services.platform_admin import get_platform_admin_service realm_service = get_platform_admin_service() @@ -29,6 +29,30 @@ def create_user_in_realm( ) +@router.post( + "/realms/{realm}/users", + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))], +) +def create_user_in_own_realm( + session: SessionDep, + realm: str, + user: OrgUserCreate, + token: OAuth2Scheme, +): + """Create a new user inside the caller's Keycloak realm/tenant.""" + realm_service.validate_realm_access(token, realm) + return realm_service.create_user_in_realm( + session, + realm=realm, + username=user.username, + name=user.name, + email=user.email, + role=user.role, + group_id=user.group_id, + ) + + @router.get("/realms/{realm}/users", dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))]) def list_users_in_realm(session: SessionDep, realm: str, token: OAuth2Scheme): """List users inside the specified Keycloak realm/tenant.""" diff --git a/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx b/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx index 38bd3448..844b9595 100644 --- a/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx +++ b/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx @@ -76,14 +76,13 @@ export function BulkImportModal({ continue; } try { - const res = await fetch(`${API_BASE}/org-manager/${encodeURIComponent(realm)}/users`, { + const res = await fetch(`${API_BASE}/realms/${encodeURIComponent(realm)}/users`, { method: "POST", headers: { Authorization: keycloak.token ? `Bearer ${keycloak.token}` : "", "Content-Type": "application/json", }, body: JSON.stringify({ - realm, username: u.username, name: u.name, email: u.email, diff --git a/web/src/components/admin/tenant-org-manager/NewUserModal.tsx b/web/src/components/admin/tenant-org-manager/NewUserModal.tsx index 62bf01d0..e79703bd 100644 --- a/web/src/components/admin/tenant-org-manager/NewUserModal.tsx +++ b/web/src/components/admin/tenant-org-manager/NewUserModal.tsx @@ -74,7 +74,7 @@ export function NewUserModal({ realm, groups, onClose, onUserCreated }: NewUserM try { const res = await fetch( - `${API_BASE}/org-manager/${encodeURIComponent(realm)}/users`, + `${API_BASE}/realms/${encodeURIComponent(realm)}/users`, { method: "POST", headers: { @@ -82,7 +82,6 @@ export function NewUserModal({ realm, groups, onClose, onUserCreated }: NewUserM "Content-Type": "application/json", }, body: JSON.stringify({ - realm, username: newUserUsername || newUserEmail.split("@")[0], name: newUserName, email: newUserEmail, From 6eaa0d2c1ca01722615ddd6b04fef4641a1af904 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Wed, 18 Mar 2026 19:02:37 +0000 Subject: [PATCH 69/87] small error fix --- api/src/services/platform_admin/user_handler.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/api/src/services/platform_admin/user_handler.py b/api/src/services/platform_admin/user_handler.py index f53123b8..142adf40 100644 --- a/api/src/services/platform_admin/user_handler.py +++ b/api/src/services/platform_admin/user_handler.py @@ -38,11 +38,6 @@ def create_user_in_realm( if location: user_id = location.rstrip("/").split("/")[-1] - user = User(keycloak_id=user_id, email=email, is_org_manager=(role or "").strip().upper() == "ORG_MANAGER") - session.add(user) - session.commit() - session.refresh(user) - if group_id and user_id: try: self.admin.add_user_to_group(realm, user_id, group_id) From becd4458e4fe4b91b1ed0fcc218fd552c503be8d Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 19 Mar 2026 10:53:46 +0000 Subject: [PATCH 70/87] .env changes --- api/.env.dev.example | 22 ++++++++++ api/.env.prod.example | 22 ++++++++++ deployment/.env.dev.example | 56 +++++++++++++++++++++++++ deployment/{.env => .env.prod.example} | 39 ++--------------- deployment/docker-compose.dev.yml | 1 + smtp/{.env.example => .env.dev.example} | 0 smtp/.env.prod.example | 9 ++++ 7 files changed, 114 insertions(+), 35 deletions(-) create mode 100644 api/.env.dev.example create mode 100644 api/.env.prod.example create mode 100644 deployment/.env.dev.example rename deployment/{.env => .env.prod.example} (58%) rename smtp/{.env.example => .env.dev.example} (100%) create mode 100644 smtp/.env.prod.example diff --git a/api/.env.dev.example b/api/.env.dev.example new file mode 100644 index 00000000..a355b486 --- /dev/null +++ b/api/.env.dev.example @@ -0,0 +1,22 @@ +POSTGRES_USER=myuser +POSTGRES_PASSWORD=mypassword +POSTGRES_SERVER=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=mydatabase +CLIENT_SECRET="MbV2B0dwJfvdfvYyeYjcay7EuT5kid2C" +KEYCLOAK_URL="http://localhost:8080" +KEYCLOAK_ISSUER_URL="http://localhost:8080" +MONGODB_URI="mongodb://template_user:template_pass@localhost:27017/securelearning?authSource=securelearning" +MONGODB_DB="securelearning" +MONGODB_COLLECTION_TEMPLATES="templates" +FILE_STORAGE_BACKEND="garage" +GARAGE_S3_ENDPOINT="http://localhost:3900" +GARAGE_S3_PUBLIC_ENDPOINT="http://localhost:3900" +GARAGE_S3_REGION="garage" +GARAGE_ACCESS_KEY_ID="garage-access-key" +GARAGE_SECRET_ACCESS_KEY="garage-secret-key" +GARAGE_FORCE_PATH_STYLE="true" +GARAGE_BUCKET_CONTENT="securelearning-content" +GARAGE_BUCKET_LOGOS="securelearning-logos" +GARAGE_CONTENT_PREFIX="content" +GARAGE_LOGOS_PREFIX="logos" diff --git a/api/.env.prod.example b/api/.env.prod.example new file mode 100644 index 00000000..fd05dc79 --- /dev/null +++ b/api/.env.prod.example @@ -0,0 +1,22 @@ +POSTGRES_USER=myuser +POSTGRES_PASSWORD=mypassword +POSTGRES_SERVER=db +POSTGRES_PORT=5432 +POSTGRES_DB=mydatabase +CLIENT_SECRET="MbV2B0dwJfvdfvYyeYjcay7EuT5kid2C" +KEYCLOAK_URL="http://keycloak:8080/kc" +KEYCLOAK_ISSUER_URL="https://mednat.ieeta.pt:9071/kc" +MONGODB_URI="mongodb://template_user:template_pass@mongo:27017/securelearning?authSource=securelearning" +MONGODB_DB="securelearning" +MONGODB_COLLECTION_TEMPLATES="templates" +FILE_STORAGE_BACKEND="garage" +GARAGE_S3_ENDPOINT="http://localhost:3900" +GARAGE_S3_PUBLIC_ENDPOINT="http://localhost:3900" +GARAGE_S3_REGION="garage" +GARAGE_ACCESS_KEY_ID="garage-access-key" +GARAGE_SECRET_ACCESS_KEY="garage-secret-key" +GARAGE_FORCE_PATH_STYLE="true" +GARAGE_BUCKET_CONTENT="securelearning-content" +GARAGE_BUCKET_LOGOS="securelearning-logos" +GARAGE_CONTENT_PREFIX="content" +GARAGE_LOGOS_PREFIX="logos" diff --git a/deployment/.env.dev.example b/deployment/.env.dev.example new file mode 100644 index 00000000..8200f6f0 --- /dev/null +++ b/deployment/.env.dev.example @@ -0,0 +1,56 @@ +# Copy this file to deployment/.env.dev and adjust the values. + +# Database Configuration +POSTGRES_USER=myuser +POSTGRES_PASSWORD=mypassword +POSTGRES_DB=mydatabase + +# MongoDB Configuration +MONGO_ROOT_USER=root +MONGO_ROOT_PASSWORD=rootpassword +MONGO_DB=securelearning +MONGO_USER=template_user +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_REGION=garage +GARAGE_ACCESS_KEY_ID=garage-access-key +GARAGE_SECRET_ACCESS_KEY=garage-secret-key +GARAGE_FORCE_PATH_STYLE=true +GARAGE_BUCKET_CONTENT=securelearning-content +GARAGE_BUCKET_LOGOS=securelearning-logos +GARAGE_CONTENT_PREFIX=content +GARAGE_LOGOS_PREFIX=logos +GARAGE_RPC_SECRET=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + +# RabbitMQ Configuration +RABBITMQ_DEFAULT_USER=admin +RABBITMQ_DEFAULT_PASS=admin +RABBITMQ_QUEUE=email_queue +RABBITMQ_API_USER=api_publisher +RABBITMQ_API_PASS=api_publisher_pass +RABBITMQ_SMTP_USER=smtp_consumer +RABBITMQ_SMTP_PASS=smtp_consumer_pass + +# SMTP Configuration +RATE_LIMITER_MAX_REQUESTS=10 +RATE_LIMITER_TIME_WINDOW_SECONDS=6 + +# Keycloak Configuration +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=admin +CLIENT_SECRET=your_very_secure_key_here +KEYCLOAK_INTERNAL_URL=http://keycloak:8080 +API_INTERNAL_URL=http://api:8000 + +# Development URLs +KC_HTTP_RELATIVE_PATH= +VITE_BASE_PATH=/ +KC_HOSTNAME=localhost +KC_HOSTNAME_URL=http://localhost:8080 +KEYCLOAK_URL=http://localhost:8080 +API_URL=http://localhost:8000 +WEB_URL=http://localhost:5173 diff --git a/deployment/.env b/deployment/.env.prod.example similarity index 58% rename from deployment/.env rename to deployment/.env.prod.example index e590c4ba..45837ba4 100644 --- a/deployment/.env +++ b/deployment/.env.prod.example @@ -1,3 +1,5 @@ +# Copy this file to deployment/.env.prod and adjust the values. + # Database Configuration POSTGRES_USER=myuser POSTGRES_PASSWORD=mypassword @@ -28,12 +30,8 @@ GARAGE_RPC_SECRET=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd RABBITMQ_DEFAULT_USER=admin RABBITMQ_DEFAULT_PASS=admin RABBITMQ_QUEUE=email_queue - -# RabbitMQ API User (Publisher) RABBITMQ_API_USER=api_publisher RABBITMQ_API_PASS=api_publisher_pass - -# RabbitMQ SMTP User (Consumer) RABBITMQ_SMTP_USER=smtp_consumer RABBITMQ_SMTP_PASS=smtp_consumer_pass @@ -45,34 +43,10 @@ RATE_LIMITER_TIME_WINDOW_SECONDS=6 KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=admin CLIENT_SECRET=your_very_secure_key_here - - -# Internal URLs (Docker network communication) KEYCLOAK_INTERNAL_URL=http://keycloak:8080/kc API_INTERNAL_URL=http://api:80 -# ============================================================ -# ENVIRONMENT-SPECIFIC SETTINGS -# ============================================================ -# Choose one of the configurations below based on your environment: - -# --- DEVELOPMENT (docker-compose.dev.yml) --- -# Use these settings when running with docker-compose.dev.yml -# KEYCLOAK_HOSTNAME=localhost -# KEYCLOAK_URL=http://localhost:8080 -# API_URL=http://localhost:8000 -# WEB_URL=http://localhost:5173 - -# --- PRODUCTION (docker-compose.yml) --- -# Use these settings when running with docker-compose.yml -# KEYCLOAK_HOSTNAME=kc.securelearning.pt -# KEYCLOAK_URL=https://kc.securelearning.pt -# API_URL=https://api.securelearning.pt -# WEB_URL=https://app.securelearning.pt - - -# --- PRODUCTION (docker-compose.yml) --- -# Use these settings when running with docker-compose.yml +# Production URLs KC_HTTP_RELATIVE_PATH=/kc VITE_BASE_PATH=/app/ KC_HOSTNAME=mednat.ieeta.pt @@ -80,13 +54,8 @@ KC_HOSTNAME_URL=https://mednat.ieeta.pt:9071/kc KEYCLOAK_URL=https://mednat.ieeta.pt:9071/kc API_URL=https://mednat.ieeta.pt:9071/api WEB_URL=https://mednat.ieeta.pt:9071/app -# # Use these settings when running with docker-compose.yml -# KEYCLOAK_HOSTNAME=kc.localhost -# KEYCLOAK_URL=http://kc.localhost -# API_URL=http://api.localhost -# WEB_URL=http://app.localhost -# Port Configuration (Production only - nginx exposes this port) +# Port Configuration NGINX_PORT=8081 SERVER_PORT=9071 diff --git a/deployment/docker-compose.dev.yml b/deployment/docker-compose.dev.yml index 86afb4b0..3757f231 100644 --- a/deployment/docker-compose.dev.yml +++ b/deployment/docker-compose.dev.yml @@ -172,6 +172,7 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} KEYCLOAK_URL: ${KEYCLOAK_INTERNAL_URL} + KEYCLOAK_ISSUER_URL: ${KC_HOSTNAME_URL} CLIENT_SECRET: ${CLIENT_SECRET} MONGODB_URI: mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/${MONGO_DB}?authSource=${MONGO_DB} MONGODB_DB: securelearning diff --git a/smtp/.env.example b/smtp/.env.dev.example similarity index 100% rename from smtp/.env.example rename to smtp/.env.dev.example diff --git a/smtp/.env.prod.example b/smtp/.env.prod.example new file mode 100644 index 00000000..e95174c3 --- /dev/null +++ b/smtp/.env.prod.example @@ -0,0 +1,9 @@ +RABBITMQ_HOST=rabbitmq +RABBITMQ_USER=smtp_consumer +RABBITMQ_PASS=smtp_consumer_pass +RABBITMQ_QUEUE=email_queue + +RATE_LIMITER_MAX_REQUESTS=10 +RATE_LIMITER_TIME_WINDOW_SECONDS=6 + +API_URL=http://localhost:8000 From e6dd1f16fa6b559020bd458aa9dbcdcc54da27ad Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Thu, 19 Mar 2026 10:56:15 +0000 Subject: [PATCH 71/87] forgor --- api/.env.example | 22 ---------- deployment/.env.example | 95 ----------------------------------------- 2 files changed, 117 deletions(-) delete mode 100644 api/.env.example delete mode 100644 deployment/.env.example diff --git a/api/.env.example b/api/.env.example deleted file mode 100644 index 53e6c22f..00000000 --- a/api/.env.example +++ /dev/null @@ -1,22 +0,0 @@ -POSTGRES_USER=myuser -POSTGRES_PASSWORD=mypassword -POSTGRES_SERVER=localhost -POSTGRES_PORT=5432 -POSTGRES_DB=mydatabase -CLIENT_SECRET="MbV2B0dwJfvdfvYyeYjcay7EuT5kid2C" -KEYCLOAK_URL="http://localhost:8080" -KEYCLOAK_ISSUER_URL="https://mednat.ieeta.pt:9071/kc/realms/platform" -MONGODB_URI="mongodb://template_user:template_pass@mongo:27017/securelearning?authSource=securelearning" -MONGODB_DB="securelearning" -MONGODB_COLLECTION_TEMPLATES="templates" -FILE_STORAGE_BACKEND="garage" -GARAGE_S3_ENDPOINT="http://localhost:3900" -GARAGE_S3_PUBLIC_ENDPOINT="http://localhost:3900" -GARAGE_S3_REGION="garage" -GARAGE_ACCESS_KEY_ID="garage-access-key" -GARAGE_SECRET_ACCESS_KEY="garage-secret-key" -GARAGE_FORCE_PATH_STYLE="true" -GARAGE_BUCKET_CONTENT="securelearning-content" -GARAGE_BUCKET_LOGOS="securelearning-logos" -GARAGE_CONTENT_PREFIX="content" -GARAGE_LOGOS_PREFIX="logos" diff --git a/deployment/.env.example b/deployment/.env.example deleted file mode 100644 index c5eddf38..00000000 --- a/deployment/.env.example +++ /dev/null @@ -1,95 +0,0 @@ -# Database Configuration -POSTGRES_USER=myuser -POSTGRES_PASSWORD=mypassword -POSTGRES_DB=mydatabase - -# MongoDB Configuration -MONGO_ROOT_USER=root -MONGO_ROOT_PASSWORD=rootpassword -MONGO_DB=securelearning -MONGO_USER=template_user -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_REGION=garage -GARAGE_ACCESS_KEY_ID=garage-access-key -GARAGE_SECRET_ACCESS_KEY=garage-secret-key -GARAGE_FORCE_PATH_STYLE=true -GARAGE_BUCKET_CONTENT=securelearning-content -GARAGE_BUCKET_LOGOS=securelearning-logos -GARAGE_CONTENT_PREFIX=content -GARAGE_LOGOS_PREFIX=logos -GARAGE_RPC_SECRET=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef - -# RabbitMQ Configuration -RABBITMQ_DEFAULT_USER=admin -RABBITMQ_DEFAULT_PASS=admin -RABBITMQ_QUEUE=email_queue - -# RabbitMQ API User (Publisher) -RABBITMQ_API_USER=api_publisher -RABBITMQ_API_PASS=api_publisher_pass - -# RabbitMQ SMTP User (Consumer) -RABBITMQ_SMTP_USER=smtp_consumer -RABBITMQ_SMTP_PASS=smtp_consumer_pass - -# SMTP Configuration -RATE_LIMITER_MAX_REQUESTS=10 -RATE_LIMITER_TIME_WINDOW_SECONDS=6 - -# Keycloak Configuration -KEYCLOAK_ADMIN=admin -KEYCLOAK_ADMIN_PASSWORD=admin -CLIENT_SECRET=your_very_secure_key_here - - -# Internal URLs (Docker network communication) -KEYCLOAK_INTERNAL_URL=http://keycloak:8080/kc -API_INTERNAL_URL=http://api:80 - -# ============================================================ -# ENVIRONMENT-SPECIFIC SETTINGS -# ============================================================ -# Choose one of the configurations below based on your environment: - -# --- DEVELOPMENT (docker-compose.dev.yml) --- -# Use these settings when running with docker-compose.dev.yml -#KEYCLOAK_HOSTNAME=localhost -#KEYCLOAK_URL=http://localhost:8080 -#API_URL=http://localhost:8000 -#WEB_URL=http://localhost:5173 - -# --- PRODUCTION (docker-compose.yml) --- -# Use these settings when running with docker-compose.yml -# KEYCLOAK_HOSTNAME=kc.securelearning.pt -# KEYCLOAK_URL=https://kc.securelearning.pt -# API_URL=https://api.securelearning.pt -# WEB_URL=https://app.securelearning.pt - - -# --- PRODUCTION (docker-compose.yml) --- -# Use these settings when running with docker-compose.yml -KC_HTTP_RELATIVE_PATH=/kc -VITE_BASE_PATH=/app/ -KC_HOSTNAME=mednat.ieeta.pt -KC_HOSTNAME_URL=https://mednat.ieeta.pt:9071/kc -KEYCLOAK_URL=https://mednat.ieeta.pt:9071/kc -API_URL=https://mednat.ieeta.pt:9071/api -WEB_URL=https://mednat.ieeta.pt:9071/app -# # Use these settings when running with docker-compose.yml -# KEYCLOAK_HOSTNAME=kc.localhost -# KEYCLOAK_URL=http://kc.localhost -# API_URL=http://api.localhost -# WEB_URL=http://app.localhost - -# Port Configuration (Production only - nginx exposes this port) -NGINX_PORT=8081 -SERVER_PORT=9071 - -# TLS certificate file names inside deployment/certs -TLS_CERT_FILE=tls.crt -TLS_KEY_FILE=tls.key From 83b84b7d81edc2ad487876f3a84da14f6bf2f609 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Fri, 20 Mar 2026 21:06:20 +0000 Subject: [PATCH 72/87] small mistake --- api/.env.prod.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/.env.prod.example b/api/.env.prod.example index fd05dc79..5b69aadd 100644 --- a/api/.env.prod.example +++ b/api/.env.prod.example @@ -1,6 +1,6 @@ POSTGRES_USER=myuser POSTGRES_PASSWORD=mypassword -POSTGRES_SERVER=db +POSTGRES_SERVER=localhost POSTGRES_PORT=5432 POSTGRES_DB=mydatabase CLIENT_SECRET="MbV2B0dwJfvdfvYyeYjcay7EuT5kid2C" From 3e3c1b542cdefce70f0716df18dd15f9320183dd Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Fri, 20 Mar 2026 21:07:03 +0000 Subject: [PATCH 73/87] forgot workflows --- scripts/copy-env-examples.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/copy-env-examples.sh b/scripts/copy-env-examples.sh index b7ddece9..3fbcd8e9 100644 --- a/scripts/copy-env-examples.sh +++ b/scripts/copy-env-examples.sh @@ -9,11 +9,11 @@ while IFS= read -r -d '' example_file; do cp -f "${example_file}" "${target_file}" echo "Copied ${example_file} -> ${target_file}" copied=$((copied + 1)) -done < <(find "${ROOT_DIR}" -type f -name ".env.example" -print0) +done < <(find "${ROOT_DIR}" -type f -name ".env.example.prod" -print0) if [[ "${copied}" -eq 0 ]]; then echo "No .env.example files found." exit 1 fi -echo "Created ${copied} .env file(s) from .env.example." +echo "Created ${copied} .env file(s) from .env.example.prod." From bf5262dba34e3e7b85f3929b491ef1b819c502a5 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Fri, 20 Mar 2026 21:29:19 +0000 Subject: [PATCH 74/87] updated deployment readme --- deployment/README.md | 163 ++++++++++++++++++++----------------------- 1 file changed, 74 insertions(+), 89 deletions(-) diff --git a/deployment/README.md b/deployment/README.md index d5200906..779619cc 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -6,10 +6,12 @@ This directory contains Docker Compose configurations and deployment files for r ``` deployment/ -├── docker-compose.yml # Production configuration -├── docker-compose.dev.yml # Development configuration (DB only) -├── nginx.conf # Nginx reverse proxy configuration -└── README.md # This file +├── docker-compose.yml # Production configuration +├── docker-compose.dev.yml # Development configuration +├── .env.prod.example # Production env template +├── .env.dev.example # Development env template +├── nginx.mednat.conf # Nginx reverse proxy configuration +└── README.md # This file ``` ## 🐳 Docker Architecture @@ -17,30 +19,46 @@ deployment/ The application uses a **multi-container architecture** with Docker Compose: ``` -┌─────────────────────────────────────────┐ -│ Nginx (Port 80) │ -│ - Serves frontend static files │ -│ - Proxies API requests to backend │ -└─────────────┬───────────────────────────┘ - │ - ┌───────┴────────┐ - │ │ -┌─────▼──────┐ ┌──────▼──────┐ -│ Web │ │ API │ -│ (React) │ │ (FastAPI) │ -└────────────┘ └──────┬──────┘ - │ - ┌──────▼──────┐ - │ PostgreSQL │ - │ (Port 5432)│ - └─────────────┘ + ┌──────────────────────────────┐ + │ Nginx / Edge Proxy │ + │ - Serves frontend │ + │ - Proxies /api and /kc │ + └───────┬───────────────┬──────┘ + │ │ + ┌──────▼──────┐ ┌──────▼──────┐ + │ Frontend │ │ Keycloak │ + │ (React) │ │ (IAM) │ + └──────┬──────┘ └─────────────┘ + │ + ┌───────────▼───────────┐ ┌───────────────┐ + │ API │◄─►│ Garage │ + │ (FastAPI) │ │ object store │ + └───────┬─────────┬─────┘ └───────────────┘ + │ │ (Frontend consumes asset URLs) + ┌────────────▼┐ ┌────▼──────────┐ + │ PostgreSQL │ │ MongoDB │ + │ relational │ │ templates etc │ + └─────────────┘ └───────────────┘ + │ + ┌──────────▼──────────┐ + │ RabbitMQ + SMTP │ + │ async email sending │ + └─────────────────────┘ + + ``` ### Services -1. **db** (PostgreSQL): Database service -2. **api** (FastAPI): Backend API service -3. **web** (Nginx): Frontend + Reverse Proxy +1. **nginx**: Edge reverse proxy for the production stack. Terminates HTTP/TLS, serves the built frontend, and routes `/api` to FastAPI and `/kc` to Keycloak. +2. **frontend**: React application. In production it is built into a static site served behind nginx; in development it runs with the dev server. +3. **api**: FastAPI backend. Handles application logic, local JWT validation, tenant management, campaign logic, and integrations with storage, queues, and Keycloak. +4. **keycloak**: Identity and access management service. Issues JWTs, manages realms, clients, users, and authorization-related data. +5. **db**: PostgreSQL database for relational application data and the Keycloak database. +6. **mongo**: MongoDB storage for templates and other document-style content. +7. **garage**: S3-compatible object storage used for content and tenant logos. +8. **rabbitmq**: Message broker used for asynchronous email workflows. +9. **smtp**: Email worker/consumer that processes RabbitMQ jobs and sends emails. ## 🎯 Deployment Configurations @@ -61,13 +79,13 @@ The application uses a **multi-container architecture** with Docker Compose: **Use this for:** - Local development -- Running only the database -- Testing with local frontend/backend +- Running the full stack locally +- Fast iteration with dev Dockerfiles and mounted source volumes **What it does:** -- Runs PostgreSQL only -- Exposes database port (5432) -- Allows local API/Web to connect +- Runs the full development stack +- Exposes service ports directly to the host +- Uses development-specific env values ## 🚀 Quick Start @@ -78,82 +96,55 @@ The application uses a **multi-container architecture** with Docker Compose: cd deployment # Start all services -docker compose up -d +docker compose --env-file .env.prod -f docker-compose.yml up -d --build # View logs -docker compose logs -f +docker compose --env-file .env.prod -f docker-compose.yml logs -f # Stop all services -docker compose down +docker compose --env-file .env.prod -f docker-compose.yml down ``` Access the application: -- **Frontend**: http://localhost -- **API Docs**: http://localhost/api/docs -- **Health Check**: http://localhost/health +- **Frontend**: `https://mednat.ieeta.pt:9071/app` +- **API Docs**: `https://mednat.ieeta.pt:9071/api/docs` +- **Keycloak**: `https://mednat.ieeta.pt:9071/kc` -### Development with Database Only +### Development Deployment ```bash -# Start PostgreSQL only +# Start the development stack cd deployment -docker compose -f docker-compose.dev.yml up -d - -# Database is now accessible at localhost:5432 +docker compose --env-file .env.dev -f docker-compose.dev.yml up -d --build ``` -Then run API and Web locally: -```bash -# Terminal 1 - API -cd ../api -uv run fastapi dev src/main.py - -# Terminal 2 - Web -cd ../web -npm run dev -``` +Access the development services: +- **Frontend**: `http://localhost:5173` +- **API Docs**: `http://localhost:8000/docs` +- **Keycloak**: `http://localhost:8080` +- **PostgreSQL**: `localhost:5432` ## ⚙️ Environment Variables -### Default Values +### Deployment Env Files -Both compose files use these defaults: +Copy the desired example env into a .env: -```env -POSTGRES_USER=myuser -POSTGRES_PASSWORD=mypassword -POSTGRES_DB=mydatabase -POSTGRES_SERVER=db -POSTGRES_PORT=5432 +```bash +deployment/.env ``` -### Override with .env File - -Create a `.env` file in the `deployment/` directory: +Templates: -```env -# Database Configuration -POSTGRES_USER=customuser -POSTGRES_PASSWORD=strongpassword -POSTGRES_DB=myapp -POSTGRES_SERVER=db -POSTGRES_PORT=5432 +```bash +deployment/.env.dev.example +deployment/.env.prod.example ``` -Docker Compose automatically reads this file. - ### Production Best Practices **Never use default passwords in production!** -```bash -# Generate a strong password -openssl rand -base64 32 - -# Set in .env file -POSTGRES_PASSWORD= -``` - ## 🔧 Nginx Configuration The `nginx.conf` file configures: @@ -464,18 +455,12 @@ web: ### Environment-Specific Configuration -Create multiple environment files: - -```bash -deployment/ -├── .env.development -├── .env.staging -├── .env.production -``` +Use the matching env file for each stack: -Use with: ```bash -docker compose --env-file .env.production up -d +cd deployment +docker compose --env-file .env.dev -f docker-compose.dev.yml up -d --build +docker compose --env-file .env.prod -f docker-compose.yml up -d --build ``` ## 📊 Monitoring & Maintenance @@ -592,4 +577,4 @@ api: reservations: cpus: '0.25' memory: 256M -``` \ No newline at end of file +``` From 22ca0b3f2406a57ece7dec6fb34434cb6cf68a7e Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Fri, 20 Mar 2026 21:30:13 +0000 Subject: [PATCH 75/87] forgor --- deployment/README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/deployment/README.md b/deployment/README.md index 779619cc..99bc9d9b 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -96,13 +96,13 @@ The application uses a **multi-container architecture** with Docker Compose: cd deployment # Start all services -docker compose --env-file .env.prod -f docker-compose.yml up -d --build +docker compose -f docker-compose.yml up -d --build # View logs -docker compose --env-file .env.prod -f docker-compose.yml logs -f +docker compose -f docker-compose.yml logs -f # Stop all services -docker compose --env-file .env.prod -f docker-compose.yml down +docker compose -f docker-compose.yml down ``` Access the application: @@ -115,7 +115,7 @@ Access the application: ```bash # Start the development stack cd deployment -docker compose --env-file .env.dev -f docker-compose.dev.yml up -d --build +docker compose -f docker-compose.dev.yml up -d --build ``` Access the development services: @@ -153,7 +153,6 @@ The `nginx.conf` file configures: Routes API requests to the backend: ``` /api/* → backend (FastAPI) -/openapi.json → backend /health → backend ``` From 03c8f7700a54f9d6a64e7415b665d5f6e1882290 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Fri, 20 Mar 2026 21:34:29 +0000 Subject: [PATCH 76/87] ? --- api/.env.prod.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/.env.prod.example b/api/.env.prod.example index fd05dc79..5b69aadd 100644 --- a/api/.env.prod.example +++ b/api/.env.prod.example @@ -1,6 +1,6 @@ POSTGRES_USER=myuser POSTGRES_PASSWORD=mypassword -POSTGRES_SERVER=db +POSTGRES_SERVER=localhost POSTGRES_PORT=5432 POSTGRES_DB=mydatabase CLIENT_SECRET="MbV2B0dwJfvdfvYyeYjcay7EuT5kid2C" From 32d9cad35a04fd44da77ba6ac133333656b2abe4 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Mon, 23 Mar 2026 15:30:50 +0000 Subject: [PATCH 77/87] oopsie --- scripts/copy-env-examples.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/copy-env-examples.sh b/scripts/copy-env-examples.sh index 3fbcd8e9..3c7710cd 100644 --- a/scripts/copy-env-examples.sh +++ b/scripts/copy-env-examples.sh @@ -9,11 +9,11 @@ while IFS= read -r -d '' example_file; do cp -f "${example_file}" "${target_file}" echo "Copied ${example_file} -> ${target_file}" copied=$((copied + 1)) -done < <(find "${ROOT_DIR}" -type f -name ".env.example.prod" -print0) +done < <(find "${ROOT_DIR}" -type f -name ".env.prod.example" -print0) if [[ "${copied}" -eq 0 ]]; then - echo "No .env.example files found." + echo "No .env.prod.example files found." exit 1 fi -echo "Created ${copied} .env file(s) from .env.example.prod." +echo "Created ${copied} .env file(s) from .env.prod.example." From 4c9a88e5c46398528cc5934d62cb97a1873ec3ef Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Mon, 23 Mar 2026 15:36:23 +0000 Subject: [PATCH 78/87] small fix --- web/src/components/phishing-kits/PhishingKitsPage.tsx | 4 ++++ .../sending-profiles/shared/SendingProfileFormContainer.tsx | 3 +-- .../sending-profiles/shared/SendingProfileLayout.tsx | 3 --- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/src/components/phishing-kits/PhishingKitsPage.tsx b/web/src/components/phishing-kits/PhishingKitsPage.tsx index 4f17849b..e5ab6812 100644 --- a/web/src/components/phishing-kits/PhishingKitsPage.tsx +++ b/web/src/components/phishing-kits/PhishingKitsPage.tsx @@ -13,10 +13,12 @@ export default function PhishingKitsPage() { kits, filteredKits, isLoading, + isFetching, searchQuery, setSearchQuery, viewMode, setViewMode, + refetch, handleDelete, isDeleting, } = usePhishingKits(); @@ -27,6 +29,8 @@ export default function PhishingKitsPage() { viewMode={viewMode} setViewMode={setViewMode} onNewKit={() => navigate({ to: "/phishing-kits/new" })} + refetch={refetch} + isFetching={isFetching} searchQuery={searchQuery} setSearchQuery={setSearchQuery} /> diff --git a/web/src/components/sending-profiles/shared/SendingProfileFormContainer.tsx b/web/src/components/sending-profiles/shared/SendingProfileFormContainer.tsx index 132eede1..c4201f90 100644 --- a/web/src/components/sending-profiles/shared/SendingProfileFormContainer.tsx +++ b/web/src/components/sending-profiles/shared/SendingProfileFormContainer.tsx @@ -44,9 +44,8 @@ export default function SendingProfileFormContainer({ mode={mode} onSubmit={onSubmit} testPassed={form.testPassed} - hasChangesSinceLastTest={form.hasChangesSinceLastTest} isFullyValid={form.isFullyValid} smtpConfigChanged={mode === "create" ? true : form.smtpConfigChanged} /> ); -} \ No newline at end of file +} diff --git a/web/src/components/sending-profiles/shared/SendingProfileLayout.tsx b/web/src/components/sending-profiles/shared/SendingProfileLayout.tsx index 4889074e..ff3c8306 100644 --- a/web/src/components/sending-profiles/shared/SendingProfileLayout.tsx +++ b/web/src/components/sending-profiles/shared/SendingProfileLayout.tsx @@ -30,7 +30,6 @@ interface Props { setUsername: (v: string) => void; password: string; setPassword: (v: string) => void; - onTest: () => void; isTesting: boolean; testStatus: string | null; @@ -62,7 +61,6 @@ export default function SendingProfileLayout({ setUsername, password, setPassword, - onTest, isTesting, testStatus, customHeaders, @@ -111,7 +109,6 @@ export default function SendingProfileLayout({ setUsername={setUsername} password={password} setPassword={setPassword} - onTest={onTest} isTesting={isTesting} testStatus={testStatus} /> From abfd39e5bb3e85857d02b739a6a13dba9b36f605 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Mon, 23 Mar 2026 15:50:53 +0000 Subject: [PATCH 79/87] ??? --- deployment/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 38c19cda..27b162b7 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -251,7 +251,7 @@ services: context: ../web dockerfile: Dockerfile args: - VITE_API_URL: ${API_URL}/api + VITE_API_URL: ${API_URL} VITE_KEYCLOAK_URL: ${KC_HOSTNAME_URL} VITE_BASE_PATH: ${VITE_BASE_PATH} VITE_WEB_URL: ${WEB_URL} From 07db7e77520fbf081dc80cd43f293ee9deebe59c Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Fri, 27 Mar 2026 21:52:07 +0000 Subject: [PATCH 80/87] npm fix --- web/src/components/phishing-kits/usePhishingKits.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/phishing-kits/usePhishingKits.ts b/web/src/components/phishing-kits/usePhishingKits.ts index ba6a87ed..6a8cb678 100644 --- a/web/src/components/phishing-kits/usePhishingKits.ts +++ b/web/src/components/phishing-kits/usePhishingKits.ts @@ -1,4 +1,4 @@ -import { useMemo, useState, useEffect } from "react"; +import { useMemo, useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@/lib/use-query"; import { fetchPhishingKits, deletePhishingKit } from "@/services/phishingKitsApi"; import { useConfirm } from "@/components/ui/confirm-modal"; From 7d6c1c1d2bf4eb917f7681ad52f6d5e0b8745091 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Fri, 27 Mar 2026 22:07:03 +0000 Subject: [PATCH 81/87] frontend errors fix --- web/package-lock.json | 3872 ++--------------------------------------- web/src/main.tsx | 2 +- 2 files changed, 126 insertions(+), 3748 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 68c5ddbf..fe37f983 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -19,8 +19,6 @@ "@react-keycloak/web": "^3.4.0", "@tailwindcss/vite": "^4.1.14", "@tanstack/react-form": "^1.23.7", - "@tanstack/react-query": "^5.90.2", - "@tanstack/react-query-devtools": "^5.90.2", "@tanstack/react-router": "^1.133.13", "@tanstack/react-router-devtools": "^1.133.13", "@tanstack/react-table": "^8.21.3", @@ -47,9 +45,7 @@ }, "devDependencies": { "@eslint/js": "^9.36.0", - "@lhci/cli": "^0.15.1", "@playwright/test": "^1.58.2", - "@tanstack/eslint-plugin-query": "^5.91.2", "@tanstack/router-plugin": "^1.133.13", "@types/chart.js": "^2.9.41", "@types/node": "^24.6.0", @@ -1108,9 +1104,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -1182,9 +1178,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -1293,62 +1289,6 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, - "node_modules/@formatjs/ecma402-abstract": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", - "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@formatjs/fast-memoize": "2.2.7", - "@formatjs/intl-localematcher": "0.6.2", - "decimal.js": "^10.4.3", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/fast-memoize": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", - "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", - "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "2.3.6", - "@formatjs/icu-skeleton-parser": "1.8.16", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.8.16", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", - "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "2.3.6", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/intl-localematcher": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", - "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.8.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1453,82 +1393,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@lhci/cli": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@lhci/cli/-/cli-0.15.1.tgz", - "integrity": "sha512-yhC0oXnXqGHYy1xl4D8YqaydMZ/khFAnXGY/o2m/J3PqPa/D0nj3V6TLoH02oVMFeEF2AQim7UbmdXMiXx2tOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@lhci/utils": "0.15.1", - "chrome-launcher": "^0.13.4", - "compression": "^1.7.4", - "debug": "^4.3.1", - "express": "^4.17.1", - "inquirer": "^6.3.1", - "isomorphic-fetch": "^3.0.0", - "lighthouse": "12.6.1", - "lighthouse-logger": "1.2.0", - "open": "^7.1.0", - "proxy-agent": "^6.4.0", - "tmp": "^0.1.0", - "uuid": "^8.3.1", - "yargs": "^15.4.1", - "yargs-parser": "^13.1.2" - }, - "bin": { - "lhci": "src/cli.js" - } - }, - "node_modules/@lhci/utils": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@lhci/utils/-/utils-0.15.1.tgz", - "integrity": "sha512-WclJnUQJeOMY271JSuaOjCv/aA0pgvuHZS29NFNdIeI14id8eiFsjith85EGKYhljgoQhJ2SiW4PsVfFiakNNw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.3.1", - "isomorphic-fetch": "^3.0.0", - "js-yaml": "^3.13.1", - "lighthouse": "12.6.1", - "tree-kill": "^1.2.1" - } - }, - "node_modules/@lhci/utils/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@lhci/utils/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@paulirish/trace_engine": { - "version": "0.0.53", - "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.53.tgz", - "integrity": "sha512-PUl/vlfo08Oj804VI5nDPeSk9vyslnBlVzDDwFt8SUVxY8+KdGMkra/vrXjEEHe8gb7+RqVTfOIlGw0nyrEelA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "legacy-javascript": "latest", - "third-party-web": "latest" - } - }, "node_modules/@playwright/test": { "version": "1.58.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", @@ -1545,161 +1409,6 @@ "node": ">=18" } }, - "node_modules/@puppeteer/browsers": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", - "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@puppeteer/browsers/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@puppeteer/browsers/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@puppeteer/browsers/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@puppeteer/browsers/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@puppeteer/browsers/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@puppeteer/browsers/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@puppeteer/browsers/node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/@puppeteer/browsers/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@puppeteer/browsers/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -3844,91 +3553,6 @@ "win32" ] }, - "node_modules/@sentry-internal/tracing": { - "version": "7.120.4", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.4.tgz", - "integrity": "sha512-Fz5+4XCg3akeoFK+K7g+d7HqGMjmnLoY2eJlpONJmaeT9pXY7yfUyXKZMmMajdE2LxxKJgQ2YKvSCaGVamTjHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sentry/core": "7.120.4", - "@sentry/types": "7.120.4", - "@sentry/utils": "7.120.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/core": { - "version": "7.120.4", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.120.4.tgz", - "integrity": "sha512-TXu3Q5kKiq8db9OXGkWyXUbIxMMuttB5vJ031yolOl5T/B69JRyAoKuojLBjRv1XX583gS1rSSoX8YXX7ATFGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sentry/types": "7.120.4", - "@sentry/utils": "7.120.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/integrations": { - "version": "7.120.4", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.4.tgz", - "integrity": "sha512-kkBTLk053XlhDCg7OkBQTIMF4puqFibeRO3E3YiVc4PGLnocXMaVpOSCkMqAc1k1kZ09UgGi8DxfQhnFEjUkpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sentry/core": "7.120.4", - "@sentry/types": "7.120.4", - "@sentry/utils": "7.120.4", - "localforage": "^1.8.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/node": { - "version": "7.120.4", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.120.4.tgz", - "integrity": "sha512-qq3wZAXXj2SRWhqErnGCSJKUhPSlZ+RGnCZjhfjHpP49KNpcd9YdPTIUsFMgeyjdh6Ew6aVCv23g1hTP0CHpYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sentry-internal/tracing": "7.120.4", - "@sentry/core": "7.120.4", - "@sentry/integrations": "7.120.4", - "@sentry/types": "7.120.4", - "@sentry/utils": "7.120.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/types": { - "version": "7.120.4", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.4.tgz", - "integrity": "sha512-cUq2hSSe6/qrU6oZsEP4InMI5VVdD86aypE+ENrQ6eZEVLTCYm1w6XhW1NvIu3UuWh7gZec4a9J7AFpYxki88Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/utils": { - "version": "7.120.4", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.120.4.tgz", - "integrity": "sha512-zCKpyDIWKHwtervNK2ZlaK8mMV7gVUijAgFeJStH+CU/imcdquizV3pFLlSQYRswG+Lbyd6CT/LGRh3IbtkCFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sentry/types": "7.120.4" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", @@ -4265,29 +3889,6 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@tanstack/eslint-plugin-query": { - "version": "5.91.4", - "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.91.4.tgz", - "integrity": "sha512-8a+GAeR7oxJ5laNyYBQ6miPK09Hi18o5Oie/jx8zioXODv/AUFLZQecKabPdpQSLmuDXEBPKFh+W5DKbWlahjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/utils": "^8.48.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/@tanstack/form-core": { "version": "1.27.2", "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.27.2.tgz", @@ -4333,26 +3934,6 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@tanstack/query-core": { - "version": "5.90.2", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz", - "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/query-devtools": { - "version": "5.90.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.90.1.tgz", - "integrity": "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, "node_modules/@tanstack/react-form": { "version": "1.27.2", "resolved": "https://registry.npmjs.org/@tanstack/react-form/-/react-form-1.27.2.tgz", @@ -4375,39 +3956,6 @@ } } }, - "node_modules/@tanstack/react-query": { - "version": "5.90.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz", - "integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.90.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/@tanstack/react-query-devtools": { - "version": "5.90.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.90.2.tgz", - "integrity": "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ==", - "license": "MIT", - "dependencies": { - "@tanstack/query-devtools": "5.90.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "@tanstack/react-query": "^5.90.2", - "react": "^18 || ^19" - } - }, "node_modules/@tanstack/react-router": { "version": "1.140.1", "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.140.1.tgz", @@ -4727,13 +4275,6 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4948,17 +4489,6 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", @@ -5242,30 +4772,6 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -5289,16 +4795,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -5316,36 +4812,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -5405,13 +4871,6 @@ "node": ">=10" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, - "license": "MIT" - }, "node_modules/ast-types": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", @@ -5431,16 +4890,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/axe-core": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", - "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, "node_modules/axios": { "version": "1.13.5", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", @@ -5452,21 +4901,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/b4a": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", - "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/babel-dead-code-elimination": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.10.tgz", @@ -5497,99 +4931,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bare-events": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", - "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz", - "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-os": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz", - "integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "bare": ">=1.14.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.1.tgz", - "integrity": "sha512-bSeR8RfvbRwDpD7HWZvn8M3uYNDrk7m9DQjYOFkENZlXW8Ju/MPaqUPQq5LqJ3kyjEm07siTaAQ7wBKCU59oHg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "streamx": "^2.21.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", - "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/baseline-browser-mapping": { "version": "2.9.6", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz", @@ -5600,16 +4941,6 @@ "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/basic-ftp": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", - "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -5623,52 +4954,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -5722,26 +5011,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -5755,23 +5024,6 @@ "node": ">= 0.4" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -5782,16 +5034,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001760", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", @@ -5880,13 +5122,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true, - "license": "MIT" - }, "node_modules/chart.js": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", @@ -5925,55 +5160,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/chrome-launcher": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.13.4.tgz", - "integrity": "sha512-nnzXiDbGKjDSK6t2I+35OAPBy5Pw/39bgkb/ZAFwMhwJbdYBp6aH+vW28ZgtjdU890Q7D+3wN/tB8N66q5Gi2A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/node": "*", - "escape-string-regexp": "^1.0.5", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^1.0.0", - "mkdirp": "^0.5.3", - "rimraf": "^3.0.2" - } - }, - "node_modules/chrome-launcher/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "mitt": "^3.0.1", - "zod": "^3.24.1" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, - "node_modules/chromium-bidi/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -5986,86 +5172,6 @@ "url": "https://polar.sh/cva" } }, - "node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", - "dev": true, - "license": "ISC" - }, - "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -6133,103 +5239,13 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, - "node_modules/configstore": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", - "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -6237,29 +5253,12 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/cookie-es": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz", "integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==", "license": "MIT" }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "dev": true, - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6275,23 +5274,6 @@ "node": ">= 8" } }, - "node_modules/crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/csp_evaluator": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.5.tgz", - "integrity": "sha512-EL/iN9etCTzw/fBnp0/uj0f5BOOGvZut2mzsiiBZ/FdT6gFQCKRO/tmcKOxn5drWZ2Ndm/xBb1SI4zwWbGtmIw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -6419,16 +5401,6 @@ "node": ">=12" } }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -6446,23 +5418,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", @@ -6489,44 +5444,6 @@ "dev": true, "license": "MIT" }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/degenerator/node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6536,16 +5453,6 @@ "node": ">=0.4.0" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -6555,17 +5462,6 @@ "node": ">=6" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -6594,13 +5490,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/devtools-protocol": { - "version": "0.0.1467305", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1467305.tgz", - "integrity": "sha512-LxwMLqBoPPGpMdRL4NkLFRNy3QLp6Uqa7GNp1v6JaBheop2QrB9Q7q0A/q/CYYP9sBfZdHOyszVx4gc9zyk7ow==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/diff": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", @@ -6611,19 +5500,6 @@ "node": ">=0.3.1" } }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -6638,13 +5514,6 @@ "node": ">= 0.4" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, "node_modules/electron-to-chromium": { "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", @@ -6652,33 +5521,6 @@ "dev": true, "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -6692,43 +5534,6 @@ "node": ">=10.13.0" } }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/enquirer/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/enquirer/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -6836,13 +5641,6 @@ "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -6856,39 +5654,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eslint": { "version": "9.39.3", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", @@ -7003,9 +5768,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -7153,216 +5918,43 @@ "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } + "license": "MIT" }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, "license": "MIT" }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/external-editor/node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7385,42 +5977,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -7453,9 +6009,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -7495,16 +6051,6 @@ "node": ">= 6" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/framer-motion": { "version": "12.23.26", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz", @@ -7532,23 +6078,6 @@ } } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -7582,16 +6111,6 @@ "node": ">=6.9.0" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -7638,22 +6157,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-tsconfig": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", @@ -7667,43 +6170,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -7717,30 +6183,6 @@ "node": ">= 6" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/globals": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", @@ -7895,78 +6337,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/http-link-header": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.3.tgz", - "integrity": "sha512-3cZ0SRL8fb9MUlU3mKM61FcQvPfXx2dBrZW3Vbg5CXa8jFlK8OaEpePenLe1oEXQduhz8b0QjsqfS59QP4AJDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -7977,20 +6347,6 @@ "node": ">= 4" } }, - "node_modules/image-ssim": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz", - "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==", - "dev": true, - "license": "MIT" - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true, - "license": "MIT" - }, "node_modules/immer": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", @@ -8028,194 +6384,39 @@ "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, - "node_modules/inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", "engines": { - "node": ">=6.0.0" + "node": ">=12" } }, - "node_modules/inquirer/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/inquirer/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/inquirer/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/inquirer/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/inquirer/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/intl-messageformat": { - "version": "10.7.18", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", - "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@formatjs/ecma402-abstract": "2.3.6", - "@formatjs/fast-memoize": "2.2.7", - "@formatjs/icu-messageformat-parser": "2.11.4", - "tslib": "^2.8.0" - } - }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" }, "funding": { "type": "github", @@ -8245,22 +6446,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -8271,16 +6456,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -8314,16 +6489,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -8336,26 +6501,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/isbot": { "version": "5.1.32", "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.32.tgz", @@ -8372,17 +6517,6 @@ "dev": true, "license": "ISC" }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -8392,23 +6526,6 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/jpeg-js": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", - "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/js-library-detector": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.7.0.tgz", - "integrity": "sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8495,13 +6612,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/legacy-javascript": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/legacy-javascript/-/legacy-javascript-0.0.1.tgz", - "integrity": "sha512-lPyntS4/aS7jpuvOlitZDFifBCb4W8L/3QU0PLbUTUj+zYah8rfVjYic88yG7ZKTxhS5h9iz7duT8oUXKszLhg==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -8516,274 +6626,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", - "dev": true, - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/lighthouse": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-12.6.1.tgz", - "integrity": "sha512-85WDkjcXAVdlFem9Y6SSxqoKiz/89UsDZhLpeLJIsJ4LlHxw047XTZhlFJmjYCB7K5S1erSBAf5cYLcfyNbH3A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@paulirish/trace_engine": "0.0.53", - "@sentry/node": "^7.0.0", - "axe-core": "^4.10.3", - "chrome-launcher": "^1.2.0", - "configstore": "^5.0.1", - "csp_evaluator": "1.1.5", - "devtools-protocol": "0.0.1467305", - "enquirer": "^2.3.6", - "http-link-header": "^1.1.1", - "intl-messageformat": "^10.5.3", - "jpeg-js": "^0.4.4", - "js-library-detector": "^6.7.0", - "lighthouse-logger": "^2.0.1", - "lighthouse-stack-packs": "1.12.2", - "lodash-es": "^4.17.21", - "lookup-closest-locale": "6.2.0", - "metaviewport-parser": "0.3.0", - "open": "^8.4.0", - "parse-cache-control": "1.0.1", - "puppeteer-core": "^24.10.0", - "robots-parser": "^3.0.1", - "semver": "^5.3.0", - "speedline-core": "^1.4.3", - "third-party-web": "^0.26.6", - "tldts-icann": "^6.1.16", - "ws": "^7.0.0", - "yargs": "^17.3.1", - "yargs-parser": "^21.0.0" - }, - "bin": { - "chrome-debug": "core/scripts/manual-chrome-launcher.js", - "lighthouse": "cli/index.js", - "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js" - }, - "engines": { - "node": ">=18.20" - } - }, - "node_modules/lighthouse-logger": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.2.0.tgz", - "integrity": "sha512-wzUvdIeJZhRsG6gpZfmSCfysaxNEr43i+QT+Hie94wvHDKFLi4n7C2GqZ4sTC+PH5b5iktmXJvU87rWvhP3lHw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^2.6.8", - "marky": "^1.2.0" - } - }, - "node_modules/lighthouse-logger/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/lighthouse-logger/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/lighthouse-stack-packs": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/lighthouse-stack-packs/-/lighthouse-stack-packs-1.12.2.tgz", - "integrity": "sha512-Ug8feS/A+92TMTCK6yHYLwaFMuelK/hAKRMdldYkMNwv+d9PtWxjXEg6rwKtsUXTADajhdrhXyuNCJ5/sfmPFw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/lighthouse/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/lighthouse/node_modules/chrome-launcher": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.2.1.tgz", - "integrity": "sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/node": "*", - "escape-string-regexp": "^4.0.0", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^2.0.1" - }, - "bin": { - "print-chrome-path": "bin/print-chrome-path.cjs" - }, - "engines": { - "node": ">=12.13.0" - } - }, - "node_modules/lighthouse/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/lighthouse/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/lighthouse/node_modules/lighthouse-logger": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.2.tgz", - "integrity": "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.4.1", - "marky": "^1.2.2" - } - }, - "node_modules/lighthouse/node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lighthouse/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/lighthouse/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lighthouse/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lighthouse/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/lighthouse/node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/lighthouse/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/lighthouse/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -9033,16 +6875,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/localforage": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "lie": "3.1.1" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -9059,20 +6891,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9090,13 +6908,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/lookup-closest-locale": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", - "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -9125,29 +6936,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/marky": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", - "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -9310,43 +7098,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/metaviewport-parser": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.3.0.tgz", - "integrity": "sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -9789,19 +7540,6 @@ ], "license": "MIT" }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -9823,16 +7561,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", @@ -9849,36 +7577,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true, - "license": "MIT" - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -9936,13 +7634,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", - "dev": true, - "license": "ISC" - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -9968,26 +7659,6 @@ "dev": true, "license": "MIT" }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -9998,27 +7669,6 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -10033,120 +7683,44 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nuqs": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.8.5.tgz", - "integrity": "sha512-ndhnNB9eLX/bsiGFkBNsrfOWf3BCbzBMD+b5GkD5o2Q96Q+llHnoUlZsrO3tgJKZZV7LLlVCvFKdj+sjBITRzg==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/franky47" - }, - "peerDependencies": { - "@remix-run/react": ">=2", - "@tanstack/react-router": "^1", - "next": ">=14.2.0", - "react": ">=18.2.0 || ^19.0.0-0", - "react-router": "^5 || ^6 || ^7", - "react-router-dom": "^5 || ^6 || ^7" - }, - "peerDependenciesMeta": { - "@remix-run/react": { - "optional": true - }, - "@tanstack/react-router": { - "optional": true - }, - "next": { - "optional": true - }, - "react-router": { - "optional": true - }, - "react-router-dom": { - "optional": true - } - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^1.0.0" - }, - "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "dev": true, + "node_modules/nuqs": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.8.5.tgz", + "integrity": "sha512-ndhnNB9eLX/bsiGFkBNsrfOWf3BCbzBMD+b5GkD5o2Q96Q+llHnoUlZsrO3tgJKZZV7LLlVCvFKdj+sjBITRzg==", "license": "MIT", "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - }, - "engines": { - "node": ">=8" + "@standard-schema/spec": "1.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/franky47" + }, + "peerDependencies": { + "@remix-run/react": ">=2", + "@tanstack/react-router": "^1", + "next": ">=14.2.0", + "react": ">=18.2.0 || ^19.0.0-0", + "react-router": "^5 || ^6 || ^7", + "react-router-dom": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "@tanstack/react-router": { + "optional": true + }, + "next": { + "optional": true + }, + "react-router": { + "optional": true + }, + "react-router-dom": { + "optional": true + } } }, "node_modules/optionator": { @@ -10167,16 +7741,6 @@ "node": ">= 0.8.0" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -10209,50 +7773,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -10266,12 +7786,6 @@ "node": ">=6" } }, - "node_modules/parse-cache-control": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", - "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", - "dev": true - }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -10297,16 +7811,6 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10317,16 +7821,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -10337,13 +7831,6 @@ "node": ">=8" } }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "dev": true, - "license": "MIT" - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -10351,13 +7838,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -10365,9 +7845,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -10478,16 +7958,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -10498,67 +7968,12 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -10569,70 +7984,6 @@ "node": ">=6" } }, - "node_modules/puppeteer-core": { - "version": "24.39.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.39.1.tgz", - "integrity": "sha512-AMqQIKoEhPS6CilDzw0Gd1brLri3emkC+1N2J6ZCCuY1Cglo56M63S0jOeBZDQlemOiRd686MYVMl9ELJBzN3A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.13.0", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1581282", - "typed-query-selector": "^2.12.1", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.19.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/puppeteer-core/node_modules/devtools-protocol": { - "version": "0.0.1581282", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", - "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/puppeteer-core/node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/radix-ui": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", @@ -10774,32 +8125,6 @@ } } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/react": { "version": "19.2.1", "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", @@ -11101,23 +8426,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true, - "license": "ISC" - }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -11144,47 +8452,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/robots-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.1.tgz", - "integrity": "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", @@ -11217,75 +8484,17 @@ "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/rxjs/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, - "license": "0BSD" - }, - "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==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } }, "node_modules/scheduler": { "version": "0.27.0", @@ -11303,48 +8512,6 @@ "semver": "bin/semver.js" } }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/seroval": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.0.tgz", @@ -11366,36 +8533,6 @@ "seroval": "^1.0" } }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, - "license": "ISC" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -11419,130 +8556,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/solid-js": { "version": "1.9.11", "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.11.tgz", @@ -11594,87 +8607,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/speedline-core": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz", - "integrity": "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "image-ssim": "^0.2.0", - "jpeg-js": "^0.4.1" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/streamx": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", - "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, - "node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -11689,19 +8621,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -11781,68 +8700,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/tar-fs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", - "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/tar-stream": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", - "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, - "node_modules/third-party-web": { - "version": "0.26.7", - "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.26.7.tgz", - "integrity": "sha512-buUzX4sXC4efFX6xg2bw6/eZsCUh8qQwSavC4D9HpONMFlRbcHhD8Je5qwYdCpViR6q0qla2wPP+t91a2vgolg==", - "dev": true, - "license": "MIT" - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true, - "license": "MIT" - }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -11889,9 +8746,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -11900,88 +8757,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tldts-icann": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-6.1.86.tgz", - "integrity": "sha512-NFxmRT2lAEMcCOBgeZ0NuM0zsK/xgmNajnY6n4S1mwAKocft2s2ise1O3nQxrH3c+uY6hgHUV9GGNVp7tUE4Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^6.1.86" - } - }, - "node_modules/tmp": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", - "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "rimraf": "^2.6.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tmp/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", - "bin": { - "tree-kill": "cli.js" + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, "node_modules/trim-lines": { @@ -12065,37 +8851,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-query-selector": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", - "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -12160,19 +8915,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "crypto-random-string": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -12241,16 +8983,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/unplugin": { "version": "2.3.11", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", @@ -12268,9 +9000,9 @@ } }, "node_modules/unplugin/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -12373,36 +9105,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -13002,9 +9704,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -13013,20 +9715,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -13034,24 +9722,6 @@ "dev": true, "license": "MIT" }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -13068,13 +9738,6 @@ "node": ">= 8" } }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "dev": true, - "license": "ISC" - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -13085,128 +9748,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true, - "license": "ISC" - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -13214,169 +9755,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/web/src/main.tsx b/web/src/main.tsx index d02ce538..4770cd66 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,4 +1,4 @@ -import { StrictMode, useState, useEffect } from 'react' +import { useState, useEffect } from 'react' import ReactDOM from 'react-dom/client' import { RouterProvider, createRouter } from '@tanstack/react-router' import { ReactKeycloakProvider } from '@react-keycloak/web' From 96092d2488bdc68fbbf3bdc452f90b1910cf3590 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Fri, 27 Mar 2026 22:21:29 +0000 Subject: [PATCH 82/87] Fix user deletion calling the wrong endpoint and some minor code refactor --- web/src/components/admin/TenantDetails.tsx | 2 +- .../admin/tenant-org-manager/BulkImportModal.tsx | 8 ++++---- .../components/admin/tenant-org-manager/NewUserModal.tsx | 4 ++-- web/src/routes/tenants-org-manager.tsx | 6 +++--- web/src/services/{tenantOrgManagerApi.ts => userApi.ts} | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) rename web/src/services/{tenantOrgManagerApi.ts => userApi.ts} (79%) diff --git a/web/src/components/admin/TenantDetails.tsx b/web/src/components/admin/TenantDetails.tsx index 89d1d3bf..d56a5cb5 100644 --- a/web/src/components/admin/TenantDetails.tsx +++ b/web/src/components/admin/TenantDetails.tsx @@ -169,7 +169,7 @@ export function TenantDetails() { setError(null); try { const res = await fetch( - `${API_BASE}/realms/admin/${encodeURIComponent(realmName)}/users/${encodeURIComponent(userId)}`, + `${API_BASE}/realms/${encodeURIComponent(realmName)}/users/${encodeURIComponent(userId)}`, { method: "DELETE", headers: { diff --git a/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx b/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx index b7b6fb90..42aeeae3 100644 --- a/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx +++ b/web/src/components/admin/tenant-org-manager/BulkImportModal.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { X, Loader2 } from "lucide-react"; import type { BulkUser } from "./types"; -import { tenantOrgManagerApi } from "@/services/tenantOrgManagerApi"; +import { userApi } from "@/services/userApi"; import { userGroupsApi } from "@/services/userGroupsApi"; interface BulkImportModalProps { @@ -16,7 +16,7 @@ export function BulkImportModal({ initialBulkUsers, onClose, onBulkCreated, -}: BulkImportModalProps) { +}: Readonly) { const [bulkUsers, setBulkUsers] = useState(initialBulkUsers); const [isBulkLoading, setIsBulkLoading] = useState(false); @@ -63,7 +63,7 @@ export function BulkImportModal({ continue; } try { - const data = await tenantOrgManagerApi.createUser( + const data = await userApi.createUser( realm, { username: u.username, @@ -82,7 +82,7 @@ export function BulkImportModal({ // Assign groups try { - const data = await tenantOrgManagerApi.getUsers(realm); + const data = await userApi.getUsers(realm); const userMap: Record = {}; (data.users || []).forEach((usr: any) => { if (usr.email && usr.id) userMap[usr.email.toLowerCase()] = usr.id; diff --git a/web/src/components/admin/tenant-org-manager/NewUserModal.tsx b/web/src/components/admin/tenant-org-manager/NewUserModal.tsx index c3807755..5c433977 100644 --- a/web/src/components/admin/tenant-org-manager/NewUserModal.tsx +++ b/web/src/components/admin/tenant-org-manager/NewUserModal.tsx @@ -3,7 +3,7 @@ import { X, User, Mail, AtSign, Check, Loader2 } from "lucide-react"; import { toast } from "sonner"; import RequiredAsterisk from "@/components/shared/RequiredAsterisk"; import type { Group, CreateUserField } from "./types"; -import { tenantOrgManagerApi } from "@/services/tenantOrgManagerApi"; +import { userApi } from "@/services/userApi"; interface NewUserModalProps { realm: string; @@ -71,7 +71,7 @@ export function NewUserModal({ realm, groups, onClose, onUserCreated }: Readonly setCreateFieldError(null); try { - const data = await tenantOrgManagerApi.createUser( + const data = await userApi.createUser( realm, { username: newUserUsername || newUserEmail.split("@")[0], diff --git a/web/src/routes/tenants-org-manager.tsx b/web/src/routes/tenants-org-manager.tsx index 2b802c09..14ed43b7 100644 --- a/web/src/routes/tenants-org-manager.tsx +++ b/web/src/routes/tenants-org-manager.tsx @@ -9,7 +9,7 @@ import { UserManagementHeader } from "../components/admin/tenant-org-manager/Use import { NewUserModal } from "../components/admin/tenant-org-manager/NewUserModal"; import { BulkImportModal } from "../components/admin/tenant-org-manager/BulkImportModal"; import { mapRole } from "../components/admin/tenant-org-manager/utils"; -import { tenantOrgManagerApi } from "../services/tenantOrgManagerApi"; +import { userApi } from "../services/userApi"; import { userGroupsApi } from "../services/userGroupsApi"; export const Route = createFileRoute("/tenants-org-manager")({ @@ -43,7 +43,7 @@ function UsersManagement() { const fetchUsers = async (targetRealm: string) => { setIsLoading(true); try { - const data = await tenantOrgManagerApi.getUsers(targetRealm); + const data = await userApi.getUsers(targetRealm); setUsers(data.users || []); } catch (err) { console.error("Failed to fetch users", err); @@ -75,7 +75,7 @@ function UsersManagement() { setDeletingIds((prev) => ({ ...prev, [id]: true })); try { - await tenantOrgManagerApi.deleteUser(realm, id); + await userApi.deleteUser(realm, id); setUsers((prev) => prev.filter((u) => (u.id || u.username) !== id)); } catch (err) { console.error("Failed to delete user", err); diff --git a/web/src/services/tenantOrgManagerApi.ts b/web/src/services/userApi.ts similarity index 79% rename from web/src/services/tenantOrgManagerApi.ts rename to web/src/services/userApi.ts index 11e067ff..c8058632 100644 --- a/web/src/services/tenantOrgManagerApi.ts +++ b/web/src/services/userApi.ts @@ -5,7 +5,7 @@ import type { TenantUserListResponse, } from "@/types/tenantOrgManager"; -export const tenantOrgManagerApi = { +export const userApi = { getUsers: (realm: string) => apiClient.get(`/realms/${encodeURIComponent(realm)}/users`), @@ -17,6 +17,6 @@ export const tenantOrgManagerApi = { deleteUser: (realm: string, userId: string) => apiClient.delete( - `/org-manager/${encodeURIComponent(realm)}/users/${encodeURIComponent(userId)}` - ) as Promise, + `/realms/${encodeURIComponent(realm)}/users/${encodeURIComponent(userId)}` + ), }; From ecbf036a0996c02d38e55654bdca3f231339016c Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Fri, 27 Mar 2026 22:41:06 +0000 Subject: [PATCH 83/87] actual user delte route fix --- api/src/routers/realm/user_routes.py | 14 +++ .../services/keycloak_admin/user_handler.py | 9 +- .../services/platform_admin/user_handler.py | 87 +++++++------------ web/src/services/userApi.ts | 2 +- 4 files changed, 50 insertions(+), 62 deletions(-) diff --git a/api/src/routers/realm/user_routes.py b/api/src/routers/realm/user_routes.py index 0e112882..2f9794e6 100644 --- a/api/src/routers/realm/user_routes.py +++ b/api/src/routers/realm/user_routes.py @@ -99,6 +99,20 @@ def delete_user_in_realm( return None +@router.delete( + "/realms/{realm}/users/{user_id}/org-manager", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))], +) +def delete_user_in_own_realm( + realm: str, user_id: str, session: SessionDep, token: OAuth2Scheme +): + """Delete a user inside the caller's own Keycloak realm/tenant.""" + realm_service.validate_realm_access(token, realm) + realm_service.delete_user_in_realm(realm, user_id, session) + return None + + @router.put( "/realms/{realm}/role/{user_id}", status_code=status.HTTP_204_NO_CONTENT, diff --git a/api/src/services/keycloak_admin/user_handler.py b/api/src/services/keycloak_admin/user_handler.py index 83cae9ea..7851a0a6 100644 --- a/api/src/services/keycloak_admin/user_handler.py +++ b/api/src/services/keycloak_admin/user_handler.py @@ -1,5 +1,5 @@ import requests -from sqlmodel import Session, select +from sqlmodel import Session from src.models import Realm, User from src.services.keycloak_admin.base_handler import base_handler @@ -24,13 +24,10 @@ def get_user_count(self, realm_name: str) -> int: return r.json() - def delete_user(self, session: Session, realm_name: str, user_id: str): - """Delete a user from a realm.""" + def delete_user(self, realm_name: str, user_id: str) -> None: + """Delete a user from a realm in Keycloak.""" token = self._get_admin_token() self.keycloak_client.delete_user(realm_name, token, user_id) - session.delete( - session.exec(select(User).where(User.keycloak_id == user_id)).one() - ) def get_user_realm_roles(self, realm_name: str, user_id: str) -> list[dict]: """Return realm roles assigned to the given user.""" diff --git a/api/src/services/platform_admin/user_handler.py b/api/src/services/platform_admin/user_handler.py index 933eac21..a4ec558e 100644 --- a/api/src/services/platform_admin/user_handler.py +++ b/api/src/services/platform_admin/user_handler.py @@ -136,55 +136,13 @@ def get_user_in_realm(self, realm: str, user_id: str) -> UserDTO: def delete_user_in_realm(self, realm: str, user_id: str, session: Session) -> None: """Delete a user from the realm.""" + if self._is_org_manager(realm, user_id) and self._count_org_managers(realm) <= 1: + raise HTTPException( + status_code=400, detail="Cannot delete the last org manager." + ) - roles = self.admin.get_user_realm_roles(realm, user_id) - - is_target_org_manager = any( - str((r.get("name", r) if isinstance(r, dict) else r)).upper() - == "ORG_MANAGER" - for r in (roles or []) - ) - if is_target_org_manager: - - kc_users = self.admin.list_users(realm) - - org_count = 0 - for u in kc_users: - uid = u.get("id") - if not uid: - continue - inline = u.get("realmRoles") or [] - has_role = ( - any(str(r).upper() == "ORG_MANAGER" for r in inline) - if isinstance(inline, list) - else False - ) - if not has_role: - try: - user_roles = self.admin.get_user_realm_roles(realm, uid) - has_role = any( - str( - (r.get("name", r) if isinstance(r, dict) else r) - ).upper() - == "ORG_MANAGER" - for r in (user_roles or []) - ) - except Exception: - has_role = False - if has_role: - org_count += 1 - - if org_count <= 1: - raise HTTPException( - status_code=400, detail="Cannot delete the last org manager." - ) - - self.admin.delete_user(session, realm, user_id) - - db_user = session.get(User, user_id) - if db_user: - session.delete(db_user) - session.commit() + self.admin.delete_user(realm, user_id) + self._delete_local_user(session, user_id) def update_user_role_in_realm( self, realm: str, user_id: str, new_role: str @@ -234,16 +192,35 @@ def _resolve_is_org_manager( try: user_roles = self.admin.get_user_realm_roles(realm, user_id) - return any( - str( - (role.get("name", role) if isinstance(role, dict) else role) - ).upper() - == "ORG_MANAGER" - for role in (user_roles or []) - ) + return self._has_org_manager_role(user_roles) except Exception: return fallback + def _has_org_manager_role(self, roles: list[dict] | list[str] | None) -> bool: + return any( + str((role.get("name", role) if isinstance(role, dict) else role)).upper() + == "ORG_MANAGER" + for role in (roles or []) + ) + + def _is_org_manager(self, realm: str, user_id: str) -> bool: + return self._has_org_manager_role( + self.admin.get_user_realm_roles(realm, user_id) + ) + + def _count_org_managers(self, realm: str) -> int: + return sum( + 1 + for user in self.admin.list_users(realm) + if self._resolve_is_org_manager(realm, user, False) + ) + + def _delete_local_user(self, session: Session, user_id: str) -> None: + db_user = session.get(User, user_id) + if db_user: + session.delete(db_user) + session.commit() + _instance: user_handler | None = None diff --git a/web/src/services/userApi.ts b/web/src/services/userApi.ts index c8058632..e44863ed 100644 --- a/web/src/services/userApi.ts +++ b/web/src/services/userApi.ts @@ -17,6 +17,6 @@ export const userApi = { deleteUser: (realm: string, userId: string) => apiClient.delete( - `/realms/${encodeURIComponent(realm)}/users/${encodeURIComponent(userId)}` + `/realms/${encodeURIComponent(realm)}/users/${encodeURIComponent(userId)}/org-manager` ), }; From 4942be0264743618a420b9f8aee82f70c122fcda Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Fri, 27 Mar 2026 23:12:01 +0000 Subject: [PATCH 84/87] test secrets only run on deployment --- .github/workflows/CD-workflow.yml | 98 ++++++++++++++++++++++++++++++- api/.env.prod.example | 22 ------- 2 files changed, 96 insertions(+), 24 deletions(-) delete mode 100644 api/.env.prod.example diff --git a/.github/workflows/CD-workflow.yml b/.github/workflows/CD-workflow.yml index 656f6216..4da01bc7 100644 --- a/.github/workflows/CD-workflow.yml +++ b/.github/workflows/CD-workflow.yml @@ -8,6 +8,53 @@ on: jobs: build: runs-on: [self-hosted, linux, x64] + env: + POSTGRES_USER: ${{ secrets.POSTGRES_USER }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + POSTGRES_DB: ${{ secrets.POSTGRES_DB }} + MONGO_ROOT_USER: ${{ secrets.MONGO_ROOT_USER }} + MONGO_ROOT_PASSWORD: ${{ secrets.MONGO_ROOT_PASSWORD }} + MONGO_DB: ${{ secrets.MONGO_DB }} + MONGO_USER: ${{ secrets.MONGO_USER }} + MONGO_PASSWORD: ${{ secrets.MONGO_PASSWORD }} + FILE_STORAGE_BACKEND: ${{ secrets.FILE_STORAGE_BACKEND }} + GARAGE_S3_ENDPOINT: ${{ secrets.GARAGE_S3_ENDPOINT }} + GARAGE_S3_PUBLIC_ENDPOINT: ${{ secrets.GARAGE_S3_PUBLIC_ENDPOINT }} + GARAGE_S3_REGION: ${{ secrets.GARAGE_S3_REGION }} + GARAGE_ACCESS_KEY_ID: ${{ secrets.GARAGE_ACCESS_KEY_ID }} + GARAGE_SECRET_ACCESS_KEY: ${{ secrets.GARAGE_SECRET_ACCESS_KEY }} + GARAGE_FORCE_PATH_STYLE: ${{ secrets.GARAGE_FORCE_PATH_STYLE }} + GARAGE_BUCKET_CONTENT: ${{ secrets.GARAGE_BUCKET_CONTENT }} + GARAGE_BUCKET_LOGOS: ${{ secrets.GARAGE_BUCKET_LOGOS }} + GARAGE_CONTENT_PREFIX: ${{ secrets.GARAGE_CONTENT_PREFIX }} + GARAGE_LOGOS_PREFIX: ${{ secrets.GARAGE_LOGOS_PREFIX }} + GARAGE_RPC_SECRET: ${{ secrets.GARAGE_RPC_SECRET }} + RABBITMQ_DEFAULT_USER: ${{ secrets.RABBITMQ_DEFAULT_USER }} + RABBITMQ_DEFAULT_PASS: ${{ secrets.RABBITMQ_DEFAULT_PASS }} + RABBITMQ_QUEUE: ${{ secrets.RABBITMQ_QUEUE }} + RABBITMQ_API_USER: ${{ secrets.RABBITMQ_API_USER }} + RABBITMQ_API_PASS: ${{ secrets.RABBITMQ_API_PASS }} + RABBITMQ_SMTP_USER: ${{ secrets.RABBITMQ_SMTP_USER }} + RABBITMQ_SMTP_PASS: ${{ secrets.RABBITMQ_SMTP_PASS }} + RATE_LIMITER_MAX_REQUESTS: ${{ secrets.RATE_LIMITER_MAX_REQUESTS }} + RATE_LIMITER_TIME_WINDOW_SECONDS: ${{ secrets.RATE_LIMITER_TIME_WINDOW_SECONDS }} + KEYCLOAK_ADMIN: ${{ secrets.KEYCLOAK_ADMIN }} + KEYCLOAK_ADMIN_PASSWORD: ${{ secrets.KEYCLOAK_ADMIN_PASSWORD }} + CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }} + KEYCLOAK_INTERNAL_URL: ${{ secrets.KEYCLOAK_INTERNAL_URL }} + API_INTERNAL_URL: ${{ secrets.API_INTERNAL_URL }} + KC_HTTP_RELATIVE_PATH: ${{ secrets.KC_HTTP_RELATIVE_PATH }} + VITE_BASE_PATH: ${{ secrets.VITE_BASE_PATH }} + KC_HOSTNAME: ${{ secrets.KC_HOSTNAME }} + KC_HOSTNAME_URL: ${{ secrets.KC_HOSTNAME_URL }} + KEYCLOAK_URL: ${{ secrets.KEYCLOAK_URL }} + API_URL: ${{ secrets.API_URL }} + WEB_URL: ${{ secrets.WEB_URL }} + KEYCLOAK_ISSUER_URL: ${{ secrets.KEYCLOAK_ISSUER_URL }} + NGINX_PORT: ${{ secrets.NGINX_PORT }} + SERVER_PORT: ${{ secrets.SERVER_PORT }} + TLS_CERT_FILE: ${{ secrets.TLS_CERT_FILE }} + TLS_KEY_FILE: ${{ secrets.TLS_KEY_FILE }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -15,9 +62,56 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f - - name: Create .env files from .env.example + - name: Create deployment .env from GitHub secrets run: | - bash scripts/copy-env-examples.sh + cat > deployment/.env < Date: Fri, 27 Mar 2026 23:24:55 +0000 Subject: [PATCH 85/87] Fix: logo path in the navbar.tsx file and fix request spam bug on templates page --- web/src/components/navbar.tsx | 3 ++- web/src/lib/use-query.ts | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/web/src/components/navbar.tsx b/web/src/components/navbar.tsx index 641b0755..06bae043 100644 --- a/web/src/components/navbar.tsx +++ b/web/src/components/navbar.tsx @@ -2,12 +2,13 @@ import { ChevronRight, User, LogOut } from "lucide-react"; import { useKeycloak } from "@react-keycloak/web"; import { useRouterState, Link } from "@tanstack/react-router"; import "../css/navbar.css"; +import { appAssetUrl } from '@/lib/app-path' export function Logo() { return (
Logo diff --git a/web/src/lib/use-query.ts b/web/src/lib/use-query.ts index 42785763..682872e5 100644 --- a/web/src/lib/use-query.ts +++ b/web/src/lib/use-query.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' export function useQuery({ queryKey, queryFn, enabled = true }: { queryKey: any[], queryFn: () => Promise, enabled?: boolean }) { const [data, setData] = useState(undefined) @@ -6,11 +6,16 @@ export function useQuery({ queryKey, queryFn, enabled = true }: { queryKey: a const [isFetching, setIsFetching] = useState(false) const [isError, setIsError] = useState(false) const [error, setError] = useState(null) + const queryFnRef = useRef(queryFn) + + useEffect(() => { + queryFnRef.current = queryFn + }, [queryFn]) const fetchData = useCallback(async () => { setIsFetching(true) try { - const result = await queryFn() + const result = await queryFnRef.current() setData(result) setIsError(false) } catch (err) { @@ -20,13 +25,13 @@ export function useQuery({ queryKey, queryFn, enabled = true }: { queryKey: a setIsLoading(false) setIsFetching(false) } - }, [queryFn]) + }, []) useEffect(() => { if (enabled) { void fetchData() } - }, [JSON.stringify(queryKey), enabled, fetchData]) + }, [queryKey, enabled, fetchData]) useEffect(() => { const handleInvalidate = () => { From 97b7a24817c9fc18191f979d0ff22a9d9fab8c4a Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Fri, 27 Mar 2026 23:41:15 +0000 Subject: [PATCH 86/87] Fix: request spam on templates and phishing kits pages; Added secrets creating script --- scripts/copy-env-examples.sh | 19 --- scripts/import-prod-example-secrets.sh | 199 +++++++++++++++++++++++++ web/src/lib/use-query.ts | 3 +- 3 files changed, 201 insertions(+), 20 deletions(-) delete mode 100644 scripts/copy-env-examples.sh create mode 100755 scripts/import-prod-example-secrets.sh diff --git a/scripts/copy-env-examples.sh b/scripts/copy-env-examples.sh deleted file mode 100644 index 3c7710cd..00000000 --- a/scripts/copy-env-examples.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - -copied=0 -while IFS= read -r -d '' example_file; do - target_file="$(dirname "${example_file}")/.env" - cp -f "${example_file}" "${target_file}" - echo "Copied ${example_file} -> ${target_file}" - copied=$((copied + 1)) -done < <(find "${ROOT_DIR}" -type f -name ".env.prod.example" -print0) - -if [[ "${copied}" -eq 0 ]]; then - echo "No .env.prod.example files found." - exit 1 -fi - -echo "Created ${copied} .env file(s) from .env.prod.example." diff --git a/scripts/import-prod-example-secrets.sh b/scripts/import-prod-example-secrets.sh new file mode 100755 index 00000000..7a14c784 --- /dev/null +++ b/scripts/import-prod-example-secrets.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +usage() { + cat <<'EOF' +Usage: + scripts/import-prod-example-secrets.sh [--repo owner/name] [--dry-run] [--env-file path ...] + +Description: + Reads variable names from deployment/.env.prod.example and uploads matching + values to GitHub Actions secrets. + +Value resolution order: + 1. Current shell environment + 2. Variables loaded from --env-file entries + 3. Variables loaded from deployment/.env, if it exists + +Requirements: + - gh CLI installed and authenticated + - Repository access to set GitHub secrets + +Examples: + scripts/import-prod-example-secrets.sh + scripts/import-prod-example-secrets.sh --dry-run + scripts/import-prod-example-secrets.sh --repo PEI-SecureLearning/Core + scripts/import-prod-example-secrets.sh --env-file deployment/.env +EOF +} + +require_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Missing required command: $1" >&2 + exit 1 + fi +} + +trim() { + local value="$1" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + printf '%s' "$value" +} + +strip_quotes() { + local value="$1" + if [[ "$value" == \"*\" && "$value" == *\" ]]; then + value="${value:1:${#value}-2}" + elif [[ "$value" == \'*\' && "$value" == *\' ]]; then + value="${value:1:${#value}-2}" + fi + printf '%s' "$value" +} + +parse_env_file() { + local env_file="$1" + [[ -f "$env_file" ]] || return 0 + + while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do + local line + line="$(trim "$raw_line")" + + [[ -n "$line" ]] || continue + [[ "$line" =~ ^# ]] && continue + [[ "$line" == export\ * ]] && line="${line#export }" + [[ "$line" == *=* ]] || continue + + local key="${line%%=*}" + local value="${line#*=}" + key="$(trim "$key")" + value="$(trim "$value")" + value="$(strip_quotes "$value")" + + [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue + FILE_VALUES["$key"]="$value" + done < "$env_file" +} + +resolve_repo() { + if [[ -n "$REPO" ]]; then + printf '%s' "$REPO" + return 0 + fi + + local remote_url + remote_url="$(git -C "$ROOT_DIR" remote get-url origin 2>/dev/null || true)" + + if [[ "$remote_url" =~ github\.com[:/]([^/]+/[^/.]+)(\.git)?$ ]]; then + printf '%s' "${BASH_REMATCH[1]}" + return 0 + fi + + echo "Could not determine GitHub repository from origin remote. Pass --repo owner/name." >&2 + exit 1 +} + +REPO="" +DRY_RUN="false" +declare -a ENV_FILES=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --repo) + REPO="${2:-}" + shift 2 + ;; + --env-file) + ENV_FILES+=("${2:-}") + shift 2 + ;; + --dry-run) + DRY_RUN="true" + shift + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +require_cmd git +require_cmd gh + +if [[ ${#ENV_FILES[@]} -eq 0 ]]; then + [[ -f "$ROOT_DIR/deployment/.env" ]] && ENV_FILES+=("$ROOT_DIR/deployment/.env") +fi + +declare -A FILE_VALUES=() +declare -A PROD_KEYS=() + +for env_file in "${ENV_FILES[@]}"; do + parse_env_file "$env_file" +done + +EXAMPLE_FILE="$ROOT_DIR/deployment/.env.prod.example" + +if [[ ! -f "$EXAMPLE_FILE" ]]; then + echo "Missing $EXAMPLE_FILE" >&2 + exit 1 +fi + +while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do + line="$(trim "$raw_line")" + [[ -n "$line" ]] || continue + [[ "$line" =~ ^# ]] && continue + [[ "$line" == export\ * ]] && line="${line#export }" + [[ "$line" == *=* ]] || continue + + key="${line%%=*}" + key="$(trim "$key")" + [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue + PROD_KEYS["$key"]=1 +done < "$EXAMPLE_FILE" + +if [[ ${#PROD_KEYS[@]} -eq 0 ]]; then + echo "No secret keys found in $EXAMPLE_FILE" >&2 + exit 1 +fi + +REPO="$(resolve_repo)" + +declare -a uploaded=() +declare -a missing=() + +while IFS= read -r key; do + value="${!key-}" + if [[ -z "${value}" && -v FILE_VALUES["$key"] ]]; then + value="${FILE_VALUES[$key]}" + fi + + if [[ -z "${value}" ]]; then + missing+=("$key") + continue + fi + + if [[ "$DRY_RUN" == "true" ]]; then + echo "[dry-run] would set secret $key in $REPO" + else + gh secret set "$key" --repo "$REPO" --body "$value" + echo "Set secret $key in $REPO" + fi + uploaded+=("$key") +done < <(printf '%s\n' "${!PROD_KEYS[@]}" | sort) + +echo +echo "Processed ${#PROD_KEYS[@]} secret name(s)." +echo "Uploaded ${#uploaded[@]} secret(s)." + +if [[ ${#missing[@]} -gt 0 ]]; then + echo "Skipped ${#missing[@]} missing value(s):" + printf ' %s\n' "${missing[@]}" +fi diff --git a/web/src/lib/use-query.ts b/web/src/lib/use-query.ts index 682872e5..d97c9c70 100644 --- a/web/src/lib/use-query.ts +++ b/web/src/lib/use-query.ts @@ -7,6 +7,7 @@ export function useQuery({ queryKey, queryFn, enabled = true }: { queryKey: a const [isError, setIsError] = useState(false) const [error, setError] = useState(null) const queryFnRef = useRef(queryFn) + const queryKeyRef = JSON.stringify(queryKey) useEffect(() => { queryFnRef.current = queryFn @@ -31,7 +32,7 @@ export function useQuery({ queryKey, queryFn, enabled = true }: { queryKey: a if (enabled) { void fetchData() } - }, [queryKey, enabled, fetchData]) + }, [queryKeyRef, enabled, fetchData]) useEffect(() => { const handleInvalidate = () => { From 3a1ef526eaec847ab3d0df64be9e3f1957399661 Mon Sep 17 00:00:00 2001 From: sle3pyy Date: Sat, 28 Mar 2026 00:16:30 +0000 Subject: [PATCH 87/87] Fix: wrong api call paht in the smtp --- .github/workflows/CD-workflow.yml | 2 ++ deployment/.env.prod.example | 1 + deployment/docker-compose.yml | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CD-workflow.yml b/.github/workflows/CD-workflow.yml index 4da01bc7..eebd901e 100644 --- a/.github/workflows/CD-workflow.yml +++ b/.github/workflows/CD-workflow.yml @@ -43,6 +43,7 @@ jobs: CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }} KEYCLOAK_INTERNAL_URL: ${{ secrets.KEYCLOAK_INTERNAL_URL }} API_INTERNAL_URL: ${{ secrets.API_INTERNAL_URL }} + PUBLIC_BASE_URL: ${{ secrets.PUBLIC_BASE_URL }} KC_HTTP_RELATIVE_PATH: ${{ secrets.KC_HTTP_RELATIVE_PATH }} VITE_BASE_PATH: ${{ secrets.VITE_BASE_PATH }} KC_HOSTNAME: ${{ secrets.KC_HOSTNAME }} @@ -99,6 +100,7 @@ jobs: CLIENT_SECRET=${CLIENT_SECRET} KEYCLOAK_INTERNAL_URL=${KEYCLOAK_INTERNAL_URL} API_INTERNAL_URL=${API_INTERNAL_URL} + PUBLIC_BASE_URL=${PUBLIC_BASE_URL} KC_HTTP_RELATIVE_PATH=${KC_HTTP_RELATIVE_PATH} VITE_BASE_PATH=${VITE_BASE_PATH} KC_HOSTNAME=${KC_HOSTNAME} diff --git a/deployment/.env.prod.example b/deployment/.env.prod.example index 84d89ca5..91e5130d 100644 --- a/deployment/.env.prod.example +++ b/deployment/.env.prod.example @@ -47,6 +47,7 @@ KEYCLOAK_INTERNAL_URL=http://keycloak:8080/kc API_INTERNAL_URL=http://api:80 # Production URLs +PUBLIC_BASE_URL=https://mednat.ieeta.pt:9071 KC_HTTP_RELATIVE_PATH=/kc VITE_BASE_PATH=/app/ KC_HOSTNAME=mednat.ieeta.pt diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 43ade710..10c2b3f3 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -138,7 +138,7 @@ services: RATE_LIMITER_MAX_REQUESTS: ${RATE_LIMITER_MAX_REQUESTS} RATE_LIMITER_TIME_WINDOW_SECONDS: ${RATE_LIMITER_TIME_WINDOW_SECONDS} API_INTERNAL_URL: ${API_INTERNAL_URL} - API_URL: ${API_URL} + API_URL: ${PUBLIC_BASE_URL} depends_on: rabbitmq: condition: service_healthy