Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ If you want to learn more about this project or have any questions, send us an e
- [Supabase](https://supabase.com)
- [inngest](https://www.inngest.com)
- [Postmark](https://postmarkapp.com)
- [Arcjet](https://arcjet.com)

## Getting Started 🚀

Expand Down
3 changes: 3 additions & 0 deletions apps/page/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ SUPABASE_SERVICE_ROLE_KEY=
# Inngest
INNGEST_EVENT_KEY=
INNGEST_SIGNING_KEY=

# Arcjet
ARCJET_KEY=
```
4 changes: 0 additions & 4 deletions apps/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ POSTMARK_WEBHOOK_KEY=
INNGEST_EVENT_KEY=
INNGEST_SIGNING_KEY=

# Redis
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

# CMS
NEXT_PUBLIC_SANITY_PROJECT_ID=

Expand Down
1 change: 0 additions & 1 deletion apps/web/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

export async function middleware(req: NextRequest) {
console.log("middleware check", req.nextUrl.pathname);
// We need to create a response and hand it to the supabase client to be able to modify the response headers.
const res = NextResponse.next();
const supabase = createMiddlewareClient({ req, res });
Expand Down
3 changes: 3 additions & 0 deletions apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ const moduleExports = {
"cdn.sanity.io",
],
},
typescript: {
ignoreBuildErrors: true,
},
};

// ensure that your source maps include changes from all other Webpack plugins
Expand Down
4 changes: 2 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"lint": "next lint"
},
"dependencies": {
"@arcjet/next": "1.0.0-alpha.20",
"@changes-page/supabase": "workspace:*",
"@changes-page/ui": "workspace:*",
"@changes-page/utils": "workspace:*",
Expand All @@ -27,7 +28,6 @@
"@supabase/supabase-js": "^2.39.3",
"@tailwindcss/typography": "^0.5.1",
"@types/canvas-confetti": "^1.6.4",
"@upstash/redis": "^1.20.6",
"@vercel/analytics": "^1.0.1",
"@vercel/og": "^0.0.20",
"canvas-confetti": "^1.9.3",
Expand Down Expand Up @@ -76,6 +76,6 @@
"postcss": "^8.4.5",
"prettier": "^2.3.2",
"tailwindcss": "^3.0.15",
"typescript": "^4.9.4"
"typescript": "^4"
}
}
4 changes: 3 additions & 1 deletion apps/web/pages/api/billing/create-billing-portal.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { IErrorResponse } from "@changes-page/supabase/types/api";
import type { NextApiRequest, NextApiResponse } from "next";
import { apiRateLimiter } from "../../../utils/rate-limit";
import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin";
import { createOrRetrieveCustomer } from "../../../utils/useDatabase";
import { getAppBaseURL } from "./../../../utils/helpers";
Expand All @@ -10,6 +11,7 @@ const createBillingSession = async (
res: NextApiResponse<{ url: string } | IErrorResponse>
) => {
if (req.method === "POST") {
await apiRateLimiter(req, res);
const { return_url } = req.body;

try {
Expand Down
2 changes: 2 additions & 0 deletions apps/web/pages/api/billing/redirect-to-checkout.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getAppBaseURL } from "../../../utils/helpers";
import { apiRateLimiter } from "../../../utils/rate-limit";
import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin";
import {
createOrRetrieveCustomer,
Expand All @@ -13,6 +14,7 @@ const redirectToCheckout = async (
res: NextApiResponse
) => {
if (req.method === "GET") {
await apiRateLimiter(req, res);
const { return_url } = req.query;

try {
Expand Down
2 changes: 2 additions & 0 deletions apps/web/pages/api/emails/subscribers/export-csv.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { supabaseAdmin } from "@changes-page/supabase/admin";
import { Parser } from "@json2csv/plainjs";
import { NextApiRequest, NextApiResponse } from "next";
import { apiRateLimiter } from "../../../../utils/rate-limit";
import { getSupabaseServerClient } from "../../../../utils/supabase/supabase-admin";

const getSubscribersExportCsv = async (
Expand All @@ -9,6 +10,7 @@ const getSubscribersExportCsv = async (
) => {
if (req.method === "GET") {
try {
await apiRateLimiter(req, res);
const { user } = await getSupabaseServerClient({ req, res });

const { page_id } = req.query;
Expand Down
2 changes: 2 additions & 0 deletions apps/web/pages/api/pages/settings/add-domain.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { NextApiRequest, NextApiResponse } from "next";
import { apiRateLimiter } from "../../../../utils/rate-limit";
import { getSupabaseServerClient } from "../../../../utils/supabase/supabase-admin";

async function addDomain(req: NextApiRequest, res: NextApiResponse) {
await apiRateLimiter(req, res);
const { user } = await getSupabaseServerClient({ req, res });

const { domain } = req.body;
Expand Down
2 changes: 2 additions & 0 deletions apps/web/pages/api/pages/settings/check-domain.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { NextApiRequest, NextApiResponse } from "next";
import { apiRateLimiter } from "../../../../utils/rate-limit";
import { getSupabaseServerClient } from "../../../../utils/supabase/supabase-admin";

async function checkDomain(req: NextApiRequest, res: NextApiResponse) {
await apiRateLimiter(req, res);
const { user } = await getSupabaseServerClient({ req, res });

const { domain } = req.query;
Expand Down
5 changes: 4 additions & 1 deletion apps/web/pages/api/pages/validate-url.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { IErrorResponse } from "@changes-page/supabase/types/api";
import { URL_SLUG_REGEX } from "@changes-page/supabase/types/page";
import type { NextApiRequest, NextApiResponse } from "next";
import { apiRateLimiter } from "../../../utils/rate-limit";
import { validatePageByUrl } from "../../../utils/useDatabase";

const BLACKLIST = [
Expand Down Expand Up @@ -74,6 +75,8 @@ const validatePageUrl = async (
res: NextApiResponse<{ status: boolean } | IErrorResponse>
) => {
if (req.method === "POST") {
await apiRateLimiter(req, res);

const { url_slug, page_id } = req.body;

console.log("validatePageUrl", { url_slug, page_id });
Expand Down
48 changes: 14 additions & 34 deletions apps/web/utils/rate-limit.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,25 @@
import arcjet, { slidingWindow } from "@arcjet/next";
import { NextApiRequest, NextApiResponse } from "next";
import { redis } from "./redis";

const updateRateLimit = async (
ip: string = "local",
limit: number = 5,
duration: number = 60
): Promise<{ limit: number; remaining: number; success: boolean }> => {
const key = `rate_limit:${ip}`;

let currentCount = await redis.get(key);

let count = parseInt(currentCount as string, 10) || 0;

if (count >= limit) {
return { limit, remaining: limit - count, success: false };
}

await redis.incr(key);
await redis.expire(key, duration);

return { limit, remaining: limit - (count + 1), success: true };
};

export async function rateLimiter(req: Request) {
const identifier = req.headers.get("x-forwarded-for") ?? "local";
const result = await updateRateLimit(identifier);

return result.success;
}
const aj = arcjet({
key: process.env.ARCJET_KEY,
characteristics: ["ip.src"], // track requests by IP address
rules: [
slidingWindow({
mode: "LIVE",
interval: 60,
max: 20,
}),
],
});

export async function apiRateLimiter(
req: NextApiRequest,
res: NextApiResponse
) {
const identifier = (req.headers["x-forwarded-for"] as string) ?? "local";
const result = await updateRateLimit(identifier);

res.setHeader("X-RateLimit-Limit", result.limit);
res.setHeader("X-RateLimit-Remaining", result.remaining);
const decision = await aj.protect(req);

if (!result.success) {
if (decision.isDenied()) {
return res.status(429).json({
error: {
statusCode: 429,
Expand Down
6 changes: 0 additions & 6 deletions apps/web/utils/redis/index.ts

This file was deleted.

18 changes: 4 additions & 14 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.