diff --git a/.github/workflows/caddy.yml b/.github/workflows/caddy.yml index 0ce5aa4754..fd12f3378c 100644 --- a/.github/workflows/caddy.yml +++ b/.github/workflows/caddy.yml @@ -17,7 +17,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 with: platforms: arm64 @@ -25,13 +25,13 @@ jobs: with: driver-opts: env.BUILDKIT_STEP_LOG_MAX_SIZE=-1 - name: docker.io-login - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 if: ${{ github.repository_owner == 'ZoeyVid' && github.event_name != 'pull_request' }} with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: ghcr.io-login - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 if: ${{ github.repository_owner == 'ZoeyVid' && github.event_name != 'pull_request' }} with: registry: ghcr.io diff --git a/.github/workflows/dependency-updates.yml b/.github/workflows/dependency-updates.yml index 912c83d3a1..c8bc6e2c37 100644 --- a/.github/workflows/dependency-updates.yml +++ b/.github/workflows/dependency-updates.yml @@ -17,8 +17,9 @@ jobs: AWSLC_VER="$( git ls-remote --tags --refs https://github.com/aws/aws-lc \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" AWSLC_SHA="$(git ls-remote https://github.com/aws/aws-lc "$AWSLC_VER" | cut -f1)" @@ -43,8 +44,9 @@ jobs: NGINX_VER="$( git ls-remote --tags --refs https://github.com/nginx/nginx \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" NGINX_SHA="$(git ls-remote https://github.com/nginx/nginx "$NGINX_VER" | cut -f1)" @@ -134,58 +136,60 @@ jobs: title: update zlib-ng-patch version to ${{ steps.update.outputs.version }} body: update zlib-ng-patch version to ${{ steps.update.outputs.version }} - ngx_brotli-update: + ngx_http_brotli_module-update: runs-on: ubuntu-slim steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: update ngx_brotli version + - name: update ngx_http_brotli_module version id: update run: | NB_VER="$( - git ls-remote --tags --refs https://github.com/google/ngx_brotli \ + git ls-remote --tags --refs https://github.com/HanadaLee/ngx_http_brotli_module \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" - if [ -z "$NB_VER" ]; then NB_VER=master; fi - NB_SHA="$(git ls-remote https://github.com/google/ngx_brotli "$NB_VER" | cut -f1)" + if [ "$NB_VER" = "v1.0.0rc" ]; then NB_VER=master; fi + NB_SHA="$(git ls-remote https://github.com/HanadaLee/ngx_http_brotli_module "$NB_VER" | cut -f1)" sed -i "s|ARG NB_VER=.*|ARG NB_VER=$NB_SHA # $NB_VER|" Dockerfile echo "version=$NB_VER" >> $GITHUB_OUTPUT - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: signoff: true delete-branch: true - commit-message: update ngx_brotli version to ${{ steps.update.outputs.version }} - branch: update-ngx_brotli-version - title: update ngx_brotli version to ${{ steps.update.outputs.version }} - body: update ngx_brotli version to ${{ steps.update.outputs.version }} - ngx_unbrotli-update: + commit-message: update ngx_http_brotli_module version to ${{ steps.update.outputs.version }} + branch: update-ngx_http_brotli_module-version + title: update ngx_http_brotli_module version to ${{ steps.update.outputs.version }} + body: update ngx_http_brotli_module version to ${{ steps.update.outputs.version }} + ngx_http_unbrotli_filter_module-update: runs-on: ubuntu-slim steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: update ngx_unbrotli version + - name: update ngx_http_unbrotli_filter_module version id: update run: | NUB_VER="$( - git ls-remote --tags --refs https://github.com/clyfish/ngx_unbrotli \ + git ls-remote --tags --refs https://github.com/HanadaLee/ngx_http_unbrotli_filter_module \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" if [ -z "$NUB_VER" ]; then NUB_VER=main; fi - NUB_SHA="$(git ls-remote https://github.com/clyfish/ngx_unbrotli "$NUB_VER" | cut -f1)" + NUB_SHA="$(git ls-remote https://github.com/HanadaLee/ngx_http_unbrotli_filter_module "$NUB_VER" | cut -f1)" sed -i "s|ARG NUB_VER=.*|ARG NUB_VER=$NUB_SHA # $NUB_VER|" Dockerfile echo "version=$NUB_VER" >> $GITHUB_OUTPUT - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: signoff: true delete-branch: true - commit-message: update ngx_unbrotli version to ${{ steps.update.outputs.version }} - branch: update-ngx_unbrotli-version - title: update ngx_unbrotli version to ${{ steps.update.outputs.version }} - body: update ngx_unbrotli version to ${{ steps.update.outputs.version }} + commit-message: update ngx_http_unbrotli_filter_module version to ${{ steps.update.outputs.version }} + branch: update-ngx_http_unbrotli_filter_module-version + title: update ngx_http_unbrotli_filter_module version to ${{ steps.update.outputs.version }} + body: update ngx_http_unbrotli_filter_module version to ${{ steps.update.outputs.version }} zstd-nginx-module-update: runs-on: ubuntu-slim steps: @@ -194,14 +198,14 @@ jobs: id: update run: | ZNM_VER="$( - git ls-remote --tags --refs https://github.com/tokers/zstd-nginx-module \ + git ls-remote --tags --refs https://github.com/hsw/zstd-nginx-module \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" - if [ "$ZNM_VER" = "0.1.1" ]; then ZNM_VER=master; fi - ZNM_SHA="$(git ls-remote https://github.com/tokers/zstd-nginx-module "$ZNM_VER" | cut -f1)" + ZNM_SHA="$(git ls-remote https://github.com/hsw/zstd-nginx-module "$ZNM_VER" | cut -f1)" sed -i "s|ARG ZNM_VER=.*|ARG ZNM_VER=$ZNM_SHA # $ZNM_VER|" Dockerfile echo "version=$ZNM_VER" >> $GITHUB_OUTPUT - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 @@ -222,8 +226,9 @@ jobs: NHUZFM_VER="$( git ls-remote --tags --refs https://github.com/HanadaLee/ngx_http_unzstd_filter_module \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" if [ -z "$NHUZFM_VER" ]; then NHUZFM_VER=main; fi @@ -248,8 +253,9 @@ jobs: NF_VER="$( git ls-remote --tags --refs https://github.com/aperezdc/ngx-fancyindex \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" NF_SHA="$(git ls-remote https://github.com/aperezdc/ngx-fancyindex "$NF_VER" | cut -f1)" @@ -273,8 +279,9 @@ jobs: HMNM_VER="$( git ls-remote --tags --refs https://github.com/openresty/headers-more-nginx-module \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" HMNM_SHA="$(git ls-remote https://github.com/openresty/headers-more-nginx-module "$HMNM_VER" | cut -f1)" @@ -298,8 +305,9 @@ jobs: NDK_VER="$( git ls-remote --tags --refs https://github.com/vision5/ngx_devel_kit \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" NDK_SHA="$(git ls-remote https://github.com/vision5/ngx_devel_kit "$NDK_VER" | cut -f1)" @@ -323,8 +331,9 @@ jobs: LNM_VER="$( git ls-remote --tags --refs https://github.com/openresty/lua-nginx-module \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" LNM_SHA="$(git ls-remote https://github.com/openresty/lua-nginx-module "$LNM_VER" | cut -f1)" @@ -350,8 +359,9 @@ jobs: NJS_VER="$( git ls-remote --tags --refs https://github.com/nginx/njs \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" NJS_SHA="$(git ls-remote https://github.com/nginx/njs "$NJS_VER" | cut -f1)" @@ -375,8 +385,9 @@ jobs: NAL_VER="$( git ls-remote --tags --refs https://github.com/kvspb/nginx-auth-ldap \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" if [ "$NAL_VER" = "v0.1" ]; then NAL_VER=master; fi @@ -401,8 +412,9 @@ jobs: VTS_VER="$( git ls-remote --tags --refs https://github.com/vozlt/nginx-module-vts \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" VTS_SHA="$(git ls-remote https://github.com/vozlt/nginx-module-vts "$VTS_VER" | cut -f1)" @@ -426,8 +438,9 @@ jobs: NNTLM_VER="$( git ls-remote --tags --refs https://github.com/gabihodoroaga/nginx-ntlm-module \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" if [ "$NNTLM_VER" = "v1.19.3-beta.1" ]; then NNTLM_VER=master; fi @@ -452,8 +465,9 @@ jobs: NHG2M_VER="$( git ls-remote --tags --refs https://github.com/leev/ngx_http_geoip2_module \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" NHG2M_SHA="$(git ls-remote https://github.com/leev/ngx_http_geoip2_module "$NHG2M_VER" | cut -f1)" @@ -478,8 +492,9 @@ jobs: LRC_VER="$( git ls-remote --tags --refs https://github.com/openresty/lua-resty-core \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" LRC_SHA="$(git ls-remote https://github.com/openresty/lua-resty-core "$LRC_VER" | cut -f1)" @@ -503,8 +518,9 @@ jobs: LRL_VER="$( git ls-remote --tags --refs https://github.com/openresty/lua-resty-lrucache \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" LRL_SHA="$(git ls-remote https://github.com/openresty/lua-resty-lrucache "$LRL_VER" | cut -f1)" @@ -529,8 +545,9 @@ jobs: LCSB_VER="$( git ls-remote --tags --refs https://github.com/crowdsecurity/lua-cs-bouncer \ | cut -d/ -f3 \ + | sed 's|rc|~&|I' \ | sort -V \ - | grep -vi rc \ + | sed 's|~||' \ | tail -1 )" LCSB_SHA="$(git ls-remote https://github.com/crowdsecurity/lua-cs-bouncer "$LCSB_VER" | cut -f1)" diff --git a/.github/workflows/docker-beta.yml b/.github/workflows/docker-beta.yml index 8b55b622a6..be848bee2f 100644 --- a/.github/workflows/docker-beta.yml +++ b/.github/workflows/docker-beta.yml @@ -16,17 +16,15 @@ jobs: with: driver-opts: env.BUILDKIT_STEP_LOG_MAX_SIZE=-1 - name: ghcr.io-login - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: zoeyvid password: ${{ github.token }} - name: version run: | - sudo apt-get update - sudo apt-get install -y moreutils - jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' frontend/package.json | sponge frontend/package.json - jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' backend/package.json | sponge backend/package.json + jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' frontend/package.json > frontend/package.json.tmp && mv frontend/package.json.tmp frontend/package.json + jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' backend/package.json > backend/package.json.tmp && mv backend/package.json.tmp backend/package.json - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . @@ -44,16 +42,15 @@ jobs: with: driver-opts: env.BUILDKIT_STEP_LOG_MAX_SIZE=-1 - name: ghcr.io-login - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: zoeyvid password: ${{ github.token }} - name: version run: | - sudo apt-get install -y moreutils - jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' frontend/package.json | sponge frontend/package.json - jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' backend/package.json | sponge backend/package.json + jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' frontend/package.json > frontend/package.json.tmp && mv frontend/package.json.tmp frontend/package.json + jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' backend/package.json > backend/package.json.tmp && mv backend/package.json.tmp backend/package.json - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . @@ -68,12 +65,12 @@ jobs: if: ${{ github.repository_owner == 'ZoeyVid' }} steps: - name: docker.io-login - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: ghcr.io-login - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: zoeyvid diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml index f42716df35..40056a7b64 100644 --- a/.github/workflows/docker-develop.yml +++ b/.github/workflows/docker-develop.yml @@ -14,7 +14,7 @@ jobs: with: driver-opts: env.BUILDKIT_STEP_LOG_MAX_SIZE=-1 - name: ghcr.io-login - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 if: ${{ github.repository_owner == 'ZoeyVid' && github.event_name != 'pull_request' }} with: registry: ghcr.io @@ -22,10 +22,8 @@ jobs: password: ${{ github.token }} - name: version run: | - sudo apt-get update - sudo apt-get install -y moreutils - jq --arg v "$(git rev-parse --short HEAD)" '.version = $v' frontend/package.json | sponge frontend/package.json - jq --arg v "$(git rev-parse --short HEAD)" '.version = $v' backend/package.json | sponge backend/package.json + jq --arg v "$(git rev-parse --short HEAD)" '.version = $v' frontend/package.json > frontend/package.json.tmp && mv frontend/package.json.tmp frontend/package.json + jq --arg v "$(git rev-parse --short HEAD)" '.version = $v' backend/package.json > backend/package.json.tmp && mv backend/package.json.tmp backend/package.json - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . @@ -42,7 +40,7 @@ jobs: with: driver-opts: env.BUILDKIT_STEP_LOG_MAX_SIZE=-1 - name: ghcr.io-login - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 if: ${{ github.repository_owner == 'ZoeyVid' && github.event_name != 'pull_request' }} with: registry: ghcr.io @@ -50,10 +48,8 @@ jobs: password: ${{ github.token }} - name: version run: | - sudo apt-get update - sudo apt-get install -y moreutils - jq --arg v "$(git rev-parse --short HEAD)" '.version = $v' frontend/package.json | sponge frontend/package.json - jq --arg v "$(git rev-parse --short HEAD)" '.version = $v' backend/package.json | sponge backend/package.json + jq --arg v "$(git rev-parse --short HEAD)" '.version = $v' frontend/package.json > frontend/package.json.tmp && mv frontend/package.json.tmp frontend/package.json + jq --arg v "$(git rev-parse --short HEAD)" '.version = $v' backend/package.json > backend/package.json.tmp && mv backend/package.json.tmp backend/package.json - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . @@ -68,12 +64,12 @@ jobs: if: ${{ github.repository_owner == 'ZoeyVid' && github.event_name != 'pull_request' }} steps: - name: docker.io-login - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: ghcr.io-login - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: zoeyvid diff --git a/.github/workflows/docker-latest.yml b/.github/workflows/docker-latest.yml index 20db08cca8..37e8a44f66 100644 --- a/.github/workflows/docker-latest.yml +++ b/.github/workflows/docker-latest.yml @@ -16,16 +16,15 @@ jobs: with: driver-opts: env.BUILDKIT_STEP_LOG_MAX_SIZE=-1 - name: ghcr.io-login - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: zoeyvid password: ${{ github.token }} - name: version run: | - sudo apt-get install -y moreutils - jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' frontend/package.json | sponge frontend/package.json - jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' backend/package.json | sponge backend/package.json + jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' frontend/package.json > frontend/package.json.tmp && mv frontend/package.json.tmp frontend/package.json + jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' backend/package.json > backend/package.json.tmp && mv backend/package.json.tmp backend/package.json - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . @@ -43,17 +42,15 @@ jobs: with: driver-opts: env.BUILDKIT_STEP_LOG_MAX_SIZE=-1 - name: ghcr.io-login - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: zoeyvid password: ${{ github.token }} - name: version run: | - sudo apt-get update - sudo apt-get install -y moreutils - jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' frontend/package.json | sponge frontend/package.json - jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' backend/package.json | sponge backend/package.json + jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' frontend/package.json > frontend/package.json.tmp && mv frontend/package.json.tmp frontend/package.json + jq --arg v "${{ inputs.tag }}-$(git rev-parse --short HEAD)-$(cat .version)" '.version = $v' backend/package.json > backend/package.json.tmp && mv backend/package.json.tmp backend/package.json - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . @@ -68,12 +65,12 @@ jobs: if: ${{ github.repository_owner == 'ZoeyVid' }} steps: - name: docker.io-login - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: ghcr.io-login - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: zoeyvid diff --git a/.github/workflows/lint-and-format.yml b/.github/workflows/lint-and-format.yml index 8616052842..879241fbd8 100644 --- a/.github/workflows/lint-and-format.yml +++ b/.github/workflows/lint-and-format.yml @@ -8,6 +8,8 @@ on: jobs: lint-and-format: runs-on: ubuntu-slim + env: + BIOME: ${{ github.event_name == 'pull_request' && 'ci' || 'check --write' }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -16,28 +18,23 @@ jobs: - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: version: latest - - name: sponge - run: | - sudo apt-get update - sudo apt-get install -y moreutils - name: backend run: | set -x cd backend pnpm install --frozen-lockfile - pnpm biome lint --write - pnpm biome format --write + pnpm biome $BIOME --reporter=default --reporter=github | sed "s|,file=|,file=backend/|g" || echo "BIOME_FAILED=1" >> "$GITHUB_ENV" - name: frontend run: | set -x cd frontend pnpm install --frozen-lockfile - pnpm biome lint --write - pnpm biome format --write + pnpm biome $BIOME --reporter=default --reporter=github | sed "s|,file=|,file=frontend/|g" || echo "BIOME_FAILED=1" >> "$GITHUB_ENV" ./src/locale/scripts/locale-sort.sh pnpm formatjs compile-folder src/locale/src src/locale/lang - pnpm tsc --noEmit + pnpm tsc -b pnpm vitest + pnpm vite build - name: nginxbeautifier run: | pnpm add -g nginxbeautifier @@ -51,3 +48,9 @@ jobs: branch: lint-and-format title: lint-and-format body: lint-and-format + - name: diff + if: ${{ github.event_name == 'pull_request' }} + run: git diff --exit-code + - name: biome + if: ${{ env.BIOME_FAILED == '1' }} + run: exit 1 diff --git a/Dockerfile b/Dockerfile index cb827ccaa3..070ca9a2a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,25 +5,25 @@ SHELL ["/bin/ash", "-eo", "pipefail", "-c"] ARG LUAJIT_INC=/usr/include/luajit-2.1 ARG LUAJIT_LIB=/usr/lib -ARG AWSLC_VER=970583b1061b407d09b5fc1f18bbba38690e1684 # v5.3.0 +ARG AWSLC_VER=f6acf748df0ea6157d55e640730b38d21a7751cd # v5.4.0 ARG NGINX_VER=a885808aa592eea32c8064624717be3d94fbdfe7 # release-1.31.3 ARG DTR_VER=1.29.2 ARG RCP_VER=1.31.3 ARG ZNP_VER=1.30.0 -ARG NB_VER=a71f9312c2deb28875acc7bacfdd5695a111aa53 # master -ARG NUB_VER=60bed634504967a323645f8f53566cca3f2c3f53 # main -ARG ZNM_VER=057a7d339af1111d04b5a9ac5ae9b0250d17cd94 # master -ARG NHUZFM_VER=37e77ed348c242e222f2ae2b02c2e445e0ee2dc6 # main +ARG NB_VER=c9eb4c75c1691e0ddbf23a490336aa5910cbea69 # master +ARG NUB_VER=34a5311898601fa2efa0f592b287d5384e0bb165 # main +ARG ZNM_VER=53927b6408ebf166496a3d79f016563f7f720cb0 # v0.4.0 +ARG NHUZFM_VER=3f0d54ff455b963fc4bfcc1fbe4c49ae464a085e # main ARG NF_VER=047589e4dc0041517b8a47739fa960c430c4045e # v0.6.0 ARG HMNM_VER=0bf283ff92017acd616814b0e5153e0ccf93e2c9 # v0.40 ARG NDK_VER=bd44d16302273052d6005d7bdb55f74e23813de3 # v0.3.4 -ARG LNM_VER=4b21d8f5fd3cc94fd25c530b3a61405af9666d0b # v0.10.31 +ARG LNM_VER=de7e57f4b6c12699b4c47b6abb70817f1124026f # v0.10.32rc3 ARG NJS_VER=ad60b62c3b4ca6339ca19c19ceed8c942dbe575d # 1.0.0 ARG NAL_VER=241200eac8e4acae74d353291bd27f79e5ca3dc4 # master -ARG VTS_VER=b2a036ab6c1ffd5615f9ea57d6710287590735cd # v0.2.5 +ARG VTS_VER=4875eef7767940528c60ffd293fb2348232d8a62 # v0.2.6 ARG NNTLM_VER=3da77b0cb30e517dfee01d7e7f7d649144d24051 # master ARG NHG2M_VER=cbaa35461c62a99d2577e6bae3273492502d8769 # 3.4 @@ -33,10 +33,10 @@ COPY patches/*.patch /src COPY rootfs/usr/local/bin/git-clone-commit.sh /usr/local/bin/git-clone-commit.sh RUN apk upgrade --no-cache -a && \ - apk add --no-cache git clang lld compiler-rt llvm-libunwind-dev libc++-dev linux-headers cmake ninja make llvm file \ + apk add --no-cache git clang lld compiler-rt llvm-libunwind-dev libc++-dev linux-headers cmake ninja make pkgconf llvm file \ libatomic_ops-dev pcre2-dev luajit-dev zlib-ng-dev brotli-dev zstd-dev libxslt-dev openldap-dev quickjs-ng-dev libmaxminddb-dev clang-dev -RUN for f in $(apk info --no-cache -qL libgcc-static libstdc++-dev); do rm /"$f"; done && \ +RUN for f in $(apk info --no-cache -qL libgcc-static libstdc++-dev); do rm -v /"$f"; done && \ echo "-fuse-ld=lld --rtlib=compiler-rt --unwindlib=libunwind -stdlib=libc++ -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE" | tee /etc/clang*/*.cfg ARG CC=clang @@ -45,7 +45,7 @@ ARG LD=ld.lld ARG AR=llvm-ar ARG FLAGS -ARG CFLAGS="$FLAGS -m64 -O3 -pipe -flto=full -ffunction-sections -fdata-sections -fno-math-errno -ffp-contract=fast -fstack-clash-protection -fstack-protector-strong -fzero-call-used-regs=used-gpr -fstrict-flex-arrays=3 -ftrivial-auto-var-init=zero -fno-delete-null-pointer-checks -fno-strict-overflow -fno-strict-aliasing -fno-semantic-interposition -fno-plt -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -Wformat=2 -Werror=format-security -Wno-sign-compare" +ARG CFLAGS="$FLAGS -m64 -O3 -pipe -flto=full -ffunction-sections -fdata-sections -fno-math-errno -ffp-contract=fast -fstack-clash-protection -fstack-protector-strong -fzero-call-used-regs=used-gpr -fstrict-flex-arrays=3 -ftrivial-auto-var-init=zero -fno-delete-null-pointer-checks -fno-strict-overflow -fno-strict-aliasing -fno-semantic-interposition -fno-plt -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -Wformat=2 -Werror=format-security" ARG CXXFLAGS="$CFLAGS" ARG LDFLAGS="-m64 -Wl,-s -Wl,-O2 -Wl,--lto-O3 -Wl,--icf=safe -Wl,--gc-sections -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now -Wl,--sort-common -Wl,--as-needed -Wl,-z,pack-relative-relocs -Wl,--no-copy-dt-needed-entries" @@ -79,30 +79,20 @@ RUN git-clone-commit.sh https://github.com/nginx/nginx "$NGINX_VER" /src/nginx & echo "bcd0f2fb9723fc1f251f94cead8d5160e767f7d4a04365331396a72a9ba54c6b /src/nginx/6.patch" | sha256sum -c - && \ git apply /src/nginx/6.patch && \ wget -q https://patch-diff.githubusercontent.com/raw/nginx/nginx/pull/1593.patch -O /src/nginx/7.patch && \ - echo "42951e1b3aab34995cff6004ce4ba3a84d86d73ae08bbb35e7a3979a5f2e2df1 /src/nginx/7.patch" | sha256sum -c - && \ + echo "04c4e7ff7289aa9719f3db88fe5fa1e139968d41179331488b3e118fa30616ba /src/nginx/7.patch" | sha256sum -c - && \ git apply /src/nginx/7.patch && \ + wget -q https://patch-diff.githubusercontent.com/raw/nginx/nginx/pull/1430.patch -O /src/nginx/8.patch && \ + echo "c8e827d50314b6ec027677ae8c70b11f805408af3efb5175bf377071bd2a14a5 /src/nginx/8.patch" | sha256sum -c - && \ + git apply /src/nginx/8.patch && \ git apply /src/nginx-footer.patch && \ git apply /src/nginx-ip-sni.patch && \ - git apply /src/nginx-gso-fix.patch && \ git apply /src/nginx-buffer-log.patch && \ git apply /src/nginx-ech-boringssl-awslc.patch && \ git apply /src/nginx-cert-compression-brotli.patch && \ \ - git-clone-commit.sh https://github.com/google/ngx_brotli "$NB_VER" /src/ngx_brotli && \ - cd /src/ngx_brotli && \ - git apply /src/ngx_brotli.patch && \ - git-clone-commit.sh https://github.com/clyfish/ngx_unbrotli "$NUB_VER" /src/ngx_unbrotli && \ - cd /src/ngx_unbrotli && \ - git apply /src/ngx_unbrotli.patch && \ - git-clone-commit.sh https://github.com/tokers/zstd-nginx-module "$ZNM_VER" /src/zstd-nginx-module && \ - cd /src/zstd-nginx-module && \ - wget -q https://patch-diff.githubusercontent.com/raw/tokers/zstd-nginx-module/pull/23.patch -O /src/zstd-nginx-module/1.patch && \ - echo "7bd3c71770305ab44defe5e2768a62d870061645b095b9564d4afd57a64ad3b9 /src/zstd-nginx-module/1.patch" | sha256sum -c - && \ - wget -q https://patch-diff.githubusercontent.com/raw/tokers/zstd-nginx-module/pull/44.patch -O /src/zstd-nginx-module/2.patch && \ - echo "577dc3e2d6e0378520cee6f621fa9824dd571992185cb58e2198ffa9bf814c6f /src/zstd-nginx-module/2.patch" | sha256sum -c - && \ - git apply /src/zstd-nginx-module.patch && \ - git apply /src/zstd-nginx-module/1.patch && \ - git apply /src/zstd-nginx-module/2.patch && \ + git-clone-commit.sh https://github.com/HanadaLee/ngx_http_brotli_module "$NB_VER" /src/ngx_http_brotli_module && \ + git-clone-commit.sh https://github.com/HanadaLee/ngx_http_unbrotli_filter_module "$NUB_VER" /src/ngx_http_unbrotli_filter_module && \ + git-clone-commit.sh https://github.com/hsw/zstd-nginx-module "$ZNM_VER" /src/zstd-nginx-module && \ git-clone-commit.sh https://github.com/HanadaLee/ngx_http_unzstd_filter_module "$NHUZFM_VER" /src/ngx_http_unzstd_filter_module && \ git-clone-commit.sh https://github.com/aperezdc/ngx-fancyindex "$NF_VER" /src/ngx-fancyindex && \ cd /src/ngx-fancyindex && \ @@ -149,8 +139,8 @@ RUN cd /src/nginx && \ --with-http_addition_module \ --with-http_stub_status_module \ --with-http_auth_request_module \ - --add-module=/src/ngx_brotli \ - --add-module=/src/ngx_unbrotli \ + --add-module=/src/ngx_http_brotli_module \ + --add-module=/src/ngx_http_unbrotli_filter_module \ --add-module=/src/zstd-nginx-module \ --add-module=/src/ngx_http_unzstd_filter_module \ --add-module=/src/ngx-fancyindex \ @@ -162,6 +152,7 @@ RUN cd /src/nginx && \ --add-dynamic-module=/src/nginx-module-vts \ --add-dynamic-module=/src/nginx-ntlm-module \ --add-dynamic-module=/src/ngx_http_geoip2_module \ + --with-cc-opt="-DZSTD_STATIC_LINKING_ONLY" \ --with-ld-opt="$LDFLAGS" && \ \ make -j "$(nproc)" install @@ -188,13 +179,13 @@ COPY frontend/package.json frontend/pnpm-lock.yaml frontend/pnpm-workspace.yaml RUN apk upgrade --no-cache -a && \ apk add --no-cache nodejs pnpm llvm file gzip brotli zstd && \ pnpm install --frozen-lockfile && \ - pnpm cache delete && \ find /app/node_modules -name "*.map" -delete && \ find /app/node_modules -name "*.node" -type f -exec llvm-strip -s {} \; && \ find /app/node_modules -name "*.node" -type f -exec file {} \; COPY frontend /app RUN pnpm formatjs compile-folder src/locale/src src/locale/lang && \ - pnpm tsc && \ + pnpm tsc -b && \ + pnpm vitest && \ pnpm vite build && \ find /app/dist -type f ! -name '*.jpg' ! -name '*.png' ! -name '*.br' ! -name '*.gz' ! -name '*.zst' -exec gzip -9 -k {} \; && \ find /app/dist -type f ! -name '*.jpg' ! -name '*.png' ! -name '*.br' ! -name '*.gz' ! -name '*.zst' -exec brotli -q 11 {} \; && \ @@ -208,9 +199,14 @@ COPY backend/package.json backend/pnpm-lock.yaml backend/pnpm-workspace.yaml /ap RUN apk upgrade --no-cache -a && \ apk add --no-cache nodejs pnpm llvm file && \ pnpm install --frozen-lockfile --prod && \ - pnpm cache delete && \ find /app/node_modules -name "*.map" -delete && \ - rm -r /app/node_modules/better-sqlite3/deps/sqlite3 && \ + rm -vr /app/node_modules/better-sqlite3/deps/sqlite3 && \ + case "$(uname -m)" in \ + x86_64) keep="linuxmusl-x64.node" ;; \ + aarch64) keep="linuxmusl-arm64.node" ;; \ + *) keep="linuxmusl-*.node" ;; \ + esac && \ + find /app/node_modules/better-sqlite3/prebuilds -name '*.node' ! -name "$keep" -delete && \ find /app/node_modules -name "*.node" -type f -exec llvm-strip -s {} \; && \ find /app/node_modules -name "*.node" -type f -exec file {} \; COPY backend /app @@ -219,9 +215,9 @@ COPY backend /app FROM alpine:3.24.1@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b SHELL ["/bin/ash", "-eo", "pipefail", "-c"] ENV NODE_ENV=production -ARG LRC_VER=6fec23e2149c88b33b39fec8a5ebdd67a3e0dd88 # v0.1.34rc3 +ARG LRC_VER=be42297c57dc2393cdbb09d4597d9d4840a7c769 # v0.1.35rc1 ARG LRL_VER=3ff6300e68b73ba20e909c7d16bd839aef2e5a4b # v0.15 -ARG LCSB_VER=c6fe0130caf1a668d1893dcc5a32f41d98cbd695 # v1.0.15 +ARG LCSB_VER=35455a64e11368b3df73a381b09c056a3ee77e24 # main COPY --from=nginx /usr/local/nginx /usr/local/nginx COPY --from=nginx /usr/local/bin/bssl /usr/local/bin/bssl @@ -236,7 +232,7 @@ COPY COPYING /COPYING WORKDIR /app RUN apk upgrade --no-cache -a && \ - apk add --no-cache tzdata tini llvm-libunwind libc++ \ + apk add --no-cache tzdata tini dinit llvm-libunwind libc++ \ pcre2 luajit zlib-ng brotli zstd lua5.1-cjson libxml2 libldap quickjs-ng-libs libmaxminddb-libs \ curl coreutils findutils grep jq openssl shadow su-exec util-linux-misc \ bash bash-completion nano \ @@ -260,13 +256,12 @@ RUN apk upgrade --no-cache -a && \ make -j "$(nproc)" install LUA_LIB_DIR=/usr/local/share/lua/5.1 && \ \ git-clone-commit.sh https://github.com/crowdsecurity/lua-cs-bouncer "$LCSB_VER" /src/lua-cs-bouncer && \ - sed -i '748s| then$| and err ~= "" then|' /src/lua-cs-bouncer/lib/crowdsec.lua && \ mv /src/lua-cs-bouncer/lib/* /usr/local/share/lua/5.1 && \ mv /src/lua-cs-bouncer/templates/captcha.html /etc/captcha.html.original && \ mv /src/lua-cs-bouncer/templates/ban.html /etc/ban.html.original && \ \ cd && \ - rm -r /src /tmp/luarocks_local_cache-* && \ + rm -vr /src /tmp/luarocks_local_cache-* && \ apk del --no-cache luarocks5.1 git make && \ \ sed -i "s|placeholder|$(cat /app/package.json | jq -r .version)|g" /usr/local/nginx/conf/conf.d/crowdsec.conf.disabled && \ @@ -288,7 +283,7 @@ RUN apk upgrade --no-cache -a && \ COPY --from=frontend /app/dist /app/frontend ENTRYPOINT ["tini", "--", "entrypoint.sh"] -HEALTHCHECK CMD healthcheck.sh +HEALTHCHECK CMD ["healthcheck.sh"] LABEL com.centurylinklabs.watchtower.monitor-only="true" LABEL wud.watch="false" diff --git a/backend/biome.json b/backend/biome.json index 61f66c36d6..7efc09da91 100644 --- a/backend/biome.json +++ b/backend/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.6/schema.json", "vcs": { "enabled": true, "clientKind": "git", @@ -7,7 +7,7 @@ }, "files": { "ignoreUnknown": false, - "includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "!**/dist/**/*"] + "includes": ["**", "!**/*.json"] }, "formatter": { "enabled": true, @@ -16,58 +16,22 @@ "lineWidth": 120, "formatWithErrors": true }, - "assist": { - "actions": { - "source": { - "organizeImports": { - "level": "on", - "options": { - "groups": [ - ":BUN:", - ":NODE:", - ["npm:*", "npm:*/**"], - ":PACKAGE_WITH_PROTOCOL:", - ":URL:", - ":PACKAGE:", - ["/src/*", "/src/**"], - ["/**"], - ["#*", "#*/**"], - ":PATH:" - ] - } - } - } - } - }, "linter": { "enabled": true, "rules": { - "preset": "recommended", - "correctness": { - "useUniqueElementIds": "off" - }, - "suspicious": { - "noExplicitAny": "off" - }, - "performance": { - "noDelete": "off" - }, - "nursery": "off", - "a11y": { - "useSemanticElements": "off", - "useValidAnchor": "off" - }, - "style": { - "noParameterAssign": "error", - "useAsConstAssertion": "error", - "useDefaultParameterLast": "error", - "useEnumInitializers": "error", - "useSelfClosingElements": "error", - "useSingleVarDeclarator": "error", - "noUnusedTemplateLiteral": "error", - "useNumberNamespace": "error", - "noInferrableTypes": "error", - "noUselessElse": "error" + "preset": "recommended" + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" } } } diff --git a/backend/index.js b/backend/index.js index df32a84276..3d6515f2c9 100755 --- a/backend/index.js +++ b/backend/index.js @@ -1,9 +1,9 @@ #!/usr/bin/env node import app from "./app.js"; -import internalNginx from "./internal/nginx.js"; import internalCertificate from "./internal/certificate.js"; import internalIpRanges from "./internal/ip_ranges.js"; +import internalNginx from "./internal/nginx.js"; import { global as logger } from "./logger.js"; import { migrateUp } from "./migrate.js"; import { getCompiledSchema } from "./schema/index.js"; diff --git a/backend/internal/2fa.js b/backend/internal/2fa.js index 1b6d15267c..6f275ebdf5 100644 --- a/backend/internal/2fa.js +++ b/backend/internal/2fa.js @@ -3,6 +3,7 @@ import bcrypt from "bcryptjs"; import { createGuardrails, generateSecret, generateURI, verify } from "otplib"; import errs from "../lib/error.js"; import authModel from "../models/auth.js"; +import internalAuditLog from "./audit-log.js"; import internalUser from "./user.js"; const APP_NAME = "NPMplus"; @@ -70,6 +71,9 @@ const internal2fa = { */ startSetup: async (access, userId) => { await access.can("users:password", userId); + if (Number(userId) !== access.token.getUserId(0)) { + throw new errs.PermissionError("2FA can only be managed for your own account"); + } const user = await internalUser.get(access, { id: userId }); const secret = generateSecret(); const otpauth_url = generateURI({ @@ -108,7 +112,10 @@ const internal2fa = { */ enable: async (access, userId, code) => { await access.can("users:password", userId); - await internalUser.get(access, { id: userId }); + if (Number(userId) !== access.token.getUserId(0)) { + throw new errs.PermissionError("2FA can only be managed for your own account"); + } + const user = await internalUser.get(access, { id: userId }); const auth = await internal2fa.getUserPasswordAuth(userId); const secret = auth?.meta?.totp_pending_secret || false; @@ -141,6 +148,16 @@ const internal2fa = { .andWhere("type", "password") .patch({ meta }); + await internalAuditLog.add(access, { + action: "updated", + object_type: "user", + object_id: user.id, + meta: { + name: user.name, + totp_enabled: true, + }, + }); + return { backup_codes: plain }; }, @@ -154,11 +171,13 @@ const internal2fa = { */ disable: async (access, userId, code) => { await access.can("users:password", userId); - await internalUser.get(access, { id: userId }); + if (Number(userId) !== access.token.getUserId(0)) { + throw new errs.PermissionError("2FA can only be managed for your own account"); + } + const user = await internalUser.get(access, { id: userId }); const auth = await internal2fa.getUserPasswordAuth(userId); - const enabled = auth?.meta?.totp_enabled === true; - if (!enabled) { + if (auth?.meta?.totp_enabled !== true) { throw new errs.ValidationError("2FA is not enabled"); } @@ -226,6 +245,54 @@ const internal2fa = { .andWhere("user_id", userId) .andWhere("type", "password") .patch({ meta }); + + await internalAuditLog.add(access, { + action: "updated", + object_type: "user", + object_id: user.id, + meta: { + name: user.name, + totp_enabled: false, + }, + }); + }, + + adminDisable: async (access, userId) => { + await access.can("users:2fadisable", userId); + if (Number(userId) === access.token.getUserId(0)) { + throw new errs.ValidationError("Missing required parameter: code"); + } + const user = await internalUser.get(access, { id: userId }); + const auth = await internal2fa.getUserPasswordAuth(userId); + + if (auth?.meta?.totp_enabled !== true) { + throw new errs.ValidationError("2FA is not enabled"); + } + + const meta = { ...auth.meta }; + delete meta.totp_secret; + delete meta.totp_enabled; + delete meta.totp_enabled_at; + delete meta.totp_pending_secret; + delete meta.backup_codes; + + await authModel + .query() + .where("id", auth.id) + .andWhere("user_id", userId) + .andWhere("type", "password") + .patch({ meta }); + + await internalAuditLog.add(access, { + action: "updated", + object_type: "user", + object_id: user.id, + meta: { + name: user.name, + totp_enabled: false, + recovery: true, + }, + }); }, /** @@ -297,7 +364,10 @@ const internal2fa = { */ regenerateBackupCodes: async (access, userId, token) => { await access.can("users:password", userId); - await internalUser.get(access, { id: userId }); + if (Number(userId) !== access.token.getUserId(0)) { + throw new errs.PermissionError("2FA can only be managed for your own account"); + } + const user = await internalUser.get(access, { id: userId }); const auth = await internal2fa.getUserPasswordAuth(userId); const enabled = auth?.meta?.totp_enabled === true; const secret = auth?.meta?.totp_secret || false; @@ -311,54 +381,23 @@ const internal2fa = { const tokenTrim = token.trim(); - if (tokenTrim.length !== 6 && tokenTrim.length !== 8) { + if (tokenTrim.length !== 6) { throw new errs.ValidationError("Invalid verification code"); } - // Try TOTP code first, if it's 6 chars. it will throw errors if it's not 6 chars - // and the backup codes are 8 chars. - if (tokenTrim.length === 6) { - const result = await verify({ - token: tokenTrim, - secret, - // These guardrails lower the minimum length requirement for secrets. - // In v12 of otplib the default minimum length is 10 and in v13 it is 16. - // Since there are 2fa secrets in the wild generated with v12 we need to allow shorter secrets - // so people won't be locked out when upgrading. - guardrails: createGuardrails({ - MIN_SECRET_BYTES: 10, - }), - }); - - if (!result.valid) { - throw new errs.ValidationError("Invalid verification code"); - } - } - - // Try backup codes - if (tokenTrim.length === 8) { - const backupCodes = auth?.meta?.backup_codes || []; - let invalid = true; - for (let i = 0; i < backupCodes.length; i++) { - const match = await bcrypt.compare(tokenTrim.toUpperCase(), backupCodes[i]); - if (match) { - // Remove used backup code - const updatedCodes = [...backupCodes]; - updatedCodes.splice(i, 1); - const meta = { ...auth.meta, backup_codes: updatedCodes }; - await authModel - .query() - .where("id", auth.id) - .andWhere("user_id", userId) - .andWhere("type", "password") - .patch({ meta }); - invalid = false; - } - } - - if (invalid) { - throw new errs.ValidationError("Invalid verification code"); - } + const result = await verify({ + token: tokenTrim, + secret, + // These guardrails lower the minimum length requirement for secrets. + // In v12 of otplib the default minimum length is 10 and in v13 it is 16. + // Since there are 2fa secrets in the wild generated with v12 we need to allow shorter secrets + // so people won't be locked out when upgrading. + guardrails: createGuardrails({ + MIN_SECRET_BYTES: 10, + }), + }); + if (!result.valid) { + throw new errs.ValidationError("Invalid verification code"); } const { plain, hashed } = await generateBackupCodes(); @@ -371,6 +410,16 @@ const internal2fa = { .andWhere("type", "password") .patch({ meta }); + await internalAuditLog.add(access, { + action: "updated", + object_type: "user", + object_id: user.id, + meta: { + name: user.name, + backup_codes_regenerated: true, + }, + }); + return { backup_codes: plain }; }, diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js index 830cc1d057..95bdeeb511 100644 --- a/backend/internal/access-list.js +++ b/backend/internal/access-list.js @@ -1,4 +1,4 @@ -import { unlink, rm, writeFile, appendFile } from "node:fs/promises"; +import { appendFile, rm, unlink, writeFile } from "node:fs/promises"; import bcrypt from "bcryptjs"; import _ from "lodash"; import errs from "../lib/error.js"; @@ -71,7 +71,7 @@ const internalAccessList = { ); // Audit log - data.meta = _.assign({}, data.meta || {}, freshRow.meta); + data.meta = { ...data.meta, ...freshRow.meta }; await internalAccessList.build(freshRow); if (Number.parseInt(freshRow.proxy_host_count, 10)) { // locations don't have accessList objects, only IDs, so populate it with the object itself diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 75bf9a9dce..e360d22f39 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -1,6 +1,7 @@ import { createPrivateKey, X509Certificate } from "node:crypto"; -import { mkdir, open, readFile, realpath, rm, stat, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, open, readFile, realpath, rm, stat, writeFile } from "node:fs/promises"; import net from "node:net"; +import { tmpdir } from "node:os"; import path from "node:path"; import { domainToASCII } from "node:url"; import { ZipArchive } from "archiver"; @@ -12,6 +13,10 @@ import error from "../lib/error.js"; import utils from "../lib/utils.js"; import { debug, ssl as logger } from "../logger.js"; import certificateModel from "../models/certificate.js"; +import deadHostModel from "../models/dead_host.js"; +import proxyHostModel from "../models/proxy_host.js"; +import redirectionHostModel from "../models/redirection_host.js"; +import streamModel from "../models/stream.js"; import pjson from "../package.json" with { type: "json" }; import internalAuditLog from "./audit-log.js"; import internalNginx from "./nginx.js"; @@ -143,9 +148,7 @@ const internalCertificate = { .then(utils.omitRow(omissions())); // Add cert data for audit log - savedRow.meta = _.assign({}, savedRow.meta, { - letsencrypt_certificate: certInfo, - }); + savedRow.meta = { ...savedRow.meta, letsencrypt_certificate: certInfo }; await internalCertificate.addCreatedAuditLog(access, certificate.id, savedRow); @@ -162,7 +165,7 @@ const internalCertificate = { throw err; } - data.meta = _.assign({}, data.meta || {}, certificate.meta); + data.meta = { ...data.meta, ...certificate.meta }; // Add to audit log await internalCertificate.addCreatedAuditLog(access, certificate.id, utils.omitRow(omissions())(data)); @@ -301,8 +304,7 @@ const internalCertificate = { } } - const downloadName = `npm-${data.id}-${Date.now()}.zip`; - const opName = `/tmp/${downloadName}`; + const opName = path.join(await mkdtemp(path.join(tmpdir(), "npm-")), `npm-${data.id}-${Date.now()}.zip`); await internalCertificate.zipFiles(certFiles, opName); debug(logger, "zip completed : ", opName); @@ -361,6 +363,19 @@ const internalCertificate = { throw new error.ItemNotFoundError(data.id); } + for (const hostModel of [proxyHostModel, redirectionHostModel, deadHostModel, streamModel]) { + const hosts = await hostModel.query().where("is_deleted", 0).select("id", "certificate_id", "meta"); + if ( + hosts.some( + (host) => + Number(host.certificate_id) === row.id || + Number(host.meta?.npmplus_mtls_certificate_id) === row.id, + ) + ) { + throw new error.ValidationError("Certificate is still in use"); + } + } + await certificateModel.query().where("id", row.id).patch({ is_deleted: 1, }); @@ -535,24 +550,22 @@ const internalCertificate = { } const certs = {}; - _.map(data.files, ([file], name) => { - if ( - (isMtls && name === "certificate") || - (!isMtls && internalCertificate.allowedSslFiles.indexOf(name) !== -1) - ) { + for (const [name, [file]] of Object.entries(data.files)) { + if ((isMtls ? ["certificate"] : internalCertificate.allowedSslFiles).includes(name)) { certs[name] = file.buffer.toString(); } - }); + } const certificate = await internalCertificate.update(access, { id: data.id, expires_on: dayjs.unix(validations.certificate.dates.to).format("YYYY-MM-DD HH:mm:ss"), domain_names: validations.certificate.cn, - meta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later + meta: { ...row.meta }, // Prevent the update method from changing this value that we'll use later }); - certificate.meta = _.assign({}, row.meta, certs); + certificate.meta = { ...row.meta, ...certs }; await internalCertificate.writeCustomCert(certificate); + await internalNginx.reload(); return _.omit(certificate.meta, internalCertificate.allowedSslFiles); }, diff --git a/backend/internal/dead-host.js b/backend/internal/dead-host.js index 7e259a7106..11a0b7365c 100644 --- a/backend/internal/dead-host.js +++ b/backend/internal/dead-host.js @@ -133,7 +133,7 @@ const internalDeadHost = { if (createCertificate) { const cert = await internalCertificate.createQuickCertificate(access, { domain_names: data.domain_names || row.domain_names, - meta: _.assign({}, row.meta, data.meta), + meta: { ...row.meta, ...data.meta }, }); // update host with cert id @@ -141,13 +141,7 @@ const internalDeadHost = { } // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. - let thisData = _.assign( - {}, - { - domain_names: row.domain_names, - }, - data, - ); + let thisData = { domain_names: row.domain_names, ...data }; thisData = internalHost.cleanSslHstsData(createCertificate, thisData, row); diff --git a/backend/internal/host.js b/backend/internal/host.js index 9cbc461eb1..bfd94fedb8 100644 --- a/backend/internal/host.js +++ b/backend/internal/host.js @@ -1,4 +1,3 @@ -import _ from "lodash"; import { castJsonIfNeed } from "../lib/helpers.js"; import deadHostModel from "../models/dead_host.js"; import proxyHostModel from "../models/proxy_host.js"; @@ -15,7 +14,7 @@ const internalHost = { * @returns {object} */ cleanSslHstsData: (newCert, data, existingData) => { - const combinedData = _.assign({}, existingData || {}, data); + const combinedData = { ...existingData, ...data }; if (!combinedData.certificate_id && !newCert) { combinedData.hsts_subdomains = false; diff --git a/backend/internal/ip_ranges.js b/backend/internal/ip_ranges.js index b7ad7c1997..fad7aa0d4f 100644 --- a/backend/internal/ip_ranges.js +++ b/backend/internal/ip_ranges.js @@ -3,8 +3,8 @@ import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; import utils from "../lib/utils.js"; import { ipRanges as logger } from "../logger.js"; -import internalNginx from "./nginx.js"; import pjson from "../package.json" with { type: "json" }; +import internalNginx from "./nginx.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index 2e999cfd3e..72c77b5027 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -1,7 +1,6 @@ import { readFile, rename, rm, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; import { domainToASCII, fileURLToPath } from "node:url"; -import _ from "lodash"; import errs from "../lib/error.js"; import utils from "../lib/utils.js"; import { debug, nginx as logger } from "../logger.js"; @@ -34,10 +33,7 @@ const internalNginx = { try { await internalNginx.test(); - combined_meta = _.assign({}, host.meta, { - nginx_online: true, - nginx_err: null, - }); + combined_meta = { ...host.meta, nginx_online: true, nginx_err: null }; await model.query().where("id", host.id).patch({ meta: combined_meta, @@ -46,10 +42,7 @@ const internalNginx = { logger.error(err.message); // config is bad, update meta and rename config - combined_meta = _.assign({}, host.meta, { - nginx_online: false, - nginx_err: err.message, - }); + combined_meta = { ...host.meta, nginx_online: false, nginx_err: err.message }; await model.query().where("id", host.id).patch({ meta: combined_meta, @@ -293,7 +286,7 @@ const internalNginx = { host.upstreams = await internalNginx.renderUpstreams(host); if (host.locations) { - _.map(host.locations, (location) => { + for (const location of host.locations) { if (location.npmplus_access_list_type === "global") { location.access_list = host.access_list; } else if (location.npmplus_access_list_type === "custom") { @@ -335,7 +328,7 @@ const internalNginx = { ) { host.create_authentik_locations = true; } - }); + } const originalLocations = [...host.locations]; host.locations = await internalNginx.renderLocations(host); diff --git a/backend/internal/proxy-host-access-list.js b/backend/internal/proxy-host-access-list.js index 67f01f4adc..609a91cae9 100644 --- a/backend/internal/proxy-host-access-list.js +++ b/backend/internal/proxy-host-access-list.js @@ -1,6 +1,6 @@ -import { rm, readdir } from "node:fs/promises"; -import { access as logger } from "../logger.js"; +import { readdir, rm } from "node:fs/promises"; import errs from "../lib/error.js"; +import { access as logger } from "../logger.js"; import accessListModel from "../models/access_list.js"; import internalAccessList from "./access-list.js"; import internalNginx from "./nginx.js"; diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js index b0e3a03ef6..06a936fedc 100644 --- a/backend/internal/proxy-host.js +++ b/backend/internal/proxy-host.js @@ -113,7 +113,7 @@ const internalProxyHost = { }) .then((row) => { // Audit log - thisData.meta = _.assign({}, thisData.meta || {}, row.meta); + thisData.meta = { ...thisData.meta, ...row.meta }; // Add to audit log return internalAuditLog @@ -181,7 +181,7 @@ const internalProxyHost = { return internalCertificate .createQuickCertificate(access, { domain_names: thisData.domain_names || row.domain_names, - meta: _.assign({}, row.meta, thisData.meta), + meta: { ...row.meta, ...thisData.meta }, }) .then((cert) => { // update host with cert id @@ -195,13 +195,7 @@ const internalProxyHost = { }) .then(async (row) => { // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. - thisData = _.assign( - {}, - { - domain_names: row.domain_names, - }, - data, - ); + thisData = { domain_names: row.domain_names, ...data }; thisData = internalHost.cleanSslHstsData(createCertificate, thisData, row); thisData = internalProxyHostAccessList.cleanAccessListTypes(thisData); diff --git a/backend/internal/redirection-host.js b/backend/internal/redirection-host.js index 5fb58a24ed..66c5252fdb 100644 --- a/backend/internal/redirection-host.js +++ b/backend/internal/redirection-host.js @@ -90,7 +90,7 @@ const internalRedirectionHost = { }); }) .then((row) => { - thisData.meta = _.assign({}, thisData.meta || {}, row.meta); + thisData.meta = { ...thisData.meta, ...row.meta }; // Add to audit log return internalAuditLog @@ -159,7 +159,7 @@ const internalRedirectionHost = { return internalCertificate .createQuickCertificate(access, { domain_names: thisData.domain_names || row.domain_names, - meta: _.assign({}, row.meta, thisData.meta), + meta: { ...row.meta, ...thisData.meta }, }) .then((cert) => { // update host with cert id @@ -173,13 +173,7 @@ const internalRedirectionHost = { }) .then((row) => { // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. - thisData = _.assign( - {}, - { - domain_names: row.domain_names, - }, - thisData, - ); + thisData = { domain_names: row.domain_names, ...thisData }; thisData = internalHost.cleanSslHstsData(createCertificate, thisData, row); diff --git a/backend/internal/setting.js b/backend/internal/setting.js index e600296564..e66baa2d92 100644 --- a/backend/internal/setting.js +++ b/backend/internal/setting.js @@ -1,6 +1,7 @@ import { writeFile } from "node:fs/promises"; import errs from "../lib/error.js"; import settingModel from "../models/setting.js"; +import internalAuditLog from "./audit-log.js"; import internalNginx from "./nginx.js"; const internalSetting = { @@ -32,6 +33,14 @@ const internalSetting = { }); }) .then(async (row) => { + await internalAuditLog.add(access, { + action: "updated", + object_type: "setting", + meta: { + id: row.id, + value: row.value, + }, + }); if (row.id === "default-site") { // write the html if we need to if (row.value === "html") { diff --git a/backend/internal/stream.js b/backend/internal/stream.js index 6a3f8943dd..12fcc72a3f 100644 --- a/backend/internal/stream.js +++ b/backend/internal/stream.js @@ -121,7 +121,7 @@ const internalStream = { return internalCertificate .createQuickCertificate(access, { domain_names: thisData.domain_names || row.domain_names, - meta: _.assign({}, row.meta, thisData.meta), + meta: { ...row.meta, ...thisData.meta }, }) .then((cert) => { // update host with cert id @@ -135,13 +135,7 @@ const internalStream = { }) .then((row) => { // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. - thisData = _.assign( - {}, - { - domain_names: row.domain_names, - }, - thisData, - ); + thisData = { domain_names: row.domain_names, ...thisData }; return streamModel .query() diff --git a/backend/internal/token.js b/backend/internal/token.js index 5e902e09d9..ffb477a89b 100644 --- a/backend/internal/token.js +++ b/backend/internal/token.js @@ -1,4 +1,3 @@ -import _ from "lodash"; import errs from "../lib/error.js"; import { parseDatePeriod } from "../lib/helpers.js"; import authModel from "../models/auth.js"; @@ -16,17 +15,11 @@ export default { * @param {Object} data * @param {String} data.identity * @param {String} data.secret - * @param {String} [data.scope] - * @param {String} [data.expiry] - * @param {String} [issuer] * @returns {Promise} */ - getTokenFromEmail: async (data, issuer) => { + getTokenFromEmail: async (data) => { const Token = TokenModel(); - data.scope = data.scope || "user"; - data.expiry = data.expiry || "1d"; - const user = await userModel .query() .where("email", data.identity.toLowerCase().trim()) @@ -49,49 +42,45 @@ export default { throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH, ERROR_MESSAGE_INVALID_AUTH_I18N); } - if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) { - // The scope requested doesn't exist as a role against the user, - // you shall not pass. - throw new errs.AuthError(`Invalid scope: ${data.scope}`); - } - // Check if 2FA is enabled const has2FA = await twoFactor.isEnabled(user.id); if (has2FA) { - // Return challenge token instead of full token - const challengeToken = await Token.create({ - iss: issuer || "api", - attrs: { - id: user.id, - }, - scope: ["2fa-challenge"], - expiresIn: "5m", - }); - - return { - requires2fa: true, - challenge_token: challengeToken.token, - }; - } - - // Create a dayjs of the expiry expression - const expiry = parseDatePeriod(data.expiry); - if (expiry === null) { - throw new errs.AuthError(`Invalid expiry time: ${data.expiry}`); + if (data.code) { + const validCode = await twoFactor.verifyForLogin(user.id, data.code); + if (!validCode) { + throw new errs.AuthError(ERROR_MESSAGE_INVALID_2FA, ERROR_MESSAGE_INVALID_2FA_I18N); + } + } else { + // Return challenge token instead of full token + const challengeToken = await Token.create({ + iss: "api", + attrs: { + id: user.id, + }, + scope: ["2fa-challenge"], + expiresIn: "3m", + }); + + return { + requires2fa: true, + token: challengeToken.token, + expires: parseDatePeriod("3m").toISOString(), + }; + } } const signed = await Token.create({ - iss: issuer || "api", + iss: "api", attrs: { id: user.id, }, - scope: [data.scope], - expiresIn: data.expiry, + scope: ["user"], + expiresIn: "1h", }); return { token: signed.token, - expires: expiry.toISOString(), + expires: parseDatePeriod("1h").toISOString(), }; }, @@ -103,9 +92,6 @@ export default { getTokenFromOAuthClaim: async (data) => { const Token = TokenModel(); - data.scope = "user"; - data.expiry = "1d"; - const user = await userModel .query() .where("email", data.identity.toLowerCase().trim()) @@ -117,10 +103,24 @@ export default { throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH); } - // Create a dayjs of the expiry expression - const expiry = parseDatePeriod(data.expiry); - if (expiry === null) { - throw new errs.AuthError(`Invalid expiry time: ${data.expiry}`); + // Check if 2FA is enabled + const has2FA = await twoFactor.isEnabled(user.id); + if (has2FA && process.env.OIDC_SKIP_MFA === "false") { + // Return challenge token instead of full token + const challengeToken = await Token.create({ + iss: "api", + attrs: { + id: user.id, + }, + scope: ["2fa-challenge"], + expiresIn: "3m", + }); + + return { + requires2fa: true, + token: challengeToken.token, + expires: parseDatePeriod("3m").toISOString(), + }; } const signed = await Token.create({ @@ -128,60 +128,36 @@ export default { attrs: { id: user.id, }, - scope: [data.scope], - expiresIn: data.expiry, + scope: ["user"], + expiresIn: "1h", }); return { token: signed.token, - expires: expiry.toISOString(), + expires: parseDatePeriod("1h").toISOString(), }; }, /** * @param {Access} access - * @param {Object} [data] - * @param {String} [data.expiry] - * @param {String} [data.scope] Only considered if existing token scope is admin * @returns {Promise} */ - getFreshToken: async (access, data) => { + getFreshToken: async (access) => { const Token = TokenModel(); - const thisData = data || {}; - - thisData.expiry = thisData.expiry || "1d"; - - if (access?.token.getUserId(0)) { - // Create a dayjs of the expiry expression - const expiry = parseDatePeriod(thisData.expiry); - if (expiry === null) { - throw new errs.AuthError(`Invalid expiry time: ${thisData.expiry}`); - } - - const token_attrs = { - id: access.token.getUserId(0), - }; - - // Only admins can request otherwise scoped tokens - let scope = access.token.get("scope"); - if (thisData.scope && access.token.hasScope("admin")) { - scope = [thisData.scope]; - - if (thisData.scope === "job-board" || thisData.scope === "worker") { - token_attrs.id = 0; - } - } + if (access?.token.getUserId(0) && access.token.hasScope("user")) { const signed = await Token.create({ iss: "api", - scope: scope, - attrs: token_attrs, - expiresIn: thisData.expiry, + scope: ["user"], + attrs: { + id: access.token.getUserId(0), + }, + expiresIn: "1h", }); return { token: signed.token, - expires: expiry.toISOString(), + expires: parseDatePeriod("1h").toISOString(), }; } throw new errs.AssertionFailedError("Existing token contained invalid user data"); @@ -191,12 +167,10 @@ export default { * Verify 2FA code and return full token * @param {string} challengeToken * @param {string} code - * @param {string} [expiry] * @returns {Promise} */ - verify2FA: async (challengeToken, code, expiry) => { + verify2FA: async (challengeToken, code) => { const Token = TokenModel(); - const tokenExpiry = expiry || "1d"; // Verify challenge token let tokenData; @@ -222,49 +196,18 @@ export default { throw new errs.AuthError(ERROR_MESSAGE_INVALID_2FA, ERROR_MESSAGE_INVALID_2FA_I18N); } - // Create full token - const expiryDate = parseDatePeriod(tokenExpiry); - if (expiryDate === null) { - throw new errs.AuthError(`Invalid expiry time: ${tokenExpiry}`); - } - const signed = await Token.create({ iss: "api", attrs: { id: userId, }, scope: ["user"], - expiresIn: tokenExpiry, - }); - - return { - token: signed.token, - expires: expiryDate.toISOString(), - }; - }, - - /** - * @param {Object} user - * @returns {Promise} - */ - getTokenFromUser: async (user) => { - const expire = "1d"; - const Token = TokenModel(); - const expiry = parseDatePeriod(expire); - - const signed = await Token.create({ - iss: "api", - attrs: { - id: user.id, - }, - scope: ["user"], - expiresIn: expire, + expiresIn: "1h", }); return { token: signed.token, - expires: expiry.toISOString(), - user: user, + expires: parseDatePeriod("1h").toISOString(), }; }, }; diff --git a/backend/internal/user.js b/backend/internal/user.js index 3d66a67b32..7c7ce8d049 100644 --- a/backend/internal/user.js +++ b/backend/internal/user.js @@ -1,20 +1,32 @@ -import _ from "lodash"; import crypto from "node:crypto"; -import { writeFile } from "node:fs/promises"; +import { rm, writeFile } from "node:fs/promises"; +import _ from "lodash"; import errs from "../lib/error.js"; import utils from "../lib/utils.js"; import { gravatar as logger } from "../logger.js"; import authModel from "../models/auth.js"; import userModel from "../models/user.js"; import userPermissionModel from "../models/user_permission.js"; +import pjson from "../package.json" with { type: "json" }; import internalAuditLog from "./audit-log.js"; import internalToken from "./token.js"; -import pjson from "../package.json" with { type: "json" }; const omissions = () => { return ["is_deleted", "permissions.id", "permissions.user_id", "permissions.created_on", "permissions.modified_on"]; }; +const avatarExts = ["png", "jpg", "gif", "webp"]; + +const avatarExt = (b) => { + if (!b || b.length < 12) return null; + if (b.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex"))) return "png"; + if (b.subarray(0, 3).equals(Buffer.from("ffd8ff", "hex"))) return "jpg"; + if (b.subarray(0, 4).toString("latin1") === "GIF8") return "gif"; + if (b.subarray(0, 4).toString("latin1") === "RIFF" && b.subarray(8, 12).toString("latin1") === "WEBP") + return "webp"; + return null; +}; + const internalUser = { /** * Create a user can happen unauthenticated only once and only when no active users exist. @@ -32,11 +44,9 @@ const internalUser = { data.roles = data.roles || []; data.email = data.email.toLowerCase().trim(); - internalUser.isEmailAvailable(data.email).then((available) => { - if (!available) { - throw new errs.ValidationError(`Email address already in use - ${data.email}`); - } - }); + if (!(await internalUser.isEmailAvailable(data.email))) { + throw new errs.ValidationError(`Email address already in use - ${data.email}`); + } if (typeof data.is_disabled !== "undefined") { data.is_disabled = data.is_disabled ? 1 : 0; @@ -92,7 +102,7 @@ const internalUser = { let user = await userModel.query().insertAndFetch(data).then(utils.omitRow(omissions())); if (auth) { - user = await authModel.query().insert({ + await authModel.query().insert({ user_id: user.id, type: auth.type, secret: auth.secret, @@ -126,6 +136,25 @@ const internalUser = { return user; }, + setAvatar: async (access, id, file) => { + await access.can("users:update", id); + const ext = avatarExt(file?.buffer); + if (!ext) throw new errs.ValidationError("Invalid avatar file type"); + const user = await internalUser.get(access, { id }); + await Promise.all(avatarExts.map((e) => rm(`/data/npmplus/avatar/${user.id}.${e}`, { force: true }))); + await writeFile(`/data/npmplus/avatar/${user.id}.${ext}`, file.buffer); + await userModel.query().patchAndFetchById(user.id, { avatar: `/images/avatar/${user.id}.${ext}` }); + return internalUser.update(access, { id: user.id }); + }, + + deleteAvatar: async (access, id) => { + await access.can("users:update", id); + const user = await internalUser.get(access, { id }); + await Promise.all(avatarExts.map((e) => rm(`/data/npmplus/avatar/${user.id}.${e}`, { force: true }))); + await userModel.query().patchAndFetchById(user.id, { avatar: "" }); + return internalUser.update(access, { id: user.id }); + }, + /** * @param {Access} access * @param {Object} data @@ -177,7 +206,9 @@ const internalUser = { ); } - if (process.env.DISABLE_GRAVATAR === "true") { + if (user.avatar?.startsWith("/images/avatar/")) { + data.avatar = user.avatar; + } else if (process.env.DISABLE_GRAVATAR === "true") { data.avatar = "/images/default-avatar.jpg"; } else { try { @@ -550,10 +581,10 @@ const internalUser = { return userPermissionModel .query() .where("user_id", user.id) - .patchAndFetchById(existing_auth.id, _.assign({ user_id: user.id }, data)); + .patchAndFetchById(existing_auth.id, { user_id: user.id, ...data }); } // insert - return userPermissionModel.query().insertAndFetch(_.assign({ user_id: user.id }, data)); + return userPermissionModel.query().insertAndFetch({ user_id: user.id, ...data }); }) .then((permissions) => { // Add to Audit Log @@ -573,20 +604,13 @@ const internalUser = { }); }, - /** - * @param {Access} access - * @param {Object} data - * @param {Integer} data.id - */ - loginAs: (access, data) => { - return access - .can("users:loginas", data.id) - .then(() => { - return internalUser.get(access, data); - }) - .then((user) => { - return internalToken.getTokenFromUser(user); - }); + revokeSessions: async (access, userId) => { + await access.can("users:revoke", userId); + await userModel + .query() + .where("id", userId) + .patch({ npmplus_token_valid_after: Math.floor(Date.now() / 1000) }); + return true; }, }; diff --git a/backend/lib/access.js b/backend/lib/access.js index bebdd68c8e..0b675ed0cf 100644 --- a/backend/lib/access.js +++ b/backend/lib/access.js @@ -10,7 +10,6 @@ import { readFile } from "node:fs/promises"; import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; import Ajv from "ajv/dist/2020.js"; -import _ from "lodash"; import { access as logger } from "../logger.js"; import proxyHostModel from "../models/proxy_host.js"; import TokenModel from "../models/token.js"; @@ -51,10 +50,7 @@ export default function (tokenString) { // - exist (and not soft deleted) // - still have the appropriate scopes for this token // This is only required when the User ID is supplied or if the token scope has `user` - if ( - tokenData.attrs.id || - (typeof tokenData.scope !== "undefined" && _.indexOf(tokenData.scope, "user") !== -1) - ) { + if (tokenData.attrs.id || tokenData.scope?.includes("user")) { // Has token user id or token user scope const user = await userModel .query() @@ -66,18 +62,15 @@ export default function (tokenString) { .first(); if (user) { + if (tokenData.iat <= user.npmplus_token_valid_after) { + throw new errs.AuthError("Token has been revoked"); + } + // make sure user has all scopes of the token // The `user` role is not added against the user row, so we have to just add it here to get past this check. user.roles.push("user"); - let ok = true; - _.forEach(tokenData.scope, (scope_item) => { - if (_.indexOf(user.roles, scope_item) === -1) { - ok = false; - } - }); - - if (!ok) { + if (!(tokenData.scope ?? []).every((scopeItem) => user.roles.includes(scopeItem))) { throw new errs.AuthError("Invalid token scope for User"); } initialised = true; @@ -126,10 +119,7 @@ export default function (tokenString) { } const rows = await query; - objects = []; - _.forEach(rows, (ruleRow) => { - objects.push(ruleRow.id); - }); + objects = rows.map((ruleRow) => ruleRow.id); // enum should not have less than 1 item if (!objects.length) { @@ -203,7 +193,7 @@ export default function (tokenString) { */ load: async (allowInternal) => { if (tokenString) { - return await Token.load(tokenString); + return await this.init(); } allowInternalAccess = allowInternal; return allowInternal || null; diff --git a/backend/lib/access/users-loginas.json b/backend/lib/access/users-2fadisable.json similarity index 100% rename from backend/lib/access/users-loginas.json rename to backend/lib/access/users-2fadisable.json diff --git a/backend/lib/access/users-revoke.json b/backend/lib/access/users-revoke.json new file mode 100644 index 0000000000..05e501f940 --- /dev/null +++ b/backend/lib/access/users-revoke.json @@ -0,0 +1,26 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": [ + "data", + "scope" + ], + "properties": { + "data": { + "$ref": "objects#/properties/users" + }, + "scope": { + "type": "array", + "contains": { + "type": "string", + "pattern": "^user$" + } + } + } + } + ] +} diff --git a/backend/lib/certbot.js b/backend/lib/certbot.js index 3eaebf6e41..5f8de84c05 100644 --- a/backend/lib/certbot.js +++ b/backend/lib/certbot.js @@ -55,4 +55,4 @@ const installPlugins = async (pluginKeys) => { } }; -export { installPlugins, installPlugin }; +export { installPlugin, installPlugins }; diff --git a/backend/lib/config.js b/backend/lib/config.js index 9d312bc376..ebb1fe194e 100644 --- a/backend/lib/config.js +++ b/backend/lib/config.js @@ -1,8 +1,8 @@ -import { existsSync, readFileSync, writeFileSync } from "node:fs"; import crypto from "node:crypto"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { global as logger } from "../logger.js"; -const keysFile = "/data/npmplus/keys.json"; +const keysFile = "/data/npmplus/keys-ec.json"; const sqliteEngine = "better-sqlite3"; const mysqlEngine = "mysql2"; const postgresEngine = "pg"; @@ -104,8 +104,8 @@ const generateKeys = () => { logger.info("Creating a new JWT key pair..."); // Now create the keys and save them in the config. - const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", { - modulusLength: 2048, + const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", { + namedCurve: "P-521", publicKeyEncoding: { type: "spki", format: "pem", @@ -211,4 +211,4 @@ const getPrivateKey = () => { return instance.keys.key; }; -export { configHas, configGet, isSqlite, isMysql, isPostgres, getPrivateKey, getPublicKey }; +export { configGet, configHas, getPrivateKey, getPublicKey, isMysql, isPostgres, isSqlite }; diff --git a/backend/lib/error.js b/backend/lib/error.js index d7dbf0c965..85a64d26f6 100644 --- a/backend/lib/error.js +++ b/backend/lib/error.js @@ -1,5 +1,3 @@ -import _ from "lodash"; - const errs = { PermissionError: function (_, previous) { Error.captureStackTrace(this, this.constructor); @@ -96,8 +94,8 @@ const errs = { }, }; -_.forEach(errs, (err) => { +for (const err of Object.values(errs)) { err.prototype = Object.create(Error.prototype); -}); +} export default errs; diff --git a/backend/lib/express/pagination.js b/backend/lib/express/pagination.js index 457c954d2d..41752cd1ee 100644 --- a/backend/lib/express/pagination.js +++ b/backend/lib/express/pagination.js @@ -1,5 +1,3 @@ -import _ from "lodash"; - export default (default_sort, default_offset, default_limit, max_limit) => { /** * This will setup the req query params with filtered data and defaults @@ -26,7 +24,7 @@ export default (default_sort, default_offset, default_limit, max_limit) => { const sort_array = []; sort = sort.split(","); - _.map(sort, (val) => { + for (const val of sort) { const matches = myRegexp.exec(val); if (matches !== null) { @@ -41,7 +39,7 @@ export default (default_sort, default_offset, default_limit, max_limit) => { dir: "asc", }); } - }); + } // Sort will now be in this format: // [ diff --git a/backend/lib/express/require-login.js b/backend/lib/express/require-login.js new file mode 100644 index 0000000000..918855f17a --- /dev/null +++ b/backend/lib/express/require-login.js @@ -0,0 +1,10 @@ +import errs from "../error.js"; +import jwtdecode from "./jwt-decode.js"; + +export default () => [ + jwtdecode(), + (_, res, next) => { + if (!res.locals.access?.token.getUserId(0)) return next(new errs.PermissionError()); + next(); + }, +]; diff --git a/backend/lib/helpers.js b/backend/lib/helpers.js index 1ff3b50610..fab36584f6 100644 --- a/backend/lib/helpers.js +++ b/backend/lib/helpers.js @@ -55,4 +55,4 @@ const convertBoolFieldsToInt = (obj, fields) => { */ const castJsonIfNeed = (colName) => (isPostgres() ? ref(colName).castText() : colName); -export { parseDatePeriod, convertIntFieldsToBool, convertBoolFieldsToInt, castJsonIfNeed }; +export { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool, parseDatePeriod }; diff --git a/backend/lib/migrate_template.js b/backend/lib/migrate_template.js index 0b8e284023..aeeac1a991 100644 --- a/backend/lib/migrate_template.js +++ b/backend/lib/migrate_template.js @@ -56,4 +56,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/lib/utils.js b/backend/lib/utils.js index d6b5562a7d..29e7035d89 100644 --- a/backend/lib/utils.js +++ b/backend/lib/utils.js @@ -1,10 +1,10 @@ import { execFile as nodeExecFile } from "node:child_process"; -import { promisify } from "node:util"; +import crypto from "node:crypto"; +import { readdir, readFile, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; import { Liquid } from "liquidjs"; -import crypto from "node:crypto"; -import { readdir, readFile, writeFile } from "node:fs/promises"; import _ from "lodash"; import { debug, global as logger } from "../logger.js"; import errs from "./error.js"; diff --git a/backend/lib/validator/index.js b/backend/lib/validator/index.js index 5d9f8f38a8..a7814cfa66 100644 --- a/backend/lib/validator/index.js +++ b/backend/lib/validator/index.js @@ -1,5 +1,4 @@ import Ajv from "ajv/dist/2020.js"; -import _ from "lodash"; import commonDefinitions from "../../schema/common.json" with { type: "json" }; import errs from "../error.js"; @@ -30,7 +29,7 @@ const validator = (schema, payload) => { const valid = validate(payload); if (valid && !validate.errors) { - resolve(_.cloneDeep(payload)); + resolve(structuredClone(payload)); } else { const message = ajv.errorsText(validate.errors); reject(new errs.InternalValidationError(message)); diff --git a/backend/logger.js b/backend/logger.js index d59a32f4ae..751069afd4 100644 --- a/backend/logger.js +++ b/backend/logger.js @@ -25,18 +25,18 @@ const debug = (logger, ...args) => { }; export { - debug, - global, - migrate, - express, access, - nginx, - ssl, certbot, + debug, + express, + global, + gravatar, importer, - setup, ipRanges, - remoteVersion, - gravatar, + migrate, + nginx, oidc, + remoteVersion, + setup, + ssl, }; diff --git a/backend/migrations/14022026145603_http3_support.js b/backend/migrations/14022026145603_http3_support.js index 85574dedea..9ebd82d7a2 100644 --- a/backend/migrations/14022026145603_http3_support.js +++ b/backend/migrations/14022026145603_http3_support.js @@ -20,4 +20,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20180618015850_initial.js b/backend/migrations/20180618015850_initial.js index 1e48f8f582..648f999b54 100644 --- a/backend/migrations/20180618015850_initial.js +++ b/backend/migrations/20180618015850_initial.js @@ -203,4 +203,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20180929054513_websockets.js b/backend/migrations/20180929054513_websockets.js index 19f515df41..22fffa9d51 100644 --- a/backend/migrations/20180929054513_websockets.js +++ b/backend/migrations/20180929054513_websockets.js @@ -33,4 +33,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20181019052346_forward_host.js b/backend/migrations/20181019052346_forward_host.js index 39a6ea4598..29a9eba7d8 100644 --- a/backend/migrations/20181019052346_forward_host.js +++ b/backend/migrations/20181019052346_forward_host.js @@ -33,4 +33,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20181113041458_http2_support.js b/backend/migrations/20181113041458_http2_support.js index 29aee7b5a5..df66eac3e0 100644 --- a/backend/migrations/20181113041458_http2_support.js +++ b/backend/migrations/20181113041458_http2_support.js @@ -47,4 +47,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20181213013211_forward_scheme.js b/backend/migrations/20181213013211_forward_scheme.js index 0b8ce1de62..f3e8766cac 100644 --- a/backend/migrations/20181213013211_forward_scheme.js +++ b/backend/migrations/20181213013211_forward_scheme.js @@ -33,4 +33,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20190104035154_disabled.js b/backend/migrations/20190104035154_disabled.js index d734a219e4..13e77eb5dd 100644 --- a/backend/migrations/20190104035154_disabled.js +++ b/backend/migrations/20190104035154_disabled.js @@ -54,4 +54,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20190215115310_customlocations.js b/backend/migrations/20190215115310_customlocations.js index 7d6ded87fa..24500b9823 100644 --- a/backend/migrations/20190215115310_customlocations.js +++ b/backend/migrations/20190215115310_customlocations.js @@ -34,4 +34,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20190218060101_hsts.js b/backend/migrations/20190218060101_hsts.js index 753a1a52c8..2c0939ddf7 100644 --- a/backend/migrations/20190218060101_hsts.js +++ b/backend/migrations/20190218060101_hsts.js @@ -50,4 +50,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20190227065017_settings.js b/backend/migrations/20190227065017_settings.js index 24407d0f6a..e1ab369566 100644 --- a/backend/migrations/20190227065017_settings.js +++ b/backend/migrations/20190227065017_settings.js @@ -37,4 +37,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20200410143839_access_list_client.js b/backend/migrations/20200410143839_access_list_client.js index d98e8c503f..43fdd504e1 100644 --- a/backend/migrations/20200410143839_access_list_client.js +++ b/backend/migrations/20200410143839_access_list_client.js @@ -49,4 +49,4 @@ const down = (knex) => { }); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20200410143840_access_list_client_fix.js b/backend/migrations/20200410143840_access_list_client_fix.js index 2176dd6c29..a82ff999b7 100644 --- a/backend/migrations/20200410143840_access_list_client_fix.js +++ b/backend/migrations/20200410143840_access_list_client_fix.js @@ -33,4 +33,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20201014143841_pass_auth.js b/backend/migrations/20201014143841_pass_auth.js index b241670b08..3d28d66312 100644 --- a/backend/migrations/20201014143841_pass_auth.js +++ b/backend/migrations/20201014143841_pass_auth.js @@ -40,4 +40,4 @@ const down = (knex) => { }); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20210210154702_redirection_scheme.js b/backend/migrations/20210210154702_redirection_scheme.js index 2b63daa521..d7ceed322e 100644 --- a/backend/migrations/20210210154702_redirection_scheme.js +++ b/backend/migrations/20210210154702_redirection_scheme.js @@ -40,4 +40,4 @@ const down = (knex) => { }); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20210210154703_redirection_status_code.js b/backend/migrations/20210210154703_redirection_status_code.js index 5b3850542c..9e80e3cd12 100644 --- a/backend/migrations/20210210154703_redirection_status_code.js +++ b/backend/migrations/20210210154703_redirection_status_code.js @@ -40,4 +40,4 @@ const down = (knex) => { }); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20210423103500_stream_domain.js b/backend/migrations/20210423103500_stream_domain.js index 53b471ea89..c8ea88d714 100644 --- a/backend/migrations/20210423103500_stream_domain.js +++ b/backend/migrations/20210423103500_stream_domain.js @@ -40,4 +40,4 @@ const down = (knex) => { }); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20211108145214_regenerate_default_host.js b/backend/migrations/20211108145214_regenerate_default_host.js index 4532bc2e39..7ba424e1c0 100644 --- a/backend/migrations/20211108145214_regenerate_default_host.js +++ b/backend/migrations/20211108145214_regenerate_default_host.js @@ -49,4 +49,4 @@ const down = (knex) => { return regenerateDefaultHost(knex); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20240427161436_stream_ssl.js b/backend/migrations/20240427161436_stream_ssl.js index 6fa6118d0f..a2824fd47a 100644 --- a/backend/migrations/20240427161436_stream_ssl.js +++ b/backend/migrations/20240427161436_stream_ssl.js @@ -40,4 +40,4 @@ const down = (knex) => { }); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20240711144745_change_incoming_port_to_string.js b/backend/migrations/20240711144745_change_incoming_port_to_string.js index 912021356f..88908bbaa5 100644 --- a/backend/migrations/20240711144745_change_incoming_port_to_string.js +++ b/backend/migrations/20240711144745_change_incoming_port_to_string.js @@ -40,4 +40,4 @@ const down = (knex) => { }); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20240921100301_regenerate_default_host.js b/backend/migrations/20240921100301_regenerate_default_host.js index 9060120387..d34e29fcb1 100644 --- a/backend/migrations/20240921100301_regenerate_default_host.js +++ b/backend/migrations/20240921100301_regenerate_default_host.js @@ -49,4 +49,4 @@ const down = (knex) => { return regenerateDefaultHost(knex); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20241230192345_change_forwarding_port_to_string.js b/backend/migrations/20241230192345_change_forwarding_port_to_string.js index 104e95e697..79a7f743c8 100644 --- a/backend/migrations/20241230192345_change_forwarding_port_to_string.js +++ b/backend/migrations/20241230192345_change_forwarding_port_to_string.js @@ -40,4 +40,4 @@ const down = (knex) => { }); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20250123132545_allow_empty_forwarding_port.js b/backend/migrations/20250123132545_allow_empty_forwarding_port.js index b023d3bb32..9fd39fea62 100644 --- a/backend/migrations/20250123132545_allow_empty_forwarding_port.js +++ b/backend/migrations/20250123132545_allow_empty_forwarding_port.js @@ -40,4 +40,4 @@ const down = (knex) => { }); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20250518142020_allow_empty_stream_forwarding_port.js b/backend/migrations/20250518142020_allow_empty_stream_forwarding_port.js index a4260df2a7..2fe6a9c06e 100644 --- a/backend/migrations/20250518142020_allow_empty_stream_forwarding_port.js +++ b/backend/migrations/20250518142020_allow_empty_stream_forwarding_port.js @@ -40,4 +40,4 @@ const down = (knex) => { }); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20250627140440_stream_proxy_protocol_forwarding.js b/backend/migrations/20250627140440_stream_proxy_protocol_forwarding.js index 58e3d8d6c0..a8af381b0a 100644 --- a/backend/migrations/20250627140440_stream_proxy_protocol_forwarding.js +++ b/backend/migrations/20250627140440_stream_proxy_protocol_forwarding.js @@ -33,4 +33,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20251111090000_redirect_auto_scheme.js b/backend/migrations/20251111090000_redirect_auto_scheme.js index 4ba990016e..45fcf020eb 100644 --- a/backend/migrations/20251111090000_redirect_auto_scheme.js +++ b/backend/migrations/20251111090000_redirect_auto_scheme.js @@ -43,4 +43,4 @@ const down = (knex) => { }); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260119174530_stream_proxy_ssl.js b/backend/migrations/20260119174530_stream_proxy_ssl.js index 7c09226893..409da48a1a 100644 --- a/backend/migrations/20260119174530_stream_proxy_ssl.js +++ b/backend/migrations/20260119174530_stream_proxy_ssl.js @@ -33,4 +33,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260127122645_stream_rename_pp_and_tls.js b/backend/migrations/20260127122645_stream_rename_pp_and_tls.js index 6a5271ec4b..6956be6719 100644 --- a/backend/migrations/20260127122645_stream_rename_pp_and_tls.js +++ b/backend/migrations/20260127122645_stream_rename_pp_and_tls.js @@ -34,4 +34,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260131163528_trust_forwarded_proto.js b/backend/migrations/20260131163528_trust_forwarded_proto.js index ad99dc0a5f..b3618c09f0 100644 --- a/backend/migrations/20260131163528_trust_forwarded_proto.js +++ b/backend/migrations/20260131163528_trust_forwarded_proto.js @@ -40,4 +40,4 @@ const down = (knex) => { }); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260161994605_reset_button_values.js b/backend/migrations/20260161994605_reset_button_values.js index a0ecceaf21..204b7602c7 100644 --- a/backend/migrations/20260161994605_reset_button_values.js +++ b/backend/migrations/20260161994605_reset_button_values.js @@ -33,4 +33,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/202602081406011_new_proxy_buttons.js b/backend/migrations/202602081406011_new_proxy_buttons.js index 85574dedea..9ebd82d7a2 100644 --- a/backend/migrations/202602081406011_new_proxy_buttons.js +++ b/backend/migrations/202602081406011_new_proxy_buttons.js @@ -20,4 +20,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260208140601_new_proxy_buttons.js b/backend/migrations/20260208140601_new_proxy_buttons.js index 23fb15b774..93476cc8a9 100644 --- a/backend/migrations/20260208140601_new_proxy_buttons.js +++ b/backend/migrations/20260208140601_new_proxy_buttons.js @@ -54,4 +54,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260208223213_new_proxy_selections.js b/backend/migrations/20260208223213_new_proxy_selections.js index 6e8ffbd07c..95338e5558 100644 --- a/backend/migrations/20260208223213_new_proxy_selections.js +++ b/backend/migrations/20260208223213_new_proxy_selections.js @@ -34,4 +34,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260214145603_http3_support.js b/backend/migrations/20260214145603_http3_support.js index 7ace889865..b8729a0b26 100644 --- a/backend/migrations/20260214145603_http3_support.js +++ b/backend/migrations/20260214145603_http3_support.js @@ -54,4 +54,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/202602142050081_new_and_split_proxy_buttons.js b/backend/migrations/202602142050081_new_and_split_proxy_buttons.js index 85574dedea..9ebd82d7a2 100644 --- a/backend/migrations/202602142050081_new_and_split_proxy_buttons.js +++ b/backend/migrations/202602142050081_new_and_split_proxy_buttons.js @@ -20,4 +20,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260214205008_new_and_split_proxy_buttons.js b/backend/migrations/20260214205008_new_and_split_proxy_buttons.js index 93dd88693e..8fdaea7e8a 100644 --- a/backend/migrations/20260214205008_new_and_split_proxy_buttons.js +++ b/backend/migrations/20260214205008_new_and_split_proxy_buttons.js @@ -54,4 +54,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260218120000_add_stream_description.js b/backend/migrations/20260218120000_add_stream_description.js index 50a2fa7f24..029a5f8d11 100644 --- a/backend/migrations/20260218120000_add_stream_description.js +++ b/backend/migrations/20260218120000_add_stream_description.js @@ -40,4 +40,4 @@ const down = (knex) => { }); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260325222306_proxy_location_config.js b/backend/migrations/20260325222306_proxy_location_config.js index 567259194a..b2bd90bf32 100644 --- a/backend/migrations/20260325222306_proxy_location_config.js +++ b/backend/migrations/20260325222306_proxy_location_config.js @@ -33,4 +33,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260325224622_stream_advanced_config.js b/backend/migrations/20260325224622_stream_advanced_config.js index c98e39717d..60492cbfd0 100644 --- a/backend/migrations/20260325224622_stream_advanced_config.js +++ b/backend/migrations/20260325224622_stream_advanced_config.js @@ -33,4 +33,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260331194500_multiple_access_lists.js b/backend/migrations/20260331194500_multiple_access_lists.js index 031eb2f1db..21fd7551d7 100644 --- a/backend/migrations/20260331194500_multiple_access_lists.js +++ b/backend/migrations/20260331194500_multiple_access_lists.js @@ -97,4 +97,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260404150850_revert_redirect_auto_scheme.js b/backend/migrations/20260404150850_revert_redirect_auto_scheme.js index 36e0c5e6e5..e634f0dc40 100644 --- a/backend/migrations/20260404150850_revert_redirect_auto_scheme.js +++ b/backend/migrations/20260404150850_revert_redirect_auto_scheme.js @@ -43,4 +43,4 @@ const down = (knex) => { }); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260604214922_auth_request_upstream.js b/backend/migrations/20260604214922_auth_request_upstream.js index ab9166b49d..ee236bfff0 100644 --- a/backend/migrations/20260604214922_auth_request_upstream.js +++ b/backend/migrations/20260604214922_auth_request_upstream.js @@ -33,4 +33,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260604223200_disable_uri_sanitisation.js b/backend/migrations/20260604223200_disable_uri_sanitisation.js index b472fa3f97..7018761640 100644 --- a/backend/migrations/20260604223200_disable_uri_sanitisation.js +++ b/backend/migrations/20260604223200_disable_uri_sanitisation.js @@ -33,4 +33,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260604231800_spoof_host_header.js b/backend/migrations/20260604231800_spoof_host_header.js index af79fa5330..bbf2cbc563 100644 --- a/backend/migrations/20260604231800_spoof_host_header.js +++ b/backend/migrations/20260604231800_spoof_host_header.js @@ -33,4 +33,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260704213418_disable_uri_sanitisation_default.js b/backend/migrations/20260704213418_disable_uri_sanitisation_default.js index 667a6c907f..2636a90385 100644 --- a/backend/migrations/20260704213418_disable_uri_sanitisation_default.js +++ b/backend/migrations/20260704213418_disable_uri_sanitisation_default.js @@ -33,4 +33,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260720220233_remove_spoof_host_header.js b/backend/migrations/20260720220233_remove_spoof_host_header.js index 691371ce1c..12dd024f05 100644 --- a/backend/migrations/20260720220233_remove_spoof_host_header.js +++ b/backend/migrations/20260720220233_remove_spoof_host_header.js @@ -33,4 +33,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/migrations/20260721120000_token_valid_after.js b/backend/migrations/20260721120000_token_valid_after.js new file mode 100644 index 0000000000..0358e9c16e --- /dev/null +++ b/backend/migrations/20260721120000_token_valid_after.js @@ -0,0 +1,36 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "token_valid_after"; + +/** + * Migrate + * + * @see https://knexjs.org/guide/migrations.html#migration-api + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .table("user", (user) => { + user.bigInteger("npmplus_token_valid_after").notNull().unsigned().defaultTo(0); + }) + .then(() => { + logger.info(`[${migrateName}] user Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} _knex + * @returns {Promise} + */ +const down = (_knex) => { + logger.warn(`[${migrateName}] You can't migrate down this one.`); + return Promise.resolve(true); +}; + +export { down, up }; diff --git a/backend/migrations/20260724211423_drop_access_list_relation_foreign_keys.js b/backend/migrations/20260724211423_drop_access_list_relation_foreign_keys.js new file mode 100644 index 0000000000..e4453e0cf5 --- /dev/null +++ b/backend/migrations/20260724211423_drop_access_list_relation_foreign_keys.js @@ -0,0 +1,35 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "drop_access_list_relation_foreign_keys"; + +/** + * Migrate + * + * @see https://knexjs.org/guide/migrations.html#migration-api + * + * @param {Object} knex + * @returns {Promise} + */ +const up = async (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + await knex.schema.alterTable("npmplus_proxy_host_access_list", (table) => { + table.dropForeign("proxy_host_id"); + table.dropForeign("access_list_id"); + }); + + logger.info(`[${migrateName}] npmplus_proxy_host_access_list Table altered`); +}; + +/** + * Undo Migrate + * + * @param {Object} _knex + * @returns {Promise} + */ +const down = (_knex) => { + logger.warn(`[${migrateName}] You can't migrate down this one.`); + return Promise.resolve(true); +}; + +export { down, up }; diff --git a/backend/migrations/20260724213114_restore_proxy_host_access_list.js b/backend/migrations/20260724213114_restore_proxy_host_access_list.js new file mode 100644 index 0000000000..f94be14839 --- /dev/null +++ b/backend/migrations/20260724213114_restore_proxy_host_access_list.js @@ -0,0 +1,42 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "restore_proxy_host_access_list"; + +/** + * Migrate + * + * @see https://knexjs.org/guide/migrations.html#migration-api + * + * @param {Object} knex + * @returns {Promise} + */ +const up = async (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + const validIds = new Set(await knex("access_list").where("is_deleted", 0).pluck("id")); + const relations = []; + for (const row of await knex("proxy_host").where("is_deleted", 0)) { + const locations = Array.isArray(row.locations) ? row.locations : JSON.parse(row.locations || "[]"); + for (const { npmplus_access_list_type: type, npmplus_access_list_ids: ids } of [row, ...locations]) { + if (type !== "custom") continue; + for (const id of Array.isArray(ids) ? ids : JSON.parse(ids || "[]")) + if (validIds.has(id)) relations.push({ proxy_host_id: row.id, access_list_id: id }); + } + } + if (relations.length) await knex("npmplus_proxy_host_access_list").insert(relations).onConflict().ignore(); + + logger.info(`[${migrateName}] npmplus_proxy_host_access_list Table restored`); +}; + +/** + * Undo Migrate + * + * @param {Object} _knex + * @returns {Promise} + */ +const down = (_knex) => { + logger.warn(`[${migrateName}] You can't migrate down this one.`); + return Promise.resolve(true); +}; + +export { down, up }; diff --git a/backend/migrations/20260725143120_not_null_optional_columns.js b/backend/migrations/20260725143120_not_null_optional_columns.js new file mode 100644 index 0000000000..ede752ffb3 --- /dev/null +++ b/backend/migrations/20260725143120_not_null_optional_columns.js @@ -0,0 +1,47 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "not_null_optional_columns"; + +/** + * Migrate + * + * @see https://knexjs.org/guide/migrations.html#migration-api + * + * @param {Object} knex + * @returns {Promise} + */ +const up = async (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + await knex("proxy_host").whereNull("locations").update({ locations: "[]" }); + await knex("proxy_host").whereNull("npmplus_access_list_ids").update({ npmplus_access_list_ids: "[]" }); + await knex("stream").whereNull("forwarding_port").update({ forwarding_port: "" }); + await knex("stream").whereNull("npmplus_description").update({ npmplus_description: "" }); + + await knex.schema.alterTable("proxy_host", (table) => { + table.json("locations").notNull().defaultTo("[]").alter(); + table.json("npmplus_access_list_ids").notNull().defaultTo("[]").alter(); + }); + + logger.info(`[${migrateName}] proxy_host Table altered`); + + await knex.schema.alterTable("stream", (table) => { + table.string("forwarding_port", 12).notNull().defaultTo("").alter(); + table.string("npmplus_description", 255).notNull().defaultTo("").alter(); + }); + + logger.info(`[${migrateName}] stream Table altered`); +}; + +/** + * Undo Migrate + * + * @param {Object} _knex + * @returns {Promise} + */ +const down = (_knex) => { + logger.warn(`[${migrateName}] You can't migrate down this one.`); + return Promise.resolve(true); +}; + +export { down, up }; diff --git a/backend/migrations/20261402145603_http3_support.js b/backend/migrations/20261402145603_http3_support.js index 85574dedea..9ebd82d7a2 100644 --- a/backend/migrations/20261402145603_http3_support.js +++ b/backend/migrations/20261402145603_http3_support.js @@ -20,4 +20,4 @@ const down = (_knex) => { return Promise.resolve(true); }; -export { up, down }; +export { down, up }; diff --git a/backend/models/token.js b/backend/models/token.js index 4edab5bbee..843dc860ea 100644 --- a/backend/models/token.js +++ b/backend/models/token.js @@ -5,12 +5,11 @@ import crypto from "node:crypto"; import jwt from "jsonwebtoken"; -import _ from "lodash"; import { getPrivateKey, getPublicKey } from "../lib/config.js"; import errs from "../lib/error.js"; import { global as logger } from "../logger.js"; -const ALGO = "RS256"; +const ALGO = "ES512"; export default () => { let tokenData = {}; @@ -57,7 +56,7 @@ export default () => { } return new Promise((resolve, reject) => { try { - if (!token || token === null || token === "null") { + if (!token || token === "null") { reject(new errs.AuthError("Empty token")); } else { jwt.verify( @@ -76,10 +75,7 @@ export default () => { // Hack: some tokens out in the wild have a scope of 'all' instead of 'user'. // For 30 days at least, we need to replace 'all' with user. - if ( - typeof tokenData.scope !== "undefined" && - _.indexOf(tokenData.scope, "all") !== -1 - ) { + if (tokenData.scope?.includes("all")) { tokenData.scope = ["user"]; } @@ -100,7 +96,7 @@ export default () => { * @param {String} scope * @returns {Boolean} */ - hasScope: (scope) => typeof tokenData.scope !== "undefined" && _.indexOf(tokenData.scope, scope) !== -1, + hasScope: (scope) => tokenData.scope?.includes(scope) ?? false, /** * @param {String} key diff --git a/backend/package.json b/backend/package.json index 234ea6e801..14eec3e429 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,21 +7,21 @@ "main": "index.js", "type": "module", "dependencies": { - "@apidevtools/json-schema-ref-parser": "15.5.0", + "@apidevtools/json-schema-ref-parser": "15.5.1", "ajv": "8.20.0", "archiver": "8.0.0", "bcryptjs": "3.0.3", - "better-sqlite3": "12.11.1", + "better-sqlite3": "13.0.2", "cookie-parser": "1.4.7", "dayjs": "1.11.21", "express": "5.2.1", - "express-rate-limit": "8.6.0", + "express-rate-limit": "8.6.1", "jsonwebtoken": "9.0.3", "knex": "3.3.0", "liquidjs": "10.27.2", "lodash": "4.18.1", "multer": "2.2.0", - "mysql2": "3.23.1", + "mysql2": "3.23.2", "objection": "3.1.5", "openid-client": "6.8.4", "otplib": "13.4.1", @@ -30,6 +30,6 @@ }, "devDependencies": { "@apidevtools/swagger-parser": "12.1.0", - "@biomejs/biome": "2.5.4" + "@biomejs/biome": "2.5.6" } } diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index d579681f5e..99b97ae156 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@apidevtools/json-schema-ref-parser': - specifier: 15.5.0 - version: 15.5.0(@types/json-schema@7.0.15) + specifier: 15.5.1 + version: 15.5.1(@types/json-schema@7.0.15) ajv: specifier: 8.20.0 version: 8.20.0 @@ -21,8 +21,8 @@ importers: specifier: 3.0.3 version: 3.0.3 better-sqlite3: - specifier: 12.11.1 - version: 12.11.1 + specifier: 13.0.2 + version: 13.0.2 cookie-parser: specifier: 1.4.7 version: 1.4.7 @@ -33,14 +33,14 @@ importers: specifier: 5.2.1 version: 5.2.1 express-rate-limit: - specifier: 8.6.0 - version: 8.6.0(express@5.2.1) + specifier: 8.6.1 + version: 8.6.1(express@5.2.1) jsonwebtoken: specifier: 9.0.3 version: 9.0.3 knex: specifier: 3.3.0 - version: 3.3.0(better-sqlite3@12.11.1)(mysql2@3.23.1(@types/node@26.1.1))(pg@8.22.0) + version: 3.3.0(better-sqlite3@13.0.2)(mysql2@3.23.2(@types/node@26.1.1))(pg@8.22.0) liquidjs: specifier: 10.27.2 version: 10.27.2 @@ -51,11 +51,11 @@ importers: specifier: 2.2.0 version: 2.2.0 mysql2: - specifier: 3.23.1 - version: 3.23.1(@types/node@26.1.1) + specifier: 3.23.2 + version: 3.23.2(@types/node@26.1.1) objection: specifier: 3.1.5 - version: 3.1.5(knex@3.3.0(better-sqlite3@12.11.1)(mysql2@3.23.1(@types/node@26.1.1))(pg@8.22.0)) + version: 3.1.5(knex@3.3.0(better-sqlite3@13.0.2)(mysql2@3.23.2(@types/node@26.1.1))(pg@8.22.0)) openid-client: specifier: 6.8.4 version: 6.8.4 @@ -73,8 +73,8 @@ importers: specifier: 12.1.0 version: 12.1.0(openapi-types@12.1.3) '@biomejs/biome': - specifier: 2.5.4 - version: 2.5.4 + specifier: 2.5.6 + version: 2.5.6 packages: @@ -82,8 +82,8 @@ packages: resolution: {integrity: sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==} engines: {node: '>= 16'} - '@apidevtools/json-schema-ref-parser@15.5.0': - resolution: {integrity: sha512-Ps4w0FwrDoeVK6hfYxWkVbkmxm+zN+6xoXF2ZfEhfiox0ZNbcSAiUWO6iAIvP5bc3DB270r+EaKcoT1IUyzfxw==} + '@apidevtools/json-schema-ref-parser@15.5.1': + resolution: {integrity: sha512-69KDKWQvk5jfDQfSTzAUHkI731X7CaJLwQxlS87AXyFHoJzsd/Pufrqsz7PqXMgrtAA99D9Y6NujkEkzli6kVA==} engines: {node: '>=20'} peerDependencies: '@types/json-schema': ^7.0.15 @@ -100,59 +100,59 @@ packages: peerDependencies: openapi-types: '>=7' - '@biomejs/biome@2.5.4': - resolution: {integrity: sha512-xy5FNE5kQJKyK5MR1gJy6ztXYx4WBAbYGlK04lMEgmyPRWKybY9NFwiG9yo0XdzOU8Xvhj41u034J1ywfoWfMw==} + '@biomejs/biome@2.5.6': + resolution: {integrity: sha512-lxVNjv7UF6KfhMJfL9gaUHbWdJdHbsAj6OSmwSYNdhRuG67NxNQ4Xdvh3TUxsSK9sBzJBQhEJj3AopmmNJ5pSA==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.5.4': - resolution: {integrity: sha512-4o3NFRobXHynkgcFVrlZsoDAFtF2ldlEGN8sORSws5ZQqyY4PXnPUIylu4ksfyHuwkfvDREuWh3JK+niRwGq3w==} + '@biomejs/cli-darwin-arm64@2.5.6': + resolution: {integrity: sha512-zMOLZP4oMrjh6m1zcSj1ud2awUPgTuMVbmQhYYWL7J8HwCnbHHBvTm7VBTRuY7epT5bez76IpKYQ11ZAqHFlnw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.5.4': - resolution: {integrity: sha512-D32P5HkU2Y6PySuC/WsVDTOgsDwVFmujzhhhOQjajtATpVWFDXuVd3oRbsWNSEA+aaFzyzZm22szsyydBYlSyQ==} + '@biomejs/cli-darwin-x64@2.5.6': + resolution: {integrity: sha512-JAC1VqzvO7Th5ZplU0G2uGfkZbxEe9uDDektPAhF0JLusoz1w+T4okp2bkykI0bbaO2vslKiRfj4gU43JaGreA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.5.4': - resolution: {integrity: sha512-Rpm5/AT1m+DlJmUoYvS4/vXc+0tXJPJ2NQz25TGPyHVF5JrWy75PE0GH6kVxsKtQDuCH4OgzquZq0R4kj/wCVg==} + '@biomejs/cli-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-eUa3jeeYvfMt19LBeh6E5PUZpxnTC4JqNWo+EDjTtQjAr2xLGnWaxACtVU1DQqmHYbvThlJzLX+ZsYgrqh2qVw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.5.4': - resolution: {integrity: sha512-pSEfW7B8kTsXUjUxC1xVVK+y85Ht3C5XxZ9gclmC7/3Ku9Vqz8jmI7k0p/BNIjQ6t4sFERI2sFeH73ybiZl6YQ==} + '@biomejs/cli-linux-arm64@2.5.6': + resolution: {integrity: sha512-6XsYwCFkp5sMxl85ffhgeGpGgs6A7dRYFnkceZ7WVxvycuTnGdD5xa534Z3xfrBQ0JCMK/mujT6ZNPJoghedwg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.5.4': - resolution: {integrity: sha512-aby/PohmmgbShcHqFsZVzG8H6D98+P+A6xRWRrQcLW1pCjabcov5UUlke4UqNQBYTkDQav+jB4zyyDDeKB2GaA==} + '@biomejs/cli-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-2Vp13QdKysH3HIWLaYLhUUwbK+jbZonJD1K+Lr0d0RO4wH7mkYd43vJixEDm8cUWrowoRz4UUHF1nm9Ae7ym8A==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.5.4': - resolution: {integrity: sha512-FNxojWJkL7EajAuzBgoLe0T2G0y112M4lBrDIFl/DomFTx8yqenYOIdsRLNXvOvBBofE8hJi85LjzLmBDpY7/Q==} + '@biomejs/cli-linux-x64@2.5.6': + resolution: {integrity: sha512-Pop9VXCFUhFTMfFefZ39S+u2rOPyNp5iHlxbZRwXGACHLy2r0jjiRgJHmaEKJzL3SyxlVeGShXhvvElvWowonA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.5.4': - resolution: {integrity: sha512-emoXexPZIPAZkz2RKmA95WJUqK3I5MJNYtwEbL5ESciRzhmFMMyekDhNG8hpeOaK+ZGRDxAU4wvGuA5IHQ0h0w==} + '@biomejs/cli-win32-arm64@2.5.6': + resolution: {integrity: sha512-tDGshcm6BdkZOCGnTDX0Y8/U4IfBSlnUU7T56nNDuPEfed+aHg+u8G36NB43fJVl0Os6+QURXIE1yuD7AaEofA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.5.4': - resolution: {integrity: sha512-U1jaluLw1qQc2Tx7/CeSoL9N5XcqIH+GWjpUAy1ouB5nVjSCMNO+NNHdY3RAs8zxNurLWAdj6pehQdCA2zyU+Q==} + '@biomejs/cli-win32-x64@2.5.6': + resolution: {integrity: sha512-WN05KwXnTO/2J45RQPvzZMXf7tZUIofHoR35xIPfCo7pQ2RFidxI8sfb5mGsaTxdMmEOzHzOPRCdA5/fCpc7xQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -281,8 +281,8 @@ packages: bare-events: optional: true - bare-url@2.4.5: - resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==} + bare-url@2.4.6: + resolution: {integrity: sha512-iQxPClE07hETVpbRoX7JXX3v/ZQViCxe/SYCxylRLzdEx1xJAufPptfiOqR8tqiCtmbtMDANKWszzjLu1PMAZQ==} base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -291,23 +291,17 @@ packages: resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} hasBin: true - better-sqlite3@12.11.1: - resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==} - engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} - - bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + better-sqlite3@13.0.2: + resolution: {integrity: sha512-jW6oufeDhXZaiX9Lw5A+oerVClx4iFrI6uDj1zu7SqUAjak9vbJvA0NEcKLNxHiQHb6kYCoFzzXYV0YOauhV3g==} + engines: {node: '>=22'} body-parser@2.3.0: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} - brace-expansion@5.0.7: - resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} - engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + engines: {node: 20 || >=22} buffer-crc32@1.0.0: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} @@ -319,9 +313,6 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -344,9 +335,6 @@ packages: call-me-maybe@1.0.2: resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} - chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - colorette@2.0.19: resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} @@ -425,14 +413,6 @@ packages: supports-color: optional: true - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -441,10 +421,6 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -459,9 +435,6 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -500,12 +473,8 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - expand-template@2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - - express-rate-limit@8.6.0: - resolution: {integrity: sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==} + express-rate-limit@8.6.1: + resolution: {integrity: sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' @@ -523,9 +492,6 @@ packages: fast-uri@3.1.4: resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} - file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -538,9 +504,6 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} - fs-constants@1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -562,9 +525,6 @@ packages: getopts@2.3.0: resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==} - github-from-package@0.0.0: - resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -591,15 +551,12 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - interpret@2.2.0: resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} engines: {node: '>= 0.10'} - ip-address@10.2.0: - resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + ip-address@10.3.1: + resolution: {integrity: sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -623,13 +580,17 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - jose@6.2.3: - resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + jose@6.2.4: + resolution: {integrity: sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==} js-yaml@4.3.0: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true + js-yaml@5.2.2: + resolution: {integrity: sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==} + hasBin: true + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -725,8 +686,8 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} engines: {node: '>= 0.8'} merge-descriptors@2.0.0: @@ -749,20 +710,10 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -773,8 +724,8 @@ packages: resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} engines: {node: '>= 10.16.0'} - mysql2@3.23.1: - resolution: {integrity: sha512-tTuRnC7qCet2IOfSNMYZ5SwXuBnfvBPAcIA28P0gtruXyZlU1LMxA6uha32kYypoFgyYklMqhLWwt4laYwXR/Q==} + mysql2@3.23.2: + resolution: {integrity: sha512-fxh3HpQ8vJtu/Mmnd4Xsur19jGjHGzRLMxptiDtOkbX7EVBgnafGSGDx1WGGVmJLClVh2LeeBMMo24IFv8wCyQ==} engines: {node: '>= 8.0'} peerDependencies: '@types/node': '>= 8' @@ -783,16 +734,13 @@ packages: resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} engines: {node: '>=8.0.0'} - napi-build-utils@2.0.0: - resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - node-abi@3.94.0: - resolution: {integrity: sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==} - engines: {node: '>=10'} + node-addon-api@8.9.0: + resolution: {integrity: sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==} + engines: {node: ^18 || ^20 || >= 21} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -890,12 +838,6 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - prebuild-install@7.1.3: - resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} - engines: {node: '>=10'} - deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. - hasBin: true - process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -907,9 +849,6 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - pump@3.0.4: - resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} - qs@6.15.3: resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} engines: {node: '>=0.6'} @@ -922,10 +861,6 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -1003,12 +938,6 @@ packages: resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} - simple-concat@1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - - simple-get@4.0.1: - resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -1034,16 +963,12 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - swagger-ui-dist@5.32.9: - resolution: {integrity: sha512-8i2tzJQi+7bgxESMD2hg/UBumbTsf6vLbtu4cW5ETPz/B070UuS0rTP1hu6WSH81HcsHqYalJE+rP21Vg96rUQ==} + swagger-ui-dist@5.32.11: + resolution: {integrity: sha512-NEZzRuxHHQkbG3GCjNbzz+XRDoM7AztnXyzc2VCW5RXUvZBDW7bb3W29/SPfvav3yOzqnDTOLP2Xzbjxo0bldQ==} swagger-ui-express@5.0.1: resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==} @@ -1051,13 +976,6 @@ packages: peerDependencies: express: '>=4.0.0 || >=5.0.0-beta' - tar-fs@2.1.5: - resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} - - tar-stream@2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} - tar-stream@3.2.0: resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} @@ -1079,9 +997,6 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -1096,6 +1011,10 @@ packages: undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + undici@6.28.0: + resolution: {integrity: sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==} + engines: {node: '>=18.17'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -1125,10 +1044,11 @@ snapshots: '@types/json-schema': 7.0.15 js-yaml: 4.3.0 - '@apidevtools/json-schema-ref-parser@15.5.0(@types/json-schema@7.0.15)': + '@apidevtools/json-schema-ref-parser@15.5.1(@types/json-schema@7.0.15)': dependencies: '@types/json-schema': 7.0.15 - js-yaml: 4.3.0 + js-yaml: 5.2.2 + undici: 6.28.0 '@apidevtools/openapi-schemas@2.1.0': {} @@ -1144,39 +1064,39 @@ snapshots: call-me-maybe: 1.0.2 openapi-types: 12.1.3 - '@biomejs/biome@2.5.4': + '@biomejs/biome@2.5.6': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.5.4 - '@biomejs/cli-darwin-x64': 2.5.4 - '@biomejs/cli-linux-arm64': 2.5.4 - '@biomejs/cli-linux-arm64-musl': 2.5.4 - '@biomejs/cli-linux-x64': 2.5.4 - '@biomejs/cli-linux-x64-musl': 2.5.4 - '@biomejs/cli-win32-arm64': 2.5.4 - '@biomejs/cli-win32-x64': 2.5.4 - - '@biomejs/cli-darwin-arm64@2.5.4': + '@biomejs/cli-darwin-arm64': 2.5.6 + '@biomejs/cli-darwin-x64': 2.5.6 + '@biomejs/cli-linux-arm64': 2.5.6 + '@biomejs/cli-linux-arm64-musl': 2.5.6 + '@biomejs/cli-linux-x64': 2.5.6 + '@biomejs/cli-linux-x64-musl': 2.5.6 + '@biomejs/cli-win32-arm64': 2.5.6 + '@biomejs/cli-win32-x64': 2.5.6 + + '@biomejs/cli-darwin-arm64@2.5.6': optional: true - '@biomejs/cli-darwin-x64@2.5.4': + '@biomejs/cli-darwin-x64@2.5.6': optional: true - '@biomejs/cli-linux-arm64-musl@2.5.4': + '@biomejs/cli-linux-arm64-musl@2.5.6': optional: true - '@biomejs/cli-linux-arm64@2.5.4': + '@biomejs/cli-linux-arm64@2.5.6': optional: true - '@biomejs/cli-linux-x64-musl@2.5.4': + '@biomejs/cli-linux-x64-musl@2.5.6': optional: true - '@biomejs/cli-linux-x64@2.5.4': + '@biomejs/cli-linux-x64@2.5.6': optional: true - '@biomejs/cli-win32-arm64@2.5.4': + '@biomejs/cli-win32-arm64@2.5.6': optional: true - '@biomejs/cli-win32-x64@2.5.4': + '@biomejs/cli-win32-x64@2.5.6': optional: true '@noble/hashes@2.2.0': {} @@ -1277,7 +1197,7 @@ snapshots: bare-events: 2.9.1 bare-path: 3.1.1 bare-stream: 2.13.3(bare-events@2.9.1) - bare-url: 2.4.5 + bare-url: 2.4.6 fast-fifo: 1.3.2 transitivePeerDependencies: - bare-abort-controller @@ -1295,7 +1215,7 @@ snapshots: transitivePeerDependencies: - react-native-b4a - bare-url@2.4.5: + bare-url@2.4.6: dependencies: bare-path: 3.1.1 @@ -1303,20 +1223,9 @@ snapshots: bcryptjs@3.0.3: {} - better-sqlite3@12.11.1: + better-sqlite3@13.0.2: dependencies: - bindings: 1.5.0 - prebuild-install: 7.1.3 - - bindings@1.5.0: - dependencies: - file-uri-to-path: 1.0.0 - - bl@4.1.0: - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 + node-addon-api: 8.9.0 body-parser@2.3.0: dependencies: @@ -1332,7 +1241,7 @@ snapshots: transitivePeerDependencies: - supports-color - brace-expansion@5.0.7: + brace-expansion@5.0.8: dependencies: balanced-match: 4.0.4 @@ -1342,11 +1251,6 @@ snapshots: buffer-from@1.1.2: {} - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -1370,8 +1274,6 @@ snapshots: call-me-maybe@1.0.2: {} - chownr@1.1.4: {} - colorette@2.0.19: {} commander@10.0.1: {} @@ -1429,18 +1331,10 @@ snapshots: dependencies: ms: 2.1.3 - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - - deep-extend@0.6.0: {} - denque@2.1.0: {} depd@2.0.0: {} - detect-libc@2.1.2: {} - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -1455,10 +1349,6 @@ snapshots: encodeurl@2.0.0: {} - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 - es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -1485,13 +1375,11 @@ snapshots: events@3.3.0: {} - expand-template@2.0.3: {} - - express-rate-limit@8.6.0(express@5.2.1): + express-rate-limit@8.6.1(express@5.2.1): dependencies: debug: 4.4.3 express: 5.2.1 - ip-address: 10.2.0 + ip-address: 10.3.1 transitivePeerDependencies: - supports-color @@ -1534,8 +1422,6 @@ snapshots: fast-uri@3.1.4: {} - file-uri-to-path@1.0.0: {} - finalhandler@2.1.1: dependencies: debug: 4.4.3 @@ -1551,8 +1437,6 @@ snapshots: fresh@2.0.0: {} - fs-constants@1.0.0: {} - function-bind@1.1.2: {} generate-function@2.3.1: @@ -1581,8 +1465,6 @@ snapshots: getopts@2.3.0: {} - github-from-package@0.0.0: {} - gopd@1.2.0: {} has-symbols@1.1.0: {} @@ -1607,11 +1489,9 @@ snapshots: inherits@2.0.4: {} - ini@1.3.8: {} - interpret@2.2.0: {} - ip-address@10.2.0: {} + ip-address@10.3.1: {} ipaddr.js@1.9.1: {} @@ -1627,12 +1507,16 @@ snapshots: isarray@1.0.0: {} - jose@6.2.3: {} + jose@6.2.4: {} js-yaml@4.3.0: dependencies: argparse: 2.0.1 + js-yaml@5.2.2: + dependencies: + argparse: 2.0.1 + json-schema-traverse@1.0.0: {} jsonwebtoken@9.0.3: @@ -1659,7 +1543,7 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 - knex@3.3.0(better-sqlite3@12.11.1)(mysql2@3.23.1(@types/node@26.1.1))(pg@8.22.0): + knex@3.3.0(better-sqlite3@13.0.2)(mysql2@3.23.2(@types/node@26.1.1))(pg@8.22.0): dependencies: colorette: 2.0.19 commander: 10.0.1 @@ -1676,8 +1560,8 @@ snapshots: tarn: 3.1.2 tildify: 2.0.0 optionalDependencies: - better-sqlite3: 12.11.1 - mysql2: 3.23.1(@types/node@26.1.1) + better-sqlite3: 13.0.2 + mysql2: 3.23.2(@types/node@26.1.1) pg: 8.22.0 transitivePeerDependencies: - supports-color @@ -1714,7 +1598,7 @@ snapshots: media-typer@0.3.0: {} - media-typer@1.1.0: {} + media-typer@1.1.1: {} merge-descriptors@2.0.0: {} @@ -1730,15 +1614,9 @@ snapshots: dependencies: mime-db: 1.54.0 - mimic-response@3.1.0: {} - minimatch@10.2.5: dependencies: - brace-expansion: 5.0.7 - - minimist@1.2.8: {} - - mkdirp-classic@0.5.3: {} + brace-expansion: 5.0.8 ms@2.1.2: {} @@ -1751,7 +1629,7 @@ snapshots: concat-stream: 2.0.0 type-is: 1.6.18 - mysql2@3.23.1(@types/node@26.1.1): + mysql2@3.23.2(@types/node@26.1.1): dependencies: '@types/node': 26.1.1 aws-ssl-profiles: 1.1.2 @@ -1767,13 +1645,9 @@ snapshots: dependencies: lru.min: 1.1.4 - napi-build-utils@2.0.0: {} - negotiator@1.0.0: {} - node-abi@3.94.0: - dependencies: - semver: 7.8.5 + node-addon-api@8.9.0: {} normalize-path@3.0.0: {} @@ -1781,12 +1655,12 @@ snapshots: object-inspect@1.13.4: {} - objection@3.1.5(knex@3.3.0(better-sqlite3@12.11.1)(mysql2@3.23.1(@types/node@26.1.1))(pg@8.22.0)): + objection@3.1.5(knex@3.3.0(better-sqlite3@13.0.2)(mysql2@3.23.2(@types/node@26.1.1))(pg@8.22.0)): dependencies: ajv: 8.20.0 ajv-formats: 2.1.1(ajv@8.20.0) db-errors: 0.2.3 - knex: 3.3.0(better-sqlite3@12.11.1)(mysql2@3.23.1(@types/node@26.1.1))(pg@8.22.0) + knex: 3.3.0(better-sqlite3@13.0.2)(mysql2@3.23.2(@types/node@26.1.1))(pg@8.22.0) on-finished@2.4.1: dependencies: @@ -1800,7 +1674,7 @@ snapshots: openid-client@6.8.4: dependencies: - jose: 6.2.3 + jose: 6.2.4 oauth4webapi: 3.8.6 otplib@13.4.1: @@ -1865,21 +1739,6 @@ snapshots: dependencies: xtend: 4.0.2 - prebuild-install@7.1.3: - dependencies: - detect-libc: 2.1.2 - expand-template: 2.0.3 - github-from-package: 0.0.0 - minimist: 1.2.8 - mkdirp-classic: 0.5.3 - napi-build-utils: 2.0.0 - node-abi: 3.94.0 - pump: 3.0.4 - rc: 1.2.8 - simple-get: 4.0.1 - tar-fs: 2.1.5 - tunnel-agent: 0.6.0 - process-nextick-args@2.0.1: {} process@0.11.10: {} @@ -1889,11 +1748,6 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - pump@3.0.4: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - qs@6.15.3: dependencies: es-define-property: 1.0.1 @@ -1908,13 +1762,6 @@ snapshots: iconv-lite: 0.7.3 unpipe: 1.0.0 - rc@1.2.8: - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -2031,14 +1878,6 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - simple-concat@1.0.1: {} - - simple-get@4.0.1: - dependencies: - decompress-response: 6.0.0 - once: 1.4.0 - simple-concat: 1.0.1 - split2@4.2.0: {} sql-escaper@1.5.1: {} @@ -2064,33 +1903,16 @@ snapshots: dependencies: safe-buffer: 5.2.1 - strip-json-comments@2.0.1: {} - supports-preserve-symlinks-flag@1.0.0: {} - swagger-ui-dist@5.32.9: + swagger-ui-dist@5.32.11: dependencies: '@scarf/scarf': 1.4.0 swagger-ui-express@5.0.1(express@5.2.1): dependencies: express: 5.2.1 - swagger-ui-dist: 5.32.9 - - tar-fs@2.1.5: - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.4 - tar-stream: 2.2.0 - - tar-stream@2.2.0: - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.5 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 + swagger-ui-dist: 5.32.11 tar-stream@3.2.0: dependencies: @@ -2122,10 +1944,6 @@ snapshots: toidentifier@1.0.1: {} - tunnel-agent@0.6.0: - dependencies: - safe-buffer: 5.2.1 - type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -2134,13 +1952,15 @@ snapshots: type-is@2.1.0: dependencies: content-type: 2.0.0 - media-typer: 1.1.0 + media-typer: 1.1.1 mime-types: 3.0.2 typedarray@0.0.6: {} undici-types@8.3.0: {} + undici@6.28.0: {} + unpipe@1.0.0: {} util-deprecate@1.0.2: {} diff --git a/backend/pnpm-workspace.yaml b/backend/pnpm-workspace.yaml index 7d087de8a0..3e70b91cea 100644 --- a/backend/pnpm-workspace.yaml +++ b/backend/pnpm-workspace.yaml @@ -1,4 +1,4 @@ minimumReleaseAge: 0 allowBuilds: '@scarf/scarf': false - better-sqlite3: true + better-sqlite3: false diff --git a/backend/routes/docs.js b/backend/routes/docs.js index defa9eac84..ebe3e596f4 100644 --- a/backend/routes/docs.js +++ b/backend/routes/docs.js @@ -1,5 +1,6 @@ import express from "express"; import swaggerUi from "swagger-ui-express"; +import requireLogin from "../lib/express/require-login.js"; import { debug, express as logger } from "../logger.js"; import PACKAGE from "../package.json" with { type: "json" }; import { getCompiledSchema } from "../schema/index.js"; @@ -10,7 +11,7 @@ const router = express.Router({ mergeParams: true, }); -router.use("/", swaggerUi.serve); +router.use("/", requireLogin(), swaggerUi.serve); router .route("/") diff --git a/backend/routes/main.js b/backend/routes/main.js index 071f07096a..a99cab2705 100644 --- a/backend/routes/main.js +++ b/backend/routes/main.js @@ -1,20 +1,20 @@ import express from "express"; import errs from "../lib/error.js"; -import pjson from "../package.json" with { type: "json" }; +import jwtdecode from "../lib/express/jwt-decode.js"; import { isSetup } from "../setup.js"; import auditLogRoutes from "./audit-log.js"; +import docsRoutes from "./docs.js"; import accessListsRoutes from "./nginx/access_lists.js"; import certificatesHostsRoutes from "./nginx/certificates.js"; import deadHostsRoutes from "./nginx/dead_hosts.js"; import proxyHostsRoutes from "./nginx/proxy_hosts.js"; import redirectionHostsRoutes from "./nginx/redirection_hosts.js"; import streamsRoutes from "./nginx/streams.js"; +import oidcRoutes from "./oidc.js"; import reportsRoutes from "./reports.js"; -import docsRoutes from "./docs.js"; import schemaRoutes from "./schema.js"; import settingsRoutes from "./settings.js"; import tokensRoutes from "./tokens.js"; -import oidcRoutes from "./oidc.js"; import usersRoutes from "./users.js"; import versionRoutes from "./version.js"; @@ -39,12 +39,19 @@ router.get(["/api", "/api/"], async (_, res /*, next*/) => { res.status(200).send({ status: "OK", setup: await isSetup(), - version: pjson.version, password: process.env.OIDC_DISABLE_PASSWORD === "false", oidc: isOIDCenabled, }); }); +/** + * Auth Check, used by the nginx auth_request directive + * GET /api/auth + */ +router.get("/api/auth", jwtdecode(), (_, res) => { + res.sendStatus(res.locals.access?.token.getUserId(0) ? 200 : 401); +}); + router.use("/api/docs", docsRoutes); router.use("/api/schema", schemaRoutes); router.use("/api/tokens", tokensRoutes); diff --git a/backend/routes/nginx/certificates.js b/backend/routes/nginx/certificates.js index a408066489..f3f39e7418 100644 --- a/backend/routes/nginx/certificates.js +++ b/backend/routes/nginx/certificates.js @@ -1,4 +1,7 @@ +import { rm } from "node:fs/promises"; +import path from "node:path"; import express from "express"; +import { rateLimit } from "express-rate-limit"; import multer from "multer"; import dnsPlugins from "../../certbot/dns-plugins.json" with { type: "json" }; import internalCertificate from "../../internal/certificate.js"; @@ -15,6 +18,16 @@ const router = express.Router({ mergeParams: true, }); +const downloadLimiter = rateLimit({ + windowMs: 10 * 60 * 1000, + limit: 10, + message: { error: { message: "Too many requests, please try again later." } }, + standardHeaders: "draft-8", + legacyHeaders: false, + ipv6Subnet: 48, + validate: { trustProxy: false }, +}); + const uploadCerts = multer({ storage: multer.memoryStorage(), limits: { fileSize: 1024 * 1024 } }).fields([ { name: "certificate", maxCount: 1 }, { name: "certificate_key", maxCount: 1 }, @@ -323,12 +336,15 @@ router * * Renew certificate */ - .get(async (req, res, next) => { + .get(downloadLimiter, async (req, res, next) => { try { const result = await internalCertificate.download(res.locals.access, { id: Number.parseInt(req.params.certificate_id, 10), }); - res.status(200).download(result.fileName); + res.status(200).download(result.fileName, (err) => { + rm(path.dirname(result.fileName), { recursive: true, force: true }); + if (err && !res.headersSent) next(err); + }); } catch (err) { debug(logger, `${req.method.toUpperCase()} ${req.originalUrl}: ${err}`); next(err); diff --git a/backend/routes/oidc.js b/backend/routes/oidc.js index 534223cce5..48c86e55fb 100644 --- a/backend/routes/oidc.js +++ b/backend/routes/oidc.js @@ -1,8 +1,8 @@ -import * as client from "openid-client"; import express from "express"; import { rateLimit } from "express-rate-limit"; -import errs from "../lib/error.js"; +import * as client from "openid-client"; import internalToken from "../internal/token.js"; +import errs from "../lib/error.js"; import { oidc as logger } from "../logger.js"; const router = express.Router({ @@ -12,8 +12,8 @@ const router = express.Router({ }); const limiter = rateLimit({ - windowMs: 10 * 60 * 1000, - limit: 10, + windowMs: 5 * 60 * 1000, + limit: 5, standardHeaders: "draft-8", legacyHeaders: false, ipv6Subnet: 48, @@ -52,30 +52,41 @@ router code_challenge: await client.calculatePKCECodeChallenge(code_verifier), }; - res.cookie("__Host-npmplus_oidc_no_redirect", "true", { secure: true, sameSite: "Strict" }); + res.cookie("__Host-npmplus_oidc_no_redirect", "true", { + secure: true, + sameSite: "Strict", + maxAge: 60 * 60 * 1000, + }); res.cookie("__Host-Http-npmplus_oidc_code_verifier", code_verifier, { signed: true, httpOnly: true, secure: true, sameSite: "Lax", + maxAge: 15 * 60 * 1000, }); res.cookie("__Host-Http-npmplus_oidc_state", parameters.state, { signed: true, httpOnly: true, secure: true, sameSite: "Lax", + maxAge: 15 * 60 * 1000, }); res.cookie("__Host-Http-npmplus_oidc_nonce", parameters.nonce, { signed: true, httpOnly: true, secure: true, sameSite: "Lax", + maxAge: 15 * 60 * 1000, }); res.redirect(await client.buildAuthorizationUrl(config, parameters).toString()); } catch (err) { logger.error(`Callback error: ${err.message}`); - res.cookie("__Host-npmplus_oidc_no_redirect", "true", { secure: true, sameSite: "Strict" }); + res.cookie("__Host-npmplus_oidc_no_redirect", "true", { + secure: true, + sameSite: "Strict", + maxAge: 60 * 60 * 1000, + }); res.clearCookie("__Host-Http-npmplus_oidc_state", { httpOnly: true, secure: true, @@ -143,15 +154,6 @@ router const data = await internalToken.getTokenFromOAuthClaim({ identity: claims.email.toLowerCase().trim() }); - res.cookie("__Host-Http-token", data.token, { - signed: true, - httpOnly: true, - secure: true, - sameSite: "Strict", - expires: new Date(data.expires), - }); - - res.clearCookie("__Host-npmplus_oidc_no_redirect", { secure: true, sameSite: "Strict" }); res.clearCookie("__Host-Http-npmplus_oidc_state", { httpOnly: true, secure: true, @@ -167,6 +169,30 @@ router secure: true, sameSite: "Lax", }); + + if (data.requires2fa) { + res.cookie("__Host-Http-challenge_token", data.token, { + signed: true, + httpOnly: true, + secure: true, + sameSite: "Strict", + expires: new Date(data.expires), + }); + res.cookie("__Host-npmplus_oidc_totp_required", "true", { + secure: true, + sameSite: "Strict", + expires: new Date(data.expires), + }); + } else { + res.cookie("__Host-Http-token", data.token, { + signed: true, + httpOnly: true, + secure: true, + sameSite: "Strict", + expires: new Date(data.expires), + }); + res.clearCookie("__Host-npmplus_oidc_no_redirect", { secure: true, sameSite: "Strict" }); + } res.redirect("/"); } catch (err) { logger.error(`Callback error: ${err.message}`); diff --git a/backend/routes/schema.js b/backend/routes/schema.js index c258331ed5..5d61c969a4 100644 --- a/backend/routes/schema.js +++ b/backend/routes/schema.js @@ -1,4 +1,5 @@ import express from "express"; +import requireLogin from "../lib/express/require-login.js"; import { debug, express as logger } from "../logger.js"; import PACKAGE from "../package.json" with { type: "json" }; import { getCompiledSchema } from "../schema/index.js"; @@ -14,6 +15,7 @@ router .options((_, res) => { res.sendStatus(204); }) + .all(requireLogin()) /** * GET /schema diff --git a/backend/routes/tokens.js b/backend/routes/tokens.js index 838908e661..4a490d2a3f 100644 --- a/backend/routes/tokens.js +++ b/backend/routes/tokens.js @@ -15,7 +15,7 @@ const router = express.Router({ const limiter = rateLimit({ windowMs: 5 * 60 * 1000, - limit: 10, + limit: 5, message: { error: { message: "Too many requests, please try again later." } }, standardHeaders: "draft-8", legacyHeaders: false, @@ -46,15 +46,16 @@ router secure: true, sameSite: "Strict", }); - res.cookie("__Host-npmplus_oidc_no_redirect", "true", { secure: true, sameSite: "Strict" }); + res.cookie("__Host-npmplus_oidc_no_redirect", "true", { + secure: true, + sameSite: "Strict", + maxAge: 60 * 60 * 1000, + }); return res.status(401).send({ expires: new Date(0).toISOString() }); } try { - const data = await internalToken.getFreshToken(res.locals.access, { - expiry: typeof req.query.expiry !== "undefined" ? req.query.expiry : null, - scope: typeof req.query.scope !== "undefined" ? req.query.scope : null, - }); + const data = await internalToken.getFreshToken(res.locals.access); res.cookie("__Host-Http-token", data.token, { signed: true, @@ -91,8 +92,16 @@ router const result = await internalToken.getTokenFromEmail(data); const { token, ...responseBody } = result; - if (result.token && result.expires) { - res.cookie("__Host-Http-token", result.token, { + if (result.requires2fa) { + res.cookie("__Host-Http-challenge_token", token, { + signed: true, + httpOnly: true, + secure: true, + sameSite: "Strict", + expires: new Date(result.expires), + }); + } else { + res.cookie("__Host-Http-token", token, { signed: true, httpOnly: true, secure: true, @@ -120,7 +129,11 @@ router secure: true, sameSite: "Strict", }); - res.cookie("__Host-npmplus_oidc_no_redirect", "true", { secure: true, sameSite: "Strict" }); + res.cookie("__Host-npmplus_oidc_no_redirect", "true", { + secure: true, + sameSite: "Strict", + maxAge: 60 * 60 * 1000, + }); res.status(200).send({ expires: new Date(0).toISOString() }); } catch (err) { debug(logger, `${req.method.toUpperCase()} ${req.originalUrl}: ${err}`); @@ -141,23 +154,24 @@ router */ .post(async (req, res, next) => { try { - if (process.env.OIDC_DISABLE_PASSWORD === "true") { - throw new errs.AuthError("Non OIDC login is disabled"); - } - - const { challenge_token, code } = await apiValidator(getValidationSchema("/tokens/2fa", "post"), req.body); - const result = await internalToken.verify2FA(challenge_token, code); + const { code } = await apiValidator(getValidationSchema("/tokens/2fa", "post"), req.body); + const result = await internalToken.verify2FA(req.signedCookies?.["__Host-Http-challenge_token"], code); const { token, ...responseBody } = result; - if (result.token && result.expires) { - res.cookie("__Host-Http-token", result.token, { - signed: true, - httpOnly: true, - secure: true, - sameSite: "Strict", - expires: new Date(result.expires), - }); - } + res.cookie("__Host-Http-token", token, { + signed: true, + httpOnly: true, + secure: true, + sameSite: "Strict", + expires: new Date(result.expires), + }); + res.clearCookie("__Host-Http-challenge_token", { + httpOnly: true, + secure: true, + sameSite: "Strict", + }); + res.clearCookie("__Host-npmplus_oidc_totp_required", { secure: true, sameSite: "Strict" }); + res.clearCookie("__Host-npmplus_oidc_no_redirect", { secure: true, sameSite: "Strict" }); res.status(200).send(responseBody); } catch (err) { diff --git a/backend/routes/users.js b/backend/routes/users.js index 3728249199..0ac4c3c5d6 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -1,8 +1,9 @@ import express from "express"; +import { rateLimit } from "express-rate-limit"; +import multer from "multer"; import internal2FA from "../internal/2fa.js"; import internalUser from "../internal/user.js"; import Access from "../lib/access.js"; -import errs from "../lib/error.js"; import jwtdecode from "../lib/express/jwt-decode.js"; import userIdFromMe from "../lib/express/user-id-from-me.js"; import apiValidator from "../lib/validator/api.js"; @@ -17,6 +18,19 @@ const router = express.Router({ mergeParams: true, }); +const limiter = rateLimit({ + windowMs: 5 * 60 * 1000, + limit: 5, + message: { error: { message: "Too many requests, please try again later." } }, + standardHeaders: "draft-8", + legacyHeaders: false, + ipv6Subnet: 48, + skipSuccessfulRequests: true, + validate: { trustProxy: false }, +}); + +router.use(limiter); + /** * /api/users */ @@ -241,47 +255,6 @@ router } }); -/** - * Specific user login as - * - * /api/users/123/login - */ -router - .route("/:user_id/login") - .options((_, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * POST /api/users/123/login - * - * Log in as a user - */ - .post(async (req, res, next) => { - try { - const result = await internalUser.loginAs(res.locals.access, { - id: Number.parseInt(req.params.user_id, 10), - }); - const { token, ...responseBody } = result; - - if (result.token && result.expires) { - res.cookie("__Host-Http-token", result.token, { - signed: true, - httpOnly: true, - secure: true, - sameSite: "Strict", - expires: new Date(result.expires), - }); - } - - res.status(200).send(responseBody); - } catch (err) { - debug(logger, `${req.method.toUpperCase()} ${req.originalUrl}: ${err}`); - next(err); - } - }); - /** * User 2FA status * @@ -333,10 +306,11 @@ router .delete(async (req, res, next) => { try { const code = typeof req.query.code === "string" ? req.query.code : null; - if (!code) { - throw new errs.ValidationError("Missing required parameter: code"); + if (code) { + await internal2FA.disable(res.locals.access, req.params.user_id, code); + } else { + await internal2FA.adminDisable(res.locals.access, req.params.user_id); } - await internal2FA.disable(res.locals.access, req.params.user_id, code); res.status(200).send(true); } catch (err) { debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); @@ -405,4 +379,80 @@ router } }); +router + .route("/:user_id/sessions") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + .all(userIdFromMe) + + /** + * DELETE /api/users/123/sessions + * + * Revoke all of a user's sessions (self or admin) + */ + .delete(async (req, res, next) => { + try { + await internalUser.revokeSessions(res.locals.access, req.params.user_id); + if (Number(req.params.user_id) === res.locals.access.token.getUserId(0)) { + res.clearCookie("__Host-Http-token", { + httpOnly: true, + secure: true, + sameSite: "Strict", + }); + } + res.status(200).send(true); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.originalUrl}: ${err}`); + next(err); + } + }); + +/** + * User avatar + * + * /api/users/123/avatar + */ +router + .route("/:user_id/avatar") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + .all(userIdFromMe) + + /** + * POST /api/users/123/avatar + * + * Upload a custom avatar + */ + .post( + multer({ storage: multer.memoryStorage(), limits: { fileSize: 1024 * 1024 } }).single("avatar"), + async (req, res, next) => { + try { + const result = await internalUser.setAvatar(res.locals.access, req.params.user_id, req.file); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.originalUrl}: ${err}`); + next(err); + } + }, + ) + + /** + * DELETE /api/users/123/avatar + * + * Remove the custom avatar, falling back to gravatar + */ + .delete(async (req, res, next) => { + try { + const result = await internalUser.deleteAvatar(res.locals.access, req.params.user_id); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.originalUrl}: ${err}`); + next(err); + } + }); + export default router; diff --git a/backend/routes/version.js b/backend/routes/version.js index e4ea53c548..1dc058ee54 100644 --- a/backend/routes/version.js +++ b/backend/routes/version.js @@ -1,5 +1,6 @@ import express from "express"; import internalRemoteVersion from "../internal/remote-version.js"; +import requireLogin from "../lib/express/require-login.js"; import { debug, express as logger } from "../logger.js"; const router = express.Router({ @@ -16,6 +17,7 @@ router .options((_, res) => { res.sendStatus(204); }) + .all(requireLogin()) /** * GET /api/version/check diff --git a/backend/schema/components/health-object.json b/backend/schema/components/health-object.json index 64fed1e63a..c749deef26 100644 --- a/backend/schema/components/health-object.json +++ b/backend/schema/components/health-object.json @@ -15,11 +15,6 @@ "type": "boolean", "description": "Whether the initial setup has been completed", "example": true - }, - "version": { - "type": "string", - "description": "The version string", - "example": "..." } } } diff --git a/backend/schema/components/stream-object.json b/backend/schema/components/stream-object.json index 5b18e8de9e..db1aa2f069 100644 --- a/backend/schema/components/stream-object.json +++ b/backend/schema/components/stream-object.json @@ -43,10 +43,7 @@ "example": "127.0.0.1" }, "forwarding_port": { - "type": [ - "string", - "null" - ], + "type": "string", "pattern": "^([0-9]{0,5}|\\$server_port)$", "maxLength": 12, "example": 80 @@ -82,7 +79,7 @@ "example": {} }, "npmplus_description": { - "type": ["string", "null"], + "type": "string", "maxLength": 255, "example": "MariaDB database connection" }, diff --git a/backend/schema/components/token-challenge.json b/backend/schema/components/token-challenge.json index 344874622a..c90e114df7 100644 --- a/backend/schema/components/token-challenge.json +++ b/backend/schema/components/token-challenge.json @@ -1,7 +1,7 @@ { "type": "object", "description": "Token object", - "required": ["requires2fa", "challenge_token"], + "required": ["requires2fa", "expires"], "additionalProperties": false, "properties": { "requires2fa": { @@ -9,9 +9,9 @@ "example": true, "type": "boolean" }, - "challenge_token": { - "description": "Challenge Token used in subsequent 2FA verification", - "example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", + "expires": { + "description": "Token Expiry ISO Time String", + "example": "2025-02-04T20:40:46.340Z", "type": "string" } } diff --git a/backend/schema/paths/get.json b/backend/schema/paths/get.json index de7378ee06..f31f34058a 100644 --- a/backend/schema/paths/get.json +++ b/backend/schema/paths/get.json @@ -13,8 +13,7 @@ "default": { "value": { "status": "OK", - "setup": true, - "version": "..." + "setup": true } } }, diff --git a/backend/schema/paths/schema/get.json b/backend/schema/paths/schema/get.json index a374d21a4d..cf1271ea2d 100644 --- a/backend/schema/paths/schema/get.json +++ b/backend/schema/paths/schema/get.json @@ -2,7 +2,12 @@ "operationId": "schema", "summary": "Returns this swagger API schema", "tags": [ - "public" + "docs" + ], + "security": [ + { + "cookieAuth": [] + } ], "responses": { "200": { diff --git a/backend/schema/paths/tokens/2fa/post.json b/backend/schema/paths/tokens/2fa/post.json index fa8390dd40..19dab49def 100644 --- a/backend/schema/paths/tokens/2fa/post.json +++ b/backend/schema/paths/tokens/2fa/post.json @@ -10,11 +10,6 @@ "schema": { "additionalProperties": false, "properties": { - "challenge_token": { - "minLength": 1, - "type": "string", - "example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" - }, "code": { "minLength": 6, "maxLength": 8, @@ -22,11 +17,10 @@ "example": "012345" } }, - "required": ["challenge_token", "code"], + "required": ["code"], "type": "object" }, "example": { - "challenge_token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", "code": "012345" } } diff --git a/backend/schema/paths/tokens/post.json b/backend/schema/paths/tokens/post.json index 7f64a92cd2..50c8bf2f63 100644 --- a/backend/schema/paths/tokens/post.json +++ b/backend/schema/paths/tokens/post.json @@ -17,18 +17,15 @@ "type": "string", "example": "me@example.com" }, - "scope": { + "secret": { "minLength": 1, "type": "string", - "enum": [ - "user" - ], - "example": "user" + "example": "bigredhorsebanana" }, - "secret": { + "code": { "minLength": 1, "type": "string", - "example": "bigredhorsebanana" + "example": "123456" } }, "required": [ diff --git a/backend/schema/paths/users/userID/2fa/delete.json b/backend/schema/paths/users/userID/2fa/delete.json index bff292d829..4098050fd3 100644 --- a/backend/schema/paths/users/userID/2fa/delete.json +++ b/backend/schema/paths/users/userID/2fa/delete.json @@ -20,10 +20,10 @@ "schema": { "type": "string", "minLength": 6, - "maxLength": 6, + "maxLength": 8, "example": "012345" }, - "required": true, + "required": false, "description": "2fa Code", "example": "012345" } diff --git a/backend/schema/paths/users/userID/sessions/delete.json b/backend/schema/paths/users/userID/sessions/delete.json new file mode 100644 index 0000000000..91eddccc5f --- /dev/null +++ b/backend/schema/paths/users/userID/sessions/delete.json @@ -0,0 +1,43 @@ +{ + "operationId": "revokeUserSessions", + "summary": "Revoke all of a user's sessions", + "tags": ["users"], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "oneOf": [ + { + "type": "string", + "pattern": "^me$" + }, + { + "type": "integer", + "minimum": 1 + } + ] + }, + "required": true, + "description": "User ID or 'me' for yourself", + "example": 2 + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + }, + "description": "200 response" + } + } +} diff --git a/backend/schema/paths/version/check/get.json b/backend/schema/paths/version/check/get.json index 4a4d6a7563..a3c64b946c 100644 --- a/backend/schema/paths/version/check/get.json +++ b/backend/schema/paths/version/check/get.json @@ -2,7 +2,12 @@ "operationId": "checkVersion", "summary": "Returns any new version data from github", "tags": [ - "public" + "version" + ], + "security": [ + { + "cookieAuth": [] + } ], "responses": { "200": { diff --git a/backend/schema/swagger.json b/backend/schema/swagger.json index d32a195a7e..1ca68d357e 100644 --- a/backend/schema/swagger.json +++ b/backend/schema/swagger.json @@ -63,6 +63,14 @@ { "name": "users", "description": "Endpoints for managing users" + }, + { + "name": "version", + "description": "Endpoints related to the running version" + }, + { + "name": "docs", + "description": "Endpoints related to this API documentation" } ], "paths": { @@ -343,6 +351,11 @@ "$ref": "./paths/users/userID/2fa/backup-codes/post.json" } }, + "/users/{userID}/sessions": { + "delete": { + "$ref": "./paths/users/userID/sessions/delete.json" + } + }, "/users/{userID}/auth": { "put": { "$ref": "./paths/users/userID/auth/put.json" diff --git a/backend/setup.js b/backend/setup.js index 9ae957325c..b65818a017 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -1,22 +1,21 @@ import { writeFile } from "node:fs/promises"; +import internalHost from "./internal/host.js"; +import internalNginx from "./internal/nginx.js"; +import internalProxyHost from "./internal/proxy-host.js"; +import internalProxyHostAccessList from "./internal/proxy-host-access-list.js"; +import Access from "./lib/access.js"; import { installPlugins } from "./lib/certbot.js"; import utils from "./lib/utils.js"; import { setup as logger } from "./logger.js"; import authModel from "./models/auth.js"; import certificateModel from "./models/certificate.js"; -import settingModel from "./models/setting.js"; -import userModel from "./models/user.js"; -import userPermissionModel from "./models/user_permission.js"; - +import deadModel from "./models/dead_host.js"; import proxyModel from "./models/proxy_host.js"; import redirectionModel from "./models/redirection_host.js"; -import deadModel from "./models/dead_host.js"; +import settingModel from "./models/setting.js"; import streamModel from "./models/stream.js"; -import Access from "./lib/access.js"; -import internalHost from "./internal/host.js"; -import internalNginx from "./internal/nginx.js"; -import internalProxyHost from "./internal/proxy-host.js"; -import internalProxyHostAccessList from "./internal/proxy-host-access-list.js"; +import userModel from "./models/user.js"; +import userPermissionModel from "./models/user_permission.js"; export const isSetup = async () => { const row = await userModel.query().select("id").where("is_deleted", 0).first(); @@ -89,7 +88,7 @@ const setupDefaultSettings = async () => { id: "default-site", name: "Default Site", description: "What to show when Nginx is hit with an unknown Host", - value: process.env.INITIAL_DEFAULT_PAGE, + value: process.env.INITIAL_DEFAULT_PAGE || "congratulations", meta: {}, }); logger.info("Default settings added"); diff --git a/backend/sqlite-vaccum.js b/backend/sqlite-vaccum.js index 5464c4cd5e..97c2464006 100755 --- a/backend/sqlite-vaccum.js +++ b/backend/sqlite-vaccum.js @@ -1,6 +1,7 @@ #!/usr/bin/env node import Database from "better-sqlite3"; + const db = new Database("/data/npmplus/database.sqlite"); db.pragma("journal_mode = WAL"); diff --git a/backend/templates/_proxy_host_custom_location.conf b/backend/templates/_proxy_host_custom_location.conf index 5b45dc6192..19e368a7b4 100644 --- a/backend/templates/_proxy_host_custom_location.conf +++ b/backend/templates/_proxy_host_custom_location.conf @@ -17,12 +17,9 @@ location {{ location_type }}{{ path }} { {% for client in access_list.clients %} {{ client | nginxAccessRule }} {% endfor %} - # Access checks must... - {% if access_list.satisfy_any %} - satisfy any; - {% else %} - satisfy all; - {% endif %} + {% endif %} + {% if access_list.satisfy_any %} + satisfy any; {% endif %} {% if npmplus_noindex == true %}include noindex-nofollow.conf;{% endif %} diff --git a/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf index fc181fdd08..126d5a15e6 100644 --- a/backend/templates/proxy_host.conf +++ b/backend/templates/proxy_host.conf @@ -150,7 +150,7 @@ # Authorization auth_basic "basic access authentication required"; auth_basic_user_file {{ filename }}; - {% if access_list.pass_auth %} + {% if not access_list.pass_auth %} proxy_set_header Authorization ""; {% endif %} {% endif %} @@ -159,12 +159,9 @@ {% for client in access_list.clients %} {{ client | nginxAccessRule }} {% endfor %} - # Access checks must... - {% if access_list.satisfy_any %} - satisfy any; - {% else %} - satisfy all; - {% endif %} + {% endif %} + {% if access_list.satisfy_any %} + satisfy any; {% endif %} {% if npmplus_noindex == true %}include noindex-nofollow.conf;{% endif %} @@ -331,7 +328,7 @@ proxy_set_header Content-Length ""; } {% if env.AUTH_REQUEST_ANUBIS_USE_CUSTOM_IMAGES == "true" %} - location /.within.website/x/cmd/anubis/static/img { + location /.within.website/x/cmd/anubis/static/img/ { alias /data/anubis/; } {% endif %} diff --git a/caddy/Dockerfile b/caddy/Dockerfile index 12731e93dc..05ec1440f7 100644 --- a/caddy/Dockerfile +++ b/caddy/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1.25.0@sha256:0adf442eae370b6087e08edc7c50b552d80ddf261576f4ebd6421006b2461f12 FROM alpine:3.24.1@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b RUN apk add --no-cache tzdata -COPY --from=caddy:2.11.4@sha256:af5fdcd76f2db5e4e974ee92f96ee8c0fc3edb55bd4ba5032547cbf3f65e486d /usr/bin/caddy /usr/bin/caddy +COPY --from=caddy:2.11.4@sha256:844f60b64e4724a5aa8245e019dace0d3f199f7433ce6c57676cb30a920dbad9 /usr/bin/caddy /usr/bin/caddy COPY Caddyfile /etc/caddy/Caddyfile CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] diff --git a/compose.yaml b/compose.yaml index f3a720ecd5..1f9c4f02b2 100644 --- a/compose.yaml +++ b/compose.yaml @@ -110,6 +110,7 @@ services: # - "OIDC_CLIENT_SECRET=..." # client secret for oidc # - "OIDC_REQUIRE_VERIFIED_EMAIL=false" # requires the email sent by the OIDC Provider to be marked as verified, default true # - "OIDC_DISABLE_PASSWORD=true" # disables plain email/password login, default false +# - "OIDC_SKIP_MFA=true" # skips the second factor after a successful OIDC login, default false # - "AUTH_REQUEST_ANUBIS_UPSTREAM=http://127.0.0.1:8923" # scheme + ip/hostname/domain (+ port) to internal anubis, without path, this is unrelated to OIDC # - "AUTH_REQUEST_TINYAUTH_UPSTREAM=http://127.0.0.1:3000" # scheme + ip/hostname/domain (+ port) to internal tinyauth, without path, this is unrelated to OIDC # - "AUTH_REQUEST_OAUTH2PROXY_UPSTREAM=http://127.0.0.1:4180" # scheme + ip/hostname/domain (+ port) to internal oauth2proxy, without path, this is unrelated to OIDC diff --git a/frontend/biome.json b/frontend/biome.json index 9d61b4f713..80d83efbe7 100644 --- a/frontend/biome.json +++ b/frontend/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.6/schema.json", "vcs": { "enabled": true, "clientKind": "git", @@ -7,7 +7,7 @@ }, "files": { "ignoreUnknown": false, - "includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "!**/dist/**/*"] + "includes": ["**", "!**/*.svg"] }, "formatter": { "enabled": true, @@ -16,59 +16,32 @@ "lineWidth": 120, "formatWithErrors": true }, - "assist": { - "actions": { - "source": { - "organizeImports": { - "level": "on", - "options": { - "groups": [ - ":BUN:", - ":NODE:", - ["npm:*", "npm:*/**"], - ":PACKAGE_WITH_PROTOCOL:", - ":URL:", - ":PACKAGE:", - ["/src/*", "/src/**"], - ["/**"], - ["#*", "#*/**"], - ":PATH:" - ] - } - } - } - } - }, "linter": { "enabled": true, "rules": { "preset": "recommended", - "correctness": { - "useUniqueElementIds": "off" - }, "suspicious": { "noExplicitAny": "off", "noArrayIndexKey": "off" }, - "performance": { - "noDelete": "off" - }, - "nursery": "off", "a11y": { - "useSemanticElements": "off", "useValidAnchor": "off" }, - "style": { - "noParameterAssign": "error", - "useAsConstAssertion": "error", - "useDefaultParameterLast": "error", - "useEnumInitializers": "error", - "useSelfClosingElements": "error", - "useSingleVarDeclarator": "error", - "noUnusedTemplateLiteral": "error", - "useNumberNamespace": "error", - "noInferrableTypes": "error", - "noUselessElse": "error" + "complexity": { + "noImportantStyles": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" } } } diff --git a/frontend/check-locales.cjs b/frontend/check-locales.cjs index 24c35f7041..bcc463073b 100644 --- a/frontend/check-locales.cjs +++ b/frontend/check-locales.cjs @@ -4,7 +4,6 @@ // - Ensures that the name of the locale exists in the language list // - Ensures that each locale contains the translations used in the application // - Ensures that there are no unused translations in the locale files -// - Also checks the error messages returned by the backend const allLocales = [ ["en", "en-US"], @@ -33,33 +32,18 @@ const allLocales = [ const ignoreUnused = [/^.*$/]; -const { spawnSync } = require("child_process"); -const fs = require("fs"); +const { spawnSync } = require("node:child_process"); const tmp = require("tmp"); -// Parse backend errors -const BACKEND_ERRORS_FILE = "../backend/internal/errors/errors.go"; -const BACKEND_ERRORS = []; -/* -try { - const backendErrorsContent = fs.readFileSync(BACKEND_ERRORS_FILE, "utf8"); - const backendErrorsContentRes = [ - ...backendErrorsContent.matchAll(/(?:errors|eris)\.New\("([^"]+)"\)/g), - ]; - backendErrorsContentRes.map((item) => { - BACKEND_ERRORS.push("error." + item[1]); - return null; - }); -} catch (err) { - console.log("\x1b[31m%s\x1b[0m", err); - process.exit(1); -} -*/ - // get all translations used in frontend code const tmpobj = tmp.fileSync({ postfix: ".json" }); -spawnSync("pnpm", ["formatjs", "extract", "'src/**/*.tsx'", "--out-file", tmpobj.name]); +const extract = spawnSync("pnpm", ["formatjs", "extract", "src/**/*.tsx", "--out-file", tmpobj.name]); + +if (extract.error || extract.status !== 0) { + console.log("\x1b[31m%s\x1b[0m", extract.error ?? extract.stderr.toString().trim()); + process.exit(1); +} const allLocalesInProject = require(tmpobj.name); @@ -69,99 +53,56 @@ const langList = require("./src/locale/src/lang-list.json"); // store a list of all validation errors const allErrors = []; const allWarnings = []; -const allKeys = []; +const allKeys = new Set(); const checkLangList = (fullCode) => { - const key = "locale-" + fullCode; - if (typeof langList[key] === "undefined") { - allErrors.push("ERROR: `" + key + "` language does not exist in lang-list.json"); - } + const key = `locale-${fullCode}`; + if (langList[key] === undefined) allErrors.push(`ERROR: \`${key}\` language does not exist in lang-list.json`); }; const compareLocale = (locale) => { - const projectLocaleKeys = Object.keys(allLocalesInProject); - // Check that locale contains the items used in the codebase - projectLocaleKeys.map((key) => { - if (typeof locale.data[key] === "undefined") { - allErrors.push("ERROR: `" + locale[0] + "` does not contain item: `" + key + "`"); - } - return null; - }); - // Check that locale contains all error.* items - BACKEND_ERRORS.forEach((key) => { - if (typeof locale.data[key] === "undefined") { - allErrors.push("ERROR: `" + locale[0] + "` does not contain item: `" + key + "`"); - } - return null; - }); - - // Check that locale does not contain items not used in the codebase - const localeKeys = Object.keys(locale.data); - localeKeys.map((key) => { - let ignored = false; - ignoreUnused.map((regex) => { - if (key.match(regex)) { - ignored = true; - } - return null; - }); - - if (!ignored && typeof allLocalesInProject[key] === "undefined") { - // ensure this key doesn't exist in the backend errors either - if (!BACKEND_ERRORS.includes(key)) { - allErrors.push("ERROR: `" + locale[0] + "` contains unused item: `" + key + "`"); - } - } - - // Add this key to allKeys - if (allKeys.indexOf(key) === -1) { - allKeys.push(key); - } - return null; - }); + // Check that locale contains the items used in the codebase + Object.keys(allLocalesInProject).forEach((key) => { + if (locale.data[key] === undefined) allErrors.push(`ERROR: \`${locale[0]}\` does not contain item: \`${key}\``); + }); + // Check that locale does not contain items not used in the codebase + Object.keys(locale.data).forEach((key) => { + if (!ignoreUnused.some((regex) => regex.test(key)) && allLocalesInProject[key] === undefined) { + allErrors.push(`ERROR: \`${locale[0]}\` contains unused item: \`${key}\``); + } + + // Add this key to allKeys + allKeys.add(key); + }); }; // Checks for any keys missing from this locale, that // have been defined in any other locales const checkForMissing = (locale) => { - allKeys.forEach((key) => { - if (typeof locale.data[key] === "undefined") { - allWarnings.push("WARN: `" + locale[0] + "` does not contain item: `" + key + "`"); - } - return null; - }); + allKeys.forEach((key) => { + if (locale.data[key] === undefined) + allWarnings.push(`WARN: \`${locale[0]}\` does not contain item: \`${key}\``); + }); }; // Local all locale data -allLocales.map((locale, idx) => { - checkLangList(locale[1]); - allLocales[idx].data = require("./src/locale/src/" + locale[0] + ".json"); - return null; +allLocales.forEach((locale) => { + checkLangList(locale[1]); + locale.data = require(`./src/locale/src/${locale[0]}.json`); }); // Verify all locale data -allLocales.map((locale) => { - compareLocale(locale); - checkForMissing(locale); - return null; -}); +allLocales.forEach(compareLocale); +allLocales.forEach(checkForMissing); -if (allErrors.length) { - allErrors.map((err) => { - console.log("\x1b[31m%s\x1b[0m", err); - return null; - }); -} -if (allWarnings.length) { - allWarnings.map((err) => { - console.log("\x1b[33m%s\x1b[0m", err); - return null; - }); -} +allErrors.forEach((err) => { + console.log("\x1b[31m%s\x1b[0m", err); +}); +allWarnings.forEach((warn) => { + console.log("\x1b[33m%s\x1b[0m", warn); +}); -if (allErrors.length) { - process.exit(1); -} +if (allErrors.length) process.exit(1); console.log("\x1b[32m%s\x1b[0m", "Locale check passed"); process.exit(0); diff --git a/frontend/index.html b/frontend/index.html index 25c90c6b61..9413c2c5a5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -36,10 +36,5 @@
- diff --git a/frontend/package.json b/frontend/package.json index dead5b4e23..c6af52059f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,8 +8,8 @@ "dependencies": { "@ebay/nice-modal-react": "1.2.13", "@tabler/core": "1.4.0", - "@tabler/icons-react": "3.45.0", - "@tanstack/react-query": "5.101.3", + "@tabler/icons-react": "3.46.0", + "@tanstack/react-query": "5.101.4", "@tanstack/react-table": "8.21.3", "clsx": "2.1.1", "country-flag-icons": "1.6.20", @@ -18,34 +18,27 @@ "markdown-to-jsx": "9.9.0", "qrcode.react": "4.2.0", "query-string": "9.4.1", - "react": "19.2.7", + "react": "19.2.8", "react-bootstrap": "2.10.10", - "react-dom": "19.2.7", - "react-intl": "10.1.18", - "react-router-dom": "7.18.1", + "react-dom": "19.2.8", + "react-intl": "10.1.19", + "react-router": "8.3.0", "react-select": "5.10.2", "react-toastify": "11.1.0", "rooks": "9.8.0" }, "devDependencies": { - "@biomejs/biome": "2.5.4", - "@formatjs/cli": "6.16.14", - "@tanstack/react-query-devtools": "5.101.3", - "@testing-library/dom": "10.4.1", - "@testing-library/jest-dom": "7.0.0", - "@testing-library/react": "16.3.2", - "@types/country-flag-icons": "1.2.2", + "@biomejs/biome": "2.5.6", + "@formatjs/cli": "6.16.15", + "@tanstack/react-query-devtools": "5.101.4", "@types/node": "25.9.5", - "@types/react": "19.2.17", - "@types/react-dom": "19.2.3", - "@types/react-table": "7.7.20", - "@vitejs/plugin-react": "6.0.3", - "happy-dom": "20.11.0", - "postcss": "8.5.20", - "sass": "1.101.0", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.4", + "@vitejs/plugin-react": "6.0.5", + "happy-dom": "20.11.1", "tmp": "0.2.7", "typescript": "7.0.2", - "vite": "8.1.5", + "vite": "8.2.0", "vitest": "4.1.10" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b90df1a7e8..e0d34dd04e 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -10,19 +10,19 @@ importers: dependencies: '@ebay/nice-modal-react': specifier: 1.2.13 - version: 1.2.13(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 1.2.13(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@tabler/core': specifier: 1.4.0 version: 1.4.0 '@tabler/icons-react': - specifier: 3.45.0 - version: 3.45.0(react@19.2.7) + specifier: 3.46.0 + version: 3.46.0(react@19.2.8) '@tanstack/react-query': - specifier: 5.101.3 - version: 5.101.3(react@19.2.7) + specifier: 5.101.4 + version: 5.101.4(react@19.2.8) '@tanstack/react-table': specifier: 8.21.3 - version: 8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) clsx: specifier: 2.1.1 version: 2.1.1 @@ -34,86 +34,65 @@ importers: version: 4.4.0 formik: specifier: 2.4.9 - version: 2.4.9(@types/react@19.2.17)(react@19.2.7) + version: 2.4.9(@types/react@19.2.18)(react@19.2.8) markdown-to-jsx: specifier: 9.9.0 - version: 9.9.0(react@19.2.7) + version: 9.9.0(react@19.2.8) qrcode.react: specifier: 4.2.0 - version: 4.2.0(react@19.2.7) + version: 4.2.0(react@19.2.8) query-string: specifier: 9.4.1 version: 9.4.1 react: - specifier: 19.2.7 - version: 19.2.7 + specifier: 19.2.8 + version: 19.2.8 react-bootstrap: specifier: 2.10.10 - version: 2.10.10(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 2.10.10(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react-dom: - specifier: 19.2.7 - version: 19.2.7(react@19.2.7) + specifier: 19.2.8 + version: 19.2.8(react@19.2.8) react-intl: - specifier: 10.1.18 - version: 10.1.18(@types/react@19.2.17)(react@19.2.7) - react-router-dom: - specifier: 7.18.1 - version: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: 10.1.19 + version: 10.1.19(@types/react@19.2.18)(react@19.2.8) + react-router: + specifier: 8.3.0 + version: 8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react-select: specifier: 5.10.2 - version: 5.10.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 5.10.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react-toastify: specifier: 11.1.0 - version: 11.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 11.1.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) rooks: specifier: 9.8.0 - version: 9.8.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 9.8.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) devDependencies: '@biomejs/biome': - specifier: 2.5.4 - version: 2.5.4 + specifier: 2.5.6 + version: 2.5.6 '@formatjs/cli': - specifier: 6.16.14 - version: 6.16.14 + specifier: 6.16.15 + version: 6.16.15 '@tanstack/react-query-devtools': - specifier: 5.101.3 - version: 5.101.3(@tanstack/react-query@5.101.3(react@19.2.7))(react@19.2.7) - '@testing-library/dom': - specifier: 10.4.1 - version: 10.4.1 - '@testing-library/jest-dom': - specifier: 7.0.0 - version: 7.0.0(@testing-library/dom@10.4.1) - '@testing-library/react': - specifier: 16.3.2 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@types/country-flag-icons': - specifier: 1.2.2 - version: 1.2.2 + specifier: 5.101.4 + version: 5.101.4(@tanstack/react-query@5.101.4(react@19.2.8))(react@19.2.8) '@types/node': specifier: 25.9.5 version: 25.9.5 '@types/react': - specifier: 19.2.17 - version: 19.2.17 + specifier: 19.2.18 + version: 19.2.18 '@types/react-dom': - specifier: 19.2.3 - version: 19.2.3(@types/react@19.2.17) - '@types/react-table': - specifier: 7.7.20 - version: 7.7.20 + specifier: 19.2.4 + version: 19.2.4(@types/react@19.2.18) '@vitejs/plugin-react': - specifier: 6.0.3 - version: 6.0.3(vite@8.1.5(@types/node@25.9.5)(sass@1.101.0)) + specifier: 6.0.5 + version: 6.0.5(vite@8.2.0(@types/node@25.9.5)) happy-dom: - specifier: 20.11.0 - version: 20.11.0 - postcss: - specifier: 8.5.20 - version: 8.5.20 - sass: - specifier: 1.101.0 - version: 1.101.0 + specifier: 20.11.1 + version: 20.11.1 tmp: specifier: 0.2.7 version: 0.2.7 @@ -121,17 +100,14 @@ importers: specifier: 7.0.2 version: 7.0.2 vite: - specifier: 8.1.5 - version: 8.1.5(@types/node@25.9.5)(sass@1.101.0) + specifier: 8.2.0 + version: 8.2.0(@types/node@25.9.5) vitest: specifier: 4.1.10 - version: 4.1.10(@types/node@25.9.5)(happy-dom@20.11.0)(vite@8.1.5(@types/node@25.9.5)(sass@1.101.0)) + version: 4.1.10(@types/node@25.9.5)(happy-dom@20.11.1)(vite@8.2.0(@types/node@25.9.5)) packages: - '@adobe/css-tools@4.5.0': - resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} - '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -177,59 +153,59 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.5.4': - resolution: {integrity: sha512-xy5FNE5kQJKyK5MR1gJy6ztXYx4WBAbYGlK04lMEgmyPRWKybY9NFwiG9yo0XdzOU8Xvhj41u034J1ywfoWfMw==} + '@biomejs/biome@2.5.6': + resolution: {integrity: sha512-lxVNjv7UF6KfhMJfL9gaUHbWdJdHbsAj6OSmwSYNdhRuG67NxNQ4Xdvh3TUxsSK9sBzJBQhEJj3AopmmNJ5pSA==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.5.4': - resolution: {integrity: sha512-4o3NFRobXHynkgcFVrlZsoDAFtF2ldlEGN8sORSws5ZQqyY4PXnPUIylu4ksfyHuwkfvDREuWh3JK+niRwGq3w==} + '@biomejs/cli-darwin-arm64@2.5.6': + resolution: {integrity: sha512-zMOLZP4oMrjh6m1zcSj1ud2awUPgTuMVbmQhYYWL7J8HwCnbHHBvTm7VBTRuY7epT5bez76IpKYQ11ZAqHFlnw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.5.4': - resolution: {integrity: sha512-D32P5HkU2Y6PySuC/WsVDTOgsDwVFmujzhhhOQjajtATpVWFDXuVd3oRbsWNSEA+aaFzyzZm22szsyydBYlSyQ==} + '@biomejs/cli-darwin-x64@2.5.6': + resolution: {integrity: sha512-JAC1VqzvO7Th5ZplU0G2uGfkZbxEe9uDDektPAhF0JLusoz1w+T4okp2bkykI0bbaO2vslKiRfj4gU43JaGreA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.5.4': - resolution: {integrity: sha512-Rpm5/AT1m+DlJmUoYvS4/vXc+0tXJPJ2NQz25TGPyHVF5JrWy75PE0GH6kVxsKtQDuCH4OgzquZq0R4kj/wCVg==} + '@biomejs/cli-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-eUa3jeeYvfMt19LBeh6E5PUZpxnTC4JqNWo+EDjTtQjAr2xLGnWaxACtVU1DQqmHYbvThlJzLX+ZsYgrqh2qVw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.5.4': - resolution: {integrity: sha512-pSEfW7B8kTsXUjUxC1xVVK+y85Ht3C5XxZ9gclmC7/3Ku9Vqz8jmI7k0p/BNIjQ6t4sFERI2sFeH73ybiZl6YQ==} + '@biomejs/cli-linux-arm64@2.5.6': + resolution: {integrity: sha512-6XsYwCFkp5sMxl85ffhgeGpGgs6A7dRYFnkceZ7WVxvycuTnGdD5xa534Z3xfrBQ0JCMK/mujT6ZNPJoghedwg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.5.4': - resolution: {integrity: sha512-aby/PohmmgbShcHqFsZVzG8H6D98+P+A6xRWRrQcLW1pCjabcov5UUlke4UqNQBYTkDQav+jB4zyyDDeKB2GaA==} + '@biomejs/cli-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-2Vp13QdKysH3HIWLaYLhUUwbK+jbZonJD1K+Lr0d0RO4wH7mkYd43vJixEDm8cUWrowoRz4UUHF1nm9Ae7ym8A==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.5.4': - resolution: {integrity: sha512-FNxojWJkL7EajAuzBgoLe0T2G0y112M4lBrDIFl/DomFTx8yqenYOIdsRLNXvOvBBofE8hJi85LjzLmBDpY7/Q==} + '@biomejs/cli-linux-x64@2.5.6': + resolution: {integrity: sha512-Pop9VXCFUhFTMfFefZ39S+u2rOPyNp5iHlxbZRwXGACHLy2r0jjiRgJHmaEKJzL3SyxlVeGShXhvvElvWowonA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.5.4': - resolution: {integrity: sha512-emoXexPZIPAZkz2RKmA95WJUqK3I5MJNYtwEbL5ESciRzhmFMMyekDhNG8hpeOaK+ZGRDxAU4wvGuA5IHQ0h0w==} + '@biomejs/cli-win32-arm64@2.5.6': + resolution: {integrity: sha512-tDGshcm6BdkZOCGnTDX0Y8/U4IfBSlnUU7T56nNDuPEfed+aHg+u8G36NB43fJVl0Os6+QURXIE1yuD7AaEofA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.5.4': - resolution: {integrity: sha512-U1jaluLw1qQc2Tx7/CeSoL9N5XcqIH+GWjpUAy1ouB5nVjSCMNO+NNHdY3RAs8zxNurLWAdj6pehQdCA2zyU+Q==} + '@biomejs/cli-win32-x64@2.5.6': + resolution: {integrity: sha512-WN05KwXnTO/2J45RQPvzZMXf7tZUIofHoR35xIPfCo7pQ2RFidxI8sfb5mGsaTxdMmEOzHzOPRCdA5/fCpc7xQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -240,14 +216,14 @@ packages: react: '>16.8.0' react-dom: '>16.8.0' - '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/core@2.0.0-alpha.3': + resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} - '@emnapi/runtime@1.11.1': - resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@2.0.0-alpha.3': + resolution: {integrity: sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==} - '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@emnapi/wasi-threads@2.0.1': + resolution: {integrity: sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==} '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -299,42 +275,42 @@ packages: '@floating-ui/utils@0.2.12': resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} - '@formatjs/cli-native-darwin-arm64@1.1.8': - resolution: {integrity: sha512-uIHA6JFbG5vE3GlQodIrbYMmQc50mhBBVJ++ze99Tetfj4eFmIOtVKz2UMt8P8QmvCUZQMyEH95Jm+FbBDY46A==} + '@formatjs/cli-native-darwin-arm64@1.1.9': + resolution: {integrity: sha512-vvCefAgtnwzwIGZO33ZNy6vED6iWE2+cVLjmbvhwV7kxXvFcFZtThn9F0BduzUPUt3yJSaN/wQ+CqQeAzP6qQw==} cpu: [arm64] os: [darwin] - '@formatjs/cli-native-linux-arm64-musl@1.0.6': - resolution: {integrity: sha512-ZiM2H4w61rrvkAwhDFWjFEhW43/AphNNHa3lTdZvNOttst1mQ2gUS4+bLGBOfMqKG8+AU+6B9PCF67AbDOlwPg==} + '@formatjs/cli-native-linux-arm64-musl@1.0.7': + resolution: {integrity: sha512-XjCGSu7nrB29HEOQeMXb5IWgvjMGeqVOavm3i4qR3z/YHt7XZ1MMJalrob5dqWEgTnqyOP4Z1nDPpfYXhlkBcw==} cpu: [arm64] os: [linux] libc: [musl] - '@formatjs/cli-native-linux-arm64@1.2.8': - resolution: {integrity: sha512-EfrurpWY66hjWvtlO9BjWMDze0SBw4dMMZkUnAZYNdBmOTTRJC+CLi62nRxiSoSVV7QzP8BN6W/zZ5qLySYl0w==} + '@formatjs/cli-native-linux-arm64@1.2.9': + resolution: {integrity: sha512-tGA6sm2yMSurUzwzo/W0mQX0eSLPgaH28nu9UmIEpvxJg4Rd6OO7LxYbrOT6Q+JoosvPVPLqTCFY72uSKZmLPQ==} cpu: [arm64] os: [linux] libc: [glibc] - '@formatjs/cli-native-linux-x64-musl@1.0.6': - resolution: {integrity: sha512-Va4rt4gL5p02gixW73VFLCvhgvXhsaT9dMdGfQE32/dqn4NYxg/8gquhauIBvTCkKPYgOhtXRcMWKQINxMB8oA==} + '@formatjs/cli-native-linux-x64-musl@1.0.7': + resolution: {integrity: sha512-p1TRiXL+Pq03pBarYwjGzvnLNY2W3Hz7/7hVEnvBeCU08szwE2KrELBQPS3Y1YJN7D05d5rT661057t2uui+eg==} cpu: [x64] os: [linux] libc: [musl] - '@formatjs/cli-native-linux-x64@1.1.8': - resolution: {integrity: sha512-J3aInCY7VKSyBrjpXedz/AYNbr+X82zLpSwfeO3obnOqUxn6dko9tnHlaQxuSnZHMx4sAOjJyTzUqDHD6Ft2GA==} + '@formatjs/cli-native-linux-x64@1.1.9': + resolution: {integrity: sha512-IdVE05YGhaPzvo/7/v16aSSnAd3NtIZa8Mo/LzvKpBRijFtMPiw9Zh/APpHz5wUO+UK/9s+Rrui7b+ou8tccSA==} cpu: [x64] os: [linux] libc: [glibc] - '@formatjs/cli-native-win32-x64@1.1.9': - resolution: {integrity: sha512-EUH4z1WhKmjJ4FIn+dvzmgbtJRZ4YHCP3kbHiZv0tgmrQiQ5Q4Flv0Smt66kwYMEnyEf9/mY1JmKdBIjPaQM6A==} + '@formatjs/cli-native-win32-x64@1.1.10': + resolution: {integrity: sha512-yhuT+0Wh941yToJRMqY72v2xyZ782FvWyCny1FAi0TQH+jQgsyT3nJIT1cYDxBLxg7XKZIUBcOnheShptBH9sw==} cpu: [x64] os: [win32] - '@formatjs/cli@6.16.14': - resolution: {integrity: sha512-aortaTMdCWuGtHnksxpznioudRVvSVkY2/6AQrpUR3tpATGl8sWCuhU9XUEG4ba7UWbFGMv7XLME93rCPfIrJw==} + '@formatjs/cli@6.16.15': + resolution: {integrity: sha512-JDsQQc2eonUmnd/zkz6rzALsTlVxZjGdjauvhrHXsoxgZqmEglADCKMaL2+NisjxMyTG/hFF6Ocut7YGszmW4A==} engines: {node: '>= 20.12.0'} hasBin: true peerDependencies: @@ -364,14 +340,14 @@ packages: '@formatjs/fast-memoize@3.1.7': resolution: {integrity: sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==} - '@formatjs/icu-messageformat-parser@3.5.15': - resolution: {integrity: sha512-5o4grXKotAB3JqQuisLApHG43g17N+paoRTa92Jiz35Zvfemq0cVf4EDvuxyHAzmsJji7igaEowicLO/VmfJ8Q==} + '@formatjs/icu-messageformat-parser@3.5.16': + resolution: {integrity: sha512-kl6b/4D56gjGZi4ZewSmvXbalHwjOUI5ogEHPZqw42goeXTTrL7/yuPzvdrvr0QigDtvaOeb+UeMf62jks43Yg==} '@formatjs/icu-skeleton-parser@2.1.11': resolution: {integrity: sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==} - '@formatjs/intl@4.1.17': - resolution: {integrity: sha512-IxluRkzOZH8ahaDip5g8YH9kJi1munUZi9i1BJs1wVFQIY2pROoCSiOlybZegyiPsZlMaYfADkD99zP2xmBfjQ==} + '@formatjs/intl@4.1.18': + resolution: {integrity: sha512-d+AFsn+luyMAUTQImPJEK76bhEvzFKRG/1PCf1Q6sCH3ojHyHgJJkfvUFElpbDkihtm4uedK7/zoG24EL285nw==} '@internationalized/date@3.12.2': resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==} @@ -399,102 +375,15 @@ packages: resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==} engines: {node: '>=12'} - '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + '@napi-rs/wasm-runtime@1.2.1': + resolution: {integrity: sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@oxc-project/types@0.139.0': - resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} - - '@parcel/watcher-android-arm64@2.5.6': - resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [android] - - '@parcel/watcher-darwin-arm64@2.5.6': - resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [darwin] - - '@parcel/watcher-darwin-x64@2.5.6': - resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [darwin] - - '@parcel/watcher-freebsd-x64@2.5.6': - resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [freebsd] - - '@parcel/watcher-linux-arm-glibc@2.5.6': - resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-arm-musl@2.5.6': - resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - libc: [musl] - - '@parcel/watcher-linux-arm64-glibc@2.5.6': - resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-arm64-musl@2.5.6': - resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@parcel/watcher-linux-x64-glibc@2.5.6': - resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-x64-musl@2.5.6': - resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@parcel/watcher-win32-arm64@2.5.6': - resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [win32] + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 - '@parcel/watcher-win32-ia32@2.5.6': - resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.5.6': - resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [win32] - - '@parcel/watcher@2.5.6': - resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} - engines: {node: '>= 10.0.0'} + '@oxc-project/types@0.142.0': + resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -527,97 +416,96 @@ packages: react: '>=16.14.0' react-dom: '>=16.14.0' - '@rolldown/binding-android-arm64@1.1.5': - resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + '@rolldown/binding-android-arm64@1.2.1': + resolution: {integrity: sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.1.5': - resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + '@rolldown/binding-darwin-arm64@1.2.1': + resolution: {integrity: sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.1.5': - resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + '@rolldown/binding-darwin-x64@1.2.1': + resolution: {integrity: sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.1.5': - resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + '@rolldown/binding-freebsd-x64@1.2.1': + resolution: {integrity: sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.1': + resolution: {integrity: sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.1.5': - resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + '@rolldown/binding-linux-arm64-gnu@1.2.1': + resolution: {integrity: sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.1.5': - resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + '@rolldown/binding-linux-arm64-musl@1.2.1': + resolution: {integrity: sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + '@rolldown/binding-linux-ppc64-gnu@1.2.1': + resolution: {integrity: sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.5': - resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + '@rolldown/binding-linux-s390x-gnu@1.2.1': + resolution: {integrity: sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.5': - resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + '@rolldown/binding-linux-x64-gnu@1.2.1': + resolution: {integrity: sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.1.5': - resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + '@rolldown/binding-linux-x64-musl@1.2.1': + resolution: {integrity: sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.1.5': - resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + '@rolldown/binding-openharmony-arm64@1.2.1': + resolution: {integrity: sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.1.5': - resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] + '@rolldown/binding-wasm32-wasi@1.2.1': + resolution: {integrity: sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - '@rolldown/binding-win32-arm64-msvc@1.1.5': - resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + '@rolldown/binding-win32-arm64-msvc@1.2.1': + resolution: {integrity: sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.5': - resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + '@rolldown/binding-win32-x64-msvc@1.2.1': + resolution: {integrity: sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -635,28 +523,28 @@ packages: resolution: {integrity: sha512-5BigzOlbOH9N0Is4u0rYNRCiwtnUXWO57K9zwuscygcicAa8UV9MGaS4zTgQsZEtZ9tsNANhN/YD8gCBGKYCiw==} engines: {node: '>=20'} - '@tabler/icons-react@3.45.0': - resolution: {integrity: sha512-t/AuKs7ALMDDTY+B9IvfZnlO0mbLlP/lJxP/HnGC49QkM8mCsTN38kYyxjXxgrb1+TeK53ioVBJqIV7n/eysXQ==} + '@tabler/icons-react@3.46.0': + resolution: {integrity: sha512-CCm7xJWhDT2PH4ZIFkP6AgYKtVhq0gpYkjUN+GVh1AzmIQaa77OW0bQPBPQiTE0PsXMR9oSxFqA3qBglzPyrVQ==} peerDependencies: react: '>= 16' - '@tabler/icons@3.45.0': - resolution: {integrity: sha512-jiATwV8+zGYLTZ7gMLGivCic+KtsMZXcDmufIG8umlLxoHhI6902hGYIEt0Oa9Y9SXblNzUlrisHm5jOFMxOQA==} + '@tabler/icons@3.46.0': + resolution: {integrity: sha512-f2RYFl3fzPwj5WO82x6en0dmkjefxEfOm16D1ByM6cj/McNiwOkL4VaPUoP9VVIrXAD9WnTSVFr70px703b//A==} - '@tanstack/query-core@5.101.3': - resolution: {integrity: sha512-pWLyu7AjFFVM5G+frjcL81kKEW9R8GCCPKnL3uaWbMwd2f3ZINFCuy+PtKkdz/+pKw/f/XMsFsYq/H+uJHM4GQ==} + '@tanstack/query-core@5.101.4': + resolution: {integrity: sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==} - '@tanstack/query-devtools@5.101.3': - resolution: {integrity: sha512-twEAOB5uBmpFAdlIuy3KIUujwmoZIhCXZpu1PFMTress2XvK/CyzsvV70WrhtTcocszKpbvwMR0Bxq4dqkvH0g==} + '@tanstack/query-devtools@5.101.4': + resolution: {integrity: sha512-z5IPHnDX3aUWeTWlRKLyooBQekaCAw4xRpZqPQ390RiWTDBcTynjpPT221BArw0u2+pnQMdGvPQI9YNNubBcmA==} - '@tanstack/react-query-devtools@5.101.3': - resolution: {integrity: sha512-Xr4gCymObeVmtImtsPDVzOlvy3Itr2ti3IrtdbcyhsmpsM54v2A7VR9hTB40C6wzCf3yM4Q2LqkFbNm1u8wd3A==} + '@tanstack/react-query-devtools@5.101.4': + resolution: {integrity: sha512-VeK2gtmfj7kvRBjtxS7TKxt/6qKhn8VzabY4UiYMr7NV9CddjSRYRgeYyld+NpjAkgMV9dd+2Qdr8ah5I03NeA==} peerDependencies: - '@tanstack/react-query': ^5.101.3 + '@tanstack/react-query': ^5.101.4 react: ^18 || ^19 - '@tanstack/react-query@5.101.3': - resolution: {integrity: sha512-ZjgcRwmQUSCythDPj6pE2NKFi9bpQegxIBxgt3hPytXI1ElFHNaa3A7aWW8vFaLCIRjPv467rbREYHbxcdgQbg==} + '@tanstack/react-query@5.101.4': + resolution: {integrity: sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==} peerDependencies: react: ^18 || ^19 @@ -671,43 +559,12 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@testing-library/dom@10.4.1': - resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} - engines: {node: '>=18'} - - '@testing-library/jest-dom@7.0.0': - resolution: {integrity: sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==} - engines: {node: '>=22', npm: '>=6', yarn: '>=1'} - peerDependencies: - '@testing-library/dom': '>=10 <11' - - '@testing-library/react@16.3.2': - resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} - engines: {node: '>=18'} - peerDependencies: - '@testing-library/dom': ^10.0.0 - '@types/react': ^18.0.0 || ^19.0.0 - '@types/react-dom': ^18.0.0 || ^19.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - '@types/aria-query@5.0.4': - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/country-flag-icons@1.2.2': - resolution: {integrity: sha512-CefEn/J336TBDp7NX8JqzlDtCBOsm8M3r1Li0gEOt0HOMHF1XemNyrx9lSHjsafcb1yYWybU0N8ZAXuyCaND0w==} - '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -728,21 +585,18 @@ packages: '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + '@types/react-dom@19.2.4': + resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} peerDependencies: '@types/react': ^19.2.0 - '@types/react-table@7.7.20': - resolution: {integrity: sha512-ahMp4pmjVlnExxNwxyaDrFgmKxSbPwU23sGQw2gJK4EhCvnvmib2s/O/+y1dfV57dXOwpr2plfyBol+vEHbi2w==} - '@types/react-transition-group@4.4.12': resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} peerDependencies: '@types/react': '*' - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} '@types/warning@3.0.4': resolution: {integrity: sha512-CqN8MnISMwQbLJXO3doBAV4Yw9hx9/Pyr2rZ78+NfaCnhyRA/nKrpyk6E7mKw17ZOaQdLpK9GiUjrqLzBlN3sg==} @@ -873,8 +727,8 @@ packages: cpu: [x64] os: [win32] - '@vitejs/plugin-react@6.0.3': - resolution: {integrity: sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==} + '@vitejs/plugin-react@6.0.5': + resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 @@ -915,25 +769,10 @@ packages: '@vitest/utils@4.1.10': resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - aria-hidden@1.2.6: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - - aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -959,10 +798,6 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - chokidar@5.0.0: - resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} - engines: {node: '>= 20.19.0'} - classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} @@ -976,9 +811,8 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie@1.1.1: - resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} - engines: {node: '>=18'} + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} @@ -987,9 +821,6 @@ packages: country-flag-icons@1.6.20: resolution: {integrity: sha512-py8JiEKzjhYw6HPJ0L7SxLgCYim36UPRTZX43/kqGueUCZLSvnrqAiwW8HtQibur7mdkFQUkjOgdK+o/9FBtaw==} - css.escape@1.5.1: - resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1021,12 +852,6 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - dom-accessibility-api@0.5.16: - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - - dom-accessibility-api@0.6.3: - resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -1087,8 +912,8 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - happy-dom@20.11.0: - resolution: {integrity: sha512-XogN4asPd1a56di9prVG6bZxteNcXsZxxKmAvcEfc5Px5Ca2hMyMgk8wvqK2K1V8zXg40j9VANXsDaJYh9DeNA==} + happy-dom@20.11.1: + resolution: {integrity: sha512-XSt8tMzbW9ymE7687xztkO1ckR7qJNQ3LywY9vlYGhGi3zXrGBHuUo2Cl1ztZaICW+1eAGdkLbj6iwVqDT33kg==} engines: {node: '>=20.0.0'} hasown@2.0.4: @@ -1098,19 +923,12 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - immutable@5.1.9: - resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} - import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - - intl-messageformat@11.2.12: - resolution: {integrity: sha512-KW70Xxfcvy7vV3qODfvShWkFDPMqKDAa4N+hSyVBWGNtVhTUFYaqlD/l88DaYPKiVcPP4rPQ3qnH7i5K82Mg7g==} + intl-messageformat@11.2.13: + resolution: {integrity: sha512-JaPaE6TIX+TAS5XLhDUh41geLw4QfBHX4s5pW8Km+L9fVC8HzB9yOuhbh4EMR/F1+8C6b9qk4763Cv+LdOG1kg==} invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -1122,14 +940,6 @@ packages: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1144,78 +954,78 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] libc: [glibc] - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] libc: [musl] - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] libc: [glibc] - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] libc: [musl] - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} lines-and-columns@1.2.4: @@ -1234,10 +1044,6 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1262,10 +1068,6 @@ packages: memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} - min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1274,9 +1076,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1313,14 +1112,10 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} - postcss@8.5.20: - resolution: {integrity: sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==} + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} - pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - prop-types-extra@1.1.1: resolution: {integrity: sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==} peerDependencies: @@ -1357,16 +1152,16 @@ packages: '@types/react': optional: true - react-dom@19.2.7: - resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} peerDependencies: - react: ^19.2.7 + react: ^19.2.8 react-fast-compare@2.0.4: resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==} - react-intl@10.1.18: - resolution: {integrity: sha512-4JMOYakqVn/IajJt8Ad341Iwd6RDtiP1WQZiHRcFpHDBhVW4E9T0vkypSOk69Adg3dEbIYOE4HKTehH/1sSwnw==} + react-intl@10.1.19: + resolution: {integrity: sha512-gND9+pkCXyE4/gBt01c37w3TkvhHOxChzQ1wYGilVIdONeZrjg1A5q8oUp2y9aU/MB1WZpBVlPa1TvrRrxE/eA==} peerDependencies: '@types/react': '>=18.0.0' react: '>=18.0.0' @@ -1374,25 +1169,15 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - react-lifecycles-compat@3.0.4: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} - react-router-dom@7.18.1: - resolution: {integrity: sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: '>=18' - react-dom: '>=18' - - react-router@7.18.1: - resolution: {integrity: sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==} - engines: {node: '>=20.0.0'} + react-router@8.3.0: + resolution: {integrity: sha512-qyPMvW83jGIct3yiieisxdk9M745anqhpIMKN5m1t6yBMfgVPpt77aHOqs5fUlEJRMCGffg9BaQLH9oPVOL7xQ==} + engines: {node: '>=22.22.0'} peerDependencies: - react: '>=18' - react-dom: '>=18' + react: '>=19.2.7' + react-dom: '>=19.2.7' peerDependenciesMeta: react-dom: optional: true @@ -1420,18 +1205,10 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' - react@19.2.7: - resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} engines: {node: '>=0.10.0'} - readdirp@5.0.0: - resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} - engines: {node: '>= 20.19.0'} - - redent@3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1441,8 +1218,8 @@ packages: engines: {node: '>= 0.4'} hasBin: true - rolldown@1.1.5: - resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + rolldown@1.2.1: + resolution: {integrity: sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -1453,17 +1230,9 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - sass@1.101.0: - resolution: {integrity: sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==} - engines: {node: '>=20.19.0'} - hasBin: true - scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - set-cookie-parser@2.7.2: - resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} - siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1485,10 +1254,6 @@ packages: std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} - strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} - stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} @@ -1553,13 +1318,13 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - vite@8.1.5: - resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + vite@8.2.0: + resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 + '@vitejs/devtools': ^0.4.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -1667,8 +1432,6 @@ packages: snapshots: - '@adobe/css-tools@4.5.0': {} - '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -1725,58 +1488,58 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@biomejs/biome@2.5.4': + '@biomejs/biome@2.5.6': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.5.4 - '@biomejs/cli-darwin-x64': 2.5.4 - '@biomejs/cli-linux-arm64': 2.5.4 - '@biomejs/cli-linux-arm64-musl': 2.5.4 - '@biomejs/cli-linux-x64': 2.5.4 - '@biomejs/cli-linux-x64-musl': 2.5.4 - '@biomejs/cli-win32-arm64': 2.5.4 - '@biomejs/cli-win32-x64': 2.5.4 + '@biomejs/cli-darwin-arm64': 2.5.6 + '@biomejs/cli-darwin-x64': 2.5.6 + '@biomejs/cli-linux-arm64': 2.5.6 + '@biomejs/cli-linux-arm64-musl': 2.5.6 + '@biomejs/cli-linux-x64': 2.5.6 + '@biomejs/cli-linux-x64-musl': 2.5.6 + '@biomejs/cli-win32-arm64': 2.5.6 + '@biomejs/cli-win32-x64': 2.5.6 - '@biomejs/cli-darwin-arm64@2.5.4': + '@biomejs/cli-darwin-arm64@2.5.6': optional: true - '@biomejs/cli-darwin-x64@2.5.4': + '@biomejs/cli-darwin-x64@2.5.6': optional: true - '@biomejs/cli-linux-arm64-musl@2.5.4': + '@biomejs/cli-linux-arm64-musl@2.5.6': optional: true - '@biomejs/cli-linux-arm64@2.5.4': + '@biomejs/cli-linux-arm64@2.5.6': optional: true - '@biomejs/cli-linux-x64-musl@2.5.4': + '@biomejs/cli-linux-x64-musl@2.5.6': optional: true - '@biomejs/cli-linux-x64@2.5.4': + '@biomejs/cli-linux-x64@2.5.6': optional: true - '@biomejs/cli-win32-arm64@2.5.4': + '@biomejs/cli-win32-arm64@2.5.6': optional: true - '@biomejs/cli-win32-x64@2.5.4': + '@biomejs/cli-win32-x64@2.5.6': optional: true - '@ebay/nice-modal-react@1.2.13(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@ebay/nice-modal-react@1.2.13(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) - '@emnapi/core@1.11.1': + '@emnapi/core@2.0.0-alpha.3': dependencies: - '@emnapi/wasi-threads': 1.2.2 + '@emnapi/wasi-threads': 2.0.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.11.1': + '@emnapi/runtime@2.0.0-alpha.3': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.2': + '@emnapi/wasi-threads@2.0.1': dependencies: tslib: 2.8.1 optional: true @@ -1809,19 +1572,19 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7)': + '@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.7) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.8) '@emotion/utils': 1.4.2 '@emotion/weak-memoize': 0.4.0 hoist-non-react-statics: 3.3.2 - react: 19.2.7 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 transitivePeerDependencies: - supports-color @@ -1837,9 +1600,9 @@ snapshots: '@emotion/unitless@0.10.0': {} - '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.7)': + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 '@emotion/utils@1.4.2': {} @@ -1856,46 +1619,46 @@ snapshots: '@floating-ui/utils@0.2.12': {} - '@formatjs/cli-native-darwin-arm64@1.1.8': + '@formatjs/cli-native-darwin-arm64@1.1.9': optional: true - '@formatjs/cli-native-linux-arm64-musl@1.0.6': + '@formatjs/cli-native-linux-arm64-musl@1.0.7': optional: true - '@formatjs/cli-native-linux-arm64@1.2.8': + '@formatjs/cli-native-linux-arm64@1.2.9': optional: true - '@formatjs/cli-native-linux-x64-musl@1.0.6': + '@formatjs/cli-native-linux-x64-musl@1.0.7': optional: true - '@formatjs/cli-native-linux-x64@1.1.8': + '@formatjs/cli-native-linux-x64@1.1.9': optional: true - '@formatjs/cli-native-win32-x64@1.1.9': + '@formatjs/cli-native-win32-x64@1.1.10': optional: true - '@formatjs/cli@6.16.14': + '@formatjs/cli@6.16.15': optionalDependencies: - '@formatjs/cli-native-darwin-arm64': 1.1.8 - '@formatjs/cli-native-linux-arm64': 1.2.8 - '@formatjs/cli-native-linux-arm64-musl': 1.0.6 - '@formatjs/cli-native-linux-x64': 1.1.8 - '@formatjs/cli-native-linux-x64-musl': 1.0.6 - '@formatjs/cli-native-win32-x64': 1.1.9 + '@formatjs/cli-native-darwin-arm64': 1.1.9 + '@formatjs/cli-native-linux-arm64': 1.2.9 + '@formatjs/cli-native-linux-arm64-musl': 1.0.7 + '@formatjs/cli-native-linux-x64': 1.1.9 + '@formatjs/cli-native-linux-x64-musl': 1.0.7 + '@formatjs/cli-native-win32-x64': 1.1.10 '@formatjs/fast-memoize@3.1.7': {} - '@formatjs/icu-messageformat-parser@3.5.15': + '@formatjs/icu-messageformat-parser@3.5.16': dependencies: '@formatjs/icu-skeleton-parser': 2.1.11 '@formatjs/icu-skeleton-parser@2.1.11': {} - '@formatjs/intl@4.1.17': + '@formatjs/intl@4.1.18': dependencies: '@formatjs/fast-memoize': 3.1.7 - '@formatjs/icu-messageformat-parser': 3.5.15 - intl-messageformat: 11.2.12 + '@formatjs/icu-messageformat-parser': 3.5.16 + intl-messageformat: 11.2.13 '@internationalized/date@3.12.2': dependencies: @@ -1928,160 +1691,99 @@ snapshots: jsbi: 4.3.2 optional: true - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + '@napi-rs/wasm-runtime@1.2.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 + '@emnapi/core': 2.0.0-alpha.3 + '@emnapi/runtime': 2.0.0-alpha.3 '@tybys/wasm-util': 0.10.3 optional: true - '@oxc-project/types@0.139.0': {} - - '@parcel/watcher-android-arm64@2.5.6': - optional: true - - '@parcel/watcher-darwin-arm64@2.5.6': - optional: true - - '@parcel/watcher-darwin-x64@2.5.6': - optional: true - - '@parcel/watcher-freebsd-x64@2.5.6': - optional: true - - '@parcel/watcher-linux-arm-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-arm-musl@2.5.6': - optional: true - - '@parcel/watcher-linux-arm64-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-arm64-musl@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-musl@2.5.6': - optional: true - - '@parcel/watcher-win32-arm64@2.5.6': - optional: true - - '@parcel/watcher-win32-ia32@2.5.6': - optional: true - - '@parcel/watcher-win32-x64@2.5.6': - optional: true - - '@parcel/watcher@2.5.6': - dependencies: - detect-libc: 2.1.2 - is-glob: 4.0.3 - node-addon-api: 7.1.1 - picomatch: 4.0.5 - optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.6 - '@parcel/watcher-darwin-arm64': 2.5.6 - '@parcel/watcher-darwin-x64': 2.5.6 - '@parcel/watcher-freebsd-x64': 2.5.6 - '@parcel/watcher-linux-arm-glibc': 2.5.6 - '@parcel/watcher-linux-arm-musl': 2.5.6 - '@parcel/watcher-linux-arm64-glibc': 2.5.6 - '@parcel/watcher-linux-arm64-musl': 2.5.6 - '@parcel/watcher-linux-x64-glibc': 2.5.6 - '@parcel/watcher-linux-x64-musl': 2.5.6 - '@parcel/watcher-win32-arm64': 2.5.6 - '@parcel/watcher-win32-ia32': 2.5.6 - '@parcel/watcher-win32-x64': 2.5.6 - optional: true + '@oxc-project/types@0.142.0': {} '@popperjs/core@2.11.8': {} - '@react-aria/ssr@3.10.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@react-aria/ssr@3.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@swc/helpers': 0.5.23 - react: 19.2.7 - react-aria: 3.50.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-aria: 3.50.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-dom: 19.2.8(react@19.2.8) - '@react-types/shared@3.36.0(react@19.2.7)': + '@react-types/shared@3.36.0(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@restart/hooks@0.4.16(react@19.2.7)': + '@restart/hooks@0.4.16(react@19.2.8)': dependencies: dequal: 2.0.3 - react: 19.2.7 + react: 19.2.8 - '@restart/hooks@0.5.1(react@19.2.7)': + '@restart/hooks@0.5.1(react@19.2.8)': dependencies: dequal: 2.0.3 - react: 19.2.7 + react: 19.2.8 - '@restart/ui@1.9.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@restart/ui@1.9.4(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 '@popperjs/core': 2.11.8 - '@react-aria/ssr': 3.10.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@restart/hooks': 0.5.1(react@19.2.7) + '@react-aria/ssr': 3.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@restart/hooks': 0.5.1(react@19.2.8) '@types/warning': 3.0.4 dequal: 2.0.3 dom-helpers: 5.2.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - uncontrollable: 8.0.4(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + uncontrollable: 8.0.4(react@19.2.8) warning: 4.0.3 - '@rolldown/binding-android-arm64@1.1.5': + '@rolldown/binding-android-arm64@1.2.1': optional: true - '@rolldown/binding-darwin-arm64@1.1.5': + '@rolldown/binding-darwin-arm64@1.2.1': optional: true - '@rolldown/binding-darwin-x64@1.1.5': + '@rolldown/binding-darwin-x64@1.2.1': optional: true - '@rolldown/binding-freebsd-x64@1.1.5': + '@rolldown/binding-freebsd-x64@1.2.1': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + '@rolldown/binding-linux-arm-gnueabihf@1.2.1': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.5': + '@rolldown/binding-linux-arm64-gnu@1.2.1': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.5': + '@rolldown/binding-linux-arm64-musl@1.2.1': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.5': + '@rolldown/binding-linux-ppc64-gnu@1.2.1': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.5': + '@rolldown/binding-linux-s390x-gnu@1.2.1': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.5': + '@rolldown/binding-linux-x64-gnu@1.2.1': optional: true - '@rolldown/binding-linux-x64-musl@1.1.5': + '@rolldown/binding-linux-x64-musl@1.2.1': optional: true - '@rolldown/binding-openharmony-arm64@1.1.5': + '@rolldown/binding-openharmony-arm64@1.2.1': optional: true - '@rolldown/binding-wasm32-wasi@1.1.5': + '@rolldown/binding-wasm32-wasi@1.2.1': dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@emnapi/core': 2.0.0-alpha.3 + '@emnapi/runtime': 2.0.0-alpha.3 + '@napi-rs/wasm-runtime': 1.2.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.5': + '@rolldown/binding-win32-arm64-msvc@1.2.1': optional: true - '@rolldown/binding-win32-x64-msvc@1.1.5': + '@rolldown/binding-win32-x64-msvc@1.2.1': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -2097,88 +1799,53 @@ snapshots: '@popperjs/core': 2.11.8 bootstrap: 5.3.7(@popperjs/core@2.11.8) - '@tabler/icons-react@3.45.0(react@19.2.7)': + '@tabler/icons-react@3.46.0(react@19.2.8)': dependencies: - '@tabler/icons': 3.45.0 - react: 19.2.7 + '@tabler/icons': 3.46.0 + react: 19.2.8 - '@tabler/icons@3.45.0': {} + '@tabler/icons@3.46.0': {} - '@tanstack/query-core@5.101.3': {} + '@tanstack/query-core@5.101.4': {} - '@tanstack/query-devtools@5.101.3': {} + '@tanstack/query-devtools@5.101.4': {} - '@tanstack/react-query-devtools@5.101.3(@tanstack/react-query@5.101.3(react@19.2.7))(react@19.2.7)': + '@tanstack/react-query-devtools@5.101.4(@tanstack/react-query@5.101.4(react@19.2.8))(react@19.2.8)': dependencies: - '@tanstack/query-devtools': 5.101.3 - '@tanstack/react-query': 5.101.3(react@19.2.7) - react: 19.2.7 + '@tanstack/query-devtools': 5.101.4 + '@tanstack/react-query': 5.101.4(react@19.2.8) + react: 19.2.8 - '@tanstack/react-query@5.101.3(react@19.2.7)': + '@tanstack/react-query@5.101.4(react@19.2.8)': dependencies: - '@tanstack/query-core': 5.101.3 - react: 19.2.7 + '@tanstack/query-core': 5.101.4 + react: 19.2.8 - '@tanstack/react-table@8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@tanstack/react-table@8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@tanstack/table-core': 8.21.3 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) '@tanstack/table-core@8.21.3': {} - '@testing-library/dom@10.4.1': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/runtime': 7.29.7 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - picocolors: 1.1.1 - pretty-format: 27.5.1 - - '@testing-library/jest-dom@7.0.0(@testing-library/dom@10.4.1)': - dependencies: - '@adobe/css-tools': 4.5.0 - '@testing-library/dom': 10.4.1 - aria-query: 5.3.2 - css.escape: 1.5.1 - dom-accessibility-api: 0.6.3 - picocolors: 1.1.1 - redent: 3.0.0 - - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@babel/runtime': 7.29.7 - '@testing-library/dom': 10.4.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true - '@types/aria-query@5.0.4': {} - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/country-flag-icons@1.2.2': {} - '@types/deep-eql@4.0.2': {} '@types/estree@1.0.9': {} - '@types/hoist-non-react-statics@3.3.7(@types/react@19.2.17)': + '@types/hoist-non-react-statics@3.3.7(@types/react@19.2.18)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 hoist-non-react-statics: 3.3.2 '@types/node@25.9.5': @@ -2189,19 +1856,15 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/react-dom@19.2.3(@types/react@19.2.17)': - dependencies: - '@types/react': 19.2.17 - - '@types/react-table@7.7.20': + '@types/react-dom@19.2.4(@types/react@19.2.18)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@types/react-transition-group@4.4.12(@types/react@19.2.17)': + '@types/react-transition-group@4.4.12(@types/react@19.2.18)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@types/react@19.2.17': + '@types/react@19.2.18': dependencies: csstype: 3.2.3 @@ -2273,10 +1936,10 @@ snapshots: '@typescript/typescript-win32-x64@7.0.2': optional: true - '@vitejs/plugin-react@6.0.3(vite@8.1.5(@types/node@25.9.5)(sass@1.101.0))': + '@vitejs/plugin-react@6.0.5(vite@8.2.0(@types/node@25.9.5))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.1.5(@types/node@25.9.5)(sass@1.101.0) + vite: 8.2.0(@types/node@25.9.5) '@vitest/expect@4.1.10': dependencies: @@ -2287,13 +1950,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@25.9.5)(sass@1.101.0))': + '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@25.9.5))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.1.5(@types/node@25.9.5)(sass@1.101.0) + vite: 8.2.0(@types/node@25.9.5) '@vitest/pretty-format@4.1.10': dependencies: @@ -2319,20 +1982,10 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - ansi-regex@5.0.1: {} - - ansi-styles@5.2.0: {} - aria-hidden@1.2.6: dependencies: tslib: 2.8.1 - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - - aria-query@5.3.2: {} - assertion-error@2.0.1: {} babel-plugin-macros@3.1.0: @@ -2353,10 +2006,6 @@ snapshots: chai@6.2.2: {} - chokidar@5.0.0: - dependencies: - readdirp: 5.0.0 - classnames@2.5.1: {} clsx@2.1.1: {} @@ -2365,7 +2014,7 @@ snapshots: convert-source-map@2.0.0: {} - cookie@1.1.1: {} + cookie-es@3.1.1: {} cosmiconfig@7.1.0: dependencies: @@ -2377,8 +2026,6 @@ snapshots: country-flag-icons@1.6.20: {} - css.escape@1.5.1: {} - csstype@3.2.3: {} date-fns@4.4.0: {} @@ -2395,10 +2042,6 @@ snapshots: detect-libc@2.1.2: {} - dom-accessibility-api@0.5.16: {} - - dom-accessibility-api@0.6.3: {} - dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.29.7 @@ -2432,14 +2075,14 @@ snapshots: find-root@1.1.0: {} - formik@2.4.9(@types/react@19.2.17)(react@19.2.7): + formik@2.4.9(@types/react@19.2.18)(react@19.2.8): dependencies: - '@types/hoist-non-react-statics': 3.3.7(@types/react@19.2.17) + '@types/hoist-non-react-statics': 3.3.7(@types/react@19.2.18) deepmerge: 2.2.1 hoist-non-react-statics: 3.3.2 lodash: 4.18.1 lodash-es: 4.18.1 - react: 19.2.7 + react: 19.2.8 react-fast-compare: 2.0.4 tiny-warning: 1.0.3 tslib: 2.8.1 @@ -2451,7 +2094,7 @@ snapshots: function-bind@1.1.2: {} - happy-dom@20.11.0: + happy-dom@20.11.1: dependencies: '@types/node': 25.9.5 '@types/whatwg-mimetype': 3.0.2 @@ -2472,19 +2115,15 @@ snapshots: dependencies: react-is: 16.13.1 - immutable@5.1.9: {} - import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - indent-string@4.0.0: {} - - intl-messageformat@11.2.12: + intl-messageformat@11.2.13: dependencies: '@formatjs/fast-memoize': 3.1.7 - '@formatjs/icu-messageformat-parser': 3.5.15 + '@formatjs/icu-messageformat-parser': 3.5.16 invariant@2.2.4: dependencies: @@ -2496,14 +2135,6 @@ snapshots: dependencies: hasown: 2.0.4 - is-extglob@2.1.1: - optional: true - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - optional: true - js-tokens@4.0.0: {} jsbi@4.3.2: @@ -2513,54 +2144,54 @@ snapshots: json-parse-even-better-errors@2.3.1: {} - lightningcss-android-arm64@1.32.0: + lightningcss-android-arm64@1.33.0: optional: true - lightningcss-darwin-arm64@1.32.0: + lightningcss-darwin-arm64@1.33.0: optional: true - lightningcss-darwin-x64@1.32.0: + lightningcss-darwin-x64@1.33.0: optional: true - lightningcss-freebsd-x64@1.32.0: + lightningcss-freebsd-x64@1.33.0: optional: true - lightningcss-linux-arm-gnueabihf@1.32.0: + lightningcss-linux-arm-gnueabihf@1.33.0: optional: true - lightningcss-linux-arm64-gnu@1.32.0: + lightningcss-linux-arm64-gnu@1.33.0: optional: true - lightningcss-linux-arm64-musl@1.32.0: + lightningcss-linux-arm64-musl@1.33.0: optional: true - lightningcss-linux-x64-gnu@1.32.0: + lightningcss-linux-x64-gnu@1.33.0: optional: true - lightningcss-linux-x64-musl@1.32.0: + lightningcss-linux-x64-musl@1.33.0: optional: true - lightningcss-win32-arm64-msvc@1.32.0: + lightningcss-win32-arm64-msvc@1.33.0: optional: true - lightningcss-win32-x64-msvc@1.32.0: + lightningcss-win32-x64-msvc@1.33.0: optional: true - lightningcss@1.32.0: + lightningcss@1.33.0: dependencies: detect-libc: 2.1.2 optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 lines-and-columns@1.2.4: {} @@ -2574,27 +2205,20 @@ snapshots: dependencies: js-tokens: 4.0.0 - lz-string@1.5.0: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - markdown-to-jsx@9.9.0(react@19.2.7): + markdown-to-jsx@9.9.0(react@19.2.8): optionalDependencies: - react: 19.2.7 + react: 19.2.8 memoize-one@6.0.0: {} - min-indent@1.0.1: {} - ms@2.1.3: {} nanoid@3.3.16: {} - node-addon-api@7.1.1: - optional: true - object-assign@4.1.1: {} obug@2.1.4: {} @@ -2622,21 +2246,15 @@ snapshots: picomatch@4.0.5: {} - postcss@8.5.20: + postcss@8.5.23: dependencies: nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 - pretty-format@27.5.1: - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - - prop-types-extra@1.1.1(react@19.2.7): + prop-types-extra@1.1.1(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 react-is: 16.13.1 warning: 4.0.3 @@ -2646,9 +2264,9 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - qrcode.react@4.2.0(react@19.2.7): + qrcode.react@4.2.0(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 query-string@9.4.1: dependencies: @@ -2660,125 +2278,109 @@ snapshots: dependencies: performance-now: 2.1.0 - react-aria@3.50.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + react-aria@3.50.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: '@internationalized/date': 3.12.2 '@internationalized/number': 3.6.7 '@internationalized/string': 3.2.9 - '@react-types/shared': 3.36.0(react@19.2.7) + '@react-types/shared': 3.36.0(react@19.2.8) '@swc/helpers': 0.5.23 aria-hidden: 1.2.6 clsx: 2.1.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-stately: 3.48.0(react@19.2.7) - use-sync-external-store: 1.6.0(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-stately: 3.48.0(react@19.2.8) + use-sync-external-store: 1.6.0(react@19.2.8) - react-bootstrap@2.10.10(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + react-bootstrap@2.10.10(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: '@babel/runtime': 7.29.7 - '@restart/hooks': 0.4.16(react@19.2.7) - '@restart/ui': 1.9.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@restart/hooks': 0.4.16(react@19.2.8) + '@restart/ui': 1.9.4(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@types/prop-types': 15.7.15 - '@types/react-transition-group': 4.4.12(@types/react@19.2.17) + '@types/react-transition-group': 4.4.12(@types/react@19.2.18) classnames: 2.5.1 dom-helpers: 5.2.1 invariant: 2.2.4 prop-types: 15.8.1 - prop-types-extra: 1.1.1(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-transition-group: 4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - uncontrollable: 7.2.1(react@19.2.7) + prop-types-extra: 1.1.1(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-transition-group: 4.4.5(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + uncontrollable: 7.2.1(react@19.2.8) warning: 4.0.3 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - react-dom@19.2.7(react@19.2.7): + react-dom@19.2.8(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 scheduler: 0.27.0 react-fast-compare@2.0.4: {} - react-intl@10.1.18(@types/react@19.2.17)(react@19.2.7): + react-intl@10.1.19(@types/react@19.2.18)(react@19.2.8): dependencies: - '@formatjs/icu-messageformat-parser': 3.5.15 - '@formatjs/intl': 4.1.17 - '@types/react': 19.2.17 - intl-messageformat: 11.2.12 - react: 19.2.7 + '@formatjs/icu-messageformat-parser': 3.5.16 + '@formatjs/intl': 4.1.18 + '@types/react': 19.2.18 + intl-messageformat: 11.2.13 + react: 19.2.8 react-is@16.13.1: {} - react-is@17.0.2: {} - react-lifecycles-compat@3.0.4: {} - react-router-dom@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-router: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - - react-router@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - cookie: 1.1.1 - react: 19.2.7 - set-cookie-parser: 2.7.2 + cookie-es: 3.1.1 + react: 19.2.8 optionalDependencies: - react-dom: 19.2.7(react@19.2.7) + react-dom: 19.2.8(react@19.2.8) - react-select@5.10.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + react-select@5.10.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: '@babel/runtime': 7.29.7 '@emotion/cache': 11.14.0 - '@emotion/react': 11.14.0(@types/react@19.2.17)(react@19.2.7) + '@emotion/react': 11.14.0(@types/react@19.2.18)(react@19.2.8) '@floating-ui/dom': 1.8.0 - '@types/react-transition-group': 4.4.12(@types/react@19.2.17) + '@types/react-transition-group': 4.4.12(@types/react@19.2.18) memoize-one: 6.0.0 prop-types: 15.8.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-transition-group: 4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.17)(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-transition-group: 4.4.5(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.18)(react@19.2.8) transitivePeerDependencies: - '@types/react' - supports-color - react-stately@3.48.0(react@19.2.7): + react-stately@3.48.0(react@19.2.8): dependencies: '@internationalized/date': 3.12.2 '@internationalized/number': 3.6.7 '@internationalized/string': 3.2.9 - '@react-types/shared': 3.36.0(react@19.2.7) + '@react-types/shared': 3.36.0(react@19.2.8) '@swc/helpers': 0.5.23 - react: 19.2.7 - use-sync-external-store: 1.6.0(react@19.2.7) + react: 19.2.8 + use-sync-external-store: 1.6.0(react@19.2.8) - react-toastify@11.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + react-toastify@11.1.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: clsx: 2.1.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) - react-transition-group@4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + react-transition-group@4.4.5(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: '@babel/runtime': 7.29.7 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - - react@19.2.7: {} - - readdirp@5.0.0: {} + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) - redent@3.0.0: - dependencies: - indent-string: 4.0.0 - strip-indent: 3.0.0 + react@19.2.8: {} resolve-from@4.0.0: {} @@ -2789,50 +2391,40 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rolldown@1.1.5: + rolldown@1.2.1: dependencies: - '@oxc-project/types': 0.139.0 + '@oxc-project/types': 0.142.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.5 - '@rolldown/binding-darwin-arm64': 1.1.5 - '@rolldown/binding-darwin-x64': 1.1.5 - '@rolldown/binding-freebsd-x64': 1.1.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 - '@rolldown/binding-linux-arm64-gnu': 1.1.5 - '@rolldown/binding-linux-arm64-musl': 1.1.5 - '@rolldown/binding-linux-ppc64-gnu': 1.1.5 - '@rolldown/binding-linux-s390x-gnu': 1.1.5 - '@rolldown/binding-linux-x64-gnu': 1.1.5 - '@rolldown/binding-linux-x64-musl': 1.1.5 - '@rolldown/binding-openharmony-arm64': 1.1.5 - '@rolldown/binding-wasm32-wasi': 1.1.5 - '@rolldown/binding-win32-arm64-msvc': 1.1.5 - '@rolldown/binding-win32-x64-msvc': 1.1.5 - - rooks@9.8.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + '@rolldown/binding-android-arm64': 1.2.1 + '@rolldown/binding-darwin-arm64': 1.2.1 + '@rolldown/binding-darwin-x64': 1.2.1 + '@rolldown/binding-freebsd-x64': 1.2.1 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.1 + '@rolldown/binding-linux-arm64-gnu': 1.2.1 + '@rolldown/binding-linux-arm64-musl': 1.2.1 + '@rolldown/binding-linux-ppc64-gnu': 1.2.1 + '@rolldown/binding-linux-s390x-gnu': 1.2.1 + '@rolldown/binding-linux-x64-gnu': 1.2.1 + '@rolldown/binding-linux-x64-musl': 1.2.1 + '@rolldown/binding-openharmony-arm64': 1.2.1 + '@rolldown/binding-wasm32-wasi': 1.2.1 + '@rolldown/binding-win32-arm64-msvc': 1.2.1 + '@rolldown/binding-win32-x64-msvc': 1.2.1 + + rooks@9.8.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: fast-deep-equal: 3.1.3 lodash.debounce: 4.0.8 raf: 3.4.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - use-sync-external-store: 1.6.0(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + use-sync-external-store: 1.6.0(react@19.2.8) optionalDependencies: '@js-temporal/polyfill': 0.5.1 - sass@1.101.0: - dependencies: - chokidar: 5.0.0 - immutable: 5.1.9 - source-map-js: 1.2.1 - optionalDependencies: - '@parcel/watcher': 2.5.6 - scheduler@0.27.0: {} - set-cookie-parser@2.7.2: {} - siginfo@2.0.0: {} source-map-js@1.2.1: {} @@ -2845,10 +2437,6 @@ snapshots: std-env@4.2.0: {} - strip-indent@3.0.0: - dependencies: - min-indent: 1.0.1 - stylis@4.2.0: {} supports-preserve-symlinks-flag@1.0.0: {} @@ -2893,46 +2481,45 @@ snapshots: '@typescript/typescript-win32-arm64': 7.0.2 '@typescript/typescript-win32-x64': 7.0.2 - uncontrollable@7.2.1(react@19.2.7): + uncontrollable@7.2.1(react@19.2.8): dependencies: '@babel/runtime': 7.29.7 - '@types/react': 19.2.17 + '@types/react': 19.2.18 invariant: 2.2.4 - react: 19.2.7 + react: 19.2.8 react-lifecycles-compat: 3.0.4 - uncontrollable@8.0.4(react@19.2.7): + uncontrollable@8.0.4(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 undici-types@7.24.6: {} - use-isomorphic-layout-effect@1.2.1(@types/react@19.2.17)(react@19.2.7): + use-isomorphic-layout-effect@1.2.1(@types/react@19.2.18)(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - use-sync-external-store@1.6.0(react@19.2.7): + use-sync-external-store@1.6.0(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 - vite@8.1.5(@types/node@25.9.5)(sass@1.101.0): + vite@8.2.0(@types/node@25.9.5): dependencies: - lightningcss: 1.32.0 + lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.20 - rolldown: 1.1.5 + postcss: 8.5.23 + rolldown: 1.2.1 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 25.9.5 fsevents: 2.3.3 - sass: 1.101.0 - vitest@4.1.10(@types/node@25.9.5)(happy-dom@20.11.0)(vite@8.1.5(@types/node@25.9.5)(sass@1.101.0)): + vitest@4.1.10(@types/node@25.9.5)(happy-dom@20.11.1)(vite@8.2.0(@types/node@25.9.5)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@25.9.5)(sass@1.101.0)) + '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@25.9.5)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -2949,11 +2536,11 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.5(@types/node@25.9.5)(sass@1.101.0) + vite: 8.2.0(@types/node@25.9.5) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.9.5 - happy-dom: 20.11.0 + happy-dom: 20.11.1 transitivePeerDependencies: - msw diff --git a/frontend/public/images/favicon/site.webmanifest b/frontend/public/images/favicon/site.webmanifest index abd3cfe125..c23ea74a7b 100644 --- a/frontend/public/images/favicon/site.webmanifest +++ b/frontend/public/images/favicon/site.webmanifest @@ -1,19 +1,19 @@ { - "name": "", - "short_name": "", - "icons": [ - { - "src": "/images/favicon/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/images/favicon/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" + "name": "", + "short_name": "", + "icons": [ + { + "src": "/images/favicon/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/images/favicon/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" } diff --git a/frontend/src/App.css b/frontend/src/App.css index 79e704314b..34bfd117fa 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -39,7 +39,7 @@ } .react-select-container { - .react-select__control { + .react-select__control { color: var(--tblr-body-color); background-color: var(--tblr-bg-forms); border: var(--tblr-border-width) solid var(--tblr-border-color); @@ -67,14 +67,14 @@ background-color: var(--tblr-bg-forms); .react-select__option { - background: rgba(var(--tblr-primary-rgb), .04); + background: rgba(var(--tblr-primary-rgb), 0.04); color: inherit !important; &.react-select__option--is-focused { - background: rgba(var(--tblr-primary-rgb), .1); + background: rgba(var(--tblr-primary-rgb), 0.1); } &.react-select__option--is-focused.react-select__option--is-selected { - background: rgba(var(--tblr-primary-rgb), .2); + background: rgba(var(--tblr-primary-rgb), 0.2); } } } @@ -88,7 +88,7 @@ label.row { display: flex; align-items: center; padding: 0; - font-size: .875rem; + font-size: 0.875rem; font-weight: 400; line-height: 1.25rem; color: var(--tblr-gray-500); @@ -112,7 +112,8 @@ label.row { /* Fix for Tabler scrollbar compensation */ @media (min-width: 992px) { - :host, :root { - margin-left: 0; - } -} \ No newline at end of file + :host, + :root { + margin-left: 0; + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b39135009d..ab7794247a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,10 +1,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; -import EasyModal from "src/modules/easyModal"; import { RawIntlProvider } from "react-intl"; import { ToastContainer } from "react-toastify"; import { AuthProvider, LocaleProvider, ThemeProvider } from "src/context"; import { intl } from "src/locale"; +import EasyModal from "src/modules/easyModal"; import Router from "src/Router.tsx"; // Create a client diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 6aa8f0894f..df15e9ba34 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -1,5 +1,5 @@ import { lazy, Suspense } from "react"; -import { BrowserRouter, Route, Routes } from "react-router-dom"; +import { BrowserRouter, Route, Routes } from "react-router"; import { ErrorNotFound, LoadingPage, diff --git a/frontend/src/api/backend/avatar.ts b/frontend/src/api/backend/avatar.ts new file mode 100644 index 0000000000..73d29f5cf3 --- /dev/null +++ b/frontend/src/api/backend/avatar.ts @@ -0,0 +1,12 @@ +import * as api from "./base"; +import type { User } from "./models"; + +export async function uploadAvatar(id: number | string, file: File): Promise { + const data = new FormData(); + data.append("avatar", file); + return await api.post({ url: `/users/${id}/avatar`, data }); +} + +export async function deleteAvatar(id: number | string): Promise { + return await api.del({ url: `/users/${id}/avatar` }); +} diff --git a/frontend/src/api/backend/base.ts b/frontend/src/api/backend/base.ts index f73030f518..e38206a566 100644 --- a/frontend/src/api/backend/base.ts +++ b/frontend/src/api/backend/base.ts @@ -2,6 +2,7 @@ import { QueryClient } from "@tanstack/react-query"; import queryString, { type StringifiableRecord } from "query-string"; import AuthStore from "src/modules/AuthStore"; import { camelizeKeys, decamelize, decamelizeKeys } from "./caseConvert"; +import { deleteToken } from "./deleteToken"; const queryClient = new QueryClient(); const contentTypeHeader = "Content-Type"; @@ -46,6 +47,7 @@ async function processResponse(response: Response, reload = true) { // Force logout user and reload the page if Unauthorized AuthStore.clear(); queryClient.clear(); + await deleteToken().catch(() => {}); if (reload) { window.location.reload(); } diff --git a/frontend/src/api/backend/getToken.ts b/frontend/src/api/backend/getToken.ts index 7f62a0e7de..4a7679410f 100644 --- a/frontend/src/api/backend/getToken.ts +++ b/frontend/src/api/backend/getToken.ts @@ -1,22 +1,16 @@ import * as api from "./base"; -import type { TokenResponse, TwoFactorChallengeResponse } from "./responseTypes"; +import type { TokenResponse } from "./responseTypes"; -export type LoginResponse = TokenResponse | TwoFactorChallengeResponse; - -export function isTwoFactorChallenge(response: LoginResponse): response is TwoFactorChallengeResponse { - return "requires2fa" in response && response.requires2fa === true; -} - -export async function getToken(identity: string, secret: string): Promise { +export async function getToken(identity: string, secret: string): Promise { return await api.post({ url: "/tokens", data: { identity, secret }, }); } -export async function verify2FA(challengeToken: string, code: string): Promise { +export async function verify2FA(code: string): Promise { return await api.post({ url: "/tokens/2fa", - data: { challengeToken, code }, + data: { code }, }); } diff --git a/frontend/src/api/backend/index.ts b/frontend/src/api/backend/index.ts index b867136aa0..be8202eddc 100644 --- a/frontend/src/api/backend/index.ts +++ b/frontend/src/api/backend/index.ts @@ -1,3 +1,4 @@ +export * from "./avatar"; export * from "./checkVersion"; export * from "./createAccessList"; export * from "./createCertificate"; @@ -39,11 +40,11 @@ export * from "./getToken"; export * from "./getUser"; export * from "./getUsers"; export * from "./helpers"; -export * from "./loginAsUser"; export * from "./models"; export * from "./refreshToken"; export * from "./renewCertificate"; export * from "./responseTypes"; +export * from "./revokeSessions"; export * from "./setPermissions"; export * from "./testHttpCertificate"; export * from "./toggleDeadHost"; @@ -51,6 +52,7 @@ export * from "./toggleProxyHost"; export * from "./toggleRedirectionHost"; export * from "./toggleStream"; export * from "./toggleUser"; +export * from "./twoFactor"; export * from "./updateAccessList"; export * from "./updateAuth"; export * from "./updateDeadHost"; @@ -61,4 +63,3 @@ export * from "./updateStream"; export * from "./updateUser"; export * from "./uploadCertificate"; export * from "./validateCertificate"; -export * from "./twoFactor"; diff --git a/frontend/src/api/backend/loginAsUser.ts b/frontend/src/api/backend/loginAsUser.ts deleted file mode 100644 index 2ade379d80..0000000000 --- a/frontend/src/api/backend/loginAsUser.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as api from "./base"; -import type { LoginAsTokenResponse } from "./responseTypes"; - -export async function loginAsUser(id: number): Promise { - return await api.post({ - url: `/users/${id}/login`, - }); -} diff --git a/frontend/src/api/backend/responseTypes.ts b/frontend/src/api/backend/responseTypes.ts index cafa786085..1d87e86bc9 100644 --- a/frontend/src/api/backend/responseTypes.ts +++ b/frontend/src/api/backend/responseTypes.ts @@ -1,8 +1,5 @@ -import type { User } from "./models"; - export interface HealthResponse { status: string; - version: string; setup: boolean; password: boolean; oidc: boolean; @@ -17,21 +14,12 @@ export interface ValidatedCertificateResponse { certificateKey: boolean; } -export interface LoginAsTokenResponse extends TokenResponse { - user: User; -} - export interface VersionCheckResponse { current: string | null; latest: string | null; updateAvailable: boolean; } -export interface TwoFactorChallengeResponse { - requires2fa: boolean; - challengeToken: string; -} - export interface TwoFactorStatusResponse { enabled: boolean; backupCodesRemaining: number; diff --git a/frontend/src/api/backend/revokeSessions.ts b/frontend/src/api/backend/revokeSessions.ts new file mode 100644 index 0000000000..f5efe2cdea --- /dev/null +++ b/frontend/src/api/backend/revokeSessions.ts @@ -0,0 +1,7 @@ +import * as api from "./base"; + +export async function revokeSessions(userId: number | "me"): Promise { + return await api.del({ + url: `/users/${userId}/sessions`, + }); +} diff --git a/frontend/src/api/backend/twoFactor.ts b/frontend/src/api/backend/twoFactor.ts index e93c0744d6..d85a6e367d 100644 --- a/frontend/src/api/backend/twoFactor.ts +++ b/frontend/src/api/backend/twoFactor.ts @@ -1,5 +1,5 @@ -import type { TwoFactorEnableResponse, TwoFactorSetupResponse, TwoFactorStatusResponse } from "./responseTypes"; import * as api from "./base"; +import type { TwoFactorEnableResponse, TwoFactorSetupResponse, TwoFactorStatusResponse } from "./responseTypes"; export async function get2FAStatus(userId: number | "me"): Promise { return await api.get({ @@ -35,3 +35,9 @@ export async function regenerateBackupCodes(userId: number | "me", code: string) data: { code }, }); } + +export async function adminDisable2FA(userId: number): Promise { + return await api.del({ + url: `/users/${userId}/2fa`, + }); +} diff --git a/frontend/src/components/ErrorNotFound.tsx b/frontend/src/components/ErrorNotFound.tsx index a162d1cd66..76f78eea2c 100644 --- a/frontend/src/components/ErrorNotFound.tsx +++ b/frontend/src/components/ErrorNotFound.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router"; import { Button } from "src/components"; import { T } from "src/locale"; diff --git a/frontend/src/components/Form/AccessFields.tsx b/frontend/src/components/Form/AccessFields.tsx index d944840198..f643366970 100644 --- a/frontend/src/components/Form/AccessFields.tsx +++ b/frontend/src/components/Form/AccessFields.tsx @@ -1,6 +1,6 @@ -import { IconArrowDown, IconWorld, IconLock, IconLockOpen2, IconArrowUp, IconX } from "@tabler/icons-react"; +import { IconArrowDown, IconArrowUp, IconList, IconLock, IconLockOpen2, IconWorld, IconX } from "@tabler/icons-react"; import { useFormikContext } from "formik"; -import { useState, type ReactNode } from "react"; +import { type ReactNode, useState } from "react"; import Select, { components, type OptionProps } from "react-select"; import type { AccessList, ProxyLocation } from "src/api/backend"; import { useLocaleState } from "src/context"; @@ -33,6 +33,14 @@ interface AccessTypeOption extends BaseOption { readonly icon?: ReactNode; } +interface AccessRow { + useIP: boolean; + accessListId: number; + allow: boolean; + address: string; +} + + const OptionContent = (label: string, subLabel: string, icon?: ReactNode) => (
@@ -56,7 +64,11 @@ const TypeOption = (props: OptionProps) => { }; export function AccessFields({ initialAccessListType, location, initialAccessListIds, name, type, onChange }: Props) { - const [values, setValues] = useState(initialAccessListIds || []); + const [values, setValues] = useState((initialAccessListIds || []).map((accessListId) => ({ + mode: "acl", + accessListId, + })), + ); const [aclValue, setAclValue] = useState(initialAccessListType); const { locale } = useLocaleState(); const { setFieldValue } = useFormikContext(); @@ -104,7 +116,7 @@ export function AccessFields({ initialAccessListType, location, initialAccessLis }; const defaultOptions: AccessOption[] = data?.map(createDefaultItem) || []; - const valuesSet = new Set(values?.map((item: number) => item || 0) || []); + const valuesSet = new Set(values?.filter((item: AccessRow) => item.mode === "acl").map((item: AccessRow) => item.accessListId || 0) || []); const options = defaultOptions.filter((option: AccessOption) => !valuesSet.has(option.value)); const typeOptions = (): AccessTypeOption[] => { @@ -134,6 +146,22 @@ export function AccessFields({ initialAccessListType, location, initialAccessLis applyUpdatedValues(newValues); }; + const setUseIP = (idx: number, useIP: boolean) => { + const current = values[idx]; + + const nextValue: AccessRow = useIP ? { + mode: "ip", + allow: true, + address: "", + } : { + mode: "acl", + accessListId: findFirstAvailableOption()?.value || 0, + }; + + const newValues = values.with(idx, nextValue); + applyUpdatedValues(newValues); + } + const handleAdd = () => { const newAccessOption = findFirstAvailableOption(); if (newAccessOption?.meta.id) { @@ -164,8 +192,8 @@ export function AccessFields({ initialAccessListType, location, initialAccessLis } }; - const handleRemove = (aclId: number) => { - const newValues = values.filter((id: number) => id !== aclId); + const handleRemove = (idx: number) => { + const newValues = values.toSpliced(idx, 1); applyUpdatedValues(newValues); }; @@ -204,24 +232,70 @@ export function AccessFields({ initialAccessListType, location, initialAccessLis <> {values.map((item: number, idx: number) => (
- - className="react-select-container col-md-8 mb-1" - classNamePrefix="react-select" - value={defaultOptions.find((o) => o.value === item) ?? null} - options={options} - components={{ Option }} - styles={{ - option: (base) => ({ - ...base, - height: "100%", - }), - }} + {item.mode === "acl" ? ( + + className="react-select-container col-md-8 mb-1" + classNamePrefix="react-select" + value={defaultOptions.find((o) => o.value === item) ?? null} + options={options} + components={{ Option }} + styles={{ + option: (base) => ({ + ...base, + height: "100%", + }), + }} + onChange={(e) => { + if (!e || Array.isArray(e)) return; + onAccessListChange(e.meta, idx); + }} + isDisabled={aclValue !== "custom"} + /> + ) : ( +
+ + + handleIPChange(idx, "address", event.target.value) + } + /> +
+ )} + { - if (!e || Array.isArray(e)) return; - onAccessListChange(e.meta, idx); + setUseIP(idx, e.target.checked); }} - isDisabled={aclValue !== "custom"} - /> + /> + {idx > 0 ? (
+ {!item.path?.endsWith("/") && item.locationType !== "= " && item.locationType !== "@" && ( +

+ + +

+ )}
diff --git a/frontend/src/components/NavLink.tsx b/frontend/src/components/NavLink.tsx index b6a414af6a..518f0b9072 100644 --- a/frontend/src/components/NavLink.tsx +++ b/frontend/src/components/NavLink.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router"; interface Props { children: React.ReactNode; diff --git a/frontend/src/components/SiteFooter.tsx b/frontend/src/components/SiteFooter.tsx index 372f8c1ed6..6ca3dd4835 100644 --- a/frontend/src/components/SiteFooter.tsx +++ b/frontend/src/components/SiteFooter.tsx @@ -1,11 +1,10 @@ -import { useCheckVersion, useHealth } from "src/hooks"; +import { useCheckVersion } from "src/hooks"; import { T } from "src/locale"; export function SiteFooter() { - const health = useHealth(); const { data: versionData } = useCheckVersion(); - const version = health.data?.version || ""; + const version = versionData?.current || ""; const githubTag = version.split("-").slice(0, 4).join("-"); const githubLinkType = githubTag.length < 13 ? "tree" : "releases/tag"; diff --git a/frontend/src/components/SiteHeader.tsx b/frontend/src/components/SiteHeader.tsx index bd4bbe3d5b..41e5f89e94 100644 --- a/frontend/src/components/SiteHeader.tsx +++ b/frontend/src/components/SiteHeader.tsx @@ -1,4 +1,4 @@ -import { IconLock, IconLogout, IconShieldLock, IconUser } from "@tabler/icons-react"; +import { IconDevicesX, IconLock, IconLogout, IconShieldLock, IconUser } from "@tabler/icons-react"; import { LocalePicker, NavLink, ThemeSwitcher } from "src/components"; import { useAuthState } from "src/context"; import { useUser } from "src/hooks"; @@ -9,7 +9,7 @@ import styles from "./SiteHeader.module.css"; export function SiteHeader() { const { data: currentUser } = useUser("me"); const isAdmin = currentUser?.roles.includes("admin"); - const { logout } = useAuthState(); + const { logout, logoutEverywhere } = useAuthState(); return (
@@ -134,6 +134,17 @@ export function SiteHeader() { + { + e.preventDefault(); + logoutEverywhere(); + }} + > + + +
diff --git a/frontend/src/components/SiteMenu.tsx b/frontend/src/components/SiteMenu.tsx index 48f74d225f..63385d1b29 100644 --- a/frontend/src/components/SiteMenu.tsx +++ b/frontend/src/components/SiteMenu.tsx @@ -139,13 +139,12 @@ const getMenuDropown = (item: MenuItem, onClick?: () => void) => { hideError >
  • - +
    {item.items?.map((subitem, idx) => { return ( diff --git a/frontend/src/components/Table/Formatter/AccessListformatter.tsx b/frontend/src/components/Table/Formatter/AccessListformatter.tsx index e8b28046b4..56e7359aeb 100644 --- a/frontend/src/components/Table/Formatter/AccessListformatter.tsx +++ b/frontend/src/components/Table/Formatter/AccessListformatter.tsx @@ -1,8 +1,8 @@ +import OverlayTrigger from "react-bootstrap/OverlayTrigger"; +import Popover from "react-bootstrap/Popover"; import type { AccessList, ProxyLocation } from "src/api/backend"; import { intl } from "src/locale"; import { showAccessListModal, showProxyHostModal } from "src/modals"; -import OverlayTrigger from "react-bootstrap/OverlayTrigger"; -import Popover from "react-bootstrap/Popover"; interface Props { access?: AccessList[]; diff --git a/frontend/src/components/Table/TableHelpers.ts b/frontend/src/components/Table/TableHelpers.ts index 0df329c804..a6302e09bf 100644 --- a/frontend/src/components/Table/TableHelpers.ts +++ b/frontend/src/components/Table/TableHelpers.ts @@ -61,4 +61,4 @@ const tableEventReducer = (state: any, { type, payload }: any) => { } }; -export { tableEvents, tableEventReducer }; +export { tableEventReducer, tableEvents }; diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index 88ce8cec18..a39d56181e 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -1,36 +1,30 @@ import { useQueryClient } from "@tanstack/react-query"; -import { createContext, type ReactNode, useContext, useState, useEffect } from "react"; +import { createContext, type ReactNode, useContext, useEffect, useState } from "react"; import { useIntervalWhen } from "rooks"; -import { - getToken, - isTwoFactorChallenge, - loginAsUser, - deleteToken, - refreshToken, - verify2FA, - type TokenResponse, -} from "src/api/backend"; +import { deleteToken, getToken, refreshToken, revokeSessions, type TokenResponse, verify2FA } from "src/api/backend"; import AuthStore from "src/modules/AuthStore"; -// 2FA challenge state -export interface TwoFactorChallenge { - challengeToken: string; -} - // Context export interface AuthContextType { authenticated: boolean; - twoFactorChallenge: TwoFactorChallenge | null; + twoFactorChallenge: boolean; login: (username: string, password: string) => Promise; verifyTwoFactor: (code: string) => Promise; cancelTwoFactor: () => void; - loginAs: (id: number) => Promise; logout: () => void; + logoutEverywhere: () => void; } const initalValue = null; const AuthContext = createContext(initalValue); +const getCookie = (name: string): string | undefined => { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop()?.split(";").shift(); + return undefined; +}; + // Provider interface Props { children?: ReactNode; @@ -39,18 +33,20 @@ interface Props { function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props) { const queryClient = useQueryClient(); const [authenticated, setAuthenticated] = useState(AuthStore.hasActiveToken()); - const [twoFactorChallenge, setTwoFactorChallenge] = useState(null); + const [twoFactorChallenge, setTwoFactorChallenge] = useState( + () => getCookie("__Host-npmplus_oidc_totp_required") === "true", + ); const handleTokenUpdate = (response: TokenResponse) => { AuthStore.set(response); setAuthenticated(true); - setTwoFactorChallenge(null); + setTwoFactorChallenge(false); }; const login = async (identity: string, secret: string) => { const response = await getToken(identity, secret); - if (isTwoFactorChallenge(response)) { - setTwoFactorChallenge({ challengeToken: response.challengeToken }); + if (response.requires2fa) { + setTwoFactorChallenge(true); return; } handleTokenUpdate(response); @@ -60,23 +56,23 @@ function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props) if (!twoFactorChallenge) { throw new Error("No 2FA challenge pending"); } - const response = await verify2FA(twoFactorChallenge.challengeToken, code); + const response = await verify2FA(code); handleTokenUpdate(response); }; const cancelTwoFactor = () => { - setTwoFactorChallenge(null); + setTwoFactorChallenge(false); }; - const loginAs = async (id: number) => { - const response = await loginAsUser(id); - AuthStore.add(response); + const logout = async () => { + await deleteToken(); + AuthStore.clear(); + setAuthenticated(false); queryClient.clear(); - window.location.reload(); }; - const logout = async () => { - await deleteToken(); + const logoutEverywhere = async () => { + await revokeSessions("me"); AuthStore.clear(); setAuthenticated(false); queryClient.clear(); @@ -89,6 +85,10 @@ function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props) useEffect(() => { if (!authenticated) { + if (twoFactorChallenge) { + window.cookieStore.delete("__Host-npmplus_oidc_totp_required"); + return; + } refresh(false).catch(() => {}); } }); @@ -109,8 +109,8 @@ function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props) login, verifyTwoFactor, cancelTwoFactor, - loginAs, logout, + logoutEverywhere, }; return {children}; diff --git a/frontend/src/hooks/useProxyHost.ts b/frontend/src/hooks/useProxyHost.ts index 43b6cfad82..673a9cde2e 100644 --- a/frontend/src/hooks/useProxyHost.ts +++ b/frontend/src/hooks/useProxyHost.ts @@ -38,6 +38,7 @@ const fetchProxyHost = (id: number | "new") => { npmplusAuthRequest: "none", npmplusAuthRequestUpstream: "", npmplusAccessListType: "public", + npmplusAccessListEntryIP: false, } as ProxyHost); } return getProxyHost(id, ["owner"]); diff --git a/frontend/src/hooks/useSetting.ts b/frontend/src/hooks/useSetting.ts index ce843d6504..9aed835363 100644 --- a/frontend/src/hooks/useSetting.ts +++ b/frontend/src/hooks/useSetting.ts @@ -37,4 +37,4 @@ const useSetSetting = () => { }); }; -export { useSetting, useSetSetting }; +export { useSetSetting, useSetting }; diff --git a/frontend/src/hooks/useStream.ts b/frontend/src/hooks/useStream.ts index b1c787eb11..a00cfe2810 100644 --- a/frontend/src/hooks/useStream.ts +++ b/frontend/src/hooks/useStream.ts @@ -57,4 +57,4 @@ const useSetStream = () => { }); }; -export { useStream, useSetStream }; +export { useSetStream, useStream }; diff --git a/frontend/src/hooks/useTheme.ts b/frontend/src/hooks/useTheme.ts index 941c0626e8..f9f42e9f1e 100644 --- a/frontend/src/hooks/useTheme.ts +++ b/frontend/src/hooks/useTheme.ts @@ -5,4 +5,4 @@ const useTheme = () => { return useThemeContext(); }; -export { useTheme, Dark, Light }; +export { Dark, Light, useTheme }; diff --git a/frontend/src/hooks/useUser.ts b/frontend/src/hooks/useUser.ts index 4f39949014..54fa9b34f5 100644 --- a/frontend/src/hooks/useUser.ts +++ b/frontend/src/hooks/useUser.ts @@ -51,4 +51,4 @@ const useSetUser = () => { }); }; -export { useUser, useSetUser }; +export { useSetUser, useUser }; diff --git a/frontend/src/locale/IntlProvider.tsx b/frontend/src/locale/IntlProvider.tsx index 249b70c13b..33278c6bcc 100644 --- a/frontend/src/locale/IntlProvider.tsx +++ b/frontend/src/locale/IntlProvider.tsx @@ -1,8 +1,8 @@ import { createIntl, createIntlCache } from "react-intl"; -import langEn from "./lang/en.json"; import langBg from "./lang/bg.json"; import langCs from "./lang/cs.json"; import langDe from "./lang/de.json"; +import langEn from "./lang/en.json"; import langEs from "./lang/es.json"; import langEt from "./lang/et.json"; import langFr from "./lang/fr.json"; @@ -12,6 +12,7 @@ import langId from "./lang/id.json"; import langIt from "./lang/it.json"; import langJa from "./lang/ja.json"; import langKo from "./lang/ko.json"; +import langList from "./lang/lang-list.json"; import langNl from "./lang/nl.json"; import langNo from "./lang/no.json"; import langPl from "./lang/pl.json"; @@ -21,7 +22,6 @@ import langSk from "./lang/sk.json"; import langTr from "./lang/tr.json"; import langVi from "./lang/vi.json"; import langZh from "./lang/zh.json"; -import langList from "./lang/lang-list.json"; // first item of each array should be the language code, // not the country code @@ -62,25 +62,6 @@ const loadMessages = (locale?: string): typeof langList & typeof langEn => { return Object.assign({}, langList, langEn, localeOptions.find(([code]) => code === thisLocale)?.[2]); }; -const getFlagCodeForLocale = (locale?: string) => { - const thisLocale = (locale || "en").slice(0, 2); - - // only add to this if your flag is different from the locale code - const specialCases: Record = { - ja: "jp", // Japan - zh: "cn", // China - vi: "vn", // Vietnam - ko: "kr", // Korea - cs: "cz", // Czechia - ga: "ie", // Ireland (Irish) - }; - - if (specialCases[thisLocale]) { - return specialCases[thisLocale].toUpperCase(); - } - return thisLocale.toUpperCase(); -}; - const getLocale = (short = false) => { let loc = window.localStorage.getItem("locale"); if (!loc) loc = document.documentElement.lang; @@ -135,4 +116,4 @@ const T = ({ //console.log("L:", localeOptions); -export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T }; +export { changeLocale, createIntl, getLocale, intl, localeOptions, T }; diff --git a/frontend/src/locale/Utils.test.tsx b/frontend/src/locale/Utils.test.tsx index b3a2d47ebb..9222992ae7 100644 --- a/frontend/src/locale/Utils.test.tsx +++ b/frontend/src/locale/Utils.test.tsx @@ -1,20 +1,13 @@ -import { formatDateTime, getFlagCodeForLocale } from "src/locale"; +import { formatDateTime, getFlagCodeForLocale } from "src/locale/Utils"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; describe("DateFormatter", () => { // Keep a reference to the real Intl to restore later - const RealIntl = global.Intl; + const RealIntl = globalThis.Intl; const desiredTimeZone = "Europe/London"; const desiredLocale = "en-GB"; beforeAll(() => { - // Ensure Node-based libs using TZ behave deterministically - try { - process.env.TZ = desiredTimeZone; - } catch { - // ignore if not available - } - // Mock Intl.DateTimeFormat so formatting is stable regardless of host const MockedDateTimeFormat = class extends RealIntl.DateTimeFormat { constructor(_locales?: string | string[], options?: Intl.DateTimeFormatOptions) { @@ -25,7 +18,7 @@ describe("DateFormatter", () => { } } as unknown as typeof Intl.DateTimeFormat; - global.Intl = { + globalThis.Intl = { ...RealIntl, DateTimeFormat: MockedDateTimeFormat, }; @@ -33,7 +26,7 @@ describe("DateFormatter", () => { afterAll(() => { // Restore original Intl after tests - global.Intl = RealIntl; + globalThis.Intl = RealIntl; }); it("format date from iso date", () => { diff --git a/frontend/src/locale/Utils.ts b/frontend/src/locale/Utils.ts index c9a7fbf878..f05891e12a 100644 --- a/frontend/src/locale/Utils.ts +++ b/frontend/src/locale/Utils.ts @@ -38,4 +38,24 @@ const formatDateTime = (value: string | number, locale = "en-US"): string => { } }; -export { formatDateTime, parseDate, isUnixTimestamp }; +const getFlagCodeForLocale = (locale?: string) => { + const thisLocale = (locale || "en").slice(0, 2); + + // only add to this if your flag is different from the locale code + const specialCases: Record = { + ja: "jp", // Japan + zh: "cn", // China + vi: "vn", // Vietnam + ko: "kr", // Korea + cs: "cz", // Czechia + ga: "ie", // Ireland (Irish) + et: "ee", // Estonia + }; + + if (specialCases[thisLocale]) { + return specialCases[thisLocale].toUpperCase(); + } + return thisLocale.toUpperCase(); +}; + +export { formatDateTime, getFlagCodeForLocale, isUnixTimestamp, parseDate }; diff --git a/frontend/src/locale/scripts/locale-sort.cjs b/frontend/src/locale/scripts/locale-sort.cjs index bcaffdbccd..d727891074 100644 --- a/frontend/src/locale/scripts/locale-sort.cjs +++ b/frontend/src/locale/scripts/locale-sort.cjs @@ -1,13 +1,13 @@ #!/usr/bin/env node -const fs = require("fs"); -const path = require("path"); +const fs = require("node:fs"); +const path = require("node:path"); const DIR = path.resolve(__dirname, "../src"); // Function to sort object keys recursively function sortKeys(obj) { - if (obj === null || typeof obj !== "object" || obj instanceof Array) { + if (obj === null || typeof obj !== "object" || Array.isArray(obj)) { return obj; } @@ -15,7 +15,7 @@ function sortKeys(obj) { const keys = Object.keys(obj).sort(); for (const key of keys) { const value = obj[key]; - if (typeof value === "object" && value !== null && !(value instanceof Array)) { + if (typeof value === "object" && value !== null && !Array.isArray(value)) { sorted[key] = sortKeys(value); } else { sorted[key] = value; @@ -51,7 +51,7 @@ files.forEach((file) => { const sortedJson = sortKeys(originalJson); // Convert back to string with tabs - const sortedContent = JSON.stringify(sortedJson, null, "\t") + "\n"; + const sortedContent = `${JSON.stringify(sortedJson, null, "\t")}\n`; // Compare (normalize whitespace) if (originalContent.trim() === sortedContent.trim()) { @@ -66,4 +66,3 @@ files.forEach((file) => { console.error(`Error processing ${file}:`, error.message); } }); - diff --git a/frontend/src/locale/src/HelpDoc/index.ts b/frontend/src/locale/src/HelpDoc/index.ts index f0be4d10d3..3bfd8e71d6 100644 --- a/frontend/src/locale/src/HelpDoc/index.ts +++ b/frontend/src/locale/src/HelpDoc/index.ts @@ -1,7 +1,7 @@ -import * as en from "./en/index"; import * as bg from "./bg/index"; import * as cs from "./cs/index"; import * as de from "./de/index"; +import * as en from "./en/index"; import * as es from "./es/index"; import * as et from "./et/index"; import * as fr from "./fr/index"; diff --git a/frontend/src/locale/src/en.json b/frontend/src/locale/src/en.json index 1d7a607b05..e0fdcb9091 100644 --- a/frontend/src/locale/src/en.json +++ b/frontend/src/locale/src/en.json @@ -152,6 +152,12 @@ "auto": { "defaultMessage": "Auto" }, + "avatar.remove": { + "defaultMessage": "Remove avatar" + }, + "avatar.upload": { + "defaultMessage": "Upload avatar" + }, "cancel": { "defaultMessage": "Cancel" }, @@ -689,6 +695,9 @@ "proxy-host.global-access-lists": { "defaultMessage": "Global Access Lists" }, + "proxy-host.location-no-trailing-slash-warning": { + "defaultMessage": "A location without a trailing slash is matched as a byte prefix, not a path segment, which can lead to over-matching and/or path traversal. Add a trailing slash unless you intend prefix matching." + }, "proxy-host.no-directory": { "defaultMessage": "No group" }, @@ -866,12 +875,27 @@ "user.logout": { "defaultMessage": "Logout" }, + "user.logout-everywhere": { + "defaultMessage": "Logout everywhere" + }, "user.new-password": { "defaultMessage": "New Password" }, "user.nickname": { "defaultMessage": "Nickname" }, + "user.reset-2fa": { + "defaultMessage": "Reset 2FA" + }, + "user.reset-2fa.content": { + "defaultMessage": "This disables two-factor authentication for this user without their recovery code. They can set it up again afterwards. Continue?" + }, + "user.revoke-sessions": { + "defaultMessage": "Revoke sessions" + }, + "user.revoke-sessions.content": { + "defaultMessage": "This logs the user out of all their sessions on every device. Continue?" + }, "user.set-password": { "defaultMessage": "Set Password" }, diff --git a/frontend/src/locale/src/pt.json b/frontend/src/locale/src/pt.json index 90229c2d62..76106567be 100644 --- a/frontend/src/locale/src/pt.json +++ b/frontend/src/locale/src/pt.json @@ -692,4 +692,4 @@ "users": { "defaultMessage": "Utilizadores" } -} \ No newline at end of file +} diff --git a/frontend/src/modals/AccessListModal.tsx b/frontend/src/modals/AccessListModal.tsx index 94136a3a07..c55d1ff296 100644 --- a/frontend/src/modals/AccessListModal.tsx +++ b/frontend/src/modals/AccessListModal.tsx @@ -1,5 +1,4 @@ import cn from "clsx"; -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { Field, Form, Formik } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; @@ -8,6 +7,7 @@ import type { AccessList, AccessListClient, AccessListItem } from "src/api/backe import { AccessClientFields, BasicAuthFields, Button, Loading } from "src/components"; import { useAccessList, useSetAccessList } from "src/hooks"; import { intl, T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { validateString } from "src/modules/Validations"; import { showObjectSuccess } from "src/notifications"; diff --git a/frontend/src/modals/ChangePasswordModal.tsx b/frontend/src/modals/ChangePasswordModal.tsx index 41c17b95f4..1a480bc519 100644 --- a/frontend/src/modals/ChangePasswordModal.tsx +++ b/frontend/src/modals/ChangePasswordModal.tsx @@ -1,4 +1,3 @@ -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { IconEye, IconEyeOff } from "@tabler/icons-react"; import { ErrorMessage, Field, Form, Formik } from "formik"; import { type ReactNode, useState } from "react"; @@ -7,6 +6,7 @@ import Modal from "react-bootstrap/Modal"; import { updateAuth } from "src/api/backend"; import { Button } from "src/components"; import { intl, T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { validateString } from "src/modules/Validations"; const showChangePasswordModal = (id: number | "me") => { diff --git a/frontend/src/modals/CustomCertificateModal.tsx b/frontend/src/modals/CustomCertificateModal.tsx index c2f37f7c61..834d365d80 100644 --- a/frontend/src/modals/CustomCertificateModal.tsx +++ b/frontend/src/modals/CustomCertificateModal.tsx @@ -1,6 +1,5 @@ import { IconAlertTriangle } from "@tabler/icons-react"; import { useQueryClient } from "@tanstack/react-query"; -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { Field, Form, Formik } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; @@ -8,6 +7,7 @@ import Modal from "react-bootstrap/Modal"; import { type Certificate, createCertificate, uploadCertificate, validateCertificate } from "src/api/backend"; import { Button } from "src/components"; import { T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { validateString } from "src/modules/Validations"; import { showObjectSuccess } from "src/notifications"; diff --git a/frontend/src/modals/DNSCertificateModal.tsx b/frontend/src/modals/DNSCertificateModal.tsx index eb260f32dc..fc017f537e 100644 --- a/frontend/src/modals/DNSCertificateModal.tsx +++ b/frontend/src/modals/DNSCertificateModal.tsx @@ -1,6 +1,5 @@ -import cn from "clsx"; import { useQueryClient } from "@tanstack/react-query"; -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; +import cn from "clsx"; import { Field, Form, Formik } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; @@ -8,6 +7,7 @@ import Modal from "react-bootstrap/Modal"; import { createCertificate } from "src/api/backend"; import { Button, DNSProviderFields, DomainNamesField } from "src/components"; import { T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { showObjectSuccess } from "src/notifications"; const showDNSCertificateModal = () => { diff --git a/frontend/src/modals/DeadHostModal.tsx b/frontend/src/modals/DeadHostModal.tsx index 64f5bfe8ec..5c835d54f0 100644 --- a/frontend/src/modals/DeadHostModal.tsx +++ b/frontend/src/modals/DeadHostModal.tsx @@ -1,4 +1,3 @@ -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { Form, Formik } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; @@ -14,6 +13,7 @@ import { } from "src/components"; import { useDeadHost, useDeadHosts, useDirectorySuggestions, useSetDeadHost } from "src/hooks"; import { T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { showObjectSuccess } from "src/notifications"; const showDeadHostModal = (id: number | "new") => { diff --git a/frontend/src/modals/DeleteConfirmModal.tsx b/frontend/src/modals/DeleteConfirmModal.tsx index 7348ee08a5..a8ab8efe4b 100644 --- a/frontend/src/modals/DeleteConfirmModal.tsx +++ b/frontend/src/modals/DeleteConfirmModal.tsx @@ -1,10 +1,10 @@ import { useQueryClient } from "@tanstack/react-query"; -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; import { Button } from "src/components"; import { T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; interface ShowProps { title?: ReactNode; diff --git a/frontend/src/modals/EventDetailsModal.tsx b/frontend/src/modals/EventDetailsModal.tsx index 51895bf97e..8592fa5760 100644 --- a/frontend/src/modals/EventDetailsModal.tsx +++ b/frontend/src/modals/EventDetailsModal.tsx @@ -1,9 +1,9 @@ -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; import { Button, EventFormatter, GravatarFormatter, Loading } from "src/components"; import { useAuditLog } from "src/hooks"; import { T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; const showEventDetailsModal = (id: number) => { EasyModal.show(EventDetailsModal, { id }); diff --git a/frontend/src/modals/HTTPCertificateModal.tsx b/frontend/src/modals/HTTPCertificateModal.tsx index 7741db6a26..8e0cd4e6a8 100644 --- a/frontend/src/modals/HTTPCertificateModal.tsx +++ b/frontend/src/modals/HTTPCertificateModal.tsx @@ -1,7 +1,6 @@ -import cn from "clsx"; import { IconAlertTriangle } from "@tabler/icons-react"; import { useQueryClient } from "@tanstack/react-query"; -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; +import cn from "clsx"; import { Field, Form, Formik } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; @@ -9,6 +8,7 @@ import Modal from "react-bootstrap/Modal"; import { createCertificate, testHttpCertificate } from "src/api/backend"; import { Button, DomainNamesField } from "src/components"; import { T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { showObjectSuccess } from "src/notifications"; const showHTTPCertificateModal = () => { @@ -74,7 +74,7 @@ const HTTPCertificateModal = EasyModal.create(({ visible, remove }: InnerModalPr <> {testResults.map((testResult) => { const { domain, status } = testResult; - let messageComponent: ReactNode = status; + let messageComponent: ReactNode; switch (status) { case "ok": diff --git a/frontend/src/modals/HelpModal.tsx b/frontend/src/modals/HelpModal.tsx index a620614485..d9d9092442 100644 --- a/frontend/src/modals/HelpModal.tsx +++ b/frontend/src/modals/HelpModal.tsx @@ -1,9 +1,9 @@ -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; +import Markdown from "markdown-to-jsx"; import { useEffect, useState } from "react"; import Modal from "react-bootstrap/Modal"; -import Markdown from "markdown-to-jsx"; import { getLocale, T } from "src/locale"; import { getHelpFile } from "src/locale/src/HelpDoc"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; interface Props extends InnerModalProps { section: string; diff --git a/frontend/src/modals/PermissionsModal.module.css b/frontend/src/modals/PermissionsModal.module.css index 6ef4265ff8..a7af7f4e0d 100644 --- a/frontend/src/modals/PermissionsModal.module.css +++ b/frontend/src/modals/PermissionsModal.module.css @@ -1,4 +1,3 @@ .active { - border-color: var(--tblr-orange) !important; + border-color: var(--tblr-orange) !important; } - diff --git a/frontend/src/modals/PermissionsModal.tsx b/frontend/src/modals/PermissionsModal.tsx index feb0270f07..428ad72f2f 100644 --- a/frontend/src/modals/PermissionsModal.tsx +++ b/frontend/src/modals/PermissionsModal.tsx @@ -1,6 +1,5 @@ import { useQueryClient } from "@tanstack/react-query"; import cn from "clsx"; -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { Field, Form, Formik } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; @@ -9,6 +8,7 @@ import { setPermissions } from "src/api/backend"; import { Button, Loading } from "src/components"; import { useUser } from "src/hooks"; import { T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import styles from "./PermissionsModal.module.css"; const showPermissionsModal = (id: number) => { @@ -84,7 +84,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => { return (
    -
    +
    { -
    +
    ); }; @@ -172,7 +172,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => { {({ field, form }: any) => ( -
    +
    { > -
    + )}
    diff --git a/frontend/src/modals/ProxyHostModal.tsx b/frontend/src/modals/ProxyHostModal.tsx index caa8d0244c..82080d9e48 100644 --- a/frontend/src/modals/ProxyHostModal.tsx +++ b/frontend/src/modals/ProxyHostModal.tsx @@ -1,6 +1,5 @@ import { IconSettings } from "@tabler/icons-react"; import cn from "clsx"; -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { Field, Form, Formik } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; @@ -19,6 +18,7 @@ import { } from "src/components"; import { useDirectorySuggestions, useProxyHost, useProxyHosts, useSetProxyHost, useUser } from "src/hooks"; import { intl, T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions"; import { validateNumber, validateUpstreamUrl } from "src/modules/Validations"; import { showObjectSuccess } from "src/notifications"; diff --git a/frontend/src/modals/ReachabilityModal.tsx b/frontend/src/modals/ReachabilityModal.tsx index 319b296dd7..f98b08ee7d 100644 --- a/frontend/src/modals/ReachabilityModal.tsx +++ b/frontend/src/modals/ReachabilityModal.tsx @@ -1,9 +1,9 @@ -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { type ReactNode, useEffect, useState } from "react"; import { Alert, Spinner } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; import { testHttpCertificate } from "src/api/backend"; import { T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; const showReachabilityModal = (domains: string[]) => { EasyModal.show(ReachabilityModal, { domains }); @@ -35,7 +35,7 @@ const ReachabilityModal = EasyModal.create(({ visible, remove, domains }: InnerM <> {testResults.map((testResult) => { const { domain, status } = testResult; - let messageComponent: ReactNode = status; + let messageComponent: ReactNode; switch (status) { case "ok": diff --git a/frontend/src/modals/RedirectionHostModal.tsx b/frontend/src/modals/RedirectionHostModal.tsx index 519cfbef06..e96fc5e677 100644 --- a/frontend/src/modals/RedirectionHostModal.tsx +++ b/frontend/src/modals/RedirectionHostModal.tsx @@ -1,5 +1,4 @@ import cn from "clsx"; -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { Field, Form, Formik } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; @@ -15,6 +14,7 @@ import { } from "src/components"; import { useDirectorySuggestions, useRedirectionHost, useRedirectionHosts, useSetRedirectionHost } from "src/hooks"; import { T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { validateString } from "src/modules/Validations"; import { showObjectSuccess } from "src/notifications"; diff --git a/frontend/src/modals/RenewCertificateModal.tsx b/frontend/src/modals/RenewCertificateModal.tsx index 2488707a11..8d5966ebd9 100644 --- a/frontend/src/modals/RenewCertificateModal.tsx +++ b/frontend/src/modals/RenewCertificateModal.tsx @@ -1,5 +1,4 @@ import { useQueryClient } from "@tanstack/react-query"; -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { type ReactNode, useEffect, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; @@ -7,6 +6,7 @@ import { renewCertificate } from "src/api/backend"; import { Button, Loading } from "src/components"; import { useCertificate } from "src/hooks"; import { T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { showObjectSuccess } from "src/notifications"; interface Props extends InnerModalProps { diff --git a/frontend/src/modals/SetPasswordModal.tsx b/frontend/src/modals/SetPasswordModal.tsx index 8760d9e1f2..84e1698f63 100644 --- a/frontend/src/modals/SetPasswordModal.tsx +++ b/frontend/src/modals/SetPasswordModal.tsx @@ -1,4 +1,3 @@ -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { IconEye, IconEyeOff } from "@tabler/icons-react"; import { ErrorMessage, Field, Form, Formik } from "formik"; import { type ReactNode, useState } from "react"; @@ -7,6 +6,7 @@ import Modal from "react-bootstrap/Modal"; import { updateAuth } from "src/api/backend"; import { Button } from "src/components"; import { intl, T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { validateString } from "src/modules/Validations"; const showSetPasswordModal = (id: number) => { diff --git a/frontend/src/modals/StreamModal.tsx b/frontend/src/modals/StreamModal.tsx index 427a398231..692ad9463c 100644 --- a/frontend/src/modals/StreamModal.tsx +++ b/frontend/src/modals/StreamModal.tsx @@ -1,4 +1,3 @@ -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { Field, Form, Formik } from "formik"; import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; @@ -6,6 +5,7 @@ import Modal from "react-bootstrap/Modal"; import { Button, DirectoryField, Loading, NginxConfigField, SSLCertificateField } from "src/components"; import { useDirectorySuggestions, useSetStream, useStream, useStreams } from "src/hooks"; import { intl, T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { validateString } from "src/modules/Validations"; import { showObjectSuccess } from "src/notifications"; diff --git a/frontend/src/modals/TwoFactorModal.tsx b/frontend/src/modals/TwoFactorModal.tsx index 635d3e2bb7..0502dd7b66 100644 --- a/frontend/src/modals/TwoFactorModal.tsx +++ b/frontend/src/modals/TwoFactorModal.tsx @@ -1,4 +1,3 @@ -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { IconEye, IconEyeOff } from "@tabler/icons-react"; import { ErrorMessage, Field, Form, Formik } from "formik"; import { QRCodeSVG } from "qrcode.react"; @@ -8,6 +7,7 @@ import Modal from "react-bootstrap/Modal"; import { disable2FA, enable2FA, get2FAStatus, regenerateBackupCodes, start2FASetup } from "src/api/backend"; import { Button } from "src/components"; import { T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { validateString } from "src/modules/Validations"; type Step = "loading" | "status" | "setup" | "verify" | "backup" | "disable"; diff --git a/frontend/src/modals/UserModal.tsx b/frontend/src/modals/UserModal.tsx index 87286b618a..6089387148 100644 --- a/frontend/src/modals/UserModal.tsx +++ b/frontend/src/modals/UserModal.tsx @@ -1,11 +1,13 @@ -import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; +import { useQueryClient } from "@tanstack/react-query"; import { Field, Form, Formik } from "formik"; -import { useState } from "react"; +import { useRef, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; +import { deleteAvatar, uploadAvatar } from "src/api/backend"; import { Button, Loading } from "src/components"; import { useSetUser, useUser } from "src/hooks"; import { intl, T } from "src/locale"; +import EasyModal, { type InnerModalProps } from "src/modules/easyModal"; import { validateEmail, validateString } from "src/modules/Validations"; import { showObjectSuccess } from "src/notifications"; @@ -22,6 +24,19 @@ const UserModal = EasyModal.create(({ id, visible, remove }: Props) => { const { mutate: setUser } = useSetUser(); const [errorMsg, setErrorMsg] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); + const queryClient = useQueryClient(); + const fileInput = useRef(null); + + const changeAvatar = async (fn: () => Promise) => { + setErrorMsg(null); + try { + await fn(); + queryClient.invalidateQueries({ queryKey: ["user"] }); + queryClient.invalidateQueries({ queryKey: ["users"] }); + } catch (err: any) { + setErrorMsg(err.message); + } + }; const onSubmit = async (values: any, { setSubmitting }: any) => { if (isSubmitting) return; @@ -90,6 +105,39 @@ const UserModal = EasyModal.create(({ id, visible, remove }: Props) => { setErrorMsg(null)} dismissible> {errorMsg} + {id !== "new" && ( +
    + + { + const file = e.target.files?.[0]; + if (file) changeAvatar(() => uploadAvatar(id, file)); + e.target.value = ""; + }} + /> + + {data?.avatar?.startsWith("/images/avatar/") && ( + + )} +
    + )}
    diff --git a/frontend/src/modals/index.ts b/frontend/src/modals/index.ts index 3e89effdd3..12df45e425 100644 --- a/frontend/src/modals/index.ts +++ b/frontend/src/modals/index.ts @@ -9,8 +9,8 @@ export * from "./HelpModal"; export * from "./HTTPCertificateModal"; export * from "./PermissionsModal"; export * from "./ProxyHostModal"; -export * from "./RedirectionHostModal"; export * from "./ReachabilityModal"; +export * from "./RedirectionHostModal"; export * from "./RenewCertificateModal"; export * from "./SetPasswordModal"; export * from "./StreamModal"; diff --git a/frontend/src/modules/Validations.tsx b/frontend/src/modules/Validations.tsx index d707ad9913..66f30d324e 100644 --- a/frontend/src/modules/Validations.tsx +++ b/frontend/src/modules/Validations.tsx @@ -106,4 +106,4 @@ const validateUpstreamUrl = () => { }; }; -export { validateEmail, validateNumber, validateString, validateDomains, validateDomain, validateUpstreamUrl }; +export { validateDomain, validateDomains, validateEmail, validateNumber, validateString, validateUpstreamUrl }; diff --git a/frontend/src/notifications/Msg.tsx b/frontend/src/notifications/Msg.tsx index 629d662b61..7451047c22 100644 --- a/frontend/src/notifications/Msg.tsx +++ b/frontend/src/notifications/Msg.tsx @@ -33,4 +33,5 @@ function Msg({ data }: any) {
    ); } + export { Msg }; diff --git a/frontend/src/notifications/helpers.tsx b/frontend/src/notifications/helpers.tsx index 07517e49c0..89eabe7a1d 100644 --- a/frontend/src/notifications/helpers.tsx +++ b/frontend/src/notifications/helpers.tsx @@ -35,4 +35,4 @@ const showObjectSuccess = (obj: string, action: string) => { ); }; -export { showSuccess, showError, showObjectSuccess }; +export { showError, showObjectSuccess, showSuccess }; diff --git a/frontend/src/pages/Certificates/TableWrapper.tsx b/frontend/src/pages/Certificates/TableWrapper.tsx index 3e805ef8a8..426fe36afa 100644 --- a/frontend/src/pages/Certificates/TableWrapper.tsx +++ b/frontend/src/pages/Certificates/TableWrapper.tsx @@ -1,7 +1,7 @@ import { IconHelp, IconSearch } from "@tabler/icons-react"; import { useState } from "react"; import Alert from "react-bootstrap/Alert"; -import { deleteCertificate, downloadCertificate, type Certificate } from "src/api/backend"; +import { type Certificate, deleteCertificate, downloadCertificate } from "src/api/backend"; import { Button, HasPermission, LoadingPage } from "src/components"; import { useCertificates } from "src/hooks"; import { T } from "src/locale"; diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx index 5cb64867f0..2817af1f1c 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/Dashboard/index.tsx @@ -1,5 +1,5 @@ import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc } from "@tabler/icons-react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router"; import { HasPermission } from "src/components"; import { useHostReport } from "src/hooks"; import { T } from "src/locale"; diff --git a/frontend/src/pages/Login/index.tsx b/frontend/src/pages/Login/index.tsx index 704c442953..ddeaaf9911 100644 --- a/frontend/src/pages/Login/index.tsx +++ b/frontend/src/pages/Login/index.tsx @@ -241,14 +241,6 @@ function LoginForm() { export default function Login() { const { twoFactorChallenge } = useAuthState(); - const health = useHealth(); - - const getVersion = () => { - if (!health.data) { - return ""; - } - return health.data.version; - }; return ( @@ -263,7 +255,6 @@ export default function Login() {
    {twoFactorChallenge ? : }
    -
    {getVersion()}
    ); diff --git a/frontend/src/pages/Users/Table.tsx b/frontend/src/pages/Users/Table.tsx index 907309693e..e0f9932a1b 100644 --- a/frontend/src/pages/Users/Table.tsx +++ b/frontend/src/pages/Users/Table.tsx @@ -1,10 +1,11 @@ import { + IconDevicesX, IconDotsVertical, IconEdit, IconLock, - IconLogin2, IconPower, IconShield, + IconShieldOff, IconTrash, } from "@tabler/icons-react"; import { createColumnHelper, getCoreRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table"; @@ -29,10 +30,11 @@ interface Props { onEditUser?: (id: number) => void; onEditPermissions?: (id: number) => void; onSetPassword?: (id: number) => void; + onReset2FA?: (id: number) => void; + onRevokeSessions?: (id: number) => void; onDeleteUser?: (id: number) => void; onDisableToggle?: (id: number, enabled: boolean) => void; onNewUser?: () => void; - onLoginAs?: (id: number) => void; } export default function Table({ data, @@ -42,10 +44,11 @@ export default function Table({ onEditUser, onEditPermissions, onSetPassword, + onReset2FA, + onRevokeSessions, onDeleteUser, onDisableToggle, onNewUser, - onLoginAs, }: Props) { const columnHelper = createColumnHelper(); const columns = useMemo( @@ -160,6 +163,28 @@ export default function Table({ + { + e.preventDefault(); + onReset2FA?.(info.row.original.id); + }} + > + + + + { + e.preventDefault(); + onRevokeSessions?.(info.row.original.id); + }} + > + + + - {info.row.original.isDisabled ? ( -
    - - -
    - ) : ( - { - e.preventDefault(); - onLoginAs?.(info.row.original.id); - }} - > - - - - )}
    {error?.message || "Unknown error"}; } - const handleLoginAs = async (id: number) => { - try { - await loginAs(id); - } catch (err) { - if (err instanceof Error) { - showError(err.message); - } - } - }; - const handleDelete = async (id: number) => { await deleteUser(id); showObjectSuccess("user", "deleted"); @@ -48,6 +36,16 @@ export default function TableWrapper() { showObjectSuccess("user", enabled ? "enabled" : "disabled"); }; + const handleReset2FA = async (id: number) => { + await adminDisable2FA(id); + showObjectSuccess("user", "updated"); + }; + + const handleRevokeSessions = async (id: number) => { + await revokeSessions(id); + showObjectSuccess("user", "updated"); + }; + let filtered = null; if (search && data) { filtered = data?.filter((item) => { @@ -105,6 +103,22 @@ export default function TableWrapper() { onEditUser={(id: number) => showUserModal(id)} onEditPermissions={(id: number) => showPermissionsModal(id)} onSetPassword={(id: number) => showSetPasswordModal(id)} + onReset2FA={(id: number) => + showDeleteConfirmModal({ + tTitle: "user.reset-2fa", + children: , + onConfirm: () => handleReset2FA(id), + invalidations: [["users"], ["user", id]], + }) + } + onRevokeSessions={(id: number) => + showDeleteConfirmModal({ + tTitle: "user.revoke-sessions", + children: , + onConfirm: () => handleRevokeSessions(id), + invalidations: [["users"], ["user", id]], + }) + } onDeleteUser={(id: number) => showDeleteConfirmModal({ title: , @@ -115,7 +129,6 @@ export default function TableWrapper() { } onDisableToggle={handleDisableToggle} onNewUser={() => showUserModal("new")} - onLoginAs={handleLoginAs} />
    diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000000..4f758c09bd --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "esnext", + "lib": ["ESNext", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + + "strict": true, + "paths": { + "src/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index fcf0db0f5c..88a659fb81 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,46 +1,4 @@ { - "compilerOptions": { - "target": "ESNext", - "types": ["node", "vitest/globals"], - "useDefineForClassFields": true, - "lib": [ - "ESNext", - "DOM", - "DOM.Iterable" - ], - "types": [ - "node" - ], - "module": "ESNext", - "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "paths": { - "src/*": [ - "./src/*" - ], - "test/*": [ - "./test/*" - ] - } - }, - "include": [ - "src", - "test" - ], - "references": [ - { - "path": "./tsconfig.node.json" - } - ] + "files": [], + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] } diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json index eca66688d2..329f451bd0 100644 --- a/frontend/tsconfig.node.json +++ b/frontend/tsconfig.node.json @@ -1,10 +1,25 @@ { "compilerOptions": { - "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "esnext", + "lib": ["ESNext"], + "types": ["node"], "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + + "strict": true }, "include": ["vite.config.ts"] } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 5d1baf470d..9be85c3521 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,51 +1,17 @@ import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import "vitest/config"; -import { execFileSync } from "node:child_process"; -const runLocaleScripts = () => { - execFileSync("pnpm", ["formatjs", "compile-folder", "src/locale/src", "src/locale/lang"], { - stdio: "inherit", - }); - execFileSync("./src/locale/scripts/locale-sort.sh", { - stdio: "inherit", - }); -}; - -// https://vitejs.dev/config/ export default defineConfig({ - plugins: [ - { - name: "run-on-start", - configureServer(_server) { - runLocaleScripts(); - }, - }, - { - name: "trigger-on-reload", - configureServer(server) { - server.watcher.on("change", (file) => { - if (file.includes("locale/src")) { - console.log(`File changed: ${file}, running locale scripts...`); - runLocaleScripts(); - } - }); - }, - }, - react(), - ], + plugins: [react()], + define: { + global: "globalThis", + }, resolve: { tsconfigPaths: true, }, - server: { - host: true, - port: 5173, - strictPort: true, - allowedHosts: true, - }, + assetsInclude: ["**/*.md"], test: { environment: "happy-dom", - setupFiles: ["./vitest-setup.js"], }, - assetsInclude: ["**/*.md", "**/*.png", "**/*.svg"], }); diff --git a/patches/nginx-gso-fix.patch b/patches/nginx-gso-fix.patch deleted file mode 100644 index bdcf8a57eb..0000000000 --- a/patches/nginx-gso-fix.patch +++ /dev/null @@ -1,23 +0,0 @@ -diff --git a/src/event/quic/ngx_event_quic_output.c b/src/event/quic/ngx_event_quic_output.c -index f98c834a1..41cd5a6d6 100644 ---- a/src/event/quic/ngx_event_quic_output.c -+++ b/src/event/quic/ngx_event_quic_output.c -@@ -1356,7 +1356,17 @@ ngx_quic_frame_sendto(ngx_connection_t *c, ngx_quic_frame_t *frame, - - ctx->pnum++; - -- sent = ngx_quic_send(c, res.data, res.len, path->sockaddr, path->socklen); -+#if ((NGX_HAVE_UDP_SEGMENT) && (NGX_HAVE_MSGHDR_MSG_CONTROL)) -+ if (qc->conf->gso_enabled) { -+ sent = ngx_quic_send_segments(c, res.data, res.len, path->sockaddr, -+ path->socklen, res.len); -+ } else -+#endif -+ { -+ sent = ngx_quic_send(c, res.data, res.len, path->sockaddr, -+ path->socklen); -+ } -+ - if (sent < 0) { - ngx_quic_free_frame(c, frame); - return sent; diff --git a/patches/ngx_brotli.patch b/patches/ngx_brotli.patch deleted file mode 100644 index 4505f5b651..0000000000 --- a/patches/ngx_brotli.patch +++ /dev/null @@ -1,37 +0,0 @@ -diff --git a/filter/config b/filter/config -index 167e1d2..edecdc4 100644 ---- a/filter/config -+++ b/filter/config -@@ -42,31 +42,8 @@ fi - ngx_module_type=HTTP_FILTER - ngx_module_name=ngx_http_brotli_filter_module - --brotli="$ngx_addon_dir/deps/brotli/c" --if [ ! -f "$brotli/include/brotli/encode.h" ]; then --cat << END -- --$0: error: \ --Brotli library is missing from the $brotli directory. -- --Please make sure that the git submodule has been checked out: -- -- cd $ngx_addon_dir && git submodule update --init && cd $PWD -- --END -- exit 1 --fi -- --BROTLI_OUTPUT_DIRECTORY="$brotli/../out" --BROTLI_ENC_H="$brotli/include/brotli/encode.h \ -- $brotli/include/brotli/port.h \ -- $brotli/include/brotli/types.h" -- -- --ngx_module_incs="$brotli/include" --ngx_module_deps="$BROTLI_ENC_H" - ngx_module_srcs="$BROTLI_MODULE_SRC_DIR/ngx_http_brotli_filter_module.c" --ngx_module_libs="-L$BROTLI_OUTPUT_DIRECTORY -lbrotlienc -lbrotlicommon -lm" -+ngx_module_libs="-lbrotlienc -lbrotlicommon -lm" - ngx_module_order="$ngx_module_name \ - ngx_pagespeed \ - ngx_http_postpone_filter_module \ diff --git a/patches/ngx_unbrotli.patch b/patches/ngx_unbrotli.patch deleted file mode 100644 index 1a148b37b9..0000000000 --- a/patches/ngx_unbrotli.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/config b/config -index 94d8c50..dd60713 100644 ---- a/config -+++ b/config -@@ -1,9 +1,7 @@ --BROTLI_DIR=/usr/local/brotli - ngx_module_type=HTTP_FILTER - ngx_module_name=ngx_http_unbrotli_filter_module --ngx_module_incs="$BROTLI_DIR/include" - ngx_module_srcs="$ngx_addon_dir/ngx_http_unbrotli_filter_module.c" --ngx_module_libs="$BROTLI_DIR/lib/libbrotlidec-static.a $BROTLI_DIR/lib/libbrotlicommon-static.a" -+ngx_module_libs="-lbrotlidec -lbrotlicommon" - - . auto/module - diff --git a/patches/zstd-nginx-module.patch b/patches/zstd-nginx-module.patch deleted file mode 100644 index 4348f35b69..0000000000 --- a/patches/zstd-nginx-module.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/filter/config b/filter/config -index 2942125..2f6636f 100644 ---- a/filter/config -+++ b/filter/config -@@ -96,8 +96,6 @@ END - # TODO we need more tries for the different OS port. - fi - --NGX_LD_OPT="$ngx_zstd_opt_L $NGX_LD_OPT" -- - HTTP_ZSTD_SRCS="$ngx_addon_dir/filter/ngx_http_zstd_filter_module.c" - - ngx_addon_name=ngx_http_zstd_filter_module -@@ -105,7 +103,7 @@ ngx_module_type=HTTP_FILTER - ngx_module_name=ngx_http_zstd_filter_module - ngx_module_incs="$ngx_zstd_opt_I" - ngx_module_srcs=$HTTP_ZSTD_SRCS --ngx_module_libs=$NGX_LD_OPT -+ngx_module_libs=$ngx_zstd_opt_L - ngx_module_order="$ngx_module_name \ - ngx_pagespeed \ - ngx_http_postpone_filter_module \ diff --git a/rootfs/etc/dinit.d/backend b/rootfs/etc/dinit.d/backend new file mode 100644 index 0000000000..463cd5bef2 --- /dev/null +++ b/rootfs/etc/dinit.d/backend @@ -0,0 +1,6 @@ +type = process +command = index.js +term-signal = TERM +restart = yes +options = shares-console +working-dir = /app diff --git a/rootfs/etc/dinit.d/crond b/rootfs/etc/dinit.d/crond new file mode 100644 index 0000000000..576b432e1c --- /dev/null +++ b/rootfs/etc/dinit.d/crond @@ -0,0 +1,5 @@ +type = process +command = crond -f -d 7 -c /tmp/crontabs +term-signal = TERM +restart = yes +options = shares-console diff --git a/rootfs/etc/dinit.d/goaccess b/rootfs/etc/dinit.d/goaccess new file mode 100644 index 0000000000..fec4920b0c --- /dev/null +++ b/rootfs/etc/dinit.d/goaccess @@ -0,0 +1,5 @@ +type = process +command = goaccess --no-global-config --num-tests=0 --tz=$TZ --time-format=%H:%M:%S --date-format=%d/%b/%Y --log-format="[%d:%t %^] %v %h %T \"%r\" %s %b %b %R %u" --unix-socket=/run/goaccess.sock --log-file=/data/nginx/logs/access.log --real-time-html --output=/tmp/goa/index.html --db-path=/data/goaccess/data --restore --persist --browsers-file=/etc/goaccess/browsers.list --browsers-file=/etc/goaccess/podcast.list $/GOACLA +term-signal = INT +restart = yes +options = shares-console diff --git a/rootfs/etc/dinit.d/nginx b/rootfs/etc/dinit.d/nginx new file mode 100644 index 0000000000..5db78929f4 --- /dev/null +++ b/rootfs/etc/dinit.d/nginx @@ -0,0 +1,5 @@ +type = process +command = nginx -e stderr +term-signal = QUIT +restart = yes +options = shares-console diff --git a/rootfs/etc/dinit.d/php-fpm83 b/rootfs/etc/dinit.d/php-fpm83 new file mode 100644 index 0000000000..b08ca5e50e --- /dev/null +++ b/rootfs/etc/dinit.d/php-fpm83 @@ -0,0 +1,5 @@ +type = process +command = env PHP_INI_SCAN_DIR=/data/php/83/conf.d php-fpm83 -c /data/php/83 -y /data/php/83/php-fpm.conf -FOR +term-signal = QUIT +restart = yes +options = shares-console diff --git a/rootfs/etc/dinit.d/php-fpm84 b/rootfs/etc/dinit.d/php-fpm84 new file mode 100644 index 0000000000..efabd0ff0f --- /dev/null +++ b/rootfs/etc/dinit.d/php-fpm84 @@ -0,0 +1,5 @@ +type = process +command = env PHP_INI_SCAN_DIR=/data/php/84/conf.d php-fpm84 -c /data/php/84 -y /data/php/84/php-fpm.conf -FOR +term-signal = QUIT +restart = yes +options = shares-console diff --git a/rootfs/etc/dinit.d/php-fpm85 b/rootfs/etc/dinit.d/php-fpm85 new file mode 100644 index 0000000000..9d2efb8e0e --- /dev/null +++ b/rootfs/etc/dinit.d/php-fpm85 @@ -0,0 +1,5 @@ +type = process +command = env PHP_INI_SCAN_DIR=/data/php/85/conf.d php-fpm85 -c /data/php/85 -y /data/php/85/php-fpm.conf -FOR +term-signal = QUIT +restart = yes +options = shares-console diff --git a/rootfs/usr/local/bin/cron-ech.sh b/rootfs/usr/local/bin/cron-ech.sh new file mode 100755 index 0000000000..d15fa0474f --- /dev/null +++ b/rootfs/usr/local/bin/cron-ech.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh + +if [ -s /data/tls/ech/cron.sh ]; then + chmod +x /data/tls/ech/cron.sh + /data/tls/ech/cron.sh + sed -i "s|#ssl_ech_file|ssl_ech_file|g" /usr/local/nginx/conf/nginx.conf + nginx -s reload +elif grep -q '^[^#]*ssl_ech_file' /usr/local/nginx/conf/nginx.conf; then + sed -i "s|ssl_ech_file|#ssl_ech_file|g" /usr/local/nginx/conf/nginx.conf + nginx -s reload +fi diff --git a/rootfs/usr/local/bin/envs.sh b/rootfs/usr/local/bin/envs.sh index e6735f015a..6e2f18c1c2 100755 --- a/rootfs/usr/local/bin/envs.sh +++ b/rootfs/usr/local/bin/envs.sh @@ -49,8 +49,6 @@ if [ "$NC_AIO" = "true" ]; then sleep inf fi export DISABLE_HTTP="${DISABLE_HTTP:-true}" - export INITIAL_ADMIN_EMAIL="${INITIAL_ADMIN_EMAIL:-admin@example.org}" - export INITIAL_ADMIN_PASSWORD="${INITIAL_ADMIN_PASSWORD:-$(openssl rand -hex 32)}" fi @@ -131,7 +129,6 @@ export GOACLA="${GOACLA:-"--agent-list --real-os --double-decode --anonymize-ip export PHP83="${PHP83:-false}" export PHP84="${PHP84:-false}" export PHP85="${PHP85:-false}" -export INITIAL_DEFAULT_PAGE="${INITIAL_DEFAULT_PAGE:-congratulations}" export DISABLE_GRAVATAR="${DISABLE_GRAVATAR:-false}" export NGINX_LOAD_NJS_MODULE="${NGINX_LOAD_NJS_MODULE:-false}" export NGINX_LOAD_GEOIP2_MODULE="${NGINX_LOAD_GEOIP2_MODULE:-false}" @@ -140,6 +137,7 @@ export NGINX_LOAD_NTLM_MODULE="${NGINX_LOAD_NTLM_MODULE:-false}" export NGINX_LOAD_VHOST_TRAFFIC_STATUS_MODULE="${NGINX_LOAD_VHOST_TRAFFIC_STATUS_MODULE:-false}" export OIDC_REQUIRE_VERIFIED_EMAIL="${OIDC_REQUIRE_VERIFIED_EMAIL:-true}" export OIDC_DISABLE_PASSWORD="${OIDC_DISABLE_PASSWORD:-false}" +export OIDC_SKIP_MFA="${OIDC_SKIP_MFA:-false}" if [ -s /data/anubis/happy.webp ] && [ -s /data/anubis/reject.webp ] && [ -s /data/anubis/pensive.webp ]; then export AUTH_REQUEST_ANUBIS_USE_CUSTOM_IMAGES="${AUTH_REQUEST_ANUBIS_USE_CUSTOM_IMAGES:-true}" else @@ -681,14 +679,19 @@ if [ -n "$INITIAL_ADMIN_EMAIL" ] && ! echo "$INITIAL_ADMIN_EMAIL" | grep -q "@.* sleep inf fi +if [ -n "$INITIAL_DEFAULT_PAGE" ] && ! echo "$INITIAL_DEFAULT_PAGE" | grep -q "^\(404\|444\|redirect\|congratulations\|html\)$"; then + echo "INITIAL_DEFAULT_PAGE needs to be 404, 444, redirect, congratulations or html." + sleep inf +fi + if { [ -n "$INITIAL_ADMIN_EMAIL" ] || [ -n "$INITIAL_ADMIN_PASSWORD" ]; } && { [ -z "$INITIAL_ADMIN_EMAIL" ] || [ -z "$INITIAL_ADMIN_PASSWORD" ]; }; then echo "You need to set INITIAL_ADMIN_EMAIL and INITIAL_ADMIN_PASSWORD (both are needed) or none of them." sleep inf fi -if ! echo "$INITIAL_DEFAULT_PAGE" | grep -q "^\(404\|444\|redirect\|congratulations\|html\)$"; then - echo "INITIAL_DEFAULT_PAGE needs to be 404, 444, redirect, congratulations or html." - sleep inf +if [ -n "$INITIAL_ADMIN_EMAIL" ] || [ -n "$INITIAL_ADMIN_PASSWORD" ] || [ -n "$INITIAL_DEFAULT_PAGE" ]; then + echo "Remember to remove INITIAL_ envs after the first start." + sleep 3 fi @@ -744,12 +747,20 @@ if ! echo "$OIDC_DISABLE_PASSWORD" | grep -q "^true$\|^false$"; then sleep inf fi +if ! echo "$OIDC_SKIP_MFA" | grep -q "^true$\|^false$"; then + echo "OIDC_SKIP_MFA needs to be true or false." + sleep inf +fi + if { [ -n "$OIDC_REDIRECT_DOMAIN" ] || [ -n "$OIDC_ISSUER_URL" ] || [ -n "$OIDC_CLIENT_ID" ] || [ -n "$OIDC_CLIENT_SECRET" ]; } && { [ -z "$OIDC_REDIRECT_DOMAIN" ] || [ -z "$OIDC_ISSUER_URL" ] || [ -z "$OIDC_CLIENT_ID" ] || [ -z "$OIDC_CLIENT_SECRET" ]; }; then echo "You need to set OIDC_REDIRECT_DOMAIN, OIDC_ISSUER_URL, OIDC_CLIENT_ID and OIDC_CLIENT_SECRET (all are needed) or none of them." sleep inf elif [ "$OIDC_DISABLE_PASSWORD" = "true" ] && [ -z "$OIDC_REDIRECT_DOMAIN" ] && [ -z "$OIDC_ISSUER_URL" ] && [ -z "$OIDC_CLIENT_ID" ] && [ -z "$OIDC_CLIENT_SECRET" ]; then echo "You need to configure OIDC to enable OIDC_DISABLE_PASSWORD." sleep inf +elif [ "$OIDC_SKIP_MFA" = "true" ] && [ -z "$OIDC_REDIRECT_DOMAIN" ] && [ -z "$OIDC_ISSUER_URL" ] && [ -z "$OIDC_CLIENT_ID" ] && [ -z "$OIDC_CLIENT_SECRET" ]; then + echo "You need to configure OIDC to enable OIDC_SKIP_MFA." + sleep inf fi @@ -829,7 +840,7 @@ if [ "$GOA" = "true" ] && [ "$LOGROTATE" = "false" ]; then fi -export TV="18" +export TV="20" if [ ! -s /data/npmplus/env.sha512sum ] || [ "$(cat /data/npmplus/env.sha512sum)" != "$( (grep "env\.[A-Z0-9_]\+" -roh /app/templates | sed "s|env.||g" | sort | uniq | xargs printenv; echo "$TV") | tr -d "\n" | sha512sum | cut -d" " -f1)" ]; then echo "At least one env or the template version changed, all hosts will be regenerated. Please make sure to read the changelog." export REGENERATE_ALL="true" diff --git a/rootfs/usr/local/bin/launch.sh b/rootfs/usr/local/bin/launch.sh index 83be604117..d2e6b4a584 100755 --- a/rootfs/usr/local/bin/launch.sh +++ b/rootfs/usr/local/bin/launch.sh @@ -48,6 +48,14 @@ if [ "$CUSTOM_OCSP_STAPLING" = "true" ]; then echo fi +if [ -s /data/tls/ech/cron.sh ]; then + chmod +x /data/tls/ech/cron.sh + /data/tls/ech/cron.sh + sed -i "s|#ssl_ech_file|ssl_ech_file|g" /usr/local/nginx/conf/nginx.conf +elif grep -q '^[^#]*ssl_ech_file' /usr/local/nginx/conf/nginx.conf; then + sed -i "s|ssl_ech_file|#ssl_ech_file|g" /usr/local/nginx/conf/nginx.conf +fi + if ! nginx -tq; then sleep inf @@ -72,28 +80,17 @@ if [ "$PHP85" = "true" ]; then fi +echo "0 */$ECH_ROTATION_INTERVAL * * * cron-ech.sh" | tee "/tmp/crontabs/$(id -un)" +if [ "$LOGROTATE" = "true" ]; then + logrotate --state /data/nginx/logs/logrotate.state /etc/logrotate + echo "0 * * * * logrotate --state /data/nginx/logs/logrotate.state /etc/logrotate" | tee -a "/tmp/crontabs/$(id -un)" +fi + +set -- backend nginx crond +[ "$GOA" = "true" ] && set -- "$@" goaccess +[ "$PHP83" = "true" ] && set -- "$@" php-fpm83 +[ "$PHP84" = "true" ] && set -- "$@" php-fpm84 +[ "$PHP85" = "true" ] && set -- "$@" php-fpm85 + echo "Starting services..." -if [ "$PHP83" = "true" ]; then while true; do PHP_INI_SCAN_DIR=/data/php/83/conf.d php-fpm83 -c /data/php/83 -y /data/php/83/php-fpm.conf -FOR; done; fi & -if [ "$PHP84" = "true" ]; then while true; do PHP_INI_SCAN_DIR=/data/php/84/conf.d php-fpm84 -c /data/php/84 -y /data/php/84/php-fpm.conf -FOR; done; fi & -if [ "$PHP85" = "true" ]; then while true; do PHP_INI_SCAN_DIR=/data/php/85/conf.d php-fpm85 -c /data/php/85 -y /data/php/85/php-fpm.conf -FOR; done; fi & -if [ "$LOGROTATE" = "true" ]; then while true; do logrotate --verbose --state /data/nginx/logs/logrotate.state /etc/logrotate; sleep 25h; done; fi & -# shellcheck disable=SC2086 -if [ "$GOA" = "true" ]; then set -f; while true; do if [ -s /data/nginx/logs/access.log ]; then goaccess --no-global-config --num-tests=0 --tz="$TZ" --time-format="%H:%M:%S" \ - --date-format="%d/%b/%Y" --log-format='[%d:%t %^] %v %h %T "%r" %s %b %b %R %u' --unix-socket=/run/goaccess.sock --log-file=/data/nginx/logs/access.log \ - --real-time-html --output=/tmp/goa/index.html --db-path=/data/goaccess/data --restore --persist \ - --browsers-file=/etc/goaccess/browsers.list --browsers-file=/etc/goaccess/podcast.list $GOACLA; else sleep 10s; fi; done; fi & -while true; do - sleep 10s - if [ -s "/data/tls/ech/cron.sh" ]; then - chmod +x /data/tls/ech/cron.sh - /data/tls/ech/cron.sh - sed -i "s|#ssl_ech_file|ssl_ech_file|g" /usr/local/nginx/conf/nginx.conf - nginx -s reload - elif grep -q '^[^#]*ssl_ech_file' /usr/local/nginx/conf/nginx.conf; then - sed -i "s|ssl_ech_file|#ssl_ech_file|g" /usr/local/nginx/conf/nginx.conf - nginx -s reload - fi - sleep "$ECH_ROTATION_INTERVAL"h -done & -while true; do nginx -e stderr; done & -while true; do index.js; done +exec dinit -s "$@" diff --git a/rootfs/usr/local/bin/start.sh b/rootfs/usr/local/bin/start.sh index 719ac69989..bde2c4e6f3 100755 --- a/rootfs/usr/local/bin/start.sh +++ b/rootfs/usr/local/bin/start.sh @@ -114,10 +114,12 @@ fi mkdir -p /tmp/npmhome \ /tmp/goa \ + /tmp/crontabs \ /tmp/certbot-log \ /tmp/certbot-work \ /tmp/certbot-credentials -mkdir -vp /data/npmplus/gravatar \ +mkdir -vp /data/npmplus/avatar \ + /data/npmplus/gravatar \ /data/tls/certbot/renewal \ /data/tls/certbot/acme-challenge/.well-known/acme-challenge \ /data/tls/custom \ @@ -171,11 +173,6 @@ if [ -s /data/npmplus/database.sqlite ]; then fi -if [ -s /data/keys.json ]; then - mv -vn /data/keys.json /data/npmplus/keys.json -fi - - if [ -n "$(ls -A /data/nginx/default_www 2> /dev/null)" ]; then cp -van /data/nginx/default_www/* /data/html fi @@ -204,7 +201,7 @@ if [ -d /data/tls/certbot/live ] && [ -d /data/tls/certbot/archive ]; then while IFS= read -r cert do rm -vf "$cert" - ln -rs "$(find /data/tls/certbot/archive/"$(echo "$cert" | sed "s|/data/tls/certbot/live/\(npm-[0-9]\+/.*\).pem|\1|g")"*.pem | sort -V | tail -n1 | sed "s|/data/tls/certbot/|../../|g")" "$cert" + ln -rs "$(find /data/tls/certbot/archive/"$(echo "$cert" | sed "s|/data/tls/certbot/live/\(npm-[0-9]\+/.*\).pem|\1|g")"*.pem | sort -V | tail -n1)" "$cert" done < /tmp/certs.txt rm /tmp/certs.txt fi @@ -236,7 +233,9 @@ fi rm -vrf /data/letsencrypt-acme-challenge \ /data/nginx/default_host \ /data/nginx/temp \ - /data/logs + /data/logs \ + /data/keys.json \ + /data/npmplus/keys.json touch /data/tls/ech/cron.sh \ /data/html/index.html \ @@ -431,10 +430,6 @@ if [ "$NGINX_TRUST_RSA_PKCS1" = "true" ]; then sed -i "s|mldsa87:mldsa65:mldsa44:ed25519:ecdsa_secp521r1_sha512:ecdsa_secp384r1_sha384:ecdsa_secp256r1_sha256:rsa_pss_rsae_sha512:rsa_pss_rsae_sha384:rsa_pss_rsae_sha256;|mldsa87:mldsa65:mldsa44:ed25519:ecdsa_secp521r1_sha512:ecdsa_secp384r1_sha384:ecdsa_secp256r1_sha256:rsa_pss_rsae_sha512:rsa_pss_rsae_sha384:rsa_pss_rsae_sha256:rsa_pkcs1_sha512:rsa_pkcs1_sha384:rsa_pkcs1_sha256;|g" /usr/local/nginx/conf/nginx.conf fi -if [ ! -s "/data/tls/ech/cron.sh" ] && grep -q '^[^#]*ssl_ech_file' /usr/local/nginx/conf/nginx.conf; then - sed -i "s|ssl_ech_file|#ssl_ech_file|g" /usr/local/nginx/conf/nginx.conf -fi - if [ "$NGINX_LOAD_NJS_MODULE" = "true" ]; then sed -i "s|#\(load_module.\+js_module.so;\)|\1|g" /usr/local/nginx/conf/nginx.conf sed -i "s|#js_engine qjs;|js_engine qjs;|g" /usr/local/nginx/conf/nginx.conf diff --git a/rootfs/usr/local/nginx/conf/conf.d/crowdsec.conf.disabled b/rootfs/usr/local/nginx/conf/conf.d/crowdsec.conf.disabled index 33e3d6dacc..ac5a648c7c 100644 --- a/rootfs/usr/local/nginx/conf/conf.d/crowdsec.conf.disabled +++ b/rootfs/usr/local/nginx/conf/conf.d/crowdsec.conf.disabled @@ -1,7 +1,7 @@ lua_shared_dict crowdsec_cache 50m; init_by_lua_block { cs = require "crowdsec" - local ok, err = cs.init("/data/crowdsec/crowdsec.conf", "crowdsec-npmplus-bouncer/v1.1.6+placeholder") + local ok, err = cs.init("/data/crowdsec/crowdsec.conf", "crowdsec-npmplus-bouncer/v1.2.0+placeholder") if ok == nil then ngx.log(ngx.ERR, "[Crowdsec] " .. err) error() diff --git a/rootfs/usr/local/nginx/conf/conf.d/crowdsec.conf.original b/rootfs/usr/local/nginx/conf/conf.d/crowdsec.conf.original index 595768c0f4..ef48feb900 100644 --- a/rootfs/usr/local/nginx/conf/conf.d/crowdsec.conf.original +++ b/rootfs/usr/local/nginx/conf/conf.d/crowdsec.conf.original @@ -3,7 +3,7 @@ lua_shared_dict crowdsec_cache 50m; lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; init_by_lua_block { cs = require "crowdsec" - local ok, err = cs.init("/etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf", "crowdsec-nginx-bouncer/v1.1.6") + local ok, err = cs.init("/etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf", "crowdsec-nginx-bouncer/v1.2.0") if ok == nil then ngx.log(ngx.ERR, "[Crowdsec] " .. err) error() diff --git a/rootfs/usr/local/nginx/conf/conf.d/npmplus.conf b/rootfs/usr/local/nginx/conf/conf.d/npmplus.conf index b6bed75712..6c0c3186b5 100644 --- a/rootfs/usr/local/nginx/conf/conf.d/npmplus.conf +++ b/rootfs/usr/local/nginx/conf/conf.d/npmplus.conf @@ -38,12 +38,19 @@ server { more_set_headers "Permissions-Policy: accelerometer=(), ambient-light-sensor=(), aria-notify=(), attribution-reporting=(), autoplay=(), bluetooth=(), browsing-topics=(), camera=(), captured-surface-control=(), ch-ua-high-entropy-values=(), compute-pressure=(), cross-origin-isolated=(), deferred-fetch=(), deferred-fetch-minimal=(), display-capture=(), encrypted-media=(), fullscreen=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), identity-credentials-get=(), idle-detection=(), local-fonts=(), local-network=(), local-network-access=(), loopback-network=(), magnetometer=(), microphone=(), midi=(), on-device-speech-recognition=(), otp-credentials=(), payment=(), picture-in-picture=(), private-state-token-issuance=(), private-state-token-redemption=(), publickey-credentials-create=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), storage-access=(), summarizer=(), usb=(), web-share=(), window-management=(), xr-spatial-tracking=()"; location / { - more_set_headers "Content-Security-Policy: default-src 'none'; script-src-elem 'self' 'sha256-DO5PcuswBpDi7xOFPuj86dwr9VuamByHpvk2ZDmlAAA='; style-src-elem 'self' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=' 'sha256-AVTm08UMHPqpttgoudpSsvenKKfidtwuSnUVJLIuqcA='; connect-src 'self' data:; img-src 'self' data:; manifest-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;"; + more_set_headers "Content-Security-Policy: default-src 'none'; script-src-elem 'self'; style-src-elem 'self' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=' 'sha256-AVTm08UMHPqpttgoudpSsvenKKfidtwuSnUVJLIuqcA='; connect-src 'self' data:; img-src 'self' data:; manifest-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;"; root /app/frontend; try_files $uri /index.html; } - location /images/gravatar { + location /images/avatar/ { + auth_request /api/auth; + more_set_headers "Content-Security-Policy: default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;"; + alias /data/npmplus/avatar/; + } + + location /images/gravatar/ { + auth_request /api/auth; more_set_headers "Content-Security-Policy: default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;"; alias /data/npmplus/gravatar/; }