From bcbb7454b49a78afcdedfbc98d60f358d88136d9 Mon Sep 17 00:00:00 2001 From: Adrien KISSIE Date: Fri, 19 Jan 2024 00:39:33 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20upgrade=20next=20to=20?= =?UTF-8?q?latest=20version=20(#134)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.mjs | 15 ++++--- package.json | 2 +- pnpm-lock.yaml | 106 ++++++++++++++++++++++-------------------------- 3 files changed, 57 insertions(+), 66 deletions(-) diff --git a/next.config.mjs b/next.config.mjs index 53c64e4a..9b467f8c 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -5,18 +5,17 @@ import "./src/env-config.mjs"; const nextConfig = { reactStrictMode: true, output: "standalone", + cacheHandler: + process.env.NODE_ENV === "production" + ? "./custom-incremental-cache-handler.mjs" + : undefined, + cacheMaxMemorySize: 0, experimental: { - isrMemoryCacheSize: 0, - taint: true, - incrementalCacheHandlerPath: - process.env.NODE_ENV === "production" - ? "./custom-incremental-cache-handler.mjs" - : undefined + taint: true }, logging: { fetches: { - // this is not yet supported by turbopack - // fullUrl: true + fullUrl: true } }, images: { diff --git a/package.json b/package.json index a05010d2..44460382 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "eslint-config-next": "14.0.0", "geist": "^1.0.0", "nanoid": "^4.0.2", - "next": "14.0.5-canary.38", + "next": "14.1.0", "nextjs-toploader": "^1.6.4", "nprogress": "^0.2.0", "pg": "^8.11.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1cf744e3..7474ee8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,7 +18,7 @@ dependencies: version: 3.0.0 '@neshca/cache-handler': specifier: ^0.6.2 - version: 0.6.2(next@14.0.5-canary.38)(redis@4.6.12) + version: 0.6.2(next@14.1.0)(redis@4.6.12) '@neshca/json-replacer-reviver': specifier: ^1.1.0 version: 1.1.0 @@ -72,16 +72,16 @@ dependencies: version: 14.0.0(eslint@8.48.0)(typescript@5.3.3) geist: specifier: ^1.0.0 - version: 1.2.0(next@14.0.5-canary.38) + version: 1.2.0(next@14.1.0) nanoid: specifier: ^4.0.2 version: 4.0.2 next: - specifier: 14.0.5-canary.38 - version: 14.0.5-canary.38(react-dom@18.2.0)(react@18.2.0) + specifier: 14.1.0 + version: 14.1.0(react-dom@18.2.0)(react@18.2.0) nextjs-toploader: specifier: ^1.6.4 - version: 1.6.4(next@14.0.5-canary.38)(react-dom@18.2.0)(react@18.2.0) + version: 1.6.4(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) nprogress: specifier: ^0.2.0 version: 0.2.0 @@ -609,7 +609,7 @@ packages: - supports-color dev: false - /@neshca/cache-handler@0.6.2(next@14.0.5-canary.38)(redis@4.6.12): + /@neshca/cache-handler@0.6.2(next@14.1.0)(redis@4.6.12): resolution: {integrity: sha512-tL0ykvUF+EFcVTqbxduCxkNAUxyAaTvhqFLD9vC1PJIhOjQuxF7AeQjBV3n1CzuJ81iYXQCOPdVGKb0z7fRcEw==} peerDependencies: next: '>=13.5.1' @@ -617,7 +617,7 @@ packages: dependencies: '@neshca/json-replacer-reviver': 1.1.0 lru-cache: 10.1.0 - next: 14.0.5-canary.38(react-dom@18.2.0)(react@18.2.0) + next: 14.1.0(react-dom@18.2.0)(react@18.2.0) redis: 4.6.12 dev: false @@ -625,8 +625,8 @@ packages: resolution: {integrity: sha512-2WU0fd15k+IAJdNB0yK4VxPbjcbwDKGLW9qfehm5u8Bavhg+QGphn4YBiKtO6yfnGK2b+ihmNK8laWIvL/l44w==} dev: false - /@next/env@14.0.5-canary.38: - resolution: {integrity: sha512-70/CkEFVhPiR0xcD2dXXRh/Ribgq0olJoqpt1E/ePaY6RC87/IY5b2lL+kLH9LMxmCHKwG83mijwWfDKojx0Fw==} + /@next/env@14.1.0: + resolution: {integrity: sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==} dev: false /@next/eslint-plugin-next@14.0.0: @@ -635,8 +635,8 @@ packages: glob: 7.1.7 dev: false - /@next/swc-darwin-arm64@14.0.5-canary.38: - resolution: {integrity: sha512-J8XHJD5TDSbLseiRSV2zQjz0GU3BDW1XcoanNEJJrkgmc7jV57dTUriqQQGe318byLyUIbst+T05IzrfYAL/Qg==} + /@next/swc-darwin-arm64@14.1.0: + resolution: {integrity: sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -644,8 +644,8 @@ packages: dev: false optional: true - /@next/swc-darwin-x64@14.0.5-canary.38: - resolution: {integrity: sha512-C4VTFFBpFTCiFFM7EUnwd+fzStQY80pqPi2FAz8cKWmEX3/ijbUUYRfG5Deh1O3qUcYYI22qcljWwOggQY25/g==} + /@next/swc-darwin-x64@14.1.0: + resolution: {integrity: sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -653,8 +653,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-gnu@14.0.5-canary.38: - resolution: {integrity: sha512-sKV8VnEXTZEPL2RWGemxt2HCEog4dqGdnAYGtCF9+E+8pWZeT/fowd+L0HjHY2FVJaAWRmPa9yCjtIC30c8RdQ==} + /@next/swc-linux-arm64-gnu@14.1.0: + resolution: {integrity: sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -662,8 +662,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-musl@14.0.5-canary.38: - resolution: {integrity: sha512-f5NdrgrFdcgCcg/D1u5cR4LAW6fsPFpInH228Z/KIPg1Lk8hoHkQSjmEFAxbPXKe3JA+23ogAMB7fwBxiDOgMw==} + /@next/swc-linux-arm64-musl@14.1.0: + resolution: {integrity: sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -671,8 +671,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-gnu@14.0.5-canary.38: - resolution: {integrity: sha512-fRGNTFOb9LHuvLGZFldktcIAPKsp1zsyjrn/QX0o542ibcrbvKeHLnh1QpoXQn9+bbIzPvx7NmPnKKJM4GAlMQ==} + /@next/swc-linux-x64-gnu@14.1.0: + resolution: {integrity: sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -680,8 +680,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-musl@14.0.5-canary.38: - resolution: {integrity: sha512-og+y5lCkRMSEcBVQfez7MrJPlMtcxDYzlF0omKUIpdV3ANB2529PeoFgRb8Xr+p/JWzjsDnN9J/XaDrJuXZKug==} + /@next/swc-linux-x64-musl@14.1.0: + resolution: {integrity: sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -689,8 +689,8 @@ packages: dev: false optional: true - /@next/swc-win32-arm64-msvc@14.0.5-canary.38: - resolution: {integrity: sha512-E3PNNDVlnRo0oIyFFMNqI02xQ1qzeluqUhzcGt7ZAKZm0nsnzTl+BWof6jP1eXiB8s2kDHDflKchAepK9u0fNA==} + /@next/swc-win32-arm64-msvc@14.1.0: + resolution: {integrity: sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -698,8 +698,8 @@ packages: dev: false optional: true - /@next/swc-win32-ia32-msvc@14.0.5-canary.38: - resolution: {integrity: sha512-t1ixIwdSXRsR0NtJr0iq+xES5awNCsFiOeQf+OHYTddiKOwtzrHP27A4xannTOSc6Twgiy5+29zOV3fVyyjFxQ==} + /@next/swc-win32-ia32-msvc@14.1.0: + resolution: {integrity: sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] @@ -707,8 +707,8 @@ packages: dev: false optional: true - /@next/swc-win32-x64-msvc@14.0.5-canary.38: - resolution: {integrity: sha512-XoMee1y41eoxLqmJbC6UHXSO11B2Tr3FA0z/m1n1QhKHWDV51pRD5exbb63A01Zm+f0mpw1PUBs2fSHvhHp6Ew==} + /@next/swc-win32-x64-msvc@14.1.0: + resolution: {integrity: sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2202,6 +2202,11 @@ packages: /caniuse-lite@1.0.30001572: resolution: {integrity: sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==} + dev: true + + /caniuse-lite@1.0.30001579: + resolution: {integrity: sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==} + dev: false /ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -3256,12 +3261,12 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: false - /geist@1.2.0(next@14.0.5-canary.38): + /geist@1.2.0(next@14.1.0): resolution: {integrity: sha512-RZsgCkGnSi1IV1Ozg3s6Ou4r/jzLff9+47ChjpJ5yX8ncEC/RwdStGwhdFzDcnSv0xU0+9J/fTX5Kht0NajTXA==} peerDependencies: next: ^13.2 || ^14 dependencies: - next: 14.0.5-canary.38(react-dom@18.2.0)(react@18.2.0) + next: 14.1.0(react-dom@18.2.0)(react@18.2.0) dev: false /gemoji@8.1.0: @@ -3320,10 +3325,6 @@ packages: dependencies: is-glob: 4.0.3 - /glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - dev: false - /glob@10.3.10: resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} engines: {node: '>=16 || 14 >=14.17'} @@ -4658,8 +4659,8 @@ packages: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} dev: true - /next@14.0.5-canary.38(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-KLIW3BvfPVO1ld7BTkeYv5FTn6IaiIehP3ar77IG/+ygLuFYWJGOwsCcjBljw0hUOwAE7SoUUR7y4v9XhtEbjQ==} + /next@14.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -4673,32 +4674,31 @@ packages: sass: optional: true dependencies: - '@next/env': 14.0.5-canary.38 + '@next/env': 14.1.0 '@swc/helpers': 0.5.2 busboy: 1.6.0 - caniuse-lite: 1.0.30001572 + caniuse-lite: 1.0.30001579 graceful-fs: 4.2.11 postcss: 8.4.31 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) styled-jsx: 5.1.1(react@18.2.0) - watchpack: 2.4.0 optionalDependencies: - '@next/swc-darwin-arm64': 14.0.5-canary.38 - '@next/swc-darwin-x64': 14.0.5-canary.38 - '@next/swc-linux-arm64-gnu': 14.0.5-canary.38 - '@next/swc-linux-arm64-musl': 14.0.5-canary.38 - '@next/swc-linux-x64-gnu': 14.0.5-canary.38 - '@next/swc-linux-x64-musl': 14.0.5-canary.38 - '@next/swc-win32-arm64-msvc': 14.0.5-canary.38 - '@next/swc-win32-ia32-msvc': 14.0.5-canary.38 - '@next/swc-win32-x64-msvc': 14.0.5-canary.38 + '@next/swc-darwin-arm64': 14.1.0 + '@next/swc-darwin-x64': 14.1.0 + '@next/swc-linux-arm64-gnu': 14.1.0 + '@next/swc-linux-arm64-musl': 14.1.0 + '@next/swc-linux-x64-gnu': 14.1.0 + '@next/swc-linux-x64-musl': 14.1.0 + '@next/swc-win32-arm64-msvc': 14.1.0 + '@next/swc-win32-ia32-msvc': 14.1.0 + '@next/swc-win32-x64-msvc': 14.1.0 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros dev: false - /nextjs-toploader@1.6.4(next@14.0.5-canary.38)(react-dom@18.2.0)(react@18.2.0): + /nextjs-toploader@1.6.4(next@14.1.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-KYLQ+0MvGdFk9JwOQfRtaYBAsyuX67Ca5QTa51RGNO4gQx64KLSE+ryHjUQ5LcDczHotp0l32GgksQW9vucUkw==} peerDependencies: next: '>= 6.0.0' @@ -4706,7 +4706,7 @@ packages: react-dom: '>= 16.0.0' dependencies: '@types/nprogress': 0.2.3 - next: 14.0.5-canary.38(react-dom@18.2.0)(react@18.2.0) + next: 14.1.0(react-dom@18.2.0)(react@18.2.0) nprogress: 0.2.0 prop-types: 15.8.1 react: 18.2.0 @@ -6335,14 +6335,6 @@ packages: vfile-message: 4.0.2 dev: false - /watchpack@2.4.0: - resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} - engines: {node: '>=10.13.0'} - dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - dev: false - /web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} dev: false From b4dc26bacc3a5f947fe1c6cdb9065e0097f49e9b Mon Sep 17 00:00:00 2001 From: Adrien KISSIE Date: Fri, 19 Jan 2024 08:13:39 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F=20make=20authenticati?= =?UTF-8?q?on=20work=20accross=20domains=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔒ī¸ make authentication work accross domains * â™ģī¸ share the key prefix accross domains * 🔧 set default formatter to biome * 🔧 remove webdis from docker and pass it as arg to build * â™ģī¸ rename shared key * 🔊 log fetchurl * 🔧 don't pass default variables for REDIS args * 🔧 allow for binding to host.docker.internal * 🚧 refactor * 🔇 remove webdis logs * 🔧 use external configured network to connect to webdis * 🩹 disable cache for now * 🩹 fix cache issues * 👷 remove build sha on ci on dev * Revert "🩹 fix cache issues" This reverts commit 7f73c7c9a3145a8dd9054bcee25d7c8a32a297b7. * Revert "🩹 disable cache for now" This reverts commit 3f65b1cac3079621d9eb231c272f99050ceedb2e. * Revert "Revert "🩹 disable cache for now"" This reverts commit 05db6905585fbc60a8a351460ea304bd5ab0aed0. * 🩹 disable cache on the homepage --- .github/workflows/deploy-with-docker-dev.yaml | 2 +- .vscode/settings.json | 3 + dc-build-local.sh | 2 +- docker/Dockerfile.dev | 8 +-- docker/Dockerfile.prod | 4 +- docker/docker-stack.dev.yaml | 9 +-- docker/docker-stack.prod.yaml | 9 +-- src/actions/auth.action.ts | 39 ++++++++----- .../[repository]/issues/[number]/page.tsx | 8 +-- src/app/(app)/[user]/[repository]/page.tsx | 18 +++--- src/app/api/auth/callback/route.ts | 31 ++++++++-- src/lib/server/kv/index.server.ts | 10 +++- src/lib/server/kv/webdis.server.mjs | 57 +++++++++++++++---- src/lib/shared/constants.ts | 1 + src/lib/shared/utils.shared.ts | 15 ----- 15 files changed, 141 insertions(+), 75 deletions(-) diff --git a/.github/workflows/deploy-with-docker-dev.yaml b/.github/workflows/deploy-with-docker-dev.yaml index 4affe382..89247db2 100644 --- a/.github/workflows/deploy-with-docker-dev.yaml +++ b/.github/workflows/deploy-with-docker-dev.yaml @@ -51,7 +51,7 @@ jobs: export DOCKER_BUILDKIT=1 # Use cache from remote repository, tag as latest, keep cache metadata - docker buildx build --push $BUILD_ARGS -f ./docker/Dockerfile.dev -t dcr.fredkiss.dev/gh-next:${GITHUB_SHA} -t dcr.fredkiss.dev/gh-next:dev . + docker buildx build --push $BUILD_ARGS -f ./docker/Dockerfile.dev -t dcr.fredkiss.dev/gh-next:dev . echo 'build successful ✅' diff --git a/.vscode/settings.json b/.vscode/settings.json index 00d460b4..97f67a7b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,8 @@ }, "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" } } diff --git a/dc-build-local.sh b/dc-build-local.sh index b837bca8..c242727d 100755 --- a/dc-build-local.sh +++ b/dc-build-local.sh @@ -2,4 +2,4 @@ while read -r line; do build_args="$build_args --build-arg $line" done < .env.docker.local echo args="'$build_args'" -docker buildx build --push -t dcr.fredkiss.dev/gh-next:latest -f docker/Dockerfile.dev $build_args --cache-from type=registry,ref=dcr.fredkiss.dev/gh-next:prod-buildcache,mode=max --cache-to type=registry,ref=dcr.fredkiss.dev/gh-next:prod-buildcache,mode=max . \ No newline at end of file +docker buildx build --push -t dcr.fredkiss.dev/gh-next:dev -f docker/Dockerfile.dev $build_args . \ No newline at end of file diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index 996aaff1..ff797314 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -29,8 +29,8 @@ ARG GITHUB_PERSONAL_ACCESS_TOKEN ARG KV_PREFIX ARG NEXT_PUBLIC_VERCEL_URL="localhost:3000" ARG REDIS_HTTP_URL="http://webdis:7379" -ARG REDIS_HTTP_USERNAME="user" -ARG REDIS_HTTP_PASSWORD="password" +ARG REDIS_HTTP_USERNAME +ARG REDIS_HTTP_PASSWORD ARG GITHUB_REDIRECT_URI="http://localhost:3000/api/auth/callback" ENV NEXT_PUBLIC_VERCEL_URL=$NEXT_PUBLIC_VERCEL_URL @@ -71,8 +71,8 @@ ARG GITHUB_PERSONAL_ACCESS_TOKEN ARG KV_PREFIX ARG NEXT_PUBLIC_VERCEL_URL="localhost:3000" ARG REDIS_HTTP_URL="http://webdis:7379" -ARG REDIS_HTTP_USERNAME="user" -ARG REDIS_HTTP_PASSWORD="password" +ARG REDIS_HTTP_USERNAME +ARG REDIS_HTTP_PASSWORD ARG GITHUB_REDIRECT_URI="http://localhost:3000/api/auth/callback" ENV NEXT_PUBLIC_VERCEL_URL=$NEXT_PUBLIC_VERCEL_URL diff --git a/docker/Dockerfile.prod b/docker/Dockerfile.prod index f5e041ef..1c6a3df2 100644 --- a/docker/Dockerfile.prod +++ b/docker/Dockerfile.prod @@ -72,8 +72,8 @@ ARG GITHUB_PERSONAL_ACCESS_TOKEN ARG KV_PREFIX ARG NEXT_PUBLIC_VERCEL_URL="localhost:3000" ARG REDIS_HTTP_URL="http://webdis:7379" -ARG REDIS_HTTP_USERNAME="user" -ARG REDIS_HTTP_PASSWORD="password" +ARG REDIS_HTTP_USERNAME +ARG REDIS_HTTP_PASSWORD ARG GITHUB_REDIRECT_URI="http://localhost:3000/api/auth/callback" ENV NEXT_PUBLIC_VERCEL_URL=$NEXT_PUBLIC_VERCEL_URL diff --git a/docker/docker-stack.dev.yaml b/docker/docker-stack.dev.yaml index 358022c8..9904b4fc 100644 --- a/docker/docker-stack.dev.yaml +++ b/docker/docker-stack.dev.yaml @@ -17,7 +17,8 @@ services: delay: 5s max_attempts: 3 window: 120s - webdis: - image: nicolas/webdis:latest - volumes: # mount volume containing the config file - - ../../gh/webdis.json:/etc/webdis.prod.json + networks: + - gh-next +networks: + gh-next: + external: true diff --git a/docker/docker-stack.prod.yaml b/docker/docker-stack.prod.yaml index 30b1c6f2..ab225984 100644 --- a/docker/docker-stack.prod.yaml +++ b/docker/docker-stack.prod.yaml @@ -17,7 +17,8 @@ services: delay: 5s max_attempts: 3 window: 120s - webdis: - image: nicolas/webdis:latest - volumes: # mount volume containing the config file - - ../../gh/webdis.json:/etc/webdis.prod.json + networks: + - gh-next +networks: + gh-next: + external: true diff --git a/src/actions/auth.action.ts b/src/actions/auth.action.ts index e056696b..a1c924e5 100644 --- a/src/actions/auth.action.ts +++ b/src/actions/auth.action.ts @@ -1,9 +1,9 @@ "use server"; import { cache } from "react"; -import { cookies } from "next/headers"; +import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; import { env } from "~/env"; -import { SESSION_COOKIE_KEY } from "~/lib/shared/constants"; +import { SESSION_COOKIE_KEY, SHARED_KEY_PREFIX } from "~/lib/shared/constants"; import { Session } from "~/lib/server/session.server"; import { getUserById, @@ -13,21 +13,39 @@ import { import { experimental_taintObjectReference as taintObjectReference } from "react"; import { revalidatePath } from "next/cache"; import { withAuth, type AuthState } from "./middlewares"; +import { nanoid } from "nanoid"; +import { kv } from "~/lib/server/kv/index.server"; export async function authenticateWithGithub(nextUrl: string | undefined) { const searchParams = new URLSearchParams(); - searchParams.append("client_id", env.GITHUB_CLIENT_ID); - searchParams.append("redirect_uri", env.GITHUB_REDIRECT_URI); + const origin = headers().get("Origin"); - // save the url to redirect after login in session - if (nextUrl) { + if (!origin) { const session = await getSession(); - await session.addAdditionnalData({ - nextUrl + session.addFlash({ + message: "Please login from the proper website", + type: "warning" }); + return revalidatePath("/login"); } + const stateParam = nanoid(24); + const FIVE_MINUTES = 5 * 60; + await kv.set( + stateParam, + { + nextUrl, + origin + }, + FIVE_MINUTES, + SHARED_KEY_PREFIX + ); + + searchParams.append("client_id", env.GITHUB_CLIENT_ID); + searchParams.append("redirect_uri", env.GITHUB_REDIRECT_URI); + searchParams.append("state", stateParam); + redirect( `https://github.com/login/oauth/authorize?${searchParams.toString()}` ); @@ -77,11 +95,6 @@ export async function loginUser(user: any) { message: "Logged in successfully." }); cookies().set(session.getCookie()); - - const data = (await session.popAdditionnalData()) as - | { nextUrl?: string } - | undefined; - return data?.nextUrl; } export const getSession = cache(async function getSession(): Promise { diff --git a/src/app/(app)/[user]/[repository]/issues/[number]/page.tsx b/src/app/(app)/[user]/[repository]/issues/[number]/page.tsx index 38c95a36..7bfd9dc3 100644 --- a/src/app/(app)/[user]/[repository]/issues/[number]/page.tsx +++ b/src/app/(app)/[user]/[repository]/issues/[number]/page.tsx @@ -81,7 +81,7 @@ export default async function IssueDetailPage({
- - - + > */} + + {/* */} {/* - - - + > */} + + {/* */} ); diff --git a/src/app/api/auth/callback/route.ts b/src/app/api/auth/callback/route.ts index a4fa8406..43075ae0 100644 --- a/src/app/api/auth/callback/route.ts +++ b/src/app/api/auth/callback/route.ts @@ -1,19 +1,33 @@ import { redirect } from "next/navigation"; import { loginUser } from "~/actions/auth.action"; import { env } from "~/env"; -import { isValidURLPathname } from "~/lib/shared/utils.shared"; +import { kv } from "~/lib/server/kv/index.server"; import type { NextRequest } from "next/server"; +import { SHARED_KEY_PREFIX } from "~/lib/shared/constants"; export const fetchCache = "force-no-store"; export const revalidate = 0; export async function GET(req: NextRequest) { const code = req.nextUrl.searchParams.get("code"); + const state = req.nextUrl.searchParams.get("state"); - if (!code) { + if (!state || !code) { redirect("/"); } + const stateData = await kv.get<{ + nextUrl: string | undefined; + origin: string; + }>(state, SHARED_KEY_PREFIX); + + // refuse auth request, it didn't originate from our server + if (!stateData) { + redirect("/"); + } + // delete state data to prevent this state from being reused again + await kv.delete(state, SHARED_KEY_PREFIX); + const response: any = await fetch( "https://github.com/login/oauth/access_token", { @@ -46,10 +60,17 @@ export async function GET(req: NextRequest) { } }).then((r) => r.json()); - const nextURL = await loginUser(githubUser); + await loginUser(githubUser); + + let url; + try { + url = new URL(stateData.nextUrl ?? "/", stateData.origin); + } catch (error) { + // pass + } - if (isValidURLPathname(nextURL)) { - return redirect(nextURL); + if (url) { + return redirect(url.toString()); } return redirect("/"); diff --git a/src/lib/server/kv/index.server.ts b/src/lib/server/kv/index.server.ts index e9086440..07203dee 100644 --- a/src/lib/server/kv/index.server.ts +++ b/src/lib/server/kv/index.server.ts @@ -5,10 +5,14 @@ export interface KVStore { set = {}>( key: string, value: T, - ttl_in_seconds?: number + ttl_in_seconds?: number, + key_prefix?: string ): Promise; - get = {}>(key: string): Promise; - delete(key: string): Promise; + get = {}>( + key: string, + key_prefix?: string + ): Promise; + delete(key: string, key_prefix?: string): Promise; } function getKV(): KVStore { diff --git a/src/lib/server/kv/webdis.server.mjs b/src/lib/server/kv/webdis.server.mjs index 62f446d6..206e6ae5 100644 --- a/src/lib/server/kv/webdis.server.mjs +++ b/src/lib/server/kv/webdis.server.mjs @@ -4,6 +4,9 @@ import { _envObject as env } from "../../../env-config.mjs"; /** * @typedef {import("./index.server").KVStore} KVStore */ +/** + * @typedef {"GET"|"SET"|"SETEX"|"DEL"|"HSET"|"HGETALL"|"SADD"|"SMEMBERS"|"SREM"} RedisCommand + */ /** * Represents a key-value store using Webdis. @@ -14,19 +17,21 @@ import { _envObject as env } from "../../../env-config.mjs"; export class WebdisKV { /** * Fetches data from the KV store. - * @param {("GET"|"SET"|"SETEX"|"DEL"|"HSET"|"HGETALL"|"SADD"|"SMEMBERS"|"SREM")} command The Redis command to execute. + * @param {RedisCommand|{ command: RedisCommand, key_prefix: string|undefined }} config The Redis command to execute. * @param {Array} args Arguments for the command. * @returns {Promise} The result of the fetch operation. */ - async #fetch(command, ...args) { - /** @type Array<(typeof command)> */ + async #fetch(config, ...args) { + /** @type Array */ const commandsWithSingleBody = ["SET", "HSET", "SETEX"]; + const command = typeof config === "string" ? config : config.command; + const key_prefix = typeof config === "string" ? null : config.key_prefix; const authString = `${env.REDIS_HTTP_USERNAME}:${env.REDIS_HTTP_PASSWORD}`; const [key, ...restArgs] = args; let body = null; - const urlParts = [env.KV_PREFIX + key, ...restArgs]; + const urlParts = [(key_prefix ?? env.KV_PREFIX) + key, ...restArgs]; const partsForTheURL = []; for (let i = 0; i < urlParts.length; i++) { @@ -102,16 +107,32 @@ export class WebdisKV { * @template T * @param {string} key The key under which to store the value. * @param {T} value The value to store. + * @param {string} [key_prefix] * @param {number} [ttl_in_seconds] Optional time-to-live in seconds. * @returns {Promise} */ - async set(key, value, ttl_in_seconds) { + async set(key, value, ttl_in_seconds, key_prefix) { const serializedValue = JSON.stringify(value); if (ttl_in_seconds) { - await this.#fetch("SETEX", key, ttl_in_seconds, serializedValue); + await this.#fetch( + { + command: "SETEX", + key_prefix + }, + key, + ttl_in_seconds, + serializedValue + ); } else { - await this.#fetch("SET", key, serializedValue); + await this.#fetch( + { + command: "SET", + key_prefix + }, + key, + serializedValue + ); } } @@ -119,20 +140,34 @@ export class WebdisKV { * Gets a value from the KV store. * @template T * @param {string} key + * @param {string} [key_prefix] * @returns {Promise} */ - async get(key) { - const value = await this.#fetch("GET", key); + async get(key, key_prefix) { + const value = await this.#fetch( + { + command: "GET", + key_prefix + }, + key + ); return value.GET ? JSON.parse(value.GET) : null; } /** * Deletes a key from the KV store. * @param {string} key The key to delete. + * @param {string} [key_prefix] * @returns {Promise} */ - async delete(key) { - await this.#fetch("DEL", key); + async delete(key, key_prefix) { + await this.#fetch( + { + command: "DEL", + key_prefix + }, + key + ); } /** diff --git a/src/lib/shared/constants.ts b/src/lib/shared/constants.ts index 81b98a46..5b59edb2 100644 --- a/src/lib/shared/constants.ts +++ b/src/lib/shared/constants.ts @@ -37,3 +37,4 @@ export const DEFAULT_ISSUE_SEARCH_QUERY = "is:open"; export const MAX_ITEMS_PER_PAGE = 25; export const UN_MATCHABLE_USERNAME = "<>"; +export const SHARED_KEY_PREFIX = "__gh_next__cache__shared_"; diff --git a/src/lib/shared/utils.shared.ts b/src/lib/shared/utils.shared.ts index f8f36895..1c410832 100644 --- a/src/lib/shared/utils.shared.ts +++ b/src/lib/shared/utils.shared.ts @@ -72,21 +72,6 @@ export function wait(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -/** - * Check if a URL is a valid pathname - * @param url - * @param base - * @returns - */ -export function isValidURLPathname(url: any): url is string { - try { - const _ = new URL(url, "http://localhost"); - return url.startsWith("/") && true; - } catch (_) { - return false; - } -} - /** * Adds a `/` at the end of a path if it does not already contains it * @param href From 7e9f32cdbc9dbc20aa9f71b74ccfaca8a0f4fdf7 Mon Sep 17 00:00:00 2001 From: Adrien KISSIE Date: Fri, 19 Jan 2024 17:40:38 +0100 Subject: [PATCH 3/4] Fix/cache issues (#136) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🩹 disable cache for now * 🩹 fix cache issues * đŸ”Ĩ remove unused cache import * â™ģī¸ ignore SSR for RSC rendering * â™ģī¸ Cache only operations and not the component --- globals.d.ts | 5 ++ .../[repository]/issues/[number]/page.tsx | 11 ++-- src/app/(app)/[user]/[repository]/page.tsx | 11 +--- src/components/cache.tsx | 21 ++++--- .../load-client-references.ts | 61 +++++++++++++++++++ .../render-rsc-to-string.ts | 4 +- .../rsc-client-renderer.tsx | 37 +++-------- .../custom-rsc-renderer/rsc-manifest.ts | 23 +------ src/components/markdown/markdown.tsx | 33 ++++++---- src/lib/server/rsc-utils.server.ts | 46 ++++++++++---- 10 files changed, 154 insertions(+), 98 deletions(-) create mode 100644 src/components/custom-rsc-renderer/load-client-references.ts diff --git a/globals.d.ts b/globals.d.ts index 98b99eda..4614f318 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -1,4 +1,5 @@ import * as ReactDOM from "react-dom"; +import * as React from "react"; declare global { namespace NodeJS { @@ -26,3 +27,7 @@ declare global { var __RSC_MANIFEST: Record | null; } + +declare module "react" { + export function unstable_postpone(reason?: string): never; +} diff --git a/src/app/(app)/[user]/[repository]/issues/[number]/page.tsx b/src/app/(app)/[user]/[repository]/issues/[number]/page.tsx index 7bfd9dc3..057cee66 100644 --- a/src/app/(app)/[user]/[repository]/issues/[number]/page.tsx +++ b/src/app/(app)/[user]/[repository]/issues/[number]/page.tsx @@ -3,7 +3,6 @@ import "server-only"; // components import { MarkdownTitle } from "~/components/markdown/markdown-title"; import { Markdown } from "~/components/markdown/markdown"; -import { Cache } from "~/components/cache"; // utils import { notFound } from "next/navigation"; @@ -81,17 +80,15 @@ export default async function IssueDetailPage({
- {/* */} - - {/* */} + content={issue.body} + /> {/* - {/* */} - {/* */} ); diff --git a/src/components/cache.tsx b/src/components/cache.tsx index 5a671f7c..73fe83e2 100644 --- a/src/components/cache.tsx +++ b/src/components/cache.tsx @@ -1,16 +1,12 @@ import { createCacheComponent } from "@rsc-cache/next"; -import fs from "fs/promises"; +import { + evaluateClientReferences, + getBuildId +} from "~/components/custom-rsc-renderer/load-client-references"; import { kv } from "~/lib/server/kv/index.server"; import { DEFAULT_CACHE_TTL } from "~/lib/shared/constants"; -import { lifetimeCache } from "~/lib/shared/lifetime-cache"; - -const getBuildId = lifetimeCache(async () => { - return process.env.NODE_ENV === "development" - ? new Date().getTime().toString() - : await fs.readFile(".next/BUILD_ID", "utf-8"); -}); -export const Cache = createCacheComponent({ +const NextRscCacheComponent = createCacheComponent({ async cacheFn(generatePayload, cacheKey, ttl) { let cachedPayload = await kv.get<{ rsc: string }>(cacheKey); const cacheHit = !!cachedPayload; @@ -33,3 +29,10 @@ export const Cache = createCacheComponent({ }, getBuildId }); + +export async function Cache( + props: React.ComponentProps +) { + await evaluateClientReferences(); + return ; +} diff --git a/src/components/custom-rsc-renderer/load-client-references.ts b/src/components/custom-rsc-renderer/load-client-references.ts new file mode 100644 index 00000000..3ce922ea --- /dev/null +++ b/src/components/custom-rsc-renderer/load-client-references.ts @@ -0,0 +1,61 @@ +import "server-only"; +import fs from "fs/promises"; +import path from "path"; +import { unstable_cache } from "next/cache"; +import { cache } from "react"; +import { lifetimeCache } from "~/lib/shared/lifetime-cache"; + +export const getBuildId = lifetimeCache(async () => { + return process.env.NODE_ENV === "development" + ? new Date().getTime().toString() + : await fs.readFile(".next/BUILD_ID", "utf-8"); +}); + +// Async function to recursively list files +async function listFilesRecursively(dir = ".next"): Promise { + let fileList: string[] = []; + const files = await fs.readdir(dir); + + for (const file of files) { + const filePath = path.join(dir, file); + const fileStats = await fs.stat(filePath); + + if (fileStats.isDirectory()) { + fileList = fileList.concat(await listFilesRecursively(filePath)); + } else if (file.endsWith("client-reference-manifest.js")) { + fileList.push(filePath); + } + } + return fileList; +} + +async function evaluateFile(filePath: string) { + try { + const fileContent = await fs.readFile(filePath, "utf8"); + eval(fileContent); + } catch (err) { + console.error(`Error evaluating client reference ${filePath}:`, err); + } +} + +export const evaluateClientReferences = cache( + async function evaluateClientReferences() { + const loadClientRefs = async () => { + await listFilesRecursively().then( + async (files) => await Promise.allSettled(files.map(evaluateFile)) + ); + return globalThis.__RSC_MANIFEST; + }; + + if (process.env.NODE_ENV === "development") { + globalThis.__RSC_MANIFEST ??= await loadClientRefs(); + } else { + const buildId = await getBuildId(); + const tags = [`__rsc_cache__client_manifest_evaluation_${buildId}`]; + const fn = unstable_cache(loadClientRefs, tags, { + tags + }); + globalThis.__RSC_MANIFEST ??= await fn(); + } + } +); diff --git a/src/components/custom-rsc-renderer/render-rsc-to-string.ts b/src/components/custom-rsc-renderer/render-rsc-to-string.ts index ee4c8610..7f1653e7 100644 --- a/src/components/custom-rsc-renderer/render-rsc-to-string.ts +++ b/src/components/custom-rsc-renderer/render-rsc-to-string.ts @@ -2,8 +2,10 @@ import "server-only"; import * as RSDW from "react-server-dom-webpack/server.edge"; import * as React from "react"; import { getClientManifest } from "./rsc-manifest"; +import { evaluateClientReferences } from "./load-client-references"; export async function renderRSCtoString(component: React.ReactNode) { + await evaluateClientReferences(); const rscPayload = RSDW.renderToReadableStream( component, // the client manifest is required for react to resolve @@ -11,7 +13,7 @@ export async function renderRSCtoString(component: React.ReactNode) { // they will be inlined into the RSC payload as references // React will use those references during SSR to resolve // the client components - getClientManifest() + await getClientManifest() ); return await transformStreamToString(rscPayload); } diff --git a/src/components/custom-rsc-renderer/rsc-client-renderer.tsx b/src/components/custom-rsc-renderer/rsc-client-renderer.tsx index 428d014f..d71ed34b 100644 --- a/src/components/custom-rsc-renderer/rsc-client-renderer.tsx +++ b/src/components/custom-rsc-renderer/rsc-client-renderer.tsx @@ -1,19 +1,18 @@ "use client"; import * as React from "react"; -import * as RSDWSSr from "react-server-dom-webpack/client.edge"; import * as RSDW from "react-server-dom-webpack/client"; - -import { getSSRManifest } from "./rsc-manifest"; +import { unstable_postpone as postpone } from "react"; export type RscClientRendererProps = { payloadOrPromise: string | Promise; - withSSR?: boolean; }; export function RscClientRenderer({ - payloadOrPromise, - withSSR = false + payloadOrPromise }: RscClientRendererProps) { + if (typeof window === "undefined") { + postpone("This component can only be used on the client"); + } const renderPromise = React.useMemo(() => { /** * This is to fix a bug that happens sometimes in the SSR phase, @@ -24,7 +23,7 @@ export function RscClientRenderer({ * these fields are used internally by `use` and are what's * prevent `use` from suspending indefinitely. */ - const pendingPromise = resolveElement(payloadOrPromise, withSSR) + const pendingPromise = resolveElement(payloadOrPromise) .then((value) => { // @ts-expect-error if (pendingPromise.status === "pending") { @@ -46,7 +45,7 @@ export function RscClientRenderer({ // @ts-expect-error pendingPromise.status = "pending"; return pendingPromise; - }, [payloadOrPromise, withSSR]); + }, [payloadOrPromise]); return ; } @@ -57,31 +56,13 @@ function RscClientRendererUse(props: { return React.use(props.promise); } -async function resolveElement( - payloadOrPromise: string | Promise, - ssr = false -) { - console.log("Render payload to JSX"); +async function resolveElement(payloadOrPromise: string | Promise) { const payload = typeof payloadOrPromise === "string" ? payloadOrPromise : await payloadOrPromise; const rscStream = transformStringToReadableStream(payload); - let rscPromise: Promise | null = null; - - // Render to HTML - if (ssr && typeof window === "undefined") { - // the SSR manifest contains all the client components that will be SSR'ed - // And also how to import them - rscPromise = RSDWSSr.createFromReadableStream(rscStream, getSSRManifest()); - } - - // Hydrate or CSR - if (rscPromise === null) { - rscPromise = RSDW.createFromReadableStream(rscStream, {}); - } - - return await rscPromise; + return await RSDW.createFromReadableStream(rscStream, {}); } export function transformStringToReadableStream(input: string) { diff --git a/src/components/custom-rsc-renderer/rsc-manifest.ts b/src/components/custom-rsc-renderer/rsc-manifest.ts index 1b8f44a4..844fc05d 100644 --- a/src/components/custom-rsc-renderer/rsc-manifest.ts +++ b/src/components/custom-rsc-renderer/rsc-manifest.ts @@ -1,4 +1,4 @@ -export function getClientManifest() { +export async function getClientManifest() { let clientManifest: ClientManifest = {}; // we concatennate all the manifest for all pages @@ -13,24 +13,3 @@ export function getClientManifest() { } return clientManifest; } -export function getSSRManifest() { - let rscManifest: RSCManifest = {}; - - // we concatennate all the manifest for all pages - if (globalThis.__RSC_MANIFEST) { - const allManifests = Object.values(globalThis.__RSC_MANIFEST); - for (const manifest of allManifests) { - rscManifest = { - ...rscManifest, - ...manifest - }; - } - } - - return { - ssrManifest: { - moduleLoading: rscManifest?.moduleLoading, - moduleMap: rscManifest?.ssrModuleMapping - } - }; -} diff --git a/src/components/markdown/markdown.tsx b/src/components/markdown/markdown.tsx index 64bbc4e6..205d2911 100644 --- a/src/components/markdown/markdown.tsx +++ b/src/components/markdown/markdown.tsx @@ -36,8 +36,10 @@ import { PRODUCTION_DOMAIN } from "~/lib/shared/constants"; import { getMultipleUserByUsername } from "~/models/user"; +import { ttlCache } from "~/lib/server/rsc-utils.server"; // types +import type { CacheId } from "~/lib/server/rsc-utils.server"; import type { UseMdxComponents } from "@mdx-js/mdx"; import type { IssueQueryResult } from "~/models/issues"; import type { UserQueryResult } from "~/models/user"; @@ -48,26 +50,34 @@ export type MarkdownProps = { className?: string; editableCheckboxes?: boolean; repository?: string; + cacheKey?: CacheId; + cacheTTL?: number; }; -export async function Markdown(props: MarkdownProps) { - return ; -} - -export async function MarkdownContent({ +export async function Markdown({ content, className, linkHeaders = false, editableCheckboxes = false, - repository: currentRepository = `${GITHUB_AUTHOR_USERNAME}/${GITHUB_REPOSITORY_NAME}` + repository: currentRepository = `${GITHUB_AUTHOR_USERNAME}/${GITHUB_REPOSITORY_NAME}`, + cacheKey, + cacheTTL }: MarkdownProps) { const dt = new Date().getTime(); console.time(`\n\x1b[34m[${dt}] \x1b[33m Markdown Rendering \x1b[37m`); - const { processedContent, references } = - await processMarkdownContentAndGetReferences(content, currentRepository); - const resolvedReferences = await resolveReferences(references); + const processFn = cacheKey + ? ttlCache(processMarkdownContentAndResolveReferences, { + id: cacheKey, + ttl: cacheTTL + }) + : processMarkdownContentAndResolveReferences; + + const { processedContent, resolvedReferences } = await processFn( + content, + currentRepository + ); const generatedMdxModule = await run(processedContent, { Fragment: React.Fragment, @@ -91,7 +101,7 @@ export async function MarkdownContent({ ); } -async function processMarkdownContentAndGetReferences( +async function processMarkdownContentAndResolveReferences( content: string, repository: string ) { @@ -146,8 +156,9 @@ async function processMarkdownContentAndGetReferences( format: "md" }); + const resolvedReferences = await resolveReferences(references); return { - references, + resolvedReferences, processedContent: String(processedContent) }; } diff --git a/src/lib/server/rsc-utils.server.ts b/src/lib/server/rsc-utils.server.ts index 763b5972..1a6d3a65 100644 --- a/src/lib/server/rsc-utils.server.ts +++ b/src/lib/server/rsc-utils.server.ts @@ -2,6 +2,8 @@ import "server-only"; import { unstable_cache } from "next/cache"; import { cache } from "react"; import { kv } from "~/lib/server/kv/index.server"; +import { env } from "~/env"; +import { DEFAULT_CACHE_TTL } from "~/lib/shared/constants"; type Callback = (...args: any[]) => Promise; export function nextCache( @@ -12,32 +14,54 @@ export function nextCache( } ) { if (process.env.NODE_ENV === "development") { - return cache(cacheForDev(cb, options)); + return cache( + ttlCache(cb, { + id: options.tags, + ttl: options.revalidate + }) + ); } return cache(unstable_cache(cb, options.tags, options)); } -/** - * This function is only used in `DEV` because fetch-cache is bypassed by nextjs on DEV - */ -function cacheForDev( +export type CacheId = string | number | (string | number)[]; +export function ttlCache( cb: T, options: { - tags: string[]; - revalidate?: number; + id: CacheId; + ttl?: number; + forceDev?: boolean; } ) { - return async (...args: Parameters) => { - const key = options.tags.join("-"); + return async (...args: Parameters): Promise>> => { + if (process.env.NODE_ENV === "development" && !options.forceDev) { + return await cb(...args); + } + + const { id, ttl = DEFAULT_CACHE_TTL } = options; + const key = + env.KV_PREFIX + (Array.isArray(id) ? id.join("-") : id.toString()); let cachedValue = await kv.get<{ - cached: ReturnType; + cached: Awaited>; }>(key); + const cacheHit = !!cachedValue?.cached; + if (!cachedValue?.cached) { cachedValue = { cached: await cb(...args) }; - await kv.set(key, cachedValue, options.revalidate); + await kv.set(key, cachedValue, ttl); + } + + if (cacheHit) { + console.log( + `\x1b[32mCACHE HIT \x1b[37mFOR key \x1b[90m"\x1b[33m${key}\x1b[90m"\x1b[37m` + ); + } else { + console.log( + `\x1b[31mCACHE MISS \x1b[37mFOR key \x1b[90m"\x1b[33m${key}\x1b[90m"\x1b[37m` + ); } return cachedValue.cached; }; From bf18297e8eb074b44dc0539ab7912d56aca9b65e Mon Sep 17 00:00:00 2001 From: Adrien KISSIE Date: Sun, 28 Jan 2024 22:34:45 +0100 Subject: [PATCH 4/4] Feat: Preview environments (#132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚧 wip: sablier is working with swarm (yay) * 👷 dev DB & PROD DB * 🔧 we should apply db migrations in dev also * 👷 make 2 replicas in prod * 🚧 CI change * 👷 update CI build system * 💚 escape env variables * 👷 error on exit in add* scripts * 💚 fix add-docker-app script * 💚 will this work ? * 💚 fix replacement command * 🔧 add caddy file * 🔧 modify caddy logfile * 👷 add dev caddy file * 🚧 WIP: dev certificates * 🔧 update caddy config * 🔧 setup on_demand tls * đŸ”Ĩ remove whoami on_demand_tls * 👷 rename domain prefix * 🔧 log pr requests * â™ģī¸ use `gh-` as the preview domain prefix instead of `pr-` * 👷 log branch slug * 💚 fix order in ci script * 🔧 disable buffering in caddyfile * 🔧 add caddy JSON LSP & autocomplete * đŸ”Ĩ remove unused deploy scripts * 🚧 wip * 🎉 start simple deploy script * đŸ”Ĩ remove caddyfile as it is not needed for now * 🚧 disable PR workflow for now * 🚧 wip * 🙈 ignore notes * 🔨 added new project for preview environment workflow setup * 👷 use bun on the CI * 💚 add missing flags to `bun run index.ts` * 👷 fix sablier URL and add better display name for the PR environment * đŸ”Ĩ cleanup unused files * 🚚 move `getSession` into session.action file * 🐛 effectively handle auth accross subdomains * â™ģī¸ redirect in login instead of revalidating * âĒ revert changes made to session * đŸ”Ĩ remove temp session hackery * 👷 update build system to show preview URL * 💚 use the correct env name * 💚 add write deployment permission on CI job * 👷 also report deployment on production * 💚 use https for env url * 💚 don't use dev in the workflow * 💚 use dev (?) env * 👷 remove deployment URL report * 👷 comment on PR the deployed url * 💚 add write permission on PR * 👷 use correct comment url value * 👷 don't pass unnecessary quotes * 🔊 log host+origin headers on login * 🔧 remove caddy JSON LSP * 🔧 remove unnecessary conf for unused files * đŸ”Ĩ remove caddy file * 👷 always reload docker stack * 👷 log exit error content --- .github/workflows/deploy-with-docker-dev.yaml | 45 +++-- .../workflows/deploy-with-docker-prod.yaml | 8 +- .github/workflows/docker-deploy-dev.old.yaml | 93 --------- .github/workflows/docker-deploy-prod.old.yaml | 95 ---------- .gitignore | 10 +- .vscode/settings.json | 11 +- docker/Dockerfile.dev | 4 +- docker/Dockerfile.prod | 2 + docker/docker-stack.dev.yaml | 24 --- docker/docker-stack.prod.yaml | 2 +- migrate.ts | 4 +- pr-preview-workflow/.gitignore | 178 ++++++++++++++++++ pr-preview-workflow/README.md | 15 ++ pr-preview-workflow/add-caddyfile.ts | 101 ++++++++++ pr-preview-workflow/add-docker-app.ts | 123 ++++++++++++ pr-preview-workflow/bun.lockb | Bin 0 -> 3851 bytes pr-preview-workflow/index.ts | 64 +++++++ pr-preview-workflow/package.json | 14 ++ pr-preview-workflow/tsconfig.json | 22 +++ src/actions/auth.action.ts | 32 +--- src/actions/middlewares.ts | 5 +- src/actions/session.action.ts | 27 +++ src/actions/theme.action.ts | 2 +- src/app/(app)/[user]/[repository]/page.tsx | 2 +- src/app/(app)/settings/sessions/[id]/page.tsx | 3 +- src/app/(app)/settings/sessions/page.tsx | 3 +- src/app/api/auth/callback/route.ts | 53 +++++- src/components/header/header.tsx | 2 +- src/components/toast/toaster.server.tsx | 2 +- tsconfig.json | 1 + 30 files changed, 673 insertions(+), 274 deletions(-) delete mode 100644 .github/workflows/docker-deploy-dev.old.yaml delete mode 100644 .github/workflows/docker-deploy-prod.old.yaml delete mode 100644 docker/docker-stack.dev.yaml create mode 100644 pr-preview-workflow/.gitignore create mode 100644 pr-preview-workflow/README.md create mode 100644 pr-preview-workflow/add-caddyfile.ts create mode 100644 pr-preview-workflow/add-docker-app.ts create mode 100755 pr-preview-workflow/bun.lockb create mode 100644 pr-preview-workflow/index.ts create mode 100644 pr-preview-workflow/package.json create mode 100644 pr-preview-workflow/tsconfig.json diff --git a/.github/workflows/deploy-with-docker-dev.yaml b/.github/workflows/deploy-with-docker-dev.yaml index 89247db2..72374ca0 100644 --- a/.github/workflows/deploy-with-docker-dev.yaml +++ b/.github/workflows/deploy-with-docker-dev.yaml @@ -12,6 +12,8 @@ on: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: build-push-docker: + permissions: + pull-requests: write runs-on: ubuntu-latest environment: dev @@ -26,8 +28,12 @@ jobs: known_hosts: ${{ secrets.KNOWN_HOSTS }} - name: Deploy to Server + id: deploy run: | - BUILD_ARGS="--build-arg NEXT_PUBLIC_VERCEL_URL=gh-dev.fredkiss.dev --build-arg GITHUB_REDIRECT_URI=https://gh-dev.fredkiss.dev/api/auth/callback --build-arg SESSION_SECRET=${{ secrets.SESSION_SECRET }} --build-arg DATABASE_URL=${{ secrets.POSTGRES_DB_URL }} --build-arg GITHUB_CLIENT_ID=${{ secrets.GH_CLIENT_ID }} --build-arg GITHUB_SECRET=${{ secrets.GH_SECRET }} --build-arg GITHUB_PERSONAL_ACCESS_TOKEN=${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} --build-arg REDIS_HTTP_USERNAME=${{ secrets.REDIS_HTTP_USERNAME }} --build-arg REDIS_HTTP_PASSWORD=${{ secrets.REDIS_HTTP_PASSWORD }} --build-arg KV_PREFIX=__gh_next__cache_dev_" + BUILD_ARGS="--build-arg NEXT_PUBLIC_VERCEL_URL=gh-${GITHUB_PR_NUMBER}.fredkiss.dev --build-arg GITHUB_REDIRECT_URI=https://gh.fredkiss.dev/api/auth/callback --build-arg SESSION_SECRET=${{ secrets.SESSION_SECRET }} --build-arg DATABASE_URL=${{ secrets.DEV_LOCAL_POSTGRES_DB_URL }} --build-arg REMOTE_DATABASE_URL=${{ secrets.DEV_REMOTE_POSTGRES_DB_URL }} --build-arg GITHUB_CLIENT_ID=${{ secrets.GH_CLIENT_ID }} --build-arg GITHUB_SECRET=${{ secrets.GH_SECRET }} --build-arg GITHUB_PERSONAL_ACCESS_TOKEN=${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} --build-arg REDIS_HTTP_USERNAME=${{ secrets.REDIS_HTTP_USERNAME }} --build-arg REDIS_HTTP_PASSWORD=${{ secrets.REDIS_HTTP_PASSWORD }} --build-arg KV_PREFIX=__gh_next__cache_dev_" + GIT_BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} + GITHUB_BRANCH_SLUG="${GIT_BRANCH//[^a-zA-Z0-9]/-}" + echo branch-slug=$GITHUB_BRANCH_SLUG ssh -p $DEPLOY_PORT $DEPLOY_USER@$DEPLOY_DOMAIN " source ~/.zshrc set -e -o errexit @@ -41,27 +47,38 @@ jobs: nvm use 20 echo Pulling latest version... - GIT_BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} - echo git branch=\$GIT_BRANCH - git fetch origin \$GIT_BRANCH - git checkout \$GIT_BRANCH - git pull origin \$GIT_BRANCH + echo git branch=$GIT_BRANCH + git fetch origin $GIT_BRANCH + git checkout $GIT_BRANCH + git pull origin $GIT_BRANCH echo 'Build with docker (and cache)...🔄' export DOCKER_BUILDKIT=1 - # Use cache from remote repository, tag as latest, keep cache metadata - docker buildx build --push $BUILD_ARGS -f ./docker/Dockerfile.dev -t dcr.fredkiss.dev/gh-next:dev . - - echo 'build successful ✅' + # Build & push docker image + docker buildx build --push ${BUILD_ARGS} -f ./docker/Dockerfile.dev -t dcr.fredkiss.dev/gh-next:pr-${GITHUB_PR_NUMBER} . - # Start docker instances - echo 'updating docker services...🔄' - docker stack deploy --with-registry-auth --compose-file ./docker/docker-stack.dev.yaml gh-stack-dev - echo 'services updated succesfully ✅' + echo 'Docker build successful ✅' + cd pr-preview-workflow + bun install --frozen-lockfile + bun run index.ts --pr-id ${GITHUB_PR_NUMBER} \ + --pr-branch ${GITHUB_BRANCH_SLUG} \ + --caddy-config-path ${{ secrets.CADDY_CONFIG_DIR }} \ + --reload-caddy --reload-docker " + echo "url=https://gh-${GITHUB_PR_NUMBER}.gh.fredkiss.dev, https://gh-${GITHUB_BRANCH_SLUG}.gh.fredkiss.dev" >> $GITHUB_OUTPUT env: DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} DEPLOY_DIR: ${{ secrets.DEPLOY_DIR }} DEPLOY_DOMAIN: ${{ secrets.DEPLOY_DOMAIN }} DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} + + - name: Comment PR with Deployed URL + uses: unsplash/comment-on-pr@v1.3.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + msg: 'Application deployed @: ${{ steps.deploy.outputs.url }}' + check_for_duplicate_msg: true + \ No newline at end of file diff --git a/.github/workflows/deploy-with-docker-prod.yaml b/.github/workflows/deploy-with-docker-prod.yaml index 3df1d6ec..20222502 100644 --- a/.github/workflows/deploy-with-docker-prod.yaml +++ b/.github/workflows/deploy-with-docker-prod.yaml @@ -29,7 +29,7 @@ jobs: - name: Deploy to Server run: | - BUILD_ARGS="--build-arg NEXT_PUBLIC_VERCEL_URL=gh.fredkiss.dev --build-arg GITHUB_REDIRECT_URI=https://gh.fredkiss.dev/api/auth/callback --build-arg SESSION_SECRET=${{ secrets.SESSION_SECRET }} --build-arg DATABASE_URL=${{ secrets.POSTGRES_DB_URL }} --build-arg GITHUB_CLIENT_ID=${{ secrets.GH_CLIENT_ID }} --build-arg GITHUB_SECRET=${{ secrets.GH_SECRET }} --build-arg GITHUB_PERSONAL_ACCESS_TOKEN=${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} --build-arg REDIS_HTTP_USERNAME=${{ secrets.REDIS_HTTP_USERNAME }} --build-arg REDIS_HTTP_PASSWORD=${{ secrets.REDIS_HTTP_PASSWORD }} --build-arg KV_PREFIX=__gh_next__cache_prod_" + BUILD_ARGS="--build-arg NEXT_PUBLIC_VERCEL_URL=gh.fredkiss.dev --build-arg GITHUB_REDIRECT_URI=https://gh.fredkiss.dev/api/auth/callback --build-arg SESSION_SECRET=${{ secrets.SESSION_SECRET }} --build-arg DATABASE_URL=${{ secrets.POSTGRES_DB_URL }} --build-arg REMOTE_DATABASE_URL=${{ secrets.REMOTE_POSTGRES_DB_URL }} --build-arg GITHUB_CLIENT_ID=${{ secrets.GH_CLIENT_ID }} --build-arg GITHUB_SECRET=${{ secrets.GH_SECRET }} --build-arg GITHUB_PERSONAL_ACCESS_TOKEN=${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} --build-arg REDIS_HTTP_USERNAME=${{ secrets.REDIS_HTTP_USERNAME }} --build-arg REDIS_HTTP_PASSWORD=${{ secrets.REDIS_HTTP_PASSWORD }} --build-arg KV_PREFIX=__gh_next__cache_prod_" ssh -p $DEPLOY_PORT $DEPLOY_USER@$DEPLOY_DOMAIN " source ~/.zshrc set -e -o errexit @@ -50,10 +50,8 @@ jobs: echo 'Build with docker (and cache)...🔄' export DOCKER_BUILDKIT=1 - # Use cache from remote repository, tag as latest, keep cache metadata - docker buildx build --push $BUILD_ARGS -f ./docker/Dockerfile.prod -t dcr.fredkiss.dev/gh-next:latest \ - --cache-from type=registry,ref=dcr.fredkiss.dev/gh-next:prod-buildcache,mode=max \ - --cache-to type=registry,ref=dcr.fredkiss.dev/gh-next:prod-buildcache,mode=max . + # Build & push docker image + docker buildx build --push $BUILD_ARGS -f ./docker/Dockerfile.prod -t dcr.fredkiss.dev/gh-next:latest . echo 'build successful ✅' diff --git a/.github/workflows/docker-deploy-dev.old.yaml b/.github/workflows/docker-deploy-dev.old.yaml deleted file mode 100644 index 93f8cacc..00000000 --- a/.github/workflows/docker-deploy-dev.old.yaml +++ /dev/null @@ -1,93 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: CI/CD For docker deploy - -# Controls when the action will run. -on: - # Triggers the workflow on push or pull request events but only for the main branch - # pull_request: - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - build-push-docker: - runs-on: ubuntu-latest - environment: dev - - steps: - - uses: actions/checkout@v2 - - - name: Get github branch name - id: get-github-ref - run: | - echo "github_ref=${{ github.head_ref }}" >> $GITHUB_OUTPUT - shell: bash - - - name: Cache build output - id: next-build-cache - uses: actions/cache@v3 - with: - path: .next - key: ${{ runner.os }}-next-build-cache-${{ steps.get-github-ref.outputs.github_ref }} - restore-keys: | - ${{ runner.os }}-next-build-cache-${{ steps.get-github-ref.outputs.github_ref }} - - - uses: whoan/docker-build-with-cache-action@v5 - with: - username: fredkiss3 - password: ${{ secrets.DCR_PASSWD }} - image_name: gh-next - image_tag: dev - push_git_tag: true - registry: dcr.fredkiss.dev - dockerfile: docker/Dockerfile.prod - context: . - build_extra_args: "--build-arg NEXT_PUBLIC_VERCEL_URL=gh-dev.fredkiss.dev --build-arg GITHUB_REDIRECT_URI=https://gh-dev.fredkiss.dev/api/auth/callback --build-arg SESSION_SECRET=${{ secrets.SESSION_SECRET }} --build-arg DATABASE_URL=${{ secrets.POSTGRES_DB_URL }} --build-arg GITHUB_CLIENT_ID=${{ secrets.GH_CLIENT_ID }} --build-arg GITHUB_SECRET=${{ secrets.GH_SECRET }} --build-arg GITHUB_PERSONAL_ACCESS_TOKEN=${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} --build-arg REDIS_HTTP_USERNAME=${{ secrets.REDIS_HTTP_USERNAME }} --build-arg REDIS_HTTP_PASSWORD=${{ secrets.REDIS_HTTP_PASSWORD }} --build-arg KV_PREFIX=__gh_next__cache_dev_" - - - name: copy build cache - run: | - ls -l ./.next/cache 2> /dev/null || true - echo ${{ secrets.DCR_PASSWD }} | docker login --username=fredkiss3 --password-stdin dcr.fredkiss.dev - docker pull dcr.fredkiss.dev/gh-next:dev - CONTAINER_ID=$(docker create dcr.fredkiss.dev/gh-next:dev) - mkdir -p .next/ - docker cp ${CONTAINER_ID}:/app/.next/cache .next/ - docker rm ${CONTAINER_ID} - ls -l ./.next/cache - - deploy: - # The type of runner that the job will run on - needs: - - build-push-docker - runs-on: ubuntu-latest - environment: dev - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - uses: actions/checkout@v2 - - - name: Install SSH key - uses: shimataro/ssh-key-action@v2 - with: - key: ${{ secrets.SSH_SERVER_KEY }} - name: id_rsa # optional - known_hosts: ${{ secrets.KNOWN_HOSTS }} - - - name: Restart docker-stack - run: | - scp -P $DEPLOY_PORT ./docker/docker-stack.dev.yaml $DEPLOY_USER@$DEPLOY_DOMAIN:$DCR_DEPLOY_DIR/docker-stack.dev.yaml - ssh -p $DEPLOY_PORT $DEPLOY_USER@$DEPLOY_DOMAIN " - cd $DCR_DEPLOY_DIR - echo updating docker services for dev environment... - echo ${{ secrets.DCR_PASSWD }} | docker login --username=fredkiss3 --password-stdin dcr.fredkiss.dev - docker stack deploy --with-registry-auth --compose-file ./docker-stack.dev.yaml gh-stack-dev - echo services updated succesfully ✅ - " - env: - DCR_DEPLOY_DIR: ${{ secrets.DCR_DEPLOY_DIR }} - DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} - DEPLOY_DOMAIN: ${{ secrets.DEPLOY_DOMAIN }} - DEPLOY_USER: ${{ secrets.DEPLOY_USER }} - DCR_PASSWD: ${{ secrets.DCR_PASSWD }} - DCR_USER: ${{ secrets.DCR_USER }} diff --git a/.github/workflows/docker-deploy-prod.old.yaml b/.github/workflows/docker-deploy-prod.old.yaml deleted file mode 100644 index 72684b93..00000000 --- a/.github/workflows/docker-deploy-prod.old.yaml +++ /dev/null @@ -1,95 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: CI/CD For docker deploy - -# Controls when the action will run. -on: - # Triggers the workflow on push or pull request events but only for the main branch - # push: - # branches: - # - main - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - build-push-docker: - runs-on: ubuntu-latest - environment: production - - steps: - - uses: actions/checkout@v2 - - - name: Get github branch name - id: get-github-ref - run: | - echo "github_ref=${{ github.head_ref }}" >> $GITHUB_OUTPUT - shell: bash - - - name: Cache build output - id: next-build-cache - uses: actions/cache@v3 - with: - path: .next - key: ${{ runner.os }}-next-build-cache-${{ steps.get-github-ref.outputs.github_ref }} - restore-keys: | - ${{ runner.os }}-next-build-cache-${{ steps.get-github-ref.outputs.github_ref }} - - - uses: whoan/docker-build-with-cache-action@v5 - with: - username: fredkiss3 - password: ${{ secrets.DCR_PASSWD }} - image_name: gh-next - image_tag: latest - push_git_tag: true - registry: dcr.fredkiss.dev - dockerfile: docker/Dockerfile.prod - context: . - build_extra_args: "--build-arg NEXT_PUBLIC_VERCEL_URL=gh.fredkiss.dev --build-arg GITHUB_REDIRECT_URI=https://gh.fredkiss.dev/api/auth/callback --build-arg SESSION_SECRET=${{ secrets.SESSION_SECRET }} --build-arg DATABASE_URL=${{ secrets.POSTGRES_DB_URL }} --build-arg GITHUB_CLIENT_ID=${{ secrets.GH_CLIENT_ID }} --build-arg GITHUB_SECRET=${{ secrets.GH_SECRET }} --build-arg GITHUB_PERSONAL_ACCESS_TOKEN=${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} --build-arg REDIS_HTTP_USERNAME=${{ secrets.REDIS_HTTP_USERNAME }} --build-arg REDIS_HTTP_PASSWORD=${{ secrets.REDIS_HTTP_PASSWORD }} --build-arg KV_PREFIX=__gh_next__cache_prod_" - - - name: copy build cache - run: | - ls -l ./.next/cache 2> /dev/null || true - echo ${{ secrets.DCR_PASSWD }} | docker login --username=fredkiss3 --password-stdin dcr.fredkiss.dev - docker pull dcr.fredkiss.dev/gh-next:latest - CONTAINER_ID=$(docker create dcr.fredkiss.dev/gh-next:latest) - mkdir -p .next/ - docker cp ${CONTAINER_ID}:/app/.next/cache .next/ - docker rm ${CONTAINER_ID} - ls -l ./.next/cache - - deploy: - # The type of runner that the job will run on - needs: - - build-push-docker - runs-on: ubuntu-latest - environment: dev - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - uses: actions/checkout@v2 - - - name: Install SSH key - uses: shimataro/ssh-key-action@v2 - with: - key: ${{ secrets.SSH_SERVER_KEY }} - name: id_rsa # optional - known_hosts: ${{ secrets.KNOWN_HOSTS }} - - - name: Restart docker-stack - run: | - scp -P $DEPLOY_PORT ./docker/docker-stack.prod.yaml $DEPLOY_USER@$DEPLOY_DOMAIN:$DCR_DEPLOY_DIR/docker-stack.prod.yaml - ssh -p $DEPLOY_PORT $DEPLOY_USER@$DEPLOY_DOMAIN " - cd $DCR_DEPLOY_DIR - echo updating docker services for dev environment... - echo ${{ secrets.DCR_PASSWD }} | docker login --username=fredkiss3 --password-stdin dcr.fredkiss.dev - docker stack deploy --with-registry-auth --compose-file ./docker-stack.prod.yaml gh-stack-prod - echo services updated succesfully ✅ - " - env: - DCR_DEPLOY_DIR: ${{ secrets.DCR_DEPLOY_DIR }} - DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} - DEPLOY_DOMAIN: ${{ secrets.DEPLOY_DOMAIN }} - DEPLOY_USER: ${{ secrets.DEPLOY_USER }} - DCR_PASSWD: ${{ secrets.DCR_PASSWD }} - DCR_USER: ${{ secrets.DCR_USER }} diff --git a/.gitignore b/.gitignore index fd994246..a33a1627 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,12 @@ next-env.d.ts # cloudfare pages .wrangler/ /cache/ -.idea/ \ No newline at end of file +.idea/ + +# Docker +docker-stack.pr.yaml +caddyfile.pr +pr.caddyfile +*.bak +*.log +notes.md \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 97f67a7b..c86052a0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,14 +1,17 @@ { - "typescript.preferences.importModuleSpecifier": "non-relative", - "typescript.tsdk": "node_modules/typescript/lib", - "editor.defaultFormatter": "biomejs.biome", + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, "[typescript]": { "editor.defaultFormatter": "biomejs.biome" }, "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" }, - "[javascript]": { + "editor.defaultFormatter": "biomejs.biome", + "typescript.preferences.importModuleSpecifier": "non-relative", + "typescript.tsdk": "node_modules/typescript/lib", + "[json]": { "editor.defaultFormatter": "biomejs.biome" } } diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index ff797314..0408c4b1 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -21,6 +21,7 @@ RUN \ FROM node:20-alpine3.19 AS builder +ARG REMOTE_DATABASE_URL ARG SESSION_SECRET ARG DATABASE_URL ARG GITHUB_CLIENT_ID @@ -33,6 +34,7 @@ ARG REDIS_HTTP_USERNAME ARG REDIS_HTTP_PASSWORD ARG GITHUB_REDIRECT_URI="http://localhost:3000/api/auth/callback" +ENV REMOTE_DATABASE_URL=$REMOTE_DATABASE_URL ENV NEXT_PUBLIC_VERCEL_URL=$NEXT_PUBLIC_VERCEL_URL ENV SESSION_SECRET=$SESSION_SECRET ENV DATABASE_URL=$DATABASE_URL @@ -54,7 +56,7 @@ ENV NEXT_TELEMETRY_DISABLED 1 RUN \ if [ -f yarn.lock ]; then yarn build; \ elif [ -f package-lock.json ]; then npm run build; \ - elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm run build; \ + elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm run db:migrate-docker && pnpm run build; \ else echo "Lockfile not found." && exit 1; \ fi diff --git a/docker/Dockerfile.prod b/docker/Dockerfile.prod index 1c6a3df2..677ad73f 100644 --- a/docker/Dockerfile.prod +++ b/docker/Dockerfile.prod @@ -23,6 +23,7 @@ FROM node:20-alpine3.19 AS builder ARG SESSION_SECRET ARG DATABASE_URL +ARG REMOTE_DATABASE_URL ARG GITHUB_CLIENT_ID ARG GITHUB_SECRET ARG GITHUB_PERSONAL_ACCESS_TOKEN @@ -33,6 +34,7 @@ ARG REDIS_HTTP_URL="http://webdis:7379" ARG NEXT_PUBLIC_VERCEL_URL="gh.fredkiss.dev" ARG GITHUB_REDIRECT_URI="https://gh.fredkiss.dev/api/auth/callback" +ENV REMOTE_DATABASE_URL=$REMOTE_DATABASE_URL ENV NEXT_PUBLIC_VERCEL_URL=$NEXT_PUBLIC_VERCEL_URL ENV SESSION_SECRET=$SESSION_SECRET ENV DATABASE_URL=$DATABASE_URL diff --git a/docker/docker-stack.dev.yaml b/docker/docker-stack.dev.yaml deleted file mode 100644 index 9904b4fc..00000000 --- a/docker/docker-stack.dev.yaml +++ /dev/null @@ -1,24 +0,0 @@ -version: "3.4" - -services: - app: - image: dcr.fredkiss.dev/gh-next:dev - ports: - - "8989:3000" - deploy: - replicas: 1 - update_config: - parallelism: 1 - delay: 5s - order: start-first - failure_action: rollback - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - networks: - - gh-next -networks: - gh-next: - external: true diff --git a/docker/docker-stack.prod.yaml b/docker/docker-stack.prod.yaml index ab225984..b187258e 100644 --- a/docker/docker-stack.prod.yaml +++ b/docker/docker-stack.prod.yaml @@ -6,7 +6,7 @@ services: ports: - "8988:3000" deploy: - replicas: 1 + replicas: 2 update_config: parallelism: 1 delay: 5s diff --git a/migrate.ts b/migrate.ts index b616fa49..1b013bf1 100644 --- a/migrate.ts +++ b/migrate.ts @@ -2,7 +2,9 @@ import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; import { drizzle } from "drizzle-orm/postgres-js"; -const db = drizzle(postgres(process.env.DATABASE_URL!)); +const db = drizzle( + postgres(process.env.REMOTE_DATABASE_URL ?? process.env.DATABASE_URL!) +); async function main() { await migrate(db, { migrationsFolder: "drizzle" }); diff --git a/pr-preview-workflow/.gitignore b/pr-preview-workflow/.gitignore new file mode 100644 index 00000000..725f68bf --- /dev/null +++ b/pr-preview-workflow/.gitignore @@ -0,0 +1,178 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +caddy-test/ +docker/ \ No newline at end of file diff --git a/pr-preview-workflow/README.md b/pr-preview-workflow/README.md new file mode 100644 index 00000000..4c7d4d3a --- /dev/null +++ b/pr-preview-workflow/README.md @@ -0,0 +1,15 @@ +# preview-workflow + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.0.25. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/pr-preview-workflow/add-caddyfile.ts b/pr-preview-workflow/add-caddyfile.ts new file mode 100644 index 00000000..3e1900fb --- /dev/null +++ b/pr-preview-workflow/add-caddyfile.ts @@ -0,0 +1,101 @@ +import { $, file, write } from "bun"; + +export async function addCaddyfile( + PR_ID: number, + PR_BRANCH: string, + CADDY_CONFIG_FOLDER_PATH: string, + shouldReloadCaddy: boolean, + MAX_CONFIG_NUMBER: number, + STARTING_PORT_RANGE: number +) { + await $`echo '[🔄 Caddy] adding preview environment config...'`; + const caddyfileToAdd = file( + `${pathWithoutSlash(CADDY_CONFIG_FOLDER_PATH)}/pull-request-${PR_ID}.caddy` + ); + + if (await caddyfileToAdd.exists()) { + await $`echo '[ℹī¸ Caddy] Configuration for preview branch pull request #${PR_ID} already exists, skipping work.'`; + return; + } + + const files = + $`ls ${CADDY_CONFIG_FOLDER_PATH} | grep -E '^pull-request\-[0-9]+\.caddy$'`.lines(); + + let caddyPRFiles = await Array.fromAsync(files).then((arr) => + arr + .filter(Boolean) + // sort from the lowest pr number to the highest + .sort((a, b) => { + // @ts-expect-error + const numberA = parseInt(a.match(/pull-request-(\d+)\.caddy/)[1], 10); + // @ts-expect-error + const numberB = parseInt(b.match(/pull-request-(\d+)\.caddy/)[1], 10); + return numberA - numberB; + }) + ); + + // don't go over 100 config max + if (caddyPRFiles.length === MAX_CONFIG_NUMBER) { + const [leastRecentPullRequestFile, ...rest] = caddyPRFiles; + + const leastRecentPullRequestID = parseInt( + // @ts-expect-error + leastRecentPullRequestFile.match(/pull-request-(\d+)\.caddy/)[1], + 10 + ); + + await $`echo '[🔄 Caddy] preview environment config for pull request #${leastRecentPullRequestID} is too old, deleting it...'`; + await $`echo '[ℹī¸ Caddy] you can still redeploy this env by deploying the associated pull request'`; + + // delete the least recent Pull Request + await $`rm '${pathWithoutSlash( + CADDY_CONFIG_FOLDER_PATH + )}/${leastRecentPullRequestFile}'`; + + await $`echo '[✅ Caddy] preview environment config for pull request #${leastRecentPullRequestID} deleted'`; + + caddyPRFiles = rest; + } + + const portNumber = STARTING_PORT_RANGE + caddyPRFiles.length; + const CADDY_TEMPLATE_CONTENT = `gh-${PR_ID}.gh.fredkiss.dev, gh-${PR_BRANCH}.gh.fredkiss.dev { + route { + sablier http://localhost:10000 { + group gh-next-${PR_ID} + session_duration 30m + dynamic { + theme ghost + display_name preview environment ${PR_BRANCH} (pull request: ${PR_ID}) + refresh_frequency 5s + } + } + reverse_proxy 127.0.0.1:${portNumber} { + header_up Host {http.request.host} + # disables buffering + flush_interval -1 + } + } + log +}`; + + await write(caddyfileToAdd, CADDY_TEMPLATE_CONTENT); + await $`echo '[✅ caddy] config for pull request #${PR_ID} added successfully'`; + if (shouldReloadCaddy) { + // reload caddy service in docker + await $`echo '[🔄 Caddy] reloading caddy server...'`; + const { exitCode, stderr } = + await $`docker exec $(docker ps -q -f name=caddy-stack_proxy) caddy reload -c /etc/caddy/Caddyfile`; + if (exitCode !== 0) { + await $`echo '[❌ Caddy] caddy service encountered an unexpected error : ${stderr.toString()}'`; + process.exit(1); + } + await $`echo '[✅ Caddy] caddy server reloaded succesfully'`; + } +} + +function pathWithoutSlash(path: string) { + if (path.endsWith("/")) { + return path.substring(0, path.length - 1); + } + return path; +} diff --git a/pr-preview-workflow/add-docker-app.ts b/pr-preview-workflow/add-docker-app.ts new file mode 100644 index 00000000..6abff0ae --- /dev/null +++ b/pr-preview-workflow/add-docker-app.ts @@ -0,0 +1,123 @@ +import { $, file, write } from "bun"; + +export async function addDockerApp( + PR_ID: number, + shouldReloadDockerStack: boolean, + MAX_OPEN_PORTS: number, + STARTING_PORT_RANGE: number +) { + await $`echo '[🔄 Docker] adding docker stack config...'`; + const COMPOSE_FILE_PATH = `./docker/docker-stack.pr-${PR_ID}.yaml`; + + const composeFile = file(COMPOSE_FILE_PATH); + + if (await composeFile.exists()) { + await $`echo '[ℹī¸ Docker] docker stack config for pull request #${PR_ID} already exists, skipping work.'`; + if (shouldReloadDockerStack) { + await $`echo '[🔄 Docker] updating docker services...'`; + const { exitCode, stderr } = + await $`docker stack deploy --with-registry-auth --compose-file ${COMPOSE_FILE_PATH} gh-stack-pr-${PR_ID}`; + + if (exitCode !== 0) { + await $`echo '[❌ Docker] docker services encountered an unexpected error : ${stderr.toString()}'`; + process.exit(1); + } + await $`echo '[✅ Docker] docker services updated succesfully'`; + } + return; + } + + const files = + $`ls ./docker | grep -E '^docker-stack\.pr\-[0-9]+\.yaml$'`.lines(); + + let stackPRFiles = await Array.fromAsync(files).then((arr) => + arr + .filter(Boolean) + // sort from the lowest pr number to the highest + .sort((a, b) => { + const numberA = parseInt( + // @ts-expect-error + a.match(/^docker-stack\.pr\-(\d+)\.yaml$/)[1], + 10 + ); + const numberB = parseInt( + // @ts-expect-error + b.match(/^docker-stack\.pr\-(\d+)\.yaml$/)[1], + 10 + ); + return numberA - numberB; + }) + ); + + // don't go over 100 config max + if (stackPRFiles.length === MAX_OPEN_PORTS) { + const [leastRecentPullRequestFile, ...rest] = stackPRFiles; + + const leastRecentPullRequestID = parseInt( + // @ts-expect-error + leastRecentPullRequestFile.match(/^docker-stack\.pr\-(\d+)\.yaml$/)[1], + 10 + ); + + await $`echo '[🔄 Docker] docker stack config for pull request #${leastRecentPullRequestID} is too old, deleting it...'`; + await $`echo '[ℹī¸ Docker] you can still recreate it by deploying the associated pull request'`; + + await $`echo '[🔄 Docker] Removing associated docker stack services...'`; + await $`docker stack rm gh-stack-pr-${PR_ID}`; + await $`echo '[✅ Docker] associated docker stack services removed succesfully'`; + + // delete the least recent Pull Request + await $`rm './docker/${leastRecentPullRequestFile}'`; + + // delete the least recent Pull Request + await $`echo '[✅ Docker] docker stack config for pull request #${leastRecentPullRequestID} deleted'`; + + stackPRFiles = rest; + } + + const portNumber = STARTING_PORT_RANGE + stackPRFiles.length; + + // Placeholder text in the Docker Compose file + const DOCKER_STACK_TEMPLATE = `# service configuration for pull request #132 +version: "3.4" + +services: + gh-next-${PR_ID}: + image: dcr.fredkiss.dev/gh-next:pr-${PR_ID} + ports: + - "${portNumber}:3000" + deploy: + replicas: 0 + update_config: + parallelism: 1 + delay: 5s + order: start-first + failure_action: rollback + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + labels: + - sablier.enable=true + - sablier.group=gh-next-${PR_ID} + networks: + - gh-next +networks: + gh-next: + external: true +`; + await write(composeFile, DOCKER_STACK_TEMPLATE); + await $`echo '[✅ Docker] Added docker stack config file for pull request #${PR_ID}.'`; + if (shouldReloadDockerStack) { + await $`echo '[🔄 Docker] updating docker services...'`; + const { exitCode, stderr } = + await $`docker stack deploy --with-registry-auth --compose-file ${COMPOSE_FILE_PATH} gh-stack-pr-${PR_ID}`; + + if (exitCode !== 0) { + await $`echo '[❌ Docker] docker services encountered an unexpected error : ${stderr.toString()}'`; + process.exit(1); + } + await $`echo '[✅ Docker] docker services updated succesfully'`; + } +} diff --git a/pr-preview-workflow/bun.lockb b/pr-preview-workflow/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..d46223a38f64b52e04b4e5292ace39b02fbb6672 GIT binary patch literal 3851 zcmeHKeNa?Y6o0#}h#P?sI#4z&U?k$cg@xrqRx`C6B^^+ZiVs70yDlud%X@EGJ`^!b z*GN+kbsWDDMA1UV&lI%}Vn#ucnot`MCdq^ioW{fv2Re86U3pg0X8zG19p=7s@8$e{ z_uPBVeebM~m(0k}vLzawjM8OEmKb$nXi}{{C0(V_Qc{(k&PZmZSpi}pK@e9Lub$QA zZfVa~ri&ab-USk;s+&3euHq9#Ztu~0R-v)T>KgucOgFS-rw*ar?_K#wSn!UoeM}43o zQ2bDT6f`7Aqj_{bNaG8EHB4FU@w!jXGvG3S7I?-YAZ)yRYLQuX#(P~@X-mQbDs5(y zhvjC7{FvDNMx~e2!SDMjK4_cr2~jP*pVs@pu-&6E(RfW?w=kxrdjFLXC7x9mUB_($ zA2BbE19Dq*HRyEBA!)_jP?PcU^%=K)zuFgvw1&K2&hRj*O*sNTGOa1xJ~e?LX>wew{} zS3}`ZQ%-PCrK0XqYe(I~xnEbG^f{T~Jl3-6PF82j+TKpX+REh>mpfzgL;63P(>w7@ zRej7B@5VUA1RgIxZ$)QQU8C!-Z!Vs6#WStv=Fit-Ew?+&6*mmW=~nq*e)Pzd;5{QJYENM0Gx;lcjlcH`9#ld>@D*kZnU0A z=re`7)%bmSp?+f1{_>OA{!^RdHVn3~BVQ==IUe6~?Hx8_mV2qceE+Wx*N+(eRzL9I zxZ(Up)FR)gk0WmFVP}XBMrW4n^;}fowrFY4FLe zt?%ZHOpuwE)f9X6b)1VFtLn<#$Kyr!1dFKLxc%^HRG5%HmS4thVJI~T?A zuy*nx4B|qZh!61~KE#Xq$j?fSs~COQLBz^WkQ#BU`l z7pNld{{VqQds!8xXb@yO4@7ZZFkEF1`2=wad{USQ15IUVD3jErr_)thy@|~+P)rJ~ zF|gCET^ankSjw^nW{ONkQ)&&v(mABgkdeladRi@mkjbRzDYZq4WYxLDm=AFR3jJ4Em7`2e#T { - const sessionId = cookies().get(SESSION_COOKIE_KEY)?.value; - - if (!sessionId) { - // Normally this code is never reached - throw new Error("Session ID must be set in middleware"); - } - - const session = await Session.get(sessionId); - - if (!session) { - // Neither this - throw new Error( - "Session must have been created in middleware to be accessed." - ); - } - - taintObjectReference("Do not pass the session object to the client", session); - return session; -}); - export const getUserOrRedirect = cache(async function getUserOrRedirect( redirectToPath?: string ) { diff --git a/src/actions/middlewares.ts b/src/actions/middlewares.ts index 6f18bc73..46ef85c9 100644 --- a/src/actions/middlewares.ts +++ b/src/actions/middlewares.ts @@ -1,7 +1,8 @@ import "server-only"; import { revalidatePath } from "next/cache"; -import { getSession, getAuthedUser } from "~/actions/auth.action"; -import type { FunctionWithoutLastArg, OmitLastItemInArray } from "~/lib/types"; +import { getAuthedUser } from "./auth.action"; +import { getSession } from "./session.action"; +import type { FunctionWithoutLastArg } from "~/lib/types"; import type { Session } from "~/lib/server/session.server"; import type { User } from "~/lib/server/db/schema/user.sql"; diff --git a/src/actions/session.action.ts b/src/actions/session.action.ts index eb4bfc57..319bebb5 100644 --- a/src/actions/session.action.ts +++ b/src/actions/session.action.ts @@ -1,11 +1,17 @@ "use server"; import { revalidatePath } from "next/cache"; +import { cookies } from "next/headers"; import { redirect } from "next/navigation"; +import { + cache, + experimental_taintObjectReference as taintObjectReference +} from "react"; import { withAuth, type AuthState } from "~/actions/middlewares"; import { nextCache } from "~/lib/server/rsc-utils.server"; import { Session } from "~/lib/server/session.server"; import { CacheKeys } from "~/lib/shared/cache-keys.shared"; +import { SESSION_COOKIE_KEY } from "~/lib/shared/constants"; import { jsonFetch } from "~/lib/shared/utils.shared"; export type SuccessfulLocationData = { @@ -73,3 +79,24 @@ export const revokeSession = withAuth(async function revokeSession( revalidatePath("/"); redirect("/settings/sessions"); }); + +export const getSession = cache(async function getSession(): Promise { + const sessionId = cookies().get(SESSION_COOKIE_KEY)?.value; + + if (!sessionId) { + // Normally this code is never reached + throw new Error("Session ID must be set in middleware"); + } + + const session = await Session.get(sessionId); + + if (!session) { + // Neither this + throw new Error( + "Session must have been created in middleware to be accessed." + ); + } + + taintObjectReference("Do not pass the session object to the client", session); + return session; +}); diff --git a/src/actions/theme.action.ts b/src/actions/theme.action.ts index 5351d552..69355a50 100644 --- a/src/actions/theme.action.ts +++ b/src/actions/theme.action.ts @@ -1,7 +1,7 @@ "use server"; import { z } from "zod"; -import { getSession } from "./auth.action"; +import { getSession } from "./session.action"; import { cache } from "react"; import { updateUserTheme } from "~/models/user"; import { revalidatePath } from "next/cache"; diff --git a/src/app/(app)/[user]/[repository]/page.tsx b/src/app/(app)/[user]/[repository]/page.tsx index 9364e5c3..88cce910 100644 --- a/src/app/(app)/[user]/[repository]/page.tsx +++ b/src/app/(app)/[user]/[repository]/page.tsx @@ -22,7 +22,7 @@ import { Skeleton } from "~/components/skeleton"; import { Markdown } from "~/components/markdown/markdown"; // utils -import { getSession } from "~/actions/auth.action"; +import { getSession } from "~/actions/session.action"; import { getGithubRepoData } from "~/actions/github.action"; import { getRepositoryByOwnerAndName } from "~/models/repository"; import { diff --git a/src/app/(app)/settings/sessions/[id]/page.tsx b/src/app/(app)/settings/sessions/[id]/page.tsx index 77394524..7370a1d0 100644 --- a/src/app/(app)/settings/sessions/[id]/page.tsx +++ b/src/app/(app)/settings/sessions/[id]/page.tsx @@ -14,7 +14,8 @@ import { Skeleton } from "~/components/skeleton"; // utils import { redirect } from "next/navigation"; -import { getSession, getUserOrRedirect } from "~/actions/auth.action"; +import { getUserOrRedirect } from "~/actions/auth.action"; +import { getSession } from "~/actions/session.action"; import { getLocationData, revokeSession, diff --git a/src/app/(app)/settings/sessions/page.tsx b/src/app/(app)/settings/sessions/page.tsx index 827334bd..bdbf8be2 100644 --- a/src/app/(app)/settings/sessions/page.tsx +++ b/src/app/(app)/settings/sessions/page.tsx @@ -6,7 +6,8 @@ import { DeviceMobileIcon, QuestionIcon } from "@primer/octicons-react"; -import { getSession, getUserOrRedirect } from "~/actions/auth.action"; +import { getUserOrRedirect } from "~/actions/auth.action"; +import { getSession } from "~/actions/session.action"; import { Button } from "~/components/button"; // utils diff --git a/src/app/api/auth/callback/route.ts b/src/app/api/auth/callback/route.ts index 43075ae0..7c06d848 100644 --- a/src/app/api/auth/callback/route.ts +++ b/src/app/api/auth/callback/route.ts @@ -8,9 +8,9 @@ import { SHARED_KEY_PREFIX } from "~/lib/shared/constants"; export const fetchCache = "force-no-store"; export const revalidate = 0; -export async function GET(req: NextRequest) { - const code = req.nextUrl.searchParams.get("code"); - const state = req.nextUrl.searchParams.get("state"); +export async function GET(request: NextRequest) { + const code = request.nextUrl.searchParams.get("code"); + const state = request.nextUrl.searchParams.get("state"); if (!state || !code) { redirect("/"); @@ -25,6 +25,53 @@ export async function GET(req: NextRequest) { if (!stateData) { redirect("/"); } + + let theirOriginURL: URL | null = null; + try { + theirOriginURL = new URL(stateData.origin); + } catch (error) { + console.error(error); + // we failed to validate that the `origin` of the request is a valid URL + // so we refuse this auth request + await kv.delete(state, SHARED_KEY_PREFIX); + redirect("/"); + } + + // In the case were the request doesn't originate from the same host as this one (ex: from preview environments) + const ourOriginHost = request.headers.get("Host"); + console.log({ + ourOriginHost, + theirOriginHost: theirOriginURL.host + }); + if (ourOriginHost !== theirOriginURL.host) { + let redirectUrl: URL | null = null; + try { + redirectUrl = new URL("/api/auth/callback", theirOriginURL); + + if ( + process.env.NODE_ENV === "production" && + !redirectUrl.hostname.endsWith("gh.fredkiss.dev") + ) { + throw new Error("Invalid Hostname provided"); + } + + redirectUrl.searchParams.set("code", code); + redirectUrl.searchParams.set("state", state); + } catch (error) { + console.error(error); + // delete state data to prevent this state from being reused again + await kv.delete(state, SHARED_KEY_PREFIX); + } + + if (!redirectUrl) { + // we failed to validate that the `origin` of the request is a valid URL + // so we refuse this auth request + redirect("/"); + } + + return redirect(redirectUrl.toString()); + } + // delete state data to prevent this state from being reused again await kv.delete(state, SHARED_KEY_PREFIX); diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx index beed8130..3b0cc9cc 100644 --- a/src/components/header/header.tsx +++ b/src/components/header/header.tsx @@ -20,7 +20,7 @@ import { } from "~/components/user-dropdown/user-dropdown.server"; // utils -import { getSession } from "~/actions/auth.action"; +import { getSession } from "~/actions/session.action"; import { clsx } from "~/lib/shared/utils.shared"; // types diff --git a/src/components/toast/toaster.server.tsx b/src/components/toast/toaster.server.tsx index e78e96d9..709b8dff 100644 --- a/src/components/toast/toaster.server.tsx +++ b/src/components/toast/toaster.server.tsx @@ -1,7 +1,7 @@ import * as React from "react"; // utils -import { getSession } from "~/actions/auth.action"; +import { getSession } from "~/actions/session.action"; import { ToasterClient } from "./toaster.client"; import { headers } from "next/headers"; diff --git a/tsconfig.json b/tsconfig.json index ad2e1f11..00df610e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,6 +35,7 @@ ], "exclude": [ "node_modules", + "pr-preview-workflow/**/*.ts", "scripts/github-query-builder-element.ts", "scripts/github-action-list-element.ts" ]