Skip to content

Commit 180bd70

Browse files
authored
Merge pull request #110 from upstash/DX-960
Add enable-protection example
2 parents 7e5e9e1 + ae41884 commit 180bd70

16 files changed

+341
-1
lines changed

Diff for: examples/cloudflare-workers/README.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11

22
## Local Development
33

4-
For testing the app locally, set the `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` environment variables after creating an Upstash Redis.
4+
For testing the app locally, start by creating an Upstash Redis. Next, create the `.dev.vars` file under `cloudflare-workers` directory and set the `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` environment variables:
5+
6+
```
7+
// .dev.vars
8+
UPSTASH_REDIS_REST_URL="****"
9+
UPSTASH_REDIS_REST_TOKEN="****"
10+
```
511

612
Then, simply run:
713

Diff for: examples/enable-protection/.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "next/core-web-vitals"
3+
}

Diff for: examples/enable-protection/.gitignore

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
.yarn/install-state.gz
8+
9+
# testing
10+
/coverage
11+
12+
# next.js
13+
/.next/
14+
/out/
15+
16+
# production
17+
/build
18+
19+
# misc
20+
.DS_Store
21+
*.pem
22+
23+
# debug
24+
npm-debug.log*
25+
yarn-debug.log*
26+
yarn-error.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts

Diff for: examples/enable-protection/README.md

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Using deny list by enabling protection
2+
3+
To use enable list, simply set the `enableProtection` parameter to true when
4+
initializing the Upstash Redis client.
5+
6+
```tsx
7+
const ratelimit = new Ratelimit({
8+
redis: Redis.fromEnv(),
9+
limiter: Ratelimit.slidingWindow(10, "10 s"),
10+
prefix: "@upstash/ratelimit",
11+
analytics: true,
12+
enableProtection: true
13+
});
14+
```
15+
16+
When this parameter is enabled, redis client will check the identifier and
17+
other parameters against a dent list managed through [Ratelimit Dashboard](https://console.upstash.com/ratelimit).
18+
19+
When analytics is set to true, requests blocked with the deny list are
20+
logged under Denied and shown in the dashboard.
21+
22+
In addition to passing an identifier, you can pass IP address, user agent
23+
and country in the `limit` method. If any of these values is in the deny
24+
list, the request will be denied.
25+
26+
```tsx
27+
const { success, limit, remaining, pending, reason } = await ratelimit.limit(
28+
identifier, {
29+
ip: ipAddress,
30+
userAgent: userAgent,
31+
country: country
32+
}
33+
);
34+
```
35+
36+
With this change, we also introduce the reason parameter, which denotes
37+
whether a request passed with timeout or rejected with caching or deny list.
38+
39+
# Running the example locally
40+
41+
To run the example, simply create a Redis instance from Upstash
42+
console and save the URL and the token to this directory under a
43+
`.env` file.
44+
45+
Then run `npm run dev`.

Diff for: examples/enable-protection/app/api/route.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export const runtime = 'edge';
2+
3+
export const dynamic = 'force-dynamic';
4+
5+
import { waitUntil } from '@vercel/functions';
6+
import { Ratelimit } from "@upstash/ratelimit";
7+
import { Redis } from "@upstash/redis";
8+
9+
// Create a new ratelimiter
10+
const ratelimit = new Ratelimit({
11+
redis: Redis.fromEnv(),
12+
limiter: Ratelimit.slidingWindow(10, "10 s"),
13+
prefix: "@upstash/ratelimit",
14+
analytics: true,
15+
enableProtection: true
16+
});
17+
18+
export async function POST(request: Request) {
19+
20+
const content = await request.json()
21+
22+
const { success, limit, remaining, pending, reason } = await ratelimit.limit(
23+
content, {ip: "10"});
24+
const response = {
25+
success: success,
26+
limit: limit,
27+
remaining: remaining
28+
}
29+
console.log(success, reason)
30+
31+
// pending is a promise for handling the analytics submission
32+
waitUntil(pending)
33+
34+
if (!success) {
35+
return new Response(JSON.stringify(response), { status: 429 });
36+
}
37+
return new Response(JSON.stringify(response));
38+
}

Diff for: examples/enable-protection/app/favicon.ico

25.3 KB
Binary file not shown.

Diff for: examples/enable-protection/app/globals.css

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
4+
5+
:root {
6+
--foreground-rgb: 0, 0, 0;
7+
--background-start-rgb: 249,250,250;
8+
--background-end-rgb: 236,238,238;
9+
}
10+
11+
@media (prefers-color-scheme: dark) {
12+
:root {
13+
--foreground-rgb: 255, 255, 255;
14+
--background-start-rgb: 0, 0, 0;
15+
--background-end-rgb: 0, 0, 0;
16+
}
17+
}
18+
19+
body {
20+
color: rgb(var(--foreground-rgb));
21+
background: linear-gradient(
22+
to bottom,
23+
transparent,
24+
rgb(var(--background-end-rgb))
25+
)
26+
rgb(var(--background-start-rgb));
27+
}
28+
29+
@layer utilities {
30+
.text-balance {
31+
text-wrap: balance;
32+
}
33+
}

Diff for: examples/enable-protection/app/layout.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Metadata } from "next";
2+
import { Inter } from "next/font/google";
3+
import "./globals.css";
4+
5+
const inter = Inter({ subsets: ["latin"] });
6+
7+
export const metadata: Metadata = {
8+
title: "Create Next App",
9+
description: "Generated by create next app",
10+
};
11+
12+
export default function RootLayout({
13+
children,
14+
}: Readonly<{
15+
children: React.ReactNode;
16+
}>) {
17+
return (
18+
<html lang="en">
19+
<body className={inter.className}>{children}</body>
20+
</html>
21+
);
22+
}

Diff for: examples/enable-protection/app/page.tsx

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"use client";
2+
import React, { useState } from "react";
3+
4+
export default function Home() {
5+
const [status, setStatus] = useState({
6+
success: true,
7+
limit: 10,
8+
remaining: 10
9+
});
10+
const [message, setMessage] = useState("");
11+
const [identifier, setIdentifier] = useState("userId");
12+
13+
const handleClick = async () => {
14+
try {
15+
const response = await fetch(`/api`, {body: JSON.stringify(identifier), method: "POST"});
16+
const data = await response.text();
17+
setStatus(JSON.parse(data));
18+
} catch (error) {
19+
console.error("Error fetching data:", error);
20+
setStatus({
21+
success: false,
22+
limit: -1,
23+
remaining: -1
24+
});
25+
setMessage(`Error fetching data. Make sure that env variables are set as explained in the README file.`,)
26+
}
27+
};
28+
const handleFormSubmit = (identifier: string) => {
29+
console.log('Form submitted with identifier:', identifier);
30+
// Add your logic to handle the form submission
31+
};
32+
33+
return (
34+
<div className="flex flex-col items-center justify-center min-h-screen">
35+
<div className="absolute top-1/3 text-center">
36+
<p className="text-lg">This app is an example of rate limiting an API on Vercel Edge.</p>
37+
<p className="text-lg">Click the button below to call the API and get the rate limit status.</p>
38+
<p className="text-lg pt-5">{message}</p>
39+
</div>
40+
41+
<div className="absolute top-1/2 grid grid-cols-3 gap-8 justify-center transform -translate-y-1/2">
42+
{Object.entries(status).map(([key, value]) => (
43+
<div key={key} className="text-center">
44+
<div className="font-semibold">{key}</div>
45+
<div>{JSON.stringify(value)}</div>
46+
</div>
47+
))}
48+
</div>
49+
<div className="absolute bottom-1/3 flex flex-row gap-10 items-center">
50+
<form
51+
className="flex flex-row gap-3"
52+
onSubmit={() => {handleFormSubmit(identifier)}}
53+
>
54+
<label>
55+
Identifier:
56+
<input
57+
type="text"
58+
value={identifier}
59+
onChange={(e) => setIdentifier(e.target.value)}
60+
/>
61+
</label>
62+
</form>
63+
<button onClick={handleClick} className="bg-[#dee2e3] hover:bg-[#9aa6a9] transition border-black text-[#5a6769] font-semibold py-2 px-4 rounded-lg">
64+
Test Rate Limit
65+
</button>
66+
</div>
67+
</div>
68+
);
69+
}

Diff for: examples/enable-protection/next.config.mjs

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/** @type {import('next').NextConfig} */
2+
const nextConfig = {};
3+
4+
export default nextConfig;

Diff for: examples/enable-protection/package.json

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "vercel-edge",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "next dev",
7+
"build": "next build",
8+
"start": "next start",
9+
"lint": "next lint"
10+
},
11+
"dependencies": {
12+
"@upstash/ratelimit": "^1.2.0-canary",
13+
"@vercel/functions": "^1.0.2",
14+
"next": "14.2.3",
15+
"react": "^18",
16+
"react-dom": "^18"
17+
},
18+
"devDependencies": {
19+
"@types/node": "^20",
20+
"@types/react": "^18",
21+
"@types/react-dom": "^18",
22+
"eslint": "^8",
23+
"eslint-config-next": "14.2.3",
24+
"postcss": "^8",
25+
"tailwindcss": "^3.4.1",
26+
"typescript": "^5"
27+
}
28+
}

Diff for: examples/enable-protection/postcss.config.mjs

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/** @type {import('postcss-load-config').Config} */
2+
const config = {
3+
plugins: {
4+
tailwindcss: {},
5+
},
6+
};
7+
8+
export default config;

Diff for: examples/enable-protection/public/next.svg

+1
Loading

Diff for: examples/enable-protection/public/vercel.svg

+1
Loading

Diff for: examples/enable-protection/tailwind.config.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { Config } from "tailwindcss";
2+
3+
const config: Config = {
4+
content: [
5+
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
6+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
7+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
8+
],
9+
theme: {
10+
extend: {
11+
backgroundImage: {
12+
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
13+
"gradient-conic":
14+
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
15+
},
16+
},
17+
},
18+
plugins: [],
19+
};
20+
export default config;

Diff for: examples/enable-protection/tsconfig.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"compilerOptions": {
3+
"lib": ["dom", "dom.iterable", "esnext"],
4+
"allowJs": true,
5+
"skipLibCheck": true,
6+
"strict": true,
7+
"noEmit": true,
8+
"esModuleInterop": true,
9+
"module": "esnext",
10+
"moduleResolution": "bundler",
11+
"resolveJsonModule": true,
12+
"isolatedModules": true,
13+
"jsx": "preserve",
14+
"incremental": true,
15+
"plugins": [
16+
{
17+
"name": "next"
18+
}
19+
],
20+
"paths": {
21+
"@/*": ["./*"]
22+
}
23+
},
24+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25+
"exclude": ["node_modules"]
26+
}

0 commit comments

Comments
 (0)