From 660cfaa39b9b375e8ffe1ca77636628f5f266a4f Mon Sep 17 00:00:00 2001 From: Rain Date: Mon, 19 Oct 2026 11:39:38 +0800 Subject: [PATCH 1/5] style(guest-registration): add new line before function --- src/app/api/guest-registration/route.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/api/guest-registration/route.ts b/src/app/api/guest-registration/route.ts index 657c38d..07c5341 100644 --- a/src/app/api/guest-registration/route.ts +++ b/src/app/api/guest-registration/route.ts @@ -10,6 +10,7 @@ import { getMeetingById, getMeetingEndsAt } from "@/lib/meetings/meetings"; import { subscribeToNewsletter } from "@/lib/newsletter/subscribe"; import { guestRegistrationSchema } from "@/lib/validations/guest-registration"; import validateFormData from "@/lib/validations/validate-form-data"; + export async function POST(request: Request) { const formData = await request.formData(); From c5e33a67dfba777607fb0616dccb67a523e5971d Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 20 Oct 2026 12:26:31 +0800 Subject: [PATCH 2/5] feat: add rate limiting to form endpoints --- .env.example | 4 +- package.json | 2 + pnpm-lock.yaml | 88 ++++++++----------- .../components/GuestRegistration.tsx | 6 +- src/app/api/guest-registration/route.ts | 22 +++++ src/lib/forms/submitForm.ts | 4 +- src/lib/redis/rate-limit.ts | 11 +++ src/lib/redis/redis.ts | 6 ++ 8 files changed, 88 insertions(+), 55 deletions(-) create mode 100644 src/lib/redis/rate-limit.ts create mode 100644 src/lib/redis/redis.ts diff --git a/.env.example b/.env.example index e54af44..57c72df 100644 --- a/.env.example +++ b/.env.example @@ -7,4 +7,6 @@ MAILERLITE_API_KEY= MAILERLITE_NEWSLETTER_GROUP_ID= GOOGLE_SHEETS_CLIENT_EMAIL= GOOGLE_SHEETS_PRIVATE_KEY= -GOOGLE_SHEETS_SPREADSHEET_ID= \ No newline at end of file +GOOGLE_SHEETS_SPREADSHEET_ID= +UPSTASH_REDIS_REST_URL= +UPSTASH_REDIS_REST_TOKEN= \ No newline at end of file diff --git a/package.json b/package.json index 47cf9d1..d3c7401 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "@react-pdf/renderer": "^4.9.0", "@supabase/ssr": "^0.12.7", "@supabase/supabase-js": "^2.117.2", + "@upstash/ratelimit": "^2.2.0", + "@upstash/redis": "^1.39.0", "@vercel/analytics": "^2.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3c1e9e..e0c16ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,12 @@ importers: '@supabase/supabase-js': specifier: ^2.117.2 version: 2.117.2 + '@upstash/ratelimit': + specifier: ^2.2.0 + version: 2.2.0(@upstash/redis@1.39.0) + '@upstash/redis': + specifier: ^1.39.0 + version: 1.39.0 '@vercel/analytics': specifier: ^2.0.1 version: 2.0.1(next@16.2.12(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) @@ -280,105 +286,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -449,28 +439,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@16.2.12': resolution: {integrity: sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@16.2.12': resolution: {integrity: sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@16.2.12': resolution: {integrity: sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@16.2.12': resolution: {integrity: sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA==} @@ -602,42 +588,36 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.2.13': resolution: {integrity: sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.2.13': resolution: {integrity: sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.2.13': resolution: {integrity: sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.2.13': resolution: {integrity: sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-musl@1.2.13': resolution: {integrity: sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-openharmony-arm64@1.2.13': resolution: {integrity: sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==} @@ -687,25 +667,21 @@ packages: resolution: {integrity: sha512-+4IwiBpGOQgLFeEG6RImxSUrMHo+LDsFNGJ4WLoLBOozFaippoz5J6CnNpVSTeJj9QaOX3G9iVsL/wYF6P1Cxw==} cpu: [arm64] os: [linux] - libc: [musl] '@supabase/cli-linux-arm64@2.120.0': resolution: {integrity: sha512-pmR61xpBmzUc79gkh3sE42KtBkludBXex1TIQTVY6jkTjs2+JhJYNKxR7udeaFgxFpiV+zTeAyqJBYXLibxQJw==} cpu: [arm64] os: [linux] - libc: [glibc] '@supabase/cli-linux-x64-musl@2.120.0': resolution: {integrity: sha512-YIrqu1+hBiLwYIk1HKesrfIm2ycQIsOfN5VGV3r/okQx8JyWl9Jrw7emKVS/F+1afe+1MqKEBP70B0PbuvX+jQ==} cpu: [x64] os: [linux] - libc: [musl] '@supabase/cli-linux-x64@2.120.0': resolution: {integrity: sha512-IWJ0yFefhX/69ofcgJfN8MyOC9TtCG4SLrlMCvXjH3vZXxnRkiVNggxQqUCIhshS9Afa+q17DRbQen7/IZTL4A==} cpu: [x64] os: [linux] - libc: [glibc] '@supabase/cli-windows-arm64@2.120.0': resolution: {integrity: sha512-U+IDegKWC4xXxk7aCRUUy2ZTJ4bwwrcVlnzG1VJqC7nB7SLgMiyUMyvuo1tB+Xt8hHf0hkuViNtohnXPdZHCIQ==} @@ -791,28 +767,24 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.3': resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.3': resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.3': resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.3': resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} @@ -972,61 +944,51 @@ packages: resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.12.2': resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} cpu: [loong64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-loong64-musl@1.12.2': resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} cpu: [loong64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.12.2': resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.12.2': resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-openharmony-arm64@1.12.2': resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} @@ -1053,6 +1015,18 @@ packages: cpu: [x64] os: [win32] + '@upstash/core-analytics@0.0.10': + resolution: {integrity: sha512-7qJHGxpQgQr9/vmeS1PktEwvNAF7TI4iJDi8Pu2CFZ9YUGHZH4fOP5TfYlZ4aVxfopnELiE4BS4FBjyK7V1/xQ==} + engines: {node: '>=16.0.0'} + + '@upstash/ratelimit@2.2.0': + resolution: {integrity: sha512-gSydXspG6TJKTh71/gGGv4IGDjD9rXglQfm2jAue2kh4bcfWtEAsG1NW4OqzZd/hubJPojsbOU6AMdS6Xqh0tQ==} + peerDependencies: + '@upstash/redis': ^1.38.2 + + '@upstash/redis@1.39.0': + resolution: {integrity: sha512-VwkpYTdxSfjSulQS5h16NHtg/1UEheBfvez2AeGKinAujOoGXhXCcohQRzsQ/d2g/XzbjzTVGhG8Ds8uOI0Mxg==} + '@vercel/analytics@2.0.1': resolution: {integrity: sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g==} peerDependencies: @@ -2073,56 +2047,48 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-gnu@1.33.0: resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-arm64-musl@1.33.0: resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-gnu@1.33.0: resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-linux-x64-musl@1.33.0: resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -2714,6 +2680,9 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -3726,6 +3695,19 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true + '@upstash/core-analytics@0.0.10': + dependencies: + '@upstash/redis': 1.39.0 + + '@upstash/ratelimit@2.2.0(@upstash/redis@1.39.0)': + dependencies: + '@upstash/core-analytics': 0.0.10 + '@upstash/redis': 1.39.0 + + '@upstash/redis@1.39.0': + dependencies: + uncrypto: 0.1.3 + '@vercel/analytics@2.0.1(next@16.2.12(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': optionalDependencies: next: 16.2.12(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -5612,6 +5594,8 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + uncrypto@0.1.3: {} + undici-types@6.21.0: {} unicode-properties@1.4.1: diff --git a/src/app/(public)/membership/components/GuestRegistration.tsx b/src/app/(public)/membership/components/GuestRegistration.tsx index 61488d7..bbe4765 100644 --- a/src/app/(public)/membership/components/GuestRegistration.tsx +++ b/src/app/(public)/membership/components/GuestRegistration.tsx @@ -54,7 +54,11 @@ export default function GuestRegistration({ console.error("Contact form failed: ", err); setStatus("idle"); - setError("We couldn't send your message. Please try again."); + setError( + err instanceof Error + ? err.message + : "We couldn't send your message. Please try again.", + ); } } diff --git a/src/app/api/guest-registration/route.ts b/src/app/api/guest-registration/route.ts index 07c5341..17eeeb5 100644 --- a/src/app/api/guest-registration/route.ts +++ b/src/app/api/guest-registration/route.ts @@ -8,10 +8,32 @@ import { sendEmail } from "@/lib/email/sendEmail"; import { formatDateTime } from "@/lib/meetings/format"; import { getMeetingById, getMeetingEndsAt } from "@/lib/meetings/meetings"; import { subscribeToNewsletter } from "@/lib/newsletter/subscribe"; +import { createGuestRateLimiter } from "@/lib/redis/rate-limit"; import { guestRegistrationSchema } from "@/lib/validations/guest-registration"; import validateFormData from "@/lib/validations/validate-form-data"; export async function POST(request: Request) { + const rateLimiter = createGuestRateLimiter(); + const identifier = + request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown"; + const result = await rateLimiter.limit(identifier); + + if (!result.success) { + const reset = result.reset; + const now = Date.now(); + const retryAfter = Math.max(0, Math.ceil((reset - now) / 1000)); + const retryAfterMinutes = Math.ceil(retryAfter / 60); + + return NextResponse.json( + { + error: `Too many requests. Please try again in ${retryAfterMinutes} minute${retryAfterMinutes === 1 ? "" : "s"}.`, + }, + { + status: 429, + }, + ); + } + const formData = await request.formData(); const rawData = { diff --git a/src/lib/forms/submitForm.ts b/src/lib/forms/submitForm.ts index ee0a8aa..6872941 100644 --- a/src/lib/forms/submitForm.ts +++ b/src/lib/forms/submitForm.ts @@ -5,7 +5,9 @@ export async function submitForm(api: string, formData: FormData) { }); if (!response.ok) { - throw new Error("Form submission failed"); + const data = await response.json().catch(() => null); + + throw new Error(data?.error || "Form submission failed"); } return response; diff --git a/src/lib/redis/rate-limit.ts b/src/lib/redis/rate-limit.ts new file mode 100644 index 0000000..e0de977 --- /dev/null +++ b/src/lib/redis/rate-limit.ts @@ -0,0 +1,11 @@ +import { Ratelimit } from "@upstash/ratelimit"; + +import { initializeRedis } from "./redis"; + +export function createGuestRateLimiter() { + return new Ratelimit({ + redis: initializeRedis(), + limiter: Ratelimit.slidingWindow(5, "15 m"), + prefix: "ratelimit:guest-registration", + }); +} diff --git a/src/lib/redis/redis.ts b/src/lib/redis/redis.ts new file mode 100644 index 0000000..c1acd9b --- /dev/null +++ b/src/lib/redis/redis.ts @@ -0,0 +1,6 @@ +import { Redis } from "@upstash/redis"; +import "server-only"; + +export function initializeRedis() { + return Redis.fromEnv(); +} From a0dc99279b122d96ca051f6aa0f6a07a10dcae2e Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 20 Oct 2026 12:28:43 +0800 Subject: [PATCH 3/5] feat: add rate limiting to contact form --- src/app/api/contact/route.ts | 22 ++++++++++++++++++++++ src/lib/redis/rate-limit.ts | 8 ++++++++ 2 files changed, 30 insertions(+) diff --git a/src/app/api/contact/route.ts b/src/app/api/contact/route.ts index e31f71a..bb32cbb 100644 --- a/src/app/api/contact/route.ts +++ b/src/app/api/contact/route.ts @@ -4,10 +4,32 @@ import { siteConfig } from "@/data/site-config"; import { escapeHtml } from "@/lib/email/escapeHtml"; import { sendEmail } from "@/lib/email/sendEmail"; import { subscribeToNewsletter } from "@/lib/newsletter/subscribe"; +import { createContactRateLimiter } from "@/lib/redis/rate-limit"; import { contactSchema } from "@/lib/validations/contact"; import validateFormData from "@/lib/validations/validate-form-data"; export async function POST(request: Request) { + const rateLimiter = createContactRateLimiter(); + const identifier = + request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown"; + const result = await rateLimiter.limit(identifier); + + if (!result.success) { + const reset = result.reset; + const now = Date.now(); + const retryAfter = Math.max(0, Math.ceil((reset - now) / 1000)); + const retryAfterMinutes = Math.ceil(retryAfter / 60); + + return NextResponse.json( + { + error: `Too many requests. Please try again in ${retryAfterMinutes} minute${retryAfterMinutes === 1 ? "" : "s"}.`, + }, + { + status: 429, + }, + ); + } + const formData = await request.formData(); const rawData = { diff --git a/src/lib/redis/rate-limit.ts b/src/lib/redis/rate-limit.ts index e0de977..c1a7108 100644 --- a/src/lib/redis/rate-limit.ts +++ b/src/lib/redis/rate-limit.ts @@ -9,3 +9,11 @@ export function createGuestRateLimiter() { prefix: "ratelimit:guest-registration", }); } + +export function createContactRateLimiter() { + return new Ratelimit({ + redis: initializeRedis(), + limiter: Ratelimit.slidingWindow(5, "15 m"), + prefix: "ratelimit:contact-registration", + }); +} From 83c4da5483a15d3471d0c86e4e02215eebface6d Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 20 Oct 2026 12:31:32 +0800 Subject: [PATCH 4/5] refactor(contact-limiter): rename prefix to contact --- src/lib/redis/rate-limit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/redis/rate-limit.ts b/src/lib/redis/rate-limit.ts index c1a7108..567d7a3 100644 --- a/src/lib/redis/rate-limit.ts +++ b/src/lib/redis/rate-limit.ts @@ -14,6 +14,6 @@ export function createContactRateLimiter() { return new Ratelimit({ redis: initializeRedis(), limiter: Ratelimit.slidingWindow(5, "15 m"), - prefix: "ratelimit:contact-registration", + prefix: "ratelimit:contact", }); } From 5c7a2749aacb97f5e02b8a7c10053ab0f31393bc Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 20 Oct 2026 12:32:54 +0800 Subject: [PATCH 5/5] feat(contact-form): display error message from response --- src/app/(public)/contact/components/ContactForm.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/(public)/contact/components/ContactForm.tsx b/src/app/(public)/contact/components/ContactForm.tsx index a767fa4..7c3bff0 100644 --- a/src/app/(public)/contact/components/ContactForm.tsx +++ b/src/app/(public)/contact/components/ContactForm.tsx @@ -44,7 +44,11 @@ export default function ContactForm() { console.error("Contact form failed: ", err); setStatus("idle"); - setError("We couldn't send your message. Please try again."); + setError( + err instanceof Error + ? err.message + : "We couldn't send your message. Please try again.", + ); } }