diff --git a/.gitea/workflows/build-and-deploy.yml b/.gitea/workflows/build-and-deploy.yml index 40fae7e..bf33406 100644 --- a/.gitea/workflows/build-and-deploy.yml +++ b/.gitea/workflows/build-and-deploy.yml @@ -27,23 +27,56 @@ jobs: image_repository: ${{ steps.image-meta.outputs.repository }} docker_image: ${{ steps.image-meta.outputs.full }} steps: - - uses: actions/checkout@v4 + - name: Check out repository + shell: bash + run: | + set -euo pipefail + + if [ -z "$(find . -mindepth 1 -maxdepth 1 -print -quit)" ]; then + REPOSITORY_URL="${{ gitea.server_url }}/${{ gitea.repository }}.git" + + git init . + git remote add origin "$REPOSITORY_URL" + if [ -n "${{ secrets.GITHUB_TOKEN }}" ]; then + AUTH_HEADER="$(printf '%s:%s' "${{ gitea.actor }}" "${{ secrets.GITHUB_TOKEN }}" | base64 | tr -d '\n')" + git -c http.extraheader="Authorization: Basic ${AUTH_HEADER}" \ + fetch --depth 1 origin "${{ gitea.ref }}" + else + git fetch --depth 1 origin "${{ gitea.ref }}" + fi + git checkout --detach "${{ gitea.sha }}" || git checkout --detach FETCH_HEAD + fi + + git rev-parse --short HEAD - name: Ensure Docker CLI is available run: | if ! command -v docker >/dev/null 2>&1; then - apt-get update -qq - apt-get install -y -qq docker.io + echo "::error::Docker CLI is not available in this Gitea runner image." + echo "::error::Configure the runner label used by this job to an image that already includes Docker CLI, for example ubuntu-latest:docker://catthehacker/ubuntu:act-latest." + echo "::error::The current runner maps ubuntu-latest to node:20-bookworm, and this job cannot install docker.io because the job container has no external DNS." + exit 1 fi docker --version docker info >/dev/null - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - buildkitd-config-inline: | - [registry."192.168.3.80"] - insecure = true + shell: bash + run: | + set -euo pipefail + + docker buildx version + printf '[registry."%s"]\n insecure = true\n' "$REGISTRY_HOST" > /tmp/buildkitd.toml + if ! docker buildx inspect rentaldrivego-builder >/dev/null 2>&1; then + docker buildx create \ + --name rentaldrivego-builder \ + --driver docker-container \ + --config /tmp/buildkitd.toml \ + --use + else + docker buildx use rentaldrivego-builder + fi + docker buildx inspect --bootstrap - name: Docker image metadata id: image-meta @@ -69,7 +102,7 @@ jobs: env: NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} NEXT_PUBLIC_HOMEPAGE_URL: ${{ secrets.NEXT_PUBLIC_HOMEPAGE_URL }} - NEXT_PUBLIC_STOREFRONT_URL: ${{ secrets.NEXT_PUBLIC_STOREFRONT_URL }} + NEXT_PUBLIC_CARPLACE_URL: ${{ secrets.NEXT_PUBLIC_CARPLACE_URL }} NEXT_PUBLIC_DASHBOARD_URL: ${{ secrets.NEXT_PUBLIC_DASHBOARD_URL }} NEXT_PUBLIC_ADMIN_URL: ${{ secrets.NEXT_PUBLIC_ADMIN_URL }} SITE_ORIGIN: ${{ secrets.SITE_ORIGIN }} @@ -77,7 +110,7 @@ jobs: for variable in \ NEXT_PUBLIC_API_URL \ NEXT_PUBLIC_HOMEPAGE_URL \ - NEXT_PUBLIC_STOREFRONT_URL \ + NEXT_PUBLIC_CARPLACE_URL \ NEXT_PUBLIC_DASHBOARD_URL \ NEXT_PUBLIC_ADMIN_URL \ SITE_ORIGIN @@ -98,34 +131,41 @@ jobs: - name: Log in to Gitea Container Registry if: steps.registry-check.outputs.available == 'true' - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY_HOST }} - username: ${{ secrets.REGISTRY_USERNAME }} - password: ${{ secrets.REGISTRY_PASSWORD }} + shell: bash + run: | + printf '%s' "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login "$REGISTRY_HOST" \ + --username "${{ secrets.REGISTRY_USERNAME }}" \ + --password-stdin - name: Build and push - uses: docker/build-push-action@v6 + shell: bash env: BUILDKIT_NO_CLIENT_TOKEN: "true" - with: - context: . - file: ${{ env.DOCKERFILE_PATH }} - platforms: ${{ env.DOCKER_PLATFORM }} - push: ${{ steps.registry-check.outputs.available == 'true' }} - tags: | - ${{ steps.image-meta.outputs.full }} - ${{ steps.image-meta.outputs.latest }} - build-args: | - API_INTERNAL_URL=http://api:4000/api/v1 - DASHBOARD_INTERNAL_URL=http://dashboard:3001 - ADMIN_INTERNAL_URL=http://admin:3002 - NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }} - NEXT_PUBLIC_HOMEPAGE_URL=${{ secrets.NEXT_PUBLIC_HOMEPAGE_URL }} - NEXT_PUBLIC_STOREFRONT_URL=${{ secrets.NEXT_PUBLIC_STOREFRONT_URL }} - NEXT_PUBLIC_DASHBOARD_URL=${{ secrets.NEXT_PUBLIC_DASHBOARD_URL }} - NEXT_PUBLIC_ADMIN_URL=${{ secrets.NEXT_PUBLIC_ADMIN_URL }} - SITE_ORIGIN=${{ secrets.SITE_ORIGIN }} + run: | + set -euo pipefail + + OUTPUT_ARG="--load" + if [ "${{ steps.registry-check.outputs.available }}" = "true" ]; then + OUTPUT_ARG="--push" + fi + + docker buildx build \ + --file "$DOCKERFILE_PATH" \ + --platform "$DOCKER_PLATFORM" \ + --tag "${{ steps.image-meta.outputs.full }}" \ + --tag "${{ steps.image-meta.outputs.latest }}" \ + --build-arg API_INTERNAL_URL=http://api:4000/api/v1 \ + --build-arg DASHBOARD_INTERNAL_URL=http://dashboard:3001 \ + --build-arg ADMIN_INTERNAL_URL=http://admin:3002 \ + --build-arg NEXT_PUBLIC_API_URL="${{ secrets.NEXT_PUBLIC_API_URL }}" \ + --build-arg NEXT_PUBLIC_HOMEPAGE_URL="${{ secrets.NEXT_PUBLIC_HOMEPAGE_URL }}" \ + --build-arg NEXT_PUBLIC_CARPLACE_URL="${{ secrets.NEXT_PUBLIC_CARPLACE_URL }}" \ + --build-arg NEXT_PUBLIC_DASHBOARD_URL="${{ secrets.NEXT_PUBLIC_DASHBOARD_URL }}" \ + --build-arg NEXT_PUBLIC_ADMIN_URL="${{ secrets.NEXT_PUBLIC_ADMIN_URL }}" \ + --build-arg SITE_ORIGIN="${{ secrets.SITE_ORIGIN }}" \ + "$OUTPUT_ARG" \ + . deploy: name: Deploy to VPS @@ -135,7 +175,27 @@ jobs: DOCKER_IMAGE: ${{ needs.build-image.outputs.docker_image }} IMAGE_REPOSITORY: ${{ needs.build-image.outputs.image_repository }} steps: - - uses: actions/checkout@v4 + - name: Check out repository + shell: bash + run: | + set -euo pipefail + + if [ -z "$(find . -mindepth 1 -maxdepth 1 -print -quit)" ]; then + REPOSITORY_URL="${{ gitea.server_url }}/${{ gitea.repository }}.git" + + git init . + git remote add origin "$REPOSITORY_URL" + if [ -n "${{ secrets.GITHUB_TOKEN }}" ]; then + AUTH_HEADER="$(printf '%s:%s' "${{ gitea.actor }}" "${{ secrets.GITHUB_TOKEN }}" | base64 | tr -d '\n')" + git -c http.extraheader="Authorization: Basic ${AUTH_HEADER}" \ + fetch --depth 1 origin "${{ gitea.ref }}" + else + git fetch --depth 1 origin "${{ gitea.ref }}" + fi + git checkout --detach "${{ gitea.sha }}" || git checkout --detach FETCH_HEAD + fi + + git rev-parse --short HEAD - name: Check deploy credentials id: check-creds @@ -152,7 +212,12 @@ jobs: - name: Install SSH client if: steps.check-creds.outputs.skip == 'false' run: | - apt-get update -qq && apt-get install -y -qq openssh-client + if ! command -v ssh >/dev/null 2>&1 || ! command -v scp >/dev/null 2>&1; then + echo "::error::OpenSSH client tools are not available in this Gitea runner image." + echo "::error::Use a runner image that already includes ssh and scp; installing packages from apt is not reliable because the job container has no external DNS." + exit 1 + fi + ssh -V - name: Set up SSH key if: steps.check-creds.outputs.skip == 'false' diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 660291c..cfb9c2f 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -24,10 +24,27 @@ jobs: name: Type Check (all packages) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} + - name: Check out repository + shell: bash + run: | + set -euo pipefail + + if [ -z "$(find . -mindepth 1 -maxdepth 1 -print -quit)" ]; then + REPOSITORY_URL="${{ gitea.server_url }}/${{ gitea.repository }}.git" + git init . + git remote add origin "$REPOSITORY_URL" + if [ -n "${{ secrets.GITHUB_TOKEN }}" ]; then + AUTH_HEADER="$(printf '%s:%s' "${{ gitea.actor }}" "${{ secrets.GITHUB_TOKEN }}" | base64 | tr -d '\n')" + git -c http.extraheader="Authorization: Basic ${AUTH_HEADER}" fetch --depth 1 origin "${{ gitea.ref }}" + else + git fetch --depth 1 origin "${{ gitea.ref }}" + fi + git checkout --detach "${{ gitea.sha }}" || git checkout --detach FETCH_HEAD + fi + - name: Check Node runtime + run: | + node --version + npm --version - run: corepack enable && corepack prepare npm@10.5.0 --activate - name: Install dependencies run: | @@ -62,10 +79,27 @@ jobs: runs-on: ubuntu-latest needs: type-check steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} + - name: Check out repository + shell: bash + run: | + set -euo pipefail + + if [ -z "$(find . -mindepth 1 -maxdepth 1 -print -quit)" ]; then + REPOSITORY_URL="${{ gitea.server_url }}/${{ gitea.repository }}.git" + git init . + git remote add origin "$REPOSITORY_URL" + if [ -n "${{ secrets.GITHUB_TOKEN }}" ]; then + AUTH_HEADER="$(printf '%s:%s' "${{ gitea.actor }}" "${{ secrets.GITHUB_TOKEN }}" | base64 | tr -d '\n')" + git -c http.extraheader="Authorization: Basic ${AUTH_HEADER}" fetch --depth 1 origin "${{ gitea.ref }}" + else + git fetch --depth 1 origin "${{ gitea.ref }}" + fi + git checkout --detach "${{ gitea.sha }}" || git checkout --detach FETCH_HEAD + fi + - name: Check Node runtime + run: | + node --version + npm --version - run: corepack enable && corepack prepare npm@10.5.0 --activate - name: Install dependencies run: | @@ -100,10 +134,27 @@ jobs: runs-on: ubuntu-latest needs: type-check steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} + - name: Check out repository + shell: bash + run: | + set -euo pipefail + + if [ -z "$(find . -mindepth 1 -maxdepth 1 -print -quit)" ]; then + REPOSITORY_URL="${{ gitea.server_url }}/${{ gitea.repository }}.git" + git init . + git remote add origin "$REPOSITORY_URL" + if [ -n "${{ secrets.GITHUB_TOKEN }}" ]; then + AUTH_HEADER="$(printf '%s:%s' "${{ gitea.actor }}" "${{ secrets.GITHUB_TOKEN }}" | base64 | tr -d '\n')" + git -c http.extraheader="Authorization: Basic ${AUTH_HEADER}" fetch --depth 1 origin "${{ gitea.ref }}" + else + git fetch --depth 1 origin "${{ gitea.ref }}" + fi + git checkout --detach "${{ gitea.sha }}" || git checkout --detach FETCH_HEAD + fi + - name: Check Node runtime + run: | + node --version + npm --version - run: corepack enable && corepack prepare npm@10.5.0 --activate - name: Install dependencies run: | @@ -138,10 +189,27 @@ jobs: runs-on: ubuntu-latest needs: type-check steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} + - name: Check out repository + shell: bash + run: | + set -euo pipefail + + if [ -z "$(find . -mindepth 1 -maxdepth 1 -print -quit)" ]; then + REPOSITORY_URL="${{ gitea.server_url }}/${{ gitea.repository }}.git" + git init . + git remote add origin "$REPOSITORY_URL" + if [ -n "${{ secrets.GITHUB_TOKEN }}" ]; then + AUTH_HEADER="$(printf '%s:%s' "${{ gitea.actor }}" "${{ secrets.GITHUB_TOKEN }}" | base64 | tr -d '\n')" + git -c http.extraheader="Authorization: Basic ${AUTH_HEADER}" fetch --depth 1 origin "${{ gitea.ref }}" + else + git fetch --depth 1 origin "${{ gitea.ref }}" + fi + git checkout --detach "${{ gitea.sha }}" || git checkout --detach FETCH_HEAD + fi + - name: Check Node runtime + run: | + node --version + npm --version - run: corepack enable && corepack prepare npm@10.5.0 --activate - name: Install dependencies run: | @@ -176,10 +244,27 @@ jobs: runs-on: ubuntu-latest needs: type-check steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} + - name: Check out repository + shell: bash + run: | + set -euo pipefail + + if [ -z "$(find . -mindepth 1 -maxdepth 1 -print -quit)" ]; then + REPOSITORY_URL="${{ gitea.server_url }}/${{ gitea.repository }}.git" + git init . + git remote add origin "$REPOSITORY_URL" + if [ -n "${{ secrets.GITHUB_TOKEN }}" ]; then + AUTH_HEADER="$(printf '%s:%s' "${{ gitea.actor }}" "${{ secrets.GITHUB_TOKEN }}" | base64 | tr -d '\n')" + git -c http.extraheader="Authorization: Basic ${AUTH_HEADER}" fetch --depth 1 origin "${{ gitea.ref }}" + else + git fetch --depth 1 origin "${{ gitea.ref }}" + fi + git checkout --detach "${{ gitea.sha }}" || git checkout --detach FETCH_HEAD + fi + - name: Check Node runtime + run: | + node --version + npm --version - run: corepack enable && corepack prepare npm@10.5.0 --activate - name: Install dependencies run: | @@ -213,10 +298,27 @@ jobs: runs-on: ubuntu-latest needs: type-check steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} + - name: Check out repository + shell: bash + run: | + set -euo pipefail + + if [ -z "$(find . -mindepth 1 -maxdepth 1 -print -quit)" ]; then + REPOSITORY_URL="${{ gitea.server_url }}/${{ gitea.repository }}.git" + git init . + git remote add origin "$REPOSITORY_URL" + if [ -n "${{ secrets.GITHUB_TOKEN }}" ]; then + AUTH_HEADER="$(printf '%s:%s' "${{ gitea.actor }}" "${{ secrets.GITHUB_TOKEN }}" | base64 | tr -d '\n')" + git -c http.extraheader="Authorization: Basic ${AUTH_HEADER}" fetch --depth 1 origin "${{ gitea.ref }}" + else + git fetch --depth 1 origin "${{ gitea.ref }}" + fi + git checkout --detach "${{ gitea.sha }}" || git checkout --detach FETCH_HEAD + fi + - name: Check Node runtime + run: | + node --version + npm --version - run: corepack enable && corepack prepare npm@10.5.0 --activate - name: Install dependencies run: | @@ -277,10 +379,27 @@ jobs: JWT_EXPIRY: 8h FILE_STORAGE_ROOT: /tmp/rentaldrivego-test-storage steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} + - name: Check out repository + shell: bash + run: | + set -euo pipefail + + if [ -z "$(find . -mindepth 1 -maxdepth 1 -print -quit)" ]; then + REPOSITORY_URL="${{ gitea.server_url }}/${{ gitea.repository }}.git" + git init . + git remote add origin "$REPOSITORY_URL" + if [ -n "${{ secrets.GITHUB_TOKEN }}" ]; then + AUTH_HEADER="$(printf '%s:%s' "${{ gitea.actor }}" "${{ secrets.GITHUB_TOKEN }}" | base64 | tr -d '\n')" + git -c http.extraheader="Authorization: Basic ${AUTH_HEADER}" fetch --depth 1 origin "${{ gitea.ref }}" + else + git fetch --depth 1 origin "${{ gitea.ref }}" + fi + git checkout --detach "${{ gitea.sha }}" || git checkout --detach FETCH_HEAD + fi + - name: Check Node runtime + run: | + node --version + npm --version - run: corepack enable && corepack prepare npm@10.5.0 --activate - name: Install dependencies run: | diff --git a/apps/dashboard/src/app/(dashboard)/layout.tsx b/apps/dashboard/src/app/(dashboard)/layout.tsx index 94e2a92..633f33a 100644 --- a/apps/dashboard/src/app/(dashboard)/layout.tsx +++ b/apps/dashboard/src/app/(dashboard)/layout.tsx @@ -4,18 +4,20 @@ import TopBar from '@/components/layout/TopBar' export default function DashboardLayout({ children }: { children: React.ReactNode }) { return ( -
-
- -
-
+ +
- + +
+
+
+ +
+
+ {children} +
-
- {children} -
-
+ ) } diff --git a/production/dynamic/mail-certificate.yml b/production/dynamic/mail-certificate.yml new file mode 100644 index 0000000..ab08614 --- /dev/null +++ b/production/dynamic/mail-certificate.yml @@ -0,0 +1,18 @@ +http: + routers: + mail-certificate: + rule: "Host(`mail.rentaldrivego.ma`)" + entryPoints: + - websecure + service: mail-certificate-placeholder + tls: + certResolver: letsencrypt + domains: + - main: "mail.rentaldrivego.ma" + services: + mail-certificate-placeholder: + loadBalancer: + servers: + # This router exists to request and renew the certificate. + # Mail protocols are not routed through this HTTP service. + - url: "http://127.0.0.1:65535" diff --git a/production/traefik.yaml b/production/traefik.yaml index e6fd4dc..2fa614f 100644 --- a/production/traefik.yaml +++ b/production/traefik.yaml @@ -1,6 +1,8 @@ services: traefik: - image: traefik:latest + # Set TRAEFIK_VERSION in .env to the exact version you are running. + # Do not use "latest" in production. + image: traefik:${TRAEFIK_VERSION} command: - --api.dashboard=false - --api.insecure=false @@ -29,6 +31,51 @@ services: - ./dynamic:/etc/traefik/dynamic:ro networks: - traefik-proxy + + traefik-certs-dumper: + image: ldez/traefik-certs-dumper:v2.11.4 + depends_on: + - traefik + restart: unless-stopped + # Wait for Traefik's ACME storage, then export certificates whenever + # acme.json changes. + entrypoint: + - /bin/sh + - -ec + command: + - | + until [ -s /data/acme.json ]; do + sleep 2 + done + + exec traefik-certs-dumper file \ + --version v3 \ + --watch \ + --source /data/acme.json \ + --dest /output \ + --domain-subdir \ + --crt-name fullchain \ + --key-name privkey \ + --crt-ext .pem \ + --key-ext .pem + volumes: + # Existing Traefik ACME volume, mounted read-only. + - traefik-letsencrypt:/data:ro + + # Normal certificate files will appear on the VPS here. + - ./exported-certs:/output + # The dumper only reads a local volume and writes exported files. + # It needs no network access. + network_mode: none + labels: + - "traefik.enable=false" + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + read_only: true + tmpfs: + - /tmp:size=16m,mode=1777 networks: traefik-proxy: external: true