From 7ec5bfe94eb58822d12c6f83f810166bce308dbe Mon Sep 17 00:00:00 2001 From: u00lipp Date: Mon, 27 Oct 2025 10:34:34 +0100 Subject: [PATCH 01/28] fix: image --- Dockerfile | 3 +++ deploy/docker-entrypoint.sh => entrypoint.sh | 0 2 files changed, 3 insertions(+) rename deploy/docker-entrypoint.sh => entrypoint.sh (100%) diff --git a/Dockerfile b/Dockerfile index 6365e0e..a6f33d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,9 @@ # Provide php alias RUN ln -sf /usr/bin/php82 /usr/bin/php + # Add custom PHP logging configuration +COPY zz-logging.ini /etc/php82/conf.d/ + # Create user, dirs, and sockets RUN addgroup -S web && adduser -S web -G web \ && mkdir -p /run/nginx /run/php /var/www/html /var/log/supervisor diff --git a/deploy/docker-entrypoint.sh b/entrypoint.sh similarity index 100% rename from deploy/docker-entrypoint.sh rename to entrypoint.sh From bd2099db7c46e0f30a1f9c6fd775a38c81d3be5e Mon Sep 17 00:00:00 2001 From: u00lipp Date: Mon, 27 Oct 2025 10:41:51 +0100 Subject: [PATCH 02/28] fix: php config --- Dockerfile | 2 +- entrypoint.sh => deploy/docker-entrypoint.sh | 0 deploy/php-fpm/{zz-logging.conf => zz-logging.ini} | 0 deploy/php-fpm/zz-socket.conf | 3 --- 4 files changed, 1 insertion(+), 4 deletions(-) rename entrypoint.sh => deploy/docker-entrypoint.sh (100%) rename deploy/php-fpm/{zz-logging.conf => zz-logging.ini} (100%) delete mode 100644 deploy/php-fpm/zz-socket.conf diff --git a/Dockerfile b/Dockerfile index a6f33d7..bc46705 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ RUN ln -sf /usr/bin/php82 /usr/bin/php # Add custom PHP logging configuration -COPY zz-logging.ini /etc/php82/conf.d/ + COPY deploy/php-fpm/zz-logging.ini /etc/php82/conf.d/ # Create user, dirs, and sockets RUN addgroup -S web && adduser -S web -G web \ diff --git a/entrypoint.sh b/deploy/docker-entrypoint.sh similarity index 100% rename from entrypoint.sh rename to deploy/docker-entrypoint.sh diff --git a/deploy/php-fpm/zz-logging.conf b/deploy/php-fpm/zz-logging.ini similarity index 100% rename from deploy/php-fpm/zz-logging.conf rename to deploy/php-fpm/zz-logging.ini diff --git a/deploy/php-fpm/zz-socket.conf b/deploy/php-fpm/zz-socket.conf deleted file mode 100644 index 7631ab7..0000000 --- a/deploy/php-fpm/zz-socket.conf +++ /dev/null @@ -1,3 +0,0 @@ -listen = 127.0.0.1:9000 -user = nginx -group = nginx From c8d9c1cfd5f36169de6dee602efe7b638d9407bc Mon Sep 17 00:00:00 2001 From: u00lipp Date: Mon, 27 Oct 2025 11:06:43 +0100 Subject: [PATCH 03/28] fix: nginx --- Dockerfile | 3 - deploy/nginx/nginx.default.conf.template | 70 ++++++++++++++++++++---- deploy/supervisord.conf | 2 + 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/Dockerfile b/Dockerfile index bc46705..a350796 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,9 +27,6 @@ # Add custom PHP logging configuration COPY deploy/php-fpm/zz-logging.ini /etc/php82/conf.d/ - # Create user, dirs, and sockets - RUN addgroup -S web && adduser -S web -G web \ - && mkdir -p /run/nginx /run/php /var/www/html /var/log/supervisor # Configure PHP-FPM socket RUN sed -i 's|^listen = 127\\.0\\.0\\.1:9000|listen = /run/php/php-fpm.sock|' /etc/php82/php-fpm.d/www.conf \ diff --git a/deploy/nginx/nginx.default.conf.template b/deploy/nginx/nginx.default.conf.template index f115970..1497cb2 100644 --- a/deploy/nginx/nginx.default.conf.template +++ b/deploy/nginx/nginx.default.conf.template @@ -5,42 +5,88 @@ server { root /var/www/html/public; index index.php; - # Cache-Control für statische Dateien - location ~* \.(?:ico|css|js|gif|jpe?g|png|svg|woff2?|eot|ttf|otf|webp|avif)$ { + # ------------------------------------------------- + # Redirect all HTTP requests to HTTPS (optional, remove if TLS is handled upstream) + # ------------------------------------------------- + # return 301 https://$host$request_uri; + + # ------------------------------------------------- + # Livewire + Laravel routes (must come before static block) + # ------------------------------------------------- + location ^~ /livewire/ { + try_files $uri /index.php?$query_string; + expires off; + add_header Cache-Control "no-store"; + } + + # ------------------------------------------------- + # Static assets with long caching + # ------------------------------------------------- + location ~* \.(?:ico|css|js|gif|jpe?g|png|svg|woff2?|eot|ttf|otf|webp|avif)(\?.*)?$ { expires 7d; add_header Cache-Control "public, max-age=604800, immutable"; try_files $uri =404; access_log off; } - # Haupt-Entry-Point (Laravel) + # ------------------------------------------------- + # Main Laravel entry point + # ------------------------------------------------- location / { try_files $uri $uri/ /index.php?$query_string; } - # PHP-FPM Verarbeitung + # ------------------------------------------------- + # PHP-FPM configuration (matches your Alpine socket setup) + # ------------------------------------------------- location ~ \.php$ { include fastcgi_params; fastcgi_index index.php; - fastcgi_pass 127.0.0.1:9000; + fastcgi_pass unix:/run/php/php-fpm.sock; # <-- Matches Dockerfile sed change fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_read_timeout 60s; fastcgi_buffering on; fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; - # Forward the X-Forwarded-Proto header - fastcgi_param HTTPS $http_x_forwarded_proto; + # --- Proxy headers for HTTPS and load balancers --- + fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; + fastcgi_param HTTP_X_FORWARDED_HOST $http_host; + fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for; + + # --- Tell PHP it's HTTPS when forwarded --- + set $https_flag ""; + if ($http_x_forwarded_proto = "https") { set $https_flag "on"; } + fastcgi_param HTTPS $https_flag; } - # Maximale Client Upload-Größe + # ------------------------------------------------- + # Upload limits + # ------------------------------------------------- client_max_body_size 64m; - # Sicherheits-Header + # ------------------------------------------------- + # Security headers + # ------------------------------------------------- add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - # Gzip-Kompression -} \ No newline at end of file + # ------------------------------------------------- + # Gzip compression + # ------------------------------------------------- + gzip on; + gzip_types + text/plain + text/css + application/json + application/javascript + text/xml + application/xml + application/xml+rss + image/svg+xml; + gzip_min_length 1000; + gzip_proxied any; + gzip_vary on; +} diff --git a/deploy/supervisord.conf b/deploy/supervisord.conf index 58394c8..4042048 100644 --- a/deploy/supervisord.conf +++ b/deploy/supervisord.conf @@ -13,6 +13,7 @@ stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 stderr_logfile=/dev/fd/2 stderr_logfile_maxbytes=0 +user=nginx [program:nginx] command=/usr/sbin/nginx -g 'daemon off;' @@ -24,3 +25,4 @@ stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 stderr_logfile=/dev/fd/2 stderr_logfile_maxbytes=0 +user=nginx From 97a144baaadb19dd18bf11f47d0a918cabd58088 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Mon, 27 Oct 2025 11:09:05 +0100 Subject: [PATCH 04/28] fix: missing logs --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index a350796..34fc1c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,8 @@ # Add custom PHP logging configuration COPY deploy/php-fpm/zz-logging.ini /etc/php82/conf.d/ + # Create user, dirs, and sockets + RUN mkdir -p /run/nginx /run/php /var/www/html /var/log/supervisor # Configure PHP-FPM socket RUN sed -i 's|^listen = 127\\.0\\.0\\.1:9000|listen = /run/php/php-fpm.sock|' /etc/php82/php-fpm.d/www.conf \ From a207b01c1206b601b9aad388681b444a3c51b2fd Mon Sep 17 00:00:00 2001 From: u00lipp Date: Mon, 27 Oct 2025 12:09:32 +0100 Subject: [PATCH 05/28] fix: permissions --- Dockerfile | 10 ++++++++-- deploy/nginx/nginx.conf | 15 ++++++++------- deploy/supervisord.conf | 2 -- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 34fc1c2..5f97d80 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,8 +53,14 @@ # Permissions for Laravel writable directories RUN mkdir -p storage bootstrap/cache \ && chown -R nginx:nginx storage bootstrap/cache \ - && chmod -R ug+rwX storage bootstrap/cache - + && chmod -R ug+rwX storage bootstrap/cache + +# Create Nginx error log file with correct permissions +RUN mkdir -p /var/log/nginx \ + && touch /var/log/nginx/error.log \ + && chown -R nginx:nginx /var/log/nginx \ + && chown -R nginx:nginx /var/log/php82 \ + && chmod -R 664 /var/log/nginx/error.log # Build frontend assets RUN npm ci && npm run build diff --git a/deploy/nginx/nginx.conf b/deploy/nginx/nginx.conf index b07e7d0..c0e766c 100644 --- a/deploy/nginx/nginx.conf +++ b/deploy/nginx/nginx.conf @@ -1,7 +1,9 @@ -user nginx; +user nginx; worker_processes auto; -error_log /var/log/nginx/error.log warn; -pid /run/nginx.pid; + +# Send logs to container fds (works regardless of privileges) +error_log /proc/self/fd/2 warn; +pid /run/nginx/nginx.pid; events { worker_connections 1024; @@ -15,14 +17,13 @@ http { '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; - access_log /dev/stdout main; - error_log /dev/stderr warn; + access_log /proc/self/fd/1 main; + # (No extra error_log here; the top-level one above is enough) sendfile on; keepalive_timeout 65; - # Server Blöcke sind NICHT hier! - # Stattdessen in: + # Include vhosts (your Laravel server block lives in one of these) include /etc/nginx/conf.d/*.conf; include /etc/nginx/http.d/*.conf; } diff --git a/deploy/supervisord.conf b/deploy/supervisord.conf index 4042048..58394c8 100644 --- a/deploy/supervisord.conf +++ b/deploy/supervisord.conf @@ -13,7 +13,6 @@ stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 stderr_logfile=/dev/fd/2 stderr_logfile_maxbytes=0 -user=nginx [program:nginx] command=/usr/sbin/nginx -g 'daemon off;' @@ -25,4 +24,3 @@ stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 stderr_logfile=/dev/fd/2 stderr_logfile_maxbytes=0 -user=nginx From aa6214f909c3fb7c2cedf4f49b49b1efc1dcc9db Mon Sep 17 00:00:00 2001 From: u00lipp Date: Mon, 27 Oct 2025 12:14:15 +0100 Subject: [PATCH 06/28] fix: php --- deploy/nginx/nginx.default.conf.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/nginx/nginx.default.conf.template b/deploy/nginx/nginx.default.conf.template index 1497cb2..9c37f54 100644 --- a/deploy/nginx/nginx.default.conf.template +++ b/deploy/nginx/nginx.default.conf.template @@ -42,7 +42,7 @@ server { location ~ \.php$ { include fastcgi_params; fastcgi_index index.php; - fastcgi_pass unix:/run/php/php-fpm.sock; # <-- Matches Dockerfile sed change + fastcgi_pass 127.0.0.1:9000; # <-- switch from socket to TCP fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $document_root; From bd1697b5b92357b04c91f47a084c8928299e888b Mon Sep 17 00:00:00 2001 From: u00lipp Date: Wed, 29 Oct 2025 10:50:51 +0100 Subject: [PATCH 07/28] feat: add docker image pipeline --- .github/workflows/docker-build.yml | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/docker-build.yml diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..64a1b88 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,50 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + name: Build and Push Docker Image + runs-on: docker + + env: + DOCKER_REGISTRY: git.trai-infra.comstack.de + DOCKER_IMAGE: marian.lippitz/tap-frontend + DOCKER_USERNAME: $DOCKER_USERNAME + DOCKER_PASSWORD: $DOCKER_PASSWORD + + steps: + # Step 1: Checkout the repository + - name: Checkout Code + uses: actions/checkout@v3 + + # Step 2: Validate Conventional Commits + - name: Validate Conventional Commits + run: | + git log -1 --pretty=%B | grep -Eq "^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\(.+\))?: .+" + shell: bash + + # Step 3: Extract Variables + - name: Extract Variables + id: vars + run: | + echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + echo "BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_ENV + echo "DOCKER_TAG=${BRANCH_NAME}-${COMMIT_HASH}" >> $GITHUB_ENV + + # Step 4: Build the Docker image + - name: Build Docker Image + run: | + docker build -t $DOCKER_REGISTRY/$DOCKER_IMAGE:$DOCKER_TAG . + + # Step 5: Push the Docker image to a registry + - name: Push Docker Image + run: | + echo "$DOCKER_PASSWORD" | docker login $DOCKER_REGISTRY -u "$DOCKER_USERNAME" --password-stdin + docker push $DOCKER_REGISTRY/$DOCKER_IMAGE:$DOCKER_TAG \ No newline at end of file From a79ab98433a9d8f83d3879288dd2bf3d3a0c1306 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Wed, 29 Oct 2025 10:52:13 +0100 Subject: [PATCH 08/28] fix: runner --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 64a1b88..0fab8e1 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -11,7 +11,7 @@ on: jobs: build: name: Build and Push Docker Image - runs-on: docker + runs-on: ubuntu-latest env: DOCKER_REGISTRY: git.trai-infra.comstack.de From e6f691e8b418fb4949fb3033e3e6eaee7fa0e4a6 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Wed, 29 Oct 2025 10:55:47 +0100 Subject: [PATCH 09/28] fix: tag --- .github/workflows/docker-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 0fab8e1..60d7a18 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -41,10 +41,10 @@ jobs: # Step 4: Build the Docker image - name: Build Docker Image run: | - docker build -t $DOCKER_REGISTRY/$DOCKER_IMAGE:$DOCKER_TAG . + docker build -t $DOCKER_REGISTRY/$DOCKER_IMAGE:latest . # Step 5: Push the Docker image to a registry - name: Push Docker Image run: | echo "$DOCKER_PASSWORD" | docker login $DOCKER_REGISTRY -u "$DOCKER_USERNAME" --password-stdin - docker push $DOCKER_REGISTRY/$DOCKER_IMAGE:$DOCKER_TAG \ No newline at end of file + docker push $DOCKER_REGISTRY/$DOCKER_IMAGE:latest \ No newline at end of file From 871dbfa9db75c88b74e6e8b74933465a9fa91ec7 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Wed, 29 Oct 2025 10:59:30 +0100 Subject: [PATCH 10/28] fix: cicd --- .github/workflows/docker-build.yml | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 60d7a18..abe03f1 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -16,21 +16,19 @@ jobs: env: DOCKER_REGISTRY: git.trai-infra.comstack.de DOCKER_IMAGE: marian.lippitz/tap-frontend - DOCKER_USERNAME: $DOCKER_USERNAME - DOCKER_PASSWORD: $DOCKER_PASSWORD + # You can set the username directly here, or as an environment secret + # If using secrets, remove the line below and set in GitHub secrets + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} steps: - # Step 1: Checkout the repository - name: Checkout Code uses: actions/checkout@v3 - # Step 2: Validate Conventional Commits - name: Validate Conventional Commits run: | git log -1 --pretty=%B | grep -Eq "^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\(.+\))?: .+" shell: bash - # Step 3: Extract Variables - name: Extract Variables id: vars run: | @@ -38,13 +36,18 @@ jobs: echo "BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_ENV echo "DOCKER_TAG=${BRANCH_NAME}-${COMMIT_HASH}" >> $GITHUB_ENV - # Step 4: Build the Docker image - - name: Build Docker Image - run: | - docker build -t $DOCKER_REGISTRY/$DOCKER_IMAGE:latest . - - # Step 5: Push the Docker image to a registry - - name: Push Docker Image + - name: Docker Login + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} run: | echo "$DOCKER_PASSWORD" | docker login $DOCKER_REGISTRY -u "$DOCKER_USERNAME" --password-stdin - docker push $DOCKER_REGISTRY/$DOCKER_IMAGE:latest \ No newline at end of file + + - name: Build Docker Image + run: | + docker build -t "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" -t "$DOCKER_REGISTRY/$DOCKER_IMAGE:$DOCKER_TAG" . + + - name: Push Docker Image + run: | + docker push "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" + docker push "$DOCKER_REGISTRY/$DOCKER_IMAGE:$DOCKER_TAG" From 1cf75b58ad487f73aec8896ad3757aae9b0e1175 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Wed, 29 Oct 2025 11:02:46 +0100 Subject: [PATCH 11/28] feat: add semantic versioniong --- .github/workflows/docker-build.yml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index abe03f1..4f335a4 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -10,31 +10,32 @@ on: jobs: build: - name: Build and Push Docker Image runs-on: ubuntu-latest env: DOCKER_REGISTRY: git.trai-infra.comstack.de DOCKER_IMAGE: marian.lippitz/tap-frontend - # You can set the username directly here, or as an environment secret - # If using secrets, remove the line below and set in GitHub secrets - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} steps: - - name: Checkout Code - uses: actions/checkout@v3 + - uses: actions/checkout@v3 - name: Validate Conventional Commits run: | git log -1 --pretty=%B | grep -Eq "^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\(.+\))?: .+" shell: bash + - name: Calculate Semantic Version tag + id: semver + uses: gandarez/semver-action@v2.0.0 + with: + main_branch_name: main + - name: Extract Variables id: vars run: | echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV echo "BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_ENV - echo "DOCKER_TAG=${BRANCH_NAME}-${COMMIT_HASH}" >> $GITHUB_ENV + echo "SEMVER_TAG=${{ steps.semver.outputs.semver_tag }}" >> $GITHUB_ENV - name: Docker Login env: @@ -45,9 +46,9 @@ jobs: - name: Build Docker Image run: | - docker build -t "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" -t "$DOCKER_REGISTRY/$DOCKER_IMAGE:$DOCKER_TAG" . + docker build -t "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" -t "$DOCKER_REGISTRY/$DOCKER_IMAGE:$SEMVER_TAG" . - - name: Push Docker Image + - name: Push Docker Images run: | docker push "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" - docker push "$DOCKER_REGISTRY/$DOCKER_IMAGE:$DOCKER_TAG" + docker push "$DOCKER_REGISTRY/$DOCKER_IMAGE:$SEMVER_TAG" From b083163efb2afb06bf61ed8091985a0debfa6bf7 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Wed, 29 Oct 2025 11:06:10 +0100 Subject: [PATCH 12/28] fix: semantic versionion --- .github/workflows/docker-build.yml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 4f335a4..7b6929e 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -17,27 +17,38 @@ jobs: DOCKER_IMAGE: marian.lippitz/tap-frontend steps: - - uses: actions/checkout@v3 + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 # Fetch full history for branch info - name: Validate Conventional Commits + id: validate_commits run: | - git log -1 --pretty=%B | grep -Eq "^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\(.+\))?: .+" - shell: bash + if git log -1 --pretty=%B | grep -Eq "^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\(.+\))?: .+"; then + echo "valid=true" >> $GITHUB_ENV + else + echo "valid=false" >> $GITHUB_ENV + fi + # Only run semantic version calculation if commit message is valid - name: Calculate Semantic Version tag + if: env.valid == 'true' id: semver uses: gandarez/semver-action@v2.0.0 with: main_branch_name: main + source_branch: ${{ github.head_ref || github.ref }} - name: Extract Variables - id: vars + if: env.valid == 'true' run: | echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV echo "BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_ENV echo "SEMVER_TAG=${{ steps.semver.outputs.semver_tag }}" >> $GITHUB_ENV - name: Docker Login + if: env.valid == 'true' env: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} @@ -45,10 +56,12 @@ jobs: echo "$DOCKER_PASSWORD" | docker login $DOCKER_REGISTRY -u "$DOCKER_USERNAME" --password-stdin - name: Build Docker Image + if: env.valid == 'true' run: | docker build -t "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" -t "$DOCKER_REGISTRY/$DOCKER_IMAGE:$SEMVER_TAG" . - name: Push Docker Images + if: env.valid == 'true' run: | docker push "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" docker push "$DOCKER_REGISTRY/$DOCKER_IMAGE:$SEMVER_TAG" From d6619a5a26b4aa93240b470211a7a0b487690009 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Wed, 29 Oct 2025 11:10:25 +0100 Subject: [PATCH 13/28] fix: ci --- .github/workflows/docker-build.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 7b6929e..a11103d 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -8,6 +8,10 @@ on: branches: - main +concurrency: + group: build-${{ github.ref }} + cancel-in-progress: false + jobs: build: runs-on: ubuntu-latest @@ -20,7 +24,7 @@ jobs: - name: Checkout Code uses: actions/checkout@v3 with: - fetch-depth: 0 # Fetch full history for branch info + fetch-depth: 0 # Full history for branch info - name: Validate Conventional Commits id: validate_commits @@ -31,20 +35,22 @@ jobs: echo "valid=false" >> $GITHUB_ENV fi - # Only run semantic version calculation if commit message is valid + - name: Determine Branch Name + run: | + echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV + - name: Calculate Semantic Version tag if: env.valid == 'true' id: semver uses: gandarez/semver-action@v2.0.0 with: main_branch_name: main - source_branch: ${{ github.head_ref || github.ref }} + source_branch: ${{ env.BRANCH_NAME }} - name: Extract Variables if: env.valid == 'true' run: | echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - echo "BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_ENV echo "SEMVER_TAG=${{ steps.semver.outputs.semver_tag }}" >> $GITHUB_ENV - name: Docker Login From afaf73e17a7ef9c5b496a0da199546c1dac8e2e2 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Wed, 29 Oct 2025 11:22:57 +0100 Subject: [PATCH 14/28] fix: build stage --- .github/workflows/docker-build.yml | 47 +++++++----------------------- 1 file changed, 11 insertions(+), 36 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index a11103d..0328444 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -1,16 +1,9 @@ -name: Build and Push Docker Image +name: Build and Push Docker Image only on Conventional Commits on: push: branches: - main - pull_request: - branches: - - main - -concurrency: - group: build-${{ github.ref }} - cancel-in-progress: false jobs: build: @@ -23,38 +16,23 @@ jobs: steps: - name: Checkout Code uses: actions/checkout@v3 - with: - fetch-depth: 0 # Full history for branch info - - name: Validate Conventional Commits - id: validate_commits + - name: Validate Conventional Commit + id: validate run: | if git log -1 --pretty=%B | grep -Eq "^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\(.+\))?: .+"; then - echo "valid=true" >> $GITHUB_ENV + echo "valid=true" >> $GITHUB_OUTPUT else - echo "valid=false" >> $GITHUB_ENV + echo "valid=false" >> $GITHUB_OUTPUT fi - - name: Determine Branch Name + - name: Stop if not a Conventional Commit + if: ${{ steps.validate.outputs.valid == 'false' }} run: | - echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV - - - name: Calculate Semantic Version tag - if: env.valid == 'true' - id: semver - uses: gandarez/semver-action@v2.0.0 - with: - main_branch_name: main - source_branch: ${{ env.BRANCH_NAME }} - - - name: Extract Variables - if: env.valid == 'true' - run: | - echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - echo "SEMVER_TAG=${{ steps.semver.outputs.semver_tag }}" >> $GITHUB_ENV + echo "Latest commit is not a valid Conventional Commit. Skipping build." + exit 0 - name: Docker Login - if: env.valid == 'true' env: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} @@ -62,12 +40,9 @@ jobs: echo "$DOCKER_PASSWORD" | docker login $DOCKER_REGISTRY -u "$DOCKER_USERNAME" --password-stdin - name: Build Docker Image - if: env.valid == 'true' run: | - docker build -t "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" -t "$DOCKER_REGISTRY/$DOCKER_IMAGE:$SEMVER_TAG" . + docker build -t "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" . - - name: Push Docker Images - if: env.valid == 'true' + - name: Push Docker Image run: | docker push "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" - docker push "$DOCKER_REGISTRY/$DOCKER_IMAGE:$SEMVER_TAG" From e005bd330e26bcfe208fc75668ad879e45ec78f5 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Thu, 30 Oct 2025 10:48:36 +0100 Subject: [PATCH 15/28] feat: add ability to inject a htpasswod file --- deploy/docker-entrypoint.sh | 28 ++++++----- deploy/nginx/nginx.default.conf.template | 1 + docker/healthcheck.sh | 12 ----- docker/nginx.conf | 29 ------------ docker/site.conf | 60 ------------------------ 5 files changed, 17 insertions(+), 113 deletions(-) delete mode 100644 docker/healthcheck.sh delete mode 100644 docker/nginx.conf delete mode 100644 docker/site.conf diff --git a/deploy/docker-entrypoint.sh b/deploy/docker-entrypoint.sh index 99e46cc..f69adbd 100644 --- a/deploy/docker-entrypoint.sh +++ b/deploy/docker-entrypoint.sh @@ -1,15 +1,22 @@ #!/bin/sh -set -eu - -SERVER_NAME="${SERVER_NAME:-$(hostname -f 2>/dev/null || hostname)}" -export SERVER_NAME - -# Ensure dirs -mkdir -p /etc/nginx/http.d /etc/nginx/conf.d /run/nginx /run/php +set -e # Render vhost ONLY into http.d so it's inside http{} context if [ -f /etc/nginx/templates/default.conf.template ]; then - envsubst '$SERVER_NAME' < /etc/nginx/templates/default.conf.template > /etc/nginx/http.d/default.conf + if [ "$ENABLE_BASIC_AUTH" = "true" ]; then + echo "[entrypoint] Enabling basic authentication..." + if [ -f /etc/nginx/htpasswd ]; then + envsubst '$SERVER_NAME' < /etc/nginx/templates/default.conf.template | \ + sed '/location \/ {/a \\t\tauth_basic "Restricted Content";\n\t\tauth_basic_user_file /etc/nginx/htpasswd;' \ + > /etc/nginx/http.d/default.conf + else + echo "[entrypoint] ERROR: htpasswd file not found at /etc/nginx/htpasswd" >&2 + exit 1 + fi + else + echo "[entrypoint] Basic authentication is disabled." + envsubst '$SERVER_NAME' < /etc/nginx/templates/default.conf.template > /etc/nginx/http.d/default.conf + fi else echo "[entrypoint] ERROR: missing /etc/nginx/templates/default.conf.template" >&2 exit 1 @@ -21,7 +28,4 @@ nginx -t || { echo "[entrypoint] nginx config test failed" >&2; cat /var/log/ngi php artisan config:clear php artisan cache:clear -envsubst '$SERVER_NAME' < /etc/nginx/templates/default.conf.template > /etc/nginx/http.d/default.conf - -exec /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf - \ No newline at end of file +exec /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf \ No newline at end of file diff --git a/deploy/nginx/nginx.default.conf.template b/deploy/nginx/nginx.default.conf.template index 9c37f54..2eaa5fd 100644 --- a/deploy/nginx/nginx.default.conf.template +++ b/deploy/nginx/nginx.default.conf.template @@ -34,6 +34,7 @@ server { # ------------------------------------------------- location / { try_files $uri $uri/ /index.php?$query_string; + ${BASIC_AUTH_DIRECTIVES} } # ------------------------------------------------- diff --git a/docker/healthcheck.sh b/docker/healthcheck.sh deleted file mode 100644 index 38ca447..0000000 --- a/docker/healthcheck.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -set -eu - -PORT="${APP_PORT:-8080}" - -# 1) App Health -curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null - -# 2) PHP-FPM Ping via Nginx -curl -fsS "http://127.0.0.1:${PORT}/fpm-ping" >/dev/null - -exit 0 diff --git a/docker/nginx.conf b/docker/nginx.conf deleted file mode 100644 index 2c9ef12..0000000 --- a/docker/nginx.conf +++ /dev/null @@ -1,29 +0,0 @@ -# keine 'user' Direktive im non-root Betrieb -worker_processes auto; -error_log /dev/stderr warn; -pid /tmp/nginx.pid; - -events { worker_connections 1024; } - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - access_log /dev/stdout main; - - sendfile on; - keepalive_timeout 65; - server_tokens off; - - # Temp/Cache-Pfade, die 'nginx' gehören - client_body_temp_path /var/lib/nginx/tmp/client_body 1 2; - proxy_temp_path /var/lib/nginx/tmp/proxy 1 2; - fastcgi_temp_path /var/lib/nginx/tmp/fastcgi 1 2; - scgi_temp_path /var/lib/nginx/tmp/scgi 1 2; - uwsgi_temp_path /var/lib/nginx/tmp/uwsgi 1 2; - - include /etc/nginx/conf.d/*.conf; -} diff --git a/docker/site.conf b/docker/site.conf deleted file mode 100644 index 55d893c..0000000 --- a/docker/site.conf +++ /dev/null @@ -1,60 +0,0 @@ -server { - listen 8080; - server_name localhost; - - root /var/www/html/public; - index index.php index.html; - - # Health checks - location = /healthz { - access_log off; - return 200 "ok\n"; - } - - # FPM ping/status (optional, secure in prod) - location = /fpm-ping { - access_log off; - include fastcgi_params; - fastcgi_pass 127.0.0.1:9000; - fastcgi_param SCRIPT_FILENAME $document_root/index.php; - fastcgi_param SCRIPT_NAME /ping; - fastcgi_param PATH_INFO /ping; - } - - location = /fpm-status { - access_log off; - include fastcgi_params; - fastcgi_pass 127.0.0.1:9000; - fastcgi_param SCRIPT_FILENAME $document_root/index.php; - fastcgi_param SCRIPT_NAME /status; - fastcgi_param PATH_INFO /status; - } - - # Main app - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - # PHP handling - location ~ \.php$ { - include fastcgi_params; - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_read_timeout 60s; - fastcgi_buffers 16 16k; - } - - # Deny hidden files - location ~ /\.ht { - deny all; - } - - # Cache static assets - location ~* \.(?:css|js|jpg|jpeg|gif|png|svg|ico|webp|woff2?)$ { - access_log off; - expires 7d; - add_header Cache-Control "public"; - try_files $uri =404; - } -} From 259dcab1b5d0497ed0ac1ba66332eacc90a3f907 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Thu, 30 Oct 2025 10:54:32 +0100 Subject: [PATCH 16/28] fix: replacement conditions# --- deploy/docker-entrypoint.sh | 40 +++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/deploy/docker-entrypoint.sh b/deploy/docker-entrypoint.sh index f69adbd..472d156 100644 --- a/deploy/docker-entrypoint.sh +++ b/deploy/docker-entrypoint.sh @@ -1,31 +1,41 @@ #!/bin/sh set -e +# Ensure SERVER_NAME is set +if [ -z "$SERVER_NAME" ]; then + echo "[entrypoint] SERVER_NAME is not set. Using default value: localhost" + SERVER_NAME="localhost" +else + echo "[entrypoint] Using SERVER_NAME=$SERVER_NAME" +fi + # Render vhost ONLY into http.d so it's inside http{} context if [ -f /etc/nginx/templates/default.conf.template ]; then - if [ "$ENABLE_BASIC_AUTH" = "true" ]; then - echo "[entrypoint] Enabling basic authentication..." - if [ -f /etc/nginx/htpasswd ]; then - envsubst '$SERVER_NAME' < /etc/nginx/templates/default.conf.template | \ - sed '/location \/ {/a \\t\tauth_basic "Restricted Content";\n\t\tauth_basic_user_file /etc/nginx/htpasswd;' \ - > /etc/nginx/http.d/default.conf - else - echo "[entrypoint] ERROR: htpasswd file not found at /etc/nginx/htpasswd" >&2 - exit 1 - fi - else - echo "[entrypoint] Basic authentication is disabled." - envsubst '$SERVER_NAME' < /etc/nginx/templates/default.conf.template > /etc/nginx/http.d/default.conf - fi + envsubst '$SERVER_NAME' < /etc/nginx/templates/default.conf.template > /etc/nginx/http.d/default.conf else echo "[entrypoint] ERROR: missing /etc/nginx/templates/default.conf.template" >&2 exit 1 fi -echo "[entrypoint] Using SERVER_NAME=$SERVER_NAME" +# Add basic authentication if enabled +if [ "$ENABLE_BASIC_AUTH" = "true" ]; then + echo "[entrypoint] Enabling basic authentication..." + if [ -f /etc/nginx/htpasswd ]; then + sed -i '/location \/ {/a \\t\tauth_basic "Restricted Content";\n\t\tauth_basic_user_file /etc/nginx/htpasswd;' /etc/nginx/http.d/default.conf + else + echo "[entrypoint] ERROR: htpasswd file not found at /etc/nginx/htpasswd" >&2 + exit 1 + fi +else + echo "[entrypoint] Basic authentication is disabled." +fi + +# Test nginx configuration nginx -t || { echo "[entrypoint] nginx config test failed" >&2; cat /var/log/nginx/error.log || true; exit 1; } +# Clear Laravel caches php artisan config:clear php artisan cache:clear +# Start supervisord exec /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf \ No newline at end of file From 399bf443941a674d5f0ad265d462c9491ee58803 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Thu, 30 Oct 2025 10:57:22 +0100 Subject: [PATCH 17/28] fix: servname --- deploy/docker-entrypoint.sh | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/deploy/docker-entrypoint.sh b/deploy/docker-entrypoint.sh index 472d156..34e1952 100644 --- a/deploy/docker-entrypoint.sh +++ b/deploy/docker-entrypoint.sh @@ -1,13 +1,8 @@ #!/bin/sh set -e -# Ensure SERVER_NAME is set -if [ -z "$SERVER_NAME" ]; then - echo "[entrypoint] SERVER_NAME is not set. Using default value: localhost" - SERVER_NAME="localhost" -else - echo "[entrypoint] Using SERVER_NAME=$SERVER_NAME" -fi +SERVER_NAME="${SERVER_NAME:-$(hostname -f 2>/dev/null || hostname)}" +export SERVER_NAME # Render vhost ONLY into http.d so it's inside http{} context if [ -f /etc/nginx/templates/default.conf.template ]; then From 13d912738da01aa928d67abb75201e03961e05c1 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Thu, 30 Oct 2025 11:00:53 +0100 Subject: [PATCH 18/28] fix: env --- deploy/docker-entrypoint.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/deploy/docker-entrypoint.sh b/deploy/docker-entrypoint.sh index 34e1952..c458434 100644 --- a/deploy/docker-entrypoint.sh +++ b/deploy/docker-entrypoint.sh @@ -16,6 +16,7 @@ fi if [ "$ENABLE_BASIC_AUTH" = "true" ]; then echo "[entrypoint] Enabling basic authentication..." if [ -f /etc/nginx/htpasswd ]; then + # Use sed to append basic auth directives inside the location block sed -i '/location \/ {/a \\t\tauth_basic "Restricted Content";\n\t\tauth_basic_user_file /etc/nginx/htpasswd;' /etc/nginx/http.d/default.conf else echo "[entrypoint] ERROR: htpasswd file not found at /etc/nginx/htpasswd" >&2 @@ -26,7 +27,11 @@ else fi # Test nginx configuration -nginx -t || { echo "[entrypoint] nginx config test failed" >&2; cat /var/log/nginx/error.log || true; exit 1; } +if ! nginx -t; then + echo "[entrypoint] nginx config test failed" >&2 + cat /var/log/nginx/error.log || true + exit 1 +fi # Clear Laravel caches php artisan config:clear From b7ff7602f63767aaab10a15ab9f95f207e95d144 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Thu, 30 Oct 2025 11:03:09 +0100 Subject: [PATCH 19/28] fix: nginx config --- deploy/docker-entrypoint.sh | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/deploy/docker-entrypoint.sh b/deploy/docker-entrypoint.sh index c458434..2de8bc1 100644 --- a/deploy/docker-entrypoint.sh +++ b/deploy/docker-entrypoint.sh @@ -1,29 +1,38 @@ #!/bin/sh set -e -SERVER_NAME="${SERVER_NAME:-$(hostname -f 2>/dev/null || hostname)}" -export SERVER_NAME - -# Render vhost ONLY into http.d so it's inside http{} context -if [ -f /etc/nginx/templates/default.conf.template ]; then - envsubst '$SERVER_NAME' < /etc/nginx/templates/default.conf.template > /etc/nginx/http.d/default.conf +# Ensure SERVER_NAME is set +if [ -z "$SERVER_NAME" ]; then + echo "[entrypoint] SERVER_NAME is not set. Using default value: localhost" + SERVER_NAME="localhost" else - echo "[entrypoint] ERROR: missing /etc/nginx/templates/default.conf.template" >&2 - exit 1 + echo "[entrypoint] Using SERVER_NAME=$SERVER_NAME" fi -# Add basic authentication if enabled +# Prepare BASIC_AUTH_DIRECTIVES if [ "$ENABLE_BASIC_AUTH" = "true" ]; then echo "[entrypoint] Enabling basic authentication..." if [ -f /etc/nginx/htpasswd ]; then - # Use sed to append basic auth directives inside the location block - sed -i '/location \/ {/a \\t\tauth_basic "Restricted Content";\n\t\tauth_basic_user_file /etc/nginx/htpasswd;' /etc/nginx/http.d/default.conf + BASIC_AUTH_DIRECTIVES=$(cat <&2 exit 1 fi else echo "[entrypoint] Basic authentication is disabled." + BASIC_AUTH_DIRECTIVES="" +fi + +# Render the NGINX configuration +if [ -f /etc/nginx/templates/default.conf.template ]; then + envsubst '$SERVER_NAME,$BASIC_AUTH_DIRECTIVES' < /etc/nginx/templates/default.conf.template > /etc/nginx/http.d/default.conf +else + echo "[entrypoint] ERROR: missing /etc/nginx/templates/default.conf.template" >&2 + exit 1 fi # Test nginx configuration From dfc3f6807a6d26aa021aee13dd9bc0de23f04884 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Thu, 30 Oct 2025 11:05:56 +0100 Subject: [PATCH 20/28] fix: nginx substitutions --- deploy/docker-entrypoint.sh | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/deploy/docker-entrypoint.sh b/deploy/docker-entrypoint.sh index 2de8bc1..fed1cfd 100644 --- a/deploy/docker-entrypoint.sh +++ b/deploy/docker-entrypoint.sh @@ -1,13 +1,8 @@ #!/bin/sh set -e -# Ensure SERVER_NAME is set -if [ -z "$SERVER_NAME" ]; then - echo "[entrypoint] SERVER_NAME is not set. Using default value: localhost" - SERVER_NAME="localhost" -else - echo "[entrypoint] Using SERVER_NAME=$SERVER_NAME" -fi +SERVER_NAME="${SERVER_NAME:-$(hostname -f 2>/dev/null || hostname)}" +export SERVER_NAME # Prepare BASIC_AUTH_DIRECTIVES if [ "$ENABLE_BASIC_AUTH" = "true" ]; then From 88137ccf66a49029c751676f97c0d43c4c2626df Mon Sep 17 00:00:00 2001 From: u00lipp Date: Thu, 30 Oct 2025 11:24:56 +0100 Subject: [PATCH 21/28] fix: entrypoint --- deploy/docker-entrypoint.sh | 105 +++++++++++++++++++++++++----------- 1 file changed, 74 insertions(+), 31 deletions(-) diff --git a/deploy/docker-entrypoint.sh b/deploy/docker-entrypoint.sh index fed1cfd..57ee9dd 100644 --- a/deploy/docker-entrypoint.sh +++ b/deploy/docker-entrypoint.sh @@ -1,45 +1,88 @@ #!/bin/sh -set -e +# Docker entrypoint for Nginx + PHP/Laravel +set -eu -SERVER_NAME="${SERVER_NAME:-$(hostname -f 2>/dev/null || hostname)}" +# -------- defaults (override via env) -------- +: "${APP_DIR:=/var/www/html}" +: "${NGINX_TEMPLATE:=/etc/nginx/templates/default.conf.template}" +: "${NGINX_HTTPD_DIR:=/etc/nginx/http.d}" +: "${NGINX_CONF_OUT:=${NGINX_HTTPD_DIR}/default.conf}" +: "${HTPASSWD_PATH:=/etc/nginx/htpasswd}" +: "${BASIC_AUTH_REALM:=Restricted Content}" +: "${SUPERVISOR_CONF:=/etc/supervisor/supervisord.conf}" + +log() { printf '[entrypoint] %s\n' "$*"; } +err() { printf '[entrypoint][ERROR] %s\n' "$*" >&2; } +die() { err "$*"; exit 1; } +need() { command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"; } + +# required tools +need envsubst; need nginx; need php; need supervisord + +# server name +SERVER_NAME="${SERVER_NAME:-$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo localhost)}" export SERVER_NAME -# Prepare BASIC_AUTH_DIRECTIVES -if [ "$ENABLE_BASIC_AUTH" = "true" ]; then - echo "[entrypoint] Enabling basic authentication..." - if [ -f /etc/nginx/htpasswd ]; then - BASIC_AUTH_DIRECTIVES=$(cat <&2 - exit 1 - fi +) + # safe placeholder substitution + esc() { printf '%s' "$1" | sed 's/[&/\]/\\&/g'; } + BASIC_AUTH_DIRECTIVES=$(printf '%s' "$BASIC_AUTH_DIRECTIVES" \ + | sed "s|__REALM__|$(esc "$BASIC_AUTH_REALM")|g" \ + | sed "s|__HTPASSWD__|$(esc "$HTPASSWD_PATH")|g") else - echo "[entrypoint] Basic authentication is disabled." - BASIC_AUTH_DIRECTIVES="" + log "Basic authentication is disabled" fi +export BASIC_AUTH_DIRECTIVES -# Render the NGINX configuration -if [ -f /etc/nginx/templates/default.conf.template ]; then - envsubst '$SERVER_NAME,$BASIC_AUTH_DIRECTIVES' < /etc/nginx/templates/default.conf.template > /etc/nginx/http.d/default.conf +# -------- render nginx config (atomic) -------- +[ -f "$NGINX_TEMPLATE" ] || die "Missing template: $NGINX_TEMPLATE" +[ -d "$NGINX_HTTPD_DIR" ] || die "Missing nginx conf dir: $NGINX_HTTPD_DIR" + +tmpconf="$(mktemp "${NGINX_CONF_OUT}.XXXXXX")" +envsubst '$SERVER_NAME,$BASIC_AUTH_DIRECTIVES' <"$NGINX_TEMPLATE" >"$tmpconf" \ + || die "envsubst failed" + +mv -f "$tmpconf" "$NGINX_CONF_OUT" +log "Wrote nginx config -> $NGINX_CONF_OUT" + +# final sanity test against active config tree +nginx -t || die "nginx config test failed after install" + +# -------- Laravel cache clears (best-effort) -------- +if [ -f "$APP_DIR/artisan" ]; then + log "Clearing Laravel caches" + ( cd "$APP_DIR" && php artisan config:clear || log "config:clear failed (continuing)" ) + ( cd "$APP_DIR" && php artisan cache:clear || log "cache:clear failed (continuing)" ) else - echo "[entrypoint] ERROR: missing /etc/nginx/templates/default.conf.template" >&2 - exit 1 + log "No Laravel app at $APP_DIR; skipping artisan" fi -# Test nginx configuration -if ! nginx -t; then - echo "[entrypoint] nginx config test failed" >&2 - cat /var/log/nginx/error.log || true - exit 1 +# -------- entrypoint behavior -------- +# If the first arg is an option (starts with -), assume supervisord. +if [ "${1:-}" ] && [ "${1#-}" != "$1" ]; then + set -- /usr/bin/supervisord -n -c "$SUPERVISOR_CONF" "$@" fi -# Clear Laravel caches -php artisan config:clear -php artisan cache:clear +# If they provided a command (e.g., sh, bash, php -v), run it. +if [ "${1:-}" ] && [ "$1" != "/usr/bin/supervisord" ] && [ "$1" != "supervisord" ]; then + log "Executing custom command: $*" + exec "$@" +fi -# Start supervisord -exec /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf \ No newline at end of file +# Default: start supervisord in the foreground (PID 1) +log "Starting supervisord" +exec /usr/bin/supervisord -n -c "$SUPERVISOR_CONF" From 6f4caddb184a32120b61ec35db2033e3ed83cea9 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Thu, 30 Oct 2025 11:42:25 +0100 Subject: [PATCH 22/28] fix: pipeline with redeploy trigger --- .github/workflows/docker-build.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 0328444..725ac59 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -46,3 +46,13 @@ jobs: - name: Push Docker Image run: | docker push "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" + + - name: Configure kubectl + run: | + mkdir -p ~/.kube + echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config + + - name: Trigger Kubernetes Redeployment + run: | + kubectl patch deployment tap-frontend -n frontend \ + -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"kubectl.kubernetes.io/restartedAt\":\"$(date -u +'%Y-%m-%dT%H:%M:%SZ')\"}}}}}" \ No newline at end of file From 2840e3f78bffc187ea808402106c1d699f9847d0 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Thu, 30 Oct 2025 11:50:59 +0100 Subject: [PATCH 23/28] fix: trigger deployment restart --- .github/workflows/docker-build.yml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 725ac59..2725057 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -47,12 +47,20 @@ jobs: run: | docker push "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" - - name: Configure kubectl - run: | - mkdir -p ~/.kube - echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config - - name: Trigger Kubernetes Redeployment + uses: docker://registry.k8s.io/kubectl:latest + env: + KUBECONFIG: ${{ secrets.KUBECONFIG }} run: | - kubectl patch deployment tap-frontend -n frontend \ - -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"kubectl.kubernetes.io/restartedAt\":\"$(date -u +'%Y-%m-%dT%H:%M:%SZ')\"}}}}}" \ No newline at end of file + DEPLOYMENT="tap-frontend" + NAMESPACE="frontend" + TIMESTAMP="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" + + echo "🔄 Restarting deployment: $DEPLOYMENT in namespace: $NAMESPACE" + kubectl patch deployment "$DEPLOYMENT" -n "$NAMESPACE" \ + -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"kubectl.kubernetes.io/restartedAt\":\"${TIMESTAMP}\"}}}}}" + + echo "⏳ Waiting for rollout to complete..." + kubectl rollout status deployment "$DEPLOYMENT" -n "$NAMESPACE" --timeout=120s + + echo "✅ Restart complete at $TIMESTAMP" From e197a7d031c8aef561bef87791c3cac4380d0da6 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Thu, 30 Oct 2025 11:57:02 +0100 Subject: [PATCH 24/28] fix: pipie --- .github/workflows/docker-build.yml | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 2725057..be2b00d 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -51,16 +51,8 @@ jobs: uses: docker://registry.k8s.io/kubectl:latest env: KUBECONFIG: ${{ secrets.KUBECONFIG }} - run: | - DEPLOYMENT="tap-frontend" - NAMESPACE="frontend" - TIMESTAMP="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" - - echo "🔄 Restarting deployment: $DEPLOYMENT in namespace: $NAMESPACE" - kubectl patch deployment "$DEPLOYMENT" -n "$NAMESPACE" \ - -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"kubectl.kubernetes.io/restartedAt\":\"${TIMESTAMP}\"}}}}}" - - echo "⏳ Waiting for rollout to complete..." - kubectl rollout status deployment "$DEPLOYMENT" -n "$NAMESPACE" --timeout=120s - - echo "✅ Restart complete at $TIMESTAMP" + with: + args: > + patch deployment tap-frontend -n frontend + -p '{"spec":{"template":{"metadata":{"annotations":{"kubectl.kubernetes.io/restartedAt":"'"$(date -u +'%Y-%m-%dT%H:%M:%SZ')"'"}}}}}' + && kubectl rollout status deployment tap-frontend -n frontend --timeout=120s From d8303ce81b2d7b14a2b5104f52ba1f8a11ed0ec7 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Thu, 30 Oct 2025 11:59:38 +0100 Subject: [PATCH 25/28] fix: kubectl image --- .github/workflows/docker-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index be2b00d..b2b2d9a 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -48,11 +48,11 @@ jobs: docker push "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" - name: Trigger Kubernetes Redeployment - uses: docker://registry.k8s.io/kubectl:latest + uses: docker://alpine/kubectl:1.34.0 env: KUBECONFIG: ${{ secrets.KUBECONFIG }} with: args: > patch deployment tap-frontend -n frontend -p '{"spec":{"template":{"metadata":{"annotations":{"kubectl.kubernetes.io/restartedAt":"'"$(date -u +'%Y-%m-%dT%H:%M:%SZ')"'"}}}}}' - && kubectl rollout status deployment tap-frontend -n frontend --timeout=120s + && kubectl rollout status deployment tap-frontend -n frontend --timeout=120s \ No newline at end of file From fcbabfde49d615402c60b0cd2a84dbff9981b776 Mon Sep 17 00:00:00 2001 From: u00lipp Date: Thu, 30 Oct 2025 12:05:49 +0100 Subject: [PATCH 26/28] fix; kuectl image --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index b2b2d9a..87ebe6d 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -48,7 +48,7 @@ jobs: docker push "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" - name: Trigger Kubernetes Redeployment - uses: docker://alpine/kubectl:1.34.0 + uses: alpine/kubectl:1.34.1 env: KUBECONFIG: ${{ secrets.KUBECONFIG }} with: From 8c6489da541efd83bb2d9cbfe331d27b7553d53c Mon Sep 17 00:00:00 2001 From: Bob Molitor Date: Fri, 31 Oct 2025 08:29:22 +0100 Subject: [PATCH 27/28] add swagger.yaml --- swagger.yaml | 626 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 626 insertions(+) create mode 100644 swagger.yaml diff --git a/swagger.yaml b/swagger.yaml new file mode 100644 index 0000000..3a2a4d4 --- /dev/null +++ b/swagger.yaml @@ -0,0 +1,626 @@ +openapi: 3.1.0 +info: + title: Trusted AI Auth API + version: '1.0.0' + description: > + Authentication and session-management endpoints required to reach the protected analyst UI + (dashboard, transaction review, company search, and company transactions). +servers: + - url: https://trusted-ai.local + description: Local development (composer run dev) + - url: https://api.trusted-ai.example.com + description: Production +tags: + - name: Auth + description: Credential, session, and verification endpoints. + - name: TwoFactor + description: Manage the analyst’s Time-based One Time Password (TOTP) second factor. +components: + securitySchemes: + sessionCookie: + type: apiKey + in: cookie + name: laravel_session + description: Issued after login/two-factor completion; required for all protected routes. + xsrfToken: + type: apiKey + in: header + name: X-XSRF-TOKEN + description: CSRF token minted via `/sanctum/csrf-cookie` (or HTML meta); required by state-changing requests. + schemas: + AuthUser: + type: object + required: [id, name, email, emailVerified] + properties: + id: + type: integer + format: int64 + example: 143 + name: + type: string + example: Alex Analyst + email: + type: string + format: email + example: analyst@example.com + emailVerified: + type: boolean + description: Indicates whether the analyst passes the `verified` middleware. + example: true + emailVerifiedAt: + type: string + format: date-time + nullable: true + example: '2024-03-18T08:31:12Z' + roles: + type: array + items: + type: string + example: [analyst] + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + LoginResponse: + type: object + required: [user, twoFactorRequired] + properties: + user: + $ref: '#/components/schemas/AuthUser' + twoFactorRequired: + type: boolean + description: True when the next step is `/two-factor-challenge`. + example: false + TwoFactorPending: + type: object + required: [twoFactorRequired] + properties: + twoFactorRequired: + type: boolean + const: true + challengeToken: + type: string + description: Echo of the `login.id` stored server-side; clients must keep the accompanying session cookie. + example: 9b6c11f7-527a-4dc9-9c2b-8923e7c7a418 + remember: + type: boolean + description: Remember-me preference preserved through the challenge. + example: true + TwoFactorSuccess: + type: object + required: [user] + properties: + user: + $ref: '#/components/schemas/AuthUser' + ValidationError: + type: object + required: [message, errors] + properties: + message: + type: string + example: The given data was invalid. + errors: + type: object + additionalProperties: + type: array + items: + type: string + example: + email: + - These credentials do not match our records. + MessageResponse: + type: object + required: [message] + properties: + message: + type: string + example: If an account exists, a reset link has been sent. + TooManyRequestsError: + type: object + required: [message, retryAfterSeconds] + properties: + message: + type: string + example: Too many login attempts. Try again in 48 seconds. + retryAfterSeconds: + type: integer + example: 48 + RecoveryCodes: + type: object + required: [codes] + properties: + codes: + type: array + items: + type: string + example: [JQ7L-9PKM, ZG28-5TQF] + QrCodeSvg: + type: object + required: [svg] + properties: + svg: + type: string + format: byte + description: Base64-encoded SVG markup for authenticator enrollment. + SecretKey: + type: object + required: [secret] + properties: + secret: + type: string + description: Plain-text TOTP secret for manual entry. + example: NB2W45DFOIZA==== +paths: + /auth/session: + get: + tags: [Auth] + summary: Fetch the authenticated analyst + description: > + Confirms session status and email verification before loading guarded UI routes. + security: + - sessionCookie: [] + responses: + '200': + description: Active, verified session. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthUser' + '401': + description: Missing or expired session cookie. + '403': + description: Analyst authenticated but email remains unverified. + /login: + post: + tags: [Auth] + summary: Authenticate an analyst + description: > + Validates credentials and issues a new session cookie, matching the login Volt component. + security: + - xsrfToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [email, password] + properties: + email: + type: string + format: email + example: analyst@example.com + password: + type: string + format: password + example: correct-horse-battery-staple + remember: + type: boolean + default: false + responses: + '200': + description: Login successful; analyst may navigate to protected pages. + headers: + Set-Cookie: + description: Refreshed `laravel_session` cookie. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + '202': + description: Second factor required before granting access. + headers: + Set-Cookie: + description: Session cookie plus `login.id`/`login.remember` context. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/TwoFactorPending' + '422': + description: Validation failed (bad credentials, throttled). + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + '429': + description: Too many attempts; obey lockout timings. + content: + application/json: + schema: + $ref: '#/components/schemas/TooManyRequestsError' + /two-factor-challenge: + post: + tags: [Auth] + summary: Confirm the TOTP or recovery code + description: > + Completes the pending session when two-factor authentication is enabled. + security: + - xsrfToken: [] + - sessionCookie: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + oneOf: + - required: [code] + properties: + code: + type: string + pattern: '^\d{6}$' + description: 6-digit TOTP value. + - required: [recovery_code] + properties: + recovery_code: + type: string + description: Emergency recovery code. + responses: + '200': + description: Two-factor validated; session promoted to fully authenticated. + headers: + Set-Cookie: + description: Session cookie extended per remember-me preference. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/TwoFactorSuccess' + '422': + description: Invalid TOTP or recovery code. + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + '429': + description: Too many challenge attempts (Fortify `two-factor` limiter). + content: + application/json: + schema: + $ref: '#/components/schemas/TooManyRequestsError' + /logout: + post: + tags: [Auth] + summary: Terminate the active session + description: > + Logs the analyst out and invalidates the session cookie. + security: + - xsrfToken: [] + - sessionCookie: [] + responses: + '204': + description: Logout succeeded; client should discard session cookies. + '401': + description: Analyst was not logged in. + /register: + post: + tags: [Auth] + summary: Register a new analyst + description: > + Creates an account and authenticates immediately, matching the register Volt component. + security: + - xsrfToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [name, email, password, password_confirmation] + properties: + name: + type: string + example: Jamie Investigator + email: + type: string + format: email + password: + type: string + format: password + password_confirmation: + type: string + format: password + responses: + '201': + description: Account created; dashboard can load. + headers: + Set-Cookie: + description: Session cookie for the new account. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + '422': + description: Validation errors (duplicate email, weak password, mismatch). + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + /forgot-password: + post: + tags: [Auth] + summary: Send password reset link + description: > + Sends the reset email while obscuring account existence. + security: + - xsrfToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [email] + properties: + email: + type: string + format: email + responses: + '202': + description: Reset notification dispatched if the account exists. + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' + '422': + description: Email field invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + /reset-password: + post: + tags: [Auth] + summary: Reset password with token + description: > + Applies a valid reset token and forces the analyst to log in again. + security: + - xsrfToken: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [token, email, password, password_confirmation] + properties: + token: + type: string + description: Token from the password reset email. + email: + type: string + format: email + password: + type: string + format: password + password_confirmation: + type: string + format: password + responses: + '204': + description: Password updated; analyst should be redirected to `/login`. + '422': + description: Invalid token or password validation failure. + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + /user/confirm-password: + post: + tags: [Auth] + summary: Confirm password for sensitive operations + description: > + Required before enabling/disabling 2FA when the `password.confirm` middleware is active. + security: + - xsrfToken: [] + - sessionCookie: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [password] + properties: + password: + type: string + format: password + responses: + '204': + description: Password confirmed for the next 900 seconds. + '422': + description: Wrong password submitted. + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + /email/verification-notification: + post: + tags: [Auth] + summary: Resend email verification link + description: > + Needed because dashboard and investigation routes require the `verified` middleware. + security: + - xsrfToken: [] + - sessionCookie: [] + responses: + '202': + description: Verification email dispatched (throttled at 6/hour). + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' + '429': + description: Throttle limit exceeded. + content: + application/json: + schema: + $ref: '#/components/schemas/TooManyRequestsError' + /email/verify/{id}/{hash}: + get: + tags: [Auth] + summary: Mark email address as verified + description: > + Consumes the signed verification link and redirects to the front end. + parameters: + - name: id + in: path + required: true + schema: + type: integer + - name: hash + in: path + required: true + schema: + type: string + - name: signature + in: query + required: true + schema: + type: string + - name: expires + in: query + required: true + schema: + type: integer + responses: + '302': + description: Redirect to `/dashboard?verified=1` (default). + headers: + Location: + schema: + type: string + '403': + description: Link expired or signature mismatch. + /user/two-factor-authentication: + post: + tags: [TwoFactor] + summary: Start enrolling two-factor authentication + description: > + Generates the TOTP secret and makes QR/manual data available. + security: + - xsrfToken: [] + - sessionCookie: [] + responses: + '200': + description: Enrollment started; fetch QR/secret next. + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' + example: + message: Two-factor authentication seed generated. + '423': + description: Password confirmation required. + '429': + description: Too many enable attempts. + delete: + tags: [TwoFactor] + summary: Disable two-factor authentication + security: + - xsrfToken: [] + - sessionCookie: [] + responses: + '204': + description: Two-factor disabled and secrets cleared. + '423': + description: Password confirmation required. + /user/confirmed-two-factor-authentication: + post: + tags: [TwoFactor] + summary: Confirm two-factor setup + description: > + Completes enrollment by verifying a TOTP code. + security: + - xsrfToken: [] + - sessionCookie: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [code] + properties: + code: + type: string + pattern: '^\d{6}$' + description: 6-digit TOTP code. + responses: + '204': + description: Two-factor authentication confirmed. + '422': + description: Invalid confirmation code. + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + /user/two-factor-qr-code: + get: + tags: [TwoFactor] + summary: Retrieve QR code SVG + description: > + Supplies the SVG rendered in the settings modal during enrollment. + security: + - sessionCookie: [] + responses: + '200': + description: SVG payload for authenticator scanning. + content: + application/json: + schema: + $ref: '#/components/schemas/QrCodeSvg' + '404': + description: Two-factor enrollment not in progress. + /user/two-factor-secret-key: + get: + tags: [TwoFactor] + summary: Retrieve manual setup key + description: > + Provides the fallback alphanumeric key shown alongside the QR code. + security: + - sessionCookie: [] + responses: + '200': + description: Secret key for manual entry. + content: + application/json: + schema: + $ref: '#/components/schemas/SecretKey' + /user/two-factor-recovery-codes: + get: + tags: [TwoFactor] + summary: List recovery codes + security: + - sessionCookie: [] + responses: + '200': + description: Recovery codes ready for display or download. + content: + application/json: + schema: + $ref: '#/components/schemas/RecoveryCodes' + post: + tags: [TwoFactor] + summary: Regenerate recovery codes + security: + - xsrfToken: [] + - sessionCookie: [] + responses: + '201': + description: New recovery codes generated and returned. + content: + application/json: + schema: + $ref: '#/components/schemas/RecoveryCodes' + '423': + description: Password confirmation required. From e4837044b5fc47a4800b51a1d68c292ff4929cf6 Mon Sep 17 00:00:00 2001 From: Bob Molitor Date: Fri, 31 Oct 2025 08:38:24 +0100 Subject: [PATCH 28/28] update swagger.yaml --- swagger.yaml | 1352 +++++++++++++++++++++++++++++++------------------- 1 file changed, 833 insertions(+), 519 deletions(-) diff --git a/swagger.yaml b/swagger.yaml index 3a2a4d4..9488ef6 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -1,626 +1,940 @@ openapi: 3.1.0 info: - title: Trusted AI Auth API + title: Trusted AI Analyst Data API version: '1.0.0' description: > - Authentication and session-management endpoints required to reach the protected analyst UI - (dashboard, transaction review, company search, and company transactions). + Data endpoints powering the analyst dashboard, portfolio-wide transaction review, + company search, and per-company transaction drill-down experiences. servers: - - url: https://trusted-ai.local + - url: http://trusted_ai.test/api description: Local development (composer run dev) - - url: https://api.trusted-ai.example.com + - url: https://api.trusted-ai.com description: Production tags: - - name: Auth - description: Credential, session, and verification endpoints. - - name: TwoFactor - description: Manage the analyst’s Time-based One Time Password (TOTP) second factor. + - name: Dashboard + description: KPIs and alert overviews for the landing dashboard. + - name: Transactions + description: Portfolio-wide transaction review tooling. + - name: Companies + description: Company discovery and drill-down data. components: securitySchemes: sessionCookie: type: apiKey in: cookie name: laravel_session - description: Issued after login/two-factor completion; required for all protected routes. - xsrfToken: - type: apiKey - in: header - name: X-XSRF-TOKEN - description: CSRF token minted via `/sanctum/csrf-cookie` (or HTML meta); required by state-changing requests. + description: Browser session issued after login; required for all endpoints documented here. schemas: - AuthUser: + PaginationMeta: type: object - required: [id, name, email, emailVerified] + required: + - page + - perPage + - total + - lastPage + properties: + page: + type: integer + minimum: 1 + example: 1 + perPage: + type: integer + minimum: 1 + example: 12 + total: + type: integer + minimum: 0 + example: 128 + lastPage: + type: integer + minimum: 1 + example: 11 + CountAmountSummary: + type: object + required: + - count + - amount + properties: + count: + type: integer + minimum: 0 + example: 42 + amount: + type: number + format: double + example: 2750000.5 + description: Monetary volume in EUR unless noted otherwise. + StatusOption: + type: object + required: + - value + - label + properties: + value: + type: string + example: true_positive + label: + type: string + example: Bestätigte Treffer + ChannelOption: + type: object + required: + - value + - label + properties: + value: + type: string + example: SEPA + label: + type: string + example: SEPA + RiskSegment: + type: object + required: + - label + - count + - volume + properties: + label: + type: string + example: Kritisch (≥80) + count: + type: integer + example: 17 + volume: + type: number + format: double + example: 8900000.0 + TopCompanyAlert: + type: object + required: + - companyId + - companyName + - ticker + - sector + - alerts + - alertVolume + - avgRiskScore + properties: + companyId: + type: integer + example: 24 + companyName: + type: string + example: Allianz SE + ticker: + type: string + example: ALV + sector: + type: string + example: Insurance + alerts: + type: integer + example: 6 + alertVolume: + type: number + format: double + example: 1450000.0 + avgRiskScore: + type: integer + minimum: 0 + maximum: 100 + example: 82 + DashboardOverview: + type: object + required: + - generatedAt + - activeAnalysts + - totalTransactions + - totalVolume + - alertsToday + - averageAlertsPerAnalyst + - precisionRate + - activeAlerts + - riskSegments + - topCompanies + - runbooksExecutedToday + properties: + generatedAt: + type: string + format: date-time + example: '2025-01-19T07:30:00Z' + activeAnalysts: + type: integer + example: 24 + totalTransactions: + type: integer + example: 312 + totalVolume: + type: number + format: double + example: 98765432.1 + alertsToday: + type: integer + example: 56 + averageAlertsPerAnalyst: + type: integer + example: 3 + precisionRate: + type: integer + minimum: 0 + maximum: 100 + example: 84 + activeAlerts: + type: object + required: + - count + - highRiskCount + properties: + count: + type: integer + example: 128 + highRiskCount: + type: integer + example: 19 + riskSegments: + type: array + items: + $ref: '#/components/schemas/RiskSegment' + topCompanies: + type: array + maxItems: 3 + items: + $ref: '#/components/schemas/TopCompanyAlert' + runbooksExecutedToday: + type: integer + example: 41 + TransactionStatus: + type: string + enum: + - true_positive + - false_positive + - cleared + CompanySummary: + type: object + required: + - id + - name + - legalName + - ticker + - sector + - country + - headquarters + - kycRiskLevel + - summary properties: id: type: integer - format: int64 - example: 143 + example: 12 name: type: string - example: Alex Analyst - email: + example: Allianz + legalName: type: string - format: email - example: analyst@example.com - emailVerified: + example: Allianz SE + ticker: + type: string + example: ALV + sector: + type: string + example: Insurance + country: + type: string + example: Germany + headquarters: + type: string + example: Munich + kycRiskLevel: + type: string + example: high + summary: + type: string + example: Multinational insurance provider with EMEA focus. + CompanyDetail: + allOf: + - $ref: '#/components/schemas/CompanySummary' + - type: object + properties: + createdAt: + type: string + format: date-time + nullable: true + updatedAt: + type: string + format: date-time + nullable: true + TransactionPreview: + type: object + required: + - id + - company + - counterparty + - counterpartyCountry + - status + - statusLabel + - requiresReview + - riskScore + - amount + - currency + - reference + - channel + - executedAt + properties: + id: + type: integer + example: 512 + company: + $ref: '#/components/schemas/CompanySummary' + counterparty: + type: string + example: Alpine Holdings Ltd. + counterpartyCountry: + type: string + example: Switzerland + status: + $ref: '#/components/schemas/TransactionStatus' + statusLabel: + type: string + example: Bestätigter Treffer + requiresReview: type: boolean - description: Indicates whether the analyst passes the `verified` middleware. example: true - emailVerifiedAt: + riskScore: + type: integer + minimum: 0 + maximum: 100 + example: 87 + amount: + type: number + format: double + example: 245000.75 + currency: + type: string + example: EUR + reference: + type: string + example: PAY-2024-10-1942 + channel: + type: string + example: SWIFT + executedAt: type: string format: date-time + example: '2025-01-18T09:14:00Z' + flaggedReason: + type: string nullable: true - example: '2024-03-18T08:31:12Z' - roles: - type: array - items: - type: string - example: [analyst] - createdAt: + example: Counterparty on sanctions watchlist + TransactionSignal: + type: object + required: + - type + - value + properties: + type: + type: string + example: Adverse Media + value: + type: string + example: Enforcement action reported in 2024-11 + weight: + type: number + format: double + nullable: true + example: 0.8 + AlertSummary: + type: object + required: + - id + - counterparty + - executedAt + - channel + - status + - statusLabel + properties: + id: + type: integer + example: 911 + counterparty: + type: string + example: Northbridge Trading Ltd. + executedAt: type: string format: date-time - updatedAt: + example: '2025-01-18T14:52:00Z' + channel: + type: string + example: SWIFT + status: + $ref: '#/components/schemas/TransactionStatus' + statusLabel: + type: string + example: Bestätigter Treffer + riskScore: + type: integer + minimum: 0 + maximum: 100 + example: 92 + TransactionHistoryItem: + type: object + required: + - id + - executedAt + - channel + - reference + - amount + - currency + - riskScore + properties: + id: + type: integer + example: 877 + executedAt: type: string format: date-time - LoginResponse: - type: object - required: [user, twoFactorRequired] - properties: - user: - $ref: '#/components/schemas/AuthUser' - twoFactorRequired: - type: boolean - description: True when the next step is `/two-factor-challenge`. - example: false - TwoFactorPending: - type: object - required: [twoFactorRequired] - properties: - twoFactorRequired: - type: boolean - const: true - challengeToken: + example: '2024-12-22T15:37:00Z' + channel: type: string - description: Echo of the `login.id` stored server-side; clients must keep the accompanying session cookie. - example: 9b6c11f7-527a-4dc9-9c2b-8923e7c7a418 - remember: - type: boolean - description: Remember-me preference preserved through the challenge. - example: true - TwoFactorSuccess: - type: object - required: [user] - properties: - user: - $ref: '#/components/schemas/AuthUser' - ValidationError: - type: object - required: [message, errors] - properties: - message: + example: SWIFT + reference: type: string - example: The given data was invalid. - errors: + example: PAY-2024-12-1187 + amount: + type: number + format: double + example: 99000.0 + currency: + type: string + example: EUR + riskScore: + type: integer + minimum: 0 + maximum: 100 + example: 74 + RecommendedAction: + type: object + required: + - title + - description + properties: + title: + type: string + example: Verdachtsmeldung vorbereiten + description: + type: string + example: Erstellen Sie den Meldeentwurf für die FIU und sichern Sie Belege. + TransactionDetail: + allOf: + - $ref: '#/components/schemas/TransactionPreview' + - type: object + required: + - flaggedBy + - flaggedReason + - signals + properties: + flaggedBy: + type: string + example: Screening Engine + flaggedReason: + type: string + example: Counterparty matched to EU sanctions list + signals: + type: array + items: + $ref: '#/components/schemas/TransactionSignal' + TransactionCaseFile: + allOf: + - $ref: '#/components/schemas/TransactionDetail' + - type: object + properties: + recommendedActions: + type: array + items: + $ref: '#/components/schemas/RecommendedAction' + counterpartyHistory: + type: array + items: + $ref: '#/components/schemas/TransactionHistoryItem' + TransactionMetrics: + type: object + required: + - total + - byStatus + properties: + total: + $ref: '#/components/schemas/CountAmountSummary' + byStatus: type: object additionalProperties: - type: array - items: - type: string - example: - email: - - These credentials do not match our records. - MessageResponse: + $ref: '#/components/schemas/CountAmountSummary' + description: Breakdown keyed by transaction status. + CompanyTransactionMetrics: type: object - required: [message] + required: + - totalCount + - totalVolume + - openAlerts + - highRiskShare + - last30Days + - byStatus properties: - message: - type: string - example: If an account exists, a reset link has been sent. - TooManyRequestsError: - type: object - required: [message, retryAfterSeconds] - properties: - message: - type: string - example: Too many login attempts. Try again in 48 seconds. - retryAfterSeconds: + totalCount: type: integer - example: 48 - RecoveryCodes: + example: 96 + totalVolume: + type: number + format: double + example: 7200000.0 + openAlerts: + $ref: '#/components/schemas/CountAmountSummary' + highRiskShare: + type: integer + minimum: 0 + maximum: 100 + example: 28 + last30Days: + $ref: '#/components/schemas/CountAmountSummary' + byStatus: + type: object + additionalProperties: + $ref: '#/components/schemas/CountAmountSummary' + TransactionCollectionResponse: type: object - required: [codes] + required: + - filters + - metrics + - data + - pagination properties: - codes: + filters: + type: object + properties: + statusOptions: + type: array + items: + $ref: '#/components/schemas/StatusOption' + perPage: + type: integer + example: 12 + metrics: + $ref: '#/components/schemas/TransactionMetrics' + data: type: array items: - type: string - example: [JQ7L-9PKM, ZG28-5TQF] - QrCodeSvg: + $ref: '#/components/schemas/TransactionPreview' + pagination: + $ref: '#/components/schemas/PaginationMeta' + selectedTransactionId: + type: integer + nullable: true + example: 512 + selectedTransaction: + allOf: + - $ref: '#/components/schemas/TransactionDetail' + nullable: true + CompanySearchOverview: type: object - required: [svg] + required: + - totalCompanies + - openAlerts + - openAlertVolume + - averageRiskScore + - watchlistHits + - automationShare properties: - svg: - type: string - format: byte - description: Base64-encoded SVG markup for authenticator enrollment. - SecretKey: + totalCompanies: + type: integer + example: 40 + openAlerts: + type: integer + example: 112 + openAlertVolume: + type: number + format: double + example: 5640000.0 + averageRiskScore: + type: integer + minimum: 0 + maximum: 100 + example: 71 + watchlistHits: + type: integer + example: 9 + automationShare: + type: integer + minimum: 0 + maximum: 100 + example: 82 + CompanyAlertPreview: type: object - required: [secret] + required: + - transactionId + - counterparty + - executedAt + - channel + - flaggedReason properties: - secret: + transactionId: + type: integer + example: 731 + counterparty: type: string - description: Plain-text TOTP secret for manual entry. - example: NB2W45DFOIZA==== + example: Baltic Commodities LLC + executedAt: + type: string + format: date-time + example: '2025-01-17T11:48:00Z' + channel: + type: string + example: SWIFT + flaggedReason: + type: string + example: Pattern matches sanctions typology + riskScore: + type: integer + minimum: 0 + maximum: 100 + example: 88 + CompanySearchResult: + allOf: + - $ref: '#/components/schemas/CompanySummary' + - type: object + required: + - alertCount + - alertVolume + - alertRiskScore + - latestAlert + properties: + alertCount: + type: integer + example: 7 + alertVolume: + type: number + format: double + example: 980000.0 + alertRiskScore: + type: integer + minimum: 0 + maximum: 100 + example: 79 + latestAlert: + allOf: + - $ref: '#/components/schemas/CompanyAlertPreview' + CompanySearchResponse: + type: object + required: + - overview + - results + - meta + properties: + overview: + $ref: '#/components/schemas/CompanySearchOverview' + results: + type: array + items: + $ref: '#/components/schemas/CompanySearchResult' + meta: + type: object + required: + - query + - resultCount + - limit + properties: + query: + type: string + nullable: true + example: Allianz + resultCount: + type: integer + example: 6 + limit: + type: integer + example: 6 + CompanyTransactionsResponse: + type: object + required: + - company + - filters + - metrics + - data + - pagination + properties: + company: + $ref: '#/components/schemas/CompanyDetail' + filters: + type: object + properties: + statusOptions: + type: array + items: + $ref: '#/components/schemas/StatusOption' + channelOptions: + type: array + items: + $ref: '#/components/schemas/ChannelOption' + metrics: + $ref: '#/components/schemas/CompanyTransactionMetrics' + data: + type: array + items: + $ref: '#/components/schemas/TransactionPreview' + pagination: + $ref: '#/components/schemas/PaginationMeta' + selectedTransactionId: + type: integer + nullable: true + example: 877 + selectedTransaction: + allOf: + - $ref: '#/components/schemas/TransactionCaseFile' + nullable: true + recentAlerts: + type: array + items: + $ref: '#/components/schemas/AlertSummary' paths: - /auth/session: + /dashboard/overview: get: - tags: [Auth] - summary: Fetch the authenticated analyst + tags: + - Dashboard + operationId: getDashboardOverview + summary: Retrieve dashboard overview metrics description: > - Confirms session status and email verification before loading guarded UI routes. + Aggregated KPIs and alert slices needed for the dashboard hero cards and side panels. security: - sessionCookie: [] responses: '200': - description: Active, verified session. + description: Overview data ready for dashboard rendering. content: application/json: schema: - $ref: '#/components/schemas/AuthUser' + $ref: '#/components/schemas/DashboardOverview' '401': - description: Missing or expired session cookie. - '403': - description: Analyst authenticated but email remains unverified. - /login: - post: - tags: [Auth] - summary: Authenticate an analyst - description: > - Validates credentials and issues a new session cookie, matching the login Volt component. - security: - - xsrfToken: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: [email, password] - properties: - email: - type: string - format: email - example: analyst@example.com - password: - type: string - format: password - example: correct-horse-battery-staple - remember: - type: boolean - default: false - responses: - '200': - description: Login successful; analyst may navigate to protected pages. - headers: - Set-Cookie: - description: Refreshed `laravel_session` cookie. - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/LoginResponse' - '202': - description: Second factor required before granting access. - headers: - Set-Cookie: - description: Session cookie plus `login.id`/`login.remember` context. - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/TwoFactorPending' - '422': - description: Validation failed (bad credentials, throttled). - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationError' - '429': - description: Too many attempts; obey lockout timings. - content: - application/json: - schema: - $ref: '#/components/schemas/TooManyRequestsError' - /two-factor-challenge: - post: - tags: [Auth] - summary: Confirm the TOTP or recovery code - description: > - Completes the pending session when two-factor authentication is enabled. - security: - - xsrfToken: [] - - sessionCookie: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - oneOf: - - required: [code] - properties: - code: - type: string - pattern: '^\d{6}$' - description: 6-digit TOTP value. - - required: [recovery_code] - properties: - recovery_code: - type: string - description: Emergency recovery code. - responses: - '200': - description: Two-factor validated; session promoted to fully authenticated. - headers: - Set-Cookie: - description: Session cookie extended per remember-me preference. - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/TwoFactorSuccess' - '422': - description: Invalid TOTP or recovery code. - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationError' - '429': - description: Too many challenge attempts (Fortify `two-factor` limiter). - content: - application/json: - schema: - $ref: '#/components/schemas/TooManyRequestsError' - /logout: - post: - tags: [Auth] - summary: Terminate the active session - description: > - Logs the analyst out and invalidates the session cookie. - security: - - xsrfToken: [] - - sessionCookie: [] - responses: - '204': - description: Logout succeeded; client should discard session cookies. - '401': - description: Analyst was not logged in. - /register: - post: - tags: [Auth] - summary: Register a new analyst - description: > - Creates an account and authenticates immediately, matching the register Volt component. - security: - - xsrfToken: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: [name, email, password, password_confirmation] - properties: - name: - type: string - example: Jamie Investigator - email: - type: string - format: email - password: - type: string - format: password - password_confirmation: - type: string - format: password - responses: - '201': - description: Account created; dashboard can load. - headers: - Set-Cookie: - description: Session cookie for the new account. - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/LoginResponse' - '422': - description: Validation errors (duplicate email, weak password, mismatch). - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationError' - /forgot-password: - post: - tags: [Auth] - summary: Send password reset link - description: > - Sends the reset email while obscuring account existence. - security: - - xsrfToken: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: [email] - properties: - email: - type: string - format: email - responses: - '202': - description: Reset notification dispatched if the account exists. - content: - application/json: - schema: - $ref: '#/components/schemas/MessageResponse' - '422': - description: Email field invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationError' - /reset-password: - post: - tags: [Auth] - summary: Reset password with token - description: > - Applies a valid reset token and forces the analyst to log in again. - security: - - xsrfToken: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: [token, email, password, password_confirmation] - properties: - token: - type: string - description: Token from the password reset email. - email: - type: string - format: email - password: - type: string - format: password - password_confirmation: - type: string - format: password - responses: - '204': - description: Password updated; analyst should be redirected to `/login`. - '422': - description: Invalid token or password validation failure. - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationError' - /user/confirm-password: - post: - tags: [Auth] - summary: Confirm password for sensitive operations - description: > - Required before enabling/disabling 2FA when the `password.confirm` middleware is active. - security: - - xsrfToken: [] - - sessionCookie: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: [password] - properties: - password: - type: string - format: password - responses: - '204': - description: Password confirmed for the next 900 seconds. - '422': - description: Wrong password submitted. - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationError' - /email/verification-notification: - post: - tags: [Auth] - summary: Resend email verification link - description: > - Needed because dashboard and investigation routes require the `verified` middleware. - security: - - xsrfToken: [] - - sessionCookie: [] - responses: - '202': - description: Verification email dispatched (throttled at 6/hour). - content: - application/json: - schema: - $ref: '#/components/schemas/MessageResponse' - '429': - description: Throttle limit exceeded. - content: - application/json: - schema: - $ref: '#/components/schemas/TooManyRequestsError' - /email/verify/{id}/{hash}: + description: Session invalid or expired. + /transactions: get: - tags: [Auth] - summary: Mark email address as verified + tags: + - Transactions + operationId: listTransactions + summary: List transactions with global filters description: > - Consumes the signed verification link and redirects to the front end. + Returns the paginated transaction stream together with per-status metrics and default selection, + mirroring the Livewire transaction review experience. + security: + - sessionCookie: [] parameters: - - name: id - in: path - required: true - schema: - type: integer - - name: hash - in: path - required: true + - name: search + in: query schema: type: string - - name: signature + description: Match against reference, counterparty, or company identifiers. + - name: status in: query - required: true schema: type: string - - name: expires + enum: + - all + - true_positive + - false_positive + - cleared + default: all + description: Filter by review status; `all` keeps every status. + - name: page in: query - required: true schema: type: integer - responses: - '302': - description: Redirect to `/dashboard?verified=1` (default). - headers: - Location: - schema: - type: string - '403': - description: Link expired or signature mismatch. - /user/two-factor-authentication: - post: - tags: [TwoFactor] - summary: Start enrolling two-factor authentication - description: > - Generates the TOTP secret and makes QR/manual data available. - security: - - xsrfToken: [] - - sessionCookie: [] + minimum: 1 + default: 1 + - name: perPage + in: query + schema: + type: integer + minimum: 1 + maximum: 100 + default: 12 responses: '200': - description: Enrollment started; fetch QR/secret next. + description: Paginated transaction set with metrics. content: application/json: schema: - $ref: '#/components/schemas/MessageResponse' - example: - message: Two-factor authentication seed generated. - '423': - description: Password confirmation required. - '429': - description: Too many enable attempts. - delete: - tags: [TwoFactor] - summary: Disable two-factor authentication - security: - - xsrfToken: [] - - sessionCookie: [] - responses: - '204': - description: Two-factor disabled and secrets cleared. - '423': - description: Password confirmation required. - /user/confirmed-two-factor-authentication: - post: - tags: [TwoFactor] - summary: Confirm two-factor setup - description: > - Completes enrollment by verifying a TOTP code. - security: - - xsrfToken: [] - - sessionCookie: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: [code] - properties: - code: - type: string - pattern: '^\d{6}$' - description: 6-digit TOTP code. - responses: - '204': - description: Two-factor authentication confirmed. - '422': - description: Invalid confirmation code. - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationError' - /user/two-factor-qr-code: + $ref: '#/components/schemas/TransactionCollectionResponse' + '401': + description: Session invalid or expired. + /transactions/{transactionId}: get: - tags: [TwoFactor] - summary: Retrieve QR code SVG + tags: + - Transactions + operationId: getTransactionCaseFile + summary: Fetch a single transaction case file description: > - Supplies the SVG rendered in the settings modal during enrollment. + Detailed transaction record used when an analyst expands a case in the transaction review workspace. security: - sessionCookie: [] + parameters: + - name: transactionId + in: path + required: true + schema: + type: integer responses: '200': - description: SVG payload for authenticator scanning. + description: Transaction detail with detection signals. content: application/json: schema: - $ref: '#/components/schemas/QrCodeSvg' + $ref: '#/components/schemas/TransactionDetail' + '401': + description: Session invalid or expired. '404': - description: Two-factor enrollment not in progress. - /user/two-factor-secret-key: + description: Transaction not found. + /companies/search: get: - tags: [TwoFactor] - summary: Retrieve manual setup key + tags: + - Companies + operationId: searchCompanies + summary: Search companies and retrieve screening overview description: > - Provides the fallback alphanumeric key shown alongside the QR code. + Provides the metrics and top results required to populate the company search tiles. security: - sessionCookie: [] + parameters: + - name: q + in: query + schema: + type: string + description: Free-text query across name, legal name, ticker, or sector. + - name: limit + in: query + schema: + type: integer + minimum: 1 + maximum: 25 + default: 6 responses: '200': - description: Secret key for manual entry. + description: Matching companies with alert context. content: application/json: schema: - $ref: '#/components/schemas/SecretKey' - /user/two-factor-recovery-codes: + $ref: '#/components/schemas/CompanySearchResponse' + '401': + description: Session invalid or expired. + /companies/{companyId}: get: - tags: [TwoFactor] - summary: List recovery codes + tags: + - Companies + operationId: getCompany + summary: Retrieve a company profile + description: > + Returns core firmographics and KYC risk tier needed for per-company transaction pages. security: - sessionCookie: [] + parameters: + - name: companyId + in: path + required: true + schema: + type: integer responses: '200': - description: Recovery codes ready for display or download. + description: Company detail. content: application/json: schema: - $ref: '#/components/schemas/RecoveryCodes' - post: - tags: [TwoFactor] - summary: Regenerate recovery codes + $ref: '#/components/schemas/CompanyDetail' + '401': + description: Session invalid or expired. + '404': + description: Company not found. + /companies/{companyId}/transactions: + get: + tags: + - Companies + operationId: listCompanyTransactions + summary: List transactions for a specific company + description: > + Supplies per-company metrics, channel/status filters, paginated cases, and the current selection + for the company drill-down view. security: - - xsrfToken: [] - sessionCookie: [] + parameters: + - name: companyId + in: path + required: true + schema: + type: integer + - name: status + in: query + schema: + type: string + enum: + - all + - true_positive + - false_positive + - cleared + default: all + - name: channel + in: query + schema: + type: string + default: all + description: Channel label returned in channelOptions; use `all` for no filtering. + - name: page + in: query + schema: + type: integer + minimum: 1 + default: 1 + - name: perPage + in: query + schema: + type: integer + minimum: 1 + maximum: 100 + default: 10 responses: - '201': - description: New recovery codes generated and returned. + '200': + description: Company-specific transaction data with metrics. content: application/json: schema: - $ref: '#/components/schemas/RecoveryCodes' - '423': - description: Password confirmation required. + $ref: '#/components/schemas/CompanyTransactionsResponse' + '401': + description: Session invalid or expired. + '404': + description: Company not found or no transactions. + /companies/{companyId}/transactions/{transactionId}: + get: + tags: + - Companies + operationId: getCompanyTransactionCaseFile + summary: Fetch a company transaction case file with history + description: > + Returns the selected transaction enriched with recommended actions and counterparty history + used in the right-hand panel of the company drill-down. + security: + - sessionCookie: [] + parameters: + - name: companyId + in: path + required: true + schema: + type: integer + - name: transactionId + in: path + required: true + schema: + type: integer + responses: + '200': + description: Transaction case file for the company context. + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionCaseFile' + '401': + description: Session invalid or expired. + '404': + description: Transaction not found for the given company.