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 @@
-