Skip to content

Commit e5196d1

Browse files
Add rate limiter to middleware example (#188)
1 parent 67c110a commit e5196d1

File tree

13 files changed

+115
-56
lines changed

13 files changed

+115
-56
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ E.g. `encore app create my-app --example=ts/hello-world`
3838
| [ts/hello-world](ts/hello-world) | REST API Starter | APIs |
3939
| [ts/ai-chat](ts/ai-chat) | LLM chat application which let's you create and chat with personalized bots. Integrates with OpenAI, Anthropic and Slack | Microservices, APIs, SQL Database, Pub/Sub, External Requests, Configs |
4040
| [ts/streaming](ts/streaming) | Examples of the different WebSocket Streaming APIs | Streaming API, Static Endpoint, Frontend |
41-
| [ts/react-starter](ts/react-starter) | Encore + React Web App Starter | APIs, Frontend |
41+
| [ts/react-starter](ts/react-starter) | Encore + React Web App Starter | APIs, Frontend |
4242
| [ts/nextjs-starter](ts/nextjs-starter) | Encore + Next.js Web App Starter | Microservices, APIs, Frontend |
4343
| [https://github.com/encoredev/nextjs-starter/](nextjs-starter)| Encore + Next.js Web App Starter (separate dependencies) | Microservices, APIs, Frontend |
4444
| [ts/graphql](ts/graphql) | Apollo GraphQL Server Starter | APIs, GraphQL |
@@ -60,6 +60,7 @@ E.g. `encore app create my-app --example=ts/hello-world`
6060
| [ts/expressjs-migration](ts/expressjs-migration) | Express.js migration guide examples | APIs, Raw Endpoints, Auth, Databases |
6161
| [ts/file-upload](ts/file-upload) | Upload files from frontend example | Raw Endpoints |
6262
| [ts/static-files](ts/static-files) | Serving static files example | Static Endpoints |
63+
| [ts/middleware](ts/middleware) | Rate limiting and Authorization middleware examples | APIs, Auth, Middleware |
6364
| [ts/template-engine](ts/template-engine) | Using a templating engine | Raw Endpoints, Static Endpoints |
6465

6566
### Go starters

ts/middleware-demo/user/encore.service.ts

Lines changed: 0 additions & 39 deletions
This file was deleted.

ts/middleware/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.encore
2+
encore.gen.go
3+
encore.gen.cue
4+
/.encore
5+
node_modules
6+
/encore.gen

ts/middleware-demo/README.md renamed to ts/middleware/README.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
# Middleware Demo
1+
# Middleware in Encore.ts
22

3-
This is the code example used in the middleware demo [stream](https://www.youtube.com/watch?v=qInxenZVDJs). It demonstrates how to implement a simple authorization middleware that only allows users to update themselves while still being able to view other users in the system.
3+
This examples contains two middleware examples: rate limiting and authorization. The rate limiting middleware limits the number of requests a user can make to auth endpoints and the authorization middleware only allows users to update themselves while still being able to view other users in the system.
44

5-
All endpoints require authentication, pass the string `userid:1` in the authorization header to login as user with id 1. Replace 1 with any number.
5+
Video tutorial of implementing the authorization middleware: https://www.youtube.com/watch?v=qInxenZVDJs
6+
7+
All endpoints require authentication, pass the string `userid:1` in the authorization header to login as user with id 1. Replace 1 with any number. Try making two request within 5 seconds to see the rate limiting middleware in action.
8+
9+
Both middleware are defined in the `encore.service.ts` file.
610

711
**Endpoints:**
812

@@ -12,7 +16,6 @@ All endpoints require authentication, pass the string `userid:1` in the authoriz
1216
- `/users/:id` (PUT) - Update user by id
1317
- `/users/:id` (DELETE) - delete user by id
1418

15-
1619
## Developing locally
1720

1821
### Prerequisite: Installing Encore
@@ -27,7 +30,7 @@ environment. Use the appropriate command for your system:
2730
When you have installed Encore, run:
2831

2932
```bash
30-
encore app create --example=ts/middleware-demo
33+
encore app create --example=ts/middleware
3134
```
3235

3336
## Running locally
@@ -44,6 +47,12 @@ You can also access Encore's [local developer dashboard](https://encore.dev/docs
4447

4548
## Deployment
4649

50+
### Self-hosting
51+
52+
See the [self-hosting instructions](https://encore.dev/docs/ts/self-host/build) for how to use `encore build docker` to create a Docker image and configure it.
53+
54+
### Encore Cloud Platform
55+
4756
Deploy your application to a staging environment in Encore's free development cloud:
4857

4958
```bash
@@ -57,10 +66,3 @@ Then head over to the [Cloud Dashboard](https://app.encore.dev) to monitor your
5766
From there you can also connect your own AWS or GCP account to use for deployment.
5867

5968
Now off you go into the clouds!
60-
61-
## Testing
62-
63-
```bash
64-
encore test
65-
```
66-
File renamed without changes.

ts/middleware-demo/package-lock.json renamed to ts/middleware/package-lock.json

Lines changed: 10 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ts/middleware-demo/package.json renamed to ts/middleware/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"vitest": "^1.5.0"
1515
},
1616
"dependencies": {
17-
"encore.dev": "^1.44.10"
17+
"encore.dev": "^1.44.10",
18+
"rate-limiter-flexible": "^5.0.4"
1819
},
1920
"optionalDependencies": {
2021
"@rollup/rollup-linux-x64-gnu": "^4.13.0"
File renamed without changes.

ts/middleware/user/encore.service.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { APICallMeta } from "encore.dev";
2+
import { APIError, middleware } from "encore.dev/api";
3+
import { Service } from "encore.dev/service";
4+
import { RateLimiterMemory } from "rate-limiter-flexible";
5+
import { getAuthData } from "~encore/auth";
6+
7+
const opts = {
8+
points: 1,
9+
duration: 5, // per second
10+
};
11+
12+
const rateLimiter = new RateLimiterMemory(opts);
13+
14+
// Rate limiting middleware.
15+
const rateLimiterMiddleware = middleware(
16+
// Should be applied to all endpoints that require authentication.
17+
{ target: { auth: true } },
18+
async (req, next) => {
19+
const userID = getAuthData()!.userID;
20+
21+
return rateLimiter
22+
.consume(userID)
23+
.then(async (rateLimiterRes) => {
24+
const res = await next(req);
25+
26+
res.header.set(
27+
"Retry-After",
28+
(rateLimiterRes.msBeforeNext / 1000).toString(),
29+
);
30+
res.header.set("X-RateLimit-Limit", opts.points.toString());
31+
res.header.set(
32+
"X-RateLimit-Remaining",
33+
rateLimiterRes.remainingPoints.toString(),
34+
);
35+
res.header.set(
36+
"X-RateLimit-Reset",
37+
new Date(Date.now() + rateLimiterRes.msBeforeNext).toISOString(),
38+
);
39+
40+
return res;
41+
})
42+
.catch((e) => {
43+
if (e instanceof APIError) throw e;
44+
throw APIError.resourceExhausted("Too Many Requests");
45+
});
46+
},
47+
);
48+
49+
// Authorization middleware that only allows users to make modifications on themselves.
50+
const permissionMiddleware = middleware(
51+
{ target: { auth: true } },
52+
async (req, next) => {
53+
const apiCallMeta = req.requestMeta as APICallMeta;
54+
55+
// check if this is a modifying request (PUT, POST, DELETE etc)
56+
if (apiCallMeta.method === "GET") {
57+
return await next(req);
58+
}
59+
60+
// check if this is an endpoint with a id in its path params (e.g /users/:id)
61+
if (apiCallMeta.pathParams.id === undefined) {
62+
return await next(req);
63+
}
64+
65+
const authedUser = getAuthData()!;
66+
67+
// check if the logged in user is the same as the user being modified.
68+
if (authedUser.userID != apiCallMeta.pathParams.id) {
69+
throw APIError.permissionDenied(
70+
`user ${authedUser.userID} is not permitted to modify user ${apiCallMeta.pathParams.id}`,
71+
);
72+
}
73+
74+
// run the handler
75+
return await next(req);
76+
},
77+
);
78+
79+
export default new Service("user", {
80+
middlewares: [rateLimiterMiddleware, permissionMiddleware],
81+
});

0 commit comments

Comments
 (0)