Skip to content

Commit 3edf699

Browse files
committed
feat(data): implement IP-based rate limiting for public routes
- Add IP-based rate limiting for unauthenticated users - Implement nullable User handling for consistent context - Update middleware execution order and comments
1 parent 14ba5d4 commit 3edf699

File tree

1 file changed

+43
-24
lines changed

1 file changed

+43
-24
lines changed

routes/api/v1/data/_middleware.dart

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,38 @@ import 'package:flutter_news_app_api_server_full_source_code/src/registry/model_
1111
// Helper middleware for applying rate limiting to the data routes.
1212
Middleware _dataRateLimiterMiddleware() {
1313
return (handler) {
14-
return (context) {
15-
final user = context.read<User>();
14+
return (context) async { // Made async because ipKeyExtractor is async
15+
final user = context.read<User?>(); // Read nullable User
1616
final permissionService = context.read<PermissionService>();
1717

1818
// Users with the bypass permission are not rate-limited.
19-
if (permissionService.hasPermission(
19+
if (user != null && permissionService.hasPermission(
2020
user,
2121
Permissions.rateLimitingBypass,
2222
)) {
2323
return handler(context);
2424
}
2525

26-
// For all other users, apply the configured rate limit.
27-
// The key is the user's ID, ensuring the limit is per-user.
26+
String? key;
27+
// If user is null, it means it's a public route (as per _conditionalAuthenticationMiddleware)
28+
// In this case, use IP-based rate limiting.
29+
if (user == null) {
30+
key = await ipKeyExtractor(context);
31+
} else {
32+
// Authenticated user: use user ID for rate limiting.
33+
key = user.id;
34+
}
35+
36+
// If a key cannot be extracted (e.g., no IP), bypass rate limiter.
37+
if (key == null || key.isEmpty) {
38+
return handler(context);
39+
}
40+
41+
// For all other users (or IPs for public routes), apply the configured rate limit.
2842
final rateLimitHandler = rateLimiter(
2943
limit: EnvironmentConfig.rateLimitDataApiLimit,
3044
window: EnvironmentConfig.rateLimitDataApiWindow,
31-
keyExtractor: (context) async => context.read<User>().id,
45+
keyExtractor: (context) async => key, // Use the determined key
3246
)(handler);
3347

3448
return rateLimitHandler(context);
@@ -120,7 +134,9 @@ Middleware _conditionalAuthenticationMiddleware() {
120134
return requireAuthentication()(handler)(context);
121135
} else {
122136
// If authentication is not required, simply pass the request through.
123-
return handler(context);
137+
// Also provide a null User to the context for consistency,
138+
// so downstream middleware can always read User?
139+
return handler(context.provide<User?>(() => null));
124140
}
125141
};
126142
};
@@ -134,28 +150,31 @@ Handler middleware(Handler handler) {
134150
// the last .use() call in the chain represents the outermost middleware layer.
135151
// Therefore, the execution order for an incoming request is:
136152
//
137-
// 1. `_conditionalAuthenticationMiddleware()`:
138-
// - This runs first. It dynamically decides whether to apply
153+
// 1. `_modelValidationAndProviderMiddleware()`:
154+
// - This runs first. It validates the `?model=` query parameter and
155+
// provides the `ModelConfig` and `modelName` into the context.
156+
// - If model validation fails, it throws a `BadRequestException`.
157+
//
158+
// 2. `_conditionalAuthenticationMiddleware()`:
159+
// - This runs second. It dynamically decides whether to apply
139160
// `requireAuthentication()` based on the `ModelConfig` for the
140161
// requested model and HTTP method.
141162
// - If authentication is required and the user is not authenticated,
142163
// it throws an `UnauthorizedException`.
164+
// - If authentication is NOT required, it provides `User?` as `null`
165+
// to the context.
143166
//
144-
// 2. `_dataRateLimiterMiddleware()`:
145-
// - This runs if authentication (if required) passes.
146-
// - It checks if the user has a bypass permission. If not, it applies
147-
// the configured rate limit based on the user's ID.
167+
// 3. `_dataRateLimiterMiddleware()`:
168+
// - This runs third. It checks if the user has a bypass permission.
169+
// If not, it applies the configured rate limit based on the user's ID
170+
// (if authenticated) or the client's IP address (if unauthenticated).
148171
// - If the limit is exceeded, it throws a `ForbiddenException`.
149172
//
150-
// 3. `_modelValidationAndProviderMiddleware()`:
151-
// - This runs if rate limiting passes.
152-
// - It validates the `?model=` query parameter and provides the
153-
// `ModelConfig` and `modelName` into the context.
154-
// - If model validation fails, it throws a `BadRequestException`.
155-
//
156173
// 4. `authorizationMiddleware()`:
157-
// - This runs if `_modelValidationAndProviderMiddleware()` passes.
158-
// - It reads the `User`, `modelName`, and `ModelConfig` from the context.
174+
// - This runs fourth. It reads the `User` (which is guaranteed to be
175+
// non-null if authentication was required and passed) or `User?` (if
176+
// authentication was not required), `modelName`, and `ModelConfig`
177+
// from the context.
159178
// - It checks if the user has permission to perform the requested HTTP
160179
// method on the specified model based on the `ModelConfig` metadata.
161180
// - If authorization fails, it throws a ForbiddenException, caught by
@@ -172,7 +191,7 @@ Handler middleware(Handler handler) {
172191
//
173192
return handler
174193
.use(authorizationMiddleware()) // Applied fourth (inner-most)
175-
.use(_modelValidationAndProviderMiddleware()) // Applied third
176-
.use(_dataRateLimiterMiddleware()) // Applied second
177-
.use(_conditionalAuthenticationMiddleware()); // Applied first (outermost)
194+
.use(_dataRateLimiterMiddleware()) // Applied third
195+
.use(_conditionalAuthenticationMiddleware()) // Applied second
196+
.use(_modelValidationAndProviderMiddleware()); // Applied first (outermost)
178197
}

0 commit comments

Comments
 (0)